import * as mc from "@minecraft/server";
import { mcerror, mclog } from "./log";
import { SafetyCheckedEntity } from "./entity";
import { Vector3 } from "./vector3";
import { waitForTicks } from "./events";
import { TextFormat } from "./text_format";
class Argument {
    constructor() {
        this.optional = false;
    }
}
export class KeywordArg extends Argument {
    constructor(keyword, arg) {
        super();
        this.keyword = keyword;
        this.arg = arg;
    }
    parse(input) {
        if (input[0] !== this.keyword)
            throw (`unexpected "${input[0]}", expected ${this.keyword}`);
        return this.arg.parse(input.slice(1));
    }
    usage() {
        this.arg.optional = this.optional;
        return `${this.keyword} ${this.arg.usage()}`;
    }
}
export class NumberArg extends Argument {
    constructor(range) {
        super();
        this.range = range;
    }
    parse(input) {
        if (!input.length)
            throw ("unexpected end of string");
        const result = Number(input[0]);
        if (isNaN(result))
            throw (`${input[0]} is not a number`);
        if (this.range && (result < this.range.min || result > this.range.max))
            throw (`"${result}" was not within the expected range [${this.range.min}, ${this.range.max}]`);
        return {
            result,
            remainingInput: input.splice(1),
        };
    }
    usage() {
        const message = `value: float`;
        return this.optional ? `[${message}]` : `<${message}>`;
    }
}
export class CoordinateArg extends Argument {
    constructor() {
        super(...arguments);
        this.origin = Vector3.ZERO;
    }
    setVector(origin) {
        this.origin = origin;
    }
    parse(input) {
        if (input.length < 3)
            throw ("unexpected end of string");
        const x = this.getCoordinate(input[0], this.origin.x);
        const y = this.getCoordinate(input[1], this.origin.y);
        const z = this.getCoordinate(input[2], this.origin.z);
        if (isNaN(x))
            throw (`${input[0]} is not a coordinate`);
        if (isNaN(y))
            throw (`${input[1]} is not a coordinate`);
        if (isNaN(z))
            throw (`${input[2]} is not a coordinate`);
        return {
            result: new Vector3({ x, y, z }),
            remainingInput: input.splice(3),
        };
    }
    getCoordinate(value, position) {
        if (value.startsWith("~"))
            return Number(value.replace("~", "")) + position;
        if (value.startsWith("^"))
            return Number(value.replace("^", "")) + position;
        return Number(value);
    }
    usage() {
        const message = `pos: x y z`;
        return this.optional ? `[${message}]` : `<${message}>`;
    }
}
export class StringArg extends Argument {
    parse(input) {
        if (!input.length || input[0] === "")
            throw ("unexpected end of string");
        const result = input[0];
        return {
            result,
            remainingInput: input.splice(1),
        };
    }
    usage() {
        const message = `value: string`;
        return this.optional ? `[${message}]` : `<${message}>`;
    }
}
export class EnumArg extends Argument {
    constructor(values) {
        super();
        this.values = values;
    }
    parse(input) {
        if (!input.length || input[0] === "")
            throw ("unexpected end of string");
        if (!this.values.includes(input[0]))
            throw (`unexpected ${input[0]} expected one of ${this.values}`);
        const result = input[0];
        return {
            result,
            remainingInput: input.splice(1),
        };
    }
    usage() {
        const message = `value: ${this.values.join("|")}`;
        return this.optional ? `[${message}]` : `<${message}>`;
    }
}
export class Command {
    static parse(key, value, source) {
        var _a;
        let input = value.split(" ");
        try {
            if (key) {
                let commands = (_a = this.registery[key]) === null || _a === void 0 ? void 0 : _a.commands;
                if (!commands) {
                    let output = `No command "${key}" in registry.`;
                    for (const key of Object.keys(this.registery)) {
                        output += `\n-${key}: ${this.registery[key].description}`;
                        this.registery[key].commands.forEach(command => output += `\n${command.getUsage()}`);
                    }
                    throw (output);
                }
                commands = commands.sort((a, b) => b.args.length - a.args.length);
                let message = "";
                for (const command of commands) {
                    try {
                        command.parse(input.join(" "), source);
                        return;
                    }
                    catch (e) {
                        message = `${key}: ${e}\nUsage: ${command.getUsage()}`;
                        continue;
                    }
                }
                throw (message);
            }
        }
        catch (e) {
            mcerror(e);
        }
    }
    constructor(name, callback) {
        this.name = name;
        this.callback = callback;
        this.args = [];
        this.parsedArgs = [];
        this.desc = "";
        if (!Command.registery[name])
            Command.registery[name] = { commands: [this], description: "" };
        else
            Command.registery[name].commands.push(this);
    }
    optional(argument) {
        argument.optional = true;
        this.args.push(argument);
        return this;
    }
    required(argument) {
        this.args.push(argument);
        return this;
    }
    description(value) {
        Command.registery[this.name].description = value;
    }
    parse(value, source) {
        this.parsedArgs = [];
        if (this.args.length) {
            let input = value.split(" ");
            for (const arg of this.args) {
                try {
                    if (arg instanceof CoordinateArg)
                        arg.setVector(source.location);
                    const result = arg.parse(input);
                    input = result.remainingInput;
                    this.parsedArgs.push(result.result);
                }
                catch (e) {
                    if (arg.optional) {
                        this.parsedArgs.push(undefined);
                    }
                    else {
                        throw (e);
                    }
                }
            }
            if (input.length && !input.every(str => str === " " || str === ""))
                throw (`unexpected ${input}`);
        }
        mclog(`Running command ${this.name} with args ${this.parsedArgs}`, "verbose");
        this.callback(source, ...this.parsedArgs);
    }
    getUsage() {
        let message = `${this.name}`;
        for (const arg of this.args) {
            message += ` ${arg.usage()}`;
        }
        return message;
    }
}
Command.registery = {};
Command.enabled = true;
mc.system.afterEvents.scriptEventReceive.subscribe(event => {
    if (event.sourceEntity && Command.enabled) {
        Command.parse(event.id.replace("ldz:", ""), event.message, new SafetyCheckedEntity(event.sourceEntity));
    }
}, { namespaces: ["ldz"] });
// #region No AI Command
new Command("summon", (source, entityId, location, target, anim, duration) => {
    noAISummon(source, entityId, { location, target, anim, duration });
})
    .required(new StringArg())
    .required(new CoordinateArg())
    .required(new KeywordArg("facing", new StringArg()))
    .required(new StringArg())
    .optional(new NumberArg())
    .description("Summons an entity with no ai, you can optionally provide an animation that it will loop");
new Command("summon", (source, entityId, anim, duration) => {
    noAISummon(source, entityId, { anim, duration });
})
    .required(new StringArg())
    .optional(new StringArg())
    .optional(new NumberArg());
new Command("summon", (source, entityId, location, anim, duration) => {
    noAISummon(source, entityId, { location, anim, duration });
})
    .required(new StringArg())
    .required(new CoordinateArg())
    .optional(new StringArg())
    .optional(new NumberArg());
new Command("summon", (source, entityId, location, target) => {
    noAISummon(source, entityId, { location, target });
})
    .required(new StringArg())
    .required(new CoordinateArg())
    .optional(new KeywordArg("facing", new StringArg()));
async function noAISummon(source, entityId, options) {
    var _a;
    options.location = (_a = options.location) !== null && _a !== void 0 ? _a : source.location;
    const entity = new SafetyCheckedEntity(source.dimension.spawnEntity(entityId, options.location));
    entity.runCommand(`aistop "${entity.id}"`);
    if (options.target) {
        entity.runCommand(`teleport @s ~ ~ ~ facing ${options.target}`);
    }
    await waitForTicks(5);
    if (options.anim) {
        const stopExpression = options.duration ? `query.anim_time >= ${options.duration}` : "false";
        entity.playAnimation(options.anim, { stopExpression });
    }
}
// #endregion
// #region Dev Toggle
new Command("dev", (source) => {
    var _a, _b;
    if (source.hasTag("dev")) {
        (_a = source.getPlayer()) === null || _a === void 0 ? void 0 : _a.sendMessage(TextFormat.aqua("Removing dev tag"));
        source.removeTag("dev");
    }
    else {
        (_b = source.getPlayer()) === null || _b === void 0 ? void 0 : _b.sendMessage(TextFormat.aqua("Adding dev tag"));
        source.addTag("dev");
    }
})
    .description("Toggle your project's dev tag on or off");
// #endregion
