# SPDX-License-Identifier: Apache-2.0
# Copyright 2018-2021 The glTF-Blender-IO authors.

import bpy
from typing import List, Optional, Tuple
import numpy as np

from .gltf2_blender_export_keys import NORMALS, MORPH_NORMAL, TANGENTS, MORPH_TANGENT, MORPH

from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, cached_by_key
from io_scene_gltf2.blender.exp import gltf2_blender_extract
from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors
from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitive_attributes
from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials
from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_variants

from io_scene_gltf2.io.com import gltf2_io
from io_scene_gltf2.io.exp import gltf2_io_binary_data
from io_scene_gltf2.io.com import gltf2_io_constants
from io_scene_gltf2.io.com import gltf2_io_extensions
from io_scene_gltf2.io.com.gltf2_io_debug import print_console


@cached
def get_primitive_cache_key(
        blender_mesh,
        blender_object,
        vertex_groups,
        modifiers,
        materials,
        export_settings):

    # Use id of mesh
    # Do not use bpy.types that can be unhashable
    # Do not use mesh name, that can be not unique (when linked)

    # TODO check what is really needed for modifiers

    return (
        (id(blender_mesh),),
        (modifiers,),
        tuple(id(m) if m is not None else None for m in materials)
    )


@cached_by_key(key=get_primitive_cache_key)
def gather_primitives(
        blender_mesh: bpy.types.Mesh,
        uuid_for_skined_data,
        vertex_groups: Optional[bpy.types.VertexGroups],
        modifiers: Optional[bpy.types.ObjectModifiers],
        materials: Tuple[bpy.types.Material],
        export_settings
        ) -> List[gltf2_io.MeshPrimitive]:
    """
    Extract the mesh primitives from a blender object

    :return: a list of glTF2 primitives
    """
    primitives = []

    # retrieve active render UVMap
    active_uvmap_idx = 0
    for i in range(len(blender_mesh.uv_layers)):
        if blender_mesh.uv_layers[i].active_render is True:
            active_uvmap_idx = i
            break

    blender_primitives = __gather_cache_primitives(blender_mesh, uuid_for_skined_data,
        vertex_groups, modifiers, export_settings)

    for internal_primitive in blender_primitives:
        material_idx = internal_primitive['material']
        material = None

        if export_settings['gltf_materials'] == "EXPORT" and material_idx is not None:
            blender_material = None
            mat = None
            if materials:
                i = material_idx if material_idx < len(materials) else -1
                mat = materials[i]
            if mat is not None:
                material = gltf2_blender_gather_materials.gather_material(
                    mat,
                    active_uvmap_idx,
                    export_settings
                )

        primitive = gltf2_io.MeshPrimitive(
            attributes=internal_primitive['attributes'],
            extensions=__gather_extensions(blender_mesh, material_idx, active_uvmap_idx, export_settings),
            extras=None,
            indices=internal_primitive['indices'],
            material=material,
            mode=internal_primitive['mode'],
            targets=internal_primitive['targets']
        )
        primitives.append(primitive)

    return primitives

@cached
def __gather_cache_primitives(
        blender_mesh: bpy.types.Mesh,
        uuid_for_skined_data,
        vertex_groups: Optional[bpy.types.VertexGroups],
        modifiers: Optional[bpy.types.ObjectModifiers],
        export_settings
) -> List[dict]:
    """
    Gather parts that are identical for instances, i.e. excluding materials
    """
    primitives = []

    blender_primitives = gltf2_blender_extract.extract_primitives(
        blender_mesh, uuid_for_skined_data, vertex_groups, modifiers, export_settings)

    for internal_primitive in blender_primitives:
        primitive = {
            "attributes": __gather_attributes(internal_primitive, blender_mesh, modifiers, export_settings),
            "indices": __gather_indices(internal_primitive, blender_mesh, modifiers, export_settings),
            "mode": internal_primitive.get('mode'),
            "material": internal_primitive.get('material'),
            "targets": __gather_targets(internal_primitive, blender_mesh, modifiers, export_settings)
        }
        primitives.append(primitive)

    return primitives

def __gather_indices(blender_primitive, blender_mesh, modifiers, export_settings):
    indices = blender_primitive.get('indices')
    if indices is None:
        return None

    # NOTE: Values used by some graphics APIs as "primitive restart" values are disallowed.
    # Specifically, the values 65535 (in UINT16) and 4294967295 (in UINT32) cannot be used as indices.
    # https://github.com/KhronosGroup/glTF/issues/1142
    # https://github.com/KhronosGroup/glTF/pull/1476/files
    # Also, UINT8 mode is not supported:
    # https://github.com/KhronosGroup/glTF/issues/1471
    max_index = indices.max()
    if max_index < 65535:
        component_type = gltf2_io_constants.ComponentType.UnsignedShort
        indices = indices.astype(np.uint16, copy=False)
    elif max_index < 4294967295:
        component_type = gltf2_io_constants.ComponentType.UnsignedInt
        indices = indices.astype(np.uint32, copy=False)
    else:
        print_console('ERROR', 'A mesh contains too many vertices (' + str(max_index) + ') and needs to be split before export.')
        return None

    element_type = gltf2_io_constants.DataType.Scalar
    binary_data = gltf2_io_binary_data.BinaryData(indices.tobytes(), bufferViewTarget=gltf2_io_constants.BufferViewTarget.ELEMENT_ARRAY_BUFFER)
    return gltf2_blender_gather_accessors.gather_accessor(
        binary_data,
        component_type,
        len(indices),
        None,
        None,
        element_type,
        export_settings
    )


def __gather_attributes(blender_primitive, blender_mesh, modifiers, export_settings):
    return gltf2_blender_gather_primitive_attributes.gather_primitive_attributes(blender_primitive, export_settings)


def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings):
    if export_settings[MORPH]:
        targets = []
        if blender_mesh.shape_keys is not None:
            morph_index = 0
            for blender_shape_key in blender_mesh.shape_keys.key_blocks:
                if blender_shape_key == blender_shape_key.relative_key:
                    continue

                if blender_shape_key.mute is True:
                    continue

                target_position_id = 'MORPH_POSITION_' + str(morph_index)
                target_normal_id = 'MORPH_NORMAL_' + str(morph_index)
                target_tangent_id = 'MORPH_TANGENT_' + str(morph_index)

                if blender_primitive["attributes"].get(target_position_id) is not None:
                    target = {}
                    internal_target_position = blender_primitive["attributes"][target_position_id]
                    target["POSITION"] = gltf2_blender_gather_primitive_attributes.array_to_accessor(
                        internal_target_position,
                        component_type=gltf2_io_constants.ComponentType.Float,
                        data_type=gltf2_io_constants.DataType.Vec3,
                        include_max_and_min=True,
                    )

                    if export_settings[NORMALS] \
                            and export_settings[MORPH_NORMAL] \
                            and blender_primitive["attributes"].get(target_normal_id) is not None:

                        internal_target_normal = blender_primitive["attributes"][target_normal_id]
                        target['NORMAL'] = gltf2_blender_gather_primitive_attributes.array_to_accessor(
                            internal_target_normal,
                            component_type=gltf2_io_constants.ComponentType.Float,
                            data_type=gltf2_io_constants.DataType.Vec3,
                        )

                    if export_settings[TANGENTS] \
                            and export_settings[MORPH_TANGENT] \
                            and blender_primitive["attributes"].get(target_tangent_id) is not None:
                        internal_target_tangent = blender_primitive["attributes"][target_tangent_id]
                        target['TANGENT'] = gltf2_blender_gather_primitive_attributes.array_to_accessor(
                            internal_target_tangent,
                            component_type=gltf2_io_constants.ComponentType.Float,
                            data_type=gltf2_io_constants.DataType.Vec3,
                        )
                    targets.append(target)
                    morph_index += 1
        return targets
    return None

def __gather_extensions(blender_mesh,
                        material_idx: int,
                        active_uvmap_idx,
                        export_settings):
    extensions = {}

    if bpy.context.preferences.addons['io_scene_gltf2'].preferences.KHR_materials_variants_ui is False:
        return None

    if bpy.data.scenes[0].get('gltf2_KHR_materials_variants_variants') is None:
        return None
    if len(bpy.data.scenes[0]['gltf2_KHR_materials_variants_variants']) == 0:
        return None

    # Material idx is the slot idx. Retrieve associated variant, if any
    mapping = []
    for i in [v for v in blender_mesh.gltf2_variant_mesh_data if v.material_slot_index == material_idx]:
        variants = []
        for idx, v in enumerate(i.variants):
            if v.variant.variant_idx in [o.variant.variant_idx for o in i.variants[:idx]]:
                # Avoid duplicates
                continue
            vari = gltf2_blender_gather_materials_variants.gather_variant(v.variant.variant_idx, export_settings)
            if vari is not None:
                variant_extension = gltf2_io_extensions.ChildOfRootExtension(
                name="KHR_materials_variants",
                path=["variants"],
                extension=vari
            )
            variants.append(variant_extension)
        if len(variants) > 0:
            if i.material:
                mat = gltf2_blender_gather_materials.gather_material(
                        i.material,
                        active_uvmap_idx,
                        export_settings
                    )
            else:
                # empty slot
                mat = None
            mapping.append({'material': mat, 'variants': variants})

    if len(mapping) > 0:
        extensions["KHR_materials_variants"] = gltf2_io_extensions.Extension(
            name="KHR_materials_variants",
            extension={
                "mappings": mapping
            }
        )

    return extensions if extensions else None
