"""Pull the gameplay markers out of a Unity scene: spawners, bases, powerup spawn points.

Colliders come out of `unity_extract.py`; this handles the objects that carry no collider and are
identified by the MonoBehaviour script attached to them (`markerScripts` in unity-import.json —
`PowerupSpawnPoint`), plus placed prefab instances of `Base.prefab` and friends.

    python3 tools/unity_markers.py "<unity-project>/Assets/Scenes/<Scene>.unity"   # defaults to the config
"""

import json
import math
import os
import re
import sys

DOC_RE = re.compile(r'\n--- !u!(\d+) &(\d+)(?: stripped)?\n')


def find_unity_root(start):
    d = os.path.dirname(os.path.abspath(start))
    while d != os.path.dirname(d):
        if os.path.isdir(os.path.join(d, 'Assets')):
            return d
        d = os.path.dirname(d)
    raise SystemExit('could not find the Unity project root above ' + start)


def guid_map(root):
    out = {}
    for base, _dirs, files in os.walk(os.path.join(root, 'Assets')):
        for f in files:
            if not f.endswith('.meta'):
                continue
            p = os.path.join(base, f)
            try:
                t = open(p, encoding='utf-8', errors='ignore').read(400)
            except Exception:
                continue
            m = re.search(r'guid: ([0-9a-f]{32})', t)
            if m:
                out[m.group(1)] = p[:-5]
    return out


def parse(path):
    txt = open(path, encoding='utf-8', errors='ignore').read()
    parts = DOC_RE.split(txt)
    docs = {}
    i = 1
    while i + 2 <= len(parts) - 1:
        docs[parts[i + 1]] = (parts[i], parts[i + 2])
        i += 3
    return docs


def fid(body, key):
    m = re.search(re.escape(key) + r': \{fileID: (-?\d+)', body)
    return m.group(1) if m else '0'


def vec3(body, key, default=(0.0, 0.0, 0.0)):
    m = re.search(re.escape(key) + r': \{x: ([-\d.eE+]+), y: ([-\d.eE+]+), z: ([-\d.eE+]+)\}', body)
    return tuple(float(v) for v in m.groups()) if m else default


def quat(body, key):
    m = re.search(re.escape(key) + r': \{x: ([-\d.eE+]+), y: ([-\d.eE+]+), z: ([-\d.eE+]+), w: ([-\d.eE+]+)\}', body)
    return tuple(float(v) for v in m.groups()) if m else (0.0, 0.0, 0.0, 1.0)


def qmul(a, b):
    ax, ay, az, aw = a
    bx, by, bz, bw = b
    return (aw * bx + ax * bw + ay * bz - az * by,
            aw * by - ax * bz + ay * bw + az * bx,
            aw * bz + ax * by - ay * bx + az * bw,
            aw * bw - ax * bx - ay * by - az * bz)


def qrot(q, v):
    x, y, z, w = q
    vx, vy, vz = v
    tx = 2 * (y * vz - z * vy)
    ty = 2 * (z * vx - x * vz)
    tz = 2 * (x * vy - y * vx)
    return (vx + w * tx + (y * tz - z * ty),
            vy + w * ty + (z * tx - x * tz),
            vz + w * tz + (x * ty - y * tx))


def world_of(docs, tid, mods):
    """World transform of a scene transform, honouring prefab-instance overrides."""
    chain = []
    guard = 0
    while tid and tid != '0' and tid in docs and guard < 64:
        cls, body = docs[tid]
        if cls == '1001':
            # A prefab instance in the parent chain: fold in its root placement.
            chain.append(('inst', body))
            tid = fid(body, 'm_TransformParent')
        elif cls == '4':
            chain.append(('t', body))
            tid = fid(body, 'm_Father')
        else:
            break
        guard += 1

    pos = (0.0, 0.0, 0.0)
    rot = (0.0, 0.0, 0.0, 1.0)
    scale = (1.0, 1.0, 1.0)
    for kind, body in reversed(chain):
        if kind == 't':
            lp = vec3(body, 'm_LocalPosition')
            lr = quat(body, 'm_LocalRotation')
            ls = vec3(body, 'm_LocalScale', (1.0, 1.0, 1.0))
        else:
            lp, lr, ls = instance_trs(body)
        sp = (lp[0] * scale[0], lp[1] * scale[1], lp[2] * scale[2])
        rp = qrot(rot, sp)
        pos = (pos[0] + rp[0], pos[1] + rp[1], pos[2] + rp[2])
        rot = qmul(rot, lr)
        scale = (scale[0] * ls[0], scale[1] * ls[1], scale[2] * ls[2])
    _ = mods
    return pos, rot, scale


def instance_trs(body):
    """The root placement recorded in a PrefabInstance's modification list."""
    p = [0.0, 0.0, 0.0]
    r = [0.0, 0.0, 0.0, 1.0]
    s = [1.0, 1.0, 1.0]
    for m in re.finditer(r'propertyPath: m_Local(Position|Rotation|Scale)\.([xyzw])\s*\n\s*value: ([-\d.eE+]+)', body):
        what, axis, val = m.group(1), m.group(2), float(m.group(3))
        idx = 'xyzw'.index(axis)
        if what == 'Position' and idx < 3:
            p[idx] = val
        elif what == 'Rotation':
            r[idx] = val
        elif what == 'Scale' and idx < 3:
            s[idx] = val
    return tuple(p), tuple(r), tuple(s)


def euler_z(q):
    x, y, z, w = q
    return math.degrees(math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)))


from import_config import CFG as _CFG, scene_path as scene_path_from_config
SCRIPTS = tuple(_CFG['markerScripts'])

# Markers are usually placed prefab instances rather than loose scene objects, so the interesting
# values arrive as property overrides on the instance rather than as fields on a scene component.
MARKER_PREFABS = tuple(_CFG['markerPrefabs'])
OVERRIDES = ('spawnerId', 'cameraZRotation', 'cameraOffset.x', 'cameraOffset.y', 'cameraOffset.z',
             'm_Name')


def prefab_markers(docs, guids):
    out = []
    for _iid, (cls, body) in docs.items():
        if cls != '1001':
            continue
        m = re.search(r'm_SourcePrefab: \{fileID: \d+, guid: ([0-9a-f]{32})', body)
        if not m:
            continue
        src = guids.get(m.group(1))
        base = os.path.basename(src) if src else ''
        if base not in MARKER_PREFABS:
            continue

        parent = fid(body, 'm_TransformParent')
        ppos, prot, pscale = world_of(docs, parent, None) if parent != '0' else (
            (0.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0))
        lp, lr, _ls = instance_trs(body)
        sp = (lp[0] * pscale[0], lp[1] * pscale[1], lp[2] * pscale[2])
        rp = qrot(prot, sp)
        pos = (ppos[0] + rp[0], ppos[1] + rp[1], ppos[2] + rp[2])
        rot = qmul(prot, lr)

        entry = dict(kind=base.replace('.prefab', ''),
                     pos=[round(v, 3) for v in pos],
                     zrot=round(euler_z(rot), 2))
        for prop in OVERRIDES:
            mm = re.search(r'propertyPath: ' + re.escape(prop) + r'\s*\n\s*value: (.*)', body)
            if mm:
                raw = mm.group(1).strip()
                try:
                    entry[prop] = float(raw)
                except ValueError:
                    entry[prop] = raw
        out.append(entry)
    return out


def main():
    scene = sys.argv[1] if len(sys.argv) > 1 else scene_path_from_config()
    root = find_unity_root(scene)
    guids = guid_map(root)
    want = {}
    for guid, path in guids.items():
        base = os.path.basename(path)
        if base in SCRIPTS:
            want[guid] = base[:-3]

    docs = parse(scene)

    # GameObject -> its Transform, so a MonoBehaviour can be located.
    go_to_transform = {}
    for tid, (cls, body) in docs.items():
        if cls == '4':
            go_to_transform[fid(body, 'm_GameObject')] = tid

    markers = []
    for _mid, (cls, body) in docs.items():
        if cls != '114':
            continue
        m = re.search(r'm_Script: \{fileID: -?\d+, guid: ([0-9a-f]{32})', body)
        if not m or m.group(1) not in want:
            continue
        kind = want[m.group(1)]
        goid = fid(body, 'm_GameObject')
        tid = go_to_transform.get(goid)
        if not tid:
            continue
        pos, rot, _scale = world_of(docs, tid, None)
        name = ''
        gd = docs.get(goid)
        if gd:
            nm = re.search(r'm_Name: (.*)', gd[1])
            if nm:
                name = nm.group(1).strip()
        entry = dict(kind=kind, name=name,
                     pos=[round(v, 3) for v in pos],
                     zrot=round(euler_z(rot), 2))
        sid = re.search(r'\n  spawnerId: (-?\d+)', body)
        if sid:
            entry['spawnerId'] = int(sid.group(1))
        cz = re.search(r'\n  cameraZRotation: ([-\d.eE+]+)', body)
        if cz:
            entry['cameraZRotation'] = float(cz.group(1))
        markers.append(entry)

    markers.extend(prefab_markers(docs, guids))
    print(json.dumps(markers, indent=1))


if __name__ == '__main__':
    main()
