/**
 * Architect language statement-level parser.
 * Converts a Token[] (from the lexer) into a Program AST.
 *
 * Design notes:
 * - Processes one line at a time; groupIntoLines splits the token stream.
 * - Uses a scope stack to build nested structures for FUNCTION, IF, DO, CASE.
 * - Unknown or unrecognised lines produce UnknownLine nodes — the parser never throws.
 * - MACRO is treated as single-line (body = everything after name/params on the same line).
 * - Standalone UNTIL (...) closes a DO FOREVER / DO WHILE block (loop-bottom condition).
 */

import { TokenType } from "../lexer/tokenTypes.js";
import { NodeType } from "./nodes.js";

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Parse a token stream into a Program AST.
 * @param {Token[]} tokens - Output of tokenize()
 * @returns {Program}
 */
function parse(tokens) {
    const lines = groupIntoLines(tokens);

    const program = { type: NodeType.Program, statements: [], line: 0 };

    // Stack entries: { node, target: Statement[], kind }
    // kind: 'program' | 'function' | 'if' | 'do' | 'case' | 'macro'
    const stack = [{ node: program, target: program.statements, kind: "program" }];

    for (const lineTokens of lines) {
        processLine(lineTokens, stack);
    }

    return program;
}

// ---------------------------------------------------------------------------
// Line grouping
// ---------------------------------------------------------------------------

function groupIntoLines(tokens) {
    const lines = [];
    let current = [];

    for (const tok of tokens) {
        if (tok.type === TokenType.EOF) break;
        if (tok.type === TokenType.NEWLINE) {
            lines.push(current);
            current = [];
        } else {
            current.push(tok);
        }
    }
    if (current.length > 0) {
        lines.push(current);
    }
    return lines;
}

// ---------------------------------------------------------------------------
// Line processing
// ---------------------------------------------------------------------------

function processLine(lineTokens, stack) {
    const lineIndex = getLineIndex(lineTokens);
    const meaningful = getMeaningful(lineTokens);

    if (meaningful.length === 0) return; // blank line

    const first = meaningful[0];

    // --- Output text line ---
    if (first.type === TokenType.OUTPUT_TEXT) {
        push(stack, { type: NodeType.OutputLine, text: first.value, line: lineIndex });
        return;
    }

    // --- Comment-only line (after HASH_PREFIX was stripped, or bare //) ---
    if (first.type === TokenType.COMMENT) {
        push(stack, { type: NodeType.CommentLine, text: first.value, line: lineIndex });
        return;
    }

    // --- Keyword-led lines ---
    if (first.type === TokenType.KEYWORD) {
        processKeywordLine(first.value.toUpperCase(), meaningful, lineTokens, lineIndex, stack);
        return;
    }

    // --- Alternate VAR syntax: TYPE varname [, varname] ---
    if (first.type === TokenType.TYPE_KEYWORD) {
        const names = collectIdentifiers(meaningful, 1);
        if (names.length > 0) {
            push(stack, {
                type:  NodeType.VarStatement,
                names,
                varType: first.value.toUpperCase(),
                line:  lineIndex
            });
            return;
        }
    }

    // --- Identifier-led lines: assignment or expression statement ---
    if (first.type === TokenType.IDENTIFIER) {
        const second = meaningful[1];
        if (second && second.type === TokenType.EQUALS) {
            // Bare assignment: target = expr
            const valueTokens = codeTokensAfter(meaningful, 2);
            push(stack, {
                type:   NodeType.AssignStatement,
                target: first.value,
                value:  makeSpan(valueTokens),
                line:   lineIndex
            });
            return;
        }
        // Otherwise: expression statement (function call, macro invocation, etc.)
        push(stack, {
            type:  NodeType.ExpressionStatement,
            value: makeSpan(meaningful),
            line:  lineIndex
        });
        return;
    }

    // --- Fallback ---
    push(stack, makeUnknownLine(lineTokens, lineIndex));
}

// ---------------------------------------------------------------------------
// Keyword dispatch
// ---------------------------------------------------------------------------

function processKeywordLine(upper, meaningful, lineTokens, lineIndex, stack) {
    switch (upper) {

        // --- Variables ---
        case "VAR": {
            const colonIdx = meaningful.findIndex(t => t.type === TokenType.COLON);
            if (colonIdx === -1) { push(stack, makeUnknownLine(lineTokens, lineIndex)); return; }
            const typeToken = meaningful[colonIdx + 1];
            if (!typeToken || typeToken.type !== TokenType.TYPE_KEYWORD) {
                push(stack, makeUnknownLine(lineTokens, lineIndex)); return;
            }
            const names = collectIdentifiers(meaningful, 1, colonIdx);
            push(stack, {
                type:    NodeType.VarStatement,
                names,
                varType: typeToken.value.toUpperCase(),
                line:    lineIndex
            });
            return;
        }

        // --- SET assignment ---
        case "SET": {
            // SET target = expr
            const targetTok = meaningful[1];
            if (!targetTok || targetTok.type !== TokenType.IDENTIFIER) {
                push(stack, makeUnknownLine(lineTokens, lineIndex)); return;
            }
            const eqIdx = meaningful.findIndex((t, i) => i > 1 && t.type === TokenType.EQUALS);
            if (eqIdx === -1) { push(stack, makeUnknownLine(lineTokens, lineIndex)); return; }
            const valueTokens = codeTokensAfter(meaningful, eqIdx + 1);
            push(stack, {
                type:   NodeType.SetStatement,
                target: targetTok.value,
                value:  makeSpan(valueTokens),
                line:   lineIndex
            });
            return;
        }

        // --- IF ---
        case "IF": {
            const condTokens = codeTokensAfter(meaningful, 1);
            const ifNode = {
                type:       NodeType.IfStatement,
                condition:  makeSpan(condTokens),
                consequent: [],
                alternate:  [],
                line:       lineIndex
            };
            push(stack, ifNode);
            stack.push({ node: ifNode, target: ifNode.consequent, kind: "if" });
            return;
        }

        case "ELSE": {
            const top = stack[stack.length - 1];
            if (top.kind === "if") {
                top.target = top.node.alternate;
                push(stack, { type: NodeType.ElseStatement, line: lineIndex });
            } else {
                push(stack, makeUnknownLine(lineTokens, lineIndex));
            }
            return;
        }

        case "ENDIF": {
            if (stack.length > 1 && stack[stack.length - 1].kind === "if") {
                stack.pop();
            }
            push(stack, { type: NodeType.EndIfStatement, line: lineIndex });
            return;
        }

        // --- DO loops ---
        case "DO": {
            const kindTok = meaningful[1];
            let doNode;
            // Iterator pattern: DO identifier = startExpr [TO endExpr] [BY stepExpr]
            if (kindTok && kindTok.type === TokenType.IDENTIFIER &&
                meaningful[2] && meaningful[2].type === TokenType.EQUALS) {
                doNode = parseDoIterator(meaningful, lineIndex);
            } else {
                const kind = kindTok ? kindTok.value.toUpperCase() : "forever";
                const condTokens = codeTokensAfter(meaningful, 2);
                doNode = {
                    type:      NodeType.DoStatement,
                    kind:      kind.toLowerCase(),
                    condition: condTokens.length > 0 ? makeSpan(condTokens) : null,
                    body:      [],
                    line:      lineIndex
                };
            }
            push(stack, doNode);
            stack.push({ node: doNode, target: doNode.body, kind: "do" });
            return;
        }

        case "ENDDO": {
            if (stack.length > 1 && stack[stack.length - 1].kind === "do") {
                stack.pop();
            }
            push(stack, { type: NodeType.EndDoStatement, keyword: "ENDDO", condition: null, line: lineIndex });
            return;
        }

        // Standalone UNTIL — closes a DO FOREVER / DO WHILE block (loop-bottom condition).
        case "UNTIL": {
            const condTokens = codeTokensAfter(meaningful, 1);
            if (stack.length > 1 && stack[stack.length - 1].kind === "do") {
                stack.pop();
            }
            push(stack, {
                type:      NodeType.EndDoStatement,
                keyword:   "UNTIL",
                condition: condTokens.length > 0 ? makeSpan(condTokens) : null,
                line:      lineIndex
            });
            return;
        }

        // --- CASE ---
        case "CASE": {
            // CASE expr [OF] — strip trailing OF if present
            const subjTokens = codeTokensAfterKeyword(meaningful, 1, "OF");
            const caseNode = {
                type:      NodeType.CaseStatement,
                subject:   makeSpan(subjTokens),
                branches:  [],
                otherwise: null,
                line:      lineIndex
            };
            push(stack, caseNode);
            stack.push({ node: caseNode, target: null, kind: "case" });
            return;
        }

        case "VALUE":
        case "WHEN": {
            const top = stack[stack.length - 1];
            if (top.kind === "case") {
                const valueTokens = codeTokensAfter(meaningful, 1);
                const branch = { values: splitByCommaSpans(valueTokens), body: [] };
                top.node.branches.push(branch);
                top.target = branch.body;
            } else {
                push(stack, makeUnknownLine(lineTokens, lineIndex));
            }
            return;
        }

        case "OTHERWISE": {
            const top = stack[stack.length - 1];
            if (top.kind === "case") {
                top.node.otherwise = [];
                top.target = top.node.otherwise;
                push(stack, { type: NodeType.OtherwiseStatement, line: lineIndex }); // boundary marker
            } else {
                push(stack, makeUnknownLine(lineTokens, lineIndex));
            }
            return;
        }

        case "ENDCASE": {
            if (stack.length > 1 && stack[stack.length - 1].kind === "case") {
                stack.pop();
            }
            push(stack, { type: NodeType.EndCaseStatement, line: lineIndex });
            return;
        }

        // --- Functions ---
        case "FUNCTION": {
            const funcNode = parseFunctionDecl(meaningful, lineIndex);
            push(stack, funcNode);
            if (funcNode.type === NodeType.FunctionDeclaration) {
                stack.push({ node: funcNode, target: funcNode.body, kind: "function" });
            }
            return;
        }

        case "ENDFUNCTION": {
            if (stack.length > 1 && stack[stack.length - 1].kind === "function") {
                stack.pop();
            }
            push(stack, { type: NodeType.EndFunctionStatement, line: lineIndex });
            return;
        }

        // --- Macros (single-line) ---
        case "MACRO": {
            const macroNode = parseMacroDecl(meaningful, lineIndex);
            push(stack, macroNode);
            return;
        }

        // --- Simple statements ---
        case "RETURN": {
            const valueTokens = codeTokensAfter(meaningful, 1);
            push(stack, {
                type:  NodeType.ReturnStatement,
                value: valueTokens.length > 0 ? makeSpan(valueTokens) : null,
                line:  lineIndex
            });
            return;
        }

        case "BREAK":
            push(stack, { type: NodeType.BreakStatement, line: lineIndex });
            return;

        case "CONTINUE":
            push(stack, { type: NodeType.ContinueStatement, line: lineIndex });
            return;

        case "EXIT":
            push(stack, { type: NodeType.ExitStatement, line: lineIndex });
            return;

        case "END-OF-CODE":
            push(stack, { type: NodeType.EndOfCodeStatement, line: lineIndex });
            return;

        // --- Directives ---
        case "HASH-ON":
            push(stack, { type: NodeType.HashOnDirective, line: lineIndex });
            return;

        case "HASH-OFF":
            push(stack, { type: NodeType.HashOffDirective, line: lineIndex });
            return;

        case "NEWINTERPRETER":
            push(stack, { type: NodeType.NewInterpreterDirective, line: lineIndex });
            return;

        case "INCLUDE":
        case "INCLUDEONCE":
        case "INCLUDETEST": {
            const filename = extractStringArg(meaningful, 1);
            push(stack, {
                type:     NodeType.IncludeDirective,
                filename: filename || "",
                once:     upper === "INCLUDEONCE",
                test:     upper === "INCLUDETEST",
                line:     lineIndex
            });
            return;
        }

        case "REQUIRE": {
            const filename = extractStringArg(meaningful, 1);
            push(stack, {
                type:     NodeType.RequireDirective,
                filename: filename || "",
                line:     lineIndex
            });
            return;
        }

        case "OPTION": {
            // OPTION key = "value" [, key = "value" ...]
            // Store the raw key/value pairs as a TokenSpan; detailed parsing is for validators.
            const rest = codeTokensAfter(meaningful, 1);
            push(stack, {
                type:    NodeType.OptionDirective,
                options: makeSpan(rest),
                line:    lineIndex
            });
            return;
        }

        // Keywords that appear inside expressions but can surface as a line start.
        // Emit as ExpressionStatement so they are not lost.
        default: {
            push(stack, {
                type:  NodeType.ExpressionStatement,
                value: makeSpan(meaningful),
                line:  lineIndex
            });
        }
    }
}

// ---------------------------------------------------------------------------
// Block push helper
// ---------------------------------------------------------------------------

/**
 * Push a statement into the innermost available target on the stack.
 * For CASE blocks that have no branch yet (null target), falls back to
 * the parent scope so boundary comments/blanks don't disappear.
 */
function push(stack, stmt) {
    for (let i = stack.length - 1; i >= 0; i--) {
        if (stack[i].target) {
            stack[i].target.push(stmt);
            return;
        }
    }
    // Should not happen (program's statements is always present), but be safe.
    stack[0].target.push(stmt);
}

// ---------------------------------------------------------------------------
// Statement-specific parsers
// ---------------------------------------------------------------------------

function parseFunctionDecl(meaningful, lineIndex) {
    // FUNCTION name ( [params] ) [: returnType]
    const nameTok = meaningful[1];
    if (!nameTok || nameTok.type !== TokenType.IDENTIFIER) {
        return makeUnknownLineFromMeaningful(meaningful, lineIndex);
    }

    const lparenIdx = meaningful.findIndex(t => t.type === TokenType.LPAREN);
    const rparenIdx = meaningful.findIndex(t => t.type === TokenType.RPAREN);

    const params = [];
    if (lparenIdx !== -1 && rparenIdx !== -1 && rparenIdx > lparenIdx) {
        const paramTokens = meaningful.slice(lparenIdx + 1, rparenIdx)
            .filter(t => t.type !== TokenType.WHITESPACE);
        parseParamList(paramTokens, params);
    }

    let returnType = null;
    // Look for : TYPE_KEYWORD after the RPAREN
    const colonAfterRparen = meaningful.findIndex((t, i) => i > rparenIdx && t.type === TokenType.COLON);
    if (colonAfterRparen !== -1) {
        const typeTok = meaningful.slice(colonAfterRparen + 1).find(t => t.type === TokenType.TYPE_KEYWORD);
        if (typeTok) returnType = typeTok.value.toUpperCase();
    }

    return {
        type:       NodeType.FunctionDeclaration,
        name:       nameTok.value,
        params,
        returnType,
        body:       [],
        line:       lineIndex
    };
}

function parseParamList(tokens, out) {
    // Tokens between ( and ), whitespace already stripped.
    // Pattern: name : type [, name : type ...]
    let i = 0;
    while (i < tokens.length) {
        const nameTok = tokens[i];
        if (nameTok.type !== TokenType.IDENTIFIER) { i++; continue; }
        const colon = tokens[i + 1];
        if (!colon || colon.type !== TokenType.COLON) { i++; continue; }
        const typeTok = tokens[i + 2];
        if (!typeTok || typeTok.type !== TokenType.TYPE_KEYWORD) { i++; continue; }
        out.push({ name: nameTok.value, type: typeTok.value.toUpperCase() });
        i += 3;
        // Skip trailing COMMA
        if (i < tokens.length && tokens[i].type === TokenType.COMMA) i++;
    }
}

function parseMacroDecl(meaningful, lineIndex) {
    // MACRO name [( params )] body_tokens...
    const nameTok = meaningful[1];
    if (!nameTok || nameTok.type !== TokenType.IDENTIFIER) {
        return makeUnknownLineFromMeaningful(meaningful, lineIndex);
    }

    const params = [];
    let bodyStart = 2;

    const lparenIdx = meaningful.findIndex((t, i) => i >= 2 && t.type === TokenType.LPAREN);
    if (lparenIdx !== -1) {
        const rparenIdx = meaningful.findIndex((t, i) => i > lparenIdx && t.type === TokenType.RPAREN);
        if (rparenIdx !== -1) {
            meaningful.slice(lparenIdx + 1, rparenIdx)
                .filter(t => t.type === TokenType.IDENTIFIER || t.type === TokenType.QUESTION)
                .forEach(t => params.push(t.value));
            bodyStart = rparenIdx + 1;
        }
    }

    const bodyTokens = codeTokensAfter(meaningful, bodyStart);
    return {
        type:   NodeType.MacroDeclaration,
        name:   nameTok.value,
        params,
        body:   bodyTokens.length > 0 ? makeSpan(bodyTokens) : null,
        line:   lineIndex
    };
}

// ---------------------------------------------------------------------------
// Token helpers
// ---------------------------------------------------------------------------

/** Get the 0-based line number from the first token in a line's token array. */
function getLineIndex(lineTokens) {
    for (const tok of lineTokens) {
        if (tok.line !== undefined) return tok.line;
    }
    return 0;
}

/**
 * Meaningful tokens: strips WHITESPACE, HASH_PREFIX, COMMENT, and a leading HASH
 * token from the token list, then returns the substantive content.
 *
 * HASH_PREFIX is the '#' that marks code lines in HASH-ON mode.
 * A leading HASH token is the '#' prefix on executable keyword lines in HASH-OFF mode
 * (e.g. '#VAR x : STRING', '#FUNCTION FOO()', '#ENDFUNCTION').
 * Both act purely as executable-line markers and carry no semantic content.
 *
 * OUTPUT_TEXT is kept only if it is the first token (it IS the content in that case).
 */
function getMeaningful(lineTokens) {
    const result = [];
    let seenContent = false;
    for (const tok of lineTokens) {
        if (tok.type === TokenType.WHITESPACE) continue;
        if (tok.type === TokenType.HASH_PREFIX) continue;
        if (!seenContent && tok.type === TokenType.HASH) continue; // HASH-OFF executable prefix
        seenContent = true;
        if (tok.type === TokenType.COMMENT) break; // comment ends meaningful content
        result.push(tok);
    }
    return result;
}

/**
 * Code tokens after a given index in the meaningful array, stripping leading whitespace.
 * These form the value/condition/expression TokenSpans.
 */
function codeTokensAfter(meaningful, startIndex) {
    const slice = meaningful.slice(startIndex);
    // Trim leading whitespace tokens
    while (slice.length > 0 && slice[0].type === TokenType.WHITESPACE) {
        slice.shift();
    }
    return slice;
}

/**
 * Like codeTokensAfter, but also excludes a trailing keyword (e.g. OF in CASE expr OF).
 */
function codeTokensAfterKeyword(meaningful, startIndex, excludeKeyword) {
    let tokens = codeTokensAfter(meaningful, startIndex);
    if (tokens.length > 0) {
        const last = tokens[tokens.length - 1];
        if (last.type === TokenType.KEYWORD && last.value.toUpperCase() === excludeKeyword) {
            tokens = tokens.slice(0, -1);
        }
    }
    return tokens;
}

/** Collect IDENTIFIER token values between startIndex and endIndex (exclusive) in meaningful. */
function collectIdentifiers(meaningful, startIndex, endIndex) {
    const end = endIndex !== undefined ? endIndex : meaningful.length;
    const names = [];
    for (let i = startIndex; i < end; i++) {
        if (meaningful[i].type === TokenType.IDENTIFIER) {
            names.push(meaningful[i].value);
        }
    }
    return names;
}

/** Extract the string value from the first STRING_LITERAL or IDENTIFIER after startIndex. */
function extractStringArg(meaningful, startIndex) {
    for (let i = startIndex; i < meaningful.length; i++) {
        const tok = meaningful[i];
        if (tok.type === TokenType.STRING_LITERAL) {
            // Strip surrounding quotes
            return tok.value.slice(1, -1);
        }
        if (tok.type === TokenType.IDENTIFIER) {
            return tok.value;
        }
    }
    return null;
}

/**
 * Split a value token array at top-level COMMA boundaries.
 * Returns an array of TokenSpan objects, one per comma-separated segment.
 */
function splitByCommaSpans(tokens) {
    const spans = [];
    let depth = 0;
    let start = 0;

    for (let i = 0; i < tokens.length; i++) {
        const tok = tokens[i];
        if (tok.type === TokenType.LPAREN) { depth++; continue; }
        if (tok.type === TokenType.RPAREN) { depth--; continue; }
        if (tok.type === TokenType.COMMA && depth === 0) {
            const segment = tokens.slice(start, i).filter(t => t.type !== TokenType.WHITESPACE);
            if (segment.length > 0) spans.push(makeSpan(segment));
            start = i + 1;
        }
    }
    const last = tokens.slice(start).filter(t => t.type !== TokenType.WHITESPACE);
    if (last.length > 0) spans.push(makeSpan(last));
    return spans;
}

/** Create a TokenSpan from an array of tokens. Returns null for empty arrays. */
function makeSpan(tokens) {
    if (!tokens || tokens.length === 0) return null;
    const first = tokens[0];
    const last  = tokens[tokens.length - 1];
    return {
        tokens,
        start: { line: first.line, char: first.char },
        end:   { line: last.line,  char: last.char + last.value.length }
    };
}

/**
 * Parse a DO iterator statement: DO identifier = startExpr [TO endExpr] [BY stepExpr]
 * meaningful[0]=DO, [1]=identifier, [2]=EQUALS, content starts at index 3.
 */
function parseDoIterator(meaningful, lineIndex) {
    const variable = meaningful[1].value;
    let toIdx = -1, byIdx = -1;
    for (let i = 3; i < meaningful.length; i++) {
        const tok = meaningful[i];
        if (tok.type !== TokenType.KEYWORD) continue;
        const upper = tok.value.toUpperCase();
        if (upper === "TO" && toIdx === -1) toIdx = i;
        else if (upper === "BY" && byIdx === -1) byIdx = i;
    }
    const startEnd = Math.min(
        toIdx !== -1 ? toIdx : meaningful.length,
        byIdx !== -1 ? byIdx : meaningful.length
    );
    const startTokens = meaningful.slice(3, startEnd);
    let toTokens = null;
    if (toIdx !== -1) {
        const toEnd = byIdx !== -1 && byIdx > toIdx ? byIdx : meaningful.length;
        toTokens = meaningful.slice(toIdx + 1, toEnd);
    }
    let byTokens = null;
    if (byIdx !== -1) {
        byTokens = meaningful.slice(byIdx + 1);
    }
    return {
        type:        NodeType.DoStatement,
        kind:        "iterator",
        keywordChar: meaningful[0].char, // column of 'DO' keyword (HASH-ON aware)
        iterator: {
            variable,
            start: makeSpan(startTokens),
            to:    toTokens && toTokens.length > 0 ? makeSpan(toTokens) : null,
            by:    byTokens && byTokens.length > 0 ? makeSpan(byTokens) : null
        },
        condition: null,
        body:      [],
        line:      lineIndex
    };
}

/** Create an UnknownLine node from a raw line token array. */
function makeUnknownLine(lineTokens, lineIndex) {
    const text = lineTokens.map(t => t.value).join("");
    return { type: NodeType.UnknownLine, text, line: lineIndex };
}

function makeUnknownLineFromMeaningful(meaningful, lineIndex) {
    const text = meaningful.map(t => t.value).join("");
    return { type: NodeType.UnknownLine, text, line: lineIndex };
}

// ---------------------------------------------------------------------------
// Exports
// ---------------------------------------------------------------------------

export { parse };
