import * as mc from "@minecraft/server";
import * as log from './log';
import { overworld } from "./dimension";
import { Vector3 } from "./vector3";
import { addUniqueToArray } from "./array";
/**
 * @remarks An array of blocks Minecraft considers non-solid.
 */
export const non_solid_blocks = [
    "minecraft:air",
    // Saplings
    "minecraft:acacia_sapling",
    "minecraft:bamboo_sapling",
    "minecraft:birch_sapling",
    "minecraft:cherry_sapling",
    "minecraft:dark_oak_sapling",
    "minecraft:jungle_sapling",
    "minecraft:oak_sapling",
    "minecraft:spruce_sapling",
    // Flowers
    "minecraft:poppy",
    "minecraft:blue_orchid",
    "minecraft:allium",
    "minecraft:azure_bluet",
    "minecraft:red_tulip",
    "minecraft:orange_tulip",
    "minecraft:white_tulip",
    "minecraft:pink_tulip",
    "minecraft:oxeye_daisy",
    "minecraft:cornflower",
    "minecraft:lily_of_the_valley",
    "minecraft:sunflower",
    "minecraft:lilac",
    "minecraft:rose_bush",
    "minecraft:peony",
    "minecraft:pink_petals",
    "minecraft:yellow_flower",
    "minecraft:pitcher_crop",
    "minecraft:pitcher_plant",
    "minecraft:torchflower_crop",
    "minecraft:torchflower",
    "minecraft:spore_blossom",
    // Mushrooms
    "minecraft:crimson_fungus",
    "minecraft:warped_fungus",
    "minecraft:brown_mushroom",
    "minecraft:red_mushroom",
    // Grasses
    "minecraft:warped_roots",
    "minecraft:crimson_roots",
    "minecraft:tall_grass",
    "minecraft:fern",
    "minecraft:short_grass",
    "minecraft:large_fern",
    "minecraft:deadbush",
    "minecraft:seagrass",
    "minecraft:nether_sprouts",
    "minecraft:weeping_vines",
    "minecraft:twisting_vines",
    "minecraft:vine",
    "minecraft:small_dripleaf_block",
    //Coral
    "minecraft:brain_coral_fan",
    "minecraft:bubble_coral_fan",
    "minecraft:fire_coral_fan",
    "minecraft:horn_coral_fan",
    "minecraft:tube_coral_fan",
    "minecraft:dead_brain_coral_fan",
    "minecraft:dead_bubble_coral_fan",
    "minecraft:dead_fire_coral_fan",
    "minecraft:dead_horn_coral_fan",
    "minecraft:dead_tube_coral_fan",
    "minecraft:coral_fan_hang",
    "minecraft:coral_fan_hang2",
    "minecraft:coral_fan_hang3",
    //Crops
    "minecraft:reeds",
    "minecraft:wheat",
    "minecraft:beetroot",
    "minecraft:carrots",
    "minecraft:potatoes",
    "minecraft:melon_stem",
    "minecraft:pumpkin_stem",
    "minecraft:nether_wart",
    "minecraft:sweet_berry_bush",
    "minecraft:cave_vines",
    "minecraft:cave_vines_body_with_berries",
    "minecraft:cave_vines_head_with_berries",
    "minecraft:kelp",
    // Fire
    "minecraft:soul_fire",
    "minecraft:fire",
    // Signs
    "minecraft:standing_sign",
    "minecraft:acacia_standing_sign",
    "minecraft:birch_standing_sign",
    "minecraft:crimson_standing_sign",
    "minecraft:darkoak_standing_sign",
    "minecraft:jungle_standing_sign",
    "minecraft:mangrove_standing_sign",
    "minecraft:spruce_standing_sign",
    "minecraft:warped_standing_sign",
    // Rails
    "minecraft:rail",
    "minecraft:golden_rail",
    "minecraft:detector_rail",
    "minecraft:activator_rail",
    // Buttons
    "minecraft:wooden_button",
    "minecraft:spruce_button",
    "minecraft:birch_button",
    "minecraft:jungle_button",
    "minecraft:acacia_button",
    "minecraft:dark_oak_button",
    "minecraft:mangrove_button",
    "minecraft:polished_blackstone_button",
    "minecraft:stone_button",
    "minecraft:crimson_button",
    "minecraft:warped_button",
    // Redstone
    "minecraft:redstone_wire",
    "minecraft:redstone_torch",
    "minecraft:unlit_redstone_torch",
    "minecraft:lever",
    "minecraft:tripwire_hook",
    // Pressure Plates
    "minecraft:acacia_pressure_plate",
    "minecraft:bamboo_pressure_plate",
    "minecraft:birch_pressure_plate",
    "minecraft:cherry_pressure_plate",
    "minecraft:crimson_pressure_plate",
    "minecraft:dark_oak_pressure_plate",
    "minecraft:jungle_pressure_plate",
    "minecraft:mangrove_pressure_plate",
    "minecraft:spruce_pressure_plate",
    "minecraft:warped_pressure_plate",
    "minecraft:polished_blackstone_pressure_plate",
    "minecraft:stone_pressure_plate",
    "minecraft:light_weighted_pressure_plate",
    "minecraft:heavy_weighted_pressure_plate",
    "minecraft:wooden_pressure_plate",
    "minecraft:redstone_torch",
    "minecraft:unlit_redstone_torch",
    // Misc
    "minecraft:powder_snow",
    "minecraft:web",
    "minecraft:sculk_vein",
    "minecraft:frog_spawn",
    "minecraft:turtle_egg",
    "minecraft:torch",
    "minecraft:soul_torch",
    "minecraft:light_block",
    // Liquids
    "minecraft:water",
    "minecraft:flowing_water",
    "minecraft:lava",
    "minecraft:flowing_lava",
    "minecraft:standing_banner",
];
/**
 * @remarks Checks if the the given block at the location.
 * @param location The location to check for the block.
 * @param block The block name to check (i.e. "stone").
 * @param properties The block's properties.
 * @param dimension The dimension to check in (defaults to overworld).
 * @returns True if the block at the location matches the parameters, false if not, undefined if out of range.
 * @example
 * ```typescript
 * isBlockAtLocation(new Vector3(0), 'minecraft:bedrock'); // Returns true if the block at 0 0 0 is bedrock
 * ```
 */
export function isBlockAtLocation(location, block, properties, dimension = overworld) {
    var _a;
    try {
        return (_a = dimension.getBlock(location)) === null || _a === void 0 ? void 0 : _a.permutation.matches(block, properties);
    }
    catch (error) {
        log.mcerror(error);
    }
}
/**
 * @remarks A block's state, air, solid, or non_solid.
 */
export var state;
(function (state) {
    state["air"] = "air";
    state["solid"] = "solid";
    state["non_solid"] = "non_solid";
})(state || (state = {}));
;
/**
 * @remarks Returns the {@link state} (air, solid, non_solid) of the block at the location.
 * @param location The location to check for the block.
 * @param dimension The dimension to check in (defaults to overworld)
 * @returns The block's state as air, solid, or non_solid.
 * @example
 * ```typescript
 * getBlockStateAtLocation(new Vector3(0)); // Returns the state of the block at 0 0 0
 * ```
 */
export function getBlockStateAtLocation(location, dimension = overworld) {
    try {
        if (isBlockAtLocation(location, 'minecraft:air', undefined, dimension)) {
            return state.air;
        }
        for (const non_solid_block of non_solid_blocks) {
            if (isBlockAtLocation(location, non_solid_block, undefined, dimension)) {
                return state.non_solid;
            }
        }
    }
    catch (error) {
        log.mcerror(error);
    }
    return state.solid;
}
/**
 * @remarks Performs a raycast to see if we hit a block or entity.
 * @param source An Entity or Dimension that is the source for this raycast.
 * @param origin The location where the raycast starts.
 * @param distance The maximum distance the raycast performs.
 * @param direction The direction the raycast should travel in.
 * @param options Additional options regarding the raycast.
 * @returns An IHitResult containing the hit entities, block, and success marking whether anything was hit.
 * @example
 * ```typescript
 * const player = getAllPlayersSafe()[0];
 * performRaycast(player, player.location, 5, player.getViewDirection()) // Returns an IHitResult containing hit entities and/or blocks;
 * ```
 */
export function performRaycast(source, origin, distance, direction, options = {}) {
    var _a;
    const entities = [];
    let block;
    let success = false;
    let dimension = source instanceof mc.Dimension ? source : source.dimension;
    let entity = source instanceof mc.Entity ? source : undefined;
    for (let index = 0; index < distance; index++) {
        const magnitude = direction.multiply(new Vector3(index));
        const destination = origin.add(magnitude);
        (_a = options.raycastEffects) === null || _a === void 0 ? void 0 : _a.forEach(effect => {
            if (index > 0 && (!effect.condition || effect.condition(source))) {
                if (effect.particle)
                    source.runCommand(`particle ${effect.particle} ${destination.toCommandString()}`);
                if (effect.sound)
                    mc.world.playSound(effect.sound, destination);
            }
        });
        block = dimension.getBlock(destination);
        if (!options.passThroughSolidBlocks) {
            const blockState = getBlockStateAtLocation(destination, dimension);
            if ((!options.passThroughNonSolidBlocks && blockState !== state.air) || (options.passThroughNonSolidBlocks && blockState === state.solid)) {
                success = true;
                break;
            }
        }
        const found_entities = dimension.getEntitiesAtBlockLocation(destination).filter(found_entity => !(found_entity === entity && !options.includeSelf));
        addUniqueToArray(entities, ...found_entities);
        if (options.stopAtFirstHit && found_entities.length > 0) {
            entities.length = 1;
            success = true;
            break;
        }
    }
    return { entities, block, success };
}
/**
 * @deprecated Deprecated in favor of {@link Vector3.inBounds}
 * @remarks Returns a boolean representing the location being within the volume.
 * @param location The location to check against the volume.
 * @param min_location The negative-most coordinate of the volume (inclusive).
 * @param max_location The positive-most coordinate of the volume (inclusive).
 * @returns True if the location is in range, false otherwise.
 * @example
 * ```typescript
 * const player = getAllPlayersSafe()[0];
 * isLocationInVolume(player.location, new Vector3(0), new Vector3(100)) // Returns true if the player is between 0 0 0 and 100 100 100
 * ```
 */
export function isLocationInVolume(location, min_location, max_location) {
    return (location.x >= min_location.x && location.y >= min_location.y && location.z >= min_location.z) &&
        (location.x <= max_location.x && location.y <= max_location.y && location.z <= max_location.z);
}
/**
 * @remarks Gets the number of times a given block appears within a volume.
 * @param min_location The negative-most coordinate of the volume (inclusive).
 * @param max_location The positive-most coordinate of the volume (inclusive).
 * @param dimension The dimension in which to perform the check.
 * @param block The block to get the count of.
 * @returns The number of instances of the block within the volume.
 * @example
 * ```typescript
 * const player = getAllPlayersSafe()[0];
 * getBlockCountInVolume(player.location.subtract(5), player.location.add(5), player.dimension, {block: 'minecraft:grass'}); // Returns the number of grass blocks in range.
 * ```
 */
export function getBlockCountInVolume(min_location, max_location, dimension, block) {
    const blocks = dimension.getBlocks(new mc.BlockVolume(min_location, max_location), { includePermutations: [mc.BlockPermutation.resolve(block.block, block.properties)] });
    return Array.from(blocks.getBlockLocationIterator()).length;
}
/**
 * @remarks Returns whether a block is solid. Note this checks if the block is not air or liquid, it does not use the {@link non_solid_blocks} list.
 * @param block The block to check.
 * @returns True if the block is solid, false otherwise.
 * @example
 * ```typescript
 * const block = overworld.getBlock(new Vector3(0));
 * isSolid(block); // Returns true if the block at 0 0 0 is not air or liquid.
 * ```
 */
export function isSolid(block) {
    return !(block === null || block === void 0 ? void 0 : block.isAir) && !(block === null || block === void 0 ? void 0 : block.isLiquid);
}
/**
 * @remarks Gets the first block adjacent to a location matching the blockdata. Returns in the order of down, up, north, east, south, west.
 * @param location The location to start the check at.
 * @param blockdata The blockdata to check for nearby.
 * @param dimension The dimension to check in.
 * @param ignoreDirections An optional condition for directions to be ignored.
 * @returns A block matching the blockdata adjacent to location or undefined if none matched.
 * @example
 * ```typescript
 * const player = getAllPlayersSafe()[0];
 *
 * // Returns a grass block to the player's north, south, east, or west. Or undefined if no grass blocks are present
 * getAdjacentBlock(player.location, {block: 'minecraft:grass'}, player.dimension, {down: true, up: true});
 * ```
 */
export function getAdjacentBlock(location, blockdata, dimension, ignoreDirections) {
    var _a;
    const origin = new Vector3(location);
    const blocks = [
        { block: dimension.getBlock(origin.down()), ignore: ignoreDirections === null || ignoreDirections === void 0 ? void 0 : ignoreDirections.down },
        { block: dimension.getBlock(origin.up()), ignore: ignoreDirections === null || ignoreDirections === void 0 ? void 0 : ignoreDirections.up },
        { block: dimension.getBlock(origin.north()), ignore: ignoreDirections === null || ignoreDirections === void 0 ? void 0 : ignoreDirections.north },
        { block: dimension.getBlock(origin.east()), ignore: ignoreDirections === null || ignoreDirections === void 0 ? void 0 : ignoreDirections.east },
        { block: dimension.getBlock(origin.south()), ignore: ignoreDirections === null || ignoreDirections === void 0 ? void 0 : ignoreDirections.south },
        { block: dimension.getBlock(origin.west()), ignore: ignoreDirections === null || ignoreDirections === void 0 ? void 0 : ignoreDirections.west },
    ];
    for (const block of blocks) {
        if (!block.ignore && ((_a = block.block) === null || _a === void 0 ? void 0 : _a.permutation.matches(blockdata.block, blockdata.properties))) {
            return block.block;
        }
    }
}
/**
 * @remarks Gets the first block adjacent to a location has the desired tag. Returns in the order of down, up, north, east, south, west.
 * @param location The location to start the check at.
 * @param tag The tag the block must have.
 * @param dimension The dimension to check in.
 * @param ignoreDirections An optional condition for directions to be ignored.
 * @returns A block with the desired tag adjacent to location or undefined if none matched.
 * @example
 * ```typescript
 * const player = getAllPlayersSafe()[0];
 *
 * // Returns a grass block to the player's north, south, east, or west. Or undefined if no grass blocks are present
 * getAdjacentBlockWithTag(player.location, 'iron_pick_diggable', player.dimension, {down: true, up: true});
 * ```
 */
export function getAdjacentBlockWithTag(location, tag, dimension, ignoreDirections) {
    var _a;
    const origin = new Vector3(location);
    const blocks = [
        { block: dimension.getBlock(origin.down()), ignore: ignoreDirections === null || ignoreDirections === void 0 ? void 0 : ignoreDirections.down },
        { block: dimension.getBlock(origin.up()), ignore: ignoreDirections === null || ignoreDirections === void 0 ? void 0 : ignoreDirections.up },
        { block: dimension.getBlock(origin.north()), ignore: ignoreDirections === null || ignoreDirections === void 0 ? void 0 : ignoreDirections.north },
        { block: dimension.getBlock(origin.east()), ignore: ignoreDirections === null || ignoreDirections === void 0 ? void 0 : ignoreDirections.east },
        { block: dimension.getBlock(origin.south()), ignore: ignoreDirections === null || ignoreDirections === void 0 ? void 0 : ignoreDirections.south },
        { block: dimension.getBlock(origin.west()), ignore: ignoreDirections === null || ignoreDirections === void 0 ? void 0 : ignoreDirections.west },
    ];
    for (const block of blocks) {
        if (!block.ignore && ((_a = block.block) === null || _a === void 0 ? void 0 : _a.hasTag(tag))) {
            return block.block;
        }
    }
}
/**
 * @remarks Searches for a block in a square around the origin using generators to improve performance.
 * @param dimension The dimension in which to search.
 * @param origin The center of the search.
 * @param searchSize The radius of the square to search.
 * @param block The block to search for.
 * @returns The block found or undefined if not found.
 * @example
 * ```typescript
 * const player = utils.getAllPlayersSafe()[0];
 * const block = await utils.searchForBlock(utils.overworld, player.location, 10, {block: "minecraft:stone"});
 * ```
 */
export async function searchForBlock(dimension, origin, searchSize, block) {
    let jobId = 0;
    const blockResult = await new Promise((resolve) => {
        function* searchGenerator(dimension, origin, searchSize, block) {
            for (let cube_size = 0; cube_size < searchSize; cube_size++) {
                for (let y = -cube_size; y <= cube_size; y++) {
                    for (let x = -cube_size; x <= cube_size; x++) {
                        for (let z = -cube_size; z <= cube_size; z++) {
                            if (Math.abs(x) === cube_size || Math.abs(y) === cube_size || Math.abs(z) === cube_size) {
                                try {
                                    const checkBlock = dimension.getBlock(origin.add({ x, y, z }));
                                    if (checkBlock === null || checkBlock === void 0 ? void 0 : checkBlock.matches(block.block, block.properties)) {
                                        resolve(checkBlock);
                                    }
                                }
                                catch (error) {
                                    log.mcerror(error);
                                    resolve(undefined);
                                }
                                yield;
                            }
                        }
                    }
                }
            }
            resolve(undefined);
        }
        jobId = mc.system.runJob(searchGenerator(dimension, origin, searchSize, block));
    });
    mc.system.clearJob(jobId);
    return blockResult;
}
/**
 * @remarks Iterates over blocks in a square around the origin using generators to improve performance.
 * @param dimension The dimension in which to iterate.
 * @param origin The center of the operation.
 * @param searchSize The radius of the square to iterate over.
 * @param callback The callback to run on each block in the search area. Return true to stop the iteration.
 * @returns A promise that resolves when the iteration is complete.
 * @example
 * ```typescript
 * const player = utils.getAllPlayersSafe()[0];
 * utils.iterateOverBlocks(utils.overworld, player.location, 10, (block) => {
 *     if (block.isAir && !block.below()?.isAir) {
 *         block.dimension.spawnParticle("minecraft:endrod", new utils.Vector3(block.location).center());
 *     }
 *     if (block.matches("minecraft:cobblestone")) {
 *         return true;
 *     }
 * });
 * ```
 */
export async function iterateOverBlocks(dimension, origin, searchSize, callback) {
    let jobId = 0;
    const block = await new Promise((resolve) => {
        function* searchGenerator(dimension, origin, searchSize, callback) {
            for (let cube_size = 0; cube_size < searchSize; cube_size++) {
                for (let y = -cube_size; y <= cube_size; y++) {
                    for (let x = -cube_size; x <= cube_size; x++) {
                        for (let z = -cube_size; z <= cube_size; z++) {
                            if (Math.abs(x) === cube_size || Math.abs(y) === cube_size || Math.abs(z) === cube_size) {
                                try {
                                    const block = dimension.getBlock(origin.add({ x, y, z }));
                                    if (block === null || block === void 0 ? void 0 : block.isValid()) {
                                        if (callback(block)) {
                                            resolve();
                                            return;
                                        }
                                        ;
                                    }
                                }
                                catch (error) {
                                    log.mcerror(error);
                                }
                                yield;
                            }
                        }
                    }
                }
            }
            resolve(undefined);
        }
        jobId = mc.system.runJob(searchGenerator(dimension, origin, searchSize, callback));
    });
    mc.system.clearJob(jobId);
    return block;
}
/**
 * @deprecated Deprecated in favor of {@link iterateOverBlocks}
 * @remarks Performs an operation on each block in and expanding cube, this can be more performant than simply iterating over a cube block by block.
 * @param origin The origin location of the operation.
 * @param coordinateCallback The callback that executes at each block in the cube, returning true ends the expansion.
 * @param layerEvaluation The callback that executes after the cube expands, returning true ends the expansion.
 * @param size The minimum and maximum size of the cube.
 * @example
 * From Craftee Companion
 * ```typescript
 * await expandingCubeOperation(this.npc.Entity.location.round(),
 * async (location) => {
 *     const groundBlock = this.npc.Entity.dimension.getBlock(location);
 *     const interactBlock = groundBlock?.above();
 *     if (this.options.blocks.some(block => groundBlock?.permutation.matches(block.block)) && interactBlock?.isAir) {
 *         const neighborBlocks = [interactBlock.north(), interactBlock.south(), interactBlock.east(), interactBlock.west()];
 *         const validNeighbor = neighborBlocks.find(neighbor => neighbor?.isAir && neighbor.above()?.isAir && isSolid(neighbor.below()));
 *         if (validNeighbor) {
 *             interactLocation = new Vector3(location).add({x: 0.5, y: 1, z: 0.5});
 *             navigationLocation = new Vector3(validNeighbor.location).add({x: 0.5, y: 0, z: 0.5});
 *             foundBlock = true;
 *             return true;
 *         }
 *     }
 *     return false;
 * },
 * async () => {
 *     await waitForTicks(1);
 *     return false;
 * });
 * ```
 */
export async function expandingCubeOperation(origin, coordinateCallback, layerEvaluation, size = { min: 1, max: 5 }) {
    let loopCount = 0;
    for (let cube_size = size.min; cube_size < size.max; cube_size++) {
        for (let y = -cube_size; y <= cube_size; y++) {
            for (let x = -cube_size; x <= cube_size; x++) {
                for (let z = -cube_size; z <= cube_size; z++) {
                    if (Math.abs(x) === cube_size || Math.abs(y) === cube_size || Math.abs(z) === cube_size) {
                        loopCount++;
                        if (await coordinateCallback(origin.add({ x, y, z }), loopCount))
                            return;
                    }
                }
            }
        }
        if (layerEvaluation && await layerEvaluation())
            return;
    }
}
