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",
    "minecraft:sapling",
    "minecraft:rail",
    "minecraft:golden_rail",
    "minecraft:detector_rail",
    "minecraft:activator_rail",
    "minecraft:crimson_fungus",
    "minecraft:warped_fungus",
    "minecraft:brown_mushroom",
    "minecraft:red_mushroom",
    "minecraft:red_flower",
    "minecraft:warped_roots",
    "minecraft:crimson_roots",
    "minecraft:double_plant",
    "minecraft:tallgrass",
    "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",
    "minecraft:nether_wart",
    "minecraft:standing_banner",
    "minecraft:coral_fan",
    "minecraft:coral_fan_dead",
    "minecraft:coral_fan_hang",
    "minecraft:coral_fan_hang2",
    "minecraft:coral_fan_hang3",
    "minecraft:redstone_wire",
    "minecraft:redstone_torch",
    "minecraft:unlit_redstone_torch",
    "minecraft:turtle_egg",
    "minecraft:seagrass",
    "minecraft:reeds",
    "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",
    "minecraft:torch",
    "minecraft:soul_torch",
    "minecraft:water",
    "minecraft:flowing_water",
    "minecraft:lava",
    "minecraft:flowing_lava",
    "minecraft:fire",
    "minecraft:lever",
    "minecraft:tripwire_hook",
];
/**
 * @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 };
}
/**
 * @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 result = dimension.fillBlocks(min_location, max_location, 'minecraft:unknown', {matchingBlock: mc.BlockPermutation.resolve(block.block, block.properties)});
    // dimension.fillBlocks(min_location, max_location, mc.BlockPermutation.resolve(block.block, block.properties), {matchingBlock: mc.BlockPermutation.resolve('minecraft:unknown')});
    const result = dimension.runCommand(`fill ${new Vector3(min_location)} ${new Vector3(max_location)} minecraft:command_block [] replace ${block.block} ${block.properties ? JSON.stringify(block.properties).replace(/{/, '[').replace(/}/, ']') : '[]'}`).successCount;
    dimension.runCommand(`fill ${new Vector3(min_location)} ${new Vector3(max_location)} ${block.block} ${block.properties ? JSON.stringify(block.properties).replace(/{/, '[').replace(/}/, ']') : '[]'} replace minecraft:command_block`);
    return result;
}
/**
 * @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 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;
    }
}
