"""Parsers for Division Ltd. dVS/DPL file formats (1994-1996), reverse-engineered
from the VWE 'Glaze' drive. Binary BIZ2 layout comes from Division's own reader
source (DPL3/BIZREAD.C, PJA 1994); text formats from surviving examples.

Formats:
  .VGF/.VMF  DIV-VIZ2 text geometry / materials
  .BGF/.BMF  DIV-BIZ2 binary geometry / materials (b2z block stream)
  .SVT       raw square texture, 4 B/texel, size implies 64/128/256 edge
  .TGA       type-2 uncompressed 24/32-bit
  .SPL       text spline paths
  .SCN       text scene scripts
"""
import os, re, struct, zlib, base64

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

def png_datauri(w, h, rgb_rows):
    raw = b"".join(b"\x00" + r for r in rgb_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()

def r3(x): return round(x, 3)
def r4(x): return round(x, 4)

# ================================================================ VGF (text geometry)
def parse_vgf(path):
    text = strip_comments(open(path, "r", errors="replace").read())
    scale = 1.0
    hm = re.search(r"HEADER\s*\((.*?)\)", text, re.S)
    if hm:
        sm = re.search(r"SCALE\s*=\s*([\d.eE+-]+)", hm.group(1))
        if sm: scale = float(sm.group(1))
    groups = []
    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 or "3D_TEXTURE" in flags
        depth, i = 1, gm.end()
        while depth > 0 and i < len(text):
            if text[i] == "{": depth += 1
            elif text[i] == "}": depth -= 1
            i += 1
        body = text[gm.end():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([r3(v * scale) for v in 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([r3(v * scale) for v in nums[k:k+3]]); k += 3
            if has_n: norms.append([r3(v) for v in nums[k:k+3]]); k += 3
            if has_uv: uvs.append([r4(v) for v in 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*\}|(?=CONNECTION_LIST))", body, re.S):
            for row in re.finditer(r"\{([^}]*)\}", cm.group(1)):
                idx = [int(x) for x in re.findall(r"\d+", row.group(1))]
                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 (text materials)
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)
        textures[name] = {"map": mmap.group(1).lower() if mmap else None,
                          "slice": int(mslice.group(1)) if mslice else None}
    ramps = {}
    for rm in re.finditer(r"RAMP\s*\(\s*NAME\s*=\s*\"?([\w-]+)\"?\s*\)\s*\{(.*?)\n\}", text, re.S):
        nums = [float(x) for x in re.findall(r"[-\d.eE+]+", rm.group(2))]
        if len(nums) >= 6: ramps[rm.group(1)] = [nums[0:3], nums[3:6]]
    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, n=3):
            m = re.search(tag + r"\s*\{([^}]*)\}", body)
            return [float(x) for x in re.findall(r"[-\d.eE+]+", m.group(1))][:n] if m else None
        mtex = re.search(r'TEXTURE\s*\{\s*"([\w-]+)"\s*\}', body)
        mramp = re.search(r"RAMP\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,
            "ramp": mramp.group(1) if mramp else None,
            "immune": "IMMUNE" in params}
    return textures, materials, ramps

# ================================================================ BIZ2 (binary container)
class B2Z:
    def __init__(self, path):
        self.d = open(path, "rb").read()
        self.p = 0
    def u8(self):  v = self.d[self.p]; self.p += 1; return v
    def u16(self): v = struct.unpack_from("<H", self.d, self.p)[0]; self.p += 2; return v
    def i32(self): v = struct.unpack_from("<i", self.d, self.p)[0]; self.p += 4; return v
    def f32(self): v = struct.unpack_from("<f", self.d, self.p)[0]; self.p += 4; return v
    def floats(self, n):
        v = struct.unpack_from("<%df" % n, self.d, self.p); self.p += 4 * n; return v
    def ints(self, n):
        v = struct.unpack_from("<%di" % n, self.d, self.p); self.p += 4 * n; return v
    def cstr(self):
        e = self.d.index(0, self.p)
        s = self.d[self.p:e].decode("latin-1"); self.p = e + 1; return s
    def block(self):
        hdr = self.u16()
        mode = (hdr >> 12) & 0xC
        if mode == 0x8: ln = self.i32()
        elif mode == 0x4: ln = self.u16()
        elif mode == 0x0: ln = self.u8()
        else: raise ValueError("bad block header 0x%x @%d" % (hdr, self.p))
        return hdr & 0x3fff, ln  # strip length-size bits, keep 0x2000 flag

VTX_LAYOUT = {  # tag: (floats_per_vertex, n_off, uv_off, uv3_off, col_off, lum_off)
    0x80: (3, -1, -1, -1, -1, -1),
    0x81: (6,  3, -1, -1, -1, -1),
    0x82: (7, -1, -1, -1,  3, -1),
    0x83: (10, 3, -1, -1,  6, -1),
    0x84: (5, -1, -1, -1, -1,  3),
    0x85: (8,  3, -1, -1, -1,  6),
    0x88: (5, -1,  3, -1, -1, -1),
    0x89: (8,  3,  6, -1, -1, -1),
    0x8a: (9, -1,  7, -1,  3, -1),
    0x8c: (7, -1,  5, -1, -1,  3),
    0x90: (6, -1, -1,  3, -1, -1),
    0x91: (9,  3, -1,  6, -1, -1),
    0x92: (10, -1, -1,  7,  3, -1),
    0x94: (8, -1, -1,  5,  3, -1),  # lum@3, 3dtex@5 per BIZREAD.C
}

def _tri_from_strip(nv):
    out = []
    for t in range(nv - 2):
        if t % 2 == 0: out += [t, t + 1, t + 2]
        else: out += [t + 1, t, t + 2]
    return out

def _read_vertex_block(bz, tag, ln):
    fpv, n_off, uv_off, uv3_off, col_off, lum_off = VTX_LAYOUT[tag]
    if tag == 0x94: fpv, lum_off, uv3_off = 8, 3, 5
    nv = (ln // 4) // fpv
    verts, norms, uvs, cols = [], [], [], []
    for _ in range(nv):
        f = bz.floats(fpv)
        verts.append([r3(f[0]), r3(f[1]), r3(f[2])])
        if n_off >= 0: norms.append([r3(f[n_off]), r3(f[n_off+1]), r3(f[n_off+2])])
        if uv_off >= 0: uvs.append([r4(f[uv_off]), r4(f[uv_off+1])])
        if uv3_off >= 0: uvs.append([r4(f[uv3_off]), r4(f[uv3_off+1])])
        if col_off >= 0: cols.append([r3(f[col_off]), r3(f[col_off+1]), r3(f[col_off+2])])
        if lum_off >= 0: cols.append([r3(f[lum_off])] * 3)
    return verts, norms or None, uvs or None, cols or None

def _parse_bgf_vertices(bz, length, geotype, out_groups, material):
    end = bz.p + length
    pend_v = pend_n = pend_uv = pend_c = None
    conns = []
    while bz.p < end:
        tag, ln = bz.block()
        nxt = bz.p + ln
        if tag == 0x47:                    # triangle connectivity
            for _ in range(ln // 12):
                conns.append(list(bz.ints(3)))
        elif tag == 0x4d:                  # polygon connectivity
            vpp = bz.u8()
            n = (ln - 1) // (vpp * 4)
            for _ in range(n):
                conns.append(list(bz.ints(vpp)))
        elif tag in VTX_LAYOUT:
            v, n, uv, c = _read_vertex_block(bz, tag, ln)
            if geotype == "pmesh":
                if pend_v is None:
                    pend_v, pend_n, pend_uv, pend_c = v, n, uv, c
                else:
                    pend_v += v
                    if pend_n and n: pend_n += n
                    if pend_uv and uv: pend_uv += uv
                    if pend_c and c: pend_c += c
            else:                          # strip / polygon: emit immediately
                idx = (_tri_from_strip(len(v)) if geotype == "tristrip"
                       else [x for t in range(1, len(v)-1) for x in (0, t, t+1)])
                out_groups.append({"material": material, "mode": "mesh",
                                   "v": v, "n": n, "uv": uv, "c": c, "i": idx})
        bz.p = nxt
    if geotype == "pmesh" and pend_v is not None:
        idx = []
        for poly in conns:
            for t in range(1, len(poly) - 1):
                idx += [poly[0], poly[t], poly[t + 1]]
        out_groups.append({"material": material, "mode": "mesh",
                           "v": pend_v, "n": pend_n, "uv": pend_uv,
                           "c": pend_c, "i": idx})

def _parse_bgf_patch(bz, length, out_groups, obj_mtl):
    end = bz.p + length
    f_mtl = obj_mtl
    pend = []   # geometry blocks seen before the material tag
    while bz.p < end:
        tag, ln = bz.block()
        nxt = bz.p + ln
        if tag == 0x2008: bz.cstr()
        elif tag in (0x2030, 0x2031):
            t = bz.u8()
            name = bz.cstr() if t == 1 else (None if t != 2 else "DEFAULT")
            if tag == 0x2030: f_mtl = name or f_mtl
        elif tag == 0x43: _parse_bgf_vertices(bz, ln, "polygon", pend, None)
        elif tag == 0x44: _parse_bgf_vertices(bz, ln, "tristrip", pend, None)
        elif tag == 0x45: _parse_bgf_vertices(bz, ln, "tristrip", pend, None)
        elif tag == 0x46: _parse_bgf_vertices(bz, ln, "pmesh", pend, None)
        bz.p = nxt
    for g in pend:
        g["material"] = f_mtl
        out_groups.append(g)

def parse_bgf(path):
    """Parse DIV-BIZ2 geometry -> groups like parse_vgf. Also returns any
    embedded materials/textures/ramps."""
    bz = B2Z(path)
    if bz.d[:8] != b"DIV-BIZ2":
        raise ValueError("not a DIV-BIZ2 file: " + path)
    bz.p = 8
    groups, materials, textures, ramps = [], {}, {}, {}
    while bz.p < len(bz.d) - 2:
        tag, ln = bz.block()
        nxt = bz.p + ln
        top = tag & 0xfff
        if top == 0x005: break              # trailer
        elif top == 0x003 or top == 0x004: pass
        elif top == 0x010: _parse_biz_texture(bz, ln, textures)
        elif top == 0x020: _parse_biz_material(bz, ln, materials)
        elif top == 0x030: _parse_biz_ramp(bz, ln, ramps)
        elif top == 0x040: _parse_bgf_object(bz, ln, groups)
        bz.p = nxt
    return groups, materials, textures, ramps

def _parse_bgf_lod(bz, length, groups, obj_mtl):
    end = bz.p + length
    while bz.p < end:
        tag, ln = bz.block()
        nxt = bz.p + ln
        if tag == 0x42: _parse_bgf_patch(bz, ln, groups, obj_mtl)
        bz.p = nxt

def _parse_bgf_object(bz, length, groups):
    end = bz.p + length
    obj_mtl = None
    lods = []   # (size, offset) of nested LOD blocks
    while bz.p < end:
        tag, ln = bz.block()
        nxt = bz.p + ln
        if tag == 0x2008: bz.cstr()
        elif tag == 0x2030:
            t = bz.u8()
            if t == 1: obj_mtl = bz.cstr()
        elif tag == 0x2031:
            t = bz.u8()
            if t == 1: bz.cstr()
        elif tag == 0x42: _parse_bgf_patch(bz, ln, groups, obj_mtl)
        elif tag == 0x41: lods.append((ln, bz.p))
        bz.p = nxt
    if lods:
        # newer writers nest patches inside LODs (Division's 1994 reader
        # predates this); take the largest LOD = highest detail
        ln, off = max(lods)
        bz.p = off
        _parse_bgf_lod(bz, ln, groups, obj_mtl)
        bz.p = end

def _parse_biz_material(bz, length, materials):
    end = bz.p + length
    m = {"ambient": None, "diffuse": None, "emissive": None,
         "texture": None, "ramp": None, "immune": False}
    name = "unnamed"
    while bz.p < end:
        tag, ln = bz.block()
        nxt = bz.p + ln
        if tag == 0x2008: name = bz.cstr()
        elif tag == 0x0021:
            mode = bz.u8()
            if mode == 2: m["texture"] = bz.cstr()
        elif tag == 0x0023: m["ambient"] = [r3(x) for x in bz.floats(3)]
        elif tag == 0x0024: m["diffuse"] = [r3(x) for x in bz.floats(3)]
        elif tag == 0x0026: m["emissive"] = [r3(x) for x in bz.floats(3)]
        elif tag == 0x0028: m["ramp"] = bz.cstr()
        bz.p = nxt
    materials[name] = m

def _parse_biz_texture(bz, length, textures):
    end = bz.p + length
    name, mapname = None, None
    while bz.p < end:
        tag, ln = bz.block()
        nxt = bz.p + ln
        if tag == 0x2008: name = bz.cstr()
        elif tag == 0x0011: mapname = bz.cstr()
        bz.p = nxt
    if name: textures[name] = {"map": (mapname or "").lower() or None, "slice": None}

def _parse_biz_ramp(bz, length, ramps):
    end = bz.p + length
    name, data = None, None
    while bz.p < end:
        tag, ln = bz.block()
        nxt = bz.p + ln
        if tag == 0x2008: name = bz.cstr()
        elif tag == 0x0031:
            f = bz.floats(6); data = [list(f[0:3]), list(f[3:6])]
        bz.p = nxt
    if name and data: ramps[name] = data

# ================================================================ textures
def tga_datauri(path):
    d = open(path, "rb").read()
    idlen, imgtype = d[0], d[2]
    w, h = struct.unpack("<HH", d[12:16])
    bpp, desc = d[16], d[17]
    if imgtype != 2 or bpp not in (24, 32): raise ValueError("TGA %s type=%d bpp=%d" % (path, imgtype, 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):
            row += bytes((d[base+x*nb+2], d[base+x*nb+1], d[base+x*nb]))
        rows.append(bytes(row))
    if not (desc & 0x20): rows.reverse()
    return png_datauri(w, h, rows)

def svt_datauri(path):
    d = open(path, "rb").read()
    edge = {16384: 64, 65536: 128, 262144: 256}.get(len(d))
    if not edge: raise ValueError("SVT %s bad size %d" % (path, len(d)))
    rows = []
    for y in range(edge):
        row = bytearray()
        base = y * edge * 4
        for x in range(edge):
            # texel layout: X R G B (first byte unused/alpha)
            row += d[base + x*4 + 1: base + x*4 + 4]
        rows.append(bytes(row))
    return png_datauri(edge, edge, rows)

# ================================================================ SPL / SCN
def parse_spl(path):
    pts = []
    for l in open(path, errors="replace").read().splitlines()[1:]:
        try: nums = [float(x) for x in l.split()]
        except ValueError: continue
        if len(nums) >= 3: pts.append([r3(v) for v in nums[:3]])
    return pts

def parse_scn(path):
    scene = {"lights": [], "placements": []}
    last = None
    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 in ("GEOMETRY", "MATERIAL", "TEXTURE"): scene[k.lower()] = t[1]
            elif k == "STATIC":
                last = {"kind": "static", "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]],
                        "children": []}
                scene["placements"].append(last)
            elif k == "DYNAMIC":
                last = {"kind": "dynamic", "geo": t[1].lower(), "scale": float(t[2]),
                        "spl": os.path.basename(t[3].replace("\\", "/")).lower(),
                        "phase": float(t[4]), "speed": float(t[5]), "children": []}
                scene["placements"].append(last)
            elif k in ("SCHILD", "DCHILD") and last is not None:
                last["children"].append({"geo": t[1].lower(), "scale": float(t[2])})
        except (ValueError, IndexError):
            pass
    return scene
