# Copyright (c) 2009-2014 Turbulenz Limited
"""
Mesh class used to hold and process vertex streams.

Supports generating NBTs.
"""

import math
import logging
LOG = logging.getLogger('asset')

# pylint: disable=W0403
import turbulenz_tools.tools.vmath as vmath
import turbulenz_tools.tools.pointmap as pointmap
# pylint: enable=W0403

__version__ = '1.1.0'
__dependencies__ = ['pointmap', 'vmath']

#######################################################################################################################

# cos( 1 pi / 8 ) = 0.923879 ~ cos 22.5 degrees
# cos( 2 pi / 8 ) = 0.707106 ~ cos 45   degrees
# cos( 2 pi / 6 ) = 0.5      ~ cos 60   degress
# cos( 3 pi / 8 ) = 0.382683 ~ cos 67.5 degrees
DEFAULT_POSITION_TOLERANCE = 1e-6
DEFAULT_NORMAL_SMOOTH_TOLERANCE = 0.5 # 0.707106
DEFAULT_TANGENT_SPLIT_TOLERANCE = 0.5 # 0.382683
DEFAULT_ZERO_TOLERANCE = 1e-6
DEFAULT_DONT_NORMALIZE_TOLERANCE = 1e-3
DEFAULT_UV_TOLERANCE = 1e-6
DEFAULT_PLANAR_TOLERANCE = 1e-6
DEFAULT_TANGENT_PROJECTION_TOLERANCE = 1e-10
DEFAULT_COLLINEAR_TOLERANCE = 1e-10
DEFAULT_COPLANAR_TOLERANCE = 1e-16
DEFAULT_PLANAR_HULL_VERTEX_THRESHOLD = 5

def similar_positions(major, positions, pos_tol=DEFAULT_POSITION_TOLERANCE):
    """Iterator to return the index of similar positions."""
    # pos_tol currently unused - using vmath default.
    for i, p in enumerate(positions):
        if vmath.v3equal(major, p):
            yield i

#######################################################################################################################

# pylint: disable=R0902
# pylint: disable=R0904
class Mesh(object):
    """Generate a mesh geometry."""

    class Stream(object):
        """Contains a single stream of vertex attributes."""
        def __init__(self, values, semantic, name, stride, offset):
            """
            ``values`` - array of stream tuples
            ``semantic`` - name of the stream semantic
            ``name`` - human readable name of the stream
            ``stride`` - length of each vertex in the stream
            ``offset`` - offset in the index buffer for this stream
            """
            self.values = values
            self.semantic = semantic
            self.name = name
            self.stride = stride
            self.offset = offset

    def __init__(self, mesh=None):
        # Positions, normals, uvs, skin_indices, skin_weights, primitives
        self.kdtree = None
        if mesh is not None:
            self.positions = mesh.positions[:]
            self.uvs = [ uvs[:] for uvs in mesh.uvs ]
            self.normals = mesh.normals[:]
            self.tangents = mesh.tangents[:]
            self.binormals = mesh.binormals[:]
            self.colors = mesh.colors[:]
            self.skin_indices = mesh.skin_indices[:]
            self.skin_weights = mesh.skin_weights[:]
            self.primitives = mesh.primitives[:]
            self.bbox = mesh.bbox.copy()
        else:
            self.positions = [ ]
            self.uvs = [ [] ]
            self.normals = [ ]
            self.tangents = [ ]
            self.binormals = [ ]
            self.colors = [ ]
            self.skin_indices = [ ]
            self.skin_weights = [ ]
            self.primitives = [ ]
            self.bbox = { }

    ###################################################################################################################

    def set_values(self, values, semantic):
        """Replace the mesh values for a specified semantic."""
        if semantic == 'POSITION':
            self.positions = values
        elif semantic.startswith('TEXCOORD'):
            if semantic == 'TEXCOORD' or semantic == 'TEXCOORD0':
                self.uvs[0] = values
            else:
                index = int(semantic[8:])
                while index >= len(self.uvs):
                    self.uvs.append([])
                self.uvs[index] = values
        elif semantic == 'NORMAL' or semantic == 'NORMAL0':
            self.normals = values
        elif semantic == 'TANGENT':
            self.tangents = values
        elif semantic == 'BINORMAL':
            self.binormals = values
        elif semantic == 'COLOR' or semantic == 'COLOR0':
            self.colors = values
        elif semantic == 'BLENDINDICES':
            self.skin_indices = values
        elif semantic == 'BLENDWEIGHT':
            self.skin_weights = values
        else:
            LOG.warning('Unknown semantic:%s', semantic)

    def get_values(self, semantic):
        """Retrieve the mesh values for a specified semantic."""
        if semantic == 'POSITION':
            values = self.positions
        elif semantic.startswith('TEXCOORD'):
            if semantic == 'TEXCOORD' or semantic == 'TEXCOORD0':
                values = self.uvs[0]
            else:
                index = int(semantic[8:])
                if index >= len(self.uvs):
                    values = []
                else:
                    values = self.uvs[index]
        elif semantic == 'NORMAL' or semantic == 'NORMAL0':
            values = self.normals
        elif semantic == 'TANGENT':
            values = self.tangents
        elif semantic == 'BINORMAL':
            values = self.binormals
        elif semantic == 'COLOR' or semantic == 'COLOR0':
            values = self.colors
        elif semantic == 'BLENDINDICES':
            values = self.skin_indices
        elif semantic == 'BLENDWEIGHT':
            values = self.skin_weights
        else:
            values = None
            LOG.warning('Unknown semantic:%s', semantic)
        return values

    ###################################################################################################################

    def transform(self, transform):
        """Transform the vertexes."""
        self.positions = [ vmath.m43transformp(transform, v) for v in self.positions ]
        self.normals = [ vmath.m43transformn(transform, v) for v in self.normals ]
        self.tangents = [ vmath.m43transformn(transform, v) for v in self.tangents ]
        self.binormals = [ vmath.m43transformn(transform, v) for v in self.binormals ]

    def rotate(self, transform):
        """Rotate the vertexes."""
        self.positions = [ vmath.v3mulm33(v, transform) for v in self.positions ]
        self.normals = [ vmath.v3mulm33(v, transform) for v in self.normals ]
        self.tangents = [ vmath.v3mulm33(v, transform) for v in self.tangents ]
        self.binormals = [ vmath.v3mulm33(v, transform) for v in self.binormals ]

    def invert_v_texture_map(self, uvs=None):
        """Invert the v texture mapping."""
        if not uvs:
            uvs = self.uvs[0]
        if len(uvs) > 0:
            if len(uvs[0]) == 2:
                elements = [ ]
                (_, min_v) = uvs[0]
                (_, max_v) = uvs[0]
                for (_, v) in uvs:
                    min_v = min(v, min_v)
                    max_v = max(v, max_v)
                midV = 2 * ((math.ceil(max_v) + math.floor(min_v)) * 0.5)
                for (u, v) in uvs:
                    elements.append( (u, midV - v) )
                self.uvs[0] = elements
            elif len(uvs[0]) == 3:
                elements = [ ]
                (_, min_v, _) = uvs[0]
                (_, max_v, _) = uvs[0]
                for (_, v, _) in uvs:
                    min_v = min(v, min_v)
                    max_v = max(v, max_v)
                midV = 2 * ((math.ceil(max_v) + math.floor(min_v)) * 0.5)
                for (u, v, w) in uvs:
                    elements.append( (u, midV - v, w) )
                self.uvs[0] = elements

    def generate_vertex_with_new_uv(self, pindex, vindex, new_uv):
        """Create a new vertex for a primitive with a new uv."""
        new_vindex = len(self.positions)                        # Get the new vindex
        while len(self.uvs[0]) < new_vindex:                       # Make sure the UVs and Positions are the same size
            self.uvs[0].append( (0, 0) )
        self.positions.append(self.positions[vindex])           # Clone the position
        self.uvs[0].append(new_uv)                                 # Create a new UV
        (ci1, ci2, ci3) = self.primitives[pindex]               # Update the primitive to use the new vindex
        if ci1 == vindex:
            self.primitives[pindex] = (new_vindex, ci2, ci3)
        elif ci2 == vindex:
            self.primitives[pindex] = (ci1, new_vindex, ci3)
        elif ci3 == vindex:
            self.primitives[pindex] = (ci1, ci2, new_vindex)
        else:
            LOG.error("Didn't find vertex:%i used on primitive:%i", vindex, pindex)

    def generate_primitives(self, indexes):
        """Generate a list of primitives from a list of indexes."""
        # Make triangles out of each sequence of 3 indices.
        self.primitives = zip(indexes[0::3], indexes[1::3], indexes[2::3])

    def generate_bbox(self):
        """Generate a bounding box for the mesh."""
        # Assumes that the positions have at least one element, or the bbox will be nonsensical
        self.bbox['min'] = (float('inf'), float('inf'), float('inf'))
        self.bbox['max'] = (float('-inf'), float('-inf'), float('-inf'))
        for pos in self.positions:
            self.bbox['min'] = vmath.v3min(self.bbox['min'], pos)
            self.bbox['max'] = vmath.v3max(self.bbox['max'], pos)

    def remove_degenerate_primitives(self, remove_zero_length_edges=True,
                                     edge_length_tol=DEFAULT_POSITION_TOLERANCE):
        """Remove degenerate triangles with duplicated indices and optionally
           zero length edges."""
        def _is_degenerate(prim):
            """ Test if a prim is degenerate """
            (i1, i2, i3) = prim
            if i1 == i2 or i1 == i3 or i2 == i3:
                return True
            elif remove_zero_length_edges:
                (v1, v2, v3) = (self.positions[i1], self.positions[i2], self.positions[i3])
                return (vmath.v3is_zero(vmath.v3sub(v2, v1), edge_length_tol) or
                        vmath.v3is_zero(vmath.v3sub(v3, v1), edge_length_tol) or
                        vmath.v3is_zero(vmath.v3sub(v3, v2), edge_length_tol))
            else:
                return False

        self.primitives = [ prim for prim in self.primitives if not _is_degenerate(prim) ]

    ###################################################################################################################

    def generate_smooth_nbts(self):
        """Helper method to generate and smooth the normals, binormals and tangents."""
        if not len(self.normals):
            # Generate the initial normals if there are none.
            self.generate_normals()
        self.generate_tangents()
        self.normalize_tangents()
        self.smooth_tangents()
        self.generate_normals_from_tangents()
        self.smooth_normals()

    def _generate_normal(self, i1, i2, i3, pos_tol):
        """Generate a normal for 3 indexes."""
        (v1, v2, v3) = (self.positions[i1], self.positions[i2], self.positions[i3])
        e1 = vmath.v3sub(v2, v1)
        e2 = vmath.v3sub(v3, v1)
        e_other = vmath.v3sub(v3, v2)
        if vmath.v3is_zero(e1, pos_tol) or vmath.v3is_zero(e2, pos_tol) or vmath.v3is_zero(e_other, pos_tol):
            LOG.warning("%s: Found degenerate primitive:%s with edge length < position tolerance[%g]:[%s,%s,%s]",
                        "generate_normals", (i1, i2, i3), pos_tol, e1, e2, e_other)
            return (0, 0, 0)
        return vmath.v3normalize(vmath.v3cross(e1, e2))

    def generate_normals(self, pos_tol=DEFAULT_POSITION_TOLERANCE,
                         dont_norm_tol=DEFAULT_DONT_NORMALIZE_TOLERANCE):
        """Generate a normal per vertex as an average of face normals the primitive is part of."""
        zero = (0, 0, 0)
        self.normals = [zero] * len(self.positions)
        for (i1, i2, i3) in self.primitives:
            n = self._generate_normal(i1, i2, i3, pos_tol)
            self.normals[i1] = vmath.v3add(self.normals[i1], n)
            self.normals[i2] = vmath.v3add(self.normals[i2], n)
            self.normals[i3] = vmath.v3add(self.normals[i3], n)
        for i, n in enumerate(self.normals):
            lsq = vmath.v3lengthsq(n)
            if lsq > dont_norm_tol: # Ensure normal isn't tiny before normalizing it.
                lr = 1.0 / math.sqrt(lsq)
                self.normals[i] = vmath.v3muls(n, lr)
            else:
                self.normals[i] = zero
                LOG.warning("%s: Found vertex[%i] with normal < normalizable tolerance[%g]:%s",
                            "generate_normals", i, dont_norm_tol, n)

    def smooth_normals(self, include_uv_tol=False,
                       root_node=None,
                       pos_tol=DEFAULT_POSITION_TOLERANCE,
                       nor_smooth_tol=DEFAULT_NORMAL_SMOOTH_TOLERANCE,
                       uv_tol=DEFAULT_UV_TOLERANCE):
        """Smooth normals within a certain position range and normal divergence limit."""

        if not root_node:
            if not self.kdtree:
                # Create a kd-tree to optimize the smoothing performance
                self.kdtree = pointmap.build_kdtree(self.positions)
            root_node = self.kdtree

        uvs = self.uvs[0]
        for i, p in enumerate(self.positions):
            original_normal = self.normals[i]
            accumulate_normal = (0, 0, 0)
            accumulated_indexes = [ ]

            # Generate a list of indexes for the positions close to the evaluation vertex.
            if include_uv_tol:
                uv = uvs[i]
                similiar_positions_indexes = root_node.points_within_uv_distance(self.positions, p, pos_tol,
                                                                                 uvs, uv, uv_tol)
            else:
                similiar_positions_indexes = root_node.points_within_distance(self.positions, p, pos_tol)

            for i in similiar_positions_indexes:
                this_normal = self.normals[i]
                if vmath.v3is_similar(this_normal, original_normal, nor_smooth_tol):
                    accumulate_normal = vmath.v3add(accumulate_normal, this_normal)
                    accumulated_indexes.append(i)
            smooth_normal = vmath.v3unitcube_clamp(vmath.v3normalize(accumulate_normal))
            for i in accumulated_indexes:
                self.normals[i] = smooth_normal

    ###################################################################################################################

    def _clone_vertex_with_new_tangents(self, prim_index, vertex_index, tangent, binormal):
        """Create a new vertex clone all attributes. Then set the new tangent and binormal."""
        clone_index = len(self.positions)
        self.positions.append(self.positions[vertex_index])
        self.normals.append(self.normals[vertex_index])
        for uvs in self.uvs:
            if len(uvs) > vertex_index:
                uvs.append(uvs[vertex_index])
        self.tangents.append(tangent)
        self.binormals.append(binormal)
        if len(self.colors) > vertex_index:
            self.colors.append(self.colors[vertex_index])
        if len(self.skin_indices) > vertex_index:
            self.skin_indices.append(self.skin_indices[vertex_index])
        if len(self.skin_weights) > vertex_index:
            self.skin_weights.append(self.skin_weights[vertex_index])
        return clone_index

    def _split_vertex_with_new_tangents(self, vertex_index, prim_index, split_map, tangent, binormal, tan_split_tol_sq):
        """Split the vertex if the tangents are outside of the accumulation tolerance."""

        def update_primitive_vertexes(primitives, prim_index, source_index, target_index):
            """Update the primitive for a cloned set of vertexes."""
            (p1, p2, p3) = primitives[prim_index]
            if p1 == source_index:
                p1 = target_index
            if p2 == source_index:
                p2 = target_index
            if p3 == source_index:
                p3 = target_index
            primitives[prim_index] = (p1, p2, p3)

        def tangents_within_tolerance(t1, b1, t2, b2, tan_split_tol_sq):
            """Test if the tangents and binormals are within tolerance."""
            tangent_within_tolerance = vmath.v3is_within_tolerance(t1, t2, tan_split_tol_sq)
            binormal_within_tolerance = vmath.v3is_within_tolerance(b1, b2, tan_split_tol_sq)
            return tangent_within_tolerance and binormal_within_tolerance

        def potential_accumulation_vertexes(vertex_index, split_map):
            """Iterator to consider all potential accumulation vertexes."""
            yield (vertex_index, vertex_index)
            for (original_index, clone_index) in split_map:
                if original_index == vertex_index:
                    yield (original_index, clone_index)

        for (original_index, index) in potential_accumulation_vertexes(vertex_index, split_map):
            if tangents_within_tolerance(self.tangents[index], self.binormals[index],
                                         tangent, binormal, tan_split_tol_sq):
                self.tangents[index] = vmath.v3add(self.tangents[index], tangent)
                self.binormals[index] = vmath.v3add(self.binormals[index], binormal)
                update_primitive_vertexes(self.primitives, prim_index, original_index, index)
                break
        else:
            # We need to split the vertex to start accumulating a different set of tangents.
            clone_index = self._clone_vertex_with_new_tangents(prim_index, vertex_index, tangent, binormal)
            update_primitive_vertexes(self.primitives, prim_index, vertex_index, clone_index)
            split_map.append( (vertex_index, clone_index) )
            LOG.debug("Splitting vertex:%i --> %i primitive[%i] is now:%s", vertex_index, clone_index, prim_index,
                      self.primitives[prim_index])
            LOG.debug("N:%s B:%s T:%s", self.normals[clone_index], self.binormals[clone_index],
                      self.tangents[clone_index])

    # pylint: disable=R0914
    def _generate_tangents_for_triangle(self, prim, pos_tol, zero_tol):
        """Generate binormals and tangents for a primitive."""
        du = [0, 0, 0]
        dv = [0, 0, 0]
        (i1, i2, i3) = prim                                                                     # Primitive indexes
        (v1, v2, v3) = (self.positions[i1], self.positions[i2], self.positions[i3])             # Vertex positions
        uvs = self.uvs[0]
        (uv1, uv2, uv3) = (uvs[i1], uvs[i2], uvs[i3])                                           # Vertex UVs
        (e21, e31, e32) = (vmath.v3sub(v2, v1), vmath.v3sub(v3, v1), vmath.v3sub(v3, v2))       # Generate edges
        # Ignore degenerates
        if vmath.v3is_zero(e21, pos_tol) or vmath.v3is_zero(e31, pos_tol) or vmath.v3is_zero(e32, pos_tol):
            LOG.warning("%s: Found degenerate triangle:%s", "_generate_tangents_for_triangle", (i1, i2, i3))
        else:
            # Calculate tangent and binormal
            edge1 = [e21[0], uv2[0] - uv1[0], uv2[1] - uv1[1]]
            edge2 = [e31[0], uv3[0] - uv1[0], uv3[1] - uv1[1]]
            cp = vmath.v3cross(edge1, edge2)
            if not vmath.iszero(cp[0], zero_tol):
                du[0] = -cp[1] / cp[0]
                dv[0] = -cp[2] / cp[0]
            edge1[0] = e21[1] # y, s, t
            edge2[0] = e31[1]
            cp = vmath.v3cross(edge1, edge2)
            if not vmath.iszero(cp[0], zero_tol):
                du[1] = -cp[1] / cp[0]
                dv[1] = -cp[2] / cp[0]
            edge1[0] = e21[2] # z, s, t
            edge2[0] = e31[2]
            cp = vmath.v3cross(edge1, edge2)
            if not vmath.iszero(cp[0], zero_tol):
                du[2] = -cp[1]/cp[0]
                dv[2] = -cp[2]/cp[0]
        return (du, dv)
    # pylint: enable=R0914

    def generate_tangents(self, pos_tol=DEFAULT_POSITION_TOLERANCE,
                          zero_tol=DEFAULT_ZERO_TOLERANCE,
                          tan_split_tol=DEFAULT_TANGENT_SPLIT_TOLERANCE):
        """Generate a NBT per vertex."""
        if 0 == len(self.uvs[0]): # We can't generate nbts without uvs
            LOG.debug("Can't generate nbts without uvs:%i", len(self.uvs[0]))
            return
        num_vertices = len(self.positions)
        self.tangents = [ (0, 0, 0) ] * num_vertices
        self.binormals = [ (0, 0, 0) ] * num_vertices

        # Split map for recording pairs of integers that represent which vertexes have been split.
        partition_vertices = True
        split_map = [ ]
        tan_split_tol_sq = (tan_split_tol * tan_split_tol)

        for prim_index, prim in enumerate(self.primitives):
            (tangent, binormal) = self._generate_tangents_for_triangle(prim, pos_tol, zero_tol)
            (i1, i2, i3) = prim
            if partition_vertices:
                self._split_vertex_with_new_tangents(i1, prim_index, split_map, tangent, binormal, tan_split_tol_sq)
                self._split_vertex_with_new_tangents(i2, prim_index, split_map, tangent, binormal, tan_split_tol_sq)
                self._split_vertex_with_new_tangents(i3, prim_index, split_map, tangent, binormal, tan_split_tol_sq)
            else:
                # Accumulate tangent and binormal
                self.tangents[i1] = vmath.v3add(self.tangents[i1], tangent)
                self.tangents[i2] = vmath.v3add(self.tangents[i2], tangent)
                self.tangents[i3] = vmath.v3add(self.tangents[i3], tangent)
                self.binormals[i1] = vmath.v3add(self.binormals[i1], binormal)
                self.binormals[i2] = vmath.v3add(self.binormals[i2], binormal)
                self.binormals[i3] = vmath.v3add(self.binormals[i3], binormal)

    def normalize_tangents(self, dont_norm_tol=DEFAULT_DONT_NORMALIZE_TOLERANCE):
        """Normalize and clamp the new tangents and binormals."""
        zero = (0, 0, 0)

        tangents = [ ]
        for i, t in enumerate(self.tangents):
            if vmath.v3lengthsq(t) > dont_norm_tol: # Ensure the tangent isn't tiny before normalizing it.
                tangents.append(vmath.v3unitcube_clamp(vmath.v3normalize(t)))
            else:
                LOG.warning("%s: Found vertex[%i] with tangent < normalizable tolerance[%g]:%s",
                            "normalize_tangents", i, dont_norm_tol, t)
                tangents.append(zero)
        self.tangents = tangents

        binormals = [ ]
        for i, b in enumerate(self.binormals):
            if vmath.v3lengthsq(b) > dont_norm_tol:
                binormals.append(vmath.v3unitcube_clamp(vmath.v3normalize(b)))
            else:
                LOG.warning("%s: Found vertex[%i] with binormal < normalizable tolerance[%g]:%s",
                            "normalize_tangents", i, dont_norm_tol, b)
                binormals.append(zero)
        self.binormals = binormals

    def smooth_tangents(self, include_uv_tol=False,
                        root_node=None,
                        pos_tol=DEFAULT_POSITION_TOLERANCE,
                        nor_smooth_tol=DEFAULT_NORMAL_SMOOTH_TOLERANCE,
                        uv_tol=DEFAULT_UV_TOLERANCE):
        """Smooth the tangents of vertices with similar positions."""
        def tangents_are_similar(t1, t2, b1, b2, nor_smooth_tol):
            """Test if both the tangents and binormals are similar."""
            tangent_similar = vmath.v3is_similar(t1, t2, nor_smooth_tol)
            binormal_similar = vmath.v3is_similar(b1, b2, nor_smooth_tol)
            return tangent_similar and binormal_similar

        if not root_node:
            if not self.kdtree:
                # Create a kd-tree to optimize the smoothing performance
                self.kdtree = pointmap.build_kdtree(self.positions)
            root_node = self.kdtree

        uvs = self.uvs[0]
        for i, p in enumerate(self.positions):
            original_tangent = self.tangents[i]
            original_binormal = self.binormals[i]
            accumulate_tangent = (0, 0, 0)
            accumulate_binormal = (0, 0, 0)
            accumulated_indexes = [ ]

            # Generate a list of indexes for the positions close to the evaluation vertex.
            if include_uv_tol:
                uv = uvs[i]
                similiar_positions_indexes = root_node.points_within_uv_distance(self.positions, p, pos_tol,
                                                                                 uvs, uv, uv_tol)
            else:
                similiar_positions_indexes = root_node.points_within_distance(self.positions, p, pos_tol)

            for i in similiar_positions_indexes:
                this_tangent = self.tangents[i]
                this_binormal = self.binormals[i]
                if tangents_are_similar(this_tangent, original_tangent,
                                        this_binormal, original_binormal, nor_smooth_tol):
                    accumulate_tangent = vmath.v3add(accumulate_tangent, this_tangent)
                    accumulate_binormal = vmath.v3add(accumulate_binormal, this_binormal)
                    accumulated_indexes.append(i)
            smooth_tangent = vmath.v3unitcube_clamp(vmath.v3normalize(accumulate_tangent))
            smooth_binormal = vmath.v3unitcube_clamp(vmath.v3normalize(accumulate_binormal))
            for i in accumulated_indexes:
                self.tangents[i] = smooth_tangent
                self.binormals[i] = smooth_binormal

    def generate_normals_from_tangents(self, zero_tol=DEFAULT_ZERO_TOLERANCE,
                                       dont_norm_tol=DEFAULT_DONT_NORMALIZE_TOLERANCE):
        """Create a new normal from the tangent and binormals."""
        if not len(self.tangents) or not len(self.binormals): # We can't generate normals without nbts
            LOG.debug("Can't generate normals from nbts without tangets:%i and binormals:%i",
                      len(self.tangents), len(self.binormals))
            return
        num_vertices = len(self.normals)
        assert num_vertices == len(self.tangents)
        assert num_vertices == len(self.binormals)
        # Regenerate the vertex normals from the new tangents and binormals
        for i in range(num_vertices):
            normal = self.normals[i]
            cp = vmath.v3cross(self.tangents[i], self.binormals[i])
            # Keep vertex normal if the tangent and the binormal are paralel
            if vmath.v3lengthsq(cp) > dont_norm_tol:
                cp = vmath.v3normalize(cp)
                # Keep vertex normal if new normal is *somehow* in the primitive plane
                cosangle = vmath.v3dot(cp, normal)
                if not vmath.iszero(cosangle, zero_tol):
                    if cosangle < 0:
                        self.normals[i] = vmath.v3neg(cp)
                    else:
                        self.normals[i] = cp

    def flip_primitives(self):
        """Change winding order"""
        self.primitives = [ (i1, i3, i2) for (i1, i2, i3) in self.primitives ]

    def mirror_in(self, axis="x", flip=True):
        """Flip geometry in axis."""
        if axis == "x":
            self.positions = [ (-x, y, z) for (x, y, z) in self.positions ]
            self.normals = [ (-x, y, z) for (x, y, z) in self.normals ]
        elif axis == "y":
            self.positions = [ (x, -y, z) for (x, y, z) in self.positions ]
            self.normals = [ (x, -y, z) for (x, y, z) in self.normals ]
        elif axis == "z":
            self.positions = [ (x, y, -z) for (x, y, z) in self.positions ]
            self.normals = [ (x, y, -z) for (x, y, z) in self.normals ]
        if flip:
            self.flip_primitives()

    ###################################################################################################################

    def remove_redundant_vertexes(self):
        """Remove redundant vertex indexes from the element streams if unused by the primitives."""
        mapping = { }
        new_index = 0
        for (i1, i2, i3) in self.primitives:
            if i1 not in mapping:
                mapping[i1] = new_index
                new_index += 1
            if i2 not in mapping:
                mapping[i2] = new_index
                new_index += 1
            if i3 not in mapping:
                mapping[i3] = new_index
                new_index += 1
        old_index = len(self.positions)
        if old_index != new_index:
            LOG.info("Remapping:remapping vertexes from %i to %i", old_index, new_index)

        def __remap_stream(source, size, mapping):
            """Remap vertex attribute stream."""
            if len(source) > 0:
                target = [0] * size
                for k, v in mapping.items():
                    target[v] = source[k]
                return target
            return [ ]

        self.positions = __remap_stream(self.positions, new_index, mapping)
        for uvs in self.uvs:
            uvs[:] = __remap_stream(uvs, new_index, mapping)
        self.normals = __remap_stream(self.normals, new_index, mapping)
        self.tangents = __remap_stream(self.tangents, new_index, mapping)
        self.binormals = __remap_stream(self.binormals, new_index, mapping)
        self.colors = __remap_stream(self.colors, new_index, mapping)
        self.skin_indices = __remap_stream(self.skin_indices, new_index, mapping)
        self.skin_weights = __remap_stream(self.skin_weights, new_index, mapping)

        primitives = [ ]
        for (i1, i2, i3) in self.primitives:
            primitives.append( (mapping[i1], mapping[i2], mapping[i3]) )
        self.primitives = primitives

    ###################################################################################################################

    def stitch_vertices(self):
        """Combine equal vertices together, adjusting indices of primitives where appropriate
           Any other vertex data like normals, tangents, colors are ignored"""

        num_points = len(self.positions)
        points = sorted(enumerate(self.positions), key=lambda (_, x): x)

        mapping = [0] * num_points
        new_index = -1
        prev_p = None
        for (index, p) in points:
            if p != prev_p:
                new_index += 1
                prev_p = p
            mapping[index] = new_index

        self.primitives = [(mapping[i1], mapping[i2], mapping[i3]) for (i1, i2, i3) in self.primitives]
        new_positions = [0] * (new_index + 1)
        for (i, to) in enumerate(mapping):
            new_positions[to] = self.positions[i]
        self.positions = new_positions

    ###################################################################################################################

    def is_convex(self, positions=None, primitives=None):
        """Check if a mesh is convex by validating no vertices lie in front of the planes defined by its faces."""
        positions = positions or self.positions
        primitives = primitives or self.primitives
        for (i1, i2, i3) in primitives:
            v1 = positions[i1]
            v2 = positions[i2]
            v3 = positions[i3]

            edge1 = vmath.v3sub(v1, v3)
            edge2 = vmath.v3sub(v2, v3)
            normal = vmath.v3normalize(vmath.v3cross(edge1, edge2))

            for p in positions:
                dist = vmath.v3dot(vmath.v3sub(p, v1), normal)
                if dist > vmath.PRECISION:
                    return False
        return True

    def simply_closed(self, primitives=None):
        """Determine if a connected mesh is closed defined by triangle indexes
           in sense that it defines the boundary of a closed region of space without any
           possible dangling triangles on boundary.

           We assume that the triangle mesh does not have any ugly self intersections."""
        primitives = primitives or self.primitives
        # We do this by counting the number of triangles on each triangle edge
        # Specifically check that this value is exactly 2 for every edge.1
        edges = { }
        def _inc(a, b):
            """Increment edge count for vertex indices a, b.
               Return True if edge count has exceeded 2"""
            if a > b:
                return _inc(b, a)
            if (a, b) in edges:
                edges[(a, b)] += 1
            else:
                edges[(a, b)] = 1
            return edges[(a, b)] > 2

        for (i1, i2, i3) in primitives:
            if _inc(i1, i2):
                return False
            if _inc(i2, i3):
                return False
            if _inc(i3, i1):
                return False

        for (_, v) in edges.items():
            if v != 2:
                return False

        return True

    def is_planar(self, positions=None, tolerance=DEFAULT_PLANAR_TOLERANCE):
        """Determine if mesh is planar; that all vertices lie in the same plane.
           If positions argument is not supplied, then self.positions is used"""
        positions = positions or self.positions
        if len(positions) <= 3:
            return True

        p0 = positions[0]
        edge1 = vmath.v3sub(positions[1], p0)
        edge2 = vmath.v3sub(positions[2], p0)
        normal = vmath.v3normalize(vmath.v3cross(edge1, edge2))

        for p in positions:
            distance = vmath.v3dot(vmath.v3sub(p, p0), normal)
            if (distance * distance) > tolerance:
                return False

        return True

    def is_convex_planar(self, positions=None):
        """Determine if a planar mesh is convex; taking virtual edge normals into account"""
        positions = positions or self.positions
        if len(positions) <= 3:
            return True

        p0 = positions[0]
        edge1 = vmath.v3sub(positions[1], p0)
        edge2 = vmath.v3sub(positions[2], p0)
        normal = vmath.v3normalize(vmath.v3cross(edge1, edge2))

        for (i, p) in enumerate(positions):
            j = (i + 1) if (i < len(positions) - 1) else 0
            q = positions[j]

            edge3 = vmath.v3sub(q, p)
            edge_normal = vmath.v3cross(normal, edge3)

            for w in positions:
                if vmath.v3dot(vmath.v3sub(w, p), edge_normal) < -vmath.PRECISION:
                    return False

        return True

    def connected_components(self):
        """Determine connected components of mesh, returning list of set of vertices and primitives."""
        # Perform this algorithm with a disjoint set forest.
        # Initialise components: [index, parent_index, rank, component]
        components = [[i, 0] for i in range(len(self.positions))]

        def _find(x):
            """Find root note in disjoint set forest, compressing path to root."""
            if components[x][0] == x:
                return x
            else:
                root = x
                stack = [ ]
                while components[root][0] != root:
                    stack.append(root)
                    root = components[root][0]
                for y in stack:
                    components[y][0] = root
                return root

        def _unify(x, y):
            """Unify two components in disjoint set forest by rank."""
            x_root = _find(x)
            y_root = _find(y)
            if x_root != y_root:
                xc = components[x_root]
                yc = components[y_root]
                if xc[1] < yc[1]:
                    xc[0] = y_root
                elif xc[1] > yc[1]:
                    yc[0] = x_root
                else:
                    yc[0] = x_root
                    xc[1] += 1

        # Unify components based on adjacency information inferred through shared vertices in primitives
        for (i1, i2, i3) in self.primitives:
            _unify(i1, i2)
            _unify(i2, i3)

        # Return list of all components associated with each root.
        ret = [ ]
        for c in [y for y in range(len(self.positions)) if _find(y) == y]:
            m = Mesh()
            m.positions = self.positions[:]
            m.primitives = [(i1, i2, i3) for (i1, i2, i3) in self.primitives if _find(i1) == c]

            m.remove_redundant_vertexes()
            ret.append((m.positions, m.primitives))

        return ret

    # pylint: disable=R0914
    def make_planar_convex_hull(self, positions=None, tangent_tolerance=DEFAULT_TANGENT_PROJECTION_TOLERANCE):
        """Convert set of co-planar positions into a minimal set of positions required to form their convex
           hull, together with a set of primitives representing one side of the hulls' surface as a
           new Mesh"""
        positions = positions or self.positions
        # Use a 2D Graham Scan with projections of positions onto their maximal plane.
        # Time complexity: O(nh) for n positions and h out positions.

        # Determine maximal plane for projection.
        edge1 = vmath.v3sub(positions[1], positions[0])
        edge2 = vmath.v3sub(positions[2], positions[0])
        (n0, _, n2) = n = vmath.v3cross(edge1, edge2)

        # compute plane tangents.
        # epsilon chosen with experiment
        if (n0 * n0) + (n2 * n2) < tangent_tolerance:
            t = (1, 0, 0)
        else:
            t = (-n2, 0, n0)
        u = vmath.v3cross(n, t)

        # Project to tangents
        projs = [(vmath.v3dot(p, t), vmath.v3dot(p, u)) for p in positions]

        # Find first vertex on hull as minimal lexicographically ordered projection
        i0 = 0
        minp = projs[0]
        for i in range(1, len(projs)):
            if projs[i] < minp:
                i0 = i
                minp = projs[i]

        # Map from old vertex indices to new index for those vertices used by hull
        outv = { i0: 0 }
        new_index = 1

        # List of output triangles.
        outtriangles = [ ]
        fsti = i0
        (p0x, p0y) = minp
        while True:
            i1 = -1
            for i in range(len(projs)):
                if i == i0:
                    continue

                (px, py) = projs[i]
                plsq = ((px - p0x) * (px - p0x)) + ((py - p0y) * (py - p0y))
                if i1 == -1:
                    i1 = i
                    maxp = (px, py)
                    maxplsq = plsq
                    continue

                # If this is not the first vertex tested, determine if new vertex makes
                # A right turn looking in direction of edge, or is further in same direction.
                (qx, qy) = maxp
                turn = ((qx - p0x) * (py - p0y)) - ((qy - p0y) * (px - p0x))
                if turn < 0 or (turn == 0 and plsq > maxplsq):
                    i1 = i
                    maxp = (px, py)
                    maxplsq = plsq

            # Append i1 vertex to hull
            if i1 in outv:
                break

            outv[i1] = new_index
            new_index += 1

            # Form triangle (fsti, i0, i1)
            # If i0 != fsti
            if i0 != fsti:
                outtriangles.append((fsti, i0, i1))

            i0 = i1
            (p0x, p0y) = projs[i1]

        # Compute output hull.
        mesh = Mesh()
        mesh.positions = [0] * len(outv.items())
        for i, j in outv.items():
            mesh.positions[j] = positions[i]
        mesh.primitives = [(outv[i1], outv[i2], outv[i3]) for (i1, i2, i3) in outtriangles]

        return mesh
    # pylint: enable=R0914

    # pylint: disable=R0914
    def make_convex_hull(self, positions=None, collinear_tolerance=DEFAULT_COLLINEAR_TOLERANCE,
                         coplanar_tolerance=DEFAULT_COPLANAR_TOLERANCE):
        """Convert set of positions into a minimal set of positions required to form their convex hull
           Together with a set of primitives representing a triangulation of the hull's surface as a
           new Mesh"""
        positions = positions or self.positions
        # Use a 3D generalisation of a Graham Scan to facilitate a triangulation of the hull in generation.
        # Time complexity: O(nh) for n positions, and h out-positions.

        # Find first vertex on hull as minimal lexicographically ordered position
        i0 = 0
        minp = positions[0]
        for i in range(1, len(positions)):
            if positions[i] < minp:
                i0 = i
                minp = positions[i]

        # Find second vertex by performing a 2D graham scan step on the xy-plane projections of positions.
        i1 = -1
        (cos1, lsq1) = (-2, 0) # will always be overriden as cos(theta) > -2
        (p0x, p0y, _) = minp
        for i in range(len(positions)):
            if i == i0:
                continue

            (px, py, _) = positions[i]
            dx = px - p0x
            dy = py - p0y
            lsq = (dx * dx) + (dy * dy)
            if lsq == 0:
                if i1 == -1:
                    i1 = i
                continue

            cos = dy / math.sqrt(lsq)
            if cos > cos1 or (cos == cos1 and lsq > lsq1):
                cos1 = cos
                lsq1 = lsq
                i1 = i

        # Dictionary of visited edges to avoid duplicates
        # List of open edges to be visited by graham scan
        closedset = set()
        openset = [ (i0, i1), (i1, i0) ]

        # Mapping from old vertex index to new index for those vertices used by hull.
        outv = { i0: 0, i1: 1 }
        new_index = 2

        # Output triangles for hull
        outtriangles = [ ]

        while len(openset) > 0:
            (i0, i1) = openset.pop()
            if (i0, i1) in closedset:
                continue

            # Find next vertex on hull to form triangle with.
            i2 = -1

            p0 = positions[i0]
            edge = vmath.v3sub(positions[i1], p0)
            isq = 1.0 / vmath.v3lengthsq(edge)

            for i in range(len(positions)):
                if i == i0 or i == i1:
                    continue

                p = positions[i]
                # Find closest point on line containing the edge to determine vector to p
                # Perpendicular to edge, this is not necessary for computing the turn
                # since the value of 'turn' computed is actually the same whether we do this
                # or not, however it is needed to be able to sort equal turn vertices
                # by distance.
                t = vmath.v3dot(vmath.v3sub(p, p0), edge) * isq
                pedge = vmath.v3sub(p, vmath.v3add(p0, vmath.v3muls(edge, t)))

                # Ignore vertex if |pedge| = 0, thus ignoring vertices on the edge itself
                # And so avoid generating degenerate triangles.
                #
                # epsilon chosen by experiment
                plsq = vmath.v3lengthsq(pedge)
                if plsq <= collinear_tolerance:
                    continue

                if i2 == -1:
                    i2 = i
                    maxpedge = pedge
                    maxplsq = plsq
                    maxt = t
                    continue

                # If this is not the first vertex tested, determine if new vertex makes
                # A right turn looking in direction of edge, or is further in same direction.
                #
                # We require a special case when pedge, and maxpedge are coplanar with edge
                # As the computed turn will be 0 and we must check if the cross product
                # Is facing into the hull or outside to determine left/right instead.
                axis = vmath.v3cross(pedge, maxpedge)
                coplanar = vmath.v3dot(pedge, vmath.v3cross(edge, maxpedge))
                # epsilon chosen by experiment
                if (coplanar * coplanar) <= coplanar_tolerance:
                    # Special case for coplanar pedge, maxpedge, edge
                    #
                    # if edges are in same direction, base on distance.
                    if vmath.v3dot(pedge, maxpedge) >= 0:
                        if plsq > maxplsq or (plsq == maxplsq and t > maxt):
                            i2 = i
                            maxpedge = pedge
                            maxplsq = plsq
                            maxt = t
                    else:
                        axis = vmath.v3cross(vmath.v3sub(p, p0), edge)
                        # Check if axis points into the hull.
                        internal = True
                        for p in positions:
                            if vmath.v3dot(axis, vmath.v3sub(p, p0)) < 0:
                                internal = False
                                break

                        if internal:
                            i2 = i
                            maxpedge = pedge
                            maxplsq = plsq
                            maxt = t
                else:
                    turn = vmath.v3dot(axis, edge)
                    # epsilon chosen by experiment
                    if turn < 0 or (turn <= collinear_tolerance and plsq > maxplsq):
                        i2 = i
                        maxpedge = pedge
                        maxplsq = plsq
                        maxt = t

            # Append i2 vertex to hull
            if i2 not in outv:
                outv[i2] = new_index
                new_index += 1

            # Form triangle iff no edge is closed.
            if ( (i0, i1) not in closedset and
                 (i1, i2) not in closedset and
                 (i2, i0) not in closedset ):

                outtriangles.append((i0, i1, i2))
                # Mark visited edges. Open new edges.
                closedset.add((i0, i1))
                closedset.add((i1, i2))
                closedset.add((i2, i0))

                openset.append((i2, i1))
                openset.append((i0, i2))

        # cnt does not 'need' to be len(positions) for convex hull to have succeeded
        #   but numerical issues with not using say fixed point means that we cannot
        #   be sure of success if it is not equal.
        # Obvious side effect is input mesh must already be a convex hull with no
        #   unnecessary vertices.
        cnt = len(outv.items())
        if cnt != len(positions):
            return None

        # Compute output mesh.
        mesh = Mesh()
        mesh.positions = [0] * cnt
        for (i, j) in outv.items():
            mesh.positions[j] = positions[i]
        mesh.primitives = [(outv[i1], outv[i2], outv[i3]) for (i1, i2, i3) in outtriangles]

        # Ensure algorithm has not failed!
        if not mesh.is_convex() or not mesh.simply_closed():
            return None

        return mesh
    # pylint: enable=R0914

    def extend_mesh(self, positions, primitives):
        """Extend mesh with extra set of positions and primitives defiend relative to positions"""
        offset = len(self.positions)
        self.positions.extend(positions)
        self.primitives.extend([(i1 + offset, i2 + offset, i3 + offset) for (i1, i2, i3) in primitives])

    def convex_hulls(self, max_components=-1, allow_non_hulls=False,
                     planar_vertex_count=DEFAULT_PLANAR_HULL_VERTEX_THRESHOLD):
        """Split triangle mesh into set of unconnected convex hulls.

           If max_components != -1, then a ValueError will be raised should the number
           of connected components exceed this value.

           If allow_non_hulls is False, and any component of the mesh was not able
           to be converted then a ValueError will be raised.

           No other vertex data is assumed to exist, and mesh is permitted to be
           mutated.

           The return value is a tuple ([Mesh], Mesh) for the list of convex hulls
           computed, and an additional mesh representing the remainder of the mesh
           which could not be converted (If allow_non_hulls is False, then this
           additional mesh will always be None, otherwise it may still be None
           if all of the mesh was able to be converted)."""
        self.stitch_vertices()
        self.remove_degenerate_primitives()
        self.remove_redundant_vertexes()

        components = self.connected_components()
        if max_components != -1 and len(components) > max_components:
            raise ValueError("Mesh has %d connected components which is more than specified max of %d" %
                             (len(components), max_components))

        if allow_non_hulls:
            triangles = Mesh()
            triangles.positions = [ ]
            triangles.primitives = [ ]

        ret = [ ]
        for (vertices, primitives) in components:
            convex = self.is_convex(vertices, primitives)
            closed = self.simply_closed(primitives)
            planar = self.is_planar(vertices)

            if convex and planar:
                if self.is_convex_planar(vertices) and len(vertices) >= planar_vertex_count:
                    print "Converted to planar convex hull!"
                    ret.append(self.make_planar_convex_hull(vertices))
                else:
                    if allow_non_hulls:
                        triangles.extend_mesh(vertices, primitives)
                    else:
                        raise ValueError("A component of the mesh was not able to be converted")

            elif convex and closed:
                mesh = self.make_convex_hull(vertices)
                if mesh == None:
                    if allow_non_hulls:
                        print "Failed to turn convex closed mesh into convex hull!"
                        triangles.extend_mesh(vertices, primitives)
                    else:
                        raise ValueError("A component of the mesh was not able to be converted")
                else:
                    # Ensure that convex hull can be re-computed correctly as this will be performed
                    # By WebGL physics device.
                    if mesh.make_convex_hull() == None:
                        if allow_non_hulls:
                            print "Convex hull failed to be re-computed!"
                            triangles.extend_mesh(vertices, primitives)
                        else:
                            raise ValueError("A component of the mesh was not able to be converted")
                    else:
                        print "Converted to convex hull!"
                        ret.append(mesh)

            else:
                # Cannot convert component to a convex hull.
                if allow_non_hulls:
                    triangles.extend_mesh(vertices, primitives)
                else:
                    raise ValueError("A component of the mesh was not able to be converted")

        if len(triangles.positions) == 0:
            triangles = None

        return (ret, triangles)

# pylint: enable=R0902
# pylint: enable=R0904

#######################################################################################################################

# pylint: disable=C0111
def __generate_test_square(json_asset):

    def __generate_square_t():
        mesh = Mesh()
        mesh.positions.extend( [ (0, 0, 0), (1, 0, 0), (1, 1, 1), (0, 1, 1), (2, 0, 0), (2, 1, 1) ] )
        mesh.uvs[0].extend( [ (0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (0, 1) ] )
        indexes = [ ]
        indexes.extend( [ 0, 1, 2, 0, 2, 3, 1, 4, 5, 1, 5, 2 ] )
        return (mesh, indexes)

    def __generate_square_b():
        mesh = Mesh()
        mesh.positions.extend( [ (0, 2, 2), (1, 2, 2), (1, 3, 3), (0, 3, 3), (2, 2, 2), (2, 3, 3) ] )
        mesh.uvs[0].extend( [ (0, 0), (0, 1), (1, 1), (1, 0), (0, 0), (1, 0) ] )
        indexes = [ ]
        indexes.extend( [ 0, 1, 2, 0, 2, 3, 1, 4, 5, 1, 5, 2 ] )
        return (mesh, indexes)

    def __generate_split_square_t():
        mesh = Mesh()
        mesh.positions.extend( [ (0, 4, 4), (1, 4, 4), (1, 5, 5), (0, 5, 5), (2, 4, 4), (2, 5, 5),
                                 (1, 4, 4), (1, 5, 5) ] )
        mesh.uvs[0].extend( [ (0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (0, 1),
                              (1, 0), (1, 1) ] )
        indexes = [ ]
        indexes.extend( [ 0, 1, 2, 0, 2, 3, 6, 4, 5, 6, 5, 7 ] )
        return (mesh, indexes)

    def __generate_split_square_b():
        mesh = Mesh()
        mesh.positions.extend( [ (0, 6, 6), (1, 6, 6), (1, 7, 7), (0, 7, 7), (2, 6, 6), (2, 7, 7),
                                 (1, 6, 6), (1, 7, 7) ] )
        mesh.uvs[0].extend( [ (0, 0), (0, 1), (1, 1), (1, 0), (0, 0), (1, 0),
                              (0, 1), (1, 1) ] )
        indexes = [ ]
        indexes.extend( [ 0, 1, 2, 0, 2, 3, 6, 4, 5, 6, 5, 7 ] )
        return (mesh, indexes)

    def __generate_control_square_t():
        r2 = 1 / math.sqrt(2)
        mesh = Mesh()
        mesh.positions.extend( [ (0, 8, 8), (1, 8, 8), (1, 9, 9), (0, 9, 9),
                                 (1, 8, 8), (2, 8, 8), (2, 9, 9), (1, 9, 9) ] )
        mesh.uvs[0].extend( [ (0, 0), (1, 0), (1, 1), (0, 1),
                              (1, 0), (0, 0), (0, 1), (1, 1) ] )
        mesh.tangents.extend( [ (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0),
                                (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0) ] )
        mesh.binormals.extend( [ (0, r2, r2), (0, r2, r2), (0, r2, r2), (0, r2, r2),
                                 (0, r2, r2), (0, r2, r2), (0, r2, r2), (0, r2, r2) ] )
        indexes = [ ]
        indexes.extend( [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7 ] )
        return (mesh, indexes)

    def __generate_control_square_b():
        r2 = 1 / math.sqrt(2)
        mesh = Mesh()
        mesh.positions.extend( [ (0, 10, 10), (1, 10, 10), (1, 11, 11), (0, 11, 11),
                                 (1, 10, 10), (2, 10, 10), (2, 11, 11), (1, 11, 11) ] )
        mesh.uvs[0].extend( [ (0, 0), (0, 1), (1, 1), (1, 0),
                              (0, 1), (0, 0), (1, 0), (1, 1) ] )
        mesh.tangents.extend( [ (0, r2, r2), (0, r2, r2), (0, r2, r2), (0, r2, r2),
                                (0, r2, r2), (0, r2, r2), (0, r2, r2), (0, r2, r2) ] )
        mesh.binormals.extend( [ (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0),
                                 (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0) ] )
        indexes = [ ]
        indexes.extend( [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7 ] )
        return (mesh, indexes)

    def __generate_square((a, i), (s, n), j):
        a.generate_primitives(i)
        a.generate_normals()
        if len(a.tangents) == 0:
            a.generate_smooth_nbts()
        j.attach_shape(s)
        n = NodeName(n)
        j.attach_node(n)
        j.attach_node_shape_instance(n, s, s, m)
        j.attach_positions(a.positions, s)
        j.attach_nbts(a.normals, a.tangents, a.binormals, s)
        j.attach_uvs(a.uvs[0], s)
        j.attach_surface(a.primitives, JsonAsset.SurfaceTriangles, s)

    m = 'material-0'
    e = 'effect-0'
    json_asset.attach_effect(e, 'normalmap')
    json_asset.attach_material(m, e)
    json_asset.attach_texture(m, 'diffuse', '/assets/checker.png')
    json_asset.attach_texture(m, 'normal_map', '/assets/monkey.png')
    __generate_square(__generate_square_t(), ('shape-0', 'node-0'), json_asset)
    __generate_square(__generate_square_b(), ('shape-1', 'node-1'), json_asset)
    __generate_square(__generate_split_square_t(), ('shape-2', 'node-2'), json_asset)
    __generate_square(__generate_split_square_b(), ('shape-3', 'node-3'), json_asset)
    __generate_square(__generate_control_square_t(), ('shape-4', 'node-4'), json_asset)
    __generate_square(__generate_control_square_b(), ('shape-5', 'node-5'), json_asset)

def __generate_test_cube():
    def add_quad_face(v, m, primitives):
        """Append a two triangle quad to the mesh."""
        offset = len(m.positions)
        m.positions.extend( [ v[0], v[1], v[2], v[3] ] )
        m.uvs[0].extend( [ (1, 1), (0, 1), (0, 0), (1, 0) ] )
        primitives.extend( [offset, offset + 2, offset + 1, offset, offset + 3, offset + 2] )

    indexes = [ ]
    mesh = Mesh()
    v = [ (-1, -1, -1), (-1, -1,  1), (-1,  1,  1), (-1,  1, -1),
          ( 1, -1, -1), ( 1, -1,  1), ( 1,  1,  1), ( 1,  1, -1) ]
    add_quad_face( [ v[0], v[1], v[2], v[3] ], mesh, indexes)
    add_quad_face( [ v[4], v[0], v[3], v[7] ], mesh, indexes)
    add_quad_face( [ v[5], v[4], v[7], v[6] ], mesh, indexes)
    add_quad_face( [ v[1], v[5], v[6], v[2] ], mesh, indexes)
    add_quad_face( [ v[7], v[3], v[2], v[6] ], mesh, indexes)
    add_quad_face( [ v[1], v[0], v[4], v[5] ], mesh, indexes)

    mesh.generate_primitives(indexes)
    return mesh

# pylint: enable=C0111

if __name__ == "__main__":
    # pylint: disable=W0403
    from turbulenz_tools.tools.asset2json import JsonAsset
    from turbulenz_tools.tools.node import NodeName
    # pylint: enable=W0403
    logging.basicConfig(level=logging.INFO)

    J = JsonAsset()
    __generate_test_square(J)
    J.clean()
    JSON = J.json_to_string()
    with open("mesh.json", 'w') as output:
        output.write(JSON)
    print JSON
    J.log_metrics()
