import bpy
import bpy_extras.object_utils
import mathutils
import numpy as np
from ..cAPI import SimulationAPI
from . import createMeshBVH
from . import createTransform

def createMeshConstraintObject(sim_obj, mesh_obj, name, bond_indices, bond_points, bond_face_ids, bond_barycentric_weights_on_mesh, bond_local_coord, operator = None):
    pointcloud = bpy.data.meshes.new(name + "Mesh")
    n_points = len(bond_points)
    pointcloud.from_pydata(bond_points, [], []) # Create a mesh with only verts

    obj_new = bpy.data.objects.new(name, pointcloud)
    obj_new.hi_phy.is_active = True
    obj_new.hi_phy.object_type = "CONSTRAINT"
    # Use the original object. We will evaluate it when we need it
    obj_new.hi_phy.constraint_object0 = sim_obj.original
    obj_new.hi_phy.constraint_object1 = mesh_obj.original
    obj_new.hi_phy.constraint_type = SimulationAPI.ConstraintType.MeshConstraint
    obj_new.hi_phy.solver = sim_obj.hi_phy.solver

    bpy.context.collection.objects.link(obj_new)

    weight_data = [1, 0, 0] * n_points
    zero_data = [0] * n_points
    zero_data_3 = [0, 0, 0] * n_points

    # Do not create multiple attributes at the same time
    # It will make the previous handle invalid
    index0_0_attr = pointcloud.attributes.new("hi_phy_constraint_point_indices0_0", 'INT', 'POINT')
    index0_0_attr.data.foreach_set('value', bond_indices);

    index0_1_attr = pointcloud.attributes.new("hi_phy_constraint_point_indices0_1", 'INT', 'POINT')
    index0_1_attr.data.foreach_set('value', bond_indices);

    index0_2_attr = pointcloud.attributes.new("hi_phy_constraint_point_indices0_2", 'INT', 'POINT')
    index0_2_attr.data.foreach_set('value', bond_indices);

    # Use this to store face id
    index1_0_attr = pointcloud.attributes.new("hi_phy_constraint_point_indices1_0", 'INT', 'POINT')
    index1_0_attr.data.foreach_set('value', bond_face_ids);

    index1_1_attr = pointcloud.attributes.new("hi_phy_constraint_point_indices1_1", 'INT', 'POINT')
    index1_1_attr.data.foreach_set('value', zero_data);

    index1_2_attr = pointcloud.attributes.new("hi_phy_constraint_point_indices1_2", 'INT', 'POINT')
    index1_2_attr.data.foreach_set('value', zero_data);

    weight0_attr = pointcloud.attributes.new("hi_phy_constraint_weight0", 'FLOAT_VECTOR', 'POINT')
    weight0_attr.data.foreach_set('vector', weight_data);

    # Use this to store barycentric weights
    weight1_attr = pointcloud.attributes.new("hi_phy_constraint_weight1", 'FLOAT_VECTOR', 'POINT')
    weight1_attr.data.foreach_set('vector', bond_barycentric_weights_on_mesh);

    # rest length is still zero (zero distance to the bond point)
    rest_length_attr = pointcloud.attributes.new("hi_phy_constraint_rest_length", 'FLOAT', 'POINT')
    rest_length_attr.data.foreach_set('value', zero_data);

    local_coord_attr = pointcloud.attributes.new("hi_phy_constraint_local_coord", 'FLOAT_VECTOR', 'POINT')
    local_coord_attr.data.foreach_set('vector', bond_local_coord);

    positions_np = np.array(bond_points, dtype=np.float32)
    positions0_attr = pointcloud.attributes.new("hi_phy_constraint_point_positions_0", 'FLOAT_VECTOR', 'POINT')
    positions0_attr.data.foreach_set('vector', np.ravel(positions_np));

    positions1_attr = pointcloud.attributes.new("hi_phy_constraint_point_positions_1", 'FLOAT_VECTOR', 'POINT')
    positions1_attr.data.foreach_set('vector', np.ravel(positions_np));

    pointcloud.update()
    return True

# we assume the verts are already part of the object
# and the object passed in has already been evaluated
def create(sim_obj, mesh_obj, verts, selection_radius, name, operator = None):
    if not sim_obj.hi_phy:
        if operator:
            operator.report({"ERROR"}, sim_obj.name + " is not a valid hi phy object")
        return False

    # get mesh topology
    if (not mesh_obj.type == 'MESH') :
        if operator:
            operator.report({"ERROR"}, "Binding mesh " + mesh_obj.name + " is not a triangle mesh")
        return False
    faces = mesh_obj.data.polygons
    n_faces = len(faces);
    # First check it is a triangle mesh
    for f in range(0, n_faces):
        if (not len(faces[f].vertices) == 3) :
            if operator:
                operator.report({"ERROR"}, "Binding mesh " + mesh_obj.name + " is not a triangle mesh")
            return False
    # build bvh in world space (because radius is in world unit)
    mesh_bvh = createMeshBVH(mesh_obj, mesh_obj.matrix_world, None, operator)
    if (not mesh_bvh) :
        if operator:
            operator.report({"ERROR"}, "Failed to build BVH for " + mesh_obj.name)
        return False

    # create the testing points
    n_points = sim_obj.data.attributes.domain_size('POINT')
    points_data = np.zeros((n_points, 3), dtype=np.float32)
    sim_obj.data.attributes['position'].data.foreach_get('vector', np.ravel(points_data))
    points = SimulationAPI.CreateVector3Array(n_points)
    SimulationAPI.CopyVector3Array(points, points_data)
    # Allocate data
    found_proximity = SimulationAPI.CreateBoolArray(n_points)
    bind_points_on_mesh = SimulationAPI.CreateVector3Array(n_points)
    bind_face_ids = SimulationAPI.CreateIntArray(n_points)
    bind_barycentric_weights_on_mesh = SimulationAPI.CreateVector3Array(n_points)
    bind_local_coord = SimulationAPI.CreateVector3Array(n_points)
    # Copy transforms
    mesh_transform = createTransform(mesh_obj.matrix_world)
    points_transform = createTransform(sim_obj.matrix_world)
    # Binding
    if not SimulationAPI.Utils.BindPointsToMesh(points, mesh_bvh, found_proximity, bind_points_on_mesh, bind_face_ids, bind_barycentric_weights_on_mesh, bind_local_coord, points_transform, mesh_transform, selection_radius):
        if operator:
            operator.report({"ERROR"}, "Binding has failed")
            return False
    bond_indices = [index for index, value in enumerate(found_proximity) if value == 1]
    if (len(bond_indices) == 0):
        if operator:
            operator.report({"ERROR"}, "Nothing was bond")
            return False
    bond_points = [points[i] for i in bond_indices]
    # bond_points_on_mesh = [bind_points_on_mesh[i] for i in bond_indices]
    bond_face_ids = [bind_face_ids[i] for i in bond_indices]
    bond_barycentric_weights_on_mesh = [bind_barycentric_weights_on_mesh[i][v] for i in bond_indices for v in range(3)]
    bond_local_coord = [bind_local_coord[i][v] for i in bond_indices for v in range(3)]
    # The bond points are in sim object space
    matrix_world = sim_obj.matrix_world;
    vs_world = [matrix_world @ mathutils.Vector((v[0], v[1], v[2])) for v in bond_points]

    if (not createMeshConstraintObject(sim_obj, mesh_obj, name, bond_indices, vs_world, bond_face_ids, bond_barycentric_weights_on_mesh, bond_local_coord, operator)):
        return False
    return True;
