import { configuration } from "../configurations.js";
import { Player as playerHandler } from "./playerBuilder.js";
import { contentLog, RawText } from "../utils/index.js";
//import { printDebug } from "@modules/../util.js"
export class CustomArgType {
}
export class CommandPosition {
    constructor() {
        this.x = 0;
        this.y = 0;
        this.z = 0;
        this.xRelative = true;
        this.yRelative = true;
        this.zRelative = true;
    }
    static parseArgs(args, index, is3d = true) {
        const pos = new CommandPosition();
        for (let i = 0; i < (is3d ? 3 : 2); i++) {
            let arg = args[index];
            if (!args) {
                const err = {
                    isSyntaxError: true,
                    stack: contentLog.stack(),
                    idx: -1
                };
                throw err;
            }
            let relative = false;
            if (arg.includes("~")) {
                arg = arg.slice(1);
                relative = true;
            }
            const val = arg == "" ? 0 : parseFloat(arg);
            if (val != val || isNaN(val)) {
                throw RawText.translate("commands.generic.num.invalid").with(arg);
            }
            if (i == 0) {
                pos.x = val;
                pos.xRelative = relative;
            }
            else if (i == 1 && is3d) {
                pos.y = val;
                pos.yRelative = relative;
            }
            else {
                pos.z = val;
                pos.zRelative = relative;
            }
            index++;
        }
        return { result: pos, argIndex: index };
    }
    static clone(original) {
        const pos = new CommandPosition();
        pos.x = original.x;
        pos.y = original.y;
        pos.z = original.z;
        pos.xRelative = original.xRelative;
        pos.yRelative = original.yRelative;
        pos.zRelative = original.zRelative;
        return pos;
    }
    relativeTo(player, isBlockLoc = false) {
        const loc = { x: 0, y: 0, z: 0 };
        const x = this.x + (this.xRelative ? player.location.x : 0);
        const y = this.y + (this.yRelative ? player.location.y : 0);
        const z = this.z + (this.zRelative ? player.location.z : 0);
        loc.x = isBlockLoc ? Math.floor(x) : x;
        loc.y = isBlockLoc ? Math.floor(y) : y;
        loc.z = isBlockLoc ? Math.floor(z) : z;
        return loc;
    }
}
export class CommandBuilder {
    constructor() {
        this.prefix = configuration.prefix;
        this._registrationInformation = [];
        this.customArgTypes = new Map();
    }
    /**
      * Register a command with a callback
      * @param {registerInformation} register An object of information needed to register the custom command
      * @param {(data: BeforeChatEvent, args: Array<string>) => void}callback Code you want to execute when the command is executed
      * @example import { Server } from "../../Minecraft";
      *  const server = new Server();
      *  server.commands.register({ name: 'ping' }, (data, args) => {
      *  server.broadcast('Pong!', data.sender.nameTag);
      * });
      */
    register(register, callback) {
        this._registrationInformation.push({
            name: register.name.toLowerCase(),
            aliases: register.aliases ? register.aliases.map(v => v.toLowerCase()) : null,
            description: register.description,
            usage: register.usage ?? [],
            permission: register.permission,
            callback
        });
    }
    /**
      * Get a list of registered commands
      * @returns {Array<string>}
      * @example getAll();
      */
    getAll() {
        const commands = [];
        this._registrationInformation.forEach(element => {
            commands.push(element.name);
        });
        return commands;
    }
    /**
      * Get all the registered informations
      * @returns {Array<storedRegisterInformation>}
      * @example getAllRegistration();
      */
    getAllRegistation() {
        return this._registrationInformation;
    }
    /**
      * Get registration information on a specific command
      * @param name The command name or alias you want to get information on
      * @returns {storedRegisterInformation}
      * @example getRegistration('ping');
      */
    getRegistration(name) {
        const command = this._registrationInformation.some(element => element.name.toLowerCase() === name || element.aliases && element.aliases.includes(name));
        if (!command)
            return;
        let register;
        this._registrationInformation.forEach(element => {
            const eachCommand = element.name.toLowerCase() === name || element.aliases && element.aliases.includes(name);
            if (!eachCommand)
                return;
            register = element;
        });
        return register;
    }
    addCustomArgType(name, argType) {
        this.customArgTypes.set(name, argType);
    }
    printCommandArguments(name, player) {
        const register = this.getRegistration(name);
        if (!register)
            return;
        const usages = [];
        function accumulate(base, args, subName = "_") {
            const text = [...base];
            let hasSubCommand = false;
            let flagText;
            if (subName.charAt(0) != "_") {
                text.push(subName);
            }
            args?.forEach(arg => {
                if ((!("flag" in arg) || arg.flag && arg.name) && flagText) {
                    text.push(flagText + "]");
                    flagText = null;
                }
                if ("subName" in arg) {
                    hasSubCommand = true;
                    if (player && !playerHandler.hasPermission(player, arg.permission)) {
                        return;
                    }
                    accumulate(text, arg.args, arg.subName);
                }
                else if ("flag" in arg) {
                    if (!flagText)
                        flagText = "[-";
                    flagText += arg.flag;
                    if (arg.name) {
                        text.push(flagText + ` <${arg.name}: ${arg.type}>]`);
                        flagText = null;
                    }
                }
                else {
                    let argText = arg.default ? "[" : "<";
                    argText += arg.name + ": ";
                    if (arg.range && typeof (arg.range[0]) == "number" && typeof (arg.range[1]) == "number")
                        argText += arg.range[0] + ".." + arg.range[1];
                    else
                        argText += arg.type;
                    text.push(argText + (arg.default ? "]" : ">"));
                }
            });
            if (flagText) {
                text.push(flagText + "]");
                flagText = null;
            }
            if (!hasSubCommand) {
                usages.push(text.join(" "));
            }
        }
        if (player && !playerHandler.hasPermission(player, register.permission)) {
            return [];
        }
        if (!register.usage.length)
            return [""];
        accumulate([], register.usage);
        return usages;
    }
    parseArgs(comnand, args) {
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        const self = this;
        const result = new Map();
        const argDefs = this.getRegistration(comnand)?.usage;
        if (argDefs == undefined)
            return;
        function processArg(idx, def, result) {
            if (def.type == "int") {
                if (!/^[-+]?(\d+)$/.test(args[idx])) {
                    throw RawText.translate("commands.generic.num.invalid").with(args[idx]);
                }
                const val = Number(args[idx]);
                if (def.range) {
                    const less = val < (def.range[0] ?? -Infinity);
                    const greater = val > (def.range[1] ?? Infinity);
                    if (less) {
                        throw RawText.translate("commands.generic.wedit:tooSmall").with(val).with(def.range[0]);
                    }
                    else if (greater) {
                        throw RawText.translate("commands.generic.wedit:tooBig").with(val).with(def.range[1]);
                    }
                }
                idx++;
                result.set(def.name, val);
            }
            else if (def.type == "float") {
                const val = parseFloat(args[idx]);
                if (val != val || isNaN(val)) {
                    throw RawText.translate("commands.generic.num.invalid").with(args[idx]);
                }
                if (def.range) {
                    const less = val < (def.range[0] ?? -Infinity);
                    const greater = val > (def.range[1] ?? Infinity);
                    if (less) {
                        throw RawText.translate("commands.generic.tooSmall").with(val).with(def.range[0]);
                    }
                    else if (greater) {
                        throw RawText.translate("commands.generic.tooBig").with(val).with(def.range[1]);
                    }
                }
                idx++;
                result.set(def.name, val);
            }
            else if (def.type == "xz") {
                const parse = CommandPosition.parseArgs(args, idx, false);
                idx = parse.argIndex;
                result.set(def.name, parse.result);
            }
            else if (def.type == "xyz") {
                const parse = CommandPosition.parseArgs(args, idx, true);
                idx = parse.argIndex;
                result.set(def.name, parse.result);
            }
            else if (def.type == "CommandName") {
                const cmdBaseInfo = self.getRegistration(args[idx]);
                if (!cmdBaseInfo)
                    throw RawText.translate("commands.generic.unknown").with(args[idx]);
                idx++;
                result.set(def.name, cmdBaseInfo.name);
            }
            else if (def.type == "string") {
                result.set(def.name, args[idx++]);
            }
            else if (self.customArgTypes.has(def.type)) {
                try {
                    const parse = self.customArgTypes.get(def.type).parseArgs(args, idx);
                    idx = parse.argIndex;
                    result.set(def.name, parse.result);
                }
                catch (error) {
                    if (error.isSyntaxError) {
                        error.idx = idx;
                    }
                    throw error;
                }
            }
            else {
                throw `Unknown argument type: ${def.type}`;
            }
            return idx;
        }
        function processList(currIdx, argDefs, result) {
            let defIdx = 0;
            let hasNamedSubCmd = false;
            const flagDefs = new Map();
            argDefs?.forEach(argDef => {
                if ("flag" in argDef && !flagDefs.has(argDef.flag)) {
                    flagDefs.set(argDef.flag, argDef);
                }
            });
            function processSubCmd(idx, arg) {
                let processed = false;
                let unnamedSubs = [];
                // process named sub-commands and collect unnamed ones
                while (defIdx < argDefs.length && ("subName" in argDefs[defIdx])) {
                    const argDef = argDefs[defIdx];
                    if (!processed) {
                        if (argDef.subName.startsWith("_")) {
                            unnamedSubs.push(argDef);
                        }
                        else {
                            hasNamedSubCmd = true;
                            if (argDef.subName == arg) {
                                idx = processList(idx + 1, argDef.args, result);
                                result.set(argDef.subName, true);
                                processed = true;
                                unnamedSubs = [];
                            }
                        }
                    }
                    defIdx++;
                }
                // Unknown subcommand
                if (!processed && hasNamedSubCmd && !unnamedSubs.length) {
                    const err = {
                        isSyntaxError: true,
                        stack: contentLog.stack(),
                        idx: i
                    };
                    throw err;
                }
                // process unnamed sub-commands
                const fails = [];
                for (const sub of unnamedSubs) {
                    try {
                        const subResult = new Map();
                        idx = processList(i, sub.args, subResult);
                        result.set(sub.subName, true);
                        subResult.forEach((v, k) => result.set(k, v));
                        break;
                    }
                    catch (e) {
                        fails.push(e);
                    }
                }
                if (fails.length != 0 && fails.length == unnamedSubs.length) {
                    throw fails[0];
                }
                return idx;
            }
            const numList = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
            let i;
            for (i = currIdx; i < args.length; i++) {
                const arg = args[i];
                if (arg.startsWith("-") && !numList.includes(arg.charAt(1))) {
                    for (const f of arg) {
                        if (f == "-")
                            continue;
                        if (flagDefs.has(f)) {
                            result.set(f, true);
                            const argDef = flagDefs.get(f);
                            if (argDef.type != undefined) {
                                i = processArg(i + 1, {
                                    name: argDef.flag + "-" + argDef.name,
                                    type: argDef.type
                                }, result);
                            }
                        }
                        else {
                            throw RawText.translate("commands.generic.wedit:invalidFlag").with(f);
                        }
                    }
                    continue;
                }
                let argDef;
                while (defIdx < (argDefs?.length ?? 0)) {
                    if (!("flag" in argDefs[defIdx])) {
                        argDef = argDefs[defIdx];
                        break;
                    }
                    defIdx++;
                }
                // Leftover arguments
                if (!argDef) {
                    const err = {
                        isSyntaxError: true,
                        stack: contentLog.stack(),
                        idx: i
                    };
                    throw err;
                }
                if ("type" in argDef && !("flag" in argDef)) {
                    i = processArg(i, argDef, result) - 1;
                    defIdx++;
                }
                else if ("subName" in argDef) {
                    i = processSubCmd(i, arg) - 1;
                }
            }
            // Process optional arguments (and throw if some are required)
            while (defIdx < (argDefs?.length ?? 0)) {
                const argDef = argDefs[defIdx];
                if (!("flag" in argDef)) {
                    if ("type" in argDef && argDef?.default != undefined && !("subName" in argDef)) {
                        // TODO: use clone command of customArgType here
                        result.set(argDef.name, argDef.default);
                    }
                    else if ("subName" in argDef) {
                        processSubCmd(i, "");
                    }
                    else {
                        // Required arguments not specified
                        const err = {
                            isSyntaxError: true,
                            stack: contentLog.stack(),
                            idx: -1
                        };
                        throw err;
                    }
                }
                defIdx++;
            }
            return i;
        }
        processList(0, argDefs, result);
        return result;
    }
    callCommand(player, command, args = []) {
        const registration = this.getRegistration(command);
        if (!playerHandler.hasPermission(player, registration.permission)) {
            throw "commands.generic.wedit:noPermission";
        }
        return registration.callback({
            cancel: true,
            sender: player,
            message: `;${command} ${args.join(" ")}`
        }, this.parseArgs(command, args));
    }
}
export const Command = new CommandBuilder();
