// Iris Language Interpreter (Browser + Node.js)
// Created by Muhammad Razhif Elfateh
// Supports: vars, funcs, if/else, for, while, arrays, objects, print, import

const fs = require('fs');
const path = require('path');

const Iris = (() => {
    // ===================== LEXER =====================
    function tokenize(source) {
        const tokens = [];
        const lines = source.split('\n');
        for (let li = 0; li < lines.length; li++) {
            let line = lines[li];
            let col = 0;
            while (col < line.length) {
                while (col < line.length && /\s/.test(line[col])) col++;
                if (col >= line.length) break;
                if (line[col] === '/' && line[col+1] === '/') break;
                if (line[col] === '/' && line[col+1] === '*') {
                    col += 2;
                    while (col < line.length - 1 && !(line[col] === '*' && line[col+1] === '/')) col++;
                    col += 2;
                    continue;
                }
                const ch = line[col];
                if (ch === '"') {
                    let str = '';
                    col++;
                    while (col < line.length && line[col] !== '"') {
                        if (line[col] === '\\') {
                            col++;
                            str += line[col] === 'n' ? '\n' : line[col];
                        } else {
                            str += line[col];
                        }
                        col++;
                    }
                    col++;
                    tokens.push({ type: 'STRING', value: str, line: li+1 });
                    continue;
                }
                if (/[0-9]/.test(ch)) {
                    let num = '';
                    while (col < line.length && /[0-9.]/.test(line[col])) num += line[col++];
                    tokens.push({ type: 'NUMBER', value: parseFloat(num), line: li+1 });
                    continue;
                }
                if (/[a-zA-Z_]/.test(ch)) {
                    let id = '';
                    while (col < line.length && /[a-zA-Z0-9_]/.test(line[col])) id += line[col++];
                    const keywords = ['var','val','fn','if','else','for','while','return','break','continue','true','false','null','and','or','not','in','import'];
                    tokens.push({ type: keywords.includes(id) ? id.toUpperCase() : 'IDENT', value: id, line: li+1 });
                    continue;
                }
                const two = line.substring(col, col+2);
                if (['==','!=','<=','>=','+=','-=','*=','/=','=>'].includes(two)) {
                    tokens.push({ type: two, value: two, line: li+1 });
                    col += 2;
                    continue;
                }
                tokens.push({ type: ch, value: ch, line: li+1 });
                col++;
            }
        }
        tokens.push({ type: 'EOF', value: '', line: lines.length });
        return tokens;
    }

    // ===================== PARSER =====================
    function parse(tokens) {
        let pos = 0;
        const peek = () => tokens[pos];
        const advance = () => tokens[pos++];
        const match = (...types) => { if (types.includes(peek().type)) { pos++; return true; } return false; };
        const expect = (type, msg) => { if (peek().type !== type) throw new Error(`${msg} at line ${peek().line}`); return advance(); };

        function parseProgram() {
            const stmts = [];
            while (peek().type !== 'EOF') stmts.push(parseStmt());
            return stmts;
        }

        function parseStmt() {
            if (match('IMPORT')) {
                const path = expect('STRING', "Expected file path string").value;
                return { type: 'import', path };
            }
            if (match('VAR', 'VAL')) {
                const kind = tokens[pos-1].type === 'VAL' ? 'val' : 'var';
                const name = expect('IDENT', 'Expected variable name').value;
                let init = null;
                if (match('=')) init = parseExpr();
                return { type: 'vardecl', kind, name, init };
            }
            if (match('FN')) {
                const name = expect('IDENT', 'Expected function name').value;
                expect('(', "Expected '('");
                const params = [];
                if (peek().type !== ')') do { params.push(expect('IDENT', 'Expected parameter').value); } while (match(','));
                expect(')', "Expected ')'");
                const body = parseBlock();
                return { type: 'funcdecl', name, params, body };
            }
            if (match('IF')) {
                const cond = parseExpr();
                const thenBranch = parseBlock();
                let elseBranch = null;
                if (match('ELSE')) {
                    if (peek().type === 'IF') elseBranch = [parseStmt()];
                    else elseBranch = parseBlock();
                }
                return { type: 'if', cond, thenBranch, elseBranch };
            }
            if (match('FOR')) {
                const varName = expect('IDENT', 'Expected loop variable').value;
                expect('IN', "Expected 'in'");
                const iterable = parseExpr();
                const body = parseBlock();
                return { type: 'for', varName, iterable, body };
            }
            if (match('WHILE')) {
                const cond = parseExpr();
                const body = parseBlock();
                return { type: 'while', cond, body };
            }
            if (match('RETURN')) return { type: 'return', value: match(';') ? null : parseExpr() };
            if (match('BREAK')) return { type: 'break' };
            if (match('CONTINUE')) return { type: 'continue' };
            return parseExprStmt();
        }

        function parseBlock() {
            expect('{', "Expected '{'");
            const stmts = [];
            while (!match('}')) {
                if (peek().type === 'EOF') break;
                stmts.push(parseStmt());
            }
            return stmts;
        }

        function parseExprStmt() {
            return { type: 'exprstmt', expr: parseExpr() };
        }

        function parseExpr() { return parseAssign(); }

        function parseAssign() {
            let left = parseOr();
            if (match('=')) {
                const right = parseAssign();
                if (left.type === 'ident') return { type: 'assign', name: left.name, right };
                if (left.type === 'member') return { type: 'setmember', object: left.object, property: left.property, right };
                if (left.type === 'index') return { type: 'setindex', object: left.object, index: left.index, right };
                throw new Error('Invalid assignment target');
            }
            if (match('+=', '-=', '*=', '/=')) {
                const op = tokens[pos-1].type[0];
                return { type: 'assign', name: left.name, right: { type: 'binop', op, left, right: parseAssign() } };
            }
            return left;
        }

        function parseOr() {
            let left = parseAnd();
            while (match('OR')) left = { type: 'binop', op: '||', left, right: parseAnd() };
            return left;
        }

        function parseAnd() {
            let left = parseEquality();
            while (match('AND')) left = { type: 'binop', op: '&&', left, right: parseEquality() };
            return left;
        }

        function parseEquality() {
            let left = parseComparison();
            while (match('==', '!=')) left = { type: 'binop', op: tokens[pos-1].value, left, right: parseComparison() };
            return left;
        }

        function parseComparison() {
            let left = parseTerm();
            while (match('<', '>', '<=', '>=')) left = { type: 'binop', op: tokens[pos-1].value, left, right: parseTerm() };
            return left;
        }

        function parseTerm() {
            let left = parseFactor();
            while (match('+', '-')) left = { type: 'binop', op: tokens[pos-1].value, left, right: parseFactor() };
            return left;
        }

        function parseFactor() {
            let left = parseUnary();
            while (match('*', '/', '%')) left = { type: 'binop', op: tokens[pos-1].value, left, right: parseUnary() };
            return left;
        }

        function parseUnary() {
            if (match('NOT', '-')) return { type: 'unary', op: tokens[pos-1].type === 'NOT' ? '!' : '-', right: parseUnary() };
            return parseCall();
        }

        function parseCall() {
            let expr = parsePrimary();
            while (true) {
                if (match('(')) {
                    const args = [];
                    if (peek().type !== ')') do { args.push(parseExpr()); } while (match(','));
                    expect(')', "Expected ')'");
                    expr = { type: 'call', callee: expr, args };
                } else if (match('.')) {
                    expr = { type: 'member', object: expr, property: expect('IDENT', 'Expected property').value };
                } else if (match('[')) {
                    const idx = parseExpr();
                    expect(']', "Expected ']'");
                    expr = { type: 'index', object: expr, index: idx };
                } else break;
            }
            return expr;
        }

        function parsePrimary() {
            if (match('NULL')) return { type: 'null' };
            if (match('TRUE')) return { type: 'bool', value: true };
            if (match('FALSE')) return { type: 'bool', value: false };
            if (peek().type === 'NUMBER') return { type: 'number', value: advance().value };
            if (peek().type === 'STRING') return { type: 'string', value: advance().value };
            if (match('[')) {
                const elems = [];
                if (peek().type !== ']') do { elems.push(parseExpr()); } while (match(','));
                expect(']', "Expected ']'");
                return { type: 'array', elements: elems };
            }
            if (match('{')) {
                const pairs = [];
                if (peek().type !== '}') {
                    do {
                        const k = expect('IDENT', 'Expected key').value;
                        expect(':', "Expected ':'");
                        pairs.push([k, parseExpr()]);
                    } while (match(','));
                }
                expect('}', "Expected '}'");
                return { type: 'object', pairs };
            }
            if (match('(')) {
                const e = parseExpr();
                expect(')', "Expected ')'");
                return e;
            }
            if (peek().type === 'IDENT') return { type: 'ident', name: advance().value };
            throw new Error(`Unexpected token ${peek().type} at line ${peek().line}`);
        }

        return parseProgram();
    }

    // ===================== EVALUATOR =====================
    function evaluate(stmts, env = {}, basePath = '.', imported = new Set()) {
        let lastValue = null;

        function exec(stmts, localEnv, isLoop = false) {
            let last = null;
            for (const stmt of stmts) {
                const v = execStmt(stmt, localEnv, isLoop);
                if (v && (v.__return || v.__break || v.__continue)) return v;
                last = v;
            }
            return last;
        }

        function execStmt(node, localEnv, isLoop = false) {
            switch (node.type) {
                case 'import': {
                    const filePath = path.resolve(basePath, node.path);
                    if (imported.has(filePath)) return null; // prevent circular import
                    if (!fs.existsSync(filePath)) throw new Error(`Import file not found: ${node.path}`);
                    imported.add(filePath);
                    const source = fs.readFileSync(filePath, 'utf8');
                    const tokens = tokenize(source);
                    const ast = parse(tokens);
                    const importBase = path.dirname(filePath);
                    evaluate(ast, env, importBase, imported);
                    return null;
                }
                case 'vardecl': {
                    const val = node.init ? evalExpr(node.init, localEnv) : null;
                    if (!localEnv.__scope) localEnv.__scope = {};
                    localEnv.__scope[node.name] = val;
                    return null;
                }
                case 'funcdecl': {
                    if (!env.__funcs) env.__funcs = {};
                    const fnObj = { params: node.params, body: node.body, closure: { ...localEnv } };
                    env.__funcs[node.name] = fnObj;
                    if (!localEnv.__scope) localEnv.__scope = {};
                    localEnv.__scope[node.name] = fnObj;
                    return null;
                }
                case 'exprstmt': return evalExpr(node.expr, localEnv);
                case 'if': {
                    if (evalExpr(node.cond, localEnv)) return exec(node.thenBranch, { ...localEnv }, isLoop);
                    if (node.elseBranch) return exec(node.elseBranch, { ...localEnv }, isLoop);
                    return null;
                }
                case 'for': {
                    const iterable = evalExpr(node.iterable, localEnv);
                    let last = null;
                    if (Array.isArray(iterable)) {
                        for (const item of iterable) {
                            const loopEnv = { ...localEnv, [node.varName]: item };
                            const v = exec(node.body, loopEnv, true);
                            if (v && v.__return) return v;
                            if (v && v.__break) break;
                            last = item;
                        }
                    }
                    return last;
                }
                case 'while': {
                    let last = null;
                    while (evalExpr(node.cond, localEnv)) {
                        const v = exec(node.body, { ...localEnv }, true);
                        if (v && v.__return) return v;
                        if (v && v.__break) break;
                    }
                    return last;
                }
                case 'return': return { __return: true, value: node.value ? evalExpr(node.value, localEnv) : null };
                case 'break': {
                    if (!isLoop) throw new Error('break outside loop');
                    return { __break: true };
                }
                case 'continue': {
                    if (!isLoop) throw new Error('continue outside loop');
                    return { __continue: true };
                }
            }
            return null;
        }

        function evalExpr(node, localEnv) {
            switch (node.type) {
                case 'null': return null;
                case 'bool': return node.value;
                case 'number': return node.value;
                case 'string': return node.value;
                case 'array': return node.elements.map(e => evalExpr(e, localEnv));
                case 'object': {
                    const obj = {};
                    for (const [k, v] of node.pairs) obj[k] = evalExpr(v, localEnv);
                    return obj;
                }
                case 'ident': {
                    if (localEnv.__scope && localEnv.__scope[node.name] !== undefined) return localEnv.__scope[node.name];
                    if (localEnv[node.name] !== undefined) return localEnv[node.name];
                    if (env.__scope && env.__scope[node.name] !== undefined) return env.__scope[node.name];
                    if (env[node.name] !== undefined) return env[node.name];
                    throw new Error(`Undefined variable: ${node.name}`);
                }
                case 'member': {
                    const obj = evalExpr(node.object, localEnv);
                    if (obj === null || obj === undefined) throw new Error('Cannot access property of null');
                    return obj[node.property];
                }
                case 'index': {
                    const obj = evalExpr(node.object, localEnv);
                    const idx = evalExpr(node.index, localEnv);
                    if (Array.isArray(obj) || typeof obj === 'string') {
                        if (idx < 0 || idx >= obj.length) throw new Error('Index out of bounds');
                        return obj[idx];
                    }
                    if (obj && typeof obj === 'object') return obj[idx];
                    throw new Error('Cannot index this type');
                }
                case 'assign': {
                    const val = evalExpr(node.right, localEnv);
                    if (!localEnv.__scope) localEnv.__scope = {};
                    localEnv.__scope[node.name] = val;
                    return val;
                }
                case 'setmember': {
                    const obj = evalExpr(node.object, localEnv);
                    const val = evalExpr(node.right, localEnv);
                    obj[node.property] = val;
                    return val;
                }
                case 'setindex': {
                    const obj = evalExpr(node.object, localEnv);
                    const idx = evalExpr(node.index, localEnv);
                    const val = evalExpr(node.right, localEnv);
                    obj[idx] = val;
                    return val;
                }
                case 'binop': {
                    const left = evalExpr(node.left, localEnv);
                    const right = evalExpr(node.right, localEnv);
                    switch (node.op) {
                        case '+': return (typeof left === 'string' || typeof right === 'string') ? String(left) + String(right) : left + right;
                        case '-': return left - right;
                        case '*': return left * right;
                        case '/': return left / right;
                        case '%': return left % right;
                        case '==': return left === right;
                        case '!=': return left !== right;
                        case '<': return left < right;
                        case '>': return left > right;
                        case '<=': return left <= right;
                        case '>=': return left >= right;
                        case '&&': return left && right;
                        case '||': return left || right;
                    }
                }
                case 'unary': {
                    const right = evalExpr(node.right, localEnv);
                    return node.op === '!' ? !right : -right;
                }
                case 'call': {
                    const args = node.args.map(a => evalExpr(a, localEnv));
                    if (node.callee.type === 'ident' && builtins[node.callee.name]) {
                        return builtins[node.callee.name](...args);
                    }
                    const fn = evalExpr(node.callee, localEnv);
                    if (typeof fn === 'function') return fn(...args);
                    if (fn && fn.params) {
                        const newEnv = { ...fn.closure };
                        for (let i = 0; i < fn.params.length; i++) newEnv[fn.params[i]] = args[i];
                        const result = exec(fn.body, newEnv);
                        return result && result.__return ? result.value : result;
                    }
                    throw new Error(`Not a function: ${node.callee.name || 'anonymous'}`);
                }
            }
            return null;
        }

        const builtins = {
            print: (...args) => {
                const msg = args.map(a => String(a)).join(' ');
                if (typeof consoleLog !== 'undefined') consoleLog(msg, 'out');
                else console.log(msg);
                return null;
            },
            input: (prompt) => {
                if (typeof prompt !== 'undefined') process.stdout.write(prompt);
                const buf = Buffer.alloc(1024);
                const bytesRead = fs.readSync(process.stdin.fd, buf, 0, 1024);
                return buf.toString('utf8', 0, bytesRead).trim();
            },
            len: (arg) => (Array.isArray(arg) || typeof arg === 'string') ? arg.length : 0,
            typeof: (arg) => {
                if (arg === null) return 'null';
                if (typeof arg === 'boolean') return 'bool';
                if (typeof arg === 'number') return 'number';
                if (typeof arg === 'string') return 'string';
                if (Array.isArray(arg)) return 'array';
                if (typeof arg === 'object') return 'object';
                return 'unknown';
            },
            push: (arr, item) => { arr.push(item); return arr; },
            pop: (arr) => arr.pop(),
            keys: (obj) => Object.keys(obj),
            values: (obj) => Object.values(obj),
            range: (start, end) => { const res = []; for (let i = start; i <= end; i++) res.push(i); return res; },
            str: (arg) => String(arg),
            num: (arg) => Number(arg),
        };

        for (const stmt of stmts) {
            const v = execStmt(stmt, env);
            if (v && v.__return) { lastValue = v.value; break; }
            lastValue = v;
        }
        return lastValue;
    }

    // ===================== PUBLIC API =====================
    return {
        run: (source, basePath = '.') => {
            try {
                const tokens = tokenize(source);
                const ast = parse(tokens);
                return evaluate(ast, {}, basePath, new Set());
            } catch (e) {
                if (typeof consoleLog !== 'undefined') consoleLog(e.message, 'err');
                else console.error('[ERROR]', e.message);
                throw e;
            }
        },
        tokenize,
        parse: (source) => parse(tokenize(source)),
        eval: (ast, basePath = '.') => evaluate(ast, {}, basePath, new Set()),
    };
})();

module.exports = { Iris };

