/**
 * Operator spacing validator for Acurity Architect.
 * Warns when binary arithmetic operators have no surrounding spaces.
 *
 * - ACU-STY-004: Operator '{operator}' must have a single space on each side.
 *
 * Enabled only in strict mode (profile severity: "warning").
 * Default and legacy profiles have this rule set to "off".
 *
 * Design:
 *   - Raw character-level scan (the lexer discards whitespace, so token-stream
 *     detection is not viable for spacing checks).
 *   - String literal content is skipped.
 *   - Line-comment text is stripped before scanning.
 *   - Output lines (HASH-ON) are skipped.
 *   - Unary minus/plus is exempted: if the preceding non-whitespace character
 *     is not a word character, ')', or ']', the operator is treated as unary.
 *   - Operators checked: ** + - * /  (longest-first so ** is matched before *)
 */

import { createDiagnostic } from "../diagnostics/diagnosticFactory.js";
import { parseIncludeDirective } from "../parser.js";

// Operators to check, longest match first so '**' before '*' and '+=' before '+'.
const BINARY_OPERATORS = ["**", "+=", "+", "-", "*", "/"];

/**
 * Collect [startIndex, endIndex] (inclusive) ranges for string literals on a line.
 * Handles double-quoted strings; single-quoted strings are not used in Architect but
 * are included defensively.
 */
function getStringRanges(line) {
    const ranges = [];
    let inString = false;
    let quote = null;
    let start = -1;
    for (let i = 0; i < line.length; i++) {
        const ch = line[i];
        if (!inString) {
            if (ch === '"' || ch === "'") {
                inString = true;
                quote = ch;
                start = i;
            }
        } else if (ch === quote) {
            ranges.push([start, i]);
            inString = false;
            quote = null;
        }
    }
    return ranges;
}

function isInStringRange(pos, ranges) {
    for (const [s, e] of ranges) {
        if (pos >= s && pos <= e) return true;
    }
    return false;
}

/**
 * Return the index of the first '//' outside a string literal, or -1 if none.
 */
function findCommentStart(line, stringRanges) {
    for (let i = 0; i < line.length - 1; i++) {
        if (line[i] === "/" && line[i + 1] === "/" && !isInStringRange(i, stringRanges)) {
            return i;
        }
    }
    return -1;
}

/**
 * Return the nearest non-whitespace character to the left of position i,
 * or null if the beginning of the string is reached.
 */
function prevNonSpace(line, i) {
    let j = i - 1;
    while (j >= 0 && line[j] === " ") j--;
    return j >= 0 ? line[j] : null;
}

/**
 * True if ch is an alphanumeric character or underscore (i.e. part of an identifier).
 */
function isWordChar(ch) {
    return ch !== null && ch !== undefined && /\w/.test(ch);
}

/**
 * Validates that binary arithmetic operators have a single space on each side.
 *
 * @param {object} document - VS Code TextDocument or PlainTextDocument
 * @param {Function} getRuleSeverity - (ruleId: string) => DiagnosticSeverity|null
 * @param {object} [options]
 * @param {object[]} [options.executionClassificationMap]
 * @returns {object[]}
 */
function validateOperatorSpacing(document, getRuleSeverity, options = {}) {
    const diagnostics = [];
    const ecm = options.executionClassificationMap || null;

    // Early exit if rule is disabled — avoids per-line scanning overhead.
    if (getRuleSeverity("operatorSpacing") === null) return diagnostics;

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
        const cls = ecm ? ecm[lineIndex] : null;

        // Skip output lines (HASH-ON text).
        if (cls && cls.isOutput) continue;

        // Skip lines that failed executable classification — ACU-SYN-002 already covers them.
        if (cls && cls.isInvalidExecutable) continue;

        const rawLine = document.lineAt(lineIndex).text;
        const trimmed = rawLine.trimStart();

        // Skip blank lines and pure comment lines.
        if (trimmed === "" || trimmed.startsWith("//")) continue;

        // Skip directive lines (# prefix).
        if (trimmed.startsWith("#")) continue;

        // Skip HASH mode control directives — their text must not be scanned for operators.
        if (/^#?\s*HASH-(ON|OFF)\b/i.test(trimmed)) continue;

        // Skip INCLUDE-family lines — the filepath argument contains hyphens and dots
        // that are not operators, and no executable expression follows the filepath.
        if (parseIncludeDirective(rawLine)) continue;

        // Build string ranges on the raw line, then strip any trailing comment.
        const stringRanges = getStringRanges(rawLine);
        const commentStart = findCommentStart(rawLine, stringRanges);
        const codeLine = commentStart >= 0 ? rawLine.substring(0, commentStart) : rawLine;

        let i = 0;
        while (i < codeLine.length) {
            // Skip characters inside string literals.
            if (isInStringRange(i, stringRanges)) {
                i++;
                continue;
            }

            // Try to match an operator at position i (longest match first).
            let matched = null;
            for (const op of BINARY_OPERATORS) {
                if (codeLine.startsWith(op, i)) {
                    matched = op;
                    break;
                }
            }

            if (!matched) {
                i++;
                continue;
            }

            // Skip '/' when it looks like a date literal separator (digit/digit pattern).
            // Date literals in Architect look like 01/01/2026 or 00/00/0000; scanning
            // them as division operators produces false positives.
            if (matched === "/") {
                const prevCh = i > 0 ? codeLine[i - 1] : null;
                const nextCh = i + 1 < codeLine.length ? codeLine[i + 1] : null;
                if (prevCh !== null && /\d/.test(prevCh) && nextCh !== null && /\d/.test(nextCh)) {
                    i += matched.length;
                    continue;
                }
            }

            // Unary exemption for '-' and '+':
            // If the preceding non-whitespace character is not a word char, ')', or ']',
            // the operator is unary (e.g. -5, -sA, -(expr)).
            if (matched === "-" || matched === "+") {
                const prev = prevNonSpace(codeLine, i);
                if (prev === null || (!isWordChar(prev) && prev !== ")" && prev !== "]")) {
                    i += matched.length;
                    continue;
                }
            }

            // Check spacing: character immediately before and after the operator.
            const charBefore = i > 0 ? codeLine[i - 1] : null;
            const opEnd = i + matched.length;
            const charAfter = opEnd < codeLine.length ? codeLine[opEnd] : null;

            const spaceBefore = charBefore === " ";
            const spaceAfter = charAfter === " " || charAfter === null;

            if (!spaceBefore || !spaceAfter) {
                const range = {
                    start: { line: lineIndex, character: i },
                    end:   { line: lineIndex, character: opEnd }
                };
                const diag = createDiagnostic(
                    range,
                    { messageId: "ACU-STY-004", params: { operator: matched } },
                    getRuleSeverity
                );
                if (diag) diagnostics.push(diag);
            }

            i += matched.length;
        }
    }

    return diagnostics;
}

export { validateOperatorSpacing };
