#!/usr/bin/env python3

"""
Generate JavaScript constants from lexbor C headers.

Parses enum values from header files and writes a JS module that can be
included via Emscripten's --pre-js flag.

Usage:
    python3 gen_constants.py <source_dir> <output_file> <enum_spec> [<enum_spec> ...]

Each <enum_spec> is: "header_rel:enum_name:js_prefix"

Example:
    python3 gen_constants.py ../source wasm_constants.js \
        "lexbor/core/base.h:lexbor_status_t:STATUS" \
        "lexbor/dom/interfaces/document.h:lxb_dom_document_opt:OPT"
"""

import sys
import os
import re


def find_strip_prefix(members):
    """Find the longest common prefix of all enum member names.

    LXB_STATUS_OK, LXB_STATUS_ERROR, ... -> LXB_STATUS_
    LXB_DOM_DOCUMENT_OPT_UNDEF, ...      -> LXB_DOM_DOCUMENT_OPT_
    """

    if not members:
        return ""

    names = [m[0] for m in members]
    prefix = names[0]

    for name in names[1:]:
        while not name.startswith(prefix):
            prefix = prefix[:-1]
            if not prefix:
                return ""

    # Trim to last underscore to get a clean prefix boundary.
    idx = prefix.rfind("_")
    if idx >= 0:
        return prefix[:idx + 1]

    return ""


def parse_enum(filepath, enum_name):
    """Parse enum members and their values from a C header file.

    Supports both forms:
        enum name { ... };
        typedef enum { ... } name_t;
    """

    with open(filepath, "r") as f:
        content = f.read()

    # Try: enum name { ... }
    pattern = r"enum\s+" + re.escape(enum_name) + r"\s*\{([^}]+)\}"
    match = re.search(pattern, content)

    if not match:
        # Try: typedef enum { ... } name_t;
        pattern = r"typedef\s+enum\s*\{([^}]+)\}\s*" + re.escape(enum_name) \
                  + r"\s*;"
        match = re.search(pattern, content)

    if not match:
        print(f"Warning: enum '{enum_name}' not found in {filepath}",
              file=sys.stderr)
        return []

    body = match.group(1)
    members = []
    value = 0

    for line in body.split("\n"):
        line = line.strip().rstrip(",")

        # Skip empty lines and comments.
        if not line or line.startswith("/*") or line.startswith("*") \
                     or line.startswith("//"):
            continue

        # Match: MEMBER_NAME = 0x1234  or  MEMBER_NAME = 1 << 3  or  MEMBER_NAME
        m = re.match(r"^(\w+)\s*(?:=\s*(.+?))?\s*$", line)
        if not m:
            continue

        name = m.group(1)

        if m.group(2):
            expr = m.group(2).strip()
            try:
                value = int(expr, 0)
            except ValueError:
                # Handle expressions like "1 << 0".
                shift = re.match(r"(\d+)\s*<<\s*(\d+)", expr)
                if shift:
                    value = int(shift.group(1)) << int(shift.group(2))
                else:
                    continue

        members.append((name, value))
        value += 1

    return members


def to_js_name(c_name, js_prefix, common_prefix):
    """Convert C enum name to JS constant name.

    Strips the common prefix found among all members, then prepends js_prefix.
    """

    if common_prefix and c_name.startswith(common_prefix):
        return js_prefix + "_" + c_name[len(common_prefix):]

    return c_name


def parse_enum_specs(args):
    """Parse "header_rel:enum_name:js_prefix" arguments into tuples."""

    specs = []

    for arg in args:
        parts = arg.split(":")
        if len(parts) != 3:
            print(f"Error: invalid enum spec '{arg}', "
                  f"expected 'header:enum_name:js_prefix'", file=sys.stderr)
            sys.exit(1)

        specs.append((parts[0], parts[1], parts[2]))

    return specs


def generate(source_dir, output_file, enum_specs):
    lines = []
    lines.append("/* Auto-generated by gen_constants.py — do not edit. */")
    lines.append("")
    lines.append("Module.LEXBOR = Module.LEXBOR || {};")
    lines.append("")

    for header_rel, enum_name, js_prefix in enum_specs:
        filepath = os.path.join(source_dir, header_rel)

        if not os.path.exists(filepath):
            print(f"Error: {filepath} not found", file=sys.stderr)
            sys.exit(1)

        members = parse_enum(filepath, enum_name)

        if not members:
            continue

        common_prefix = find_strip_prefix(members)

        lines.append(f"/* {enum_name} from {header_rel} */")

        for c_name, val in members:
            js_name = to_js_name(c_name, js_prefix, common_prefix)
            lines.append(f"Module.LEXBOR.{js_name} = 0x{val:04X};")

        lines.append("")

    with open(output_file, "w") as f:
        f.write("\n".join(lines))

    print(f"Generated {output_file} ({len(lines)} lines)")


if __name__ == "__main__":
    if len(sys.argv) < 4:
        print(f"Usage: {sys.argv[0]} <source_dir> <output_file> "
              f"<header:enum:prefix> [...]", file=sys.stderr)
        sys.exit(1)

    enum_specs = parse_enum_specs(sys.argv[3:])
    generate(sys.argv[1], sys.argv[2], enum_specs)
