"""Blender: preserve authored clips, normalise scale/origin, prune skin weights.
All source/final paths are in the explicitly supplied disposable task directory.
"""
import bpy,bmesh,sys,json,math
from pathlib import Path
from mathutils import Vector
P=Path(sys.argv[sys.argv.index('--')+1]); ONLY=sys.argv[sys.argv.index('--')+2:]
SPECS=[
('animals/Wolf','wolf',.95,'A lean grey wolf with a long muzzle and bushy tail; walks, gallops, bites, then falls onto its side.'),
('animals/Fox','fox',.65,'A small orange fox with a white chest and bushy tail; walks, gallops, bites, then falls onto its side.'),
('animals/Husky','dog',.85,'A black and white husky with pointed ears; walks, gallops, bites, then falls onto its side.'),
('animals/Deer','deer',1.4,'A brown deer with long slender legs; walks, gallops, headbutts, then collapses.'),
('animals/Stag','stag',2.1,'An antlered brown stag; walks, gallops, headbutts, then collapses.'),
('animals/Horse','horse',2.,'A brown horse with a dark mane; walks, gallops, strikes with its head, then falls.'),
('animals/Bull','bull',1.7,'A heavy horned bull; walks, gallops, headbutts, then falls.'),
('animals/Cow','cow',1.6,'A brown and white cow; walks, gallops, headbutts, then falls.'),
('animals/Alpaca','alpaca',1.7,'A woolly long-necked alpaca; walks, gallops, headbutts, then falls.'),
('animals/Donkey','donkey',1.65,'A grey long-eared donkey; walks, gallops, strikes with its head, then falls.'),
('monsters/Big/Bunny','rabbit-brute',1.4,'A white long-eared rabbit creature with empty hands; walks, runs, punches, then collapses.'),
('monsters/Big/Birb','bird-brute',1.3,'A round flightless bird creature; walks, runs, strikes with a wing, then collapses.'),
('monsters/Big/Monkroose','monkroose',1.7,'A green round-eared beaked chimera with orange hands; walks, runs, punches, then collapses.'),
('monsters/Big/Tribal','tribal-monster',1.8,'A red forest creature wearing a green mask and white feather crest; walks, runs, punches, then collapses.'),
('monsters/Big/Orc','orc',2.,'A stocky green tusked orc with empty hands; walks, runs, punches, then collapses.'),
('monsters/Big/Yeti','yeti',2.5,'A bulky white-furred yeti; walks, runs, punches, then collapses.'),
('monsters/Big/Dino','dinosaur',2.6,'A red and yellow upright dinosaur; walks, runs, punches, then collapses.'),
('monsters/Big/Frog','frog-brute',1.3,'A yellow and black frog monster with long arms; walks, runs, punches, then collapses.'),
('monsters/Big/MushroomKing','mushroom-king',2.2,'A broad blue-capped mushroom creature with empty hands; walks, runs, punches, then collapses.'),
('monsters/Big/Cactoro','cactus-monster',1.8,'A spiny green cactus creature; walks, runs, punches, then collapses.'),
('monsters/Big/Fish','fish-raider',1.5,'An upright turquoise fish monster with fins and legs; walks, runs, punches, then collapses.'),
('monsters/Big/Alien','alien',2.,'A purple alien with curled green antennae; walks, runs, punches, then collapses.'),
('monsters/Big/BlueDemon','blue-demon',2.4,'A blue horned demon with empty hands; walks, runs, punches, then collapses.'),
('monsters/Flying/Goleling_Evolved','floating-golem',2.2,'A green crowned flying goblin head with bat wings; hovers, flies, headbutts, then drops.'),
('monsters/Flying/Dragon_Evolved','dragon',3.,'A winged dragon; hovers, flies, headbutts, then drops to the ground.'),
('monsters/Flying/Armabee_Evolved','giant-bee',1.,'A round black and yellow bee monster; hovers, flies, headbutts, then drops.'),
('monsters/Flying/Pigeon','pigeon',.35,'A small round purple bird with grey wings; hovers, flies, pecks, then drops.'),
('monsters/Flying/Squidle','flying-squid',1.8,'A pink horned winged imp; hovers, flies, headbutts, then drops.'),
]
def bounds(meshes):
 dg=bpy.context.evaluated_depsgraph_get();pts=[]
 for o in meshes:
  ev=o.evaluated_get(dg); me=ev.to_mesh();pts.extend(ev.matrix_world@v.co for v in me.vertices);ev.to_mesh_clear()
 return Vector([min(v[i] for v in pts) for i in range(3)]),Vector([max(v[i] for v in pts) for i in range(3)])
def set_action(rig,a):
 ad=rig.animation_data_create();ad.action=a
 if a and len(a.slots):ad.action_slot=a.slots[0]
 for t in ad.nla_tracks:t.mute=True
 bpy.context.scene.frame_set(int(a.frame_range[0]) if a else 0);bpy.context.view_layer.update()
def build(spec):
 src,kind,height,about=spec; ident='enemy-'+kind
 bpy.ops.wm.read_factory_settings(use_empty=True)
 bpy.context.scene.render.fps=30
 bpy.ops.import_scene.gltf(filepath=str(P/'sources'/(src+'.gltf')))
 rigs=[o for o in bpy.context.scene.objects if o.type=='ARMATURE'];assert len(rigs)==1
 rig=rigs[0]
 source_doc=json.loads((P/'sources'/(src+'.gltf')).read_text())
 source_mesh_names={n.get('name','') for n in source_doc['nodes'] if 'mesh' in n}
 meshes=[o for o in bpy.context.scene.objects if o.type=='MESH' and (any(m.type=='ARMATURE' for m in o.modifiers) or o.name in source_mesh_names)]
 removed=[]
 for o in list(meshes):
  if 'Weapon' in o.name or o.name=='Carrot' or (kind=='mushroom-king' and o.name=='Mushroom'):
   removed.append(o.name);meshes.remove(o);bpy.data.objects.remove(o,do_unlink=True)
 for o in list(bpy.context.scene.objects):
  if o.type=='MESH' and o not in meshes:bpy.data.objects.remove(o,do_unlink=True)
 # BlueDemon has its club joined into the body object. Remove only the two
 # disconnected club/grip islands, using topology plus their source-space bounds.
 if kind=='blue-demon':
  for o in meshes:
   bm=bmesh.new();bm.from_mesh(o.data);bm.verts.ensure_lookup_table();parents=list(range(len(bm.verts)))
   def find(i):
    while parents[i]!=i:parents[i]=parents[parents[i]];i=parents[i]
    return i
   def union(a,b):parents[find(a)]=find(b)
   seen={}
   for v in bm.verts:
    key=tuple(round(c,5) for c in v.co)
    if key in seen:union(v.index,seen[key])
    else:seen[key]=v.index
   for e in bm.edges:union(e.verts[0].index,e.verts[1].index)
   groups={}
   for v in bm.verts:groups.setdefault(find(v.index),[]).append(v)
   bad=[]
   for vs in groups.values():
    if min(v.co.x for v in vs)>1.5 and max(v.co.y for v in vs)-min(v.co.y for v in vs)>.5:bad.extend(vs)
   if bad:bmesh.ops.delete(bm,geom=bad,context='VERTS');bm.to_mesh(o.data);removed.append('embedded club and grip')
   bm.free()

 actions={a.name:a for a in bpy.data.actions};fly='/Flying/' in src
 mapping={'idle':'Flying_Idle' if fly else 'Idle','run':'Fast_Flying' if fly else 'Gallop' if src.startswith('animals') else 'Run','attack':'Headbutt' if fly else 'Attack' if 'Attack' in actions else 'Attack_Headbutt' if 'Attack_Headbutt' in actions else 'Punch','die':'Death','hit':'Idle_HitReact_Left' if 'Idle_HitReact_Left' in actions else 'HitReact'}
 if not fly:mapping['walk']='Walk'
 mapping={k:v for k,v in mapping.items() if v in actions}
 for t in list(rig.animation_data.nla_tracks):rig.animation_data.nla_tracks.remove(t)
 set_action(rig,actions[mapping['idle']]);bpy.context.scene.frame_set(0)
 lo,hi=bounds(meshes);scale=height/(hi.z-lo.z)
 root=bpy.data.objects.new('CreatureRoot',None);bpy.context.collection.objects.link(root)
 for o in list(bpy.context.scene.objects):
  if o!=root and o.parent is None:o.parent=root
 root.scale=(scale,)*3;root.location=(-float((lo.x+hi.x)/2)*scale,-float((lo.y+hi.y)/2)*scale,-lo.z*scale)
 bpy.context.view_layer.update()
 # A single SIMPLE subdivision raises low-poly sources into the requested 3k minimum.
 # It adds no invented surface detail and retains the source silhouette.
 tris=sum(sum(len(p.vertices)-2 for p in o.data.polygons) for o in meshes);subdivided=tris<3000
 if subdivided:
  rig.data.pose_position='REST'
  for o in meshes:
   bpy.context.view_layer.objects.active=o;o.select_set(True)
   mod=o.modifiers.new('BudgetTessellation','SUBSURF');mod.subdivision_type='SIMPLE';mod.levels=1
   while o.modifiers.find(mod.name)>0:bpy.ops.object.modifier_move_up(modifier=mod.name)
   bpy.ops.object.modifier_apply(modifier=mod.name);o.select_set(False)
  rig.data.pose_position='POSE'
 for o in meshes:
  for v in o.data.vertices:
   ws=sorted([(g.group,g.weight) for g in v.groups if g.weight>1e-8],key=lambda x:-x[1]);kept=ws[:4];total=sum(w for _,w in kept)
   for g,_ in ws[4:]:o.vertex_groups[g].remove([v.index])
   if total:
    for g,w in kept:o.vertex_groups[g].add([v.index],w/total,'REPLACE')
 for a in list(bpy.data.actions):
  if a.name not in mapping.values():bpy.data.actions.remove(a)
 set_action(rig,actions[mapping['idle']])
 bpy.ops.object.select_all(action='DESELECT')
 for o in [root,rig]+meshes:o.select_set(True)
 out=P/'glb'/(ident+'.glb');out.parent.mkdir(exist_ok=True)
 bpy.ops.export_scene.gltf(filepath=str(out),export_format='GLB',use_selection=True,export_yup=True,export_animations=True,export_animation_mode='ACTIONS',export_force_sampling=True,export_anim_single_armature=True,export_optimize_animation_size=True,export_skins=True,export_all_influences=False,export_influence_nb=4,export_leaf_bone=False,export_materials='EXPORT',export_cameras=False,export_lights=False)
 lo,hi=bounds(meshes)
 provenance=next(x for x in json.loads((P/'evidence/sources.json').read_text()) if x['path']=='sources/'+src+'.gltf')
 entry={'id':ident,'role':'boss' if kind in ['dragon','mushroom-king','yeti'] else 'enemy','about':about,'good_for':[kind,'fantasy','forest' if src.startswith('animals') else 'dungeon'],'names':[kind,kind+'s'],'file':out.name,'height_m':round(hi.z-lo.z,4),'clips':mapping,'tint':sorted(set(m.name for o in meshes for m in o.data.materials if m)),'licence':'CC0-1.0','credit':'Quaternius, '+('Ultimate Animated Animals' if src.startswith('animals') else 'Ultimate Monsters')+', '+provenance['pack_url']+' ; model: '+provenance['url'],'speeds':{},'style':'toon','mood':['playful','stylised'],'small':height<.5}
 detail={'source':provenance,'target_height':height,'bounds_idle':[list(lo),list(hi)],'removed_objects':removed,'simple_subdivision':subdivided,'triangles':sum(sum(len(p.vertices)-2 for p in o.data.polygons) for o in meshes),'bones':[b.name for b in rig.data.bones],'missing_clips':[x for x in ['idle','walk','run','attack','hit','die'] if x not in mapping],'flyer':fly}
 (P/'evidence'/(ident+'-normalization.json')).write_text(json.dumps(detail,indent=2));(P/'evidence'/(ident+'-entry.json')).write_text(json.dumps(entry,indent=2))
 print('BUILT',ident,out.stat().st_size,detail['triangles'],flush=True)
for spec in SPECS:
 if not ONLY or spec[1] in ONLY:build(spec)
