import bpy
import numpy as np
import ctypes
import pathlib
import addon_utils
import bpy
from ..cAPI import SimulationAPI
from . import createTransform

def ValidMapAttribute(attribute, obj):
    # check if an attribute
    #  1. exists
    #  2. we can't assume the map is point, for binding constraints, the attribute can be edge attribute
    if not attribute in obj.data.attributes:
        return False
    attr = obj.data.attributes[attribute]
    if not attr:
        return False
    return True

def GetMPMMaterial(mpm_material):
    if (mpm_material == "SNOW"):
        return SimulationAPI.MPMMaterialType.Snow
    if (mpm_material == "SAND"):
        return SimulationAPI.MPMMaterialType.Sand

def GetTag(type):
    if (type == "AFFINE_BODY"):
        return SimulationAPI.Property.Tag.AffineBody;
    elif (type == "CLOTH"):
        return SimulationAPI.Property.Tag.Cloth;
    elif (type == "ELASTIC_ROD"):
        return SimulationAPI.Property.Tag.ElasticRod;
    elif (type == "DEFORMABLE_BODY"):
        return SimulationAPI.Property.Tag.DeformableBody;
    elif (type == "SOFT_COLLIDER"):
        return SimulationAPI.Property.Tag.SoftCollider
    elif (type == "MPM_MESHLESS_PARTICLES"):
        return SimulationAPI.Property.Tag.MPMMeshlessParticles;
    elif (type == "COLLIDER"):
        return SimulationAPI.Property.Tag.Collider;
    elif (type == "LAGRANGIAN_SOLVER"):
        return SimulationAPI.Property.Tag.LagrangianSolver;
    elif (type == "MPM_SOLVER"):
        return SimulationAPI.Property.Tag.MPMSolver;
    elif (type == "CONSTRAINT"):
        return SimulationAPI.Property.Tag.Constraint;
    elif (type == "FORCE_FIELD"):
        return SimulationAPI.Property.Tag.ForceField;
    return None

def GetSimType(type):
    if (type == "AFFINE_BODY"):
        return SimulationAPI.SimulationObjectType.AffineBody
    elif (type == "CLOTH"):
        return SimulationAPI.SimulationObjectType.Cloth
    elif (type == "ELASTIC_ROD"):
        return SimulationAPI.SimulationObjectType.ElasticRod
    elif (type == "DEFORMABLE_BODY"):
        return SimulationAPI.SimulationObjectType.DeformableBody
    elif (type == "SOFT_COLLIDER"):
        return SimulationAPI.SimulationObjectType.SoftCollider
    elif (type == "MPM_MESHLESS_PARTICLES"):
        return SimulationAPI.SimulationObjectType.MPMMeshlessParticles;
    elif (type == "COLLIDER"):
        return SimulationAPI.SimulationObjectType.Collider
    return None

def GetGeometryType(type):
    if (type == "ELASTIC_ROD"):
        return SimulationAPI.GeometryType.Curves
    elif (type == "MPM_MESHLESS_PARTICLES"):
        return SimulationAPI.GeometryType.Points
    elif (type == "LAGRANGIAN_SOLVER" or type == "MPM_SOLVER" or type == "CONSTRAINT" or type == "FORCE_FIELD"):
        return None
    else:
        return SimulationAPI.GeometryType.Mesh

class Cache:
    objs = [] # List of nodes matching the current scene objects
    scene = None
    driver = None
    def __init__(self):
        self.objs = []
        self.scene = None
        self.driver = None
    def __del__(self):
        self.objs = []
        self.scene = None
        self.driver = None
    def Clear(self):
        self.objs = []
        self.scene = None
        self.driver = None
    def GetProgress(self):
        if (self.scene):
            return [self.scene.simulation_end_time - self.scene.start_time, self.scene.end_time - self.scene.start_time];
        return [0, 0];
    def GetStartFrame(self):
        if self.scene:
            return self.scene.start_time
        else:
            return 0
    def SetToStartFrame(self):
        if self.scene:
            start_frame = self.scene.start_time
            bpy.context.scene.frame_set(int(start_frame))
    def IsSimulationRunning(self):
        if self.driver:
            return self.driver.IsRunning()
        return False
    def SetScene(self, scene):
        self.scene = scene
        nodes = scene.GetNodes()
        for n in nodes.GetNames():
            node = nodes[n]
            material = node.GetStaticEnumValue("material")
            if (material == SimulationAPI.SimulationObjectType.Invalid or material == SimulationAPI.SimulationObjectType.Collider):
                # it is not a simulation object. Skip
                continue;
            obj = bpy.data.objects.get(node.name)
            if obj and obj.hi_phy.is_active:
                self.objs.append(node);

    def SetDriver(self, driver):
        self.driver = driver

def ExportBlenderIntAttribute(blender_attr_name, hi_phy_prop, blender_obj, node, time = None):
    blender_attr = blender_obj.data.attributes[blender_attr_name]
    if not blender_attr:
        return False
    n = blender_obj.data.attributes.domain_size(blender_attr.domain)
    values = [0] * n
    blender_attr.data.foreach_get('value', values)
    data_array = SimulationAPI.IntArray(values)
    hi_phi_attr = node.GetOrCreateAttribute(hi_phy_prop);
    hi_phi_attr.SetDataArrayType(SimulationAPI.Attribute.DataArrayType.PerVertex);

    if (time):
        hi_phi_attr.SetIntArrayValue(data_array, time)
    else:
        hi_phi_attr.SetIntArrayValue(data_array)

    return True

def ExportBlenderFloatAttribute(blender_attr_name, hi_phy_prop, blender_obj, node, time = None):
    blender_attr = blender_obj.data.attributes[blender_attr_name]
    if not blender_attr:
        return False
    n = blender_obj.data.attributes.domain_size(blender_attr.domain)
    values = [0] * n
    blender_attr.data.foreach_get('value', values)
    data_array = SimulationAPI.FloatArray(values)
    hi_phi_attr = node.GetOrCreateAttribute(hi_phy_prop);
    hi_phi_attr.SetDataArrayType(SimulationAPI.Attribute.DataArrayType.PerVertex);

    if (time):
        hi_phi_attr.SetFloatArrayValue(data_array, time)
    else:
        hi_phi_attr.SetFloatArrayValue(data_array)

    return True

def ExportBlenderVector3Attribute(blender_attr_name, hi_phy_prop, blender_obj, node, time = None):
    blender_attr = blender_obj.data.attributes[blender_attr_name]
    if not blender_attr:
        return False
    n = blender_obj.data.attributes.domain_size(blender_attr.domain)
    values = np.zeros((n, 3), dtype=np.float32)
    blender_attr.data.foreach_get('vector', np.ravel(values))
    data_array = SimulationAPI.CreateVector3Array(n)
    SimulationAPI.CopyVector3Array(data_array, values)
    hi_phi_attr = node.GetOrCreateAttribute(hi_phy_prop);
    hi_phi_attr.SetDataArrayType(SimulationAPI.Attribute.DataArrayType.PerVertex);

    if (time):
        hi_phi_attr.SetVector3ArrayValue(data_array, time)
    else:
        hi_phi_attr.SetVector3ArrayValue(data_array)

    return True

def ExportBlenderInt3Attribute(blender_attr0_name, blender_attr1_name, blender_attr2_name, hi_phy_prop, blender_obj, node, time = None):
    blender_attr0 = blender_obj.data.attributes[blender_attr0_name]
    if not blender_attr0:
        return False
    n = blender_obj.data.attributes.domain_size(blender_attr0.domain)
    values0 = [0] * n
    blender_attr0.data.foreach_get('value', values0)

    blender_attr1 = blender_obj.data.attributes[blender_attr1_name]
    if not blender_attr1:
        return False
    values1 = [0] * n
    blender_attr1.data.foreach_get('value', values1)

    blender_attr2 = blender_obj.data.attributes[blender_attr2_name]
    if not blender_attr2:
        return False
    values2 = [0] * n
    blender_attr2.data.foreach_get('value', values2)

    values = [values0, values1, values2]
    data_array = SimulationAPI.IntArray([values[v][i] for i in range(n) for v in range(len(values))])
    hi_phi_attr = node.GetOrCreateAttribute(hi_phy_prop);
    hi_phi_attr.SetDataArrayType(SimulationAPI.Attribute.DataArrayType.PerVertex);

    if (time):
        hi_phi_attr.SetIntArrayValue(data_array, time)
    else:
        hi_phi_attr.SetIntArrayValue(data_array)

    return True

def ExportClothUV(obj, node, frame) :
    # If cloth is the spring model, we don't need to export UV
    if (obj.hi_phy.cloth_material == 'UVPANEL'):
        if (not ExportBlenderVector3Attribute("hi_phy_cloth_uv",
                                              "clothUV", obj, node, frame)):
            return False
    return True

def ExportForceField(scene, parent_node, force_field, name) :
    node = scene.CreateNode(name, SimulationAPI.Node.Type.ForceField, parent_node)
    influenced_objects = node.GetOrCreateAttribute("ForceFieldInfluencedObjects");
    object_list = force_field.hi_phy.force_field_object_list
    object_list_data = SimulationAPI.StringArray()
    for o in object_list:
        if (not o.object.hi_phy.is_active or o.object.hi_phy.solver.name != force_field.hi_phy.solver.name):
            return None
        object_list_data.append(o.object.name)

    influenced_objects.SetStringArrayStaticValue(object_list_data)
    # Expression type
    expression_type = node.GetOrCreateAttribute("ForceFieldExpressionType");
    expression_type.SetEnumValue(SimulationAPI.ExpressionType.SeExpr)

    return node

def ExportForceFieldWeights(force_field, node, frame) :
    weights = node.GetOrCreateAttribute("ForceFieldInfluencedObjectWeights");
    object_list = force_field.hi_phy.force_field_object_list
    object_weight_data = SimulationAPI.FloatArray()
    for o in object_list:
        object_weight_data.append(o.weight)

    weights.SetFloatArrayValue(object_weight_data, frame)

def ExportConstraint(scene, parent_node, constraint, name) :
    constraint_node = scene.CreateNode(name, SimulationAPI.Node.Type.Constraint, parent_node)
    constraint_type = constraint_node.GetOrCreateAttribute("constraintType");
    constraint_type.SetEnumValue(constraint.hi_phy.constraint_type);

    constraint_object0 = constraint_node.GetOrCreateAttribute("constraintObject0");
    constraint_object0.SetStringValue(constraint.hi_phy.constraint_object0.name);

    constraint_object1 = constraint_node.GetOrCreateAttribute("constraintObject1");
    constraint_object1.SetStringValue(constraint.hi_phy.constraint_object1.name);
    return constraint_node

def ExportConstraintProperties(constraint_node, constraint, time = None) :
    if (not ExportBlenderInt3Attribute("hi_phy_constraint_point_indices0_0",
                                       "hi_phy_constraint_point_indices0_1",
                                       "hi_phy_constraint_point_indices0_2",
                                       "constraintVerts0", constraint, constraint_node, time)):
        return False
    if (not ExportBlenderInt3Attribute("hi_phy_constraint_point_indices1_0",
                                       "hi_phy_constraint_point_indices1_1",
                                       "hi_phy_constraint_point_indices1_2",
                                       "constraintVerts1", constraint, constraint_node, time)):
        return False
    if (not ExportBlenderVector3Attribute("hi_phy_constraint_weight0",
                                          "constraintWeights0", constraint, constraint_node, time)):
        return False
    if (not ExportBlenderVector3Attribute("hi_phy_constraint_weight1",
                                          "constraintWeights1", constraint, constraint_node, time)):
        return False
    if (not ExportBlenderFloatAttribute("hi_phy_constraint_rest_length",
                                        "constraintRestLength", constraint, constraint_node, time)):
        return False
    if (not ExportBlenderVector3Attribute("hi_phy_constraint_point_positions_0",
                                          "constraintBindingPosition0", constraint, constraint_node, time)):
        return False
    if (not ExportBlenderVector3Attribute("hi_phy_constraint_point_positions_1",
                                          "constraintBindingPosition1", constraint, constraint_node, time)):
        return False
    return True

def ExportCurvesTopology(scene, parent_node, curves, name) :
    # Blender has a new CURVES system and a old CURVE system, we want to support both
    if ((not curves.type == 'CURVES') and (not curves.type == 'CURVE')) :
        return None
    if (curves.type == 'CURVES'):
        n_curves = len(curves.data.curves)
        vert_counts = SimulationAPI.CreateIntArray(n_curves)
        offset = 0
        # blender stores the first offset as 0 there are totally n_curves + 1 offsets
        for i in range(n_curves):
            next_offset = curves.data.curve_offset_data[i + 1].value
            vert_counts[i] = next_offset - offset;
            offset = next_offset
        geo_node = scene.CreateNode(name, SimulationAPI.Node.Type.Geometry, parent_node)
        num_verts_per_prim = geo_node.GetOrCreateAttribute("numVertsPerPrim");
        num_verts_per_prim.SetIntArrayValue(vert_counts);
        return geo_node
    if (curves.type == 'CURVE'):
        # TODO
        return None

def ExportCurvesPosition(curves, node, time, position_attribute_name = 'position', normal_attribute_name = 'frameNormal') :
    obj_transform = createTransform(curves.matrix_world)
    transform_attr = node.GetOrCreateAttribute("transform")
    node_transform = transform_attr.GetTransformValue(time)
    obj_to_node = node_transform.Inverse() * obj_transform;
    # TODO: use a buffer instead of creating a new array everytime
    if (curves.type == 'CURVES'):
        n_curves = len(curves.data.curves)
        n_verts = curves.data.attributes.domain_size('POINT')
        positions = curves.data.attributes['position']
        positions_data = np.zeros((n_verts, 3), dtype=np.float32)
        positions.data.foreach_get('vector', np.ravel(positions_data))
        verts = SimulationAPI.CreateVector3Array(n_verts)
        SimulationAPI.CopyVector3Array(verts, positions_data)
        start_cvs = SimulationAPI.CreateIntArray(n_curves + 1)
        for i in range(n_curves + 1):
            start_cvs[i] = curves.data.curve_offset_data[i].value

        if(normal_attribute_name):
            base_normals_attr = curves.data.attributes['hi_phy_base_normal']
            base_normals_data = np.zeros((n_curves, 3), dtype=np.float32)
            base_normals_attr.data.foreach_get('vector', np.ravel(base_normals_data))
            base_normals = SimulationAPI.CreateVector3Array(n_curves)
            SimulationAPI.CopyVector3Array(base_normals, base_normals_data)
            twists = SimulationAPI.FloatArray([0] * n_verts)
    if (curves.type == 'CURVE'):
        return False;

    # compute normals
    if(normal_attribute_name):
        normals = SimulationAPI.CreateVector3Array(n_verts)
        SimulationAPI.Utils.ComputeNormal(start_cvs, twists, verts, base_normals, normals);

    # set position
    SimulationAPI.ApplyTransform(verts, obj_to_node, SimulationAPI.VectorType.Position)
    positions_attr = node.GetOrCreateAttribute(position_attribute_name);
    positions_attr.SetDataArrayType(SimulationAPI.Attribute.DataArrayType.PerVertex);
    positions_attr.SetVector3ArrayValue(verts, time);

    # set normals
    if(normal_attribute_name):
        SimulationAPI.ApplyTransform(normals, obj_to_node, SimulationAPI.VectorType.Normal)
        normals_attr = node.GetOrCreateAttribute(normal_attribute_name);
        normals_attr.SetDataArrayType(SimulationAPI.Attribute.DataArrayType.PerVertex);
        normals_attr.SetVector3ArrayValue(normals, time);

    return True

def ExportTriangleMeshTopology(scene, parent_node, triangle_mesh, name) :
    if (not triangle_mesh.type == 'MESH') :
        return None
    faces = triangle_mesh.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) :
            return None

    geo_node = scene.CreateNode(name, SimulationAPI.Node.Type.Geometry, parent_node)

    vert_counts = SimulationAPI.IntArray([3] * n_faces)
    tris = SimulationAPI.CreateIntArray(n_faces * 3)

    # Sigh, blender does not guerantee vertex ordering when export,
    # so we will have to manually check the face normals and fix the winding
    for f in range(0, n_faces):
        v1 = faces[f].vertices[0];
        v2 = faces[f].vertices[1];
        v3 = faces[f].vertices[2];
        co1 = triangle_mesh.data.vertices[v1].co;
        co2 = triangle_mesh.data.vertices[v2].co;
        co3 = triangle_mesh.data.vertices[v3].co;
        edge1 = co2 - co1
        edge2 = co3 - co1
        # Cross product and normalize
        normal = edge1.cross(edge2).normalized()
        if (normal.dot(faces[f].normal)):
            tris[f * 3 + 0] = v1;
            tris[f * 3 + 1] = v2;
            tris[f * 3 + 2] = v3;
        else:
            tris[f * 3 + 0] = v1;
            tris[f * 3 + 1] = v3;
            tris[f * 3 + 2] = v2;


    num_verts_per_prim = geo_node.GetOrCreateAttribute("numVertsPerPrim");
    verts_of_prim = geo_node.GetOrCreateAttribute("vertsOfPrim");
    num_verts_per_prim.SetIntArrayValue(vert_counts);
    verts_of_prim.SetIntArrayValue(tris);

    return geo_node

def ExportPointsTopology(scene, parent_node, points, name) :
    if (not points.type == 'MESH') :
        return None
    geo_node = scene.CreateNode(name, SimulationAPI.Node.Type.Geometry, parent_node)
    return geo_node

# We split the map object and prop obj, as the map can live on a different object (such as mesh bone properties)
def ExportProperty(p, tag, node, prop_obj, map_obj, op = None, time = 0):
    if ((not p.is_internal) and (p.tag.value & tag.value)):
        # export maps
        if (p.is_mappable and getattr(prop_obj.hi_phy, p.name + "_use_attribute")):
            attribute = getattr(prop_obj.hi_phy, p.name + "_attribute");
            if (not ValidMapAttribute(attribute, map_obj)):
                if (op):
                    op.report({"ERROR"}, attribute + " is not a valid attribute for property " + p.name)
                return False
            if (p.type == SimulationAPI.Property.Type.Invalid):
                # This should not happen
                return True;
            elif (p.type == SimulationAPI.Property.Type.Float):
                if (not ExportBlenderFloatAttribute(attribute, p.name, map_obj, node, time)):
                    if (op):
                        op.report({"ERROR"}, attribute + " is not a valid attribute for property " + p.name)
                    return False
            elif (p.type == SimulationAPI.Property.Type.Int):
                if (not ExportBlenderIntAttribute(attribute, p.name, map_obj, node, time)):
                    if (op):
                        op.report({"ERROR"}, attribute + " is not a valid attribute for property " + p.name)
                    return False
            elif (p.type == SimulationAPI.Property.Type.Bool):
                if (not ExportBlenderBoolAttribute(attribute, p.name, map_obj, node, time)):
                    if (op):
                        op.report({"ERROR"}, attribute + " is not a valid attribute for property " + p.name)
                    return False;
            elif (p.type == SimulationAPI.Property.Type.Vector3):
                if (not ExportBlenderVector3Attribute(attribute, p.name, map_obj, node, time)):
                    if (op):
                        op.report({"ERROR"}, attribute + " is not a valid attribute for property " + p.name)
                    return False;
            elif (p.type == SimulationAPI.Property.Type.String):
                if (not ExportBlenderStringttribute(attribute, p.name, map_obj, node, time)):
                    if (op):
                        op.report({"ERROR"}, attribute + " is not a valid attribute for property " + p.name)
                    return False;
        else:
            value = getattr(prop_obj.hi_phy, p.name);
            attr = node.GetOrCreateAttribute(p.name)
            attr.SetDataArrayType(SimulationAPI.Attribute.DataArrayType.PerObject);
            if (p.type == SimulationAPI.Property.Type.Invalid):
                # This should not happen
                return True;
            elif (p.type == SimulationAPI.Property.Type.Float):
                attr.SetFloatValue(value, time);
            elif (p.type == SimulationAPI.Property.Type.Int):
                attr.SetIntValue(value, time);
            elif (p.type == SimulationAPI.Property.Type.Vector3):
                v3 = SimulationAPI.Vector3();
                v3[0] = value[0]
                v3[1] = value[1]
                v3[2] = value[2]
                attr.SetVector3Value(v3, time);
            elif (p.type == SimulationAPI.Property.Type.String):
                attr.SetStringValue(value, time);
            elif (p.type == SimulationAPI.Property.Type.Bool):
                attr.SetBoolValue(value, time);
    return True;

def ExportDynamicAttributes(node, obj, time, op = None, exclusive_list = []) :
    tag = GetTag(obj.hi_phy.object_type)
    if (not obj.hi_phy.is_active):
        return False
    for n in SimulationAPI.properties:
        p = SimulationAPI.properties[n]
        if (p.name in exclusive_list) :
            continue;
        if (not p.is_static):
            ExportProperty(p, tag, node, obj, obj, op, time)
    return True

# It is designed that the property map, for some reason, lives on a different object (such as mesh bone for deformable body)
def ExportDynamicAttributesFromList(list, node, prop_obj, map_obj, time, op = None) :
    tag = GetTag(prop_obj.hi_phy.object_type)
    if (not prop_obj.hi_phy.is_active):
        return False
    for n in list:
        p = SimulationAPI.properties[n]
        if (not p.is_static):
            ExportProperty(p, tag, node, prop_obj, map_obj, op, time)
    return True

def ExportStaticAttributes(node, obj, op = None) :
    tag = GetTag(obj.hi_phy.object_type)
    sim_type = GetSimType(obj.hi_phy.object_type)
    geo_type = GetGeometryType(obj.hi_phy.object_type)
    if (not obj.hi_phy.is_active):
        return False
    for n in SimulationAPI.properties:
        p = SimulationAPI.properties[n]
        if (p.is_static):
            ExportProperty(p, tag, node, obj, obj, op)
    # Export material model and geometry type
    if (sim_type):
        material_attribute = node.GetOrCreateAttribute("material");
        material_attribute.SetEnumValue(sim_type);
    if (geo_type):
        geometry_attribute = node.GetOrCreateAttribute("geometryType");
        geometry_attribute.SetEnumValue(geo_type);
    if (obj.hi_phy.object_type == "MPM_MESHLESS_PARTICLES"):
        mpm_material_attribute = node.GetOrCreateAttribute("MPMMaterialType");
        mpm_material_attribute.SetEnumValue(GetMPMMaterial(obj.hi_phy.mpm_material));
    return True

def ExportSolver(scene, solver) :
    if (not solver.hi_phy.is_active) or ((not solver.hi_phy.object_type == 'LAGRANGIAN_SOLVER') and (not solver.hi_phy.object_type == 'MPM_SOLVER')):
        return None
    solver_node = scene.GetSolver();
    solver_node.Rename(solver.name)
    # Change this if we want restart sim
    scene.SetTimeRange(solver.hi_phy.frame_range_simulation_start, solver.hi_phy.frame_range_simulation_end)

    return solver_node

def ExportTransform(obj, node, time) :
    matrix_world = obj.matrix_world
    transform = createTransform(obj.matrix_world)
    transform_attr = node.GetOrCreateAttribute("transform")
    transform_attr.SetTransformValue(transform, time)

def ExportClothMaterial(obj, node, time) :
    cloth_material_attr = node.GetOrCreateAttribute("clothMaterialModel")
    if (obj.hi_phy.cloth_material == 'UVPANEL'):
        cloth_material_attr.SetEnumValue(SimulationAPI.ClothMaterial.UVPanel, time)
    if (obj.hi_phy.cloth_material == 'ELASTIC_MEMBRANE'):
        cloth_material_attr.SetEnumValue(SimulationAPI.ClothMaterial.ElasticMembrane, time)

def ExportDeformableBodyMaterial(obj, node, time) :
    deformable_body_material_attr = node.GetOrCreateAttribute("deformableBodyMaterialModel")
    if (obj.hi_phy.deformable_body_material == 'COROTATED_LINEAR_ELASTIC'):
        deformable_body_material_attr.SetEnumValue(SimulationAPI.DeformableBodyMaterial.CorotatedLinearElastic, time)
    if (obj.hi_phy.cloth_material == 'STABLE_NEOHOOKEAN'):
        deformable_body_material_attr.SetEnumValue(SimulationAPI.DeformableBodyMaterial.StableNeoHookean, time)

# Sometimes we want to export rest_shape, target shape on a sim shape
# But they might be under a different transform
# We assume node already has the transform exported, before calling the mesh position
def ExportMeshPosition(obj, node, time, attribute_name = 'position'):
    obj_transform = createTransform(obj.matrix_world)
    transform_attr = node.GetOrCreateAttribute("transform")
    node_transform = transform_attr.GetTransformValue(time)

    obj_to_node = node_transform.Inverse() * obj_transform;

    positions_attr = obj.data.attributes['position']
    n_verts = obj.data.attributes.domain_size('POINT')
    # TODO: use a buffer instead of creating a new array everytime
    positions_data = np.zeros((n_verts, 3), dtype=np.float32)
    positions_attr.data.foreach_get('vector', np.ravel(positions_data))
    verts = SimulationAPI.CreateVector3Array(n_verts)
    SimulationAPI.CopyVector3Array(verts, positions_data)
    SimulationAPI.ApplyTransform(verts, obj_to_node, SimulationAPI.VectorType.Position)

    positions = node.GetOrCreateAttribute(attribute_name);
    positions.SetDataArrayType(SimulationAPI.Attribute.DataArrayType.PerVertex);
    positions.SetVector3ArrayValue(verts, time);
    return True
