#!/usr/bin/env python

import os, time
import bpy
from math import pi
import glob
from mathutils import Vector, Matrix
from math import radians, pi
import json

class ExportMappingFemale:
    char_blend_path = "assets/blender/" + "vroid-normal-female.blend"
    cloth_blend_paths = ["assets/blender/female-coat2.blend"]
    gltf_path = "godot/clothes/female-coat.gltf"
    inner_path = "Object"
    objs = ["skeleton", "body", "privates", "ref-bodyskirt-noimp", "ref-bodydress-noimp", "ref-bodycap-noimp", "ref-bodyshoes-noimp", "ref-dress-noimp", "ref-topskirt-noimp", "ref-skirt4-noimp", "ref-skirt3-noimp"]
    objs_remove = []
    armature_name = "skeleton"
    outfile = "tmp-female-cloth.blend"
    default_action = 'default'
    sex = "female"
    def __init__(self):
        self.files = []
        for fobj in self.objs:
            self.files.append({"name": fobj})

class ExportMappingMale:
    char_blend_path = "assets/blender/vroid1-man-animate.blend"
    cloth_blend_paths = ["assets/blender/male-coat2.blend"]
    gltf_path = "godot/clothes/male-coat.gltf"
    inner_path = "Object"
    objs = ["pxy", "body", "ref-topbottom-noimp", "ref-bodytopbottom-noimp"]
    objs_remove = []
    armature_name = "pxy"
    outfile = "tmp-male-cloth.blend"
    default_action = 'default'
    sex = "male"
    def __init__(self):
        self.files = []
        for fobj in self.objs:
            self.files.append({"name": fobj})


basepath = os.getcwd()
def check_bone(bname):
    ok = True
    baddie = ["ctrl_", "mch_", "MCH_"]
    for bd in baddie:
        if bname.startswith(bd):
            ok = False
            break
    return ok

def clamp_angle_deg(angle, min_angle_deg, max_angle_deg):
    min_angle = radians(min_angle_deg)
    max_angle = radians(max_angle_deg)
    if angle < min_angle:
        angle = min_angle
    if angle > max_angle:
        angle = max_angle
    return angle
def angle_to_linear(angle, divider):
    if angle < 0.0:
        return angle / divider
    else:
        return 0.0
def angle_to_linear_x(bone, angle):
    skel = bpy.data.objects["skeleton_orig"]
    left_base = "ctrl_base_upperleg.L.001"
    right_base = "ctrl_base_upperleg.R.001"
    base = ""
    if base == "":
        for e in ["_R", ".R"]:
            if bone.name.endswith(e):
                base = right_base
                break
    if base == "":
        for e in ["_L", ".L"]:
            if bone.name.endswith(e):
                base = left_base
                break
    if base == "":
        for e in ["_R.", ".R."]:
            if bone.name.find(e) >= 0:
                base = right_base
                break
    if base == "":
        for e in ["_L.", ".L."]:
            if bone.name.find(e) >= 0:
                base = left_base
                break
    mul = skel.pose.bones[base]["to_linear_x_base"]
    offset = skel.pose.bones[base]["angle_offset"]
#    print("bone: ", bone.name, "base: ", base, "mul: ", mul)
#    print("angle: ", angle, " angle_offset: ", offset, " angle_sum: ", angle + offset)
    print("offset: ", mul * (angle + offset), "bone: ", base, "angle: ", angle)
    return (angle + offset) * mul
def extra_linear(angle, offset):
    ret = 0.0
    offt = offset * angle * 2.0 / -radians(-90)
    if angle * 2.0 < -radians(65):
        if angle * 2.0 > -radians(65):
            ret += offset
        else:
            ret += offt
    return ret

def prepare_armature(mapping):
    print("Preparing...")
    bpy.ops.object.armature_add(enter_editmode=False)
    new_armature = bpy.context.object
    orig_armature = bpy.data.objects[mapping.armature_name]
    armature_name = orig_armature.name
    orig_armature.name = orig_armature.name + "_orig"
    new_armature.name = armature_name
    queue = []
    if new_armature.animation_data is None:
        new_armature.animation_data_create()
    bpy.context.view_layer.objects.active = new_armature
    bpy.ops.object.mode_set(mode='EDIT')
    for b in new_armature.data.edit_bones:
        new_armature.data.edit_bones.remove(b)
    bpy.context.view_layer.objects.active = orig_armature
    bpy.ops.object.mode_set(mode='EDIT')
    for b in orig_armature.data.edit_bones:
        print(b.name)
        if b.parent is None:
            queue.append(b.name)
    print("Copying bones...")
    while len(queue) > 0:
        item = queue.pop(0)
        print(item)
        itemb = orig_armature.data.edit_bones[item]
        if not itemb.use_deform and not check_bone(item):
            continue
        for cb in orig_armature.data.edit_bones:
            if cb.parent == itemb:
                queue.append(cb.name)
        nb = new_armature.data.edit_bones.new(item)
        nb.name = item
        nb.head = itemb.head
        nb.tail = itemb.tail
        nb.matrix = itemb.matrix
        nb.use_deform = itemb.use_deform
        if itemb.parent is not None:
            ok = True
            pname = itemb.parent.name
            while not check_bone(pname):
                bparent = itemb.parent.parent
                if bparent is None:
                    ok = False
                    break
                pname = bparent.name
            if ok:
                nb.parent = new_armature.data.edit_bones[itemb.parent.name]
            else:
                nb.parent = None
        else:
            nb.parent = None
        nb.use_connect = itemb.use_connect
#    drivers_data = new_armature.animation_data.drivers
    print("Creating constraints...")
    bpy.context.view_layer.objects.active = new_armature
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.context.view_layer.objects.active = orig_armature
    bpy.ops.object.mode_set(mode='OBJECT')
    for b in new_armature.pose.bones:
        print(b.name)
        c = b.constraints.new(type='COPY_TRANSFORMS')
        c.target = orig_armature
        c.subtarget = b.name
    for obj in bpy.data.objects:
        if obj.parent == orig_armature:
            obj.parent = new_armature
            for mod in obj.modifiers:
                if mod.type == 'ARMATURE':
                    mod.object = new_armature
    print("Baking actions...")
    bpy.context.view_layer.objects.active = new_armature
    bpy.ops.object.mode_set(mode='POSE')
    for track in orig_armature.animation_data.nla_tracks:
        print(track.name)
        for s in track.strips:
            action = s.action
            print(action.name)
            orig_armature.animation_data.action = action
            new_armature.animation_data.action = None
            bpy.context.view_layer.objects.active = new_armature
            firstFrame = int(s.action_frame_start)
            lastFrame = int(s.action_frame_end)
            bpy.ops.nla.bake(frame_start=firstFrame, frame_end=lastFrame, step=5, only_selected=False, visual_keying=True, clear_constraints=False, clean_curves=True, use_current_action=False, bake_types={'POSE'})
            aname = orig_armature.animation_data.action.name
            orig_armature.animation_data.action.name = "bake_" + aname
            new_armature.animation_data.action.name = aname
            track = new_armature.animation_data.nla_tracks.new()
            track.name = aname
            track.strips.new(track.name, int(new_armature.animation_data.action.frame_range[0]), new_armature.animation_data.action)
            track.mute = True
            track.lock = True
    print("Removing constraints...")
    for b in new_armature.pose.bones:
        for c in b.constraints:
            b.constraints.remove(c)
    new_armature.animation_data.action = bpy.data.actions[mapping.default_action]
    bpy.context.view_layer.objects.active = new_armature
    bpy.ops.object.mode_set(mode='OBJECT')


obj_data = {}
for mapping in [ExportMappingFemale(), ExportMappingMale()]:
    print("Initializing...")
    bpy.ops.wm.read_homefile(use_empty=True)
    print("Preparing driver setup...")
    bpy.app.driver_namespace["clamp_angle_deg"] = clamp_angle_deg
    bpy.app.driver_namespace["angle_to_linear"] = angle_to_linear
    bpy.app.driver_namespace["extra_linear"] = extra_linear
    bpy.app.driver_namespace["angle_to_linear_x"] = angle_to_linear_x
    print("Driver setup done...")

    bpy.ops.wm.append(
        filepath=os.path.join(mapping.char_blend_path, mapping.inner_path),
        directory=os.path.join(mapping.char_blend_path, mapping.inner_path),
        files=mapping.files)
    for obj in bpy.data.objects:
        if obj.name.startswith("bone-"):
            bpy.data.objects.remove(obj)
    print("Append character done...")
    prepare_armature(mapping)
    print("Armature done...")
    print("Removing original armature and actions...")
    orig_arm = bpy.data.objects[mapping.armature_name + '_orig']
    bpy.data.objects.remove(orig_arm)
    for act in bpy.data.actions:
        if act.name.startswith("bake_"):
            act.name = act.name + "-noimp"
    for act in bpy.data.actions:
        if act.name.startswith("bake_"):
            bpy.data.actions.remove(act)
    print("Removing original armature and actions done...")

    for filepath in mapping.cloth_blend_paths:
        with bpy.data.libraries.load(filepath) as (data_from, data_to):
            data_to.objects = [ob for ob in data_from.objects if not ob.endswith('noimp')]
        for obj in data_to.objects:
            if obj.type == 'MESH':
                bpy.context.scene.collection.objects.link(obj)
                obj.parent = bpy.data.objects[mapping.armature_name]
                arm_mod = obj.modifiers.new('armature', 'ARMATURE')
                arm_mod.object = bpy.data.objects[mapping.armature_name]
                for vg in obj.vertex_groups:
                    obj.vertex_groups.remove(vg)
            bpy.ops.object.select_all(action='DESELECT')
            bpy.data.objects[obj.name].select_set(True)
            src_name = "body"
            params = ""
            otype = "nearest"
            distance = 0.1
            if "src_obj" in obj:
                obname = obj["src_obj"]
                if obname.find(";") >= 0:
                    src_name, params = obname.split(";");
                    vpar, vdist = params.split("=")
                    otype = vpar
                    distance = float(vdist)
                else:
                    src_name = obname
                if src_name not in bpy.data.objects:
                    src_name = "ref-" + src_name + "-noimp"
            keywords = []
            keywords.append(mapping.sex)
            if "slot" in obj:
                keywords.append(obj["slot"])
            if "keywords" in obj:
                kw = obj["keywords"].split(",")
                for xtk in kw:
                    keywords.append(xtk.strip())
            if "state" in obj:
                keywords.append(obj["state"].strip())
            else:
                if obj.name.endswith("-damaged"):
                    keywords.append("damaged")
                elif obj.name.endswith("-revealing"):
                    keywords.append("revealing")
                else:
                    keywords.append("normal")
            if "speciality" in obj:
                keywords.append(obj["speciality"].strip())
            else:
                keywords.append("common")
            keywords.append(obj.name.replace("-damaged", "").replace("-revealing", ""))
            obj_data[obj.name] = {}
            obj_data[obj.name]["keywords"] = keywords
            sobj = src_name
            vert_mapping = "NEAREST"
            use_distance = True
            bpy.data.objects[src_name].select_set(True)
            bpy.context.view_layer.objects.active = bpy.data.objects[obj.name]
            bpy.ops.paint.weight_paint_toggle()
            if otype == "nearest":
                vert_mapping = "NEAREST"
            elif otype == "poly":
                vert_mapping = "POLYINTERP_NEAREST"
            if distance <= 0.0001:
                use_distance = False
            bpy.ops.object.data_transfer(use_reverse_transfer=True,
                    data_type='VGROUP_WEIGHTS', use_create=True,
                    vert_mapping = vert_mapping,
                    layers_select_src='NAME',
                    max_distance = distance,
                    use_max_distance = use_distance,
                    layers_select_dst='ALL')
            bpy.ops.paint.weight_paint_toggle()
    print("Append clothes done...")
    for obj in bpy.data.objects:
        if obj.name.endswith("noimp"):
            bpy.data.objects.remove(obj)
        elif obj.name == "body":
            obj.name = "body-noimp"
        else:
            bpy.context.view_layer.objects.active = obj
            for modifier in obj.modifiers:
                if modifier.type != 'ARMATURE':
                    bpy.ops.object.modifier_apply(modifier = modifier.name)


#    print("Removing original armature and actions...")
#    orig_arm = bpy.data.objects[mapping.armature_name + '_orig']
#    bpy.data.objects.remove(orig_arm)
#    for act in bpy.data.actions:
#        if act.name.startswith("bake_"):
#            bpy.data.actions.remove(act)
#    for obj in bpy.data.objects:
#        if obj.type == 'MESH':
#            if not obj.name in mapping.objs and obj.parent is None:
#                if not obj.name.endswith("-noimp"):
#                    obj.name = obj.name + "-noimp"
    for obj in bpy.data.objects:
        if obj.name in mapping.objs_remove:
            bpy.data.objects.remove(obj)

    bpy.ops.wm.save_as_mainfile(filepath=(basepath + "/assets/blender/scripts/" + mapping.outfile))

    bpy.ops.export_scene.gltf(filepath=mapping.gltf_path,
            use_selection=False,
            check_existing=False,
            export_format='GLTF_SEPARATE',
            export_texture_dir='textures', export_texcoords=True,
            export_normals=True,
            export_tangents=False,
            export_materials='EXPORT',
            export_colors=True,
            use_mesh_edges=False,
            use_mesh_vertices=False,
            export_cameras=False,
            use_visible=False,
            use_renderable=False,
            export_yup=True,
            export_animations=False,
            export_force_sampling=True,
            export_def_bones=False,
            export_current_frame=False,
            export_morph=True,
            export_morph_normal=True,
            export_lights=False)

with open(basepath + "/godot/clothes/clothes.json", "w") as write_file:
    json.dump(obj_data, write_file, indent=8)

bpy.ops.wm.read_homefile(use_empty=True)
time.sleep(2)
bpy.ops.wm.quit_blender()
