"""Convert Division DIV-VIZ2 assets (VGF/VMF/TGA/SPL/SCN) to JSON for the web viewer.

Formats reverse-engineered from the VWE 'Glaze' drive (STDAVE, 1996):
- VGF: text geometry. OBJECT { GEOGROUP(F_MATERIAL=..;VERTEX=flags) {
         PMESH { VERTEX_POOL {..} CONNECTION_LIST (PCOUNT=n) {..} .. }
         | SPHERELIST {..} } }
  Vertex tuple = pos(3) [+ normal(3) if NORMALS] [+ uv(2) if 2D_TEXTURE].
- VMF: text materials. TEXTURE(NAME=..){MAP{"base"} BITSLICE{n}} selects
  grayscale TGA "BASE<n+1>.TGA"; MATERIAL(NAME=..){AMBIENT/DIFFUSE/EMISSIVE
  {r,g,b} TEXTURE{"t"}}.
- SPL: first line = knot count, rows = x y z (+3 unused here).
- SCN: scene script (camera, lights, fog, STATIC/DYNAMIC placements).
- TGA: type-2 uncompressed 24bpp, bottom-up.
"""
import json, re, struct, sys, zlib, base64, os

ROOT = r"c:\VWE\TeslaRel410\sda4\STDAVE"
BUILD = os.path.join(ROOT, "VIDEO", "BUILD")

# ---------------------------------------------------------------- tokenizer
def strip_comments(text):
    text = re.sub(r"/\*.*?\*/", " ", text, flags=re.S)
    text = re.sub(r"//[^\n]*", " ", text)
    return text

# ---------------------------------------------------------------- VGF
def parse_vgf(path):
    """Return list of groups: {material, mode, verts/normals/uvs/indices or points}."""
    text = strip_comments(open(path, "r", errors="replace").read())
    groups = []
    pos = 0
    for gm in re.finditer(r"GEOGROUP\s*\(([^)]*)\)\s*\{", text):
        params = gm.group(1)
        mmat = re.search(r'F_MATERIAL\s*=\s*"([^"]+)"', params)
        material = mmat.group(1) if mmat else None
        vflags = re.search(r"VERTEX\s*=\s*([^;]+);", params)
        flags = vflags.group(1).upper() if vflags else ""
        has_n = "NORMALS" in flags
        has_uv = "2D_TEXTURE" in flags
        body_start = gm.end()
        # find matching close brace for the group body
        depth, i = 1, body_start
        while depth > 0 and i < len(text):
            if text[i] == "{": depth += 1
            elif text[i] == "}": depth -= 1
            i += 1
        body = text[body_start:i - 1]

        g = {"material": material}
        sph = re.search(r"SPHERELIST\s*\{(.*?)\n\s*\}", body, re.S)
        if sph:
            pts = []
            for row in re.finditer(r"\{([^}]*)\}", sph.group(1)):
                nums = [float(x) for x in re.findall(r"[-\d.eE]+", row.group(1))]
                pts.append(nums[:3])
            g["mode"] = "points"
            g["points"] = pts
            groups.append(g)
            continue

        vp = re.search(r"VERTEX_POOL\s*\{(.*?)\n\s*\}", body, re.S)
        if not vp:
            continue
        verts, norms, uvs = [], [], []
        for row in re.finditer(r"\{([^}]*)\}", vp.group(1)):
            nums = [float(x) for x in re.findall(r"[-\d.eE+]+", row.group(1))]
            k = 0
            verts.append(nums[k:k+3]); k += 3
            if has_n:
                norms.append(nums[k:k+3]); k += 3
            if has_uv:
                uvs.append(nums[k:k+2]); k += 2
        indices = []
        for cm in re.finditer(r"CONNECTION_LIST\s*\(\s*PCOUNT\s*=\s*(\d+)\s*\)\s*\{(.*?)\n\s*\}", body, re.S):
            pc = int(cm.group(1))
            for row in re.finditer(r"\{([^}]*)\}", cm.group(2)):
                idx = [int(x) for x in re.findall(r"\d+", row.group(1))]
                # fan-triangulate polygons
                for t in range(1, len(idx) - 1):
                    indices += [idx[0], idx[t], idx[t + 1]]
        g["mode"] = "mesh"
        g["v"] = verts
        g["n"] = norms if has_n else None
        g["uv"] = uvs if has_uv else None
        g["i"] = indices
        groups.append(g)
    return groups

# ---------------------------------------------------------------- VMF
def parse_vmf(path, prefix):
    text = strip_comments(open(path, "r", errors="replace").read())
    textures, materials = {}, {}
    for tm in re.finditer(r"TEXTURE\s*\(\s*NAME\s*=\s*\"?([\w]+)\"?\s*;?([^)]*)\)\s*\{(.*?)\n\}", text, re.S):
        name, params, body = tm.group(1), tm.group(2), tm.group(3)
        mmap = re.search(r'MAP\s*\{\s*"(\w+)"\s*\}', body)
        mslice = re.search(r"BITSLICE\s*\{\s*(\d+)\s*\}", body)
        scroll = re.search(r"SCROLL\s+([-\d. ]+)", params + body)
        textures[name] = {
            "map": mmap.group(1) if mmap else None,
            "slice": int(mslice.group(1)) if mslice else 0,
            "scroll": [float(x) for x in scroll.group(1).split()] if scroll else None,
        }
    for mm in re.finditer(r"MATERIAL\s*\(\s*NAME\s*=\s*\"?([\w]+)\"?\s*;?([^)]*)\)\s*\{(.*?)\n\}", text, re.S):
        name, params, body = mm.group(1), mm.group(2), mm.group(3)
        def col(tag):
            m = re.search(tag + r"\s*\{([^}]*)\}", body)
            return [float(x) for x in re.findall(r"[-\d.eE]+", m.group(1))][:3] if m else None
        mtex = re.search(r'TEXTURE\s*\{\s*"(\w+)"\s*\}', body)
        materials[prefix + ":" + name] = {
            "ambient": col("AMBIENT"), "diffuse": col("DIFFUSE"),
            "emissive": col("EMISSIVE"),
            "texture": mtex.group(1) if mtex else None,
            "immune": "IMMUNE" in params,  # fog-immune (self-lit stars, running lights)
        }
    return textures, materials

# ---------------------------------------------------------------- TGA -> PNG data URI
def tga_to_png_datauri(path):
    d = open(path, "rb").read()
    idlen, cmap, imgtype = d[0], d[1], d[2]
    w, h = struct.unpack("<HH", d[12:16])
    bpp, desc = d[16], d[17]
    assert imgtype == 2 and bpp in (24, 32), f"unsupported TGA {path} type={imgtype} bpp={bpp}"
    off = 18 + idlen
    nb = bpp // 8
    rows = []
    for y in range(h):
        row = bytearray()
        base = off + y * w * nb
        for x in range(w):
            b, g, r = d[base + x*nb], d[base + x*nb + 1], d[base + x*nb + 2]
            row += bytes((r, g, b))
        rows.append(bytes(row))
    if not (desc & 0x20):  # bottom-up -> flip to top-down
        rows.reverse()
    raw = b"".join(b"\x00" + r for r in rows)
    def chunk(tag, data):
        c = struct.pack(">I", len(data)) + tag + data
        return c + struct.pack(">I", zlib.crc32(tag + data) & 0xffffffff)
    png = (b"\x89PNG\r\n\x1a\n"
           + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
           + chunk(b"IDAT", zlib.compress(raw, 9))
           + chunk(b"IEND", b""))
    return "data:image/png;base64," + base64.b64encode(png).decode()

# ---------------------------------------------------------------- SPL
def parse_spl(path):
    lines = [l for l in open(path).read().splitlines() if l.strip()]
    n = int(lines[0].split()[0])
    pts = []
    for l in lines[1:]:
        try:
            nums = [float(x) for x in l.split()]
        except ValueError:
            continue
        if len(nums) >= 3:
            pts.append(nums[:3])
    return pts

# ---------------------------------------------------------------- SCN
def parse_scn(path):
    scene = {"lights": [], "statics": [], "dynamics": []}
    for raw in open(path, errors="replace"):
        line = raw.split("//")[0].split("#")[0].strip()
        if not line: continue
        t = line.split()
        k = t[0].upper()
        try:
            if k == "VIEWANGLE": scene["fov"] = float(t[1])
            elif k == "CLIP": scene["clip"] = [float(t[1]), float(t[2])]
            elif k == "FOG": scene["fog"] = [float(x) for x in t[1:6]]
            elif k == "BACKGND": scene["bg"] = [float(x) for x in t[1:4]]
            elif k == "AMBIENT": scene["ambient"] = [float(x) for x in t[1:4]]
            elif k == "LIGHT": scene["lights"].append([float(x) for x in t[1:7]])
            elif k == "START": scene["start"] = [float(x) for x in t[1:7]]
            elif k == "STATIC":
                scene["statics"].append({"geo": t[1].lower(), "scale": float(t[2]),
                    "pos": [float(x) for x in t[3:6]], "rot": [float(x) for x in t[6:9]]})
            elif k == "DYNAMIC":
                scene["dynamics"].append({"geo": t[1].lower(), "scale": float(t[2]),
                    "spl": os.path.basename(t[3].replace("\\", "/")).lower(),
                    "phase": float(t[4]), "speed": float(t[5])})
        except (ValueError, IndexError):
            pass
    return scene

# ---------------------------------------------------------------- drive it
def main():
    out = {"models": {}, "materials": {}, "textures": {}, "images": {},
           "splines": {}, "scenes": {}}

    tex1, mat1 = parse_vmf(os.path.join(BUILD, "STSHIPS.VMF"), "stships")
    tex2, mat2 = parse_vmf(os.path.join(BUILD, "STAR.VMF"), "star")
    out["textures"] = {**tex1, **tex2}
    out["materials"] = {**mat1, **mat2}

    for name, f in [("tesall", "TESALL.VGF"), ("tesall_lp", "TESALL_L.VGF"),
                    ("klngn", "KLNGN.VGF"), ("klngn_lp", "KLNGN_LP.VGF"),
                    ("stars", "STARS.VGF")]:
        gs = parse_vgf(os.path.join(BUILD, f))
        out["models"][name] = gs
        nv = sum(len(g.get("v", g.get("points", []))) for g in gs)
        nt = sum(len(g.get("i", [])) // 3 for g in gs)
        print(f"model {name}: {len(gs)} groups, {nv} verts, {nt} tris")

    # texture maps: map base 'steh'/'klng', slice n -> BUILD/<BASE><n+1>.TGA
    used_maps = {(t["map"], t["slice"]) for t in out["textures"].values() if t["map"]}
    for base, sl in sorted(used_maps):
        f = os.path.join(BUILD, f"{base.upper()}{sl+1}.TGA")
        if os.path.exists(f):
            out["images"][f"{base}{sl}"] = tga_to_png_datauri(f)
            print(f"texture {base} slice {sl}: {os.path.basename(f)} ok")
        else:
            print(f"texture MISSING: {f}")

    for i in range(1, 7):
        out["splines"][f"field{i}.spl"] = parse_spl(os.path.join(ROOT, f"FIELD{i}.SPL"))
    for f in ["KLNGN2.SPL", "KLNGN3.SPL"]:
        out["splines"][f.lower()] = parse_spl(os.path.join(ROOT, f))

    out["scenes"]["TREK.SCN"] = parse_scn(os.path.join(ROOT, "TREK.SCN"))
    out["scenes"]["KLNGVID.SCN"] = parse_scn(os.path.join(ROOT, "KLNGVID.SCN"))

    dst = os.path.join(os.path.dirname(os.path.abspath(__file__)), "trekdata.json")
    with open(dst, "w") as fh:
        json.dump(out, fh, separators=(",", ":"))
    print("wrote", dst, f"{os.path.getsize(dst)/1e6:.2f} MB")

if __name__ == "__main__":
    main()
