#!/usr/bin/env python3
"""Extract strings and sound-ID references from Godot 4 compiled GDScript.

The exported `.gdc` files in this project contain a 12-byte `GDSC` header
followed by a Zstandard payload. Some strings are plain length-prefixed ASCII;
many identifiers are XOR-obfuscated with byte `0xb6` and stored in a
UTF-32LE-like layout. This script recovers both forms statically.
"""

from __future__ import annotations

import argparse
import csv
import json
import re
import subprocess
from pathlib import Path


PLAIN_TOKEN_RE = re.compile(rb"[A-Za-z0-9_./:-]{3,}")
SOUND_RE = re.compile(r"SE_[A-Z0-9_]+")


def decompress_gdc(path: Path) -> bytes:
    data = path.read_bytes()
    if not data.startswith(b"GDSC"):
        return b""
    proc = subprocess.run(["zstd", "-dc"], input=data[12:], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if proc.returncode != 0:
        raise RuntimeError(f"zstd failed for {path}: {proc.stderr.decode(errors='replace')}")
    return proc.stdout


def utf32_xor_tokens(payload: bytes) -> set[str]:
    xored = bytes(byte ^ 0xB6 for byte in payload)
    chars: list[str] = []
    index = 0
    while index + 3 < len(xored):
        if xored[index + 1 : index + 4] == b"\0\0\0" and 32 <= xored[index] <= 126:
            chars.append(chr(xored[index]))
            index += 4
        else:
            chars.append("\n")
            index += 1
    text = "".join(chars)
    return set(re.findall(r"[A-Za-z0-9_./:-]{3,}", text))


def extract_tokens(path: Path) -> tuple[set[str], int]:
    payload = decompress_gdc(path)
    plain = {match.group(0).decode("ascii", errors="replace") for match in PLAIN_TOKEN_RE.finditer(payload)}
    return plain | utf32_xor_tokens(payload), len(payload)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--root", type=Path, default=Path("assets/godot_raw"))
    parser.add_argument("--out-dir", type=Path, default=Path("analysis/godot-introspection"))
    parser.add_argument("--constant-map", type=Path, default=Path("analysis/godot-introspection/spike_sound_constant_map.json"))
    args = parser.parse_args()

    args.out_dir.mkdir(parents=True, exist_ok=True)
    known_sound_names: set[str] = set()
    if args.constant_map.exists():
        known_sound_names = set(json.loads(args.constant_map.read_text()).keys())
    string_rows: list[dict[str, object]] = []
    sound_rows: list[dict[str, object]] = []
    script_rows: list[dict[str, object]] = []
    for path in sorted(args.root.rglob("*.gdc")):
        tokens, payload_size = extract_tokens(path)
        rel = path.as_posix()
        sound_names = sorted({token for token in tokens if SOUND_RE.fullmatch(token) and (not known_sound_names or token in known_sound_names)})
        script_rows.append(
            {
                "path": rel,
                "compressed_bytes": path.stat().st_size,
                "decompressed_bytes": payload_size,
                "token_count": len(tokens),
                "sound_ref_count": len(sound_names),
                "has_spike_play_sound": "Spike" in tokens and "play_sound" in tokens,
            }
        )
        for token in sorted(tokens):
            string_rows.append({"path": rel, "token": token})
        for sound_name in sound_names:
            sound_rows.append({"path": rel, "sound_name": sound_name})

    for name, rows, fields in [
        ("gdc_script_summary.tsv", script_rows, ["path", "compressed_bytes", "decompressed_bytes", "token_count", "sound_ref_count", "has_spike_play_sound"]),
        ("gdc_strings.tsv", string_rows, ["path", "token"]),
        ("gdc_sound_refs.tsv", sound_rows, ["path", "sound_name"]),
    ]:
        with (args.out_dir / name).open("w", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=fields, delimiter="\t")
            writer.writeheader()
            writer.writerows(rows)
    print(f"scripts={len(script_rows)} strings={len(string_rows)} sound_refs={len(sound_rows)} out={args.out_dir}")
    return 0


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