"use strict";
/**
 * Core Nexus builtins shared by the tree-walk evaluator and bytecode VM.
 * Index order in BUILTIN_NAMES is stable for OpGetBuiltin.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.BUILTIN_NAMES = exports.BuiltinObj = void 0;
exports.objectsEqual = objectsEqual;
exports.createBuiltins = createBuiltins;
exports.getBuiltinByIndex = getBuiltinByIndex;
exports.installCoreBuiltins = installCoreBuiltins;
const values_1 = require("./values");
class BuiltinObj {
    fn;
    type = "BUILTIN";
    constructor(fn) {
        this.fn = fn;
    }
    inspect() {
        return "builtin function";
    }
}
exports.BuiltinObj = BuiltinObj;
exports.BUILTIN_NAMES = [
    "len",
    "puts",
    "str",
    "int",
    "type",
    "typeof",
    "push",
    "first",
    "last",
    "rest",
    "keys",
    "has",
    "get",
    "split",
    "join",
    "trim",
    "lower",
    "upper",
    "contains",
    "starts_with",
    "replace",
    "slice",
    "ok",
    "err",
    "is_ok",
    "is_err",
    "unwrap",
    "map",
    "filter",
    "assert",
    "assert_eq",
    "getenv",
    "escape_html",
    "merge",
];
function objectsEqual(a, b) {
    if (a === b) {
        return true;
    }
    if (a.type !== b.type) {
        return false;
    }
    if (a instanceof values_1.IntegerObj && b instanceof values_1.IntegerObj) {
        return a.value === b.value;
    }
    if (a instanceof values_1.StringObj && b instanceof values_1.StringObj) {
        return a.value === b.value;
    }
    if (a instanceof values_1.BooleanObj && b instanceof values_1.BooleanObj) {
        return a.value === b.value;
    }
    if (a instanceof values_1.ArrayObj && b instanceof values_1.ArrayObj) {
        if (a.elements.length !== b.elements.length) {
            return false;
        }
        return a.elements.every((el, i) => objectsEqual(el, b.elements[i]));
    }
    if (a instanceof values_1.HashObj && b instanceof values_1.HashObj) {
        if (a.pairs.size !== b.pairs.size) {
            return false;
        }
        for (const [k, pair] of a.pairs) {
            const other = b.pairs.get(k);
            if (!other || !objectsEqual(pair.value, other.value)) {
                return false;
            }
        }
        return true;
    }
    return a.inspect() === b.inspect();
}
function asResultOk(value) {
    const h = new values_1.HashObj();
    h.setString("ok", values_1.TRUE_OBJ);
    h.setString("value", value);
    h.setString("error", values_1.NULL_OBJ);
    return h;
}
function asResultErr(error) {
    const h = new values_1.HashObj();
    h.setString("ok", values_1.FALSE_OBJ);
    h.setString("value", values_1.NULL_OBJ);
    h.setString("error", error);
    return h;
}
function defaultApply(fn, args) {
    if (fn instanceof BuiltinObj) {
        return fn.fn(...args);
    }
    return (0, values_1.newError)(`not a function: ${fn.type}`);
}
function createBuiltins(host = {}) {
    const write = host.writeOutput ??
        ((line) => {
            // eslint-disable-next-line no-console
            console.log(line);
        });
    const apply = host.applyFunction ?? defaultApply;
    const map = new Map();
    const set = (name, fn) => {
        map.set(name, new BuiltinObj(fn));
    };
    set("len", (...args) => {
        if (args.length !== 1) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=1`);
        }
        const arg = args[0];
        if (arg instanceof values_1.StringObj) {
            return new values_1.IntegerObj(arg.value.length);
        }
        if (arg instanceof values_1.ArrayObj) {
            return new values_1.IntegerObj(arg.elements.length);
        }
        if (arg instanceof values_1.HashObj) {
            return new values_1.IntegerObj(arg.pairs.size);
        }
        return (0, values_1.newError)(`argument to \`len\` not supported, got ${arg.type}`);
    });
    set("puts", (...args) => {
        for (const arg of args) {
            write(arg.inspect());
        }
        return values_1.NULL_OBJ;
    });
    set("str", (...args) => {
        if (args.length !== 1) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=1`);
        }
        return new values_1.StringObj(args[0].inspect());
    });
    set("int", (...args) => {
        if (args.length !== 1) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=1`);
        }
        const arg = args[0];
        if (arg instanceof values_1.IntegerObj) {
            return arg;
        }
        if (arg instanceof values_1.StringObj) {
            const trimmed = arg.value.trim();
            if (trimmed === "") {
                return new values_1.IntegerObj(0);
            }
            const n = Number.parseInt(trimmed, 10);
            if (Number.isNaN(n)) {
                return (0, values_1.newError)(`cannot parse int: ${arg.value}`);
            }
            return new values_1.IntegerObj(n);
        }
        return (0, values_1.newError)(`int expects string or integer, got ${arg.type}`);
    });
    set("type", (...args) => {
        if (args.length !== 1) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=1`);
        }
        return new values_1.StringObj(args[0].type);
    });
    // Alias: `type` is a keyword in source, so call sites use `typeof`.
    set("typeof", (...args) => {
        if (args.length !== 1) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=1`);
        }
        return new values_1.StringObj(args[0].type);
    });
    set("push", (...args) => {
        if (args.length < 2) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want>=2`);
        }
        const arr = args[0];
        if (!(arr instanceof values_1.ArrayObj)) {
            return (0, values_1.newError)("push expects array as first argument");
        }
        return new values_1.ArrayObj([...arr.elements, ...args.slice(1)]);
    });
    set("first", (...args) => {
        if (args.length !== 1 || !(args[0] instanceof values_1.ArrayObj)) {
            return (0, values_1.newError)("first expects array");
        }
        return args[0].elements[0] ?? values_1.NULL_OBJ;
    });
    set("last", (...args) => {
        if (args.length !== 1 || !(args[0] instanceof values_1.ArrayObj)) {
            return (0, values_1.newError)("last expects array");
        }
        const el = args[0].elements;
        return el[el.length - 1] ?? values_1.NULL_OBJ;
    });
    set("rest", (...args) => {
        if (args.length !== 1 || !(args[0] instanceof values_1.ArrayObj)) {
            return (0, values_1.newError)("rest expects array");
        }
        return new values_1.ArrayObj(args[0].elements.slice(1));
    });
    set("keys", (...args) => {
        if (args.length !== 1 || !(args[0] instanceof values_1.HashObj)) {
            return (0, values_1.newError)("keys expects hash");
        }
        const keys = [];
        for (const { key } of args[0].pairs.values()) {
            keys.push(key);
        }
        return new values_1.ArrayObj(keys);
    });
    set("has", (...args) => {
        if (args.length !== 2 || !(args[0] instanceof values_1.HashObj)) {
            return (0, values_1.newError)("has expects (hash, key)");
        }
        const key = args[1];
        for (const { key: k } of args[0].pairs.values()) {
            if (objectsEqual(k, key)) {
                return values_1.TRUE_OBJ;
            }
        }
        return values_1.FALSE_OBJ;
    });
    set("get", (...args) => {
        if (args.length !== 2 || !(args[0] instanceof values_1.HashObj)) {
            return (0, values_1.newError)("get expects (hash, key)");
        }
        return args[0].get(args[1]);
    });
    set("split", (...args) => {
        if (args.length !== 2) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=2`);
        }
        if (!(args[0] instanceof values_1.StringObj) || !(args[1] instanceof values_1.StringObj)) {
            return (0, values_1.newError)("split expects (string, separator)");
        }
        return new values_1.ArrayObj(args[0].value.split(args[1].value).map((s) => new values_1.StringObj(s)));
    });
    set("join", (...args) => {
        if (args.length !== 2) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=2`);
        }
        if (!(args[0] instanceof values_1.ArrayObj) || !(args[1] instanceof values_1.StringObj)) {
            return (0, values_1.newError)("join expects (array, separator)");
        }
        return new values_1.StringObj(args[0].elements.map((e) => e.inspect()).join(args[1].value));
    });
    set("trim", (...args) => {
        if (args.length !== 1 || !(args[0] instanceof values_1.StringObj)) {
            return (0, values_1.newError)("trim expects string");
        }
        return new values_1.StringObj(args[0].value.trim());
    });
    set("lower", (...args) => {
        if (args.length !== 1 || !(args[0] instanceof values_1.StringObj)) {
            return (0, values_1.newError)("lower expects string");
        }
        return new values_1.StringObj(args[0].value.toLowerCase());
    });
    set("upper", (...args) => {
        if (args.length !== 1 || !(args[0] instanceof values_1.StringObj)) {
            return (0, values_1.newError)("upper expects string");
        }
        return new values_1.StringObj(args[0].value.toUpperCase());
    });
    set("contains", (...args) => {
        if (args.length !== 2) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=2`);
        }
        if (args[0] instanceof values_1.StringObj && args[1] instanceof values_1.StringObj) {
            return (0, values_1.nativeBool)(args[0].value.includes(args[1].value));
        }
        if (args[0] instanceof values_1.ArrayObj) {
            return (0, values_1.nativeBool)(args[0].elements.some((e) => objectsEqual(e, args[1])));
        }
        return (0, values_1.newError)("contains expects (string, string) or (array, value)");
    });
    set("starts_with", (...args) => {
        if (args.length !== 2 ||
            !(args[0] instanceof values_1.StringObj) ||
            !(args[1] instanceof values_1.StringObj)) {
            return (0, values_1.newError)("starts_with expects (string, prefix)");
        }
        return (0, values_1.nativeBool)(args[0].value.startsWith(args[1].value));
    });
    set("replace", (...args) => {
        if (args.length !== 3 ||
            !(args[0] instanceof values_1.StringObj) ||
            !(args[1] instanceof values_1.StringObj) ||
            !(args[2] instanceof values_1.StringObj)) {
            return (0, values_1.newError)("replace expects (string, from, to)");
        }
        return new values_1.StringObj(args[0].value.split(args[1].value).join(args[2].value));
    });
    set("slice", (...args) => {
        if (args.length < 2 || args.length > 3) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=2 or 3`);
        }
        const startObj = args[1];
        if (!(startObj instanceof values_1.IntegerObj)) {
            return (0, values_1.newError)("slice start must be integer");
        }
        const start = startObj.value;
        if (args[0] instanceof values_1.ArrayObj) {
            let end = args[0].elements.length;
            if (args.length === 3) {
                if (!(args[2] instanceof values_1.IntegerObj)) {
                    return (0, values_1.newError)("slice end must be integer");
                }
                end = args[2].value;
            }
            return new values_1.ArrayObj(args[0].elements.slice(start, end));
        }
        if (args[0] instanceof values_1.StringObj) {
            let end = args[0].value.length;
            if (args.length === 3) {
                if (!(args[2] instanceof values_1.IntegerObj)) {
                    return (0, values_1.newError)("slice end must be integer");
                }
                end = args[2].value;
            }
            return new values_1.StringObj(args[0].value.slice(start, end));
        }
        return (0, values_1.newError)("slice expects array or string");
    });
    set("ok", (...args) => {
        if (args.length !== 1) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=1`);
        }
        return asResultOk(args[0]);
    });
    set("err", (...args) => {
        if (args.length !== 1) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=1`);
        }
        return asResultErr(args[0]);
    });
    set("is_ok", (...args) => {
        if (args.length !== 1) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=1`);
        }
        if (!(args[0] instanceof values_1.HashObj)) {
            return values_1.FALSE_OBJ;
        }
        const flag = args[0].getString("ok");
        return (0, values_1.nativeBool)(flag instanceof values_1.BooleanObj && flag.value);
    });
    set("is_err", (...args) => {
        if (args.length !== 1) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=1`);
        }
        if (!(args[0] instanceof values_1.HashObj)) {
            return values_1.FALSE_OBJ;
        }
        const flag = args[0].getString("ok");
        return (0, values_1.nativeBool)(flag instanceof values_1.BooleanObj && !flag.value);
    });
    set("unwrap", (...args) => {
        if (args.length !== 1) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=1`);
        }
        if (!(args[0] instanceof values_1.HashObj)) {
            return (0, values_1.newError)("unwrap expects Result hash");
        }
        const flag = args[0].getString("ok");
        if (!(flag instanceof values_1.BooleanObj)) {
            return (0, values_1.newError)("unwrap expects Result with ok field");
        }
        if (flag.value) {
            return args[0].getString("value");
        }
        return (0, values_1.newError)(`unwrap on Err: ${args[0].getString("error").inspect()}`);
    });
    set("assert", (...args) => {
        if (args.length < 1 || args.length > 2) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=1 or 2`);
        }
        let msg = "assertion failed";
        if (args.length === 2) {
            msg =
                args[1] instanceof values_1.StringObj ? args[1].value : args[1].inspect();
        }
        if (!(0, values_1.isTruthy)(args[0])) {
            return (0, values_1.newError)(msg);
        }
        return values_1.TRUE_OBJ;
    });
    set("assert_eq", (...args) => {
        if (args.length < 2 || args.length > 3) {
            return (0, values_1.newError)(`wrong number of arguments. got=${args.length}, want=2 or 3`);
        }
        if (objectsEqual(args[0], args[1])) {
            return values_1.TRUE_OBJ;
        }
        let msg = `assert_eq failed: got ${args[0].inspect()}, want ${args[1].inspect()}`;
        if (args.length === 3) {
            const label = args[2] instanceof values_1.StringObj ? args[2].value : args[2].inspect();
            msg = `${label}: ${msg}`;
        }
        return (0, values_1.newError)(msg);
    });
    set("getenv", (...args) => {
        if (args.length !== 1 || !(args[0] instanceof values_1.StringObj)) {
            return (0, values_1.newError)("getenv expects string");
        }
        return new values_1.StringObj(process.env[args[0].value] ?? "");
    });
    set("escape_html", (...args) => {
        if (args.length !== 1 || !(args[0] instanceof values_1.StringObj)) {
            return (0, values_1.newError)("escape_html expects string");
        }
        const s = args[0].value
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#39;");
        return new values_1.StringObj(s);
    });
    set("merge", (...args) => {
        if (args.length < 1) {
            return (0, values_1.newError)("merge expects one or more hashes");
        }
        const out = new values_1.HashObj();
        for (const arg of args) {
            if (!(arg instanceof values_1.HashObj)) {
                return (0, values_1.newError)("merge expects hashes");
            }
            for (const { key, value } of arg.pairs.values()) {
                out.set(key, value);
            }
        }
        return out;
    });
    set("map", (...args) => {
        if (args.length !== 2 || !(args[0] instanceof values_1.ArrayObj)) {
            return (0, values_1.newError)("map expects (array, fn)");
        }
        const out = [];
        for (const el of args[0].elements) {
            const mapped = apply(args[1], [el]);
            if ((0, values_1.isError)(mapped)) {
                return mapped;
            }
            out.push(mapped);
        }
        return new values_1.ArrayObj(out);
    });
    set("filter", (...args) => {
        if (args.length !== 2 || !(args[0] instanceof values_1.ArrayObj)) {
            return (0, values_1.newError)("filter expects (array, fn)");
        }
        const out = [];
        for (const el of args[0].elements) {
            const keep = apply(args[1], [el]);
            if ((0, values_1.isError)(keep)) {
                return keep;
            }
            if ((0, values_1.isTruthy)(keep)) {
                out.push(el);
            }
        }
        return new values_1.ArrayObj(out);
    });
    return map;
}
function getBuiltinByIndex(index, host) {
    const name = exports.BUILTIN_NAMES[index];
    if (!name) {
        return undefined;
    }
    return createBuiltins(host).get(name);
}
function installCoreBuiltins(env, host = {}) {
    const builtins = createBuiltins(host);
    for (const [name, builtin] of builtins) {
        env.set(name, builtin);
    }
    return builtins;
}
//# sourceMappingURL=builtins.js.map