"""Convert the original Unity level and ship models into GLBs the engine can load.

    python3 tools/convert_assets.py [prefab name ...]

For each prefab: walk it for renderers (unity_renderers), resolve every material to its real
texture files (unity_assets), then hand the lot to Blender, which imports the source FBX models
and writes one GLB per prefab into build/glb/.
"""
import json
import os
import re
import subprocess
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from import_config import CFG as _CFG, unity_root as _unity_root  # noqa: E402
UNITY = _unity_root()
ASSETS = os.path.join(UNITY, 'Assets')
BLENDER = '/Applications/Blender.app/Contents/MacOS/Blender'
HERE = os.path.dirname(os.path.abspath(__file__))
PROJECT = os.path.dirname(HERE)
OUT_DIR = os.path.join(PROJECT, 'build', 'glb')
SPEC_DIR = os.path.join(PROJECT, 'build', 'specs')

# unity_renderers resolves the Unity project root from argv[1] at import time; take a copy of the
# real arguments first, since that import consumes them.
_ARGS = sys.argv[1:]
sys.argv = [sys.argv[0], os.path.join(ASSETS, 'Scenes')]
from fbx_axis import build_axis_map, permutation_for  # noqa: E402
from fbx_bounds import read_fbx  # noqa: E402
from unity_assets import build_guid_index, parse_material  # noqa: E402
from unity_renderers import extract  # noqa: E402
import unity_extract as _ue  # noqa: E402

# Prefabs to convert when not run in scene mode (`prefabs` in unity-import.json). Marker-only
# prefabs (spawners, force walls) are Unity primitives with no
# model of their own, so they stay procedural.
PREFABS = list(_CFG['prefabs'])


# The arena is 68 units across; a few stray prefab copies sit far outside it in the scene file
# and are never seen in play.
ARENA_LIMIT = float(_CFG['arenaLimit'])


def find_scene(name):
    path = os.path.join(ASSETS, 'Scenes', name + '.unity')
    return path if os.path.exists(path) else None


def find_prefab(name):
    for base, _dirs, files in os.walk(ASSETS):
        if name + '.prefab' in files:
            return os.path.join(base, name + '.prefab')
    return None


_meta_cache = {}


def mesh_name_for(fbx, file_id):
    """Which mesh inside a multi-mesh FBX a Unity fileID refers to.

    Unity records the mapping in the model's .meta, under either the modern
    internalIDToNameTable or the older fileIDToRecycleName.
    """
    if fbx not in _meta_cache:
        table = {}
        try:
            with open(fbx + '.meta', encoding='utf-8', errors='ignore') as fh:
                text = fh.read()
        except OSError:
            text = ''
        for m in re.finditer(r'second: (-?\d+)\s*\n\s*second: (.+)', text):
            table[m.group(1)] = m.group(2).strip()
        for m in re.finditer(r'- first:\s*\n\s*(?:\d+): (-?\d+)\s*\n\s*second: (.+)', text):
            table[m.group(1)] = m.group(2).strip()
        for m in re.finditer(r'^\s{4}(-?\d+): (.+)$', text, re.M):
            table[m.group(1)] = m.group(2).strip()
        _meta_cache[fbx] = table
    name = _meta_cache[fbx].get(str(file_id))
    if not name or name.startswith('//'):
        return None
    return name


def slug(name):
    return re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-')


def main():
    wanted = _ARGS if _ARGS else PREFABS
    os.makedirs(OUT_DIR, exist_ok=True)
    os.makedirs(SPEC_DIR, exist_ok=True)
    print('indexing Unity GUIDs...')
    guid_index = build_guid_index(ASSETS)
    print(f'  {len(guid_index)} assets')
    # Unity's mesh axes are a per-pack permutation of the raw FBX axes; derive it from Unity's own
    # BoxCollider sizes so the GLB and the extracted colliders agree.
    print('deriving FBX axis corrections...')
    axis_map = build_axis_map(_ue, ASSETS, verbose=True)

    manifest = []
    for name in wanted:
        is_scene = name.startswith('scene:')
        if is_scene:
            label = name[6:]
            path = find_scene(label)
            if not path:
                print(f'!! {label}: no scene found')
                continue
        else:
            label = name
            path = find_prefab(name)
            if not path:
                print(f'!! {name}: no prefab found')
                continue
        renderers = extract(path)
        if is_scene:
            renderers = [r for r in renderers
                         if abs(r['pos'][0]) <= ARENA_LIMIT and abs(r['pos'][1]) <= ARENA_LIMIT]
        # Layer 18 is Unity's SHADOWS layer: low-poly stand-ins the original rendered only into
        # the line-of-sight mask, never to the screen.
        visible = [r for r in renderers if r.get('layer') != 18]
        if not visible:
            print(f'-- {label}: nothing visible')
            continue

        mat_paths = sorted({m for r in visible for m in r['materials']})
        materials = []
        for mp in mat_paths:
            parsed = parse_material(mp, guid_index)
            if parsed:
                parsed['path'] = mp
                materials.append(parsed)

        for r in visible:
            r['meshName'] = mesh_name_for(r['fbx'], r['meshFileId'])

        out_glb = os.path.join(OUT_DIR, slug(label) + '.glb')
        axis = {}
        geometric = {}
        for r in visible:
            perm, signs = permutation_for(r['fbx'], ASSETS, axis_map)
            axis[r['fbx']] = {'perm': list(perm), 'signs': list(signs)}
            if r['fbx'] not in geometric:
                # The Model node's geometric transform, per mesh, in the file's own units. Unity
                # bakes this into the mesh; Blender folds it into the object matrix we discard, so
                # the GLB build has to apply it to the mesh data itself.
                try:
                    ms, _us = read_fbx(r['fbx'])
                except Exception:
                    ms = None
                geometric[r['fbx']] = [
                    ({k: list(v) for k, v in e[5].items() if k != '_model'} if (ms and e[5]) else None)
                    for e in (ms or [])
                ]
        spec = {'name': label, 'out': out_glb, 'renderers': visible, 'materials': materials,
                'axis': axis, 'geometric': geometric}
        spec_path = os.path.join(SPEC_DIR, slug(label) + '.json')
        with open(spec_path, 'w') as fh:
            json.dump(spec, fh)

        print(f'>> {label}: {len(visible)} renderers, {len(materials)} materials -> blender')
        res = subprocess.run(
            [BLENDER, '--background', '--python', os.path.join(HERE, 'blender_build.py'),
             '--', spec_path],
            capture_output=True, text=True,
        )
        tail = [ln for ln in res.stdout.splitlines()
                if ln.startswith(('PLACED', 'EXPORTED', 'FBXFAIL'))]
        for ln in tail:
            print('   ', ln)
        if not os.path.exists(out_glb):
            print('   !! no GLB produced')
            print(res.stdout[-2500:])
            print(res.stderr[-2500:])
            continue
        manifest.append({'name': label, 'glb': out_glb, 'size': os.path.getsize(out_glb)})

    print()
    total = 0
    for m in manifest:
        print(f"  {m['size']/1e6:7.2f} MB  {os.path.basename(m['glb'])}")
        total += m['size']
    print(f"  {total/1e6:7.2f} MB  TOTAL")
    with open(os.path.join(OUT_DIR, 'manifest.json'), 'w') as fh:
        json.dump(manifest, fh, indent=1)


if __name__ == '__main__':
    main()
