import io
import copy
import uuid

import numpy as np

try:
    # pip install pycollada
    import collada
except BaseException:
    collada = None

try:
    import PIL.Image
except ImportError:
    pass

from .. import util
from .. import visual

from ..constants import log


def load_collada(file_obj, resolver=None, **kwargs):
    """
    Load a COLLADA (.dae) file into a list of trimesh kwargs.

    Parameters
    ----------
    file_obj : file object
      Containing a COLLADA file
    resolver : trimesh.visual.Resolver or None
      For loading referenced files, like texture images
    kwargs : **
      Passed to trimesh.Trimesh.__init__

    Returns
    -------
    loaded : list of dict
      kwargs for Trimesh constructor
    """
    # load scene using pycollada
    c = collada.Collada(file_obj)

    # Create material map from Material ID to trimesh material
    material_map = {}
    for m in c.materials:
        effect = m.effect
        material_map[m.id] = _parse_material(effect, resolver)

    # name : kwargs
    meshes = {}
    # list of dict
    graph = []

    for node in c.scene.nodes:
        _parse_node(node=node,
                    parent_matrix=np.eye(4),
                    material_map=material_map,
                    meshes=meshes,
                    graph=graph,
                    resolver=resolver)

    # create kwargs for load_kwargs
    result = {'class': 'Scene',
              'graph': graph,
              'geometry': meshes}

    return result


def export_collada(mesh, **kwargs):
    """
    Export a mesh or a list of meshes as a COLLADA .dae file.

    Parameters
    -----------
    mesh: Trimesh object or list of Trimesh objects
        The mesh(es) to export.

    Returns
    -----------
    export: str, string of COLLADA format output
    """
    meshes = mesh
    if not isinstance(mesh, (list, tuple, set, np.ndarray)):
        meshes = [mesh]

    c = collada.Collada()
    nodes = []
    for i, m in enumerate(meshes):

        # Load uv, colors, materials
        uv = None
        colors = None
        mat = _unparse_material(None)
        if m.visual.defined:
            if m.visual.kind == 'texture':
                mat = _unparse_material(m.visual.material)
                uv = m.visual.uv
            elif m.visual.kind == 'vertex':
                colors = (m.visual.vertex_colors / 255.0)[:, :3]
        c.effects.append(mat.effect)
        c.materials.append(mat)

        # Create geometry object
        vertices = collada.source.FloatSource(
            'verts-array', m.vertices.flatten(), ('X', 'Y', 'Z'))
        normals = collada.source.FloatSource(
            'normals-array', m.vertex_normals.flatten(), ('X', 'Y', 'Z'))
        input_list = collada.source.InputList()
        input_list.addInput(0, 'VERTEX', '#verts-array')
        input_list.addInput(1, 'NORMAL', '#normals-array')
        arrays = [vertices, normals]
        if uv is not None:
            texcoords = collada.source.FloatSource(
                'texcoords-array', uv.flatten(), ('U', 'V'))
            input_list.addInput(2, 'TEXCOORD', '#texcoords-array')
            arrays.append(texcoords)
        if colors is not None:
            idx = 2
            if uv:
                idx = 3
            colors = collada.source.FloatSource('colors-array',
                                                colors.flatten(), ('R', 'G', 'B'))
            input_list.addInput(idx, 'COLOR', '#colors-array')
            arrays.append(colors)
        geom = collada.geometry.Geometry(
            c, uuid.uuid4().hex, uuid.uuid4().hex, arrays
        )
        indices = np.repeat(m.faces.flatten(), len(arrays))

        matref = u'material{}'.format(i)
        triset = geom.createTriangleSet(indices, input_list, matref)
        geom.primitives.append(triset)
        c.geometries.append(geom)

        matnode = collada.scene.MaterialNode(matref, mat, inputs=[])
        geomnode = collada.scene.GeometryNode(geom, [matnode])
        node = collada.scene.Node(u'node{}'.format(i), children=[geomnode])
        nodes.append(node)
    scene = collada.scene.Scene('scene', nodes)
    c.scenes.append(scene)
    c.scene = scene

    b = io.BytesIO()
    c.write(b)
    b.seek(0)
    return b.read()


def _parse_node(node,
                parent_matrix,
                material_map,
                meshes,
                graph,
                resolver=None):
    """
    Recursively parse COLLADA scene nodes.
    """

    # Parse mesh node
    if isinstance(node, collada.scene.GeometryNode):
        geometry = node.geometry

        # Create local material map from material symbol to actual material
        local_material_map = {}
        for mn in node.materials:
            symbol = mn.symbol
            m = mn.target
            if m.id in material_map:
                local_material_map[symbol] = material_map[m.id]
            else:
                local_material_map[symbol] = _parse_material(m, resolver)

        # Iterate over primitives of geometry
        for i, primitive in enumerate(geometry.primitives):
            if isinstance(primitive, collada.polylist.Polylist):
                primitive = primitive.triangleset()
            if isinstance(primitive, collada.triangleset.TriangleSet):
                vertex = primitive.vertex
                vertex_index = primitive.vertex_index
                vertices = vertex[vertex_index].reshape(
                    len(vertex_index) * 3, 3)

                # Get normals if present
                normals = None
                if primitive.normal is not None:
                    normal = primitive.normal
                    normal_index = primitive.normal_index
                    normals = normal[normal_index].reshape(
                        len(normal_index) * 3, 3)

                # Get colors if present
                colors = None
                s = primitive.sources
                if ('COLOR' in s and len(s['COLOR'])
                        > 0 and len(primitive.index) > 0):
                    color = s['COLOR'][0][4].data
                    color_index = primitive.index[:, :, s['COLOR'][0][0]]
                    colors = color[color_index].reshape(
                        len(color_index) * 3, 3)

                faces = np.arange(
                    vertices.shape[0]).reshape(
                    vertices.shape[0] // 3, 3)

                # Get UV coordinates if possible
                vis = None
                if primitive.material in local_material_map:
                    material = copy.copy(
                        local_material_map[primitive.material])
                    uv = None
                    if len(primitive.texcoordset) > 0:
                        texcoord = primitive.texcoordset[0]
                        texcoord_index = primitive.texcoord_indexset[0]
                        uv = texcoord[texcoord_index].reshape(
                            (len(texcoord_index) * 3, 2))
                    vis = visual.texture.TextureVisuals(
                        uv=uv, material=material)

                primid = u'{}.{}'.format(geometry.id, i)
                meshes[primid] = {
                    'vertices': vertices,
                    'faces': faces,
                    'vertex_normals': normals,
                    'vertex_colors': colors,
                    'visual': vis}

                graph.append({'frame_to': primid,
                              'matrix': parent_matrix,
                              'geometry': primid})

    # recurse down tree for nodes with children
    elif isinstance(node, collada.scene.Node):
        if node.children is not None:
            for child in node.children:
                # create the new matrix
                matrix = np.dot(parent_matrix, node.matrix)
                # parse the child node
                _parse_node(
                    node=child,
                    parent_matrix=matrix,
                    material_map=material_map,
                    meshes=meshes,
                    graph=graph,
                    resolver=resolver)

    elif isinstance(node, collada.scene.CameraNode):
        # TODO: convert collada cameras to trimesh cameras
        pass
    elif isinstance(node, collada.scene.LightNode):
        # TODO: convert collada lights to trimesh lights
        pass


def _load_texture(file_name, resolver):
    """
    Load a texture from a file into a PIL image.
    """
    file_data = resolver.get(file_name)
    image = PIL.Image.open(util.wrap_as_stream(file_data))
    return image


def _parse_material(effect, resolver):
    """
    Turn a COLLADA effect into a trimesh material.
    """

    # Compute base color
    baseColorFactor = np.ones(4)
    baseColorTexture = None
    if isinstance(effect.diffuse, collada.material.Map):
        try:
            baseColorTexture = _load_texture(
                effect.diffuse.sampler.surface.image.path, resolver)
        except BaseException:
            log.warning('unable to load base texture',
                        exc_info=True)
    elif effect.diffuse is not None:
        baseColorFactor = effect.diffuse

    # Compute emission color
    emissiveFactor = np.zeros(3)
    emissiveTexture = None
    if isinstance(effect.emission, collada.material.Map):
        try:
            emissiveTexture = _load_texture(
                effect.diffuse.sampler.surface.image.path, resolver)
        except BaseException:
            log.warning('unable to load emissive texture',
                        exc_info=True)
    elif effect.emission is not None:
        emissiveFactor = effect.emission[:3]

    # Compute roughness
    roughnessFactor = 1.0
    if (not isinstance(effect.shininess, collada.material.Map)
            and effect.shininess is not None):
        roughnessFactor = np.sqrt(2.0 / (2.0 + effect.shininess))

    # Compute metallic factor
    metallicFactor = 0.0

    # Compute normal texture
    normalTexture = None
    if effect.bumpmap is not None:
        try:
            normalTexture = _load_texture(
                effect.bumpmap.sampler.surface.image.path, resolver)
        except BaseException:
            log.warning('unable to load bumpmap',
                        exc_info=True)

    return visual.material.PBRMaterial(
        emissiveFactor=emissiveFactor,
        emissiveTexture=emissiveTexture,
        normalTexture=normalTexture,
        baseColorTexture=baseColorTexture,
        baseColorFactor=baseColorFactor,
        metallicFactor=metallicFactor,
        roughnessFactor=roughnessFactor
    )


def _unparse_material(material):
    """
    Turn a trimesh material into a COLLADA material.
    """
    # TODO EXPORT TEXTURES
    if isinstance(material, visual.material.PBRMaterial):
        diffuse = material.baseColorFactor
        if diffuse is not None:
            diffuse = list(diffuse)

        emission = material.emissiveFactor
        if emission is not None:
            emission = [float(emission[0]), float(emission[1]),
                        float(emission[2]), 1.0]

        shininess = material.roughnessFactor
        if shininess is not None:
            shininess = 2.0 / shininess**2 - 2.0

        effect = collada.material.Effect(
            uuid.uuid4().hex, params=[], shadingtype='phong',
            diffuse=diffuse, emission=emission,
            specular=[1.0, 1.0, 1.0, 1.0], shininess=float(shininess)
        )
        material = collada.material.Material(
            uuid.uuid4().hex, 'pbrmaterial', effect
        )
    else:
        effect = collada.material.Effect(
            uuid.uuid4().hex, params=[], shadingtype='phong'
        )
        material = collada.material.Material(
            uuid.uuid4().hex, 'defaultmaterial', effect
        )
    return material


def load_zae(file_obj, resolver=None, **kwargs):
    """
    Load a ZAE file, which is just a zipped DAE file.

    Parameters
    -------------
    file_obj : file object
      Contains ZAE data
    resolver : trimesh.visual.Resolver
      Resolver to load additional assets
    kwargs : dict
      Passed to load_collada

    Returns
    ------------
    loaded : dict
      Results of loading
    """

    # a dict, {file name : file object}
    archive = util.decompress(file_obj,
                              file_type='zip')

    # load the first file with a .dae extension
    file_name = next(i for i in archive.keys()
                     if i.lower().endswith('.dae'))

    # a resolver so the loader can load textures / etc
    resolver = visual.resolvers.ZipResolver(archive)

    # run the regular collada loader
    loaded = load_collada(archive[file_name],
                          resolver=resolver,
                          **kwargs)
    return loaded


# only provide loaders if `pycollada` is installed
_collada_loaders = {}
_collada_exporters = {}
if collada is not None:
    _collada_loaders['dae'] = load_collada
    _collada_loaders['zae'] = load_zae
    _collada_exporters['dae'] = export_collada
