import math 
import numpy
import re

from typing import List, Union, Tuple, Union, Sequence, Callable
from enums import ObjectTypeEnum, NodeGroupTypeEnum, EDMPropsSpecialTypeStr

import animation as anim
import bpy
from bpy.types import Object, Mesh, MeshVertex, MeshVertices
from mathutils import Vector, Quaternion, Matrix

from objects_custom_props import get_edm_props
from pyedm_platform_selector import pyedm
from logger import log
from material_wrap import (FakeOmniLightMaterialWrap, FakeSpotLightMaterialWrap, g_missing_texture_name)
from mesh_storage import MeshStorage
from math_tools import RIGHT_TRANSFORM_MATRIX
import utils

FakeLightIndex = int
FakeLightDelay = float
FakeLightIdxToFrameList = List[Tuple[FakeLightIndex, anim.KeyFramePoints]]

def is_fake_light(object: bpy.types.Object) -> bool:
    edm_props = get_edm_props(object)
    if object.type == ObjectTypeEnum.MESH and edm_props.SPECIAL_TYPE in ('FAKE_LIGHT'):
        return True
    return False

def get_delay_anim_list(lights_anim_delay_list: List[FakeLightDelay], key_list: anim.KeyFramePoints) -> FakeLightIdxToFrameList:
    fake_lights_anim_list: List[FakeLightIndex, anim.KeyFramePoints] = []
    if not key_list:
        return fake_lights_anim_list
    first_key_frame: anim.KeyFramePoint = key_list[0]
    for fake_light_index in range(0, len(lights_anim_delay_list)):
        delay: float = lights_anim_delay_list[fake_light_index]
        new_key_frame_points: anim.KeyFramePoints = []
        for key_frame_index in range(0, len(key_list)):
            key_frame: anim.KeyFramePoint = key_list[key_frame_index]
            #old_key_fame_time = key_frame[0]
            old_key_fame_time = key_frame[0] - first_key_frame[0]
            old_key_fame_point = key_frame[1]
            new_key_fame_time: float = numpy.clip(old_key_fame_time + delay, -1.0, 1.0)
            #new_key_fame_time_normalized: float = (new_key_fame_time + 1.0) * 0.5
            new_key_fame_time_normalized: float = new_key_fame_time * 0.5
            new_key: anim.KeyFrameTime = new_key_fame_time_normalized
            new_key_frame_points.append((new_key, old_key_fame_point))
        fake_lights_anim_list.append((fake_light_index, new_key_frame_points))
    return fake_lights_anim_list

def get_pos(vertex: MeshVertex) -> Tuple[float, float, float]:
    pos_x: float = vertex.co[0]
    pos_y: float = vertex.co[1]
    pos_z: float = vertex.co[2]
    return pos_x, pos_y, pos_z

def get_pos_list(vertices_list: MeshVertices) -> List[Tuple[float, float, float]]:
    pos_list: List[Tuple[float, float, float]] = []
    for vertex in vertices_list:
        pos1 = get_pos(vertex)
        pos_list.append(pos1)
    return pos_list

def make_fake_omni_edm_mat_blocks(object: Object, material_wrap: FakeOmniLightMaterialWrap, mesh_storage: MeshStorage) -> pyedm.FakeOmniLights:
    if material_wrap.node_group_type != NodeGroupTypeEnum.FAKE_OMNI:
        log.warning(f"{object.name} has no fake omni material.")
        return None

    edm_props = get_edm_props(object)

    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]
    light_size: float = edm_props.SIZE
    is_color_texture: bool = not material_wrap.textures.emissive_map.texture == None
    if is_color_texture:
        light_texture: str = material_wrap.textures.emissive_map.texture.texture_name
    else:
        light_texture: str = g_missing_texture_name
        log.warning(f"{object.name} fake omni 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
    edm_luminance_prop = pyedm.PropertyFloat(luminance)
    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

    fake_lights: List[pyedm.FakeOmniLight] = []
    lights_anim_delay_list: List[FakeLightDelay] = []
    is_vertex_group_set: bool = False
    if object.type == ObjectTypeEnum.MESH:
        #bpy_mesh: Mesh = object.to_mesh()
        bpy_mesh: Mesh = object.data
        FakeLightIndex = 0
        for vertex in bpy_mesh.vertices:
            pos: Tuple[float, float, float] = get_pos(vertex)
            edm_fake_omni = pyedm.FakeOmniLight()
            edm_fake_omni.setSize(light_size)
            edm_fake_omni.setPos(pos)
            edm_fake_omni.setUV(uv_start, uv_end)
            fake_lights.append(edm_fake_omni)
            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 omni 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 omni 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 omni 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)
    
    if is_brightness_animated and brightness_arg_n != -1 and is_vertex_group_set:
        edm_render_node = pyedm.AnimatedFakeOmniLight(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)
    elif is_brightness_animated and brightness_arg_n != -1 and not is_vertex_group_set:
        edm_render_node = pyedm.FakeOmniLights(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)
    else:
        edm_render_node = pyedm.FakeOmniLights(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.setTexture(light_texture)
    edm_render_node.setMaxDistance(max_distance)
    edm_render_node.set(fake_lights)

    return edm_render_node

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 = RIGHT_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)

def make_fake_spot_edm_mat_blocks(object: Object, material_wrap: FakeSpotLightMaterialWrap, mesh_storage: MeshStorage) -> pyedm.FakeSpotLights:
    if material_wrap.node_group_type != NodeGroupTypeEnum.FAKE_SPOT:
        log.warning(f"{object.name} has no fake spot material.")
        return None
    
    edm_props = get_edm_props(object)

    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
    two_sided: bool = edm_props.TWO_SIDED
    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 omni 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
    edm_luminance_prop = pyedm.PropertyFloat(luminance)
    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
    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: Tuple[float, float, float] = get_fake_light_direction(object)

    fake_lights: List[pyedm.FakeSpotLight] = []
    lights_anim_delay_list: List[FakeLightDelay] = []
    is_vertex_group_set: bool = False
    if object.type == ObjectTypeEnum.MESH:
        bpy_mesh: Mesh = object.to_mesh()
        FakeLightIndex = 0
        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)
            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)

    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)
    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)
    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.setDirection(light_direction)
    edm_render_node.setMaxDistance(max_distance)    
    edm_render_node.setTexture(light_texture)
    edm_render_node.set(fake_lights)

    return edm_render_node

class FakeLightChildPanel(bpy.types.Panel):
    bl_label = "Fake light type Properties"
    bl_idname = "OBJECT_PT_fake_light_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_parent_id = "OBJECT_PT_edm_data"

    @classmethod
    def poll(cls, context):
        if not utils.has_object(context):
            return

        object: Object = context.object
        props = get_edm_props(object)

        result = props.SPECIAL_TYPE in (EDMPropsSpecialTypeStr.FAKE_LIGHT) and not(object.type == ObjectTypeEnum.LIGHT or object.type == ObjectTypeEnum.LAMP)
        return result

    def draw(self, context):
        if not utils.has_object(context):
            return

        layout = self.layout
        object: Object = context.object
        props = get_edm_props(object)

        row = layout.row()
        row.prop(props, "TWO_SIDED")

        row = layout.row()
        row.prop(props, "LUMINANCE_ARG")

        box = layout.box()

        row = box.row()
        row.prop(props, "UV_LB")

        row = box.row()
        row.prop(props, "UV_RT")

        if props.TWO_SIDED:
            row = box.row()
            row.prop(props, "UV_LB_BACK")

            row = box.row()
            row.prop(props, "UV_RT_BACK")

        row = box.row()
        row.prop(props, "ANIMATED_BRIGHTNESS")
        row.prop(props, "SIZE")
