import { system, world } from "@minecraft/server";
import { allDirections, flipDir, getADirection, offsetPos, offsetVec } from "./util/dirUtils";
import { lengthSquared, vecDiff, vecNormalize, vecSum, vecToString } from "./util/mthUtils";
import { isCreative } from "./util/miscUtils";
// can we get an API for this ffs
// high-tech stuff, don't distribute
// Author: MehVahdJukaar
const neighborUpdateListeners = [];
const pistonPushedListeners = [];
const blockFallenListeners = [];
const blockCreativeBroken = [];
export const suppEvents = {
    subscribeOnNeighborChanged(callback) {
        neighborUpdateListeners.push(callback);
    },
    subscribeOnPistonPushed(callback) {
        pistonPushedListeners.push(callback);
    },
    subscribeBeforePlayerFallenOnBlock(callback) {
        blockFallenListeners.push(callback);
    },
    subscribeOnBlockCreativeBroken(callback) {
        blockCreativeBroken.push(callback);
    },
    fireOnBlockBroken(target) {
        onBlockChanged(target, false);
    },
    fireOnBlockPlaced(above) {
        onBlockChanged(above, true);
    }
};
function onBlockChanged(center, updateSelf) {
    for (const dir of allDirections) {
        const neighbor = offsetPos(center, dir);
        if (neighbor) {
            const event = new NeighborUpdateEvent(neighbor, center, flipDir(dir));
            neighborUpdateListeners.forEach((listener) => {
                listener(event);
            });
            if (updateSelf) {
                const inverse = new NeighborUpdateEvent(center, neighbor, dir);
                neighborUpdateListeners.forEach((listener) => {
                    listener(inverse);
                });
            }
        }
    }
}
world.afterEvents.playerPlaceBlock.subscribe((event) => {
    onBlockChanged(event.block, false); //could also use this to update the block itself
});
world.afterEvents.playerBreakBlock.subscribe((event) => {
    if (isCreative(event.player)) {
        for (const listener of blockCreativeBroken) {
            listener(event);
        }
    }
    onBlockChanged(event.block, false);
});
world.afterEvents.blockExplode.subscribe((event) => {
    onBlockChanged(event.block, false);
});
world.afterEvents.pistonActivate.subscribe((event) => {
    const pistonBlock = event.block;
    const piston = event.piston;
    const pistonDir = getADirection(pistonBlock);
    const expanding = event.isExpanding;
    const oldPositions = piston.getAttachedBlocksLocations();
    system.runTimeout(() => {
        const dimension = pistonBlock.dimension;
        const newBlocks = [];
        for (let p of oldPositions) {
            const offsetPos = offsetVec(p, pistonDir, expanding ? 1 : -1);
            const offsetBlock = dimension.getBlock(offsetPos);
            if (offsetBlock && offsetBlock.isValid()) {
                newBlocks.push(offsetBlock);
            }
        }
        let afterEvent = new PistonPushedEvent(pistonBlock.dimension, newBlocks, pistonDir, expanding);
        pistonPushedListeners.forEach((listener) => {
            listener(afterEvent);
        });
    }, 2);
    //run block update immediately here
    //not optimal. it doesnt take int account that many direcions this will call will be just pistons. also the blocks fetched we aleady have many of
    for (const b of piston.getAttachedBlocks()) {
        onBlockChanged(b, false);
    }
    //on block change infront
    const pistonArm = offsetPos(pistonBlock, pistonDir);
    if (pistonArm) {
        onBlockChanged(pistonArm, false);
    }
});
//fires neighbor update from piston pushed...
suppEvents.subscribeOnPistonPushed((event) => {
    //This could be expensive... not my fault microsoft has no api for this
    let actualUpdateList = [];
    if (event.isExpanding) {
        actualUpdateList = event.attachedBlocks;
    }
    else {
        const map = new Map();
        //offset all in that dir
        for (const b of event.attachedBlocks) {
            map.set(vecToString(b.location), b);
        }
        for (const b of event.attachedBlocks) {
            const offset = offsetVec(b, event.pistonDir);
            const key = vecToString(offset);
            if (!map.has(key)) {
                map.set(key, b.dimension.getBlock(offset));
            }
        }
        actualUpdateList = Array.from(map.values());
    }
    for (const b of actualUpdateList) {
        //thees room for optimization here, we aleady fetched some blocks
        onBlockChanged(b, true);
    }
});
system.runInterval(() => {
    //IDK how performance intensive this would be. White super basic you never know with sh*trock. too bad
    for (const player of world.getAllPlayers()) {
        if (player.isFalling && !isCreative(player)) {
            const vel = player.getVelocity();
            const [speed, dir] = vecNormalize(vel);
            //raytrace next hit block
            const hit = player.dimension.getBlockFromRay(player.location, dir, {
                includePassableBlocks: false,
                maxDistance: Math.max(1, speed * 2) //needs to be at least 1 and we also need to shoot in the future due to when this is called. we later validate the hit result
            });
            //console.log("falling" + speed + " || " + vecToString(dir) + " ");
            if (hit) {
                const hitBlock = hit.block;
                const distance = vecDiff(vecSum(hit.faceLocation, hit.block.location), player.location);
                //console.log("distance: ", vecToString(distance));
                if (lengthSquared(distance) <= (speed * speed)) {
                    //console.log("Fallen block: ", hitBlock.typeId);
                    const event = new BlockFallenBeforeEvent(hitBlock, player, vel, speed);
                    blockFallenListeners.forEach((listener) => {
                        listener(event);
                    });
                }
            }
        }
    }
}, 1);
export class BlockFallenBeforeEvent {
    constructor(block, player, playerVelocity, playerSpeed) {
        this.block = block;
        this.player = player;
        this.playerVelocity = playerVelocity;
        this.playerSpeed = playerSpeed;
    }
}
export class BlockCreativeBrokenEvent {
    constructor(block, brokenPerm, player) {
        this.block = block;
        this.brokenPerm = brokenPerm;
        this.player = player;
    }
}
export class NeighborUpdateEvent {
    constructor(block, neighborBlock, fromDir) {
        this.block = block;
        this.neighborBlock = neighborBlock;
        this.fromDir = fromDir;
    }
}
export class PistonPushedEvent {
    constructor(dimension, attachedBlocks, pistonDir, isExpanding) {
        this.dimension = dimension;
        this.attachedBlocks = attachedBlocks;
        this.pistonDir = pistonDir;
        this.isExpanding = isExpanding;
    }
}
