// Addon made by @GamerDos/@gamerdos on YouTube

import { Player, world, EntityHealthComponent, system, EntityInventoryComponent, ItemStack, ItemTypes, Vector, Entity, Block, Vector3, ScoreboardIdentityType, EntityHurtAfterEvent, GameMode } from "@minecraft/server";
import { ActionFormData, FormCancelationReason, ModalFormData } from "@minecraft/server-ui";
import { config, sendAllMessage, sendPlayerMessage } from "./config";
import { Command, commands } from "./lifesteal/handler";
import { reviveGuiForm } from "./lifesteal/gui/items/beacon_of_life";
import { Database } from "./database";

export const lifestealVersion = `1.15.2-release`;

// Normal commands
import './lifesteal/commands/withdraw';
import './lifesteal/commands/revive';
import './lifesteal/commands/help';
import './lifesteal/commands/heart';

// Misc commands
import './lifesteal/commands/misc/deadplayers';
import './lifesteal/commands/misc/welcome';

// Ghost only commands
import './lifesteal/commands/ghostsOnly/cookieclicker';
import './lifesteal/commands/ghostsOnly/guess';
import './lifesteal/commands/ghostsOnly/jumpscare';
import './lifesteal/commands/ghostsOnly/rps';
import './lifesteal/commands/ghostsOnly/leaderboard';

//Staff only commands
import './lifesteal/commands/staffOnly/adminpanel';
import './lifesteal/commands/staffOnly/logs';
import './lifesteal/commands/staffOnly/forcerevive';
import './lifesteal/commands/staffOnly/kill';
import './lifesteal/commands/staffOnly/ghostbarrier';
import './lifesteal/commands/staffOnly/get';
import './lifesteal/commands/staffOnly/reviveall';
import './lifesteal/commands/staffOnly/reviveamount';
import './lifesteal/commands/staffOnly/startamount';
import './lifesteal/commands/staffOnly/op';
import './lifesteal/commands/staffOnly/banghosts';
import './lifesteal/commands/staffOnly/compatibility';

// GUI imports
import './lifesteal/gui/welcome';
import './lifesteal/gui/items/beacon_of_life';
import './lifesteal/gui/items/heart_of_life';
import './lifesteal/gui/items/withdraw_heart';

// Core Mechanics
import './lifesteal/core_mechanics/heartCore';
import './lifesteal/core_mechanics/heartItem';
import './lifesteal/core_mechanics/reviveAndDead';
import './lifesteal/core_mechanics/reviveSystem';

/**
 * Produces a random string
 * @param length Length of the random string
 * @param autoCharacters Enable premade characters for the string
 * @param characters If autoCharacters is false, use these characters instead of premade ones
 * @returns string
 */
export function random(length: number, autoCharacters: boolean, characters?: string) {
    if (autoCharacters) {
        const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789_';
        let result: string = ``;
        const charactersLength = characters.length;
        for (let i = 0; i < length; i++) {
            result += characters.charAt(Math.floor(Math.random() * charactersLength));
        }
        return result;
    }
    else {
        let result: string = ``;
        const charactersLength = characters.length;
        for (let i = 0; i < length; i++) {
            result += characters.charAt(Math.floor(Math.random() * charactersLength));
        }
        return result;
    }
}

export function gameruleSwitcheroo(gamerule: string, value: boolean) {
    world.getDimension(`overworld`).runCommand(`gamerule ${gamerule} ${value}`);
    system.runTimeout(() => {
        world.getDimension(`overworld`).runCommand(`gamerule ${gamerule} ${!value}`);
    }, 2);
};

/**
 * Returns a random number between min and max
 * @param min Minimum
 * @param max Maximum
 */
export function randomN(min: number, max: number) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

/**
 * @param playerName Player to get
 */
export function getPlayer(playerName: string) {
    if (!playerName) return;
    return world.getAllPlayers().find(player => player.name.toLowerCase().replace(/"|\\|@/g, "") === playerName.toLowerCase().replace(/"|\\|@/g, ""));
}

/**
 * Gets a player's score
 * @param player Player to get the score from
 * @param objective Which objective to get the player's score from
 * @returns
 */
export function getScore(player: Player, objective: string) {
    try {
        return world.scoreboard.getObjective(objective)?.getScore(player.scoreboardIdentity);
    } catch { }
    return undefined;
}

/**
 * Sets a player's score
 * @param player Player to set the score from
 * @param objective Which objective to set the player's score from
 * @param score What score the player should be set to
 * @returns
 */
export function setScore(player: Player, objective: string, score: number) {
    if (!player.scoreboardIdentity) return player.runCommand(`scoreboard players set @s ${objective} ${score}`);
    return world.scoreboard.getObjective(objective).setScore(player.scoreboardIdentity, score);
}

/**
 * Adds to a player's score
 * @param player Player to add the score from
 * @param objective Which objective to add the player's score from
 * @param score How much score to add to the player
 * @returns
 */
export function addScore(player: Player, objective: string, amount: number) {
    return world.scoreboard.getObjective(objective).addScore(player.scoreboardIdentity, amount);
}

/**
 * I found this from WavePlayz#7915
 */
export function sleep(seconds: number, ticks: boolean = true) {
    if (ticks) {
        return new Promise<void>(resolve => {
            system.run(function runnable() {
                if (seconds-- > 0) system.run(runnable);
                else resolve();
            });
        });
    }
    else {
        seconds = seconds * 20;
        return new Promise<void>(resolve => {
            system.run(function runnable() {
                if (seconds-- > 0) system.run(runnable);
                else resolve();
            });
        });
    }
}

export const lifesteal = new Database(`lifesteal`);

function migrateData(scoreboardDatabase: string, databaseToMigrate: Database) {
    if (!world.scoreboard.getObjective(scoreboardDatabase)) return false;
    const decrypt = (encrypted: string, salt: string): string => {
        const decryptedChars: number[] = [];
        for (let i = 0; i < encrypted.length; i++) {
            const charCode = encrypted.charCodeAt(i) - salt.charCodeAt(i % salt.length);
            decryptedChars.push(charCode);
        }
        return String.fromCharCode(...decryptedChars);
    };

    const DisplayName = {
        parse(text: string, salt?: string): Record<string, any> {
            try {
                const a = salt ? decrypt(text, salt) : text;
                return JSON.parse(`{${a}}`);
            } catch (error) {
            }
        }
    };

    for (let participant of world.scoreboard.getObjective(scoreboardDatabase).getParticipants()) {
        if (participant.type !== ScoreboardIdentityType.FakePlayer) continue;
        const data = DisplayName.parse(participant.displayName.replace(/\\"/g, `"`));
        const key = Object.keys(data)[0];
        const value = data[key];

        databaseToMigrate.set(key, value);
    };

    world.scoreboard.removeObjective(scoreboardDatabase);
}

const defaultSettings = {
    maxhearts: `20`,
    envsteal: `true`,
    banghosts: `false`,
    ghostbarrier: `100`,
    reviveamount: `5`,
    startamount: `10`,
    heartgain: `1`,
    heartlose: `1`,
    lifestealmode: `classic`,
    useplayerjson: `true`,
    antilore: `false`
};

for (const key in defaultSettings) {
    if (!lifesteal.has(key)) {
        lifesteal.set(key, defaultSettings[key as keyof typeof defaultSettings]);
    }
}

export const revivelist = new Database(`revivelist`);
export const deadplayers = new Database(`deadplayers`);
export const ccLd = new Database(`ccLd`);
export const rpsLd = new Database(`rpsLd`);

migrateData(`jaylydb:lifesteal`, lifesteal);
migrateData(`jaylydb:revivelist`, revivelist);
migrateData(`jaylydb:deadplayers`, deadplayers);
migrateData(`jaylydb:ccLd`, ccLd);
migrateData(`jaylydb:rpsLd`, rpsLd);

const objectives = [`DB_ccLd`, `DB_deadplayers`, `DB_revivelist`, `DB_rpsLd`];

for (const objective of objectives) {
    const ScoreboardObjective = world.scoreboard.getObjective(objective);
    if (ScoreboardObjective) {
        const participants = ScoreboardObjective.getParticipants();
        for (const participant of participants) {
            const dis = JSON.parse(participant.displayName.replace(/\\"/g, `"`));
            if (objective === `DB_ccLd`) {
                ccLd.set(dis[0], dis[1]);
            }
            else if (objective === `DB_deadplayers`) {
                deadplayers.set(dis[0], dis[1]);
            }
            else if (objective === `DB_revivelist`) {
                revivelist.set(dis[0], dis[1]);
            }
            else if (objective === `DB_rpsLd`) {
                rpsLd.set(dis[0], dis[1]);
            }
        }
        world.scoreboard.removeObjective(objective);
    }
}

export let lifestealLogs: string[] = [];

// Hidden commands (fun commands and testing stuff, don't show up in ?help)
new Command({
    name: `forcejumpscare`,
    aliases: [`fjs`],
    staffOnly: true,
    code({ player, args }) {
        let target = getPlayer(args.join(` `));

        if (!target) return sendPlayerMessage(player, `§cFailed to run command, player must be online`);

        try {
            player.runCommandAsync(`title ${target.name} actionbar §cA ghost has jumpscared you!`);
            target.runCommandAsync(`playsound ls_jumpscare @s`);
            player.runCommandAsync(`title ${target.name} title §c§lBOO`);
            player.runCommandAsync(`title ${target.name} subtitle §c${player.name} has jumpscared you!`);
            setScore(player, `ls-ghost-cd`, 6002);
            player.runCommandAsync(`tag @s add ls-ghost_jumpscare_cd`);
        }
        catch {
            sendPlayerMessage(player, `§cFailed to run command, player must be online`);
        }
    }
});

new Command({
    name: `simulategdjoin`,
    staffOnly: true,
    code({ player }) {
        sendAllMessage(`§c§krick§r §c§lThe creator of this Lifesteal Addon/Plugin has joined the game! (${player.name}) §r§c§kroll`);
        player.runCommandAsync(`playsound gd_join @a`);
        player.runCommandAsync(`effect @s darkness 2 0 true`);
    }
});

// This command is used to test the revive system by adding a fake player to the deadplayers list
new Command({
    name: `fakeplayer`,
    staffOnly: true,
    code({ player, args }) {
        const randomName = random(6, true);

        deadplayers.set(args[0], args[0]);
        sendPlayerMessage(player, `§aAdded ${args[0]} to the deadplayers list!`);
    }
});

new Command({
    name: `simulategain`,
    staffOnly: true,
    code({ player }) {
        const heartgain: string = lifesteal.get(`heartgain`);
        const randomNumber = randomN(+heartgain.split(`-`)[1], +heartgain.split(`-`)[2]);
        const heartsToAdd = heartgain.includes(`random`) ? randomNumber : +lifesteal.get(`heartgain`);
        addScore(player, `ls-death`, heartsToAdd);
    }
});

new Command({
    name: `simulatelose`,
    staffOnly: true,
    code({ player }) {
        addScore(player, `ls-death`, -+lifesteal.get(`heartlose`));
    }
});

new Command({
    name: `revivepanel`,
    staffOnly: true,
    code({ player, args }) {
        reviveGuiForm(player);

        player.clearDynamicProperties();
    },
});

/**
 * Heart system
 * This controls how many hearts you have based off of your score on "ls-death"
 */
system.runInterval(() => {
    for (let player of world.getAllPlayers()) {
        let score = getScore(player, `ls-death`);
        if (!score) {
            let startAmount = lifesteal.get(`startamount`);
            setScore(player, `ls-death`, +startAmount);
            continue;
        }

        if (player.hasTag(`ls-dead`)) {
            player.setGameMode(GameMode.spectator);
        }

        // Compatibility mode
        if (lifesteal.get(`useplayerjson`) !== `true`) {
            player.triggerEvent(`lifesteal:health-10`);
            const health = player.getComponent(`health`);
            if (score <= 10) {
                if (health.currentValue / 2 > score) {
                    health.setCurrentValue(score * 2);
                }
                return;
            }
            let level = -1;
            if (score > 10) {
                for (let i = 11; i <= score; i += 2) {
                    level++;
                }
            }

            player.addEffect(`health_boost`, 5, { amplifier: level, showParticles: false });

            if (health.currentValue / 2 > score) {
                health.setCurrentValue(score * 2);
            }
            return;
        }

        let health = player.getComponent(`health`);
        let find = +player.getProperty(`lifesteal:health`);

        if (score !== 0) {
            player.triggerEvent(`lifesteal:health-${score}`);
            setScore(player, `hearts`, score);
            player.setProperty(`lifesteal:health`, score);
        }

        system.run(() => {
            if (find && score > find && health.currentValue === score * 2) {
                let currentHealth = health.currentValue;
                health.setCurrentValue(currentHealth - (score - find) * 2);
            }
        });
    }
}, 1);

function ghostBarrier(player: Player) {
    if (lifesteal.get(`ghostbarrier`) == `disabled`) return;
    let entites = <Player[]>[...world.getDimension(`overworld`).getEntities({ location: { x: 0, y: 90, z: 0 }, minDistance: +lifesteal.get(`ghostbarrier`) })];
    if (entites.find(x => x.name == player.name)) {
        if (player.hasTag(`ls-revive`)) return;
        player.teleport({ x: 0, y: 90, z: 0 }, { dimension: player.dimension });
        sendPlayerMessage(player, `§cYou may not leave spawn!`);
        system.runTimeout(() => {
            player.playSound(`note.pling`, { pitch: 1.1 });
        }, 1);
    }
}

function exceededHearts(player: Player) {
    let current = getScore(player, `ls-death`);
    setScore(player, `ls-death`, +lifesteal.get(`maxhearts`));
    let amount = current - +lifesteal.get(`maxhearts`);
    let inventory = player.getComponent(`inventory`) as EntityInventoryComponent;
    let inventorySize = inventory.container.size;
    for (let i = 0; i < inventorySize; i++) {
        let item = inventory.container.getItem(i);
        if (!item || item.typeId !== `lifesteal:heart`) continue;
        inventory.container.setItem(i, new ItemStack(item.typeId, item.amount));
    }
    player.runCommand(`give @s lifesteal:heart ${amount}`);
}

// You can probably figure it out
system.runInterval(() => {
    for (let player of world.getAllPlayers()) {
        if (player.hasTag(`ls-dead`)) {
            ghostBarrier(player);
        }
        if (getScore(player, `ls-death`) > +lifesteal.get(`maxhearts`)) {
            exceededHearts(player);
        }

        let health = <EntityHealthComponent>player.getComponent(`health`);
        if (player.hasTag(`heart-show_on`)) {
            player.runCommandAsync(`title @s actionbar §a${(health.currentValue / 2).toFixed(1)}/${getScore(player, `ls-death`)}`);
        }

        if (system.currentTick % 20 !== 0) return;
        if (player.hasTag(`heart-display_on`)) {
            player.nameTag = `${player.name}\n§c${Math.round(health.currentValue / 2)}/${getScore(player, `ls-death`)}`;
        }
    }
}, 1);

/**
 * If true, you will lose hearts when dying from natural sources (Default: True)
 * Can be toggled with ?envsteal <true/false>
 */
export let envSteal: boolean;

/**
 * If true, ghosts will be kicked when they join (Bit of delay for the Heart of Life to work)
 */
export let banGhosts: Boolean;

system.runInterval(() => {
    if (lifestealLogs.length > 500) {
        lifestealLogs = [];
    }

    envSteal = JSON.parse(lifesteal.get(`envsteal`));
    banGhosts = JSON.parse(lifesteal.get(`banghosts`));
}, 20);

world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => {

    // Shows up when a player dies and becomes a ghost
    if (player.hasTag(`ls-dead`)) {
        function ghostWelcomeForm() {
            if (initialSpawn) return;
            let ghostWelcome = new ActionFormData()
                .title(`You died!`)
                .body(`§gYou have lost your last heart and have become a ghost!\n\n§bTo get revived, a player must use a Heart of Life item and revive you`)
                .button(`Alright`);
            ghostWelcome.show(player).then(result => {
                if (result.canceled && result.cancelationReason == `UserBusy`) return ghostWelcomeForm();
            });
        }
        ghostWelcomeForm();
    }

    if (!initialSpawn) return;

    if (player.runCommand(`event entity @s lifesteal:health-10`).successCount === 0 && lifesteal.get(`useplayerjson`) === `true`) {
        lifesteal.set(`useplayerjson`, `false`);
        sendAllMessage(`§c§lAnother mod is not compatible with lifesteal! Compatibility mode turned on, the health system now gives hearts using health boost instead of player.json`);
    }

    player.addTag(`ls-player`);
    player.addEffect(`resistance`, 60, { amplifier: 255, showParticles: false });
    sendPlayerMessage(player, `§c-----
§6Lifesteal version: ${lifestealVersion}
Command Prefix: §l${config.prefix}§r
§dSupport the addon creator! https://patreon.com/gamerdos
§c-----`);
    if (player.hasTag(`ls-ghostcommand-cd`)) {
        player.removeTag(`ls-ghostcommand-cd`);
    }

    player.getTags()?.forEach(x => {
        if (x.startsWith(`lsh-`)) {
            player.removeTag(x);
        }
    });

    // Shows when the creator (GamerDos) spawns
    if (player.id === `-4294967295` && player.name == `GamerDos7511`) {
        sendAllMessage(`§c§krick§r §c§lThe creator of this Lifesteal Addon/Plugin has joined the game! (${player.name}) §r§c§kroll`);
        player.runCommandAsync(`playsound gd_join @a`);
        player.addEffect(`darkness`, 40, { showParticles: false });
        if (!player.hasTag(config.staffTag)) player.addTag(config.staffTag);
    }

    try {
        let item = player.dimension.spawnItem(new ItemStack(`lifesteal:heart`), player.location);
        item.kill();
    } catch {
        sendPlayerMessage(player, `§cYou didn't enable Holiday Creator Features! Go to experiments and turn on HCF for custom items to work`);
    }
});

const lores = {
    "lifesteal:beacon_of_life": "§r§7This is the beacon of life.\n§7Use this to revive a player.\n\n§7(( §o§fRight click/eat or do ?revive§7 to use ))",
    "lifesteal:heart_of_life": "§r§7This is the heart of life.\n§7Permanently increase your revive heart count\n\n§7(( §o§fRight click/eat to use ))",
    "lifesteal:heart_fragment": "§r§dUse 4 of these to craft a Heart.",
    "lifesteal:heart": "§r§7A heart is used to add a heart to your health.\n\n§7(( §o§fRight click/eat§7 to recieve a heart ))",
    "lifesteal:heart_essence": "§r§dUse this to craft a Heart.",
};

// Item lore
system.runInterval(() => {
    if (lifesteal.get(`antilore`) === `true`) return;
    for (let player of world.getAllPlayers()) {
        let inv = player.getComponent(`inventory`) as EntityInventoryComponent;

        let inventorySize = inv.container.size;

        for (let i = 0; i < inventorySize; i++) {
            const item = inv.container.getItem(i);
            if (!item) continue;

            const lore = lores[item.typeId as keyof typeof lores];

            if (lore && item.getLore()[0] !== lore) {
                const newItem = new ItemStack(item.typeId, item.amount);
                newItem.setLore(lore.split(`\n`));
                inv.container.setItem(i, newItem);
            }
        }
    }
}, 1);