#!/usr/bin/env python3
"""Verify the collision trace-model material refresh contract."""

from __future__ import annotations

import argparse
import pathlib
import re


PATCH_NAME = "0035-refresh-tracemodel-material-after-decl-reload.patch"
OLD_PATCH_NAME = "0035-reset-tracemodel-material-after-map-purge.patch"
ASSIGNMENT = 'trmMaterial = declManager->FindMaterial( "_tracemodel", false );'


def shell_array(source: str, variable: str) -> tuple[str, ...]:
    match = re.search(
        rf"^{variable}=\((.*?)^\)$",
        source,
        flags=re.MULTILINE | re.DOTALL,
    )
    if not match:
        raise ValueError(f"missing shell array {variable}")
    return tuple(re.findall(r"patches/(\d{4}-[^/\"']+\.patch)", match.group(1)))


def function_body(source: str, signature: str) -> str:
    start = source.find(signature)
    if start < 0:
        raise ValueError(f"missing function {signature}")
    opening = source.find("{", start + len(signature))
    if opening < 0:
        raise ValueError(f"missing body for {signature}")
    depth = 0
    for index in range(opening, len(source)):
        if source[index] == "{":
            depth += 1
        elif source[index] == "}":
            depth -= 1
            if depth == 0:
                return source[opening + 1:index]
    raise ValueError(f"unterminated body for {signature}")


def verify_patch_lists(root: pathlib.Path) -> None:
    scripts = (
        root / "tools/build-native.sh",
        root / "tools/build-wasm.sh",
        root / "tools/stage-source-offer.sh",
    )
    for script in scripts:
        source = script.read_text(encoding="utf-8")
        openq4 = shell_array(source, "OPENQ4_PATCH_FILES")
        gamelibs = shell_array(source, "GAMELIBS_PATCH_FILES")
        if openq4.count(PATCH_NAME) != 1:
            raise ValueError(f"{script} must include {PATCH_NAME} exactly once")
        if PATCH_NAME in gamelibs:
            raise ValueError(f"{script} classifies {PATCH_NAME} as a GameLib patch")
        if OLD_PATCH_NAME in source:
            raise ValueError(f"{script} still references obsolete {OLD_PATCH_NAME}")


def verify_source(source_root: pathlib.Path) -> None:
    path = source_root / "src/cm/CollisionModel_load.cpp"
    source = path.read_text(encoding="utf-8")

    setup = function_body(
        source,
        "void idCollisionModelManagerLocal::SetupTrmModelStructure( void )",
    )
    if setup.count(ASSIGNMENT) != 1:
        raise ValueError("SetupTrmModelStructure must resolve _tracemodel exactly once")
    if "if ( trmMaterial == NULL )" in setup:
        raise ValueError("SetupTrmModelStructure still guards the material refresh")
    if setup.find(ASSIGNMENT) > setup.find("if ( !trmMaterial )"):
        raise ValueError("SetupTrmModelStructure validates the cache before refreshing it")

    load_map = function_body(
        source,
        "void idCollisionModelManagerLocal::LoadMap( const idMapFile *mapFile, bool forceCreateMap )",
    )
    setup_call = load_map.find("SetupTrmModelStructure();")
    find_model = load_map.find("FindModel(")
    if setup_call < 0 or find_model < 0 or setup_call > find_model:
        raise ValueError("LoadMap must refresh _tracemodel before reusing collision models")

    model_from_trm = function_body(
        source,
        "idCollisionModel *idCollisionModelManagerLocal::ModelFromTrm",
    )
    if "if ( trmMaterial == NULL )" not in model_from_trm:
        raise ValueError("ModelFromTrm lost its no-map fallback refresh")
    if "SetupTrmModelStructure();" not in model_from_trm:
        raise ValueError("ModelFromTrm fallback no longer resolves _tracemodel")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--root", type=pathlib.Path, required=True)
    parser.add_argument("--source-root", type=pathlib.Path, required=True)
    args = parser.parse_args()

    try:
        verify_patch_lists(args.root.resolve())
        verify_source(args.source_root.resolve())
    except (OSError, UnicodeError, ValueError) as error:
        raise SystemExit(f"tracemodel-material-refresh: {error}") from error
    print("Trace-model material refresh contract passed.")


if __name__ == "__main__":
    main()
