import re
import math
from typing import List, Union, Tuple, Union, Sequence, Callable
from mathutils import Vector, Quaternion, Matrix

from bpy.types import Object, Mesh, MeshVertex, MeshVertices

import utils
import animation as anim
from logger import log
from materials.material_interface import IMaterial
from enums import ObjectTypeEnum, NodeGroupTypeEnum
from mesh_builder import get_mesh
from objects_custom_props import get_edm_props 
from math_tools import RIGHT_TRANSFORM_MATRIX, ROOT_TRANSFORM_MATRIX
from pyedm_platform_selector import pyedm
from materials.material_wrap import FakeSpotLightMaterialWrap, g_missing_texture_name
from materials.material_fake_common import FakeLightIndex, FakeLightDelay, parse_faces, get_pos, get_delay_anim_list

ObjectCheckFn = Callable[[Object], bool]

fake_light_dir_re_c = re.compile(r'^([Ll]ight_[Dd]ir(ection)?|[Ll][Dd])')
def fake_light_obj_test(obj: Object) -> bool:
    is_type_match: bool = obj.type == ObjectTypeEnum.EMPTY
    is_name_match: bool = re.match(fake_light_dir_re_c, obj.name)
    return is_type_match and is_name_match

def get_first_children(object_children: Sequence[Object], obj_fn: ObjectCheckFn) -> Union[Object, None]:
    if not object_children:
        return None
    for child_obj in object_children:
        is_obj_match: bool = obj_fn(child_obj)
        if is_obj_match:
            return child_obj
    return None

def get_children(object_children: Sequence[Object], obj_fn: ObjectCheckFn) -> List[Object]:
    result: List[Object] = []
    if not object_children:
        return result
    for child_obj in object_children:
        is_obj_match: bool = obj_fn(child_obj)
        if is_obj_match:
            result.append(child_obj)
    return result

def check_fake_light_direction(object: Object) -> None:
    chilren_dir: List[Object] = get_children(object.children, fake_light_obj_test)

    if len(chilren_dir) > 1:
        log.warning(f"{object.name} fake spot light has more then one child direction objects.")
        for chold_obj in chilren_dir:
            log.warning(f"{chold_obj.name} -- fake spot light child direction.")

def get_fake_light_direction(object: Object) -> Tuple[float, float, float]:
    light_direction: Tuple[float, float, float] = (1.0, 0.0, 0.0)
    check_fake_light_direction(object)
    light_dir_obj: Object = get_first_children(object.children, fake_light_obj_test)
    if light_dir_obj:
        world_matrix: Matrix = light_dir_obj.matrix_world
    else:
        world_matrix: Matrix = object.matrix_world
        
    loc, rot, sca = world_matrix.decompose()
    qrot: Quaternion = rot
    rot_mat = qrot.to_matrix()
    rot_mat = ROOT_TRANSFORM_MATRIX.to_3x3() @ rot_mat.copy()
    trans_ld: Vector = rot_mat @ Vector(light_direction)
    return (trans_ld.x, trans_ld.y, trans_ld.z)

# --- Spot
class SpotFakeLightsMaterial(IMaterial):
    name = NodeGroupTypeEnum.FAKE_SPOT
    description_file_name = 'data/EDM_Fake_Spot_Material.pickle'
    factory = FakeSpotLightMaterialWrap
    
    @classmethod
    def build_blocks(cls, object, material_wrap, mesh_storage):
        if material_wrap.node_group_type != SpotFakeLightsMaterial.name:
            log.warning(f"{object.name} has no fake omni material.")
            return None

        edm_props = get_edm_props(object)

        is_color_texture: bool = not material_wrap.textures.emissive.texture == None
        if is_color_texture:
            light_texture: str = material_wrap.textures.emissive.texture.texture_name
        else:
            light_texture: str = g_missing_texture_name
            log.warning(f"{object.name} fake spot must have emissive texture.")
        
        min_size_pixels: float = material_wrap.values.min_size_pixels.value
        max_distance: float = material_wrap.values.max_distance.value
        shift_to_camera: float = material_wrap.values.shift_to_camera.value
        luminance: float = material_wrap.values.luminance.value
        
        luminance_animation_path: str =  material_wrap.values.luminance.anim_path if material_wrap.valid else None
        is_luminance_animated: bool = luminance_animation_path and anim.has_path_anim(material_wrap.material.node_tree.animation_data, luminance_animation_path)
        brightness_animation_path: str = 'EDMProps.ANIMATED_BRIGHTNESS'
        is_brightness_animated: bool = anim.has_path_anim(object.animation_data, brightness_animation_path)
        brightness_arg_n: int = utils.extract_arg_number(object.animation_data.action.name) if is_brightness_animated else -1
        
        edm_luminance_prop = pyedm.PropertyFloat(luminance)

        # spot light characteristics
        phi: float = material_wrap.values.phi.value
        theta: float = material_wrap.values.theta.value
        cone_setup: Tuple[float, float, float] = (
            math.cos(math.radians(theta)),  #cos of inner cone angle
            math.cos(math.radians(phi)),    #cos of outer cone angle
            0.05                            #min attenuation value
        )

        light_direction = None
        if not edm_props.SURFACE_MODE:
            two_sided: bool = edm_props.TWO_SIDED
            uv_start: Tuple[float, float] = edm_props.UV_LB[0], edm_props.UV_LB[1]
            uv_end: Tuple[float, float] = edm_props.UV_RT[0], edm_props.UV_RT[1]
            uv_start_back: Tuple[float, float] = edm_props.UV_LB_BACK[0], edm_props.UV_LB_BACK[1]
            uv_end_back: Tuple[float, float] = edm_props.UV_RT_BACK[0], edm_props.UV_RT_BACK[1]
            light_size: float = edm_props.SIZE 
            
            # single light direction   
            light_direction = get_fake_light_direction(object) #: Tuple[float, float, float]

        fake_lights: List[pyedm.FakeSpotLight] = []
        lights_anim_delay_list: List[FakeLightDelay] = [] # only for rabbit lights
        is_vertex_group_set: bool = False
        
        if object.type == ObjectTypeEnum.MESH:
            FakeLightIndex = 0
            bpy_mesh = get_mesh(object)

            if edm_props.SURFACE_MODE:
                centers, normals, light_sizes, uv_layers = parse_faces(bpy_mesh, object)
                uvs_front = uv_layers['front']

                for i, (center, light_size, uv_f) in enumerate(zip(centers, light_sizes, uvs_front)):
                    edm_fake_spot = pyedm.FakeSpotLight()
                    edm_fake_spot.setSize(float(light_size))
                    edm_fake_spot.setPos(tuple(center))
                    (pt1, pt2) = tuple(map(tuple, uv_f))
                    edm_fake_spot.setUV(pt1, pt2)         
                    if len(uv_layers) > 1:
                        edm_fake_spot.setBackSide(True)
                        (pt1, pt2) = tuple(map(tuple, uv_layers['back'][i]))
                        edm_fake_spot.setBackUV(pt1, pt2)
                    else:
                        edm_fake_spot.setBackSide(False)
                    edm_fake_spot.setDirection(normals[i])
                    
                    fake_lights.append(edm_fake_spot)

                    # TODO: not sure this is needed for surface mode
                    lights_anim_delay_list.append(0.0)
                    FakeLightIndex += 1
            else:
                for vertex in bpy_mesh.vertices:
                    pos: Tuple[float, float, float] = get_pos(vertex)
                    edm_fake_spot = pyedm.FakeSpotLight()
                    edm_fake_spot.setSize(light_size)
                    edm_fake_spot.setPos(pos)
                    edm_fake_spot.setUV(uv_start, uv_end)
                    edm_fake_spot.setBackSide(two_sided)
                    edm_fake_spot.setBackUV(uv_start_back, uv_end_back)
                    edm_fake_spot.setDirection(light_direction)
                    fake_lights.append(edm_fake_spot)
                    if len(vertex.groups) > 0:
                        is_vertex_group_set = True
                        lights_anim_delay_list.append(vertex.groups[0].weight)
                    else:
                        lights_anim_delay_list.append(0.0)
                    FakeLightIndex += 1
        elif object.type == ObjectTypeEnum.CURVE:
            pass

        if is_luminance_animated and edm_props.LUMINANCE_ARG != -1 and is_brightness_animated and brightness_arg_n != -1 and is_vertex_group_set:
            log.warning(f"{object.name} fake spot light has material and geometry luminamce animation. Only geometry luminamce animation exported!")
        if is_luminance_animated and edm_props.LUMINANCE_ARG == -1:
            log.warning(f"{object.name} fake spot light has material animation but LUMINANCE_ARG not set.")
        if is_brightness_animated and brightness_arg_n == -1 and is_vertex_group_set:
            log.warning(f"{object.name} fake spot light has geometry animation but brightness action name not has arg.")
        
        if is_luminance_animated and edm_props.LUMINANCE_ARG != -1:
            key_list: anim.KeyFramePoints = anim.extract_anim_float(material_wrap.material.node_tree.animation_data.action, luminance_animation_path)
            arg_n: int = edm_props.LUMINANCE_ARG
            edm_luminance_prop = pyedm.PropertyFloat(arg_n, key_list)

        # light animation: rabbit lights
        if is_brightness_animated and brightness_arg_n != -1 and is_vertex_group_set:
            edm_render_node = pyedm.AnimatedFakeSpotLight(object.name)
            key_list: anim.KeyFramePoints = anim.extract_anim_float(object.animation_data.action, brightness_animation_path)
            fake_lights_anim_list: List[FakeLightIndex, anim.KeyFramePoints] = get_delay_anim_list(lights_anim_delay_list, key_list)
            edm_render_node.setAnimationArg(brightness_arg_n)
            edm_render_node.setLightsAnimation(fake_lights_anim_list)
        # light animation: flashing lamp
        elif is_brightness_animated and brightness_arg_n != -1 and not is_vertex_group_set:
            edm_render_node = pyedm.FakeSpotLights(object.name)
            key_list: anim.KeyFramePoints = anim.extract_anim_float(object.animation_data.action, brightness_animation_path)
            edm_luminance_prop = pyedm.PropertyFloat(brightness_arg_n, key_list)
        # no animation
        else:
            edm_render_node = pyedm.FakeSpotLights(object.name)

        edm_render_node.setMinSizeInPixels(min_size_pixels)
        edm_render_node.setShiftToCamera(shift_to_camera)
        edm_render_node.setLuminance(edm_luminance_prop)
        edm_render_node.setConeSetup(cone_setup)
        edm_render_node.setMaxDistance(max_distance)    
        edm_render_node.setTexture(light_texture)
        edm_render_node.set(fake_lights)

        return edm_render_node
    
    @classmethod
    def process_links(cls, old_links, version, group_node_type_name):
        return old_links
    
    @classmethod
    def restore_defaults(cls, old_sockest, new_node_group, old_version, material_name):
        pass
