// lexer_js.c -- JavaScript language tokenizer
//
// Supports nested tagged template literals with ${...} interpolation.
// Uses a depth stack to track template literal nesting so that expressions
// inside ${...} can themselves contain template literals at arbitrary depth.
//
// Example:  html`outer ${css`inner ${x}`} rest`
//           ^^^^ ^^^^^^ ^^^ ^^^^^^ ^ ^  ^^^^^^
//           func string  func str  id str string

#include "lexer/lexer.h"
#include <ctype.h>
#include <string.h>

////////////////////////////////
// JavaScript keyword tables

typedef struct JSKeywordEntry {
    const char *word;
    Token_Type  type;
} JSKeywordEntry;

static const JSKeywordEntry js_keywords[] = {
    // Control-flow & language keywords
    {"break",           TOK_KEYWORD},
    {"case",            TOK_KEYWORD},
    {"catch",           TOK_KEYWORD},
    {"class",           TOK_KEYWORD},
    {"const",           TOK_KEYWORD},
    {"continue",        TOK_KEYWORD},
    {"debugger",        TOK_KEYWORD},
    {"default",         TOK_KEYWORD},
    {"delete",          TOK_KEYWORD},
    {"do",              TOK_KEYWORD},
    {"else",            TOK_KEYWORD},
    {"extends",         TOK_KEYWORD},
    {"finally",         TOK_KEYWORD},
    {"for",             TOK_KEYWORD},
    {"function",        TOK_KEYWORD},
    {"if",              TOK_KEYWORD},
    {"in",              TOK_KEYWORD},
    {"instanceof",      TOK_KEYWORD},
    {"let",             TOK_KEYWORD},
    {"new",             TOK_KEYWORD},
    {"of",              TOK_KEYWORD},
    {"return",          TOK_KEYWORD},
    {"switch",          TOK_KEYWORD},
    {"throw",           TOK_KEYWORD},
    {"try",             TOK_KEYWORD},
    {"typeof",          TOK_KEYWORD},
    {"var",             TOK_KEYWORD},
    {"void",            TOK_KEYWORD},
    {"while",           TOK_KEYWORD},
    {"with",            TOK_KEYWORD},
    {"yield",           TOK_KEYWORD},
    {"async",           TOK_KEYWORD},
    {"await",           TOK_KEYWORD},

    // Module keywords (directive-style coloring)
    {"import",          TOK_DIRECTIVE},
    {"export",          TOK_DIRECTIVE},
    {"from",            TOK_DIRECTIVE},
    {"as",              TOK_DIRECTIVE},

    // Values
    {"true",            TOK_VALUE},
    {"false",           TOK_VALUE},
    {"null",            TOK_VALUE},
    {"undefined",       TOK_VALUE},
    {"NaN",             TOK_VALUE},
    {"Infinity",        TOK_VALUE},
    {"this",            TOK_VALUE},
    {"super",           TOK_VALUE},

    // Modifiers / contextual keywords
    {"static",          TOK_MODIFIER},
    {"get",             TOK_MODIFIER},
    {"set",             TOK_MODIFIER},
};

#define JS_KEYWORD_COUNT (S32)(sizeof(js_keywords) / sizeof(js_keywords[0]))

static Token_Type js_lookup_keyword(const char *word, S32 len) {
    for (S32 i = 0; i < JS_KEYWORD_COUNT; i++) {
        const char *kw = js_keywords[i].word;
        S32 kwlen = (S32)strlen(kw);
        if (kwlen == len && memcmp(kw, word, len) == 0)
            return js_keywords[i].type;
    }
    return TOK_IDENTIFIER;
}

////////////////////////////////
// Character helpers

static B32 js_is_ident_start(char c) {
    return isalpha((unsigned char)c) || c == '_' || c == '$';
}

static B32 js_is_ident_char(char c) {
    return isalnum((unsigned char)c) || c == '_' || c == '$';
}

static B32 js_is_hex(char c) {
    return isdigit((unsigned char)c) ||
           (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}

////////////////////////////////
// Individual token parsers

static Token js_parse_identifier(Tokenizer *tok) {
    Token token;
    token.start = (S32)(tok->start_t - tok->buf);
    const char *begin = tok->t;
    while (tok->t < tok->max_t && js_is_ident_char(*tok->t))
        tok->t++;
    S32 len = (S32)(tok->t - begin);
    token.type = js_lookup_keyword(begin, len);
    token.len = (S32)(tok->t - tok->start_t);
    return token;
}

static Token js_parse_number(Tokenizer *tok) {
    Token token;
    token.start = (S32)(tok->start_t - tok->buf);
    token.type = TOK_NUMBER;

    char start_char = *tok->t;
    tok->t++;
    if (tok->t >= tok->max_t) goto done;

    if (start_char == '0' && tok->t < tok->max_t) {
        // Hex: 0x / 0X
        if (*tok->t == 'x' || *tok->t == 'X') {
            tok->t++;
            while (tok->t < tok->max_t && (js_is_hex(*tok->t) || *tok->t == '_')) tok->t++;
            if (tok->t < tok->max_t && *tok->t == 'n') tok->t++; // BigInt
            goto done;
        }
        // Octal: 0o / 0O
        if (*tok->t == 'o' || *tok->t == 'O') {
            tok->t++;
            while (tok->t < tok->max_t && ((*tok->t >= '0' && *tok->t <= '7') || *tok->t == '_')) tok->t++;
            if (tok->t < tok->max_t && *tok->t == 'n') tok->t++;
            goto done;
        }
        // Binary: 0b / 0B
        if (*tok->t == 'b' || *tok->t == 'B') {
            tok->t++;
            while (tok->t < tok->max_t && (*tok->t == '0' || *tok->t == '1' || *tok->t == '_')) tok->t++;
            if (tok->t < tok->max_t && *tok->t == 'n') tok->t++;
            goto done;
        }
    }

    // Decimal digits
    while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_'))
        tok->t++;

    // Float: .digits
    if (tok->t < tok->max_t && *tok->t == '.') {
        tok->t++;
        while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_'))
            tok->t++;
    }

    // Exponent
    if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) {
        tok->t++;
        if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
        while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_'))
            tok->t++;
    }

    // BigInt suffix
    if (tok->t < tok->max_t && *tok->t == 'n') tok->t++;

done:
    token.len = (S32)(tok->t - tok->start_t);
    return token;
}

// Float starting with dot: .5, .123e4
static Token js_parse_dot_number(Tokenizer *tok) {
    Token token;
    token.start = (S32)(tok->start_t - tok->buf);
    token.type = TOK_NUMBER;

    tok->t++; // skip '.'
    while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_'))
        tok->t++;
    if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) {
        tok->t++;
        if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
        while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_'))
            tok->t++;
    }

    token.len = (S32)(tok->t - tok->start_t);
    return token;
}

// String literal with given quote character (" or ')
static Token js_parse_string(Tokenizer *tok, char quote) {
    Token token;
    token.start = (S32)(tok->start_t - tok->buf);
    token.type = TOK_STRING_LITERAL;

    B32 escape = 0;
    tok->t++; // skip opening quote
    while (tok->t < tok->max_t && *tok->t != '\n') {
        if (*tok->t == quote && !escape) { tok->t++; break; }
        escape = !escape && (*tok->t == '\\');
        tok->t++;
    }
    token.len = (S32)(tok->t - tok->start_t);
    return token;
}

// / followed by / or * (comments), or /= (divide-assign), or bare / (divide)
static Token js_parse_slash_or_comment(Tokenizer *tok) {
    Token token;
    token.start = (S32)(tok->start_t - tok->buf);

    tok->t++; // skip '/'
    if (tok->t >= tok->max_t) {
        token.type = TOK_OPERATION;
        token.len = 1;
        return token;
    }

    if (*tok->t == '/') {
        // Line comment
        token.type = TOK_COMMENT;
        tok->t++;
        while (tok->t < tok->max_t && *tok->t != '\n') tok->t++;
    } else if (*tok->t == '*') {
        // Block comment
        token.type = TOK_MULTILINE_COMMENT;
        tok->t++;
        while (tok->t < tok->max_t) {
            if (*tok->t == '*' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '/') {
                tok->t += 2;
                break;
            }
            tok->t++;
        }
    } else if (*tok->t == '=') {
        // /=
        token.type = TOK_OPERATION;
        tok->t++;
    } else {
        // bare / (division)
        token.type = TOK_OPERATION;
    }

    token.len = (S32)(tok->t - tok->start_t);
    return token;
}

// Regex literal: /pattern/flags
static Token js_parse_regex(Tokenizer *tok) {
    Token token;
    token.start = (S32)(tok->start_t - tok->buf);
    token.type = TOK_STRING_LITERAL; // color regex like strings

    B32 escape = 0;
    B32 in_class = 0; // inside character class [...]
    tok->t++; // skip opening /

    while (tok->t < tok->max_t && *tok->t != '\n') {
        if (escape) {
            escape = 0;
            tok->t++;
            continue;
        }
        if (*tok->t == '\\') {
            escape = 1;
            tok->t++;
            continue;
        }
        if (*tok->t == '[') { in_class = 1; tok->t++; continue; }
        if (*tok->t == ']') { in_class = 0; tok->t++; continue; }
        if (*tok->t == '/' && !in_class) {
            tok->t++; // closing /
            // Consume flags: d, g, i, m, s, u, v, y
            while (tok->t < tok->max_t && isalpha((unsigned char)*tok->t)) tok->t++;
            break;
        }
        tok->t++;
    }

    token.len = (S32)(tok->t - tok->start_t);
    return token;
}

// Operators (handles multi-character sequences)
static Token js_parse_operator(Tokenizer *tok) {
    Token token;
    token.start = (S32)(tok->start_t - tok->buf);
    token.type = TOK_OPERATION;

    char c = *tok->t;
    tok->t++;

    if (tok->t < tok->max_t) {
        char n = *tok->t;

        // Three-character (and four-character) operators
        if ((tok->t + 1) < tok->max_t) {
            char nn = *(tok->t + 1);
            // >>>  and  >>>=
            if (c == '>' && n == '>' && nn == '>') {
                tok->t += 2;
                if (tok->t < tok->max_t && *tok->t == '=') tok->t++;
                goto done;
            }
            // ===
            if (c == '=' && n == '=' && nn == '=') { tok->t += 2; goto done; }
            // !==
            if (c == '!' && n == '=' && nn == '=') { tok->t += 2; goto done; }
            // **=
            if (c == '*' && n == '*' && nn == '=') { tok->t += 2; goto done; }
            // <<=
            if (c == '<' && n == '<' && nn == '=') { tok->t += 2; goto done; }
            // >>=
            if (c == '>' && n == '>' && nn == '=') { tok->t += 2; goto done; }
            // &&=
            if (c == '&' && n == '&' && nn == '=') { tok->t += 2; goto done; }
            // ||=
            if (c == '|' && n == '|' && nn == '=') { tok->t += 2; goto done; }
            // ??=
            if (c == '?' && n == '?' && nn == '=') { tok->t += 2; goto done; }
        }

        // Two-character operators
        if ((c == '=' && n == '=') || (c == '!' && n == '=') ||
            (c == '<' && n == '=') || (c == '>' && n == '=') ||
            (c == '+' && n == '=') || (c == '-' && n == '=') ||
            (c == '*' && n == '=') || (c == '%' && n == '=') ||
            (c == '&' && n == '=') || (c == '|' && n == '=') ||
            (c == '^' && n == '=') || (c == '+' && n == '+') ||
            (c == '-' && n == '-') || (c == '&' && n == '&') ||
            (c == '|' && n == '|') || (c == '<' && n == '<') ||
            (c == '>' && n == '>') || (c == '=' && n == '>') ||
            (c == '*' && n == '*') || (c == '?' && n == '?') ||
            (c == '?' && n == '.')) {
            tok->t++;
        }
    }

done:
    token.len = (S32)(tok->t - tok->start_t);
    return token;
}

////////////////////////////////
// Regex vs division heuristic
//
// After identifiers, numbers, values, strings, ) and ] -- slash is division.
// After keywords, operators, most punctuation, start of file -- slash is regex.

static B32 js_slash_is_regex(Token prev, const char *data) {
    switch (prev.type) {
    case TOK_DEFAULT:     return 1; // start of file / no previous token
    case TOK_KEYWORD:     return 1; // e.g. return /regex/
    case TOK_DIRECTIVE:   return 1; // after import/export
    case TOK_OPERATION:
        // After ++ or --, it's division (x++ / y)
        if (prev.len == 2) {
            char c0 = data[prev.start];
            char c1 = data[prev.start + 1];
            if ((c0 == '+' && c1 == '+') || (c0 == '-' && c1 == '-'))
                return 0;
        }
        return 1;
    case TOK_PUNCTUATION:
        // After ) or ], it's division
        if (prev.len == 1) {
            char c = data[prev.start];
            if (c == ')' || c == ']') return 0;
        }
        return 1;
    case TOK_IDENTIFIER:
    case TOK_FUNCTION:
    case TOK_NUMBER:
    case TOK_STRING_LITERAL:
    case TOK_CHAR_LITERAL:
    case TOK_VALUE:
        return 0; // division
    default:
        return 1;
    }
}

////////////////////////////////
// Template literal content scanner
//
// Scans the string content inside a template literal (the text between
// backticks, or between a closing } and the next ${ or closing backtick).
// Paints each byte as TOK_STRING_LITERAL.
//
// Returns 1 if we hit ${  (caller should enter expression mode),
//         0 if we hit closing ` or EOF (template is done).

static int js_scan_template_content(Tokenizer *tok, U8 *out_tokens) {
    while (tok->t < tok->max_t) {
        if (*tok->t == '`') {
            // Closing backtick -- template done
            S32 pos = (S32)(tok->t - tok->buf);
            out_tokens[pos] = TOK_STRING_LITERAL;
            tok->t++;
            return 0;
        }
        if (*tok->t == '$' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '{') {
            // Interpolation start -- paint ${ as punctuation
            S32 pos = (S32)(tok->t - tok->buf);
            out_tokens[pos]     = TOK_PUNCTUATION;
            out_tokens[pos + 1] = TOK_PUNCTUATION;
            tok->t += 2;
            return 1;
        }
        if (*tok->t == '\\' && (tok->t + 1) < tok->max_t) {
            // Escape sequence -- paint both chars as string
            S32 pos = (S32)(tok->t - tok->buf);
            out_tokens[pos]     = TOK_STRING_LITERAL;
            out_tokens[pos + 1] = TOK_STRING_LITERAL;
            tok->t += 2;
            continue;
        }
        // Regular string character
        S32 pos = (S32)(tok->t - tok->buf);
        out_tokens[pos] = TOK_STRING_LITERAL;
        tok->t++;
    }
    return 0; // EOF -- unclosed template
}

////////////////////////////////
// Get next non-template token
//
// Called by the main loop for normal expression parsing.  Template backticks
// and interpolation-closing braces are handled directly by the main loop
// before this function is reached.

static Token js_get_next_token(Tokenizer *tok, Token prev, const char *data) {
    tokenizer_eat_whitespace(tok);

    Token token = {0};
    token.start = (S32)(tok->t - tok->buf);
    token.type = TOK_DEFAULT;

    if (tok->t >= tok->max_t) {
        token.len = 0;
        return token;
    }

    tok->start_t = tok->t;
    char c = *tok->t;

    if (js_is_ident_start(c)) {
        return js_parse_identifier(tok);
    }
    if (isdigit((unsigned char)c)) {
        return js_parse_number(tok);
    }

    switch (c) {
    case '"':  return js_parse_string(tok, '"');
    case '\'': return js_parse_string(tok, '\'');

    case '/':
        // Check for comment first (// or /*)
        if ((tok->t + 1) < tok->max_t) {
            char n = *(tok->t + 1);
            if (n == '/' || n == '*')
                return js_parse_slash_or_comment(tok);
        }
        // Regex or division based on context
        if (js_slash_is_regex(prev, data))
            return js_parse_regex(tok);
        return js_parse_slash_or_comment(tok);

    // Dot: float literal (.5), spread (...), or member access
    case '.':
        if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) {
            return js_parse_dot_number(tok);
        }
        if ((tok->t + 2) < tok->max_t && *(tok->t + 1) == '.' && *(tok->t + 2) == '.') {
            token.type = TOK_OPERATION;
            tok->t += 3;
            token.len = 3;
            return token;
        }
        token.type = TOK_PUNCTUATION;
        tok->t++;
        token.len = 1;
        return token;

    // Punctuation
    case ';': case ',':
    case '(': case ')': case '[': case ']':
    case '{': case '}':
    case ':':
        token.type = TOK_PUNCTUATION;
        tok->t++;
        token.len = 1;
        return token;

    // Operators
    case '=': case '!': case '<': case '>':
    case '+': case '-': case '*': case '%':
    case '&': case '|': case '^': case '~':
    case '?':
        return js_parse_operator(tok);

    // Private class fields: #name
    case '#':
        token.type = TOK_IDENTIFIER;
        tok->t++;
        while (tok->t < tok->max_t && js_is_ident_char(*tok->t)) tok->t++;
        token.len = (S32)(tok->t - tok->start_t);
        return token;

    // Decorators: @name
    case '@':
        token.type = TOK_DIRECTIVE;
        tok->t++;
        while (tok->t < tok->max_t && js_is_ident_char(*tok->t)) tok->t++;
        token.len = (S32)(tok->t - tok->start_t);
        return token;

    default:
        token.type = TOK_INVALID;
        tok->t++;
        token.len = 1;
        return token;
    }
}

////////////////////////////////
// tokenize_js -- main entry point
//
// Tokenizes the full buffer and paints out_tokens[] with token types.
//
// Template literal nesting is tracked with a depth stack.  Each level
// records how many unmatched { braces exist in the current interpolation
// expression.  When a } is encountered and the brace count is zero, it
// closes the interpolation and we resume scanning template string content.
//
//   html`text ${obj.x} more ${css`inner ${y}`} end`
//        |str  ||expr | str  ||    |str  ||  || str|
//               ^punc  ^punc  ^punc      ^   ^^
//               ${     }      ${         }   }` (nesting!)

#define JS_MAX_TEMPLATE_DEPTH 32

static void tokenize_js(const char *data, S32 length, U8 *out_tokens) {
    memset(out_tokens, TOK_DEFAULT, length);

    Tokenizer tok;
    tokenizer_init(&tok, data, length);

    S32 tmpl_depth = 0;
    S32 brace_count[JS_MAX_TEMPLATE_DEPTH];
    memset(brace_count, 0, sizeof(brace_count));

    Token prev = {0};
    prev.type = TOK_DEFAULT;

    while (tok.t < tok.max_t) {
        tokenizer_eat_whitespace(&tok);
        if (tok.t >= tok.max_t) break;

        char c = *tok.t;

        // ---- Template literal: opening backtick ----
        // This handles both top-level template literals and nested ones
        // (e.g. a tagged template inside a ${...} interpolation).
        if (c == '`') {
            S32 pos = (S32)(tok.t - tok.buf);
            out_tokens[pos] = TOK_STRING_LITERAL;
            tok.t++;

            int result = js_scan_template_content(&tok, out_tokens);
            if (result == 1) {
                // Hit ${  -- push template depth
                if (tmpl_depth < JS_MAX_TEMPLATE_DEPTH) {
                    brace_count[tmpl_depth] = 0;
                    tmpl_depth++;
                }
            }
            // result == 0: self-contained template (no interpolation, or
            // all interpolations already resolved recursively)

            prev.type = TOK_STRING_LITERAL;
            prev.start = pos;
            prev.len = (S32)(tok.t - tok.buf) - pos;
            continue;
        }

        // ---- Closing brace that ends a template interpolation ----
        // When we're inside a template expression and the brace count is
        // zero, this } closes the ${...} and we resume string scanning.
        if (c == '}' && tmpl_depth > 0 && brace_count[tmpl_depth - 1] == 0) {
            S32 pos = (S32)(tok.t - tok.buf);
            out_tokens[pos] = TOK_PUNCTUATION;
            tok.t++;
            tmpl_depth--;

            // Resume template string content
            int result = js_scan_template_content(&tok, out_tokens);
            if (result == 1) {
                // Hit another ${  -- re-enter expression mode
                if (tmpl_depth < JS_MAX_TEMPLATE_DEPTH) {
                    brace_count[tmpl_depth] = 0;
                    tmpl_depth++;
                }
            }
            // result == 0: template closed with backtick

            prev.type = TOK_STRING_LITERAL;
            prev.start = pos;
            prev.len = (S32)(tok.t - tok.buf) - pos;
            continue;
        }

        // ---- Normal token ----
        tok.start_t = tok.t;
        Token token = js_get_next_token(&tok, prev, data);
        if (token.len == 0) break;

        paint_token(out_tokens, token.start, token.len, token.type);

        // Track brace depth for template interpolation
        if (tmpl_depth > 0 && token.type == TOK_PUNCTUATION && token.len == 1) {
            char tc = data[token.start];
            if (tc == '{')      brace_count[tmpl_depth - 1]++;
            else if (tc == '}') brace_count[tmpl_depth - 1]--;
        }

        // Retroactively mark identifier before '(' as function
        if (token.type == TOK_PUNCTUATION && token.len == 1 &&
            data[token.start] == '(' && prev.type == TOK_IDENTIFIER) {
            paint_token(out_tokens, prev.start, prev.len, TOK_FUNCTION);
        }

        prev = token;
    }
}
