/* eslint-disable @typescript-eslint/no-explicit-any */
import { system, EquipmentSlot, ItemStack, MinecraftDimensionTypes, GameMode, world, Direction, LocationInUnloadedChunkError, LocationOutOfWorldBoundariesError, } from '@minecraft/server';
/**
 * Add together a sequence of numbers
 * @param numbers The numbers to add together
 * @returns The summation
 */
export function sum(...numbers) {
    let start = 0;
    numbers.forEach((number) => {
        start += number;
    });
    return start;
}
/**
 * Get the average of a set of numbers
 * @param numbers The numbers to the get the average of
 * @returns The average
 */
export function avg(...numbers) {
    return sum(...numbers) / numbers.length;
}
export function degToRad(degrees) {
    return degrees * (Math.PI / 180);
}
export function radToDeg(radians) {
    return radians * (180 / Math.PI);
}
export const dimensions = [
    MinecraftDimensionTypes.overworld,
    MinecraftDimensionTypes.nether,
    MinecraftDimensionTypes.theEnd
];
/** @deprecated Use Player.gameMode instead */
export function isGameMode(player, mode) {
    return player.dimension.getPlayers({ location: player.location, maxDistance: 1, gameMode: mode })
        .filter((p) => p.id === player.id).length > 0;
}
/** @deprecated Use Player.gameMode instead */
export function getGameMode(player, orElse = GameMode.survival) {
    // Loop through 'GameMode' enum values
    for (const mode of Object.values(GameMode)) {
        if (isGameMode(player, mode))
            return mode;
    }
    return orElse;
}
export function getTypeFamilies(entity) {
    if (!entity.hasComponent('minecraft:type_family'))
        return undefined;
    const families = entity.getComponent('minecraft:type_family');
    return families.getTypeFamilies();
}
/**
 * Returns the item in the player's mainhand
 * @param player The event data of the player
 * @param slot The slot in the inventory: f.e. 'Mainhand'.
 */
export function getItemInSlot(player, slot = EquipmentSlot.Mainhand) {
    const equippableComponent = player.getComponent('minecraft:equippable');
    return equippableComponent.getEquipment(slot);
}
/**
 * Sets the item in the player's mainhand
 * @param player The event data of the player
 * @param slot The slot in the inventory: f.e. 'Mainhand'.
 * @param itemStack The item to set in the slot.
 */
export function setItemInSlot(player, slot, itemStack) {
    const equippableComponent = player.getComponent('minecraft:equippable');
    equippableComponent.setEquipment(slot, itemStack);
}
/**
 * Adds an item to an entity's inventory
 * @param entity The entity to receive the item
 * @param itemStack The item to give to the entity
 */
export function addItemToInventory(entity, itemStack, options) {
    const inventory = entity.getComponent('inventory');
    const container = inventory?.container;
    // If entity has no container, spawn the item on the entity's location
    if (!container) {
        entity.dimension.spawnItem(itemStack, entity.location);
        return;
    }
    // If entity has no empty slots, we may overflow and need to drop some
    // items on the ground
    if (inventory.container.emptySlotsCount === 0) {
        // Count how many of this item still fit in the inventory
        let spaceRemaining = 0;
        for (let slotId = 0; slotId < container.size; slotId++) {
            const slot = container.getSlot(slotId);
            if (slot.getItem()?.typeId === itemStack.typeId) {
                spaceRemaining += Math.max(slot.maxAmount - slot.amount, 0);
                if (spaceRemaining >= itemStack.amount)
                    break;
            }
        }
        // Calculate how many items will not fit
        const overflowAmount = itemStack.amount - spaceRemaining;
        // Spawn items that will not fit on the ground
        if (overflowAmount > 0) {
            const droppedStack = itemStack.clone();
            droppedStack.amount = overflowAmount;
            const spawnLocation = entity.location;
            if (options?.spawnOffset) {
                // Not using WBVector3 due to cyclic dependency
                spawnLocation.x += options.spawnOffset.x;
                spawnLocation.y += options.spawnOffset.y;
                spawnLocation.z += options.spawnOffset.z;
            }
            const itemEntity = entity.dimension.spawnItem(droppedStack, spawnLocation);
            options?.spawnCallback?.(itemEntity);
        }
    }
    // Add items to the container
    inventory.container.addItem(itemStack);
}
/**
 * Remove an item from an entity's inventory
 * @param entity The entity to remove the item from
 * @param itemId The item to remove
 * @returns Whether the operation was successful
 */
export function removeItemFromInventory(entity, itemId) {
    const inventory = entity.getComponent('inventory');
    const container = inventory?.container;
    if (!container)
        return false;
    for (let slotId = 0; slotId < container.size; slotId++) {
        const slot = container.getSlot(slotId);
        const item = slot.getItem();
        if (item?.typeId === itemId) {
            if (item.amount > 1) {
                item.amount--;
                container.setItem(slotId, item);
            }
            else {
                container.setItem(slotId, undefined);
            }
            return true;
        }
    }
    return false;
}
/**
 * Search for an item in an entity's inventory
 * @param entity The entity whose inventory to search
 * @param itemId The item to search for
 * @returns Whether the item was found
 */
export function hasItemFromInventory(entity, itemId) {
    const inventory = entity.getComponent('inventory');
    const container = inventory?.container;
    if (!container)
        return false;
    for (let slotId = 0; slotId < container.size; slotId++) {
        const slot = container.getSlot(slotId);
        const item = slot.getItem();
        if (item?.typeId === itemId)
            return true;
    }
    return false;
}
/**
 * Count how many items an entity has in their inventory
 * @param entity The entity whose inventory to search
 * @param itemId An optional item ID to filter for
 * @returns The amount of items the entity has
 */
export function countItemsInInventory(entity, itemId) {
    const inventory = entity.getComponent('inventory');
    const container = inventory?.container;
    if (!container)
        return 0;
    let amount = 0;
    for (let slotId = 0; slotId < container.size; slotId++) {
        const slot = container.getSlot(slotId);
        const item = slot.getItem();
        if (item && (!itemId || item.typeId === itemId))
            amount += item.amount;
    }
    return amount;
}
/**
 * Decrease the amount of the item in the player's mainhand
 * @param player The event data of the player
 * @param amount The amount to decrease
 * @param ignoreInCreative If true, the item amount will not be decreased
 *                         if the player is in creative mode
 * @returns True if the item amount was decreased, false otherwise.
 *          Always returns true if ignoreInCreative is true and the player is in creative mode.
 */
export function decreaseItemAmountInHand(player, amount = 1, ignoreInCreative = false) {
    if (ignoreInCreative && isGameMode(player, GameMode.creative))
        return true;
    const item = getItemInSlot(player, EquipmentSlot.Mainhand);
    if (item) {
        if (item.amount > amount) {
            item.amount -= amount;
            setItemInSlot(player, EquipmentSlot.Mainhand, item);
        }
        else {
            setItemInSlot(player, EquipmentSlot.Mainhand, undefined);
        }
        return true;
    }
    return false;
}
/**
 * Decrease the durability of the item in the player's mainhand
 * @param player The event data of the player
 * @param options Configuration options
 * @returns True if the item was damaged, false otherwise.
 *          Always returns true if ignoreInCreative is true and the player is in creative mode.
 */
export function decreaseItemDurabilityInHand(player, options) {
    const ignoreInCreative = !(options?.ignoreInCreative === false);
    if (ignoreInCreative && isGameMode(player, GameMode.creative))
        return true;
    const item = getItemInSlot(player, EquipmentSlot.Mainhand);
    const durability = item?.getComponent('minecraft:durability');
    if (item && durability) {
        const chance = durability.getDamageChance();
        if (Math.random() < chance) {
            if (durability.damage === durability.maxDurability) {
                setItemInSlot(player, EquipmentSlot.Mainhand, options?.convertInto ? new ItemStack(options.convertInto, item.amount) : undefined);
                world.playSound(options?.destructionSound || 'random.break', player.location);
            }
            else {
                durability.damage++;
                setItemInSlot(player, EquipmentSlot.Mainhand, item);
            }
            return true;
        }
    }
    return false;
}
export function getBlockSafe(dimension, location) {
    try {
        return dimension.getBlock(location);
    }
    catch (e) {
        // eslint-disable-next-line max-len
        if (e instanceof LocationInUnloadedChunkError || e instanceof LocationOutOfWorldBoundariesError) {
            return undefined;
        }
        throw e;
    }
}
export function getBlockSafeDL(dimensionLocation) {
    return getBlockSafe(dimensionLocation.dimension, dimensionLocation);
}
export function isBlockSolid(block) {
    return !!block.dimension.getBlockFromRay(block.location, { x: 0, y: 1, z: 0 }, { includePassableBlocks: false, maxDistance: 1 });
}
/**
 * Effectively kills an entity, by also despawning and teleporting it far away before killing.
 * @param entity The entity to kill
 * @returns True if entity can be killed (even if it is already dead), false otherwise.
 */
export function kill(entity) {
    entity.teleport({ x: 0, y: -9999, z: 0 });
    entity.runCommand('event entity @s twb_wisu:despawn');
    return entity.kill();
}
export var CardinalDirection;
(function (CardinalDirection) {
    CardinalDirection[CardinalDirection["NORTH"] = 0] = "NORTH";
    CardinalDirection[CardinalDirection["EAST"] = 90] = "EAST";
    CardinalDirection[CardinalDirection["SOUTH"] = 180] = "SOUTH";
    CardinalDirection[CardinalDirection["WEST"] = 270] = "WEST";
})(CardinalDirection || (CardinalDirection = {}));
export function getCardinalDirection(yRotation) {
    switch ((Math.floor(((yRotation + 45) % 360) / 90) + 4) % 4) {
        case 1:
            return CardinalDirection.EAST;
        case 2:
            return CardinalDirection.SOUTH;
        case 3:
            return CardinalDirection.WEST;
        default:
            return CardinalDirection.NORTH;
    }
}
export function getOrCreateObjective(scoreboard, objectiveId, displayName) {
    return scoreboard.getObjective(objectiveId)
        || scoreboard.addObjective(objectiveId, displayName || objectiveId);
}
export function sleep(ticks) {
    // eslint-disable-next-line no-promise-executor-return
    return new Promise((r) => system.runTimeout(() => r(undefined), ticks));
}
/**
 * Wait for an event to occur.
 * @param eventSignal The signal to wait for. Note that this parameter needs to be
 * explicitly casted to EventSignal.
 * @param condition An optional function that takes the event as parameter. If provided, the
 * promise will only be resolved if this function returns true.
 * @param timeout An optional timeout, in game ticks, after which the promise is rejected.
 * @returns The event
 */
export async function waitFor(eventSignal, condition, timeout) {
    return new Promise((resolve, reject) => {
        // Subscribe to the signal
        const callback = eventSignal.subscribe((arg) => {
            try {
                if (!condition || condition(arg)) {
                    // Resolve, unless the condition prevents it
                    eventSignal.unsubscribe(callback);
                    resolve(arg);
                }
            }
            catch (e) {
                // In case of an exception, unsubscribe and reject
                eventSignal.unsubscribe(callback);
                reject(e);
            }
        });
        if (timeout) {
            // Optionally set a timeout
            system.runTimeout(() => {
                eventSignal.unsubscribe(callback);
                reject(new Error('Time limit exceeded'));
            }, timeout);
        }
    });
}
export function translate(key) {
    return { rawtext: [{ translate: key }] };
}
export function inRange(number, range) {
    return number >= range.min && number <= range.max;
}
export function directionToNormal(direction) {
    switch (direction) {
        case Direction.Up:
            return { x: 0, y: 1, z: 0 };
        case Direction.Down:
            return { x: 0, y: -1, z: 0 };
        case Direction.North:
            return { x: 0, y: 0, z: 1 };
        case Direction.East:
            return { x: 1, y: 0, z: 0 };
        case Direction.South:
            return { x: 0, y: 0, z: -1 };
        case Direction.West:
            return { x: -1, y: 0, z: 0 };
        default:
            throw new Error('Unknown direction');
    }
}
