import { world, EquipmentSlot, EntityComponentTypes, system, BlockPermutation } from "@minecraft/server";
import { lightItemsData } from "../util/dynamicLightingData";
import { settingEnabled } from "../settings";

// Constants
const dynamicLightingDelayTicks = 1;
const LIGHT_BLOCK = "minecraft:light_block";
const FLOWING_WATER = "minecraft:flowing_water";
const WATER = "minecraft:water";
const AIR = "minecraft:air";
const placedLightblocks = [];
let onlinePlayers = world.getAllPlayers();

world.afterEvents.playerSpawn.subscribe((event) => {
    setOnlinePlayers();
});

world.afterEvents.playerLeave.subscribe((event) => {
    setOnlinePlayers();
});

function getOnlinePlayers() {
    return onlinePlayers;
}

function setOnlinePlayers() {
    onlinePlayers = world.getAllPlayers();
}

// Dynamic property enums
const dynamicPropertyEnums = Object.freeze({
    LIGHT_LEVEL: "lightLevel"
});

// Create a map to grab from data file
const lightItemsMap = new Map(
    lightItemsData.map(item => [item.id, {
        id: item.id,
        lightLevel: item.lightLevel,
        equipmentSlots: item.equipmentSlots,
        allowedBlocks: item.allowedBlocks
    }])
);

// Get players location
function getPlayerLocation(player) {
    try {
        const { x, y, z } = player.location;
        // Adjust y-coordinate if player is swimming
        return { x: Math.floor(x), y: Math.floor(y) + (player.isSwimming ? 0 : 1), z: Math.floor(z) };
    } catch {
        return null;
    }
}

// Check if item can be used
function canUseLightItem(player, allowedBlocks) {
    // Grab dimension
    const playerDimension = player.dimension.id;
    // Get players location
    const playerLocation = getPlayerLocation(player);

    if (!playerLocation) return { canUse: false, offset: 0 };

    // Positions to check: head level, one block above, and one block below
    const positions = [
        playerLocation,
        {...playerLocation, y: playerLocation.y + 1 },
        {...playerLocation, y: playerLocation.y - 1 }
    ];

    // Check each position for allowed blocks
    // Code is kind of horrible but it works
    for (let offset = 0; offset < positions.length; offset++) {
        const loc = positions[offset];
        const block = world.getDimension(playerDimension).getBlock(loc);
        const blockPermutation = block.permutation;

        if (blockPermutation.matches(LIGHT_BLOCK) || blockPermutation.matches(FLOWING_WATER)) {
            return { canUse: false, offset: 0 };
        }

        if (allowedBlocks.some(allowedBlock => blockPermutation.matches(allowedBlock))) {
            return { canUse: true, offset: offset - 1 };
        }
    }

    return { canUse: false, offset: 0 };
}

// Main function to get item info
function getItemInfo(player) {
    // 'Empty' Map for when player is not holding a valid item
    const canceledObject = {
        hasItem: false,
        lightLevel: 0,
        location: null,
        isLightBlock: false,
        offset: 0
    };

    if (!player) return canceledObject;

    // Grab component
    const component = player.getComponent(EntityComponentTypes.Equippable);
    if (!component) return canceledObject;

    const items = [];
    let offset = 0;

    // Check each equipment slot for a light item
    // This code makes me cry a little. It's efficient but also kind of gross
    Object.values(EquipmentSlot).forEach(slot => {
        const item = component.getEquipment(slot);
        if (item) {
            const lightItem = lightItemsMap.get(item.typeId);
            if (lightItem && lightItem.equipmentSlots.includes(slot)) {
                const checkUse = canUseLightItem(player, lightItem.allowedBlocks);
                if (checkUse.canUse) {
                    items.push(item);
                    offset = checkUse.offset;
                }
            }
        }
    });

    // Get players location
    const playerLocation = getPlayerLocation(player);
    if (!playerLocation) return canceledObject;

    // Determine the highest light level among equipped items
    const highestLightLevel = items.reduce((highest, item) => {
        const lightLevel = lightItemsMap.get(item.typeId).lightLevel;
        return lightLevel > highest ? lightLevel : highest;
    }, 0);

    // Map to put out
    return {
        hasItem: items.length > 0,
        lightLevel: highestLightLevel,
        location: {
            x: playerLocation.x,
            y: playerLocation.y + offset,
            z: playerLocation.z
        },
        isLightBlock: false,
        offset: offset
    };
}

// Set light block
function setLightBlock(player, itemInfo) {
    const { location, lightLevel } = itemInfo;
    const dimension = player.dimension.id;
    const block = world.getDimension(dimension).getBlock(location);
    const replacedBlock = block.permutation.matches(WATER) ? WATER : AIR;

    if (!block.isAir && !block.matches('minecraft:water')) return

    block.setPermutation(
        BlockPermutation.resolve(LIGHT_BLOCK, {
            block_light_level: lightLevel
        })
    );

    savePlacedLightBlocks(location, dimension, replacedBlock, lightLevel, itemInfo.offset, 5);
    saveLightLevel(player, lightLevel);
}

// Save placed light blocks
function savePlacedLightBlocks(location, dimension, replacedBlock, lightLevel, offset, delay) {
    const combinedData = {
        location: location,
        dimension: dimension,
        replacedBlock: replacedBlock,
        lightLevel: lightLevel,
        offset: offset,
        delay: delay,
    };
    hardSaveLightBlock(combinedData);
}

// Save light level
function saveLightLevel(player, lightLevel) {
    player.setDynamicProperty(dynamicPropertyEnums.LIGHT_LEVEL, lightLevel);
}

// Clean up light blocks
function cleanUpLightBlocks() {
    getHardSavedLightBlocks();
    if (placedLightblocks.length > 0) {
        // Create an array to store the indices of items to be removed
        let indicesToRemove = [];

        // Loop through each item in the array
        for (let i = 0; i < placedLightblocks.length; i++) {
            const item = placedLightblocks[i];
            const block = world.getDimension(item.dimension).getBlock(item.location);

            if (block == undefined) continue;

            if (item.delay > 0) {
                item.delay--;
                hardSaveLightBlock(item);
                continue;
            }

            let inUse = isLightBlockInUse(item);

            if (!inUse) {
                // Remove the block if it's not in use
                world
                    .getDimension(item.dimension)
                    .getBlock(item.location)
                    .setPermutation(BlockPermutation.resolve(item.replacedBlock));

                indicesToRemove.push(i); // Add the index to the removal list
                removeHardSavedLightBlock(item);
            }
        }

        // Remove items from the array in reverse order to avoid shifting indices
        for (let i = indicesToRemove.length - 1; i >= 0; i--) {
            placedLightblocks.splice(indicesToRemove[i], 1);
        }
    }
}

function hardSaveLightBlock(combinedData) {
    const combinedDataString = JSON.stringify(combinedData);
    const identifierString = `${combinedData.location.x}:${combinedData.location.y}:${combinedData.location.z}`;

    world.setDynamicProperty(`squaredreams_realism_${identifierString}`, combinedDataString);
}

function getHardSavedLightBlocks() {
    const allPropertyIDs = world.getDynamicPropertyIds();
    const savedBlocksStrings = [];

    if (allPropertyIDs.length == 0) return;

    for (const ID of allPropertyIDs) {
        if (ID.startsWith("squaredreams_realism_")) {
            const DP = world.getDynamicProperty(ID);
            savedBlocksStrings.push(DP);
        }
    }

    if (savedBlocksStrings.length == 0) return;

    placedLightblocks.length = 0;

    savedBlocksStrings.forEach((string) => {
        placedLightblocks.push(JSON.parse(string));
    });
}

function removeHardSavedLightBlock(combinedData) {
    const identifierString = `${combinedData.location.x}:${combinedData.location.y}:${combinedData.location.z}`;
    world.setDynamicProperty(`squaredreams_realism_${identifierString}`, undefined);
}

function isLightBlockInUse(placedLightblock) {
    let dimension = world.getDimension(placedLightblock.dimension);
    let inUse = false;
    let playersInDimension = dimension.getPlayers();
    let block = dimension.getBlock(placedLightblock.location);

    if (playersInDimension && playersInDimension.length > 0) {
        // The array is not empty; proceed with looping through each player
        for (const player of playersInDimension) {
            // Check if players in location is at the light location - 1y+
            const headLocation = getPlayerLocation(player);
            if (
                headLocation.x === placedLightblock.location.x &&
                headLocation.y + placedLightblock.offset === placedLightblock.location.y &&
                headLocation.z === placedLightblock.location.z
            ) {
                const item = getItemInfoForCleanup(player, placedLightblock);
                // Check if light level matches placed light level

                if (
                    getLightLevelData(player) === placedLightblock.lightLevel &&
                    item.hasItem
                ) {
                    inUse = true;
                    break; // Exit the loop
                }
            }
        }
    } else {
        inUse = true;
    }
    return inUse;
}

function getItemInfoForCleanup(player, placedLightblock) {
    // 'Empty' Map for when player is not holding a valid item
    const canceledObject = {
        hasItem: false,
        lightLevel: 0,
        location: null,
        isLightBlock: false,
    };

    if (!player) return canceledObject;

    // Grab component
    const component = player.getComponent(EntityComponentTypes.Equippable);
    if (!component) return canceledObject;

    const items = [];

    Object.values(EquipmentSlot).forEach(slot => {
        const item = component.getEquipment(slot);

        if (item && lightItemsMap.has(item.typeId)) {
            const lightItem = lightItemsMap.get(item.typeId);

            if (lightItem.equipmentSlots.includes(slot)) {
                // Check if item can be used in the block the player is in, i.e., air or water
                if (!canLightItemBeUsedInCleanUp(lightItem.allowedBlocks, placedLightblock.replacedBlock)) {
                    return canceledObject; // Exit early if item cannot be used
                }
                items.push(item);
            }
        }
    });

    function canLightItemBeUsedInCleanUp(allowedBlocks, replacedBlock) {
        // Loop through the allowedBlocks array
        for (const allowedBlock of allowedBlocks) {
            if (replacedBlock === allowedBlock) {
                return true; // Return true if a match is found
            }
        }

        return false; // Return false if no matches are found
    }

    // Get players location
    const playerLocation = getPlayerLocation(player);
    if (!playerLocation) return canceledObject;

    // Determine the highest light level among equipped items
    const lightLevel = items.reduce((highest, item) => {
        const lightLevel = lightItemsMap.get(item.typeId).lightLevel;
        return lightLevel > highest ? lightLevel : highest;
    }, 0);

    // Map to put out
    return {
        hasItem: items.length > 0,
        lightLevel: lightLevel,
        location: {
            x: playerLocation.x,
            y: playerLocation.y,
            z: playerLocation.z
        },
        isLightBlock: false
    };
}

function getLightLevelData(player) {
    return player.getDynamicProperty(dynamicPropertyEnums.LIGHT_LEVEL) || 0;
}

function runLight() {
    const players = getOnlinePlayers();

    // Check if players
    if (players && players.length > 0) {
        // Loop through each player
        players.forEach(function(player) {
            let itemInfo = getItemInfo(player);

            if (itemInfo.hasItem) {
                setLightBlock(player, itemInfo);
            }
        });
    }
}

// Main loop to check all players at regular intervals
system.runInterval(() => {
    // Return if not enabled
    if (!settingEnabled("dynamic_lighting")) return;
    // Set light blocks
    runLight();
    // Clean up
    cleanUpLightBlocks();
}, dynamicLightingDelayTicks);