#!/usr/bin/env python3
from __future__ import annotations

import sys

from typing import List
from typing import Optional

from libjs_bytecode_def import Field
from libjs_bytecode_def import OpDef
from libjs_bytecode_def import parse_bytecode_def


def mname_to_param(name: str) -> str:
    if name.startswith("m_") and len(name) > 2:
        return name[2:]
    return name


def getter_name_for_field(field_name: str) -> str:
    return mname_to_param(field_name)


def is_operand_type(t: str) -> bool:
    t = t.strip()
    return t == "Operand" or t == "Optional<Operand>"


def is_optional_operand_type(t: str) -> bool:
    return t.strip() == "Optional<Operand>"


def is_label_type(t: str) -> bool:
    t = t.strip()
    return t == "Label" or t == "Optional<Label>"


def is_optional_label_type(t: str) -> bool:
    return t.strip() == "Optional<Label>"


def is_value_type(t: str) -> bool:
    t = t.strip()
    return t == "Value" or t == "Optional<Value>"


def find_count_field_name(op: OpDef, array_field: Field) -> Optional[str]:
    """
    Heuristic: look for a u32/size_t field matching
      - <name>_count
      - if name ends with 's', also <name_without_s>_count
    """
    candidates = [f"{array_field.name}_count"]

    if array_field.name.endswith("s"):
        base = array_field.name[:-1]
        candidates.append(f"{base}_count")

    for cand in candidates:
        for f in op.fields:
            if f.name == cand and not f.is_array and f.type.strip() in ("u32", "size_t"):
                return cand
    return None


def get_count_field_name_or_die(op: OpDef, array_field: Field) -> str:
    name = find_count_field_name(op, array_field)
    if name is None:
        raise RuntimeError(f"No count field (u32/size_t) found for array field '{array_field.name}' in op '{op.name}'")
    return name


def generate_enum_macro(ops: List[OpDef]) -> str:
    filtered = [op for op in ops if op.name != "Instruction"]
    lines = []
    lines.append("#define ENUMERATE_BYTECODE_OPS(O) \\")
    for i, op in enumerate(filtered):
        last = i == len(filtered) - 1
        if last:
            lines.append(f"    O({op.name})")
        else:
            lines.append(f"    O({op.name}) \\")
    return "\n".join(lines)


def generate_visit_operands(op: OpDef) -> Optional[str]:
    has_any_operand = any(is_operand_type(f.type) for f in op.fields)
    if not has_any_operand:
        return None

    lines: List[str] = []
    lines.append("    void visit_operands_impl(Function<void(Operand&)> visitor)")
    lines.append("    {")

    for f in op.fields:
        t = f.type.strip()
        if not is_operand_type(t):
            continue

        if not f.is_array:
            if is_optional_operand_type(t):
                lines.append(f"        if ({f.name}.has_value())")
                lines.append(f"            visitor({f.name}.value());")
            else:
                lines.append(f"        visitor({f.name});")
        else:
            count_name = get_count_field_name_or_die(op, f)

            if is_optional_operand_type(t):
                lines.append(f"        for (size_t i = 0; i < {count_name}; ++i) {{")
                lines.append(f"            if ({f.name}[i].has_value())")
                lines.append(f"                visitor({f.name}[i].value());")
                lines.append("        }")
            else:
                lines.append(f"        for (size_t i = 0; i < {count_name}; ++i)")
                lines.append(f"            visitor({f.name}[i]);")

    lines.append("    }")
    return "\n".join(lines)


def generate_visit_labels(op: OpDef) -> Optional[str]:
    has_any_label = any(is_label_type(f.type) for f in op.fields)
    if not has_any_label:
        return None

    lines: List[str] = []
    lines.append("    void visit_labels_impl(Function<void(Label&)> visitor)")
    lines.append("    {")

    for f in op.fields:
        t = f.type.strip()
        if not is_label_type(t):
            continue

        if not f.is_array:
            if is_optional_label_type(t):
                lines.append(f"        if ({f.name}.has_value())")
                lines.append(f"            visitor({f.name}.value());")
            else:
                lines.append(f"        visitor({f.name});")
        else:
            count_name = get_count_field_name_or_die(op, f)

            if is_optional_label_type(t):
                lines.append(f"        for (size_t i = 0; i < {count_name}; ++i) {{")
                lines.append(f"            if ({f.name}[i].has_value())")
                lines.append(f"                visitor({f.name}[i].value());")
                lines.append("        }")
            else:
                lines.append(f"        for (size_t i = 0; i < {count_name}; ++i)")
                lines.append(f"            visitor({f.name}[i]);")

    lines.append("    }")
    return "\n".join(lines)


def generate_getters(op: OpDef) -> List[str]:
    lines: List[str] = []
    for f in op.fields:
        gname = getter_name_for_field(f.name)
        if f.is_array:
            count_name = get_count_field_name_or_die(op, f)
            rettype = f"ReadonlySpan<{f.type}>"
            lines.append(
                f"    {rettype} {gname}() const {{ return ReadonlySpan<{f.type}> {{ {f.name}, {count_name} }}; }}"
            )
        else:
            lines.append(f"    auto const& {gname}() const {{ return {f.name}; }}")
    return lines


def execute_return_type(op: OpDef) -> str:
    return "void" if op.is_nothrow else "ThrowCompletionOr<void>"


def generate_class(op: OpDef) -> str:
    if op.name == "Instruction":
        return ""

    base = op.base or "Instruction"
    lines: List[str] = []

    lines.append(f"class {op.name} final : public {base} {{")
    lines.append("public:")

    arrays: List[Field] = [f for f in op.fields if f.is_array]
    has_array = len(arrays) > 0
    has_m_length = any(f.name == "m_length" for f in op.fields)

    if has_array:
        lines.append("    static constexpr bool IsVariableLength = true;")
    if has_m_length:
        lines.append("    size_t length_impl() const { return m_length; }")
    if op.is_terminator:
        lines.append("    static constexpr bool IsTerminator = true;")

    # Map arrays -> count fields (error if missing)
    array_to_count: dict[str, str] = {}
    count_to_array_param: dict[str, str] = {}
    for af in arrays:
        count_name = get_count_field_name_or_die(op, af)
        array_to_count[af.name] = count_name
        count_to_array_param[count_name] = mname_to_param(af.name)

    count_fields = set(count_to_array_param.keys())

    # ctor params: scalars first, then spans
    ctor_params: List[str] = []
    for f in op.fields:
        if f.is_array:
            continue
        if f.type.strip() == "EnvironmentCoordinate":
            continue
        if f.name in count_fields:
            continue
        if f.name == "m_length":
            # synthesized, don't take as parameter
            continue
        ctor_params.append(f"{cpp_type_for_field(f.type)} {mname_to_param(f.name)}")

    span_param_for_array: dict[str, str] = {}
    for af in arrays:
        span_param = mname_to_param(af.name)
        span_param_for_array[af.name] = span_param

        span_elem_type = af.type

        ctor_params.append(f"ReadonlySpan<{span_elem_type}> {span_param}")

    if ctor_params:
        lines.append(f"    {op.name}({', '.join(ctor_params)})")
    else:
        lines.append(f"    {op.name}()")

    # initializer list
    init_entries: List[str] = [f"{base}(Type::{op.name})"]
    for f in op.fields:
        if f.is_array:
            continue
        if f.type.strip() == "EnvironmentCoordinate":
            continue
        if f.name in count_fields:
            span_param = count_to_array_param[f.name]
            init_entries.append(f"{f.name}({span_param}.size())")
            continue
        if f.name == "m_length":
            # choose the first array field as the one that defines the tail
            array_for_length: Optional[Field] = arrays[0] if arrays else None
            if array_for_length is not None:
                span_param = span_param_for_array.get(array_for_length.name)
                elem_t = array_for_length.type.strip()
                if span_param is not None:
                    init_entries.append(
                        "m_length(round_up_to_power_of_two("
                        "alignof(void*), sizeof(*this) + sizeof("
                        f"{elem_t}) * {span_param}.size()))"
                    )
                else:
                    init_entries.append("m_length(0)")
            else:
                init_entries.append("m_length(0)")
            continue

        init_entries.append(f"{f.name}({mname_to_param(f.name)})")

    if init_entries:
        lines.append(f"        : {init_entries[0]}")
        for entry in init_entries[1:]:
            lines.append(f"        , {entry}")
    lines.append("    {")
    # copy spans into trailing arrays
    for af in arrays:
        span_param = span_param_for_array[af.name]
        count_name = array_to_count[af.name]
        elem_t = af.type.strip()
        if elem_t == "Optional<Operand>":
            lines.append(f"        for (size_t i = 0; i < {span_param}.size(); ++i) {{")
            lines.append(f"            if ({span_param}[i].has_value())")
            lines.append(f"                {af.name}[i] = {span_param}[i].value();")
            lines.append("            else")
            lines.append(f"                {af.name}[i] = {{}};")
            lines.append("        }")
        else:
            lines.append(f"        for (size_t i = 0; i < {span_param}.size(); ++i)")
            lines.append(f"            {af.name}[i] = {span_param}[i];")
    lines.append("    }")
    lines.append("")

    ret_type = execute_return_type(op)
    lines.append(f"    {ret_type} execute_impl(VM&) const;")
    lines.append("    ByteString to_byte_string_impl(Bytecode::Executable const&) const;")

    visit_operands = generate_visit_operands(op)
    if visit_operands:
        lines.append(visit_operands)

    visit_labels = generate_visit_labels(op)
    if visit_labels:
        lines.append(visit_labels)

    getters = generate_getters(op)
    if getters:
        lines.append("")
        lines.extend(getters)

    lines.append("")
    lines.append("private:")
    for f in op.fields:
        cpp_t = cpp_type_for_field(f.type)
        if f.is_array:
            lines.append(f"    {cpp_t} {f.name}[];")
        else:
            lines.append(f"    {cpp_t} {f.name};")

    lines.append("};")
    lines.append(f"static_assert(IsTriviallyDestructible<{op.name}>);")
    return "\n".join(lines)


def generate_op_namespace_body(ops: List[OpDef]) -> str:
    lines: List[str] = []
    lines.append("namespace JS::Bytecode::Op {")
    lines.append("")
    for op in ops:
        cls = generate_class(op)
        if cls:
            lines.append(cls)
            lines.append("")
    lines.append("} // namespace JS::Bytecode::Op")
    return "\n".join(lines)


NUMERIC_TYPES = {
    "u8",
    "u16",
    "u32",
    "u64",
    "i8",
    "i16",
    "i32",
    "i64",
    "int",
    "unsigned",
    "unsigned int",
    "size_t",
}

CACHE_INDEX_TYPES = {
    "PropertyLookupCacheIndex",
    "GlobalVariableCacheIndex",
    "EnvironmentCoordinateCacheIndex",
    "TemplateObjectCacheIndex",
    "ObjectShapeCacheIndex",
    "ObjectPropertyIteratorCacheIndex",
}


def is_cache_index_type(t: str) -> bool:
    return t.strip() in CACHE_INDEX_TYPES


def cpp_type_for_field(t: str) -> str:
    """Return the C++ storage type for a Bytecode.def field type."""
    if is_cache_index_type(t):
        return "u32"
    return t


def generate_to_byte_string_impl(op: OpDef) -> str:
    if op.name == "Instruction":
        return ""

    lines: List[str] = []
    lines.append(
        f"ByteString {op.name}::to_byte_string_impl([[maybe_unused]] Bytecode::Executable const& executable) const"
    )
    lines.append("{")
    lines.append("    StringBuilder builder;")
    lines.append(f'    builder.append("{op.name}"sv);')
    lines.append("")
    lines.append("    bool first = true;")
    lines.append("    [[maybe_unused]] auto append_piece = [&](auto const& piece) {")
    lines.append("        if (first) {")
    lines.append("            builder.append(' ');")
    lines.append("            first = false;")
    lines.append("        } else {")
    lines.append('            builder.append(", "sv);')
    lines.append("        }")
    lines.append("        builder.append(piece);")
    lines.append("    };")
    lines.append("")

    arrays: List[Field] = [f for f in op.fields if f.is_array]
    array_to_count = {af.name: get_count_field_name_or_die(op, af) for af in arrays}
    count_fields = set(array_to_count.values())

    for f in op.fields:
        if f.name == "m_length" or f.name == "m_cache":
            continue

        t = f.type.strip()
        label = getter_name_for_field(f.name)

        if f.is_array:
            count_name = array_to_count[f.name]

            if t == "Operand":
                lines.append(f"    if ({count_name} != 0)")
                lines.append(
                    f'        append_piece(format_operand_list("{label}"sv, {{ {f.name}, {count_name} }}, executable));'
                )
                lines.append("")
                continue

            if t == "Optional<Operand>":
                lines.append(f"    if ({count_name} != 0) {{")
                lines.append("        StringBuilder list_builder;")
                lines.append(f'        list_builder.appendff("{{}}:[", "{label}"sv);')
                lines.append("        bool first_elem = true;")
                lines.append(f"        for (size_t i = 0; i < {count_name}; ++i) {{")
                lines.append(f"            if (!{f.name}[i].has_value())")
                lines.append("                continue;")
                lines.append("            if (!first_elem)")
                lines.append('                list_builder.append(", "sv);')
                lines.append("            first_elem = false;")
                lines.append(
                    f'            list_builder.append(format_operand("{label}"sv, {f.name}[i].value(), executable));'
                )
                lines.append("        }")
                lines.append("        list_builder.append(']');")
                lines.append("        append_piece(list_builder.to_byte_string());")
                lines.append("    }")
                lines.append("")
                continue

            if t == "Value":
                lines.append(f"    if ({count_name} != 0)")
                lines.append(
                    f'        append_piece(format_value_list("{label}"sv, ReadonlySpan<Value> {{ {f.name}, {count_name} }}));'
                )
                lines.append("")
                continue

            if t == "Label":
                lines.append(f"    if ({count_name} != 0) {{")
                lines.append("        StringBuilder list_builder;")
                lines.append(f'        list_builder.appendff("{{}}:[", "{label}"sv);')
                lines.append("        bool first_elem = true;")
                lines.append(f"        for (size_t i = 0; i < {count_name}; ++i) {{")
                lines.append("            if (!first_elem)")
                lines.append('                list_builder.append(", "sv);')
                lines.append("            first_elem = false;")
                lines.append(f'            list_builder.append(format_label(""sv, {f.name}[i], executable));')
                lines.append("        }")
                lines.append("        list_builder.append(']');")
                lines.append("        append_piece(list_builder.to_byte_string());")
                lines.append("    }")
                lines.append("")
                continue

            if t == "Optional<Label>":
                lines.append(f"    if ({count_name} != 0) {{")
                lines.append("        StringBuilder list_builder;")
                lines.append(f'        list_builder.appendff("{{}}:[", "{label}"sv);')
                lines.append("        bool first_elem = true;")
                lines.append(f"        for (size_t i = 0; i < {count_name}; ++i) {{")
                lines.append(f"            if (!{f.name}[i].has_value())")
                lines.append("                continue;")
                lines.append("            if (!first_elem)")
                lines.append('                list_builder.append(", "sv);')
                lines.append("            first_elem = false;")
                lines.append(f'            list_builder.append(format_label(""sv, {f.name}[i].value(), executable));')
                lines.append("        }")
                lines.append("        list_builder.append(']');")
                lines.append("        append_piece(list_builder.to_byte_string());")
                lines.append("    }")
                lines.append("")
                continue

            # other array types not printed
            continue

        if t == "Operand":
            lines.append(f'    append_piece(format_operand("{label}"sv, {f.name}, executable));')
            lines.append("")
            continue

        if t == "Optional<Operand>":
            lines.append(f"    if ({f.name}.has_value())")
            lines.append(f'        append_piece(format_operand("{label}"sv, {f.name}.value(), executable));')
            lines.append("")
            continue

        if t == "Label":
            lines.append(f'    append_piece(format_label("{label}"sv, {f.name}, executable));')
            lines.append("")
            continue

        if t == "Optional<Label>":
            lines.append(f"    if ({f.name}.has_value())")
            lines.append(f'        append_piece(format_label("{label}"sv, {f.name}.value(), executable));')
            lines.append("")
            continue

        if t == "PropertyKeyTableIndex":
            lines.append(
                f'    append_piece(ByteString::formatted("\\033[36m`{{}}`\\033[0m", executable.property_key_table->get({f.name})));'
            )
            lines.append("")
            continue

        if t == "IdentifierTableIndex":
            lines.append(
                f'    append_piece(ByteString::formatted("\\033[36m`{{}}`\\033[0m", executable.identifier_table->get({f.name})));'
            )
            lines.append("")
            continue

        if t == "Optional<IdentifierTableIndex>":
            # Find a property field in the same op to join with.
            property_key_field = None
            property_operand_field = None
            for other in op.fields:
                if other.type.strip() == "PropertyKeyTableIndex":
                    property_key_field = other
                    break
                if other.type.strip() == "Operand" and other.name == "m_property":
                    property_operand_field = other
                    break

            lines.append(f"    if ({f.name}.has_value())")
            if property_key_field:
                lines.append(
                    f'        builder.appendff(" \\033[37;1m({{}}.{{}})\\033[0m", executable.identifier_table->get({f.name}.value()), executable.property_key_table->get({property_key_field.name}));'
                )
            elif property_operand_field:
                lines.append("    {")
                lines.append(
                    f'        auto property_hint = format_operand(""sv, {property_operand_field.name}, executable);'
                )
                lines.append(
                    f'        builder.appendff(" \\033[37;1m({{}}[\\033[0m{{}}\\033[37;1m])\\033[0m", executable.identifier_table->get({f.name}.value()), property_hint);'
                )
                lines.append("    }")
            elif op.name == "GetLength":
                lines.append(
                    f'        builder.appendff(" \\033[37;1m({{}}.length)\\033[0m", executable.identifier_table->get({f.name}.value()));'
                )
            else:
                lines.append(
                    f'        builder.appendff(" \\033[37;1m({{}})\\033[0m", executable.identifier_table->get({f.name}.value()));'
                )
            lines.append("")
            continue

        if t == "StringTableIndex":
            lines.append(f"    append_piece(executable.get_string({f.name}));")
            lines.append("")
            continue

        if t == "Optional<StringTableIndex>":
            lines.append(f"    if ({f.name}.has_value())")
            lines.append(f"        append_piece(executable.get_string({f.name}.value()));")
            lines.append("")
            continue

        if is_value_type(t):
            # not printed for now
            continue

        if t == "bool":
            lines.append(f'    append_piece(ByteString::formatted("{label}:{{}}", {f.name}));')
            lines.append("")
            continue

        if t in NUMERIC_TYPES and f.name not in count_fields:
            lines.append(f'    append_piece(ByteString::formatted("{label}:{{}}", {f.name}));')
            lines.append("")
            continue

        if t == "PutKind":
            lines.append(f'    append_piece(ByteString::formatted("{label}:{{}}", put_kind_to_string({f.name})));')
            lines.append("")
            continue

        # other types (enums, refs, etc.) skipped

    lines.append("    return builder.to_byte_string();")
    lines.append("}")
    lines.append("")
    return "\n".join(lines)


def generate_op_cpp_body(ops: List[OpDef]) -> str:
    lines: List[str] = []
    lines.append("#include <AK/StringBuilder.h>")
    lines.append("#include <AK/StringView.h>")
    lines.append("#include <LibJS/Bytecode/FormatOperand.h>")
    lines.append("#include <LibJS/Bytecode/Op.h>")
    lines.append("")
    lines.append("namespace JS::Bytecode::Op {")
    lines.append("")

    for op in ops:
        impl = generate_to_byte_string_impl(op)
        if impl:
            lines.append(impl)

    lines.append("} // namespace JS::Bytecode::Op")
    return "\n".join(lines)


def generate_opcodes_h(ops: List[OpDef]) -> str:
    macro = generate_enum_macro(ops)
    lines: List[str] = []
    lines.append("#pragma once")
    lines.append("")
    lines.append(macro)
    lines.append("")
    return "\n".join(lines)


def generate_op_h(ops: List[OpDef]) -> str:
    includes = """#pragma once

#include <AK/Span.h>
#include <AK/StdLibExtras.h>
#include <LibJS/Bytecode/Builtins.h>
#include <LibJS/Bytecode/IdentifierTable.h>
#include <LibJS/Bytecode/Instruction.h>
#include <LibJS/Bytecode/Label.h>
#include <LibJS/Bytecode/Operand.h>
#include <LibJS/Bytecode/PutKind.h>
#include <LibJS/Bytecode/RegexTable.h>
#include <LibJS/Bytecode/Register.h>
#include <LibJS/Bytecode/StringTable.h>
#include <LibJS/Runtime/BigInt.h>
#include <LibJS/Runtime/Environment.h>
#include <LibJS/Runtime/Iterator.h>
#include <LibJS/Runtime/Value.h>
"""
    body = generate_op_namespace_body(ops)
    return includes + "\n" + body


def usage(prog: str) -> None:
    print(
        f"Usage: {prog} -c path/to/Op.cpp -h path/to/Op.h -x path/to/OpCodes.h -i path/to/Bytecode.def",
        file=sys.stderr,
    )


def main(argv: List[str]) -> None:
    c_path = None
    h_path = None
    x_path = None
    def_path = None

    i = 1
    while i < len(argv):
        arg = argv[i]
        if arg == "-c" and i + 1 < len(argv):
            c_path = argv[i + 1]
            i += 2
        elif arg == "-h" and i + 1 < len(argv):
            h_path = argv[i + 1]
            i += 2
        elif arg == "-x" and i + 1 < len(argv):
            x_path = argv[i + 1]
            i += 2
        elif arg == "-i" and i + 1 < len(argv):
            def_path = argv[i + 1]
            i += 2
        else:
            usage(argv[0])
            sys.exit(1)

    if not (c_path and h_path and x_path and def_path):
        usage(argv[0])
        sys.exit(1)

    ops = parse_bytecode_def(def_path)

    op_h = generate_op_h(ops)
    op_cpp = generate_op_cpp_body(ops)
    opcodes_h = generate_opcodes_h(ops)

    with open(h_path, "w", encoding="utf-8") as f:
        f.write(op_h)

    with open(c_path, "w", encoding="utf-8") as f:
        f.write(op_cpp)

    with open(x_path, "w", encoding="utf-8") as f:
        f.write(opcodes_h)


if __name__ == "__main__":
    main(sys.argv)
