"""Lossless glTF 1 binary -> glTF 2 conversion of FR24's GPL A350 model.
Geometry/index/UV/normal bytes and node transforms are copied unchanged.
Legacy GLSL techniques are replaced by physically based material placeholders.
"""
from pathlib import Path
import argparse,json,struct
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('input',type=Path,help='Original Flightradar24 models/a359.glb')
parser.add_argument('--output',type=Path,default=Path('a350-900-flightgear.glb'))
args=parser.parse_args()
src=args.input.read_bytes()
jlen=struct.unpack_from('<I',src,12)[0]
j=json.loads(src[20:20+jlen]);binary=src[20+jlen:]
out={'asset':{'version':'2.0','generator':'Operator lossless glTF1 to glTF2 converter','copyright':'A350XWB by Sbyx, Jormappapa and contributors; FlightGear/FGMEMBERS; Flightradar24 conversion; GPL-2.0-or-later'},'buffers':[{'byteLength':len(binary)}],'bufferViews':[]}
ids={k:{key:i for i,key in enumerate(j[k])} for k in ['accessors','images','materials','meshes','nodes','samplers','textures','scenes']}
out['accessors']=[]
components={'SCALAR':1,'VEC2':2,'VEC3':3,'VEC4':4,'MAT4':16}
bytesize={5120:1,5121:1,5122:2,5123:2,5125:4,5126:4}
for key,a in j['accessors'].items():
 v=j['bufferViews'][a['bufferView']];length=components[a['type']]*bytesize[a['componentType']]
 stride=a.get('byteStride') or length
 nv={'buffer':0,'byteOffset':v.get('byteOffset',0)+a.get('byteOffset',0),'byteLength':(a['count']-1)*stride+length,'target':v.get('target',34962)}
 if stride!=length:nv['byteStride']=stride
 na={x:a[x] for x in ['componentType','count','type','min','max','normalized'] if x in a};na['bufferView']=len(out['bufferViews']);out['bufferViews'].append(nv);out['accessors'].append(na)
out['images']=[]
for key,img in j['images'].items():
 ext=img['extensions']['KHR_binary_glTF'];v=j['bufferViews'][ext['bufferView']]
 out['images'].append({'name':img['name'],'bufferView':len(out['bufferViews']),'mimeType':ext['mimeType']})
 out['bufferViews'].append({k:v[k] for k in ['byteLength','byteOffset']}|{'buffer':0})
 Path(key+'.jpg').write_bytes(binary[v['byteOffset']:v['byteOffset']+v['byteLength']])
out['samplers']=list(j['samplers'].values())
out['textures']=[{'source':ids['images'][t['source']],'sampler':ids['samplers'][t['sampler']]} for t in j['textures'].values()]
out['materials']=[]
for key,m in j['materials'].items():
 v=m['values'];d=v.get('diffuse',[.8,.8,.8,1]);pbr={'metallicFactor':.15,'roughnessFactor':.35}
 if isinstance(d,str):pbr['baseColorTexture']={'index':ids['textures'][d]}
 else:pbr['baseColorFactor']=d
 out['materials'].append({'name':key,'pbrMetallicRoughness':pbr,'doubleSided':True})
out['meshes']=[]
for key,m in j['meshes'].items():
 primitives=[]
 for p in m['primitives']:
  np={'attributes':{k:ids['accessors'][v] for k,v in p['attributes'].items()},'indices':ids['accessors'][p['indices']],'material':ids['materials'][p['material']],'mode':p.get('mode',4)}
  primitives.append(np)
 out['meshes'].append({'name':m['name'],'primitives':primitives})
out['nodes']=[]
for key,n in j['nodes'].items():
 nn={k:n[k] for k in ['name','matrix','translation','rotation','scale'] if k in n}
 nn['children']=[ids['nodes'][k] for k in n.get('children',[])]
 if n.get('meshes'):assert len(n['meshes'])==1;nn['mesh']=ids['meshes'][n['meshes'][0]]
 out['nodes'].append(nn)
out['scenes']=[{'nodes':[ids['nodes'][k] for k in s['nodes']]} for s in j['scenes'].values()];out['scene']=ids['scenes'][j['scene']]
data=json.dumps(out,separators=(',',':')).encode();data+=b' ' *((-len(data))%4);binary+=b'\x00'*((-len(binary))%4)
glb=struct.pack('<III',0x46546c67,2,12+8+len(data)+8+len(binary))+struct.pack('<II',len(data),0x4e4f534a)+data+struct.pack('<II',len(binary),0x004e4942)+binary
args.output.write_bytes(glb)
print('Converted',len(glb),'bytes; meshes',len(out['meshes']),'nodes',len(out['nodes']))
# Read transformed vertices to establish the source's true span/length and parts.
def mm(a,b):return [sum(a[k*4+r]*b[c*4+k] for k in range(4)) for c in range(4) for r in range(4)]
ident=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]
def points(akey,m):
 a=j['accessors'][akey];v=j['bufferViews'][a['bufferView']];off=v.get('byteOffset',0)+a.get('byteOffset',0);stride=a.get('byteStride') or 12
 for i in range(a['count']):
  x,y,z=struct.unpack_from('<fff',binary,off+i*stride)
  yield [m[k]*x+m[4+k]*y+m[8+k]*z+m[12+k] for k in range(3)]
allpts=[];report=[]
def visit(key,parent):
 n=j['nodes'][key];m=mm(parent,n.get('matrix',ident));p=[]
 for meshkey in n.get('meshes',[]):
  for primitive in j['meshes'][meshkey]['primitives']:p.extend(points(primitive['attributes']['POSITION'],m))
 if p:
  b=[[min(v[d] for v in p),max(v[d] for v in p)] for d in range(3)];allpts.extend(p);report.append({'name':n['name'],'bounds':b,'vertices':len(p)})
 for c in n.get('children',[]):visit(c,m)
for n in j['scenes'][j['scene']]['nodes']:visit(n,ident)
print('WORLD BOUNDS',[[min(v[d] for v in allpts),max(v[d] for v in allpts)] for d in range(3)])
Path('geometry-report.json').write_text(json.dumps(report,indent=2))
print(json.dumps([r for r in report if any(x in r['name'].lower() for x in ['fuselage','winglet','windshield','nacelle','fan','engine','cabin'])],indent=2))
