from ac3d import read_ac
from pathlib import Path
from collections import defaultdict
from PIL import Image
import sys,re,json,math,struct,shutil,array
ROOT=Path('assets/source/v3'); OUT=Path('public/assets/v3')
def convert(id,car=False,wheel=False):
 src=ROOT/id; target=OUT/id;target.mkdir(parents=True,exist_ok=True)
 ac=next(src.glob('*.acc')) if wheel else src/(id+'.acc')
 mats,objects=read_ac(ac)
 roads=sorted([o for o in objects if re.fullmatch(r'TKMN\d+',o['name'])],key=lambda o:int(o['name'][4:]))
 line=[];rights=[]
 for o in roads:
  for i in range(0,len(o['v'])-1,2):
   p=[(o['v'][i][j]+o['v'][i+1][j])/2 for j in range(3)]
   if not line or math.dist(p,line[-1])>.1:
    line.append(p);r=[o['v'][i+1][j]-o['v'][i][j] for j in range(3)];d=math.sqrt(sum(x*x for x in r));rights.append([x/d for x in r])
 offset=line[0] if line else [0,0,0]
 if line and math.dist(line[0],line[-1])<.5:line.pop();rights.pop()
 def transform(v):
  p=[v[j]-offset[j] for j in range(3)]
  return [p[2],p[1]-.70,-p[0]] if car else p
 def normal(v):return [v[2],v[1],-v[0]] if car else v
 groups=defaultdict(lambda:{'p':[],'n':[],'uv':[]});collision={'road':[],'terrain':[],'solid':[]};used=set();missing=set()
 for o in objects:
  if not o['f']:continue
  name=o['name'];texture=o['texture'];positions=[transform(v) for v in o['v']]
  cx=sum(p[0] for p in positions)/len(positions);cz=sum(p[2] for p in positions)/len(positions)
  groupkey=(texture,0 if car or wheel else math.floor(cx/200),0 if car or wheel else math.floor(cz/200), 'glass' if 'GLASS' in name else 'brake' if name.startswith('BACKLIGHTS') else 'body')
  g=groups[groupkey]
  for mat,tri in o['f']:
   pp=[positions[r[0]] for r in tri];u=[pp[1][j]-pp[0][j] for j in range(3)];v=[pp[2][j]-pp[0][j] for j in range(3)];n=[u[1]*v[2]-u[2]*v[1],u[2]*v[0]-u[0]*v[2],u[0]*v[1]-u[1]*v[0]];norm=math.sqrt(sum(x*x for x in n))
   if norm<1e-8:continue
   for r in tri:
    g['p'].extend(positions[r[0]]);source=o['v'][r[0]];g['n'].extend(normal(source[3:6]) if len(source)>=6 else [x/norm for x in n]);g['uv'].extend([r[1],1-r[2]])
   if not car and not wheel:
    category='road' if name.startswith('TKMN') or (name.startswith('TK') and any(k in (texture or '').lower() for k in ['asphalt','curb','kerb','concrete'])) else 'terrain' if name.startswith(('TERR','TKLS','TKRS')) else 'solid'
    if category!='solid' or not any(k in (texture or '').lower() for k in ['arbor','tree','sitter','forest']):
     for p in pp:collision[category].extend(p)
  if texture:used.add(texture)
 gltf={'asset':{'version':'2.0','generator':'APEX AC3D source-preserving importer','copyright':'See /credits.html; modified art remains Free Art License'},'scene':0,'scenes':[{'nodes':[]}],'nodes':[],'meshes':[],'materials':[],'accessors':[],'bufferViews':[],'buffers':[{}],'images':[],'textures':[],'samplers':[{'magFilter':9729,'minFilter':9987,'wrapS':10497,'wrapT':10497}]};bin=bytearray();textures={};materialIds={}
 for tex in sorted(used):
  path=src/tex
  if not path.exists():path=ROOT/'shared'/tex
  if not path.exists():missing.add(tex);continue
  image=Image.open(path);image.thumbnail((2048,2048));dest=tex.rsplit('.',1)[0]+'.png';image.save(target/dest)
  textures[tex]=len(gltf['textures']);gltf['textures'].append({'source':len(gltf['images']),'sampler':0});gltf['images'].append({'uri':dest})
 if missing:raise RuntimeError(f'{id} missing textures: {sorted(missing)}')
 def accessor(values,width):
  data=array.array('f',values).tobytes();view=len(gltf['bufferViews']);gltf['bufferViews'].append({'buffer':0,'byteOffset':len(bin),'byteLength':len(data)});bin.extend(data)
  a={'bufferView':view,'componentType':5126,'count':len(values)//width,'type':{2:'VEC2',3:'VEC3'}[width]}
  if width==3:a['min']=[min(values[j::3]) for j in range(3)];a['max']=[max(values[j::3]) for j in range(3)]
  idx=len(gltf['accessors']);gltf['accessors'].append(a);return idx
 for key,g in groups.items():
  if not g['p']:continue
  tex,_,_,kind=key;mk=(tex,kind)
  if mk not in materialIds:
   m={'name':str(tex or 'untextured')+'-'+kind,'doubleSided':True,'pbrMetallicRoughness':{'baseColorFactor':[1,1,1,1],'roughnessFactor':.48 if car else .92,'metallicFactor':.18 if car else 0}}
   if tex:m['pbrMetallicRoughness']['baseColorTexture']={'index':textures[tex]};m['alphaMode']='MASK';m['alphaCutoff']=.45
   if kind=='glass':m['alphaMode']='BLEND';m['pbrMetallicRoughness']['baseColorFactor'][3]=.52
   materialIds[mk]=len(gltf['materials']);gltf['materials'].append(m)
  primitive={'attributes':{'POSITION':accessor(g['p'],3),'NORMAL':accessor(g['n'],3),'TEXCOORD_0':accessor(g['uv'],2)},'material':materialIds[mk]}
  mesh=len(gltf['meshes']);gltf['meshes'].append({'primitives':[primitive]});gltf['scenes'][0]['nodes'].append(len(gltf['nodes']));gltf['nodes'].append({'mesh':mesh,'name':str(key)})
 gltf['buffers'][0]['byteLength']=len(bin);js=json.dumps(gltf,separators=(',',':')).encode();js+=b' '*((-len(js))%4);bin+=b'\0'*((-len(bin))%4)
 glb=struct.pack('<III',0x46546c67,2,28+len(js)+len(bin))+struct.pack('<II',len(js),0x4e4f534a)+js+struct.pack('<II',len(bin),0x004e4942)+bin;(target/'scene.glb').write_bytes(glb)
 if line:
  line=[[round(p[j]-offset[j],4) for j in range(3)] for p in line];length=sum(math.dist(line[i],line[(i+1)%len(line)]) for i in range(len(line)))
  meta={'id':id,'line':line,'rights':rights,'length':length,'width':15 if id=='brno' else 10,'bounds':{'minX':min(p[0] for p in line),'maxX':max(p[0] for p in line),'minZ':min(p[2] for p in line),'maxZ':max(p[2] for p in line)},'elevation':max(p[1] for p in line)-min(p[1] for p in line),'chunks':len(groups),'triangles':len(bin)//(3*8*4),'offset':offset}
  (target/'route.json').write_text(json.dumps(meta,separators=(',',':')))
  for k,v in collision.items():(target/(k+'.bin')).write_bytes(array.array('f',v).tobytes())
  print(id,'length',round(length),'elevation',round(meta['elevation']), 'collision', {k:len(v)//9 for k,v in collision.items()})
 print(id,'glbMB',round(len(glb)/1e6,2),'draw chunks',len(groups),'textures',len(textures))
if __name__=='__main__':
 for id in sys.argv[1:]:convert(id,car=id.startswith('sc-'),wheel=id=='shared')
