import { EntityComponentTypes, system, world } from "@minecraft/server";
import { suppEvents } from "./suppEventHandler";
import { EntityTracker } from "./util/entityTracker";
import { McBlocks } from "./reg/mcBlocks";
import { isMovingBlock } from "./util/blockProperties";
import { dirToYaw, flipDir, getADirection, offsetPos } from "./util/dirUtils";
// High-tech block entity code. Don't distribute!
// Author: MehVahdJukaar
class BlockEntityManager extends EntityTracker {
    constructor() {
        super();
        this.entityToBlocks = new Map();
        this.blockToEntity = new Map();
        this.entityToDefinitions = new Map();
        this.knownProperties = new Set();
        this.setupEvents();
    }
    setupEvents() {
        //not needed anymore, all done with entity components. Will be an issue if entity has more than inventory data...
        world.afterEvents.entityDie.subscribe((event) => {
            const entity = event.deadEntity;
            const block = this.entityToBlocks.get(entity.typeId);
            //const inv = entity.getComponent(EntityComponentTypes.Inventory) as EntityInventoryComponent;
            //if (inv && inv.container && entity.typeId == SuppEntities.Present) {
            //printAllInventory(inv.container);
            //}
            if (block) {
                //idk why its needed... as the entity also assigns this event itself
                // entity.triggerEvent("razz_sup:cancel_death_animation")
                /*
                if (hasMatchingBlock(entity)) {
                    //check if theres correct block at
                    //respawn entity, copying the old one
                    cloneEntity(entity);
                }
                entity.remove();
                */
            }
        });
        world.afterEvents.blockExplode.subscribe((event) => {
            const block = event.block;
            this.removeBlockEntityOnBlockBroken(block.permutation, block, block.typeId);
        });
        world.afterEvents.playerPlaceBlock.subscribe((event) => {
            if (this.isEntityBlock(event.block)) {
                const beType = this.blockToEntity.get(event.block.typeId);
                if (beType) {
                    const def = this.entityToDefinitions.get(beType);
                    if (!def || !def.isOptional?.()) {
                        this.getOrCreateBlockEntity(event.block);
                    }
                }
            }
        });
        suppEvents.subscribeOnPistonPushed((event) => {
            if (event.isExpanding)
                return;
            for (const b of event.attachedBlocks) {
                const entity = this.blockToEntity.get(b.typeId);
                if (entity) {
                    const behindPos = offsetPos(b, flipDir(event.pistonDir));
                    const e = this.getExistingBlockEntityAt(behindPos, entity); //dont need create as onPlace will be called when moved
                    if (e) {
                        //move the entity back to the target position
                        e.teleport(b.center());
                        //console.log("Pulled pack block entity: " + e.typeId + " to " + b.center());
                    }
                    else {
                        console.error("!!!No entity found for block: " + b.typeId);
                    }
                }
            }
        });
    }
    //why is this uglyness needed? because for some reason we cant iterate over existing properties...
    registerBEProperty(prop) {
        this.knownProperties.add(prop);
    }
    registerEntityBlock(entity, block, definition) {
        const blockArray = Array.isArray(block) ? block : [block];
        for (const b of blockArray) {
            this.blockToEntity.set(b, entity);
        }
        const blocks = this.entityToBlocks.get(entity);
        if (definition) {
            this.entityToDefinitions.set(entity, definition);
        }
        if (blocks) {
            if (!blocks.includes(blockArray[0])) {
                blocks.push(...blockArray);
            }
        }
        else {
            this.entityToBlocks.set(entity, blockArray);
        }
    }
    syncBlockEntityRotation(block) {
        const be = blockEntityManager.getExistingBlockEntityAt(block);
        if (be) {
            this.syncRotation(be, block);
        }
    }
    isEntityBlock(block) {
        return this.blockToEntity.has(block.typeId);
    }
    moveBlockEntity(block, targetPos) {
        const e = blockEntityManager.getExistingBlockEntityAt(block);
        if (e) {
            //move the entity to the target position
            e.teleport(targetPos);
            //console.log("Moving entity: " + e.typeId + " to " + targetPos);
        }
    }
    //internal
    removeBlockEntityOnBlockBroken(perm, pos, blockId) {
        const be = blockEntityManager.getExistingBlockEntityAt(pos, blockId);
        if (be) {
            const def = this.entityToDefinitions.get(be.typeId);
            if (def && def.onBroken) {
                def.onBroken(perm, pos.center(), be);
            }
            be.remove();
            this.unTrack(be.id);
        }
    }
    cloneEntity(entity) {
        const dim = entity.dimension;
        const newEntity = dim.spawnEntity(entity.typeId, entity.location);
        const newC = newEntity.getComponent(EntityComponentTypes.Inventory);
        const c = entity.getComponent(EntityComponentTypes.Inventory);
        if (newC && newC.container && c && c.container) {
            const size = c.container.size;
            for (let i = 0; i < size; i++) {
                c.container.moveItem(i, i, newC.container);
            }
            c.container.clearAll();
        }
    }
    getOrCreateBlockEntity(block) {
        let be = this.getExistingBlockEntityAt(block);
        if (!be) {
            const identifier = this.blockToEntity.get(block.typeId);
            if (identifier) {
                be = block.dimension.spawnEntity(identifier, block.center());
                this.syncRotation(be, block);
                this.entityToDefinitions.get(identifier)?.onCreated?.(be);
            }
        }
        return be;
    }
    getExistingBlockEntityAt(block, blockId) {
        const entityId = this.blockToEntity.get(blockId ?? block.typeId);
        if (entityId) {
            const opt = {
                location: block.center(),
                maxDistance: 0.5,
                type: entityId,
                closest: 1
            };
            const entities = block.dimension.getEntities(opt);
            if (entities.length == 0)
                return undefined;
            return entities[0];
        }
        return undefined;
    }
    syncRotation(be, block) {
        const dir = getADirection(block);
        if (dir) {
            const yaw = dirToYaw(dir);
            be.setRotation({ x: 0, y: 180 - yaw });
        }
    }
    shouldTrack(entity) {
        return this.entityToBlocks.has(entity.typeId);
    }
    onTick(entity) {
        //if (system.currentTick % 100 == 0) console.log("Checking entity: " + entity.typeId);
        //first adjust the entity position if its not at a block position
        const position = entity.location;
        if (Math.abs(position.z % 1) != 0.5 || Math.abs(position.x % 1) != 0.5 || Math.abs(position.y % 1) != 0.5) {
            if (entity.dimension.getBlock(entity.location).typeId != McBlocks.MovingBlock) {
                //snap position back to grid
                const newPos = {
                    x: Math.floor(position.x) + 0.5,
                    y: Math.floor(position.y) + 0.5,
                    z: Math.floor(position.z) + 0.5
                };
                entity.teleport(newPos);
                //console.log("Snapped block entity to grid: " + entity.typeId);
            }
        }
        const entityId = entity.id;
        if (!this.hasMatchingBlock(entity)) {
            //1 tick grace period. Might be needed as this could ruh right before the piston callback code is called
            system.run(() => {
                const ee = world.getEntity(entityId);
                if (ee && !this.hasMatchingBlock(ee)) {
                    // Remove the entity if the block is not present
                    //we got to decide who is master and who is slave. probably block is the master but for inventories it wouldnt work that well
                    //manually drop stuff or remove kill anim
                    if (ee) {
                        entity.remove();
                        this.unTrack(entityId);
                    }
                }
            });
        }
    }
    hasMatchingBlock(entity) {
        const blockId = this.entityToBlocks.get(entity.typeId);
        if (blockId) {
            // Check if the block is still present
            const blockAtEntity = entity.dimension.getBlock(entity.location);
            if (!blockAtEntity)
                return true; //unloaded chunk
            if (!blockAtEntity.isValid() || (!blockId.includes(blockAtEntity.typeId) && !isMovingBlock(blockAtEntity))) {
                //console.log("Entity: " + entity.typeId + " has no matching block: " + blockId + " was: " + blockAtEntity.typeId);
                return false;
            }
        }
        return true;
    }
    //refresh tile while keeping dynamic prop of the old one
    transmuteBlockEntity(block, perm) {
        const existing = this.getExistingBlockEntityAt(block);
        const propMap = new Map();
        const dynPropMap = new Map();
        if (existing) {
            for (const prop of this.knownProperties) {
                const value = existing.getProperty(prop);
                if (value != undefined) {
                    propMap.set(prop, value);
                }
            }
            for (const prop of existing.getDynamicPropertyIds()) {
                const value = existing.getDynamicProperty(prop);
                if (value != undefined) {
                    dynPropMap.set(prop, value);
                }
            }
        }
        //set block
        block.setPermutation(perm);
        const newBE = this.getOrCreateBlockEntity(block);
        if (newBE && newBE !== existing) {
            for (const [prop, value] of propMap) {
                newBE.setProperty(prop, value);
            }
            for (const [prop, value] of dynPropMap) {
                newBE.setDynamicProperty(prop, value);
            }
        }
    }
}
export const blockEntityManager = new BlockEntityManager();
export class BaseBlockEntityComponent {
    //also happens when this block is pushed by a piston... we could use this
    onPlace(event) {
        const block = event.block;
        system.runTimeout(() => {
            if (block && block.isValid()) {
                blockEntityManager.syncBlockEntityRotation(block);
            }
        }, 1);
    }
    onPlayerDestroy(event) {
        //remove entity at position
        let perm = event.destroyedBlockPermutation;
        const blockId = perm.type.id;
        blockEntityManager.removeBlockEntityOnBlockBroken(perm, event.block, blockId);
    }
}
export const BlockEntityComponent = new BaseBlockEntityComponent();
