import { BlockComponentTypes, BlockPermutation, BlockTypes, Direction, EntityComponentTypes, EquipmentSlot, GameMode, InputPermissionCategory, ItemComponentTypes, ItemStack, Player, SignSide, world } from "@minecraft/server";
import { allDirections, directionToVector, flipDir, isSameAxis, offsetPos, offsetVec, vectorToDirection, } from "./dirUtils";
import { lengthSquared, traverseGrid, vecEqual, vecScale, vecToString } from "./mthUtils";
import { blockEntityManager } from "../blockEntitiesManager";
import { suppEvents } from "../suppEventHandler";
import { hasNoCollision, isReplaceable } from "./blockProperties";
import { McBlocks } from "../reg/mcBlocks";
import { addonId } from "../main";
import { McTags } from "../reg/mcTags";
export function printAllPerms(perm) {
    console.error(stateToString(perm));
}
export function printAllStates(block) {
    printAllPerms(block.permutation);
}
export function printAllComponents(item) {
    const comp = item.getComponents();
    for (const c of comp) {
        console.error("component: " + c.typeId);
    }
}
export function printAllInventory(inv) {
    console.log("Inventory: [");
    for (let i = 0; i < inv.size; i++) {
        const item = inv.getItem(i);
        if (item) {
            console.error(itemToString(item));
        }
    }
    console.log("]");
}
export function printAllTags(block) {
    const tags = block.getTags();
    for (const tag of tags) {
        console.error("tag: " + tag);
    }
    console.error("ID: " + block.typeId);
}
export function printAllFamilies(entity) {
    const fam = entity.getComponent(EntityComponentTypes.TypeFamily);
    if (fam) {
        console.log("Families:[");
        for (const family of fam.getTypeFamilies()) {
            console.log(family);
        }
        console.log("]");
    }
}
//TODO: new api when it comes out
export const isOp = (p) => {
    const ap = p;
    try {
        return ap.isOp();
    }
    catch (e) {
    }
    //check permission level here once its added //TODO
    return isCreative(p);
};
export function getBlockSafe(dim, pos) {
    try {
        return dim.getBlock(pos);
    }
    catch (e) {
        return undefined;
    }
}
export function breakBlock(target) {
    if (!target.isAir) {
        target.dimension.runCommand(`/setblock ${target.location.x}
                 ${target.location.y} ${target.location.z} air destroy`);
        suppEvents.fireOnBlockBroken(target);
    }
}
export function pushEntity(entity, vec) {
    try {
        entity.clearVelocity();
        entity.applyImpulse(vec);
    }
    catch (e) {
        //TODO: this was fixed, remove
        //thanks mojang for not implementing apply impulse for players
        //calculate x dir and z dir and their magnitudes
        let horVec = { x: vec.x, y: 0, z: vec.z };
        const horMag = Math.sqrt(lengthSquared(horVec));
        if (horMag == 0) {
            entity.applyKnockback(0, 0, 0, vec.y);
        }
        else
            entity.applyKnockback(vec.x / horMag, vec.z / horMag, horMag, vec.y);
    }
}
export function spawnParticleInBlock(block, particle, amountPerFace, molang) {
    const pos = block.location;
    for (let i = 0; i < amountPerFace; i++) {
        block.dimension.spawnParticle(particle, {
            x: pos.x + Math.random(),
            y: pos.y + Math.random(),
            z: pos.z + Math.random()
        }, molang);
    }
}
export function spawnParticleOnBlockFaces(block, particle, amountPerFace, offset = 0, //offset from the center of the block
molang) {
    for (const dir of allDirections) {
        spawnParticleOnBlockFace(block, dir, particle, amountPerFace, offset, molang);
    }
}
export function spawnParticleOnBlockFace(block, dir, particle, amountPerFace, offset = 0, molang) {
    const position = block.center();
    const halfSide = 0.5;
    const vec = directionToVector(dir); // Normal vector of the face
    for (let i = 0; i < amountPerFace; i++) {
        let offsetX = (Math.random() - 0.5) * 2 * halfSide;
        let offsetY = (Math.random() - 0.5) * 2 * halfSide;
        let spawnX = position.x + (vec.x * (halfSide + offset));
        let spawnY = position.y + (vec.y * (halfSide + offset));
        let spawnZ = position.z + (vec.z * (halfSide + offset));
        const dim = block.dimension;
        if (vec.x !== 0) {
            spawnParticleSafe(dim, particle, { x: spawnX, y: position.y + offsetY, z: position.z + offsetX }, molang);
        }
        else if (vec.y !== 0) {
            spawnParticleSafe(dim, particle, { x: position.x + offsetX, y: spawnY, z: position.z + offsetY }, molang);
        }
        else {
            spawnParticleSafe(dim, particle, { x: position.x + offsetX, y: position.y + offsetY, z: spawnZ }, molang);
        }
    }
}
export function isDimensionNatural(dim) {
    return dim.id != "minecraft:nether";
}
export function spawnParticleSafe(dim, particle, pos, molang) {
    try {
        dim.spawnParticle(particle, pos, molang);
    }
    catch (e) {
    }
}
//mega dumb. replace when they add new stuff
export function isBlockSolid(block, face = Direction.Up) {
    if (!block)
        return false;
    //this at least works
    if (hasNoCollision(block))
        return false;
    //sends to ray of length 1 from the neighbor face.
    const options = {
        includePassableBlocks: false,
        maxDistance: 1,
    };
    const dir = vecScale(directionToVector(flipDir(face)), 1);
    const centerPos = offsetVec(block.center(), face, 1.49);
    const hit = block.dimension.getBlockFromRay(centerPos, dir, options);
    //caveat is that it needs a non solid in front...
    return hit && vecEqual(hit.block.location, block.location);
}
export function transmuteBlock(permutation, newBlock, exclude = []) {
    let perm;
    try {
        perm = BlockPermutation.resolve(newBlock);
    }
    catch (e) {
        return undefined;
    }
    if (!perm)
        return undefined;
    const allStates = permutation.getAllStates();
    for (const key in allStates) {
        try {
            const value = allStates[key];
            if (exclude.includes(key))
                continue;
            perm = perm.withState(key, value);
        }
        catch (e) {
        }
    }
    if (perm != permutation) {
        return perm;
    }
}
export function isLivingEntity(entity) {
    //mega stupid. why isn't this using tags. I swear bedrock is a legacy code hell.
    //ah yes only animal are goats which btw aren't mobs according to this
    const family = entity.getComponent(EntityComponentTypes.TypeFamily);
    if (!family)
        return false;
    if (entity instanceof Player)
        return true;
    return (family.hasTypeFamily("mob") || family.hasTypeFamily("player") ||
        family.hasTypeFamily("animal")) &&
        (!family.hasTypeFamily("inanimate") || !family.hasTypeFamily("razz_sup:inanimate"));
}
export function giveItemToPlayer(player, item, itemSlot) {
    const inv = player.getComponent(EntityComponentTypes.Inventory);
    if (inv && inv.container) {
        let remainder;
        if (itemSlot && inv.container.getItem(itemSlot) == undefined) {
            inv.container.setItem(itemSlot, item);
        }
        else {
            remainder = inv.container.addItem(item);
        }
        if (remainder && !isCreative(player)) {
            //drop item on player
            player.dimension.spawnItem(remainder, player.location);
        }
    }
}
export function setItemInInventory(entity, item, slot = 0) {
    const inv = entity.getComponent(EntityComponentTypes.Inventory);
    if (inv && inv.container) {
        inv.container.setItem(slot, item);
    }
}
export function getItemInInventory(entity, slot = 0) {
    const inv = entity.getComponent(EntityComponentTypes.Inventory);
    if (inv && inv.container) {
        return inv.container.getItem(slot);
    }
    return undefined;
}
export function moveBlock(block, target) {
    const level = block.dimension;
    const targetBlock = level.getBlock(target);
    if (target && isReplaceable(targetBlock)) {
        breakBlock(targetBlock);
    }
    level.setBlockPermutation(target, block.permutation);
    const newBlock = level.getBlock(target);
    blockEntityManager.moveBlockEntity(block, target);
    //gets all known components
    const oldInv = block.getComponent(BlockComponentTypes.Inventory);
    if (oldInv && oldInv.container) {
        const newInv = newBlock.getComponent(BlockComponentTypes.Inventory);
        if (newInv && newInv.container) {
            //copy content, iterate through all slots indices
            for (let i = 0; i < oldInv.container.size; i++) {
                const item = oldInv.container.getItem(i);
                if (item) {
                    newInv.container.setItem(i, item);
                }
            }
        }
        oldInv.container.clearAll();
    }
    const oldSign = block.getComponent(BlockComponentTypes.Sign);
    if (oldSign) {
        const newSign = newBlock.getComponent(BlockComponentTypes.Sign);
        if (newSign) {
            newSign.setText(oldSign.getText());
            newSign.setWaxed(oldSign.isWaxed);
            newSign.setTextDyeColor(oldSign.getTextDyeColor(SignSide.Front), SignSide.Front);
            newSign.setTextDyeColor(oldSign.getTextDyeColor(SignSide.Back), SignSide.Back);
        }
    }
    return true;
}
export function hasAnyBlockAround(block, myDir, typeId) {
    return hasAnyAround(block, myDir, (b) => {
        return b.typeId == typeId;
    });
}
export function hasAnyAround(block, myDir, test) {
    for (const dir of Object.values(Direction)) {
        if (!myDir || !isSameAxis(dir, myDir)) {
            const target = offsetPos(block, dir);
            if (target && test(target)) {
                return dir;
            }
        }
    }
    return undefined;
}
export function getItemInHand(player) {
    const inv = player.getComponent(EntityComponentTypes.Equippable);
    if (inv && inv.isValid()) {
        return inv.getEquipment(EquipmentSlot.Mainhand);
    }
    return undefined;
}
export function isShovel(itemStack) {
    return itemStack.hasTag(McTags.Shovel);
}
export function isHoe(itemStack) {
    return itemStack.hasTag(McTags.Hoe);
}
export function isAxe(itemStack) {
    return itemStack.hasTag(McTags.Axe);
}
export function isPickaxe(itemStack) {
    return itemStack.hasTag(McTags.Pickaxe);
}
export function isSword(itemStack) {
    return itemStack.hasTag(McTags.Sword);
}
export function isSpawnEgg(itemStack) {
    return itemStack.hasTag(McTags.SpawnEgg);
}
export function getPlayerHorizontalDir(player) {
    let facing = player.getViewDirection();
    facing = { x: facing.x, y: 0, z: facing.z };
    return vectorToDirection(facing);
}
//single item
export function swapItem(player, item, newItem, itemSlot) {
    shrinkIfInSurvival(player, item, 1, itemSlot);
    if (newItem)
        giveItemToPlayer(player, new ItemStack(newItem), itemSlot);
}
export function splitItem(player, item, shrinkBy = 1, itemSlot) {
    const newItem = item.clone();
    newItem.amount = shrinkIfInSurvival(player, item, shrinkBy, itemSlot);
    return newItem;
}
export function shrinkIfInSurvival(player, item, shrinkBy = 1, itemSlot) {
    //item is not valid anymore here
    let shrunkBy = 0;
    const newAmount = item.amount - shrinkBy;
    let newItem = item.clone();
    if (newAmount <= 0) {
        shrunkBy = item.amount; //all amount
        newItem = undefined;
    }
    else {
        shrunkBy = shrinkBy;
        newItem.amount = newAmount;
    }
    if (!isCreative(player)) {
        if (itemSlot == undefined) {
            setItemInMainHand(player, newAmount <= 0 ? undefined : newItem);
        }
        else {
            setItemInSlot(player, newItem, itemSlot);
        }
    }
    return shrunkBy;
}
export function setItemInSlot(player, item, slot) {
    const comp = player?.getComponent(EntityComponentTypes.Inventory);
    if (comp && comp.container) {
        comp.container.setItem(slot, item);
    }
}
export function setItemInMainHand(player, item) {
    const comp = player?.getComponent(EntityComponentTypes.Equippable);
    if (comp) {
        comp.setEquipment(EquipmentSlot.Mainhand, item);
    }
    else {
        //doesn't support full item for now
        const itemId = item?.typeId ?? "air";
        player.runCommand("/replaceitem entity @s slot.weapon.mainhand 0 " + itemId);
    }
}
export function setItemOnHead(player, item) {
    const comp = player?.getComponent(EntityComponentTypes.Equippable);
    if (comp) {
        comp.setEquipment(EquipmentSlot.Head, item);
    }
    else {
        // doesn't support full item for now
        player.runCommand("/replaceitem entity @s slot.armor.head 0 " + (item?.typeId ?? "air"));
    }
}
export function goesInHatSlot(item) {
    const id = item.typeId;
    const [namespace, name] = splitRes(id);
    return id == McBlocks.CarvedPumpkin ||
        name.endsWith("_helmet") || name.endsWith("_head") || name.endsWith("_cap") || name.endsWith("_skull");
}
export function spillInventory(entity) {
    const inv = entity.getComponent(EntityComponentTypes.Inventory);
    if (inv && inv.container) {
        for (let i = 0; i < inv.container.size; i++) {
            const item = inv.container.getItem(i);
            if (item) {
                inv.container.setItem(i, undefined);
                entity.dimension.spawnItem(item.clone(), entity.location);
            }
        }
    }
}
export function printInv(inventory) {
    const inv = inventory.container;
    for (let i = 0; i < inv.size; i++) {
        const item = inv.getItem(i);
        if (item) {
            console.error("slot: " + i + " item: " + item.typeId);
        }
    }
}
export function damageItemIfInSurvival(player, item, toAdd = 1, canBreak = true) {
    const damage = item.getComponent(ItemComponentTypes.Durability);
    if (damage) {
        damage.damage = Math.min(damage.maxDurability, damage.damage + toAdd);
        if (damage.damage >= damage.maxDurability && canBreak) {
            setItemInMainHand(player, undefined);
            //TODO: break sound
        }
        else {
            setItemInMainHand(player, item);
        }
    }
}
export function lockPlayerCamera(player) {
    const oldState = {
        [InputPermissionCategory.Camera]: player.inputPermissions.isPermissionCategoryEnabled(InputPermissionCategory.Camera),
        [InputPermissionCategory.Movement]: player.inputPermissions.isPermissionCategoryEnabled(InputPermissionCategory.Movement)
    };
    player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, false);
    player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, false);
    return oldState;
}
export function unlockPlayerCamera(player, oldState) {
    const cameraEnabled = oldState?.[InputPermissionCategory.Camera] ?? true;
    const movementEnabled = oldState?.[InputPermissionCategory.Movement] ?? true;
    player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, cameraEnabled);
    player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, movementEnabled);
}
export function stateToString(permutation) {
    let id = permutation.type.id;
    const allStates = permutation.getAllStates();
    const entries = Object.entries(allStates);
    if (entries.length == 0)
        return id;
    id = id + "[";
    let index = 0;
    for (const [key, value] of entries) {
        if (index != 0)
            id += ",";
        id += key + "=" + value;
        index++;
    }
    id += "]";
    return id;
}
export function stringToState(encoded) {
    const bracketIndex = encoded.indexOf("[");
    let blockId;
    let states = {};
    if (bracketIndex === -1) {
        // No state info, only the block ID
        blockId = encoded;
    }
    else {
        blockId = encoded.substring(0, bracketIndex);
        const stateStr = encoded.substring(bracketIndex + 1, encoded.length - 1); // remove [ and ]
        const statePairs = stateStr.split(",");
        for (const pair of statePairs) {
            const [key, rawValue] = pair.split("=");
            let value;
            // Try to coerce value to number or boolean
            if (rawValue === "true" || rawValue === "false") {
                value = rawValue === "true";
            }
            else if (!isNaN(Number(rawValue))) {
                value = Number(rawValue);
            }
            else {
                value = rawValue;
            }
            states[key] = value;
        }
    }
    return BlockPermutation.resolve(blockId, states);
}
export function enumToString(e, value) {
    const stringValue = e[value];
    if (typeof stringValue === 'string') {
        return stringValue;
    }
    throw new Error('Enum value is not a string');
}
export function stringToEnum(e, str) {
    return e[str];
}
export function setWorldData(key, value) {
    world.setDynamicProperty(key, value);
}
export function getOrCreateWorldData(key, defaultValue) {
    const existing = world.getDynamicProperty(key);
    //console.log(key, " config: " + existing);
    if (existing !== undefined) {
        return existing;
    }
    else {
        world.setDynamicProperty(key, defaultValue);
        return defaultValue;
    }
}
// deprecated
// Creates the scoreboard if it doesn't exist
export function getOrCreateScore(score, player, defaultValue) {
    const obj = world.scoreboard.getObjective(score);
    if (!obj) {
        const obj = world.scoreboard.addObjective(score, "dummy");
        obj.setScore(player, defaultValue);
    }
    else {
        try {
            return obj.getScore(player);
        }
        catch (e) {
            obj.setScore(player, defaultValue);
        }
    }
    return defaultValue;
}
export function isCreative(player) {
    return player.getGameMode() === GameMode.creative;
}
export function itemToString(item) {
    if (item) {
        const id = item.typeId;
        const [namespace, path] = splitRes(id);
        const mcId = namespace == "minecraft" ? path : id;
        if (BlockTypes.get(item.typeId)) {
            return { translate: "tile." + mcId + ".name" };
        }
        const typeId = { translate: "item." + mcId };
        const amount = item.amount;
        return typeId; //TODO:
    }
    return { text: "empty" };
}
export function hasFamily(entity, family) {
    const comp = entity.getComponent(EntityComponentTypes.TypeFamily);
    if (comp) {
        return comp.hasTypeFamily(family);
    }
    return false;
}
// once again bedrock at its finest. We can swing player hand in first person. No way. In third we play this copy of the swing hand animation that player has
//just third person
export function playSwingArm(player) {
    player.playAnimation("animation.razz_sup.player.third_person.swing_arm", { blendOutTime: 0 });
}
export function getBlocksInRadius(position, radius, dimension, validBlocks = (b => true)) {
    const blocks = [];
    const visited = new Set();
    const queue = [{ pos: position, dist: 0 }];
    visited.add(vecToString(position));
    while (queue.length > 0) {
        const { pos, dist } = queue.shift();
        if (dist > radius)
            continue;
        const block = getBlockSafe(dimension, pos);
        if (dist > 0) {
            if (!block || !validBlocks(block)) {
                continue; // Stop this branch if block is invalid (but not at center)
            }
            blocks.push(block);
        }
        for (const dir of allDirections) {
            const neighbor = offsetVec(pos, dir);
            const key = vecToString(neighbor);
            if (!visited.has(key)) {
                visited.add(key);
                queue.push({ pos: neighbor, dist: dist + 1 });
            }
        }
    }
    return blocks;
}
export function traverseBlocksUntil(level, start, direction, predicate = (b => b.isAir)) {
    let previousPos = undefined;
    let currentBlock = undefined;
    for (const currentPos of traverseGrid(start, direction)) {
        const block = level.getBlock(currentPos);
        if (!block || !block.isValid())
            return null; // Unloaded chunk
        if (previousPos === undefined) {
            // First position: derive face from the ray direction
            if (!predicate(block)) {
                //guess?
                const face = vectorToDirection(direction);
                return new Hit(block, face);
            }
            previousPos = currentPos;
            currentBlock = block;
            continue;
        }
        // Calculate delta between current and previous position
        const delta = {
            x: currentPos.x - previousPos.x,
            y: currentPos.y - previousPos.y,
            z: currentPos.z - previousPos.z,
        };
        // Determine the face we entered through
        const face = vectorToDirection(delta);
        if (predicate(block)) {
            previousPos = currentPos;
            currentBlock = block;
        }
        else {
            return new Hit(block, face);
        }
    }
    return undefined;
}
export class Hit {
    constructor(hitBlock, enteredFace) {
        this.hitBlock = hitBlock;
        this.hitFace = flipDir(enteredFace);
    }
}
export function splitRes(id) {
    const parts = id.split(':');
    if (parts.length == 2) {
        return [parts[0], parts[1]];
    }
    return ["minecraft", id];
}
export function translate(prefix, string, args) {
    const str = `${prefix}.${addonId()}:${string}`;
    return { translate: str, with: args };
}
