#!/usr/bin/env python3
# Copyright 2026, Beyley Cardellio
# SPDX-License-Identifier: BSL-1.0

"""Generate a unified header containing versioned OpenVR interface classes."""

from __future__ import annotations

import argparse
import json
from dataclasses import dataclass
from pathlib import Path
import re
import subprocess
import sys


JSON_PATTERN = "openvr_api_v*.json"
HEADER_PATTERN = "openvr_{version}.h"


@dataclass(frozen=True)
class InterfaceDefinition:
    class_name: str
    interface_version: str
    source_version: str
    header_path: Path


def version_sort_key(version: str) -> tuple[tuple[int | str, ...], str]:
    normalized = version[1:] if version.startswith("v") else version
    parts = re.findall(r"\d+|[A-Za-z]+", normalized)
    key = tuple(int(part) if part.isdigit() else part.lower() for part in parts)
    return key, normalized.lower()


def label_sort_key(label: str) -> tuple[tuple[int | str, ...], str]:
    parts = re.findall(r"\d+|[A-Za-z]+", label)
    key = tuple(int(part) if part.isdigit() else part.lower() for part in parts)
    return key, label.lower()


def read_header_text(path: Path) -> str:
    data = path.read_bytes()
    for encoding in ("utf-8", "cp1252", "latin-1"):
        try:
            return data.decode(encoding)
        except UnicodeDecodeError:
            continue
    return data.decode("latin-1", errors="replace")


def load_interface_versions(json_path: Path) -> dict[str, str] | None:
    try:
        payload = json.loads(json_path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return None

    class_names = {
        method.get("classname", "").split("::")[-1]
        for method in payload.get("methods", [])
        if method.get("classname")
    }
    if not class_names:
        return None

    const_versions = {
        const.get("constname")[: -len("_Version")]: const.get("constval")
        for const in payload.get("consts", [])
        if const.get("constname", "").endswith("_Version") and const.get("constval")
    }

    interface_versions = {
        class_name: const_versions[class_name]
        for class_name in sorted(class_names)
        if class_name in const_versions
    }
    if not interface_versions:
        return None

    return interface_versions


def collect_interfaces(input_dir: Path) -> dict[str, InterfaceDefinition]:
    definitions: dict[str, InterfaceDefinition] = {}
    header_text_cache: dict[Path, str] = {}

    json_paths = sorted(
        input_dir.glob(JSON_PATTERN),
        key=lambda path: version_sort_key(path.stem.removeprefix("openvr_api_")),
    )
    for json_path in json_paths:
        source_version = json_path.stem.removeprefix("openvr_api_")
        header_path = input_dir / HEADER_PATTERN.format(version=source_version)
        if not header_path.exists():
            print(f"Skipping {json_path.name}: missing {header_path.name}", file=sys.stderr)
            continue

        interface_versions = load_interface_versions(json_path)
        if interface_versions is None:
            print(f"Skipping {json_path.name}: could not read interface versions", file=sys.stderr)
            continue

        for class_name, interface_version in interface_versions.items():
            header_text = header_text_cache.get(header_path)
            if header_text is None:
                header_text = read_header_text(header_path)
                header_text_cache[header_path] = header_text

            if extract_class_definition(header_text, class_name) is None:
                continue

            definitions[interface_version] = InterfaceDefinition(
                class_name=class_name,
                interface_version=interface_version,
                source_version=source_version,
                header_path=header_path,
            )

    return definitions


def extract_class_definition(header_text: str, class_name: str) -> str | None:
    lines = header_text.splitlines(keepends=True)
    class_index = None

    class_pattern = re.compile(rf"^\s*class\s+{re.escape(class_name)}\b")
    for index, line in enumerate(lines):
        if class_pattern.search(line):
            class_index = index
            break

    if class_index is None:
        return None

    start_index = class_index
    previous = class_index - 1
    while previous >= 0 and not lines[previous].strip():
        previous -= 1

    if previous >= 0 and lines[previous].lstrip().startswith("*/"):
        start_index = previous
        while start_index >= 0 and "/**" not in lines[start_index]:
            start_index -= 1
        if start_index < 0:
            start_index = class_index
    elif previous >= 0 and lines[previous].lstrip().startswith("//"):
        start_index = previous
        while start_index > 0 and lines[start_index - 1].lstrip().startswith("//"):
            start_index -= 1

    end_index = None
    for index in range(class_index, len(lines)):
        if lines[index].strip() == "};":
            end_index = index
            break

    if end_index is None:
        return None

    return "".join(lines[start_index : end_index + 1]).rstrip()


def strip_comments(text: str) -> str:
    result: list[str] = []
    index = 0
    state = "normal"
    quote = ""

    while index < len(text):
        char = text[index]
        next_char = text[index + 1] if index + 1 < len(text) else ""

        if state == "normal":
            if char == "/" and next_char == "/":
                state = "line_comment"
                index += 2
                continue
            if char == "/" and next_char == "*":
                if result and not result[-1].isspace():
                    result.append(" ")
                state = "block_comment"
                index += 2
                continue
            if char in ('\"', "'"):
                state = "string"
                quote = char
                result.append(char)
                index += 1
                continue

            result.append(char)
            index += 1
            continue

        if state == "line_comment":
            if char == "\n":
                result.append("\n")
                state = "normal"
            index += 1
            continue

        if state == "block_comment":
            if char == "\n":
                result.append("\n")
                index += 1
                continue
            if char == "*" and next_char == "/":
                state = "normal"
                index += 2
                continue
            index += 1
            continue

        result.append(char)
        index += 1
        if char == "\\" and index < len(text):
            result.append(text[index])
            index += 1
            continue
        if char == quote:
            state = "normal"

    return "".join(result)


def normalize_class_definition(definition: str) -> str:
    without_comments = strip_comments(definition)
    normalized_lines = []
    for line in without_comments.splitlines():
        stripped_line = line.rstrip()
        if not stripped_line.strip():
            continue
        normalized_lines.append(stripped_line)

    return "\n".join(normalized_lines)


def rename_class_definition(definition: str, class_name: str, interface_version: str) -> str:
    renamed = re.sub(
        rf"\bclass\s+{re.escape(class_name)}\b",
        f"class {interface_version}",
        definition,
        count=1,
    )
    return re.sub(
        rf"~{re.escape(class_name)}\b",
        f"~{interface_version}",
        renamed,
    )


def run_clang_format(path: Path) -> None:
    subprocess.run([
        "clang-format",
        "-style=file",
        "-i",
        str(path),
    ], check=True)


def generate_header(definitions: dict[str, InterfaceDefinition], include_name: str) -> str:
    header_text_cache: dict[Path, str] = {}
    blocks: list[tuple[str, str]] = []

    ordered_definitions = sorted(
        definitions.values(),
        key=lambda definition: (
            definition.class_name.lower(),
            label_sort_key(definition.interface_version),
        ),
    )

    for definition in ordered_definitions:
        header_text = header_text_cache.get(definition.header_path)
        if header_text is None:
            header_text = read_header_text(definition.header_path)
            header_text_cache[definition.header_path] = header_text

        class_definition = extract_class_definition(header_text, definition.class_name)
        if class_definition is None:
            print(
                f"Skipping {definition.interface_version}: could not find {definition.class_name} in {definition.header_path.name}",
                file=sys.stderr,
            )
            continue

        renamed_definition = rename_class_definition(
            class_definition,
            definition.class_name,
            definition.interface_version,
        )
        blocks.append(
            (definition.interface_version, normalize_class_definition(renamed_definition))
        )

    output_lines = [
        "//========= Copyright Valve Corporation ============//",
        "",
        "// Generated from versioned OpenVR headers.",
        "// Each interface version is copied from the newest header that still contains it.",
        "",
        "#pragma once",
        "",
        f'#include "{include_name}"',
        "#include \"openvr_old_types.h\"",
        "",
        "namespace vr",
        "{",
    ]

    for version_name, block in blocks:
        output_lines.append(
            f'static const char * const {version_name}_Version = "{version_name}";'
        )
        output_lines.append("")
        output_lines.append(block)
        output_lines.append("")
        output_lines.append("")

    if output_lines[-1] == "":
        output_lines.pop()
    if output_lines[-1] == "":
        output_lines.pop()

    output_lines.extend(("", "}", ""))
    return "\n".join(output_lines)


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Generate a unified header containing all OpenVR interface versions."
    )
    parser.add_argument(
        "-i",
        "--input-dir",
        type=Path,
        required=True,
        help="Directory containing versioned openvr_*.h and openvr_api_*.json files.",
    )
    parser.add_argument(
        "-o",
        "--output-file",
        type=Path,
        required=True,
        help="Header file to write.",
    )
    parser.add_argument(
        "--include",
        default="openvr.h",
        help='Header to include at the top of the generated file (default: "openvr.h").',
    )
    args = parser.parse_args()

    input_dir = args.input_dir.resolve()
    if not input_dir.exists():
        parser.error(f"{input_dir} does not exist")

    output_file = args.output_file.resolve()
    output_file.parent.mkdir(parents=True, exist_ok=True)

    definitions = collect_interfaces(input_dir)
    if not definitions:
        print(f"No interface versions found in {input_dir}", file=sys.stderr)
        return 1

    output_file.write_text(generate_header(definitions, args.include), encoding="utf-8")
    try:
        run_clang_format(output_file)
    except FileNotFoundError:
        print("clang-format was not found in PATH", file=sys.stderr)
        return 1
    except subprocess.CalledProcessError:
        print(f"clang-format failed for {output_file}", file=sys.stderr)
        return 1

    print(f"Wrote {len(definitions)} interface version definition(s) to {output_file}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
