/**
 * Buddy Script Runtime
 *
 * Executes parsed AST nodes
 */
import { DEFAULT_SCRIPT_CONFIG, } from './types.js';
import { createBuiltins } from './builtins.js';
// Special return value for control flow
class ReturnValue {
    value;
    constructor(value) {
        this.value = value;
    }
}
class BreakSignal {
}
class ContinueSignal {
}
export class Runtime {
    config;
    globalContext;
    output = [];
    startTime = 0;
    constructor(config = {}) {
        this.config = { ...DEFAULT_SCRIPT_CONFIG, ...config };
        this.globalContext = {
            variables: new Map(),
            functions: new Map(),
        };
        // Initialize builtins
        this.initializeBuiltins();
    }
    initializeBuiltins() {
        const builtins = createBuiltins(this.config, (msg) => {
            this.output.push(msg);
        });
        for (const [name, fn] of Object.entries(builtins)) {
            this.globalContext.functions.set(name, fn);
        }
        // Inject user variables
        if (this.config.variables) {
            for (const [name, value] of Object.entries(this.config.variables)) {
                this.globalContext.variables.set(name, value);
            }
        }
    }
    async execute(program) {
        this.startTime = Date.now();
        this.output = [];
        let returnValue = null;
        try {
            for (const statement of program.body) {
                const result = await this.executeStatement(statement, this.globalContext);
                if (result instanceof ReturnValue) {
                    returnValue = result.value;
                    break;
                }
            }
        }
        catch (error) {
            if (error instanceof ReturnValue) {
                returnValue = error.value;
            }
            else {
                throw error;
            }
        }
        return {
            output: this.output,
            returnValue,
            duration: Date.now() - this.startTime,
        };
    }
    async executeStatement(stmt, ctx) {
        // Check timeout
        if (Date.now() - this.startTime > this.config.timeout) {
            throw new Error(`Script execution timed out after ${this.config.timeout}ms. Consider breaking the script into smaller parts or increasing the timeout limit.`);
        }
        switch (stmt.type) {
            case 'VariableDeclaration':
                return this.executeVariableDeclaration(stmt, ctx);
            case 'FunctionDeclaration':
                return this.executeFunctionDeclaration(stmt, ctx);
            case 'ExpressionStatement':
                return this.evaluateExpression(stmt.expression, ctx);
            case 'IfStatement':
                return this.executeIfStatement(stmt, ctx);
            case 'WhileStatement':
                return this.executeWhileStatement(stmt, ctx);
            case 'ForStatement':
                return this.executeForStatement(stmt, ctx);
            case 'ForInStatement':
                return this.executeForInStatement(stmt, ctx);
            case 'ReturnStatement':
                return this.executeReturnStatement(stmt, ctx);
            case 'TryStatement':
                return this.executeTryStatement(stmt, ctx);
            case 'ThrowStatement':
                return this.executeThrowStatement(stmt, ctx);
            case 'BreakStatement':
                return new BreakSignal();
            case 'ContinueStatement':
                return new ContinueSignal();
            case 'BlockStatement':
                return this.executeBlock(stmt, ctx);
            case 'ImportStatement':
                // Imports are handled at initialization
                return null;
            default:
                throw new Error(`Unsupported statement type "${stmt.type}". This may be a syntax error or an unsupported feature.`);
        }
    }
    async executeVariableDeclaration(stmt, ctx) {
        const value = stmt.init ? await this.evaluateExpression(stmt.init, ctx) : null;
        ctx.variables.set(stmt.name, value);
        return null;
    }
    async executeFunctionDeclaration(stmt, ctx) {
        const fn = async (...args) => {
            const localCtx = {
                variables: new Map(),
                functions: new Map(ctx.functions),
                parent: ctx,
            };
            // Bind parameters
            for (let i = 0; i < stmt.params.length; i++) {
                const param = stmt.params[i];
                const value = args[i] !== undefined ? args[i] : (param.defaultValue ? await this.evaluateExpression(param.defaultValue, ctx) : null);
                localCtx.variables.set(param.name, value);
            }
            try {
                const result = await this.executeBlock(stmt.body, localCtx);
                if (result instanceof ReturnValue) {
                    return result.value;
                }
                return null;
            }
            catch (error) {
                if (error instanceof ReturnValue) {
                    return error.value;
                }
                throw error;
            }
        };
        ctx.functions.set(stmt.name, fn);
        ctx.variables.set(stmt.name, fn);
        return null;
    }
    async executeIfStatement(stmt, ctx) {
        const condition = await this.evaluateExpression(stmt.condition, ctx);
        if (this.isTruthy(condition)) {
            return this.executeBlock(stmt.consequent, ctx);
        }
        else if (stmt.alternate) {
            if (stmt.alternate.type === 'IfStatement') {
                return this.executeIfStatement(stmt.alternate, ctx);
            }
            else {
                return this.executeBlock(stmt.alternate, ctx);
            }
        }
        return null;
    }
    async executeWhileStatement(stmt, ctx) {
        while (this.isTruthy(await this.evaluateExpression(stmt.condition, ctx))) {
            const result = await this.executeBlock(stmt.body, ctx);
            if (result instanceof ReturnValue)
                return result;
            if (result instanceof BreakSignal)
                break;
            if (result instanceof ContinueSignal)
                continue;
        }
        return null;
    }
    async executeForStatement(stmt, ctx) {
        // Create local context for loop-scoped variables (like the init var)
        // Use empty map - only loop variables go here, parent lookup for outer vars
        const localCtx = {
            variables: new Map(),
            functions: ctx.functions,
            parent: ctx,
        };
        // Execute init
        if (stmt.init) {
            if (stmt.init.type === 'VariableDeclaration') {
                await this.executeVariableDeclaration(stmt.init, localCtx);
            }
            else {
                await this.evaluateExpression(stmt.init, localCtx);
            }
        }
        // Loop while test is true
        while (stmt.test === null || this.isTruthy(await this.evaluateExpression(stmt.test, localCtx))) {
            const result = await this.executeBlock(stmt.body, localCtx);
            if (result instanceof ReturnValue)
                return result;
            if (result instanceof BreakSignal)
                break;
            if (result instanceof ContinueSignal) {
                // Still run the update on continue
                if (stmt.update) {
                    await this.evaluateExpression(stmt.update, localCtx);
                }
                continue;
            }
            // Execute update
            if (stmt.update) {
                await this.evaluateExpression(stmt.update, localCtx);
            }
        }
        return null;
    }
    async executeForInStatement(stmt, ctx) {
        const iterable = await this.evaluateExpression(stmt.iterable, ctx);
        if (!Array.isArray(iterable) && typeof iterable !== 'object') {
            throw new Error('for-in loop requires an array or object to iterate over. Got: ' + (typeof iterable));
        }
        const items = Array.isArray(iterable) ? iterable : Object.entries(iterable);
        // Create local context with only the loop variable - parent lookup for outer vars
        const localCtx = {
            variables: new Map(),
            functions: ctx.functions,
            parent: ctx,
        };
        for (const item of items) {
            localCtx.variables.set(stmt.variable, item);
            const result = await this.executeBlock(stmt.body, localCtx);
            if (result instanceof ReturnValue)
                return result;
            if (result instanceof BreakSignal)
                break;
            if (result instanceof ContinueSignal)
                continue;
        }
        return null;
    }
    async executeReturnStatement(stmt, ctx) {
        const value = stmt.argument ? await this.evaluateExpression(stmt.argument, ctx) : null;
        return new ReturnValue(value);
    }
    async executeTryStatement(stmt, ctx) {
        try {
            return await this.executeBlock(stmt.block, ctx);
        }
        catch (error) {
            if (stmt.handler) {
                const localCtx = {
                    variables: new Map(ctx.variables),
                    functions: ctx.functions,
                    parent: ctx,
                };
                const errorValue = error instanceof Error ? {
                    message: error.message,
                    stack: error.stack,
                } : error;
                localCtx.variables.set(stmt.handler.param, errorValue);
                return this.executeBlock(stmt.handler.body, localCtx);
            }
            throw error;
        }
    }
    async executeThrowStatement(stmt, ctx) {
        const value = await this.evaluateExpression(stmt.argument, ctx);
        if (typeof value === 'string') {
            throw new Error(value);
        }
        throw value;
    }
    async executeBlock(block, ctx) {
        // Use the same context - don't create a new scope for blocks
        // New variables declared inside will still be added to this context
        // but assignments to existing variables will propagate up via setVariable
        for (const stmt of block.body) {
            const result = await this.executeStatement(stmt, ctx);
            if (result instanceof ReturnValue || result instanceof BreakSignal || result instanceof ContinueSignal) {
                return result;
            }
        }
        return null;
    }
    // ============================================
    // Expression Evaluation
    // ============================================
    async evaluateExpression(expr, ctx) {
        switch (expr.type) {
            case 'Literal':
                return expr.value;
            case 'Identifier':
                return this.lookupVariable(expr.name, ctx);
            case 'BinaryExpression':
                return this.evaluateBinaryExpression(expr, ctx);
            case 'UnaryExpression':
                return this.evaluateUnaryExpression(expr, ctx);
            case 'LogicalExpression':
                return this.evaluateLogicalExpression(expr, ctx);
            case 'AssignmentExpression':
                return this.evaluateAssignmentExpression(expr, ctx);
            case 'CallExpression':
                return this.evaluateCallExpression(expr, ctx);
            case 'MemberExpression':
                return this.evaluateMemberExpression(expr, ctx);
            case 'ArrayExpression':
                return this.evaluateArrayExpression(expr, ctx);
            case 'ObjectExpression':
                return this.evaluateObjectExpression(expr, ctx);
            case 'ConditionalExpression':
                return this.evaluateConditionalExpression(expr, ctx);
            case 'AwaitExpression':
                return this.evaluateExpression(expr.argument, ctx);
            case 'ArrowFunction':
                return this.createArrowFunction(expr, ctx);
            default:
                throw new Error(`Unsupported expression type "${expr.type}". Check syntax or upgrade to a newer version.`);
        }
    }
    lookupVariable(name, ctx) {
        if (ctx.variables.has(name)) {
            return ctx.variables.get(name);
        }
        if (ctx.functions.has(name)) {
            return ctx.functions.get(name);
        }
        if (ctx.parent) {
            return this.lookupVariable(name, ctx.parent);
        }
        throw new Error(`Variable "${name}" is not defined. Check the variable name or define it before use.`);
    }
    async evaluateBinaryExpression(expr, ctx) {
        const left = await this.evaluateExpression(expr.left, ctx);
        const right = await this.evaluateExpression(expr.right, ctx);
        switch (expr.operator) {
            case '+':
                if (typeof left === 'string' || typeof right === 'string') {
                    return String(left) + String(right);
                }
                return left + right;
            case '-': return left - right;
            case '*':
                // Support string repetition: "x" * 3 = "xxx"
                if (typeof left === 'string' && typeof right === 'number') {
                    return left.repeat(right);
                }
                return left * right;
            case '/': return left / right;
            case '%': return left % right;
            case '**': return Math.pow(left, right);
            case '==':
            case '===': return left === right;
            case '!=':
            case '!==': return left !== right;
            case '<': return left < right;
            case '<=': return left <= right;
            case '>': return left > right;
            case '>=': return left >= right;
            default:
                throw new Error(`Unsupported binary operator "${expr.operator}". Valid operators: +, -, *, /, %, **, ==, !=, <, <=, >, >=`);
        }
    }
    async evaluateUnaryExpression(expr, ctx) {
        const argument = await this.evaluateExpression(expr.argument, ctx);
        switch (expr.operator) {
            case '-': return -argument;
            case '!': return !this.isTruthy(argument);
            default:
                throw new Error(`Unsupported unary operator "${expr.operator}". Valid operators: -, !`);
        }
    }
    async evaluateLogicalExpression(expr, ctx) {
        const left = await this.evaluateExpression(expr.left, ctx);
        if (expr.operator === '&&') {
            if (!this.isTruthy(left))
                return left;
            return this.evaluateExpression(expr.right, ctx);
        }
        else {
            if (this.isTruthy(left))
                return left;
            return this.evaluateExpression(expr.right, ctx);
        }
    }
    async evaluateAssignmentExpression(expr, ctx) {
        let value = await this.evaluateExpression(expr.right, ctx);
        if (expr.operator === '+=') {
            const current = await this.evaluateExpression(expr.left, ctx);
            if (typeof current === 'string' || typeof value === 'string') {
                value = String(current) + String(value);
            }
            else {
                value = current + value;
            }
        }
        else if (expr.operator === '-=') {
            const current = await this.evaluateExpression(expr.left, ctx);
            value = current - value;
        }
        if (expr.left.type === 'Identifier') {
            this.setVariable(expr.left.name, value, ctx);
        }
        else if (expr.left.type === 'MemberExpression') {
            const member = expr.left;
            const object = await this.evaluateExpression(member.object, ctx);
            const property = member.computed
                ? await this.evaluateExpression(member.property, ctx)
                : member.property.name;
            object[property] = value;
        }
        return value;
    }
    setVariable(name, value, ctx) {
        // Find where the variable is defined
        let current = ctx;
        while (current) {
            if (current.variables.has(name)) {
                current.variables.set(name, value);
                return;
            }
            current = current.parent;
        }
        // If not found, create in current context
        ctx.variables.set(name, value);
    }
    async evaluateCallExpression(expr, ctx) {
        const callee = await this.evaluateExpression(expr.callee, ctx);
        if (typeof callee !== 'function') {
            throw new Error(`Cannot call ${JSON.stringify(callee)}: it is not a function. Check if the function name is correct.`);
        }
        const args = [];
        for (const arg of expr.arguments) {
            args.push(await this.evaluateExpression(arg, ctx));
        }
        return callee(...args);
    }
    async evaluateMemberExpression(expr, ctx) {
        const object = await this.evaluateExpression(expr.object, ctx);
        if (object === null || object === undefined) {
            throw new Error('Cannot read property of null or undefined. Ensure the object exists before accessing its properties.');
        }
        const property = expr.computed
            ? await this.evaluateExpression(expr.property, ctx)
            : expr.property.name;
        const obj = object;
        const value = obj[property];
        // Bind methods
        if (typeof value === 'function') {
            return value.bind(obj);
        }
        return value;
    }
    async evaluateArrayExpression(expr, ctx) {
        const elements = [];
        for (const element of expr.elements) {
            elements.push(await this.evaluateExpression(element, ctx));
        }
        return elements;
    }
    async evaluateObjectExpression(expr, ctx) {
        const obj = {};
        for (const prop of expr.properties) {
            const key = typeof prop.key === 'string'
                ? prop.key
                : await this.evaluateExpression(prop.key, ctx);
            obj[key] = await this.evaluateExpression(prop.value, ctx);
        }
        return obj;
    }
    async evaluateConditionalExpression(expr, ctx) {
        const test = await this.evaluateExpression(expr.test, ctx);
        if (this.isTruthy(test)) {
            return this.evaluateExpression(expr.consequent, ctx);
        }
        return this.evaluateExpression(expr.alternate, ctx);
    }
    createArrowFunction(expr, ctx) {
        const arrow = expr;
        return async (...args) => {
            const localCtx = {
                variables: new Map(ctx.variables),
                functions: ctx.functions,
                parent: ctx,
            };
            for (let i = 0; i < arrow.params.length; i++) {
                localCtx.variables.set(arrow.params[i].name, args[i]);
            }
            if (arrow.body.type === 'BlockStatement') {
                const result = await this.executeBlock(arrow.body, localCtx);
                if (result instanceof ReturnValue) {
                    return result.value;
                }
                return null;
            }
            return this.evaluateExpression(arrow.body, localCtx);
        };
    }
    isTruthy(value) {
        if (value === null || value === undefined || value === false || value === 0 || value === '') {
            return false;
        }
        return true;
    }
}
//# sourceMappingURL=runtime.js.map