import {
    Container,
    Dimension,
    Entity,
    EntityEquippableComponent,
    EntityInventoryComponent,
    EntityProjectileComponent,
    EntityRideableComponent,
    ItemStack,
    system,
    world,
} from "@minecraft/server";

/** @type {Map<string, RuntimeItem>} */
const RUNNING_ENTITIES = {};

const CREATORS = {};

const LOG_TAG = "EntityRuntime";

function gc() {
    const deletes = [];

    Object.keys(RUNNING_ENTITIES).forEach((id) => {
        if (Date.now() - RUNNING_ENTITIES[id]["time"] >= 2000) {
            deletes.push(id);
        }
    });

    for (let key of deletes) {
        delete RUNNING_ENTITIES[key];
    }
}

class RuntimeItem {
    time = 0;
    /** @type {EntityObject} */
    data = null;
    /** @type {Entity} */
    entity = null;
}

export class EntityObjectComponent {
    get data() {
        return this.getObject().data;
    }

    /** @returns {EntityObject} */
    getObject() {
        return this.__object;
    }

    /** @returns  {Entity}*/
    getEntity() {
        return this.getObject()?.getEntity();
    }

    onTick() {}
    onUpdate() {}

    dropComponent() {
        const obj = this.getObject();

        if (Boolean(obj)) {
            obj.removeComponent(this);
        }
    }
}

export class EntityObject {
    updateInterval = 20;
    data = {};
    #components = [];

    constructor() {
        const self = this;

        const rawData = {};

        this.data = new Proxy(
            {},
            {
                get(target, prop, receiver) {
                    return rawData[prop] ?? self.getEntity().getDynamicProperty(prop);
                },

                set(target, prop, value, receiver) {
                    try {
                        rawData[prop] = value;

                        self.getEntity()?.setDynamicProperty(prop, value);
                    } catch (error) {}

                    return true;
                },

                has(target, prop) {
                    return self.getEntity().getDynamicPropertyIds().includes(prop);
                },
            }
        );
    }

    onTick() {}
    onUpdate() {}

    /** @returns {Array<Entity>} */
    getRiders() {
        try {
            /** @type {EntityRideableComponent} */
            const riderable = this.getEntity().getComponent(EntityRideableComponent.componentId);

            return riderable.getRiders() ?? [];
        } catch (e) {
            return [];
        }
    }

    triggerEntityEvent(name) {
        try {
            this.getEntity().triggerEvent(name);
        } catch (x) {
            // log("RUNTIME", "Error on trigger event", name, " on ", this.getEntity()?.typeId);
        }
    }

    /** @returns {Container | undefined} */
    getInventory() {
        return this.getEntity()?.getComponent(EntityInventoryComponent.componentId)?.container;
    }

    /** @returns {ItemStack | undefined} */
    getEquipament(slot) {
        return this.getEntity()
            ?.getComponent(EntityEquippableComponent.componentId)
            ?.getEquipmentSlot(slot);
    }

    getComponents() {
        return this.#components;
    }

    addComponent(component) {
        component.__object = this;

        this.#components.push(component);
    }

    removeComponent(component) {
        if (component.__object == this) {
            component.__object = undefined;
        }

        this.#components = this.#components.filter((comp) => comp != component);
    }

    clearComponents() {
        this.#components = [];
    }

    hasRiders() {
        return this.getRiders().length > 0;
    }

    isSneaking() {
        return this.getEntity().isSneaking;
    }

    /** @returns {Entity} */
    getEntity() {
        return this.__item.entity;
    }

    getActiveTime() {
        return this.getEntity().getDynamicProperty("__lifetime__") ?? 0;
    }

    setProperty(name, value) {
        try {
            this.getEntity().setProperty(name, value);
        } catch (x) {}
    }

    getProperty(name) {
        try {
            return this.getEntity().getProperty(name);
        } catch (e) {
            return undefined;
        }
    }
}

export class EntityRuntime {
    static register(id, clazz) {
        CREATORS[id] = clazz;
    }

    static remove(id) {
        delete CREATORS[id];
    }

    /**
     * @param {Entity} entity
     * @param {string} name
     * @param {*} args
     */
    static invokeEvent(entity, name, ...args) {
        if (entity && entity.isValid() && entity.typeId in CREATORS) {
            executeSafeEvent(entity, name, ...args);
        }
    }

    /** @returns {EntityObject} */
    static getObjectFor(entity) {
        if (entity && entity.isValid()) {
            return RUNNING_ENTITIES[entity.id];
        } else {
            return undefined;
        }
    }
}

/**
 * @param {Entity} entity
 * @param {string} name
 * @param  {...any} args
 */
function executeEvent(entity, name, ...args) {
    try {
        if (!(entity.id in RUNNING_ENTITIES)) {
            const item = new RuntimeItem();

            item.time = Date.now();

            item.entity = entity;

            item.data = new CREATORS[entity.typeId]();

            item.data.__item = item;

            RUNNING_ENTITIES[entity.id] = item;

            if (!entity.getDynamicProperty("[core::initialized]")) {
                entity.setDynamicProperty("[core::initialized]", true);

                executeSafeEvent(entity, "begin");
            }

            executeSafeEvent(entity, "create");
        }

        /** @type {RuntimeItem} */
        const data = RUNNING_ENTITIES[entity.id];

        data.entity = entity;

        data.time = Date.now();

        const entityObject = data.data;

        if (name in entityObject) {
            try {
                entityObject[name](...args);
            } catch (error) {
                // log(LOG_TAG, "Error on call entity function ", name, error);
            }
        }

        for (let component of entityObject.getComponents()) {
            component.__object = entityObject;

            if (name in component) {
                try {
                    component[name](...args);
                } catch (e) {
                    // log(LOG_TAG, "Error on call entity function on component", name, e);
                }
            }
        }
    } catch (e) {
        //  log(LOG_TAG, "Error: ", e);
    }
}

/** @param {string} name  */
function parseEventName(name) {
    name = name + "";

    if (name.includes(":")) {
        name = name.split(":")[1];
    }

    name = name.toLowerCase();

    const allowed = "abcdefghijklmnopqrstuvwxyz";

    let formatted = "";

    for (let i = 0; i < name.length; i++) {
        const ch = name.charAt(i);

        if (allowed.includes(ch)) {
            formatted += ch;
        } else if (!formatted.endsWith(".")) {
            formatted += ".";
        }
    }

    let finalName = "";

    formatted.split(".").forEach((segment) => {
        if (segment.length > 1) {
            finalName += segment.charAt(0).toUpperCase() + segment.substring(1).toLowerCase();
        }
    });

    return finalName;
}

/**
 * @param {Entity} entiy
 * @param {string} rawName
 */
function executeSafeEvent(entiy, rawName, ...args) {
    const name = "on" + parseEventName(rawName);

    if (entiy && entiy.isValid() && entiy.typeId in CREATORS) {
        executeEvent(entiy, name, ...args);
    }
}

system.runInterval(() => {
    let activeDimensions = {};

    world
        .getAllPlayers()
        .forEach((player) => (activeDimensions[player.dimension.id] = player.dimension));

    /** @type {Array<Entity>} */
    let entities = [];

    for (let dimen of Object.values(activeDimensions)) {
        /** @type {Dimension} */
        const dimension = dimen;

        dimension.getEntities().forEach((e) => {
            if (e && e.isValid() && e.typeId in CREATORS) {
                entities.push(e);
            }
        });
    }

    for (let entity of entities) {
        try {
            const ticks = (entity.getDynamicProperty("__lifetime__") ?? -1) + 1;

            entity.setDynamicProperty("__lifetime__", ticks);

            executeEvent(entity, "onTick");

            const obj = RUNNING_ENTITIES[entity.id].data;

            if (ticks % obj.updateInterval == 0) {
                executeEvent(entity, "onUpdate");
            }
        } catch (e) {
            // log(LOG_TAG, "Error: ", entity?.typeId, e);
        }
    }

    entities = [];

    gc();
});

world.afterEvents.entityHitEntity.subscribe((e) => {
    executeSafeEvent(e.damagingEntity, "hit", e.hitEntity);

    executeSafeEvent(e.hitEntity, "damage", e.damagingEntity);
});

system.afterEvents.scriptEventReceive.subscribe((e) => {
    if (e.sourceEntity && e.sourceEntity.isValid() && e.sourceEntity.typeId in CREATORS) {
        try {
            let msg = e.message;

            while (msg.includes("  ")) msg = msg.replace("  ", " ");

            msg = msg.split(" ");

            let args = [];

            for (let msgPart of msg) {
                if (msgPart.replace(".", "").match(/^\d*$/)) {
                    msg = parseFloat(msgPart);
                } else if (msgPart == "true" || msgPart == "false") {
                    args.push(msgPart == "true");
                } else {
                    args.push(msgPart);
                }
            }

            executeSafeEvent(e.sourceEntity, e.id, ...args);
        } catch (e) {
            //  log(LOG_TAG, "Error on process script event", e);
        }
    }
});

world.afterEvents.entityHealthChanged.subscribe((e) => {
    executeSafeEvent(e.entity, "health_changed", e.newValue, e.oldValue);
});

world.afterEvents.entityLoad.subscribe((e) => executeSafeEvent(e.entity, "load"));

world.beforeEvents.entityRemove.subscribe((e) => executeSafeEvent(e.removedEntity, "remove"));

world.afterEvents.itemUse.subscribe((e) => executeSafeEvent(e.source, "item_use", e.itemStack));

world.afterEvents.dataDrivenEntityTrigger.subscribe((e) => executeSafeEvent(e.entity, e.eventId));

world.afterEvents.entitySpawn.subscribe((e) => {
    executeSafeEvent(e.entity, "spawn");

    if (e?.entity?.hasComponent(EntityProjectileComponent.componentId)) {
        /** @type {EntityProjectileComponent} */ const component = e.entity.getComponent(
            EntityProjectileComponent.componentId
        );

        if (component.owner) {
            executeSafeEvent(component.owner, "throw_projectile", e.entity);
        }
    }
});

world.afterEvents.projectileHitEntity.subscribe((e) => {
    executeSafeEvent(e.projectile, "hit", e.getEntityHit().entity);

    executeSafeEvent(e.projectile, "projectile_hit", e.location);
});

world.afterEvents.projectileHitBlock.subscribe((e) => {
    executeSafeEvent(e.projectile, "hit_block", e.getBlockHit().block);

    executeSafeEvent(e.projectile, "projectile_hit", e.location);
});
