import * as mc from "@minecraft/server";
import { SafetyCheckedEntity, despawnOnReload, getEntitiesSafe } from "./entity";
import { waitForEvent, waitForTicks } from "./events";
import { getRandomInteger } from "./math";
import { GlobalNamespace } from "./namespace";
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;
    }
    /**
     * @remakrs The entity that spawned this projectile. May be undefined.
     */
    get Owner() {
        return this.owner;
    }
    get Speed() {
        return this.power * Math.pow(this.inertia, this.age);
    }
    /**
     * @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;
        this.age = 0;
        this.stuck_in_ground = false;
        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) {
            dimension.runCommand(`summon ${entityId} ${this.location} facing ${this.location.add(this.direction)}`);
        }
        else {
            dimension.runCommand(`summon ${entityId} ${this.location}`);
        }
        waitForEvent(0, () => {
            this.entity = getEntitiesSafe(dimension, { type: entityId, location, excludeTags: [GlobalNamespace.Namespace + "spawned"] })[0];
            if (!this.entity || !this.entity.isValid())
                throw new Error(`Failed to summon ${entityId} at ${location}. Cannot create projectile.`);
            despawnOnReload(this.Entity);
            this.Entity.addTag("spawned");
            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.Entity.dimension.getEntitiesFromRay(this.Entity.location, this.direction, { maxDistance: Math.max(this.Speed + this.power, 1) }).map(hit => new SafetyCheckedEntity(hit.entity));
        hitEntities = hitEntities.filter(entity => entity.id !== this.owner.id && entity.id !== this.Entity.id);
        if (this.on_hit.filters)
            hitEntities = hitEntities.filter(entity => entity.matches(this.on_hit.filters));
        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.Entity.teleport(new Vector3(hitBlock.block.location).add(hitBlock.faceLocation));
            this.stuck_in_ground = true;
        }
        else if (hitBlock && this.on_hit.destroy_on_hit) {
            this.Entity.teleport(new Vector3(hitBlock.block.location).add(hitBlock.faceLocation));
            this.stuck_in_ground = true;
            waitForEvent(6, () => this.Entity.remove());
        }
        const damage = Array.isArray(this.on_hit.damage) ? getRandomInteger(this.on_hit.damage[0], this.on_hit.damage[1]) : Number(this.on_hit.damage);
        if (this.on_hit.knockback)
            hitEntities.forEach(entity => { var _a; return entity.applyDamage(damage, { cause: mc.EntityDamageCause.entityAttack, damagingProjectile: this.Entity.UnsafeEntity, damagingEntity: (_a = this.owner) === null || _a === void 0 ? void 0 : _a.UnsafeEntity }); });
        else
            hitEntities.forEach(entity => entity.applyDamage(damage));
        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 });
    }
}
