import * as mc from "@minecraft/server";
import { SafetyCheckedEntity, despawnOnReload, spawnEntitySafe } from "./entity";
import { waitForEvent, waitForTicks } from "./events";
import { getRandomInteger } from "./math";
import { Vector3 } from "./vector3";
import { IGNORED_ENTITIES } from "./melee_system";
/**
 * @remarks A class that handles projectile logic.
 */
export class Projectile {
    /**
     * @remarks The projectile entity.
     */
    get Entity() {
        return this.entity;
    }
    /**
     * @remarks The entity that spawned this projectile. May be undefined.
     */
    get Owner() {
        return this.owner;
    }
    /**
     * @remarks The speed of the projectile based on its power and inertia.
     */
    get Speed() {
        return this.power * Math.pow(this.inertia, this.age);
    }
    /**
     * @remarks The location of the projectile.
     */
    get Location() {
        return this.Entity.location;
    }
    /**
     * @remarks The direction in which the projectile is traveling.
     */
    get Direction() {
        return this.direction;
    }
    /**
     * @remarks The direction in which the projectile is traveling.
     */
    set Direction(v) {
        this.direction = v;
    }
    /**
     * @remarks The age of the projectile in ticks.
     */
    get Age() {
        return this.age;
    }
    /**
     * @remarks Whether the projectile is stuck in the ground.
     */
    get StuckInGround() {
        return this.stuck_in_ground;
    }
    /**
     * @remarks Shoots an entity as projectile using reasonable defaults.
     * @param location The location to spawn the projectile at.
     * @param dimension The dimension in which to spawn the projectile.
     * @param entityId The type id of the projectile entity to create.
     * @param direction The direction in which the projectile should travel.
     * @param owner An optional parameter for the entity who owns this projectile.
     * @param options Additional options as {@link IProjectileOptions}.
     * @example
     * ### Flaming Projectile Example
     * ```typescript
     * new Projectile(player.getHeadLocation(), player.dimension, "ldz:custom_projectile", player.getViewDirection(), player, {
     *     on_hit: {knockback: true, multiple_targets: true, hit_callback(projectile, hitBlock, hitEntities) {
     *         hitEntities?.forEach(entity => entity.setOnFire(10));
     *     },}
     * });
     * ```
     * ### Stick In Ground Projectile
     * #### Entity Property
     * ```json
     * {
     *     "format_version": "1.20.50",
     *     "minecraft:entity": {
     *         "description": {
     *             //...
     *             "properties": {
     *                 "ldz:in_ground": {
     *                     "client_sync": true,
     *                     "default": false,
     *                     "type": "bool"
     *                 }
     *             }
     *         },
     *         //...
     *     }
     * }
     * ```
     * #### Client Entity
     * ```json
     * {
     *     "format_version": "1.20.50",
     *     "minecraft:client_entity": {
     *         "description": {
     *             //...
     *             "animations": {
     *                 "look": "animation.projectiles.look"
     *             },
     *             "scripts": {
     *                 "initialize": [
     *                     "v.x = query.target_x_rotation;"
     *                 ],
     *                 "pre_animation": [
     *                     "v.x = !query.property('ldz_240329:in_ground') ? query.target_x_rotation : v.x;"
     *                 ],
     *                 "animate": [
     *                     "look"
     *                 ]
     *             }
     *         }
     *     }
     * }
     * ```
     * #### Projectile Animation
     * ```json
     * {
     *     "format_version": "1.8.0",
     *     "animations": {
     *         "animation.projectiles.look": {
     *             "loop": true,
     *             "bones": {
     *                 "root": {
     *                     "rotation": ["v.x", "this", 0]
     *                 }
     *             }
     *         }
     *     }
     * }
     * ```
     * #### Projectile
     * ```typescript
     * new Projectile(player.getHeadLocation(), player.dimension, "ldz_240329:ivory_javelin_entity", player.getViewDirection(), player, {
     *     stick_in_ground: true,
     *     on_hit: {hit_callback(projectile, hitBlock, hitEntities) {
     *         if (hitBlock) {
     *             projectile.Entity.setProperty("in_ground", true);
     *         }
     *     }}
     * });
     * ```
     */
    constructor(location, dimension, entityId, direction, owner, options) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
        this.age = 0;
        this.stuck_in_ground = false;
        this.hit_id_list = [];
        this.faceDirection = true;
        this.gravity = 0.01;
        this.inertia = 0.99;
        this.power = 5.0;
        this.offset = new Vector3(0);
        this.shoot_sound = "";
        this.hit_sound = "random.bowhit";
        this.stick_in_ground = false;
        this.lifetime_seconds = 5;
        this.on_hit = { damage: 1, knockback: false, filters: {} };
        this.faceDirection = (_a = options === null || options === void 0 ? void 0 : options.faceDirection) !== null && _a !== void 0 ? _a : this.faceDirection;
        this.gravity = (_b = options === null || options === void 0 ? void 0 : options.gravity) !== null && _b !== void 0 ? _b : this.gravity;
        this.inertia = (_c = options === null || options === void 0 ? void 0 : options.inertia) !== null && _c !== void 0 ? _c : this.inertia;
        this.power = (_d = options === null || options === void 0 ? void 0 : options.power) !== null && _d !== void 0 ? _d : this.power;
        this.offset = (_e = options === null || options === void 0 ? void 0 : options.offset) !== null && _e !== void 0 ? _e : this.offset;
        this.shoot_sound = (_f = options === null || options === void 0 ? void 0 : options.shoot_sound) !== null && _f !== void 0 ? _f : this.shoot_sound;
        this.hit_sound = (_g = options === null || options === void 0 ? void 0 : options.hit_sound) !== null && _g !== void 0 ? _g : this.hit_sound;
        this.stick_in_ground = (_h = options === null || options === void 0 ? void 0 : options.stick_in_ground) !== null && _h !== void 0 ? _h : this.stick_in_ground;
        this.lifetime_seconds = (_j = options === null || options === void 0 ? void 0 : options.lifetime_seconds) !== null && _j !== void 0 ? _j : this.lifetime_seconds;
        this.on_hit = Object.assign(Object.assign({}, this.on_hit), options === null || options === void 0 ? void 0 : options.on_hit);
        this.on_hit.filters.excludeTypes = (_m = (_l = (_k = options === null || options === void 0 ? void 0 : options.on_hit) === null || _k === void 0 ? void 0 : _k.filters) === null || _l === void 0 ? void 0 : _l.excludeTypes) !== null && _m !== void 0 ? _m : [];
        (_p = (_o = this.on_hit.filters) === null || _o === void 0 ? void 0 : _o.excludeTypes) === null || _p === void 0 ? void 0 : _p.push(...IGNORED_ENTITIES);
        this.location = location.add(this.offset);
        this.direction = direction;
        this.owner = owner;
        if (this.faceDirection) {
            this.entity = spawnEntitySafe(dimension, entityId, this.location, { facing: this.location.add(this.direction) });
        }
        else {
            this.entity = spawnEntitySafe(dimension, entityId, this.location);
        }
        this.hit_id_list = [this.entity.id, (_r = (_q = this.owner) === null || _q === void 0 ? void 0 : _q.id) !== null && _r !== void 0 ? _r : ""];
        if (!this.entity || !this.entity.isValid())
            throw new Error(`Failed to summon ${entityId} at ${location}. Cannot create projectile.`);
        despawnOnReload(this.Entity);
        const player = (_s = this.owner) === null || _s === void 0 ? void 0 : _s.getPlayer();
        if (player) {
            (_t = this.Entity.getComponent("tameable")) === null || _t === void 0 ? void 0 : _t.tame(player.UnsafeEntity);
        }
        waitForEvent(1, () => this.shoot());
        waitForEvent(20 * this.lifetime_seconds, () => {
            if (this.Entity.isValid()) {
                this.Entity.remove();
            }
        });
    }
    async shoot() {
        mc.world.playSound(this.shoot_sound, this.location);
        while (this.Entity.isValid() && !this.stuck_in_ground) {
            try {
                this.hitChecks();
            }
            catch (error) {
                break;
            }
            if (!this.Entity.isValid() || this.stuck_in_ground)
                break;
            this.Entity.teleport(this.Entity.location.add(this.direction.multiply(this.Speed)));
            this.age++;
            this.direction.y -= this.gravity;
            this.direction = this.direction.normalize();
            this.updateRotation();
            await waitForTicks(0);
        }
        while (this.Entity.isValid()) {
            this.updateRotation();
            await waitForTicks(0);
        }
    }
    hitChecks() {
        let hitEntities = this.getHitEntities();
        let hitBlock = this.Entity.dimension.getBlockFromRay(this.Entity.location, this.direction, { includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: Math.max(this.Speed + this.power, 1) });
        if (!hitBlock && !hitEntities.length)
            return;
        mc.world.playSound(this.hit_sound, this.Entity.location);
        if (!this.on_hit.multiple_targets && hitEntities.length > 0) {
            hitEntities.length = 1;
            const { x, z } = hitEntities[0].location;
            this.Entity.teleport({ x, y: this.Entity.location.y, z });
            this.stuck_in_ground = true;
            if (this.on_hit.destroy_on_hit) {
                waitForEvent(10, () => this.Entity.remove());
            }
        }
        ;
        if (hitBlock && (this.stick_in_ground || this.on_hit.destroy_on_hit)) {
            this.Entity.teleport(new Vector3(hitBlock.block.location).add(hitBlock.faceLocation));
            this.stuck_in_ground = true;
            if (this.on_hit.destroy_on_hit) {
                waitForEvent(6, () => this.Entity.remove());
            }
        }
        this.applyHit(hitEntities, hitBlock);
    }
    getHitEntities() {
        let hits = this.Entity.dimension.getEntitiesFromRay(this.Entity.location, this.direction, { maxDistance: Math.max(this.Speed + this.power, 1) });
        if (hits.length) {
            hits = hits.filter(hit => !this.hit_id_list.includes(hit.entity.id));
            this.hit_id_list.push(...hits.map(entity => entity.entity.id));
        }
        if (hits.length && this.on_hit.filters) {
            hits = hits.filter(hit => hit.entity.matches(this.on_hit.filters));
        }
        return hits.map(hit => new SafetyCheckedEntity(hit.entity));
    }
    applyHit(hitEntities, hitBlock) {
        const damage = Array.isArray(this.on_hit.damage) ? getRandomInteger(this.on_hit.damage[0], this.on_hit.damage[1]) : Number(this.on_hit.damage);
        hitEntities.forEach(entity => {
            var _a;
            const damagingEntity = this.on_hit.damageSource === "projectile" ? this.Entity.UnsafeEntity : (_a = this.Owner) === null || _a === void 0 ? void 0 : _a.UnsafeEntity;
            if (this.on_hit.canHurtDragon && entity.typeId === "minecraft:ender_dragon") {
                entity.applyDamage(damage, { cause: mc.EntityDamageCause.blockExplosion, damagingEntity });
            }
            else if (this.on_hit.damageSource === "projectile") {
                entity.runCommand(`damage @s ${damage} projectile entity @e[type=${this.Entity.typeId},c=1]`);
            }
            else {
                entity.applyDamage(damage, { cause: mc.EntityDamageCause.entityAttack, damagingEntity });
            }
            if (!this.on_hit.knockback) {
                entity.clearVelocity();
            }
        });
        if (this.on_hit.hit_callback)
            this.on_hit.hit_callback(this, hitEntities, hitBlock);
    }
    updateRotation() {
        const pitch = Math.asin(-this.direction.y) * (180 / Math.PI);
        const yaw = this.Entity.getRotation().y;
        this.Entity.setRotation({ x: pitch, y: yaw });
    }
}
