import { Block, BlockPermutation, MinecraftDimensionTypes, system, world } from '@minecraft/server';  
import { ModalFormData, ActionFormData } from '@minecraft/server-ui';
import Area_Names from "../Area_Names.js";

let globalWaypoints = [];
let distanceDivide;
let waystoneConsumeXP;

// Initialize waystone blocks custom components
world.beforeEvents.worldInitialize.subscribe((initEvent)=>{
    loadConfiguration()
    initEvent.blockComponentRegistry.registerCustomComponent('sw:control', {
        onPlace: event => {
            const {block} = event
            if(block.typeId == "sw:waystone"){
                const location = block.location
                const dimension = block.dimension
                const topBlock = dimension.getBlock({x:location.x, y:location.y+1, z:location.z})
                topBlock.setPermutation(BlockPermutation.resolve("sw:waystone_top"))
                block.setPermutation(BlockPermutation.resolve("sw:waystone_bottom"))
            }else if(block.typeId.includes("waystone") || block.typeId.includes("sw:")){
                //variant waystone
                const waystoneVariant = getWaystoneVariant(block)
                const variantBlockId = waystoneVariant.variantBlockId
                if(waystoneVariant.isVariant){
                    const location = block.location
                    const dimension = block.dimension
                    const topBlock = dimension.getBlock({x:location.x, y:location.y+1, z:location.z})
                    topBlock.setPermutation(BlockPermutation.resolve(`${variantBlockId}_top`))
                    block.setPermutation(BlockPermutation.resolve(`${variantBlockId}_bottom`))
                }
            }
        },
        onPlayerInteract: event => {
            const {player, block} = event
            if(block.typeId.includes("bottom") || block.typeId.includes("top")){
                handleWaypointInteraction(player, block)
            }
        }
    })
})

//Load waypoint configuration
function loadConfiguration(){
    distanceDivide = world.getDynamicProperty("distanceDivide")
    waystoneConsumeXP = world.getDynamicProperty("waystoneConsumeXP")
    if(distanceDivide == undefined){
        world.setDynamicProperty("distanceDivide", 1000)
        distanceDivide = 1000
    }
    if(waystoneConsumeXP == undefined){
        world.setDynamicProperty("waystoneConsumeXP", true)
        waystoneConsumeXP = true
    }
}

//reset config
function resetConfig(player = undefined){
    world.setDynamicProperty("distanceDivide", undefined)
    world.setDynamicProperty("waystoneConsumeXP", undefined)
    loadConfiguration()
    if(player){
        player.sendMessage("§a" + "[Waypoint Config] Reset Success.")
    }else{
        world.sendMessage("§a" + "[Waypoint Config] Reset Success.")
    }
}

// Initialize Waypoints
system.runTimeout(() => {
    const waypointData = world.getDynamicProperty("waypoints");
    if (waypointData) {
        globalWaypoints = JSON.parse(waypointData);
    }
    console.warn(`§e[Simple Waystone] Addon Active`)
}, 40);

// Set input permissions for newly joined players
world.afterEvents.playerJoin.subscribe(({ playerName }) => {
    const dimensions = [MinecraftDimensionTypes.overworld, MinecraftDimensionTypes.nether, MinecraftDimensionTypes.theEnd];
    for (const dimension of dimensions) {
        const player = world.getDimension(dimension).getEntities({ name: playerName })[0];
        if (!player) continue;
        system.runTimeout(() => {
            playerResetPermissions(player)
        }, 20);
    }
});

//Handle waypoint breaking
world.beforeEvents.playerBreakBlock.subscribe((eventData)=>{
    const {block, player, dimension} = eventData
    const brokenBlockPermutation = block.permutation
    const {location} = block
    const blockName = brokenBlockPermutation.type.id;

    if (!blockName.match(/(sw:\w+_waystone)|(sw:waystone)/g)) return;

    const newLocation = { x: location.x, y: location.y, z: location.z };
    const index = findWaypointIndex(blockName, newLocation);
    if (index === -1){
        system.run(()=>{
            updateWaypointsAfterBreak(blockName, location, dimension);
        })
        return
    }

    const waystone = globalWaypoints[index]

    if(waystone?.private){
        if(waystone.owner != player.id){
            player.sendMessage("§cUnable to break private waystone you don't own.");
            eventData.cancel = true
        }else{
            system.run(()=>{
                updateWaypointsAfterBreak(blockName, location, dimension);
                globalWaypoints.splice(index, 1);
                updateWorldWaypoints();
            })   
        }    
    }else{
        system.run(()=>{
            updateWaypointsAfterBreak(blockName, location, dimension);
            globalWaypoints.splice(index, 1);
            updateWorldWaypoints();
        })   
    }
})

// Handle Gate Token Use
world.afterEvents.itemUse.subscribe(({source : player, itemStack}) => { 
    if(itemStack.typeId == "sw:gate_token"){
        const playerLocation = player.location
        handleWaypointMenu(player,{name:"Gate Token"},playerLocation, true)
    }
})

/*
system.afterEvents.scriptEventReceive.subscribe(({id, sourceEntity, message})=>{
    const idArr = id.split(":")
    const cmd = `!${idArr[0]} ${idArr[1]} ${message}`
    command(cmd, sourceEntity)
});
*/

//Retrieve waystone variant
/**
 * 
 * @param {Block} block 
 * @returns {Object}
 */
function getWaystoneVariant(block){
    const variantType = block.typeId.replace(/(sw:)|(_waystone)/g,"")
    const variantBlockId = `sw:${variantType}_waystone`
    const isVariant = block.typeId == variantBlockId ? true : false
    return {isVariant, variantType, variantBlockId}
}

// Handle Addon Commands
function command(message, player){
    const playerMsg = message.toLowerCase()
    const msgArgs = playerMsg.split(" ")
    if(msgArgs[0].includes("!sw")){
        switch(msgArgs[1]){
            case "delete":
                player.setDynamicProperty("waypoints", "[]");
                player.sendMessage(`§e[Simple Waystone] Cleared Personal Waypoints.`)
                break;
            
            case "config":
                showWaypointConfigMenu(player)
                break;
            case "cfg":
                showWaypointConfigMenu(player)
                break;

            case "configreset":
                resetConfig(player)
                break;
            case "cfgreset":
                resetConfig(player)
                break;

            case "remove":
                showWaypointDeleteMenu(player)
                break;


            case "diagnose":
                switch(msgArgs[2]){
                    case "world":
                        player.sendMessage(`§eTotal waypoints in world: ${globalWaypoints.length}, World Property Character Limit: ${JSON.stringify(globalWaypoints).length}/32767`)
                        for(let wpEntry of JSON.parse(world.getDynamicProperty("waypoints"))){
                            player.sendMessage("§a" + JSON.stringify(wpEntry))
                        }
                        break;
                    case "me":
                        player.sendMessage(`§eTotal waypoints registered: ${JSON.parse(player.getDynamicProperty("waypoints")).length}, Own Property Character Limit: ${player.getDynamicProperty("waypoints").length}/32767`)
                        for(let wpEntry of JSON.parse(player.getDynamicProperty("waypoints"))){
                            player.sendMessage("§a" + JSON.stringify(wpEntry))
                        }
                        break;
                    default:
                }
                break;
            default:
                const message = `§a<Simple Waystone Commands>
                                o /scriptevent sw:delete - Deletes all waystones.
                                o /scriptevent sw:remove - Delete selected waystone.                              
                                o /scriptevent sw:config - Edit waypoint configuration.
                                o /scriptevent sw:configreset - Reset waypoint configuration.
                                o /scriptevent sw:diagnose <world | me> - shows total bits occupied.
                                `;

                const formattedMessage = message.split('\n').map(line => line.trim()).join('\n');
                player.sendMessage(formattedMessage);                  
        }
    }
}

function updateWaypointsAfterBreak(blockName, location, dimension) {
    const air = BlockPermutation.resolve("minecraft:air");
    if (blockName.includes("bot")) {
        dimension.getBlock(location).setPermutation(air);
        dimension.getBlock({ x: location.x, y: location.y + 1, z: location.z }).setPermutation(air);
    } else {
        dimension.getBlock(location).setPermutation(air);
        dimension.getBlock({ x: location.x, y: location.y - 1, z: location.z }).setPermutation(air);
    }
}

function findWaypointIndex(blockName, location) {
    return globalWaypoints.findIndex((wp) => {
        const wpLocation = blockName.includes("bot") ? wp.locationBot : wp.locationTop;
        return wpLocation.x === location.x && wpLocation.y === location.y && wpLocation.z === location.z;
    });
}

function updateWorldWaypoints() {
    const waypointString = JSON.stringify(globalWaypoints);
    world.setDynamicProperty("waypoints", waypointString);
    globalWaypoints = JSON.parse(world.getDynamicProperty("waypoints"));
}

function handleWaypointInteraction(player, block = null) {
    const waypointBlock = block;
    if(!waypointBlock) return;
    const waypointLocation = waypointBlock.location;
    const waypointBlockName = waypointBlock.typeId;

    processWaypointInteraction(player, waypointBlockName, waypointLocation);
}

function processWaypointInteraction(player, blockName, location) {
    for (const wp of globalWaypoints) {
        const oldLocation = blockName.includes("bot") ? wp.locationBot : wp.locationTop;
        if (JSON.stringify(oldLocation) === JSON.stringify(location)) {
            handleWaypointMenu(player, wp, location);
            return;
        }

        if (getDistance(oldLocation, location) <= 2) {
            player.sendMessage("Waypoint can't be near to other waypoints");
            return;
        }
    }

    showWaypointSetupForm(player, blockName, location);
}

async function showWaypointConfigMenu(player){
    const configMenu = new ModalFormData()
    configMenu.title("Configure Waypoins")
    configMenu.textField("XP Math (Higher the Number lesser the waypoint XP):", "", distanceDivide.toString())
    configMenu.dropdown("Waypoint Consume XP:",["True","False"], waystoneConsumeXP?0:1)
    configMenu.submitButton("Save Config")
    await configMenu.show(player).then(({canceled,formValues})=>{
        if(canceled) return
        const values = formValues;
        const textFieldValue = values[0].trim().replace(" ", "");
        const dropDownValue = values[1] == 0 ? true : false;
        if (!isNaN(textFieldValue) && textFieldValue !== "") {
            const numValue = parseInt(textFieldValue);
            if (numValue <= 0) {
                world.setDynamicProperty("distanceDivide", 1);
            } else {
                world.setDynamicProperty("distanceDivide", numValue);
            }
        } else {
            player.sendMessage("§c[Waypoint Config] XP Math invalid value, skipping.");
        }
        world.setDynamicProperty("waystoneConsumeXP", dropDownValue);
        loadConfiguration()
        player.sendMessage(`§a[Waypoint Config] Configuration Saved.`)
    }).catch((error)=>{
        console.warn(`[Waypoint] Error while saving config. ${error}`)
    })
}

async function showWaypointDeleteMenu(player) {
    const waypointList = new ActionFormData()
        .title("Delete Waypoints");
    for(const waypoint of globalWaypoints){
        if(Array.isArray(waypoint.name)){
            waypointList.button(waypoint.name[0], "textures/blocks/barrier")
        }else{
            waypointList.button(waypoint.name, "textures/blocks/barrier")
        }
    }
    await waypointList.show(player).then(({selection, canceled}) => {
        if(canceled) return
        const selectedWaypoint = globalWaypoints[selection]
        const waypointID = selectedWaypoint.id
        globalWaypoints = globalWaypoints.filter((waypoint) => waypoint.id != waypointID)
        player.sendMessage(`§eSuccessfully removed waypoint ${selectedWaypoint.name}.`)
        updateWorldWaypoints()
    }).catch((error) => {
        console.warn("[Simple Waystone] " + error)
        player.sendMessage(`§cSomething went wrong while deleting the waypoint.`)
    })
}

function handleWaypointMenu(player, wp, location, itemToken = false) {
    let playerWaypoints = getPlayerWaypoints(player);    
    if(!itemToken){
        const waypointRegistration = isWaypointRegistered(playerWaypoints, wp)
        if(wp?.private){
            if(wp.owner != player.id){
                player.sendMessage("§cThis waypoint is private!");
                return
            }         
        }
        if (!waypointRegistration) {
            registerWaypoint(player, wp);
            playerWaypoints = getPlayerWaypoints(player); //Update playWaypoints after register
            player.sendMessage("§eWaypoint Registered!");
            player.playSound("random.levelup");
        }
    }

    validatePlayerWaypoints(player, playerWaypoints);
    playerWaypoints = getPlayerWaypoints(player); //Update playerWaypoints after validate
    showWaypointListForm(player, wp, playerWaypoints, location);
}

function getPlayerWaypoints(player) {
    const publicWaypoints = getPublicWaypoints()
    const playerWaypointData = player.getDynamicProperty("waypoints");
    if (playerWaypointData) {
        return JSON.parse(playerWaypointData).concat(publicWaypoints);
    } else {
        player.setDynamicProperty("waypoints", "[]");
        return [];
    }
}

function getPublicWaypoints(){
    const publicWaypoints = globalWaypoints.filter((waypointData) => {
        const isPublic = waypointData.public
        if(isPublic){
            return true
        }
        return false
    })
    return publicWaypoints
}

function isWaypointRegistered(playerWaypoints, waypoint) {
    return playerWaypoints.some(wp => JSON.stringify(wp.id) === JSON.stringify(waypoint.id));
}

function isWaypointPrivate(playerWaypoints, waypoint){
    return globalWaypoints
}

function registerWaypoint(player, waypoint) {
    const playerWaypoints = getPlayerWaypoints(player);
    playerWaypoints.push(waypoint);
    try {
        player.setDynamicProperty("waypoints", JSON.stringify(playerWaypoints));
    } catch (e) {
        player.sendMessage(`§cMaximum waystone reached! (Current Waystones:${playerWaypoints.length})`);
        playerWaypoints.pop();
    }
}

function validatePlayerWaypoints(player, playerWaypoints) {
    const validWaypoints = playerWaypoints.filter(wp => {
        if(wp.public) return false
        return globalWaypoints.some(globalWp => JSON.stringify(wp.id) === JSON.stringify(globalWp.id));
    });
    player.setDynamicProperty("waypoints", JSON.stringify(validWaypoints));
}

function showWaypointListForm(player, currentWaypoint, playerWaypoints, location) {
    system.runTimeout(() => {
        const waypointList = new ActionFormData()
            .title("Waypoint List")
            .body(`Current Waypoint: ${currentWaypoint.name}`);
        const xpNeeded = getXpNeededList(player, currentWaypoint, playerWaypoints, location, waypointList);

        waypointList.show(player).then(response => {
            if (response && response.selection !== undefined) {
                handleWaypointSelection(player, xpNeeded, response.selection, playerWaypoints, location);
            }
        }).catch(error => {
            player.sendMessage(`Error showing waypoint list: ${error.message}`);
        });
    }, 1);
}

function getXpNeededList(player, currentWaypoint, playerWaypoints, location, waypointList) {
    const xpNeeded = [];
    for (const wp of playerWaypoints) {
        if (JSON.stringify(currentWaypoint.id) === JSON.stringify(wp.id)) {
            addButtonForCurrentWaypoint(waypointList, wp);
            xpNeeded.push(0);
        } else {
            const xpRequirement = calculateXpRequirement(wp, location);
            addButtonForOtherWaypoint(waypointList, wp, xpRequirement);
            xpNeeded.push(xpRequirement);
        }
    }
    return xpNeeded;
}

function addButtonForCurrentWaypoint(waypointList, waypoint) {
    const buttonText = `§e§l>> §r${waypoint.name} §l§e<<\nYour Here`;
    waypointList.button(buttonText);
}

function calculateXpRequirement(waypoint, location) {
    const distance = getDistance(waypoint.locationTop, location);
    if(waystoneConsumeXP){
        return Math.min(Math.floor(distance / distanceDivide), 16);
    }else{
        return 0;
    }
}

function addButtonForOtherWaypoint(waypointList, waypoint, xpRequirement) {
    const dimensionColor = getDimensionColor(waypoint.dimension);
    const buttonText = `§r${dimensionColor}${waypoint.name} §2§l${xpRequirement}`;
    switch(waypoint.dimension){
        case "overworld":
            waypointList.button(buttonText, "textures/waystone_ui_image/overworld");
        break;
        
        case "nether":
            waypointList.button(buttonText, "textures/waystone_ui_image/nether");
        break;

        case "the_end":
            waypointList.button(buttonText, "textures/waystone_ui_image/end");
        break;

        default:
            waypointList.button(buttonText);
        break
    }
    
}

function getDimensionColor(dimension) {
    switch (dimension) {
        case "":
            return "";
        case "nether":
            return "§4";
        case "the_end":
            return "§5";
        default:
            return "";
    }
}

function handleWaypointSelection(player, xpNeeded, selection, playerWaypoints, location) {
    if (xpNeeded[selection] <= player.level) {
        teleportPlayerToWaypoint(player, playerWaypoints[selection], xpNeeded[selection], location);
    } else {
        player.sendMessage("§cNot Enough Levels to teleport.");
    }
}

function teleportPlayerToWaypoint(player, waypoint, xpRequired, location) {
    const waypointDimension = world.getDimension(waypoint.dimension)
    const targetLocation = waypoint.locationBot;
    const tpDelay = xpRequired > 7 ? 150 : 80;
    if (!targetLocation) return player.sendMessage("§cWaypoint is Obstructed.");
    const {x,y,z} = targetLocation

	player.addLevels(-xpRequired);
	player.playSound("mob.shulker.teleport");
	let removedToken = removeGateToken(player)        
	system.runTimeout(() => {
		playerResetPermissions(player);
		player.teleport(targetLocation, { dimension: waypointDimension });
	}, removedToken ? 19 : 8);
}

function removeGateToken(player){
    const playerContainer = player.getComponent("inventory").container;
    const itemStack = playerContainer.getItem(player.selectedSlotIndex);
    if(itemStack?.typeId == "sw:gate_token"){
        if(itemStack.amount == 1){
            player.getComponent("inventory").container.setItem(player.selectedSlotIndex, undefined)
        }else{
            itemStack.amount -= 1
            player.getComponent("inventory").container.setItem(player.selectedSlotIndex, itemStack)
        }
        player.runCommandAsync(`inputpermission set @s camera disabled`)
        player.runCommandAsync(`inputpermission set @s movement disabled`)
        player.runCommandAsync(`execute as @s at @s rotated as @s run summon sw:token ~~0.65~ ~ ~`)
        return true
    }
    return false
}

function playerResetPermissions(player){
    player.runCommandAsync(`inputpermission set @s camera enabled`)  
    player.runCommandAsync(`inputpermission set @s movement enabled`)
}

function showWaypointSetupForm(player, blockName, location) {
    const setupForm = new ModalFormData()
        .title("Waystone")
        .textField("Waystone Name", "Name", Area_Names.generateName(globalWaypoints))
        .toggle("Global Waypoint:",false)
        .toggle("Private Waypoint:", false);
    setupForm.show(player).then(formData => {
        if (formData.canceled) return;

        const waypointName = formData.formValues[0];
        const waypointPublicToggle = formData.formValues[1];
        const waypointPrivateToggle = formData.formValues[2];
        if (blockName.includes("bot")) {
            setBottomWaypoint(player, blockName, location, waypointName, waypointPublicToggle, waypointPrivateToggle);
        } else {
            setTopWaypoint(player, blockName, location, waypointName, waypointPublicToggle, waypointPrivateToggle);
        }
    }).catch(error => {
        player.sendMessage(`Error showing waypoint setup form: ${error.message}`);
    });
}

function setBottomWaypoint(player, blockName, location, name, waypointPublicToggle, waypointPrivateToggle) {
    const dimension = player.dimension.id.split(":")[1];
    const newWaypoint = {
        owner: player.id,
        id: Date.now() + globalWaypoints.length,
        name,
        locationBot: location,
        locationTop: { x: location.x, y: location.y + 1, z: location.z },
        dimension,
        public: waypointPublicToggle,
        private: waypointPrivateToggle
    };
    globalWaypoints.push(newWaypoint);
    updateWorldWaypoints();
    player.sendMessage("§eWaypoint Added!");
}

function setTopWaypoint(player, blockName, location, name, waypointPublicToggle, waypointPrivateToggle) {
    const dimension = player.dimension.id.split(":")[1];
    const newWaypoint = {
        owner: player.id,
        id: Date.now() + globalWaypoints.length,
        name,
        locationBot: { x: location.x, y: location.y - 1, z: location.z },
        locationTop: location,
        dimension,
        public: waypointPublicToggle,
        private: waypointPrivateToggle
    };
    globalWaypoints.push(newWaypoint);
    updateWorldWaypoints();
    player.sendMessage("§eWaypoint Added!");
}

function getFreeArea(dimension, location) {
    const blocks = [
        dimension.getBlock(location),
        dimension.getBlock({ x: location.x, y: location.y + 1, z: location.z }),
        dimension.getBlock({ x: location.x, y: location.y + 2, z: location.z })
    ];
    return blocks.every(block => block.typeId.includes("air")) ? location : false;
}

function getDistance(loc1, loc2) {
    const dx = loc1.x - loc2.x;
    const dy = loc1.y - loc2.y;
    const dz = loc1.z - loc2.z;
    return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
