"""Generate loadable NWN tile models (`twu01_<code>.mdl` + `.wok` + textures).

    python make_mdl.py [outdir]       # default: ./mdl

Pure Python, no Blender. Emits Aurora ASCII models directly from tiles.py so the
result is deterministic and testable without a headless Blender/Neverblender run.
(Neverblender is kept as an alternate path in export_mdl.py, but it flattens our
multi-material tiles to one unloadable material, so this is the shipping path.)

Each tile .mdl contains:
  * an AuroraBase dummy node named == resref (classification TILE),
  * one trimesh node PER texture used on the tile (NWN allows one bitmap/node) —
    faces triangulated, real tverts, and a `bitmap <resref>` the engine can load,
  * the aabb walkmesh node (shared with make_wok, embedded per NWN tile rules).
The standalone `.wok` (same walkmesh) is written alongside.

Textures: a <=16-char .tga per tiles.TEXTURES. Each is a procedural structural
read-out (distinct role colour + dark border + orientation pip) so geometry reads
in the toolset (KICKOFF.md: geometry not art); SRC_TEX swaps in a real stock tga
where a photographic surface helps (e.g. canal water). Real art can replace any of
these files later without touching geometry.
"""

import os
import shutil
import sys

import tiles
import make_wok

VIS_MATID = 65536   # constant last-field on visible faces (matches stock tiles)

# Real stock-NWN textures copied verbatim for surfaces where a photographic read
# beats a flat colour (user: "pull cobbles/water from refs"). Everything else is
# the procedural structural read-out below. Missing source -> procedural fallback.
REFS = "/home/vicky/Projects/nwnee-refs"
SRC_TEX = {
    "MAT_canal_black_water": f"{REFS}/aurora_tin.bif/tin01_water01.tga",   # animated-look water
}

TEX_N = 64          # texture size (power of 2)
_BORDER = 3         # dark inset ring — makes within-tile paint seams read
_PIP = 9            # bright corner pip — reveals tile ORIENTATION (debug rotations)


def write_tga(path, rgb):
    """64x64 24-bit TGA: base `rgb` (0..1), a dark inset border, and a white
    corner pip. Not art — a structural read-out: the pip exposes each tile's
    orientation (so the paint auto-picker's rotations are visible) and the border
    outlines internal texture edges. TGA origin is lower-left (descriptor 0), so
    the pip sits at the tile's south-west (x=-5, y=-5) corner."""
    r, g, b = (max(0, min(255, round(c * 255))) for c in rgb)
    dr, dg, db = (v * 35 // 100 for v in (r, g, b))     # 0.35x -> border
    n = TEX_N
    body = bytearray()
    for j in range(n):
        for i in range(n):
            if i < _PIP and j < _PIP:
                body += bytes((255, 255, 255))                          # pip
            elif i < _BORDER or i >= n - _BORDER or j < _BORDER or j >= n - _BORDER:
                body += bytes((db, dg, dr))                             # border (BGR)
            else:
                body += bytes((b, g, r))                                # base (BGR)
    assert len(body) == n * n * 3
    header = bytes([0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                    n & 0xFF, n >> 8, n & 0xFF, n >> 8, 24, 0])
    with open(path, "wb") as fh:
        fh.write(header + bytes(body))


def _trimesh(ref, idx, tex, verts, quads):
    """One trimesh node: local verts + planar tverts + triangulated faces."""
    local, lmap, tvs = [], {}, []

    def li(gi):
        if gi not in lmap:
            x, y, z = verts[gi]
            lmap[gi] = len(local)
            local.append((x, y, z))
            tvs.append(((x + tiles.HALF) / (2 * tiles.HALF),
                        (y + tiles.HALF) / (2 * tiles.HALF)))
        return lmap[gi]

    tris = []
    for a, b, c, d in quads:
        la, lb, lc, ld = li(a), li(b), li(c), li(d)
        tris.append((la, lb, lc))
        tris.append((la, lc, ld))

    L = [f"node trimesh {ref}_{idx:02d}",
         f"  parent {ref}",
         "  position  0.00000  0.00000  0.00000",
         "  orientation  0.00000  0.00000  0.00000  0.00000",
         "  wirecolor 1.0 1.0 1.0",
         "  ambient 1.0 1.0 1.0",
         "  diffuse 1.0 1.0 1.0",
         "  specular 0.0 0.0 0.0",
         "  shininess 1",
         f"  bitmap {tex}",
         "  render 1",
         "  shadow 0",
         "  tilefade 0",
         "  rotatetexture 0",
         f"  verts {len(local)}"]
    L += [f"    {v[0]:.5f} {v[1]:.5f} {v[2]:.5f}" for v in local]
    L.append(f"  tverts {len(tvs)}")
    L += [f"    {u:.6f} {v:.6f} 0" for u, v in tvs]
    L.append(f"  faces {len(tris)}")
    # face: v1 v2 v3 smoothgroup tv1 tv2 tv3 matid  (tvert idx == vert idx here)
    L += [f"    {a} {b} {c} 1 {a} {b} {c} {VIS_MATID}" for a, b, c in tris]
    L.append("endnode")
    return L


def emit_mdl(spec):
    ref = tiles.resref(spec)
    verts, faces = tiles.vis_geometry(spec)

    # group quads by texture, preserving first-seen order for stable node numbers
    groups, order = {}, []
    for a, b, c, d, mat in faces:
        tex = tiles.texname(mat)
        if tex not in groups:
            groups[tex] = []
            order.append(tex)
        groups[tex].append((a, b, c, d))

    L = [f"# {ref} - Westgate Urban Foundation MVP tile (ASCII, generated)",
         f"newmodel {ref}",
         f"setsupermodel {ref} null",
         "classification TILE",
         "setanimationscale 1.0",
         f"beginmodelgeom {ref}",
         f"node dummy {ref}",
         "  parent null",
         "endnode"]
    for idx, tex in enumerate(order, start=1):
        L += _trimesh(ref, idx, tex, verts, groups[tex])
    _, aabb = make_wok.aabb_node(spec)
    L += aabb
    L += [f"endmodelgeom {ref}", f"donemodel {ref}"]
    return ref, "\n".join(L) + "\n", set(order)


def main():
    outdir = sys.argv[1] if len(sys.argv) > 1 else "mdl"
    os.makedirs(outdir, exist_ok=True)

    used = set()
    for spec in tiles.TILES:
        ref, mdl, texs = emit_mdl(spec)
        used |= texs
        with open(os.path.join(outdir, ref + ".mdl"), "w") as fh:
            fh.write(mdl)
        _, wok = make_wok.emit(spec)
        with open(os.path.join(outdir, ref + ".wok"), "w") as fh:
            fh.write(wok)

    # write every texture (not just `used`) so nothing dangles if a tile changes;
    # copy a real stock tga where SRC_TEX has one, else the procedural read-out.
    for mat, tex in tiles.TEXTURES.items():
        dst = os.path.join(outdir, tex + ".tga")
        src = SRC_TEX.get(mat)
        if src and os.path.exists(src):
            shutil.copyfile(src, dst)
        else:
            write_tga(dst, tiles.COLORS[mat])

    print(f"wrote {len(tiles.TILES)} .mdl + .wok and "
          f"{len(tiles.TEXTURES)} .tga to {outdir}/")


if __name__ == "__main__":
    main()
