# Blender (3.2) batch: SKCombat props / tiles from prefab_jobs.json → glTF, each placed in its square as the game did.
# Run: blender -b --factory-startup --python convert2.py -- <prefab_jobs.json> <FBXs dir> <out dir> <textures dir>
# A job's meshes are FBX meshes in their own (node) space, as Unity used them, with a matrix to the prefab root
# (FBX axes, raw units: 1.5 = one square). The root is the square's anchor: its middle, at floor level. Output: one
# unit = one square, y up, the origin on that anchor (catalog `ground: 'origin'` → the VTT stands it exactly there).
# Tiles (`cut`) are columns sunk into the ground; only their top square's worth is kept.
import bpy, bmesh, sys, os, json, mathutils
jobs_path, fbx_dir, out_dir, tex_dir = sys.argv[sys.argv.index('--') + 1:]
jobs = json.load(open(jobs_path))
only = os.environ.get('PACKS')
names = os.environ.get('NAMES')
# FBX space (y up, raw units) → Blender (z up, squares); the glTF exporter turns it back to y up
C = mathutils.Matrix.Scale(1 / 1.5, 4) @ mathutils.Matrix(((1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 0, 0, 1)))
done = 0
mats = {}

def material(tex, kind):
    key = tex + '|' + kind
    if key in mats:
        return mats[key]
    ext = '.png' if os.path.exists(os.path.join(tex_dir, tex + '.png')) and (kind == 'alpha' or tex == 'basic') else '.jpg'
    if not os.path.exists(os.path.join(tex_dir, tex + ext)):
        ext = '.png' if ext == '.jpg' else '.jpg'
    img = bpy.data.images.load(os.path.join(tex_dir, tex + ext), check_existing=True)
    img.name = tex
    mat = bpy.data.materials.new('sk-' + tex + ('-' + kind if kind != 'opaque' else ''))
    mat.use_nodes = True
    nt = mat.node_tree
    bsdf = nt.nodes.get('Principled BSDF')
    tn = nt.nodes.new('ShaderNodeTexImage')
    tn.image = img
    nt.links.new(tn.outputs['Color'], bsdf.inputs['Base Color'])
    bsdf.inputs['Roughness'].default_value = 0.85
    bsdf.inputs['Specular'].default_value = 0.2
    mat.use_backface_culling = True  # single-sided like the game: tiles side by side share their walls without flickering
    if kind == 'alpha':
        nt.links.new(tn.outputs['Alpha'], bsdf.inputs['Alpha'])
        mat.blend_method = 'CLIP'
        mat.alpha_threshold = 0.4
        mat.use_backface_culling = False
    if kind == 'glow':
        nt.links.new(tn.outputs['Color'], bsdf.inputs['Emission'])
    if kind == 'water':  # a see-through surface (the game scrolled this sheet; here it is still)
        bsdf.inputs['Alpha'].default_value = 0.8
        bsdf.inputs['Roughness'].default_value = 0.15
        mat.blend_method = 'BLEND'
        mat.use_backface_culling = False
    mats[key] = mat
    return mat

for j in jobs:
    if only and j['pack'] not in only.split(','):
        continue
    if names and j['name'] not in names.split(','):
        continue
    bpy.ops.wm.read_factory_settings(use_empty=True)
    mats = {}
    cache = {}
    parts = []
    for i, me in enumerate(j['meshes']):
        if me['fbx'] not in cache:
            before = set(bpy.data.objects)
            try:
                bpy.ops.import_scene.fbx(filepath=os.path.join(fbx_dir, me['fbx']), anim_offset=0)
            except Exception as e:
                print('SKIP', j['name'], me['fbx'], e); cache[me['fbx']] = {}; continue
            new = [o for o in bpy.data.objects if o not in before]
            for o in new:
                o.animation_data_clear()
            cache[me['fbx']] = {o.name.split('.')[0]: o for o in new if o.type == 'MESH'}
            cache[me['fbx']].update({o.data.name.split('.')[0]: o for o in new if o.type == 'MESH' and o.data.name.split('.')[0] not in cache[me['fbx']]})
        objs = cache[me['fbx']]
        src = objs.get(me['mesh']) or (next(iter(objs.values())) if len(objs) == 1 else None)
        if src is None:
            print('MISSING', j['name'], me['fbx'], me['mesh'], list(objs)[:6]); continue
        data = src.data.copy()
        data.transform(C @ mathutils.Matrix(me['m']))  # its place in the square
        if me['kind'] == 'water':
            # the game's water sheet is an animation strip the surfaces sample a sliver of; here the calm frame is
            # laid flat over the square (one repeat per square), so pools and creeks read as water, not as a smear
            uv = data.uv_layers.active or data.uv_layers.new()
            for poly in data.polygons:
                for li in poly.loop_indices:
                    co = data.vertices[data.loops[li].vertex_index].co
                    uv.data[li].uv = (co.x, co.y)
        data.materials.clear()
        data.materials.append(material(me['tex'], me['kind']))
        ob = bpy.data.objects.new(j['name'] + '-' + str(i), data)
        bpy.context.scene.collection.objects.link(ob)
        parts.append(ob)
    for o in list(bpy.data.objects):
        if o not in parts:
            bpy.data.objects.remove(o, do_unlink=True)
    if not parts:
        print('EMPTY', j['name']); continue
    bpy.ops.object.select_all(action='DESELECT')
    for o in parts:
        o.select_set(True)
    bpy.context.view_layer.objects.active = parts[0]
    if len(parts) > 1:
        bpy.ops.object.join()
    obj = bpy.context.view_layer.objects.active
    obj.name = j['name']
    if j.get('cut') is not None:
        bm = bmesh.new(); bm.from_mesh(obj.data)
        geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
        bmesh.ops.bisect_plane(bm, geom=geom, plane_co=(0, 0, j['cut']), plane_no=(0, 0, 1), clear_inner=True)
        bm.to_mesh(obj.data); bm.free(); obj.data.update()
    if not obj.data.vertices:
        print('EMPTY', j['name']); continue
    pack_dir = os.path.join(out_dir, j['pack'])
    os.makedirs(pack_dir, exist_ok=True)
    bpy.ops.export_scene.gltf(filepath=os.path.join(pack_dir, j['name'] + '.gltf'), export_format='GLTF_SEPARATE',
                              export_texture_dir='tex', export_materials='EXPORT', export_yup=True, export_apply=True,
                              export_cameras=False, export_lights=False, export_animations=False)
    zs = [v.co.z for v in obj.data.vertices]
    print('MODEL', j['pack'], j['name'], 'height', round(min(zs), 2), round(max(zs), 2))
    done += 1
print('CONVERTED', done)
