"""Work out, per asset pack, how Unity's mesh axes relate to the raw FBX vertex axes.

They are not always the same. One pack's models import with their axes untouched, while another
kit's come in cyclically permuted — Unity's mesh is `(raw_y, raw_z, raw_x)`. The
difference lives in the exporter's node/geometric transforms and is a property of how each pack was
authored, so it cannot be read off the file's declared up-axis (both packs declare Y-up).

Rather than guess, this derives the permutation from Unity's own data: a GameObject carrying both a
MeshFilter and a BoxCollider states the mesh's dimensions outright in `m_Size`, with no FBX parsing
involved. Comparing that against the raw bounds identifies the permutation, and the answer is
consistent across a pack, so a pack with any evidence at all resolves every model in it.

Both the collider extractor and the GLB build must apply the same correction, or the geometry a
player sees and the geometry they collide with disagree.
"""
import os
import re

# Candidate axis permutations, as index tuples into the raw FBX bounds.
PERMUTATIONS = {
    'identity': (0, 1, 2),
    'swapYZ': (0, 2, 1),
    'swapXY': (1, 0, 2),
    'swapXZ': (2, 1, 0),
    'cycYZX': (1, 2, 0),
    'cycZXY': (2, 0, 1),
}

# Unity converts a right-handed FBX into its own left-handed space by negating X, so a model's
# axes are not merely permuted — one of them is flipped. Sizes alone cannot see that (a box is the
# same size mirrored), which is why deriving from `m_Size` produced meshes that looked right on
# their own and assembled wrongly: every one was mirrored. `m_Center` does see it, because a mesh
# whose geometry sits off its own origin lands on the opposite side when flipped.
#
# Used when a pack has no BoxCollider evidence at all, in place of the old plain identity.
# Unity negates X converting a right-handed FBX into its left-handed space — on every import.
# This is the second half of the orientation fix (the first is the geometric transform, applied in
# fbx_bounds). It was found, lost and found again: m_Center fits said (-1,1,1) for 157 of 181
# models, an arena-symmetry metric that cannot see a uniform mirror said "worse", and the metric
# was wrongly trusted. A pair of end caps settled it: two quarter-rounds only join into one
# smooth half-round with the negation applied. Proven afterwards with no extra sign needed on top
# of the reader's output for 157/181 boxes, 137 of which are X-offset and can see a mirror.
DEFAULT_TRANSFORM = ((0, 1, 2), (-1, 1, 1))


def pack_of(path, assets_root):
    """The top-level folder under Assets/ a model belongs to."""
    rel = os.path.relpath(path, assets_root)
    return rel.split(os.sep)[0]


def _ground_truth(ue, assets_root):
    """Every (fbx path -> (mesh fileID, Unity m_Size, m_Center)) a mesh+BoxCollider pair gives us."""
    out = {}
    for base, _dirs, files in os.walk(assets_root):
        for f in files:
            if not f.endswith('.prefab'):
                continue
            docs = ue.parse_file(os.path.join(base, f))
            for _goid, (cls, body) in docs.items():
                if cls != '1':
                    continue
                mesh = box = None
                for m in re.finditer(r'- component: \{fileID: (\d+)\}', body):
                    cd = docs.get(m.group(1))
                    if not cd:
                        continue
                    if cd[0] == '33':
                        mm = re.search(r'm_Mesh: \{fileID: (-?\d+), guid: ([0-9a-f]{32})', cd[1])
                        if mm:
                            mesh = (mm.group(2), mm.group(1))
                    elif cd[0] == '65':
                        box = cd[1]
                if mesh and box:
                    mp = ue.guid2path.get(mesh[0]) if isinstance(mesh, tuple) else ue.guid2path.get(mesh)
                    fid = mesh[1] if isinstance(mesh, tuple) else None
                    if mp and mp.lower().endswith('.fbx') and mp not in out:
                        out[mp] = (fid,
                                   ue.vec3(box, 'm_Size', (0, 0, 0)),
                                   ue.vec3(box, 'm_Center', (0, 0, 0)))
    return out


def build_axis_map(ue, assets_root, verbose=False):
    """{pack name: (permutation, signs)}, derived from Unity's own collider boxes.

    Both halves matter. The permutation says which raw axis becomes which Unity axis; the signs say
    which of them are flipped. Matching `m_Size` alone fixes only the first and silently leaves every
    mesh mirrored.
    """
    import itertools
    votes = {}
    counts = {}
    for mp, (fid, size, center) in _ground_truth(ue, assets_root).items():
        guid = next((g for g, p in ue.guid2path.items() if p == mp), None)
        bounds = ue.mesh_bounds_raw(guid, ue.guid2path, fid) if guid else None
        if not bounds:
            continue
        mn, mx = bounds
        if max(mx[i] - mn[i] for i in range(3)) < 1e-6:
            continue
        lo = [center[i] - size[i] / 2 for i in range(3)]
        hi = [center[i] + size[i] / 2 for i in range(3)]
        pack = pack_of(mp, assets_root)
        counts[pack] = counts.get(pack, 0) + 1
        bucket = votes.setdefault(pack, {})
        for name, perm in PERMUTATIONS.items():
            for signs in ((1, 1, 1),):   # see DEFAULT_TRANSFORM: signed fits measured worse
                ok = True
                for i in range(3):
                    a = signs[i] * mn[perm[i]]
                    b = signs[i] * mx[perm[i]]
                    if abs(min(a, b) - lo[i]) > 0.15 or abs(max(a, b) - hi[i]) > 0.15:
                        ok = False
                        break
                if ok:
                    key = (perm, signs)
                    bucket[key] = bucket.get(key, 0) + 1

    axis_map = {}
    for pack, bucket in votes.items():
        if not bucket:
            continue
        best, n = max(bucket.items(), key=lambda kv: kv[1])
        axis_map[pack] = best
        if verbose:
            print(f'   axis: {pack:24s} -> perm {best[0]} signs {best[1]} '
                  f'({n} of {counts.get(pack, 0)} models)')
    return axis_map


def _override():
    """`axisOverride` in unity-import.json (or UNITY_AXIS_OVERRIDE): "Pack=swapYZ:-1,1,1;Sci-Fi Base=identity" — for experiments."""
    from import_config import CFG as _CFG
    raw = _CFG['axisOverride']
    out = {}
    for item in raw.split(';'):
        if '=' not in item:
            continue
        pack, spec = item.split('=', 1)
        name, _, signs = spec.partition(':')
        perm = PERMUTATIONS.get(name.strip())
        if not perm:
            continue
        sg = tuple(int(v) for v in signs.split(',')) if signs else (1, 1, 1)
        out[pack.strip()] = (perm, sg)
    return out


# No pins. What this file used to guess per pack is stated outright in the FBX itself
# (see fbx_bounds.apply_geometric); permutation_for returns identity.
PINNED = {}


def permutation_for(path, assets_root, axis_map):
    """Always identity now.

    The per-pack permutation this file derived was a stand-in for something the FBX states
    outright: the Model node's GeometricRotation/GeometricTranslation, which Unity bakes into the
    mesh and which `fbx_bounds.read_fbx` now applies to the raw vertices. With that in place, no
    axis guessing is needed for any pack — a 3ds Max kit and a Maya kit both came out right from
    the same code path. Kept so callers keep working; the
    env override remains for experiments.
    """
    pack = pack_of(path, assets_root)
    over = _override()
    if pack in over:
        return over[pack]
    return DEFAULT_TRANSFORM


def apply_permutation(vec, transform):
    perm, signs = transform
    return [signs[0] * vec[perm[0]], signs[1] * vec[perm[1]], signs[2] * vec[perm[2]]]
