#!/usr/bin/env python3
"""Convert JME3-extracted JSON (from J3OConverter.java) to glTF 2.0 GLB with animations."""

import json
import struct
import math
import os
import sys

def quat_mult(qa, qb):
    ax, ay, az, aw = qa
    bx, by, bz, bw = qb
    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 pad4(data):
    while len(data) % 4:
        data += b'\x00'
    return data

def mat4_mult(a, b):
    r = [0]*16
    for i in range(4):
        for j in range(4):
            for k in range(4):
                r[j*4 + i] += a[k*4 + i] * b[j*4 + k]
    return r

def mat4_from_trs(t, r, s):
    x, y, z, w = r
    xx, yy, zz = x*x, y*y, z*z
    xy, xz, yz = x*y, x*z, y*z
    wx, wy, wz = w*x, w*y, w*z
    return [
        (1-2*(yy+zz))*s[0], (2*(xy+wz))*s[0],   (2*(xz-wy))*s[0],   0,
        (2*(xy-wz))*s[1],   (1-2*(xx+zz))*s[1], (2*(yz+wx))*s[1],   0,
        (2*(xz+wy))*s[2],   (2*(yz-wx))*s[2],   (1-2*(xx+yy))*s[2], 0,
        t[0], t[1], t[2], 1,
    ]

def invert_4x4_row(m):
    a,b,c,d, e,f,g,h, i,j,k,l, m_,n,o,p = m
    det = (a*(f*k*p+g*l*n+h*j*o-h*k*n-f*l*o-g*j*p)
           - b*(e*k*p+g*l*m_+h*i*o-h*k*m_-e*l*o-g*i*p)
           + c*(e*j*p+f*l*m_+h*i*n-h*j*m_-e*l*n-f*i*p)
           - d*(e*j*o+f*k*m_+g*i*n-g*j*m_-e*k*n-f*i*o))
    if abs(det) < 1e-12:
        return [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]
    idet = 1.0/det
    return [
        (f*k*p+g*l*n+h*j*o-h*k*n-f*l*o-g*j*p)*idet,
        -(b*k*p+c*l*n+d*j*o-d*k*n-b*l*o-c*j*p)*idet,
        (b*g*p+c*h*n+d*f*o-d*g*n-b*h*o-c*f*p)*idet,
        -(b*g*l+c*h*j+d*f*k-d*g*j-b*h*k-c*f*l)*idet,
        -(e*k*p+g*l*m_+h*i*o-h*k*m_-e*l*o-g*i*p)*idet,
        (a*k*p+c*l*m_+d*i*o-d*k*m_-a*l*o-c*i*p)*idet,
        -(a*g*p+c*h*m_+d*e*o-d*g*m_-a*h*o-c*e*p)*idet,
        (a*g*l+c*h*i+d*e*k-d*g*i-a*h*k-c*e*l)*idet,
        (e*j*p+f*l*m_+h*i*n-h*j*m_-e*l*n-f*i*p)*idet,
        -(a*j*p+b*l*m_+d*i*n-d*j*m_-a*l*n-b*i*p)*idet,
        (a*f*p+b*h*m_+d*e*n-d*f*m_-a*h*n-b*e*p)*idet,
        -(a*f*l+b*h*i+d*e*j-d*f*i-a*h*j-b*e*l)*idet,
        -(e*j*o+f*k*m_+g*i*n-g*j*m_-e*k*n-f*i*o)*idet,
        (a*j*o+b*k*m_+c*i*n-c*j*m_-a*k*n-b*i*o)*idet,
        -(a*f*o+b*g*m_+c*e*n-c*f*m_-a*g*n-b*e*o)*idet,
        (a*f*k+b*g*i+c*e*j-c*f*i-a*g*j-b*e*k)*idet,
    ]

def convert(json_path, output_path):
    with open(json_path) as f:
        data = json.load(f)

    geos = data.get('geometries', [])
    skels = data.get('skeletons', [])
    anims = data.get('animations', {})

    if not geos:
        print(f'  No geometry in {json_path}')
        return

    geo = geos[0]
    positions = geo['positions']
    normals = geo.get('normals', [])
    indices = geo.get('indices', [])
    texcoords = geo.get('texcoords', [])
    vertex_count = geo.get('vertexCount', len(positions) // 3)

    bone_weights = geo.get('boneWeights', [])
    bone_indices = geo.get('boneIndices', [])

    skel = skels[0] if skels else None

    nodes = []
    skin = None
    animations_out = []

    # ── Skeleton → glTF nodes ──────────────────────────────────────────────
    if skel:
        bones = skel['bones']
        roots = skel['roots']
        name_to_bone = {b['name']: b for b in bones}
        name_to_idx = {}

        # IMPORTANT: Keep the ORIGINAL bone list order (from JME3 skeleton.getBone(i)).
        # BoneTrack.getTargetBoneIndex() and the mesh JOINTS_0 data reference
        # this exact order.
        order = [b['name'] for b in bones]
        for i, name in enumerate(order):
            name_to_idx[name] = i
            b = name_to_bone[name]
            nodes.append({
                'name': name,
                'translation': b['position'],
                'rotation': b['rotation'],  # x,y,z,w
                'scale': b['scale'],
            })

        # Hierarchy (by name)
        for name in order:
            b = name_to_bone[name]
            for child_name in b['children']:
                if child_name in name_to_idx and name in name_to_idx:
                    pi = name_to_idx[name]
                    ci = name_to_idx[child_name]
                    nodes[pi].setdefault('children', []).append(ci)

        # Inverse bind matrices
        def world_transform(name, cache={}):
            if name in cache:
                return cache[name]
            b = name_to_bone[name]
            local = mat4_from_trs(b['position'], b['rotation'], b['scale'])
            # Find parent
            parent = None
            for pn, pb in name_to_bone.items():
                if name in pb['children']:
                    parent = pn
                    break
            if parent and parent in name_to_bone:
                w = mat4_mult(world_transform(parent, cache), local)
            else:
                w = local
            cache[name] = w
            return w

        ibms = []
        for name in order:
            w = world_transform(name)
            # Convert col-major to row-major, invert, back to col-major
            row = [w[i + j*4] for j in range(4) for i in range(4)]
            inv_row = invert_4x4_row(row)
            inv_col = [inv_row[j + i*4] for i in range(4) for j in range(4)]
            ibms.extend(inv_col)

        # Joint indices (identity: bone list order == glTF node order)
        joints = list(range(len(order)))
        skin = {'joints': joints}

        # Mesh node with skin
        mesh_node_idx = len(nodes)
        nodes.append({'name': 'mesh', 'mesh': 0, 'skin': 0})

        # Skeleton root node
        root_node_idx = len(nodes)
        root_children = [name_to_idx[r] for r in roots if r in name_to_idx]
        root_children.append(mesh_node_idx)
        nodes.append({'name': 'skeleton_root', 'children': root_children})

        # Scene root
        scene_node_idx = len(nodes)
        nodes.append({'name': 'scene_root', 'children': [root_node_idx]})

        # ── Animations ──────────────────────────────────────────────────────
        anim_bin = bytearray()
        anim_buf_views = []
        anim_accessors = []

        for anim_name, anim in anims.items():
            tracks = anim.get('tracks', [])
            if not tracks:
                continue

            # Collect all keyframe times
            all_times = set()
            for t in tracks:
                for tm in t.get('times', []):
                    all_times.add(round(tm, 6))
            sorted_times = sorted(all_times)
            if len(sorted_times) < 2:
                continue

            # Per-animation sampler/channel lists
            anim_samplers_this = []
            anim_channels_this = []

            # Time buffer
            time_bytes = pad4(struct.pack(f'<{len(sorted_times)}f', *sorted_times))
            time_off = len(anim_bin)
            anim_bin.extend(time_bytes)
            tv_idx = len(anim_buf_views)
            anim_buf_views.append({'byteOffset': time_off, 'byteLength': len(time_bytes)})
            t_acc = len(anim_accessors)
            anim_accessors.append({'type': 'SCALAR', 'count': len(sorted_times),
                                   'min': [sorted_times[0]], 'max': [sorted_times[-1]]})

            for track in tracks:
                bone_idx = track.get('boneIndex', 0)
                if bone_idx >= len(order):
                    continue
                node_idx = name_to_idx.get(order[bone_idx], None)
                if node_idx is None:
                    node_idx = bone_idx

                # Get bind pose for this bone (JME3 tracks are relative to bind)
                bone_name = order[bone_idx]
                bind = name_to_bone[bone_name]
                bind_pos = bind['position']
                bind_rot = bind['rotation']
                bind_scale = bind['scale']

                kf_times = list(track.get('times', []))
                kf_trans = list(track.get('translations', []))
                kf_rot = list(track.get('rotations', []))
                kf_scale = list(track.get('scales', []))

                # Combine with bind pose: final = bind * relative
                # rotation: bindRot * trackRot
                combined_rot = []
                for i in range(0, len(kf_rot), 4):
                    q = quat_mult(bind_rot, kf_rot[i:i+4])
                    combined_rot.extend(q)
                kf_rot = combined_rot

                # translation: bindPos + trackTrans
                combined_trans = []
                for i in range(0, len(kf_trans), 3):
                    combined_trans.extend([
                        bind_pos[0] + kf_trans[i],
                        bind_pos[1] + kf_trans[i+1],
                        bind_pos[2] + kf_trans[i+2],
                    ])
                kf_trans = combined_trans

                # scale: bindScale * trackScale
                combined_scale = []
                for i in range(0, len(kf_scale), 3):
                    combined_scale.extend([
                        bind_scale[0] * kf_scale[i],
                        bind_scale[1] * kf_scale[i+1],
                        bind_scale[2] * kf_scale[i+2],
                    ])
                kf_scale = combined_scale

                def lerp_to(times, values, val_size, targets):
                    if not values:
                        return []
                    result = []
                    n_kf = len(times)
                    for qt in targets:
                        if qt <= times[0]:
                            result.extend(values[0:val_size])
                        elif qt >= times[-1]:
                            result.extend(values[(n_kf-1)*val_size:(n_kf-1)*val_size+val_size])
                        else:
                            for i in range(n_kf - 1):
                                if times[i] <= qt <= times[i+1]:
                                    a = (qt - times[i]) / (times[i+1] - times[i]) if times[i+1] != times[i] else 0
                                    base_i = i * val_size
                                    base_j = (i+1) * val_size
                                    result.extend([
                                        values[base_i + j] + a * (values[base_j + j] - values[base_i + j])
                                        for j in range(val_size)
                                    ])
                                    break
                            else:
                                result.extend(values[(n_kf-1)*val_size:(n_kf-1)*val_size+val_size])
                    return result

                trans_interp = lerp_to(kf_times, kf_trans, 3, sorted_times) if kf_trans else []
                rot_interp = lerp_to(kf_times, kf_rot, 4, sorted_times) if kf_rot else []
                scale_interp = lerp_to(kf_times, kf_scale, 3, sorted_times) if kf_scale else []

                for vals, count, acc_type, path in [
                    (trans_interp, 3, 'VEC3', 'translation'),
                    (rot_interp, 4, 'VEC4', 'rotation'),
                    (scale_interp, 3, 'VEC3', 'scale'),
                ]:
                    if not vals:
                        continue
                    raw = pad4(struct.pack(f'<{len(vals)}f', *vals))
                    off2 = len(anim_bin)
                    anim_bin.extend(raw)
                    dv_idx = len(anim_buf_views)
                    anim_buf_views.append({'byteOffset': off2, 'byteLength': len(raw)})
                    acc_idx = len(anim_accessors)
                    anim_accessors.append({'type': acc_type, 'count': len(sorted_times)})
                    samp_idx = len(anim_samplers_this)
                    anim_samplers_this.append({'input': t_acc, 'output': acc_idx})
                    anim_channels_this.append({'sampler': samp_idx,
                                               'target': {'node': node_idx, 'path': path}})

            if anim_samplers_this:
                animations_out.append({
                    'name': anim_name,
                    'samplers': anim_samplers_this,
                    'channels': anim_channels_this,
                })
    else:
        # No skeleton - simple mesh node
        mesh_node_idx = len(nodes)
        nodes.append({'name': 'mesh', 'mesh': 0})
        scene_node_idx = len(nodes)
        nodes.append({'name': 'scene_root', 'children': [mesh_node_idx]})
        anim_bin = bytearray()
        anim_buf_views = []
        anim_accessors = []
        anim_samplers = []
        anim_channels = []

    # ── Build binary buffer ─────────────────────────────────────────────────
    pos_bytes = pad4(struct.pack(f'<{len(positions)}f', *positions))
    norm_bytes = pad4(struct.pack(f'<{len(normals)}f', *normals))
    idx_bytes = pad4(struct.pack(f'<{len(indices)}H', *indices))
    uv_bytes = pad4(struct.pack(f'<{len(texcoords)}f', *texcoords)) if texcoords else b''

    joint_bytes = b''
    weight_bytes = b''
    if skel and bone_weights:
        # Joints: use existing bone_indices if available
        joint_vals = [i & 0xFFFF for i in bone_indices] if bone_indices else [0] * (vertex_count * 4)
        joint_bytes = pad4(struct.pack(f'<{len(joint_vals)}H', *joint_vals))
        weight_bytes = pad4(struct.pack(f'<{len(bone_weights)}f', *bone_weights))

    ibm_bytes = pad4(struct.pack(f'<{len(ibms)}f', *ibms)) if skel else b''

    all_bin = pos_bytes + norm_bytes + idx_bytes + uv_bytes + joint_bytes + weight_bytes + ibm_bytes + bytes(anim_bin)

    # ── Buffer views and accessors ──────────────────────────────────────────
    off = 0
    buffer_views = []
    bv_pos = {'buffer': 0, 'byteOffset': off, 'byteLength': len(pos_bytes), 'target': 34962}; off += len(pos_bytes)
    bv_nrm = {'buffer': 0, 'byteOffset': off, 'byteLength': len(norm_bytes), 'target': 34962}; off += len(norm_bytes)
    bv_idx = {'buffer': 0, 'byteOffset': off, 'byteLength': len(idx_bytes), 'target': 34963}; off += len(idx_bytes)
    buffer_views = [bv_pos, bv_nrm, bv_idx]

    accessors = [
        {'bufferView': 0, 'componentType': 5126, 'count': vertex_count, 'type': 'VEC3'},
        {'bufferView': 1, 'componentType': 5126, 'count': vertex_count, 'type': 'VEC3'},
        {'bufferView': 2, 'componentType': 5123, 'count': len(indices), 'type': 'SCALAR'},
    ]

    attributes = {'POSITION': 0, 'NORMAL': 1}

    if texcoords:
        bv_uv = {'buffer': 0, 'byteOffset': off, 'byteLength': len(uv_bytes), 'target': 34962}; off += len(uv_bytes)
        buffer_views.append(bv_uv)
        accessors.append({'bufferView': 3, 'componentType': 5126, 'count': vertex_count, 'type': 'VEC2'})
        attributes['TEXCOORD_0'] = 3

    # Joint/weight accessor indices shift if UV present
    jnt_acc = 4 if texcoords else 3
    wgt_acc = 5 if texcoords else 4

    if skel and bone_weights:
        bv_jnt = {'buffer': 0, 'byteOffset': off, 'byteLength': len(joint_bytes), 'target': 34962}; off += len(joint_bytes)
        bv_wgt = {'buffer': 0, 'byteOffset': off, 'byteLength': len(weight_bytes), 'target': 34962}; off += len(weight_bytes)
        buffer_views.extend([bv_jnt, bv_wgt])
        accessors.append({'bufferView': 3 if not texcoords else 4, 'componentType': 5123, 'count': vertex_count, 'type': 'VEC4'})
        accessors.append({'bufferView': 4 if not texcoords else 5, 'componentType': 5126, 'count': vertex_count, 'type': 'VEC4'})
        attributes['JOINTS_0'] = jnt_acc
        attributes['WEIGHTS_0'] = wgt_acc

    ibm_accessor_idx = None
    if skel:
        bv_ibm = {'buffer': 0, 'byteOffset': off, 'byteLength': len(ibm_bytes)}; off += len(ibm_bytes)
        buffer_views.append(bv_ibm)
        ibm_accessor_idx = len(accessors)
        accessors.append({'bufferView': len(buffer_views)-1, 'componentType': 5126,
                          'count': len(skin['joints']), 'type': 'MAT4'})
        skin['inverseBindMatrices'] = ibm_accessor_idx

    anim_base = off
    for bv in anim_buf_views:
        buffer_views.append({'buffer': 0, 'byteOffset': anim_base + bv['byteOffset'], 'byteLength': bv['byteLength']})

    acc_offset = len(accessors)
    for acc in anim_accessors:
        a = {'componentType': 5126, 'type': acc['type'], 'count': acc['count'],
             'bufferView': len(buffer_views) - len(anim_accessors) + 0}
        accessors.append(a)

    # Fix animation accessor bufferView indices
    for i, acc in enumerate(anim_accessors):
        accessors[acc_offset + i]['bufferView'] = len(buffer_views) - len(anim_accessors) + i

    # Fix sampler input/output indices (they reference anim_accessors relative)
    for anim in animations_out:
        for s in anim['samplers']:
            s['input'] += acc_offset
            s['output'] += acc_offset

    # ── Assemble glTF JSON ──────────────────────────────────────────────────
    meshes = [{'name': geo.get('name', 'mesh'), 'primitives': [{
        'attributes': attributes,
        'indices': 2,
        'material': 0,
    }]}]

    gltf = {
        'asset': {'version': '2.0', 'generator': 'jme3-json2gltf.py'},
        'scene': 0,
        'scenes': [{'nodes': [scene_node_idx]}],
        'nodes': nodes,
        'meshes': meshes,
        'materials': [{'name': 'default', 'pbrMetallicRoughness': {
            'baseColorFactor': [1, 1, 1, 1], 'metallicFactor': 0, 'roughnessFactor': 0.5}}],
        'animations': animations_out,
        'accessors': accessors,
        'bufferViews': buffer_views,
        'buffers': [{'byteLength': len(all_bin)}],
    }

    if skin:
        gltf['skins'] = [skin]

    gltf_json = json.dumps(gltf, separators=(',', ':')).encode('utf-8')
    while len(gltf_json) % 4:
        gltf_json += b' '
    while len(all_bin) % 4:
        all_bin += b'\x00'

    header = struct.pack('<III', 0x46546C67, 2, 12 + 8 + len(gltf_json) + 8 + len(all_bin))
    json_chunk = struct.pack('<II', len(gltf_json), 0x4E4F534A)
    bin_chunk = struct.pack('<II', len(all_bin), 0x004E4942)

    with open(output_path, 'wb') as f:
        f.write(header + json_chunk + gltf_json + bin_chunk + all_bin)

    print(f'  -> {output_path} (verts={vertex_count}, bones={len(skin["joints"]) if skin else 0}, '
          f'anims={len(animations_out)})')

def main():
    in_dir = '/var/home/nico/Gamedev/Wackelpeter/web/public/models'
    models = ['spider', 'staff', 'cross', 'helmet', 'wall1', 'tile1', 'tile2', 'blobknochen']
    for m in models:
        jp = os.path.join(in_dir, f'{m}.json')
        gp = os.path.join(in_dir, f'{m}.glb')
        if os.path.exists(jp):
            print(f'Converting {m}...')
            try:
                convert(jp, gp)
            except Exception as e:
                print(f'  ERROR: {e}')
                import traceback
                traceback.print_exc()

if __name__ == '__main__':
    main()
