import * as mc from "@minecraft/server";
import { Vector3 } from "./vector3";
import { mcerror, mclog } from "./log";
import { runCommand } from "./commands";
import { abortEvents, waitForEvent, waitForLoopEvent, waitForTicks, waitForWorldLoad } from "./events";
import { getScoreAsBool, setScore } from "./scores";
import { SafetyCheckedPlayer, getAllPlayersSafe, getEntitiesSafe } from "./entity";
import { overworld } from "./dimension";
import { clearDynamicProperty, readDynamicProperty, writeDynamicProperty } from "./properties";
import { GlobalNamespace } from "./namespace";
import { getRandomArrayEntry } from "./array";
import { ButtonMenu, MessageMenu, ModalMenu } from "./menu";
const DEFAULT_BIOME_FILTERS = {
    sand: {
        surface: [{ block: 'minecraft:sand', properties: { 'sand_type': 'normal' } }],
        replace: [
            { from: { block: 'minecraft:grass' }, to: { block: 'minecraft:sand' } },
            { from: { block: 'minecraft:dirt' }, to: { block: 'minecraft:sandstone' } },
            { from: { block: 'minecraft:tallgrass' }, to: { block: 'minecraft:air' } },
            { from: { block: 'minecraft:double_plant' }, to: { block: 'minecraft:air' } },
            { from: { block: 'minecraft:yellow_flower' }, to: { block: 'minecraft:air' } },
            { from: { block: 'minecraft:red_flower' }, to: { block: 'minecraft:air' } },
            { from: { block: 'minecraft:stone', properties: { 'stone_type': 'stone' } }, to: { block: 'minecraft:sandstone' } },
        ]
    },
    red_sand: {
        surface: [{ block: 'minecraft:sand', properties: { 'sand_type': 'red' } }],
        replace: [
            { from: { block: 'minecraft:grass' }, to: { block: 'minecraft:sand', properties: { 'sand_type': 'red' } } },
            { from: { block: 'minecraft:dirt' }, to: { block: 'minecraft:red_sandstone' } },
            { from: { block: 'minecraft:tallgrass' }, to: { block: 'minecraft:deadbush' } },
            { from: { block: 'minecraft:double_plant' }, to: { block: 'minecraft:deadbush' } },
            { from: { block: 'minecraft:yellow_flower' }, to: { block: 'minecraft:deadbush' } },
            { from: { block: 'minecraft:red_flower' }, to: { block: 'minecraft:deadbush' } },
            { from: { block: 'minecraft:stone', properties: { 'stone_type': 'stone' } }, to: { block: 'minecraft:red_sandstone' } },
        ]
    },
    mesa: {
        surface: [{ block: 'minecraft:hardened_clay' }],
        replace: [
            { from: { block: 'minecraft:grass' }, to: { block: 'minecraft:sand', properties: { 'sand_type': 'red' } } },
            { from: { block: 'minecraft:dirt' }, to: { block: 'minecraft:hardened_clay' } },
            { from: { block: 'minecraft:tallgrass' }, to: { block: 'minecraft:deadbush' } },
            { from: { block: 'minecraft:double_plant' }, to: { block: 'minecraft:deadbush' } },
            { from: { block: 'minecraft:yellow_flower' }, to: { block: 'minecraft:deadbush' } },
            { from: { block: 'minecraft:red_flower' }, to: { block: 'minecraft:deadbush' } },
            { from: { block: 'minecraft:stone', properties: { 'stone_type': 'stone' } }, to: { block: 'minecraft:hardened_clay' } },
        ]
    },
    snowy: {
        surface: [{ block: 'minecraft:snow_layer' }, { block: 'minecraft:snow' }],
        replace: [
            { from: { block: 'minecraft:grass' }, to: { block: 'minecraft:snow' } },
            { from: { block: 'minecraft:dirt' }, to: { block: 'minecraft:snow' } },
            { from: { block: 'minecraft:tallgrass' }, to: { block: 'minecraft:air' } },
            { from: { block: 'minecraft:double_plant' }, to: { block: 'minecraft:air' } },
            { from: { block: 'minecraft:yellow_flower' }, to: { block: 'minecraft:air' } },
            { from: { block: 'minecraft:red_flower' }, to: { block: 'minecraft:air' } },
        ]
    },
    ice: {
        surface: [{ block: 'minecraft:ice' }, { block: 'minecraft:packed_ice' }],
        replace: [
            { from: { block: 'minecraft:grass' }, to: { block: 'minecraft:ice' } },
            { from: { block: 'minecraft:dirt' }, to: { block: 'minecraft:packed_ice' } },
            { from: { block: 'minecraft:tallgrass' }, to: { block: 'minecraft:air' } },
            { from: { block: 'minecraft:double_plant' }, to: { block: 'minecraft:air' } },
            { from: { block: 'minecraft:yellow_flower' }, to: { block: 'minecraft:air' } },
            { from: { block: 'minecraft:red_flower' }, to: { block: 'minecraft:air' } },
            { from: { block: 'minecraft:stone', properties: { 'stone_type': 'stone' } }, to: { block: 'minecraft:packed_ice' } },
        ]
    }
};
export class Build {
    constructor(options) {
        var _a, _b, _c, _d;
        this.name = (_a = options.name) !== null && _a !== void 0 ? _a : "spawn_cell";
        this.floorOffset = (_b = options.floorOffset) !== null && _b !== void 0 ? _b : 1;
        this.cellCount = new Vector3((_c = options.cellCount) !== null && _c !== void 0 ? _c : 1);
        this.playerSpawnOffset = new Vector3((_d = options.playerSpawnOffset) !== null && _d !== void 0 ? _d : 0);
        this.blockFilters = options.blockFilters;
    }
    // #region Load
    async load(origin, options) {
        await waitForWorldLoad();
        mclog(`Loading Build ${this.name}`);
        if (!options.useOriginDirectly) {
            origin = this.getLoadOrigin(origin);
        }
        if (origin.y >= overworld.heightRange.max) {
            mcerror("Failed to find a valid build point.");
            return;
        }
        const filter = this.getBlockFilters(origin);
        origin.y -= this.floorOffset;
        for (let x = 1; x <= this.cellCount.x; x++) {
            for (let y = 1; y <= this.cellCount.y; y++) {
                for (let z = 1; z <= this.cellCount.z; z++) {
                    const cellIndex = new Vector3({ x, y, z });
                    const useIndex = cellIndex.subtract(new Vector3(1));
                    const cellLocation = origin.add(Build.CellSize.multiply(useIndex)).add(useIndex);
                    await this.loadCell(cellLocation, cellIndex, false, filter);
                }
            }
        }
        if (options.spawnBuild) {
            await waitForEvent(5, () => this.setWorldData(origin));
        }
        for (let x = 1; x <= this.cellCount.x; x++) {
            for (let z = 1; z <= this.cellCount.z; z++) {
                const cellIndex = new Vector3({ x, y: 0, z });
                const useIndex = cellIndex.subtract(new Vector3({ x: 1, y: 0, z: 1 }));
                const cellLocation = origin.add(Build.FoundationSize.multiply(useIndex)).add(useIndex);
                for (let y = origin.y - 1; y > 60; y--) {
                    cellLocation.y = y;
                    await this.loadCell(cellLocation, cellIndex, true, filter);
                }
            }
        }
        mclog(`Finished Loading Build ${this.name}`);
    }
    getLoadOrigin(origin) {
        const raycastOrigins = [
            new Vector3({ x: origin.x, y: Build.Ceiling, z: origin.z }),
            new Vector3({ x: origin.x + (Build.CellSize.x * this.cellCount.x), y: Build.Ceiling, z: origin.z }),
            new Vector3({ x: origin.x, y: Build.Ceiling, z: origin.z + (Build.CellSize.x * this.cellCount.x) }),
            new Vector3({ x: origin.x + (Build.CellSize.x * this.cellCount.x), y: Build.Ceiling, z: origin.z + (Build.CellSize.x * this.cellCount.x) }),
        ];
        origin.y = raycastOrigins.map(origin => this.performOriginRaycast(origin)).sort((a, b) => b - a)[0];
        return origin;
    }
    performOriginRaycast(raycastOrigin) {
        const blacklist = [
            "minecraft:oak_log",
            "minecraft:spruce_log",
            "minecraft:birch_log",
            "minecraft:jungle_log",
            "minecraft:acacia_log",
            "minecraft:mangrove_log",
            "minecraft:cherry_log",
            "minecraft:dark_oak_log",
            "minecraft:leaves",
            "minecraft:leaves2",
            "minecraft:mangrove_leaves",
            "minecraft:cherry_leaves",
            "minecraft:azalea_leaves",
            "minecraft:vine",
            "minecraft:red_mushroom_block",
            "minecraft:brown_mushroom_block",
        ];
        let hit = overworld.getBlockFromRay(raycastOrigin, Vector3.DOWN, { includeLiquidBlocks: true, maxDistance: 1000 });
        if (hit === null || hit === void 0 ? void 0 : hit.block) {
            while (blacklist.some(block => hit === null || hit === void 0 ? void 0 : hit.block.permutation.matches(block))) {
                const newHit = overworld.getBlockFromRay(new Vector3(hit.block.location).down(), Vector3.DOWN, { includeLiquidBlocks: true, maxDistance: 1000 });
                if (newHit === null || newHit === void 0 ? void 0 : newHit.block) {
                    hit = newHit;
                }
                else if (hit.block.location.x <= overworld.heightRange.min) {
                    return overworld.heightRange.max;
                }
            }
            return hit.block.location.y;
        }
        else {
            return overworld.heightRange.max;
        }
    }
    getBlockFilters(origin) {
        var _a;
        const block = overworld.getBlock(origin);
        if (!block)
            return this.blockFilters;
        for (const id of Object.keys(DEFAULT_BIOME_FILTERS)) {
            const filter = DEFAULT_BIOME_FILTERS[id];
            if (filter.surface.some(surface => block.permutation.matches(surface.block, surface.properties))) {
                mclog(`Biome Filter: ${id}`);
                const defaultFilters = (_a = this.blockFilters) !== null && _a !== void 0 ? _a : [];
                return [...filter.replace, ...defaultFilters];
            }
        }
        return this.blockFilters;
    }
    async loadCell(origin, index, foundation, filter) {
        const name = this.name + (foundation ? "_foundation" : "");
        const suffix = foundation ? `${index.x}_${index.z}` : `${index.x}_${index.y}_${index.z}`;
        const structureName = `${name}_${suffix}`;
        const size = foundation ? Build.FoundationSize : Build.CellSize;
        const end = origin.add(size);
        mclog(`Loading ${structureName}`);
        getEntitiesSafe(overworld, { excludeTypes: ["minecraft:player"] }).forEach(entity => {
            const shouldDelete = readDynamicProperty(entity, "shouldDeleteOnBuild");
            if (shouldDelete) {
                entity.remove();
            }
            else {
                clearDynamicProperty(entity, "shouldDeleteOnBuild");
            }
        });
        runCommand(`tickingarea add ${origin} ${end} build_loader`);
        await waitForTicks(0);
        mc.world.structureManager.place("mystructure:" + structureName, overworld, origin);
        await waitForTicks(0);
        runCommand(`tickingarea remove build_loader`);
        if (filter) {
            filter.forEach(option => {
                const fromOptions = option.from.properties ? JSON.stringify(option.from.properties).replace(':', '=').replace('{', '[').replace('}', ']') : '';
                const toOptions = option.to.properties ? JSON.stringify(option.to.properties).replace(':', '=').replace('{', '[').replace('}', ']') : '';
                overworld.runCommand(`fill ${origin} ${end} ${option.to.block} ${toOptions} replace ${option.from.block} ${fromOptions}`);
            });
        }
    }
    setWorldData(origin) {
        setScore(".var", Build.BuildObjective, 1);
        const spawnpoint = origin.add(this.playerSpawnOffset);
        getAllPlayersSafe().forEach(player => {
            player.teleport(spawnpoint);
            player.setSpawnPoint(Object.assign({ dimension: overworld }, spawnpoint));
        });
        mc.world.setDefaultSpawnLocation(spawnpoint);
        runCommand("gamerule spawnradius 0");
        writeDynamicProperty(mc.world, GlobalNamespace.Namespace + "spawn_build", { origin, options: {
                name: this.name,
                cellCount: this.cellCount,
                floorOffset: this.floorOffset,
                playerSpawnOffset: this.playerSpawnOffset,
            } });
    }
    // #endregion
    // #region Save
    async save(origin) {
        origin.y += 1;
        for (let x = 1; x <= this.cellCount.x; x++) {
            for (let y = 1; y <= this.cellCount.y; y++) {
                for (let z = 1; z <= this.cellCount.z; z++) {
                    const cellIndex = new Vector3({ x, y, z });
                    const useIndex = cellIndex.subtract(new Vector3(1));
                    const cellLocation = origin.add(Build.CellSize.multiply(useIndex)).add(useIndex);
                    await this.saveCell(cellLocation, cellIndex, false);
                }
            }
        }
        origin.y -= 1;
        for (let x = 1; x <= this.cellCount.x; x++) {
            for (let z = 1; z <= this.cellCount.z; z++) {
                const cellIndex = new Vector3({ x, y: 0, z });
                const useIndex = cellIndex.subtract(new Vector3({ x: 1, y: 0, z: 1 }));
                const cellLocation = origin.add(Build.FoundationSize.multiply(useIndex)).add(useIndex);
                await this.saveCell(cellLocation, cellIndex, true);
            }
        }
    }
    async saveCell(origin, index, foundation) {
        const name = this.name + (foundation ? "_foundation" : "");
        const suffix = foundation ? `${index.x}_${index.z}` : `${index.x}_${index.y}_${index.z}`;
        const structureName = `${name}_${suffix}`;
        const size = foundation ? Build.FoundationSize : Build.CellSize;
        const end = origin.add(size);
        runCommand(`tickingarea add ${origin} ${end} build_saver`);
        await waitForTicks(0);
        const entities = this.getEntitiesInCell(origin, foundation);
        if (entities.length) {
            getEntitiesSafe(overworld, { excludeTypes: ["minecraft:player"] }).forEach(entity => writeDynamicProperty(entity, "shouldDeleteOnBuild", true));
            entities.forEach(entity => writeDynamicProperty(entity, "shouldDeleteOnBuild", false));
        }
        runCommand(`structure save ${structureName} ${origin} ${end} ${entities.length > 0} disk`);
        await waitForTicks(0);
        runCommand(`tickingarea remove build_saver`);
        runCommand(`structure-export ${structureName}`);
        mclog(`Saving ${structureName} with ${entities.length} entities`);
    }
    // #endregion
    getEntitiesInCell(origin, foundation) {
        const offset = foundation ? Build.FoundationSize.add({ x: 1, y: 0, z: 1 }) : Build.CellSize.add(1);
        return getEntitiesSafe(overworld).filter(entity => entity.typeId !== "minecraft:player" && entity.location.inBounds(origin, origin.add(offset)));
    }
}
Build.CellSize = new Vector3(15);
Build.FoundationSize = new Vector3({ x: 15, y: 0, z: 15 });
Build.Ceiling = overworld.heightRange.max;
Build.BuildObjective = "HasBuiltSpawn";
Build.CachedBuilds = [];
/**
 * @remarks Creates the spawn build on the first world load. If the spawn build has already been created, this will not run again.
 * @param builds An array of build options. One build will randomly be selected to use as the spawn build.
 */
export async function buildOnFirstLoad(...builds) {
    await waitForWorldLoad();
    Build.CachedBuilds.push(...builds.map(option => new Build(option)));
    if (!getScoreAsBool(".var", Build.BuildObjective)) {
        const build = new Build(getRandomArrayEntry(Build.CachedBuilds));
        const origin = getAllPlayersSafe()[0].location;
        build.load(origin, { spawnBuild: true });
    }
    else {
        mclog("Already Built Spawn");
    }
}
// #region Build Helper
class BuildHelper {
    constructor(player) {
        this.buildOptions = { cellCount: new Vector3(1) };
        this.itemUseEvent = this.itemUse.bind(this);
        this.stopParticles = false;
        this.MainMenu = new ButtonMenu("Build Helper", [
            {
                text: "Save Menu",
                texture: "textures/ui/structure_block_save",
                onClick: (player) => {
                    this.SettingMenu.showMenuToPlayer(player);
                    const inventory = player.Inventory;
                    let item = new mc.ItemStack("minecraft:stick", 1);
                    item.nameTag = "Save Build";
                    item.setLore(["Use item on negative-most corner", "to select coordinate.", "Make sure to get the foundation,", "not the build."]);
                    item.lockMode = mc.ItemLockMode.slot;
                    inventory === null || inventory === void 0 ? void 0 : inventory.setItem(0, item);
                    item = new mc.ItemStack("minecraft:barrier", 1);
                    item.nameTag = "Cancel";
                    item.setLore(["Exit Build Helper"]);
                    item.lockMode = mc.ItemLockMode.slot;
                    inventory === null || inventory === void 0 ? void 0 : inventory.setItem(1, item);
                },
            },
            {
                text: "Load Menu",
                texture: "textures/ui/structure_block_load",
                onClick: (player) => {
                    this.SettingMenu.showMenuToPlayer(player);
                    const inventory = player.Inventory;
                    let item = new mc.ItemStack("minecraft:stick", 1);
                    item.nameTag = "Load Build";
                    item.setLore(["Use item on negative-most corner", "to select coordinate"]);
                    item.lockMode = mc.ItemLockMode.slot;
                    inventory === null || inventory === void 0 ? void 0 : inventory.setItem(0, item);
                    item = new mc.ItemStack("minecraft:barrier", 1);
                    item.nameTag = "Cancel";
                    item.setLore(["Exit Build Helper"]);
                    item.lockMode = mc.ItemLockMode.slot;
                    inventory === null || inventory === void 0 ? void 0 : inventory.setItem(1, item);
                },
            },
            {
                text: "Reset Build Score",
                texture: "textures/ui/structure_block_corner",
                onClick: (player) => {
                    setScore(".var", Build.BuildObjective, 0);
                },
            },
        ]);
        this.SettingMenu = new ModalMenu("Build Settings", [
            {
                type: "textField",
                text: "Build Name",
                placeholderText: "spawn_cell",
                defaultValue: "spawn_cell",
                onSubmit: (player, textFieldValue) => {
                    this.buildOptions.name = textFieldValue;
                },
            },
            {
                type: "slider",
                text: "X Cell Count",
                minimumValue: 1,
                maximumValue: 10,
                valueStep: 1,
                onSubmit: (player, sliderValue) => {
                    this.buildOptions.cellCount.x = sliderValue;
                },
            },
            {
                type: "slider",
                text: "Y Cell Count",
                minimumValue: 1,
                maximumValue: 10,
                valueStep: 1,
                onSubmit: (player, sliderValue) => {
                    this.buildOptions.cellCount.y = sliderValue;
                },
            },
            {
                type: "slider",
                text: "Z Cell Count",
                minimumValue: 1,
                maximumValue: 10,
                valueStep: 1,
                onSubmit: (player, sliderValue) => {
                    this.buildOptions.cellCount.z = sliderValue;
                },
            }
        ]);
        this.player = player;
        this.MainMenu.showMenuToPlayer(player);
        mc.world.afterEvents.itemUse.subscribe(this.itemUseEvent);
    }
    itemUse(event) {
        if (event.source.id === this.player.id && (event.itemStack.typeId === "minecraft:stick" || event.itemStack.typeId === "minecraft:barrier")) {
            const hit = event.source.getBlockFromViewDirection({ maxDistance: 8 });
            switch (event.itemStack.nameTag) {
                case "Save Build":
                    if (this.origin && !hit) {
                        this.confirmSaveOrLoad("Save Build Here?", true);
                    }
                    else if (hit) {
                        this.selectOrigin(new Vector3(hit.block.location));
                    }
                    break;
                case "Load Build":
                    if (this.origin && !hit) {
                        this.confirmSaveOrLoad("Load Build Here?", false);
                    }
                    else if (hit) {
                        this.selectOrigin(new Vector3(hit.block.location));
                    }
                    break;
                case "Cancel":
                    this.cleanup();
                    break;
            }
        }
    }
    selectOrigin(origin) {
        this.origin = origin;
        this.stopParticles = false;
        this.showParticles();
    }
    confirmSaveOrLoad(message, save) {
        if (!this.origin)
            return;
        this.stopParticles = true;
        new MessageMenu("Confirm", {
            confirm: {
                text: "Yes",
                onClick: (player) => {
                    if (save) {
                        new Build(this.buildOptions).save(this.origin);
                    }
                    else {
                        new Build(this.buildOptions).load(this.origin, { spawnBuild: false, useOriginDirectly: true });
                    }
                    this.cleanup();
                },
            },
            deny: {
                text: "No",
                onClick: (player) => {
                    this.origin = undefined;
                }
            }
        }, message).showMenuToPlayer(this.player);
    }
    showParticles() {
        const size = Build.CellSize.add(1).multiply(this.buildOptions.cellCount);
        waitForLoopEvent(20, () => {
            if (!this.origin)
                return true;
            for (let x = this.origin.x; x <= this.origin.x + size.x; x++) {
                overworld.spawnParticle('minecraft:endrod', { x, y: this.origin.y, z: this.origin.z });
            }
            for (let y = this.origin.y; y <= this.origin.y + size.y + 1; y++) {
                overworld.spawnParticle('minecraft:endrod', { x: this.origin.x, y, z: this.origin.z });
            }
            for (let z = this.origin.z; z <= this.origin.z + size.z; z++) {
                overworld.spawnParticle('minecraft:endrod', { x: this.origin.x, y: this.origin.y, z: z });
            }
            return this.stopParticles;
        }, "BuildHelper" + this.player.id);
    }
    cleanup() {
        mc.world.afterEvents.itemUse.unsubscribe(this.itemUseEvent);
        this.stopParticles = true;
        abortEvents("BuildHelper" + this.player.id);
        const inventory = this.player.Inventory;
        inventory === null || inventory === void 0 ? void 0 : inventory.setItem(0);
        inventory === null || inventory === void 0 ? void 0 : inventory.setItem(1);
        this.origin = undefined;
    }
}
mc.system.afterEvents.scriptEventReceive.subscribe(event => {
    var _a;
    if (event.sourceEntity && event.id.includes("ldz:")) {
        const player = new SafetyCheckedPlayer(event.sourceEntity);
        switch (event.id) {
            case "ldz:build":
                new BuildHelper(player);
                break;
            case "ldz:load_build":
                if (event.message !== "") {
                    const loadBuild = Build.CachedBuilds.find(build => build.name === event.message);
                    if (loadBuild) {
                        loadBuild.load(player.location, { spawnBuild: true });
                    }
                    else {
                        player.sendMessage(`§cCouldn't find build with name ${event.message}`);
                        player.sendMessage(`Valid options are: ${Build.CachedBuilds.map(build => build.name)}`);
                    }
                }
                else {
                    const build = (_a = getRandomArrayEntry(Build.CachedBuilds)) !== null && _a !== void 0 ? _a : new Build({});
                    build.load(player.location, { spawnBuild: true });
                }
                break;
            case "ldz:save_build":
                const saveOptions = event.message === "" ? undefined : JSON.parse(event.message);
                new Build(saveOptions !== null && saveOptions !== void 0 ? saveOptions : {}).save(player.location);
                break;
            default:
                if (event.id.includes("build")) {
                    player.sendMessage(`§c${event.id} is not a valid command. Did you mean:`);
                    player.sendMessage(`§9ldz:build§r (Opens the build helper menu)`);
                    player.sendMessage(`§9ldz:load_build§r (Loads a random build that was used in "buildOnFirstLoad", or a specific build)`);
                    player.sendMessage(`§9ldz:save_build§r (Saves a build using your location as the foundation origin)`);
                }
                break;
        }
    }
});
// #endregion
