import bpy
import bmesh
import os
from ..utils import compute_curves_normal
from ..utils import exporter
from ..utils import createTransform
from ..utils import motion_mapper_data_conversion
from .. import utils
from ..cAPI import SimulationAPI
import numpy as np

class HI_PHY_OT_MotionMapperCurvesBinding(bpy.types.Operator):
    bl_idname = "hi_phy.motion_mapper_curves_binding"
    bl_label = "Hi Phy Motion Mapper Curves Binding"
    bl_options = {'REGISTER', 'UNDO_GROUPED'}
    motion_mapper : bpy.props.StringProperty(
        name='Motion Mapper'
    )
    guide : bpy.props.StringProperty(
        name='Guide Curves'
    )
    driven : bpy.props.StringProperty(
        name='Driven Curves'
    )
    use_id : bpy.props.BoolProperty(
        name='Use ID'
    )
    use_frame : bpy.props.BoolProperty(
        name='Use Frame'
    )
    id_attribute : bpy.props.StringProperty(
        name='ID Attribute'
    )
    frame_attribute : bpy.props.StringProperty(
        name='Frame Attribute'
    )
    use_base_normal_twist : bpy.props.BoolProperty(
        name='Use Base Normal Twist'
    )
    base_normal_attribute : bpy.props.StringProperty(
        name='Base Normal Attribute'
    )
    twist_attribute : bpy.props.StringProperty(
        name='Twist Attribute'
    )
    number_of_guides : bpy.props.IntProperty(
        name='Number of Guides'
    )
    binding_radius : bpy.props.FloatProperty(
        name='Binding Radius'
    )
    def execute(self, context):
        if (not self.guide in context.scene.objects):
            self.report({"ERROR"}, "No valid guide shape to bind")
            return {'CANCELLED'}
        if (not self.driven in context.scene.objects):
            self.report({"ERROR"}, "No valid driven shape to bind")
            return {'CANCELLED'}
        depsgraph = bpy.context.evaluated_depsgraph_get()
        # Guide is evaluated
        guide_orig = context.scene.objects[self.guide]
        guide = guide_orig.evaluated_get(depsgraph)
        if (guide.type != 'CURVES'):
            self.report({"ERROR"}, "Guide shape is not a CURVES object")
            return {'CANCELLED'}
        # Driven is not evaluated
        driven = context.scene.objects[self.driven]
        if (driven.type != 'CURVES'):
            self.report({"ERROR"}, "Driven shape is not a CURVES object")
            return {'CANCELLED'}
        motion_mapper_object = context.scene.objects[self.motion_mapper];
        motion_mapper_object.hi_phy.motion_mapper_is_bond = False;
        motion_mapper = SimulationAPI.CurvesMotionMapper();
        utils.curves_motion_mappers[self.motion_mapper] = motion_mapper;
        # Get the topology of guide and driven
        # guide
        n_guide_curves = len(guide.data.curves)
        guide_offset_data = np.zeros(n_guide_curves + 1, dtype=np.int32)
        # blender stores the first offset as 0 there are totally n_curves + 1 offsets
        for i in range(n_guide_curves + 1):
            guide_offset_data[i] = guide.data.curve_offset_data[i].value
        n_guide_cvs = guide_offset_data[-1]
        motion_mapper.SetGuideCurvesStartCV(guide_offset_data)
        # driven
        n_driven_curves = len(driven.data.curves)
        driven_offset_data = np.zeros(n_driven_curves + 1, dtype=np.int32)
        # blender stores the first offset as 0 there are totally n_curves + 1 offsets
        for i in range(n_driven_curves + 1):
            driven_offset_data[i] = driven.data.curve_offset_data[i].value
        n_driven_cvs = driven_offset_data[-1]
        motion_mapper.SetDrivenCurvesStartCV(driven_offset_data)
        # get transforms
        guide_transform = createTransform(guide.matrix_world)
        driven_transform = createTransform(driven.matrix_world)
        driven_to_guide = guide_transform.Inverse() * driven_transform
        identity_transform = SimulationAPI.TransformMatrix();
        # set position
        # guide
        guide_position_data = np.zeros((n_guide_cvs, 3), dtype=np.float32)
        guide_positions = guide.data.attributes['position']
        guide_positions.data.foreach_get('vector', np.ravel(guide_position_data))
        if (not motion_mapper.SetGuidePosition(guide_position_data, identity_transform)):
            self.report({"ERROR"}, "Failed to set guide id")
            return {'CANCELLED'}
        # driven
        driven_position_data = np.zeros((n_driven_cvs, 3), dtype=np.float32)
        driven_positions = driven.data.attributes['position']
        driven_positions.data.foreach_get('vector', np.ravel(driven_position_data))
        if (not motion_mapper.SetDrivenPosition(driven_position_data, driven_to_guide)):
            self.report({"ERROR"}, "Failed to set driven id")
            return {'CANCELLED'}
        # id
        if (self.use_id):
            if ((not self.id_attribute in guide.data.attributes) or (not self.id_attribute in driven.data.attributes)):
                self.report({"ERROR"}, "Guide or Driven shape is not the " + self.id_attribute + " attribute, requested as guide id")
                return {'CANCELLED'}
            # guide
            guide_id_data = np.zeros(n_guide_curves, dtype=np.int32)
            guide_ids = guide.data.attributes[self.id_attribute]
            if (n_guide_curves != guide.data.attributes.domain_size(guide_ids.domain)):
                self.report({"ERROR"}, "The ID attribute for guide shape is not a per curve attribute")
                return {'CANCELLED'}
            guide_ids.data.foreach_get('value', guide_id_data)
            if (not motion_mapper.SetGuideID(guide_id_data)):
                self.report({"ERROR"}, "Failed to set guide id")
                return {'CANCELLED'}
            # driven
            driven_id_data = np.zeros(n_driven_curves, dtype=np.int32)
            driven_ids = driven.data.attributes[self.id_attribute]
            if (n_driven_curves != driven.data.attributes.domain_size(driven_ids.domain)):
                self.report({"ERROR"}, "The ID attribute for driven shape is not a per curve attribute")
                return {'CANCELLED'}
            driven_ids.data.foreach_get('value', driven_id_data)
            if (not motion_mapper.SetDrivenID(driven_id_data)):
                self.report({"ERROR"}, "Failed to set driven id")
                return {'CANCELLED'}
        else:
            motion_mapper.ResetGuideID()
            motion_mapper.ResetDrivenID()
        # normal
        if (self.use_frame):
            if (self.use_base_normal_twist):
                guide_normals = compute_curves_normal.Compute(guide, self.base_normal_attribute, self.twist_attribute)
                if (not motion_mapper.SetGuideNormalRaw(guide_normals, identity_transform)):
                    self.report({"ERROR"}, "Failed to set guide normal")
                    return {'CANCELLED'}
            else:
                if (not self.frame_attribute in guide.data.attributes):
                    self.report({"ERROR"}, "Guide shape does not the " + self.frame_attribute + " attribute, requested as frame normal")
                    return {'CANCELLED'}
                guide_normals = guide.data.attributes[self.frame_attribute]
                if (n_guide_cvs != guide.data.attributes.domain_size(guide_normals.domain)):
                    self.report({"ERROR"}, "The frame attribute for guide shape is not a per point attribute")
                    return {'CANCELLED'}
                guide_normal_data = np.zeros((n_guide_cvs, 3), dtype=np.float32)
                guide_normals.data.foreach_get('vector', np.ravel(guide_normal_data))
                if (not motion_mapper.SetGuideNormal(guide_normal_data, identity_transform)):
                    self.report({"ERROR"}, "Failed to set guide normal")
                    return {'CANCELLED'}
        if (not motion_mapper.Bind(self.number_of_guides, self.binding_radius, self.use_frame)):
            self.report({"ERROR"}, "Curves binding has failed")
            return {'CANCELLED'}
        if (not motion_mapper_data_conversion.storeCurvesMotionMapperDataToBlender(motion_mapper_object, motion_mapper)):
            self.report({"ERROR"}, "Saving binding data to blender has failed")
            return {'CANCELLED'}
        return {'FINISHED'}

__CLASSES__ = [
    HI_PHY_OT_MotionMapperCurvesBinding
]

def register():
    for class_ in __CLASSES__:
        bpy.utils.register_class(class_)


def unregister():
    for class_ in reversed(__CLASSES__):
        bpy.utils.unregister_class(class_)
