# SKCombat props and tiles rebuilt the way the game placed them: one VTT model per Unity prefab (Tiles(Atrezzos) →
# *-props, Tiles(Platforms) → *-tiles), each mesh piece with its transform from the prefab root (the square's anchor:
# its middle, at floor level), plus the prefab's size in squares. Names already in use (the first import, one model
# per FBX file) are kept where a prefab is that same single mesh, so saved maps keep their props.
# Run: python -I prefab_jobs.py <Unity project dir> <old jobs.json> <out prefab_jobs.json>
import os, re, sys, json, math
root, old_jobs_path, out_path = sys.argv[1:4]
guid = {}
for d, _, fs in os.walk(os.path.join(root, 'Assets')):
    for f in fs:
        if f.endswith('.meta'):
            t = open(os.path.join(d, f), encoding='utf-8', errors='ignore').read(3000)
            m = re.search(r'guid: ([0-9a-f]{32})', t)
            if m: guid[m.group(1)] = os.path.join(d, f[:-5])
FBX_ROOT = os.path.join(root, 'Assets', 'FBXs')
def docs(path):
    out = {}
    for part in re.split(r'^--- ', open(path, encoding='utf-8', errors='ignore').read(), flags=re.M)[1:]:
        h = re.match(r'!u!(\d+) &(\d+)', part)
        if h: out[h.group(2)] = (h.group(1), part)
    return out
def nums(s, key, n):
    m = re.search(key + r': \{' + ', '.join(c + r': ([-\d.e]+)' for c in 'xyzw'[:n]), s)
    return [float(v) for v in m.groups()]
def trs(body):
    """A Unity Transform as a 4×4 matrix (rows), Unity space."""
    x, y, z, w = nums(body, 'm_LocalRotation', 4)
    px, py, pz = nums(body, 'm_LocalPosition', 3)
    sx, sy, sz = nums(body, 'm_LocalScale', 3)
    r = [[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
         [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
         [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)]]
    return [[r[i][0] * sx, r[i][1] * sy, r[i][2] * sz, [px, py, pz][i]] for i in range(3)] + [[0, 0, 0, 1]]
def mul(a, b):
    return [[sum(a[i][k] * b[k][j] for k in range(4)) for j in range(4)] for i in range(4)]
S = [[-1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]  # Unity mirrors the FBX's x
def recycle(fbx):
    t = open(fbx + '.meta', encoding='utf-8', errors='ignore').read()
    return dict(re.findall(r'(\d+): (\S+)', t.split('fileIDToRecycleName:')[1].split('materials:')[0])) if 'fileIDToRecycleName:' in t else {}
def slug(s):
    s = re.sub(r'([a-z])([A-Z])', r'\1 \2', s)
    s = re.sub(r'[^A-Za-z0-9]+', '-', s).strip('-').lower()
    return s or 'item'

old = json.load(open(old_jobs_path))
old_by_fbx = {}
for j in old:
    srcs = j.get('srcs') or [j['src']]
    if len(srcs) == 1:
        old_by_fbx.setdefault(os.path.basename(srcs[0]), []).append(j)
# Unity material → our texture sheet and how it's drawn (None = the piece is dropped: the floor top that showed the
# map image in the game, and particle effects; the VTT's board shows through instead)
MATS = {'Basic.mat': ('basic', 'opaque'), '[01]AncientGreeceI.mat': ('greece', 'opaque'), '[03]NatureI.mat': ('nature', 'opaque'),
        '[00]SkAlpha.mat': ('alpha', 'alpha'), '[00]SkSelfIllum.mat': ('glow', 'glow'), '[00]SkWater.mat': ('water', 'water'),
        'MatProjectorMap.mat': None, '[02]FxEffectsI.mat': None, '[02]FxEffectsI(Add).mat': None, '[02]FxEffectsI(Mult).mat': None}
SETS = [('Tiles(Atrezzos)/[00]Corpses', 'dungeon-props', None), ('Tiles(Atrezzos)/[00]Free', 'dungeon-props', None), ('Tiles(Atrezzos)/[01]AncientGreeceI', 'greece-props', None), ('Tiles(Atrezzos)/[03]NatureI', 'nature-props', None),
        ('Tiles(Platforms)/[00]Free', 'dungeon-tiles', -1.0), ('Tiles(Platforms)/[01]AncientGreeceI', 'greece-tiles', -1.0), ('Tiles(Platforms)/[03]NatureI', 'nature-tiles', -1.0),
        ('Stuff(Tiles)/Misc', 'nature-props', None)]  # grass and flowers: laid on a tile in the game; props here (they sit on painted ground)
jobs, claimed, skipped = [], set(), []
for folder, pack, cut in SETS:
    used = set()
    base = os.path.join(root, 'Assets', 'References', *folder.split('/'))
    for d, _, fs in sorted(os.walk(base)):
        if 'OLD' in d: continue
        for f in sorted(fs):
            if not f.endswith('.prefab') or (folder.startswith('Stuff') and not re.search(r'Grass|Flowers', f)): continue
            path = os.path.join(d, f)
            text = open(path, encoding='utf-8', errors='ignore').read()
            D = docs(path)
            byfid = {fid: body for fid, (typ, body) in D.items() if typ == '4'}
            tf_of_go = {re.search(r'm_GameObject: \{fileID: (\d+)\}', body).group(1): fid for fid, body in byfid.items()}
            def to_root(fid):
                m = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
                while fid in byfid:
                    body = byfid[fid]
                    father = re.search(r'm_Father: \{fileID: (\d+)\}', body).group(1)
                    if father == '0': break  # the root = the square's anchor
                    m = mul(trs(body), m)
                    fid = father
                return m
            def hidden(go):
                """Is this GameObject or one of its parents switched off?"""
                while go in D:
                    act = re.search(r'm_IsActive: (\d)', D[go][1])
                    if act and act.group(1) == '0': return True
                    fid = tf_of_go.get(go)
                    father = re.search(r'm_Father: \{fileID: (\d+)\}', byfid[fid]).group(1) if fid in byfid else '0'
                    if father == '0': return False
                    go = re.search(r'm_GameObject: \{fileID: (\d+)\}', byfid[father]).group(1) if father in byfid else ''
                return False
            renderers = {}
            rmat = {}  # GameObject → its renderer's first material (file name)
            for fid, (typ, body) in D.items():
                if typ == '23':
                    go = re.search(r'm_GameObject: \{fileID: (\d+)\}', body).group(1)
                    en = re.search(r'm_Enabled: (\d)', body)
                    renderers[go] = en is None or en.group(1) == '1'
                    g = re.search(r'm_Materials:\s*- \{fileID: \d+, guid: ([0-9a-f]{32})', body)
                    rmat[go] = os.path.basename(guid.get(g.group(1), '')) if g else ''
            meshes = []
            for fid, (typ, body) in D.items():
                if typ != '33': continue
                mm = re.search(r'm_Mesh: \{fileID: (\d+), guid: ([0-9a-f]{32})', body)
                if not mm or mm.group(2) not in guid: continue
                fbx = guid[mm.group(2)]
                if not fbx.lower().endswith('.fbx'): continue
                go = re.search(r'm_GameObject: \{fileID: (\d+)\}', body).group(1)
                gname = re.search(r'm_Name: (.*)', D[go][1]).group(1).strip() if go in D else ''
                active = re.search(r'm_IsActive: (\d)', D[go][1]) if go in D else None
                if hidden(go) or not renderers.get(go, False): continue  # the fog-of-war twin (under an inactive "MeshFoW"), switched-off parts
                m = mul(S, mul(to_root(tf_of_go[go]), S)) if go in tf_of_go else S
                rel = os.path.relpath(fbx, FBX_ROOT).replace(os.sep, '/')
                mat = rmat.get(go, '')
                if mat in MATS and MATS[mat] is None: continue
                if mat not in MATS:  # a material missing from the backup: the pack's own sheet, or water for water pieces
                    tex, kind = ('water', 'water') if 'Water' in os.path.basename(fbx) else ({'greece': 'greece', 'nature': 'nature'}.get(pack.split('-')[0], 'basic'), 'opaque')
                else:
                    tex, kind = MATS[mat]
                meshes.append({'fbx': rel, 'mesh': recycle(fbx).get(mm.group(1), ''), 'm': [[round(v, 5) for v in row] for row in m], 'tex': tex, 'kind': kind})
            if not meshes:
                skipped.append(f); continue
            size = re.search(r'mySize: \{x: ([\d.]+), y: ([\d.]+)\}', text)
            size = max(1, int(round(float(size.group(1))))) if size else 1
            title = re.search(r'myName: (.*)', text)
            title = title.group(1).strip() if title else ''
            if not title:  # no in-game name: the prefab's file name without its catalogue number ("Z03040-WallTorch")
                title = re.sub(r'^[A-Z]\d+-', '', f[:-len('.prefab')])
            # a name: the first import's, when this prefab is that FBX's single mesh; else from the prefab's own name
            name = None
            if len(meshes) == 1:
                fb = os.path.basename(meshes[0]['fbx'])
                n_in_fbx = sum(1 for k in recycle(os.path.join(FBX_ROOT, meshes[0]['fbx'])) if k.startswith('43'))
                for oj in old_by_fbx.get(fb, []):
                    if oj['pack'] == pack and n_in_fbx == 1 and (pack, oj['name']) not in claimed:
                        name = oj['name']; break
            if not name:
                # the prefab's file name is the game's own id ("Z01061-CorpseB"; its myName labels are not always right)
                stem = re.sub(r'^[A-Z]\d+-', '', f[:-len('.prefab')])
                stem = re.sub(r'^Gen_', '', stem)
                stem = re.sub(r'^HumF00_', 'Woman', stem)
                stem = re.sub(r'^HumM00_', 'Man', stem)
                name = slug(stem)
                k = 2
                base_name = name
                while (pack, name) in claimed or name in used:
                    name = base_name + '-' + str(k); k += 1
            claimed.add((pack, name)); used.add(name)
            # add-ons: pieces the game stacked on another prop (a body on the pyre, a load on the cart…)
            addon = bool(re.search(r'content|TableLantern', f, re.I))
            jobs.append({'pack': pack, 'name': name, 'title': title, 'prefab': f, 'size': size, 'cut': cut, 'meshes': meshes, 'addon': addon})
json.dump(jobs, open(out_path, 'w'), indent=1)
kept = sum(1 for j in jobs if any(o['name'] == j['name'] and o['pack'] == j['pack'] for o in old))
print(len(jobs), 'models;', kept, 'keep their old name; skipped (no mesh):', len(skipped))
olds = {(o['pack'], o['name']) for o in old if o['pack'].endswith(('-props', '-tiles'))}
print('old names not rebuilt (kept only for saved maps):', sorted(n for p, n in olds - claimed))
