import {
    Block,
    BlockPermutation,
    Dimension,
    ItemComponent,
    system,
    world,
} from "@minecraft/server";

const DETECTOR_LISTENERS = [];

const DIMEN_HEIGHT = {
    overworld: -63,
    nether: 0,
    the_end: 0,
};

function LOG(...data) {
    //console.error("§bError", "§r", ...data)
}

const DIMEN_FILTERS = {
    overworld: [
        "minecraft:deadbush",
        "minecraft:seagrass",
        "minecraft:short_grass",
        "minecraft:tall_grass",
        "minecraft:dirt",
        "minecraft:ice",
        "minecraft:slown_layer",
        "minecraft:packed_ice",
        "minecraft:slown",
        "minecraft:stone",
        "minecraft:grass",
        "minecraft:grass_block",
        "minecraft:sand",
        "minecraft:gravel",
        "minecraft:acacia_leaves",
        "minecraft:acacia_log",
        "minecraft:oak_leaves",
        "minecraft:oak_log",
        "minecraft:blackstone",
        "minecraft:snow_layer",
        "minecraft:water",
    ],
    nether: [
        "minecraft:crimson_nylium",
        "minecraft:crimson_stem",
        "minecraft:basalt",
        "minecraft:crimson_wart_block",
        "minecraft:crimson_roots",

        "minecraft:warped_nylium",
        "minecraft:warped_stem",
        "minecraft:warped_wart_block",
        "minecraft:warped_funguns",

        "minecraft:sandstone",
        "minecraft:magma",
        "minecraft:soul_sand",
        "minecraft:soul_soil",

        "minecraft:brown_mushroom",
        "minecraft:quartz_ore",
        "minecraft:magma_block",
        "minecraft:netherrack",
        "minecraft:lava",
    ],
    the_end: [],
    global: ["minecraft:air"],
};

function arrayFromArrays(...lists) {
    const data = [];
    lists.forEach((list) => {
        list.forEach((v) => {
            data.push(v);
        });
    });
    return data;
}

/** @interface */
class BlendOptions {
    offsetY = 0;
    alignGround = true;
    airBlocks = ["minecraft:bedrock"];
    /**
     * @param {BlockPermutation} permutation
     * @param {Block} block
     */
    setter(permutation, block) {
        block.setPermutation(permutation);
    }
}

/** @interface */
class SpawnStructureOptions {
    id = "none";
    biomeTags = [];
    aboveBlock = null;
    chance = 100;
    blendOptions = new BlendOptions();
}

export default class ChunkerGenerator {
    static CHECKER_BLOCK_ID = "exp_mod:checker";
    static DETECTOR_ENTITY_ID = "exp_mod:detector";
    static addDetectorListener(callback) {
        DETECTOR_LISTENERS.push(callback);
    }

    static addByPassBlocks(dimen, ...list) {
        DIMEN_FILTERS[dimen] = arrayFromArrays(DIMEN_FILTERS[dimen] ?? [], list);
    }

    /** @param {SpawnStructureOptions} opts  */
    static insertSpawner(opts) {
        opts = Object.assign(new SpawnStructureOptions(), opts);
        ChunkerGenerator.addDetectorListener((tags, dimension, locationXZ) => {
            let biomeCheck = true;
            for (let tag of opts.biomeTags) {
                biomeCheck = biomeCheck & tags.includes(tag);
            }

            LOG("OK: " + opts.id);
            if (Math.random() * 100 > opts.chance || !biomeCheck) return;

            let upBlock = SURFACE_PROCESSOR[dimension.id.split(":")[1]](dimension, {
                x: locationXZ.x + Math.random() * 16,
                z: locationXZ.z + +(Math.random() * 16),
            });

            if (upBlock.above().isAir) {
                if (
                    (opts.aboveBlock != null && opts.aboveBlock == upBlock.typeId) ||
                    opts.aboveBlock == null
                ) {
                    ChunkerGenerator.blendStructure(
                        upBlock.dimension,
                        upBlock.above().location,
                        opts.id,
                        opts.blendOptions
                    );
                }
            }
        });
    }

    /**
     * @param {Dimension} dimension
     * @param {import("@minecraft/server").Vector3} location
     * @param {string} strucutrId
     * @param {BlendOptions} opts
     */
    static blendStructure(dimension, location, strucutrId, opts = new BlendOptions()) {
        opts = Object.assign(new BlendOptions(), opts);
        const strcutre = world.structureManager.get(strucutrId);
        const data = {};
        const size = strcutre.size;
        const basePosition = {
            x: location.x - size.x / 2,
            y: location.y,
            z: location.z - size.z / 2,
        };

        if (opts.alignGround) {
            // Align Strcutre to Ground
            for (let p = 0; p < 16; p++) {
                let air = false;
                for (let x = 0; !air && x < size.x; x++) {
                    for (let y = 0; y < Math.max(opts.offsetY, 1); y++) {
                        for (let z = 0; !air && z < size.z; z++) {
                            const target = strcutre.getBlockPermutation({ x, z, y: 0 });
                            if (target.type.id == "minecraft:air") continue;
                            const block = dimension
                                .getBlock({
                                    x: basePosition.x + x,
                                    y: basePosition.y + opts.offsetY,
                                    z: basePosition.z,
                                })
                                ?.below();
                            if (block && block.isAir) {
                                air = true;
                                break;
                            }
                        }
                    }
                }
                if (air) {
                    basePosition.y--;
                    if (p == 15) {
                        LOG("CANNOT LOCK ON GROUND");
                        return false;
                    }
                } else {
                    break;
                }
            }
        }
        const byPassBlocks = arrayFromArrays(
            DIMEN_FILTERS[dimension.id.split(":")[1].toLowerCase()] ?? [],
            DIMEN_FILTERS["global"]
        );

        for (let x = 0; x < size.x; x++) {
            for (let z = 0; z < size.z; z++) {
                for (let y = 0; y < size.y; y++) {
                    const worldPos = {
                        x: basePosition.x + x,
                        y: basePosition.y + opts.offsetY + y,
                        z: basePosition.z + z,
                    };
                    let target = strcutre.getBlockPermutation({ x, y, z });
                    if (target.type.id == "minecraft:air") continue;
                    const currentBlock = dimension.getBlock(worldPos);
                    if (
                        currentBlock &&
                        currentBlock.isValid() &&
                        byPassBlocks.includes(currentBlock.typeId)
                    ) {
                        if (opts.airBlocks.includes(target.type.id)) {
                            target = BlockPermutation.resolve("minecraft:air");
                        } else if (
                            target.type.id.includes("leaves") &&
                            "persistent_bit" in target.getAllStates()
                        ) {
                            target = target.withState("persistent_bit", true);
                        }
                        data[x + ":" + y + ":" + z] = [currentBlock, target];
                    } else {
                        // Broken case find block
                        LOG("CANNOT PASS BLOCK", currentBlock, currentBlock?.typeId);
                        return false;
                    }
                }
            }
        }
        LOG("PLACED");
        Object.values(data).forEach((values) => opts.setter(values[1], values[0]));
        return true;
    }
}

/**
 * @param {Dimension} dimension
 * @param {import("@minecraft/server").Vector3} location
 * @param {*} callback
 */
function detectBiomeTags(dimension, location, callback) {
    const biomeDetector = dimension.spawnEntity(ChunkerGenerator.DETECTOR_ENTITY_ID, {
        x: Math.floor(location.x / 16.0) * 16,
        y: location.y,
        z: Math.floor(location.z / 16.0) * 16,
    });
    system.runTimeout(() => {
        let tags = [];
        biomeDetector
            .getTags()
            .forEach((tag) =>
                tag.startsWith("biome_data:") ? tags.push(tag.substring(11)) : null
            );
        biomeDetector.remove();
        callback(tags, location);
    }, 1);
}

const SURFACE_PROCESSOR = {
    /** @param {Dimension} dimension */
    nether: (dimension, xz) => {
        return dimension.getTopmostBlock({ x: xz.x, z: xz.z }, 1);
    },
    the_end: (dimension, xz) => {
        return dimension.getTopmostBlock({ x: xz.x, z: xz.z }, 16);
    },
    /** @param {Dimension} dimension */
    overworld: (dimension, xz) => {
        let block = dimension.getTopmostBlock({ x: xz.x, z: xz.z }, 50);
        let oldBock = block;
        let cy = -1;
        let y = 70;
        while (block && cy < block.y) {
            oldBock = block;
            cy = block.location.y;
            block = dimension.getTopmostBlock({ x: xz.x, z: xz.z }, y);
            y += 10;
        }
        while (oldBock && !oldBock.above().isAir) {
            oldBock = oldBock.above();
        }
        return oldBock;
    },
};

/**
 * @param {Dimension} dimension
 * @param {import("@minecraft/server").Vector3} location
 */
function processChunk(dimension, location) {
    const chunkPos = {
        x: Math.floor(location.x / 16.0) * 16,
        y: DIMEN_HEIGHT[dimension.id.split(":")[1].toLowerCase()] ?? -63,
        z: Math.floor(location.z / 16.0) * 16,
    };
    const pos = dimension.getBlock(chunkPos);
    if (pos.typeId != ChunkerGenerator.CHECKER_BLOCK_ID) {
        pos.setType(ChunkerGenerator.CHECKER_BLOCK_ID);
        detectBiomeTags(pos.dimension, location, (tags) => {
            LOG("List Callers", DETECTOR_LISTENERS.length);
            DETECTOR_LISTENERS.forEach((event) => {
                try {
                    let i = Date.now();
                    event(tags, pos.dimension, { x: pos.location.x, z: pos.location.z });
                    LOG("Date", Date.now() - i);
                } catch (e) {
                    LOG("Error on call listener", e, e.stack);
                }
            });
        });
    }
}

system.runInterval(() => {
    world.getAllPlayers().forEach((player) => {
        for (let x = -4; x < 4; x++) {
            for (let z = -4; z < 4; z++) {
                try {
                    const loc = {
                        x: player.location.x + x * 16,
                        z: player.location.z + z * 16,
                        y: player.location.y,
                    };
                    processChunk(player.dimension, loc);
                } catch (e) {
                    //LOG("ERROR ON PROCESS", e, e.stack)
                }
            }
        }
    });
});
