# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.

import bpy
from bpy.props import BoolProperty, StringProperty, FloatProperty
from bpy.types import Operator, Context, Object, Material

from typing import Set, Dict, Any, List, Tuple, Optional, Union, cast

from .. import cycles_converter
from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial
from ..core.shader import _NodeGroupUtils
from ....core.logging_setup import logger
import traceback


class ConvertMaterialsForCycles(Operator):
    bl_idname = "mmd_tools.convert_materials_for_cycles"
    bl_label = "Convert Materials For Cycles"
    bl_description = "Convert materials of selected objects for Cycles."
    bl_options = {"REGISTER", "UNDO"}

    use_principled: BoolProperty(
        name="Convert to Principled BSDF",
        description="Convert MMD shader nodes to Principled BSDF as well if enabled",
        default=False,
        options={"SKIP_SAVE"},
    )

    clean_nodes: BoolProperty(
        name="Clean Nodes",
        description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
        default=False,
        options={"SKIP_SAVE"},
    )

    @classmethod
    def poll(cls, context: Context) -> bool:
        return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None

    def draw(self, context: Context) -> None:
        layout = self.layout
        layout.prop(self, "use_principled")
        layout.prop(self, "clean_nodes")

    def execute(self, context: Context) -> Set[str]:
        try:
            context.scene.render.engine = "CYCLES"
        except Exception:
            logger.error(f"Failed to change to Cycles render engine: {traceback.format_exc()}")
            self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
            return {"CANCELLED"}
            
        logger.info(f"Converting materials for Cycles with principled={self.use_principled}, clean_nodes={self.clean_nodes}")
        for obj in (x for x in context.selected_objects if x.type == "MESH"):
            logger.debug(f"Converting materials for object: {obj.name}")
            cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
            
        return {"FINISHED"}


class ConvertMaterials(Operator):
    bl_idname = "mmd_tools.convert_materials"
    bl_label = "Convert Materials"
    bl_description = "Convert materials of selected objects."
    bl_options = {"REGISTER", "UNDO"}

    use_principled: BoolProperty(
        name="Convert to Principled BSDF",
        description="Convert MMD shader nodes to Principled BSDF as well if enabled",
        default=True,
        options={"SKIP_SAVE"},
    )

    clean_nodes: BoolProperty(
        name="Clean Nodes",
        description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
        default=True,
        options={"SKIP_SAVE"},
    )

    subsurface: FloatProperty(
        name="Subsurface",
        default=0.001,
        soft_min=0.000,
        soft_max=1.000,
        precision=3,
        options={"SKIP_SAVE"},
    )

    @classmethod
    def poll(cls, context: Context) -> bool:
        return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None

    def execute(self, context: Context) -> Set[str]:
        logger.info(f"Converting materials with principled={self.use_principled}, clean_nodes={self.clean_nodes}, subsurface={self.subsurface}")
        for obj in context.selected_objects:
            if obj.type != "MESH":
                continue
            logger.debug(f"Converting materials for object: {obj.name}")
            cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface)
        return {"FINISHED"}

class ConvertBSDFMaterials(Operator):
    bl_idname = 'mmd_tools.convert_bsdf_materials'
    bl_label = 'Convert Blender Materials'
    bl_description = 'Convert materials of selected objects.'
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context: Context) -> bool:
        return next((x for x in context.selected_objects if x.type == 'MESH'), None) is not None

    def execute(self, context: Context) -> Set[str]:
        logger.info("Converting BSDF materials to MMD shader")
        for obj in context.selected_objects:
            if obj.type != 'MESH':
                continue
            logger.debug(f"Converting BSDF materials for object: {obj.name}")
            cycles_converter.convertToMMDShader(obj)
        return {'FINISHED'}

class _OpenTextureBase:
    """Create a texture for mmd model material."""

    bl_options: Set[str] = {"REGISTER", "UNDO", "INTERNAL"}

    filepath: StringProperty(
        name="File Path",
        description="Filepath used for importing the file",
        maxlen=1024,
        subtype="FILE_PATH",
    )

    use_filter_image: BoolProperty(
        default=True,
        options={"HIDDEN"},
    )

    def invoke(self, context: Context, event: Any) -> Set[str]:
        context.window_manager.fileselect_add(self)
        return {"RUNNING_MODAL"}


class OpenTexture(Operator, _OpenTextureBase):
    bl_idname = "mmd_tools.material_open_texture"
    bl_label = "Open Texture"
    bl_description = "Create main texture of active material"

    def execute(self, context: Context) -> Set[str]:
        mat = context.active_object.active_material
        if not mat:
            logger.error("No active material found")
            return {"CANCELLED"}
            
        logger.info(f"Creating texture for material: {mat.name} from {self.filepath}")
        fnMat = FnMaterial(mat)
        fnMat.create_texture(self.filepath)
        return {"FINISHED"}


class RemoveTexture(Operator):
    """Create a texture for mmd model material."""

    bl_idname = "mmd_tools.material_remove_texture"
    bl_label = "Remove Texture"
    bl_description = "Remove main texture of active material"
    bl_options = {"REGISTER", "UNDO", "INTERNAL"}

    def execute(self, context: Context) -> Set[str]:
        mat = context.active_object.active_material
        if not mat:
            logger.error("No active material found")
            return {"CANCELLED"}
            
        logger.info(f"Removing texture from material: {mat.name}")
        fnMat = FnMaterial(mat)
        fnMat.remove_texture()
        return {"FINISHED"}


class OpenSphereTextureSlot(Operator, _OpenTextureBase):
    """Create a texture for mmd model material."""

    bl_idname = "mmd_tools.material_open_sphere_texture"
    bl_label = "Open Sphere Texture"
    bl_description = "Create sphere texture of active material"

    def execute(self, context: Context) -> Set[str]:
        mat = context.active_object.active_material
        if not mat:
            logger.error("No active material found")
            return {"CANCELLED"}
            
        logger.info(f"Creating sphere texture for material: {mat.name} from {self.filepath}")
        fnMat = FnMaterial(mat)
        fnMat.create_sphere_texture(self.filepath, context.active_object)
        return {"FINISHED"}


class RemoveSphereTexture(Operator):
    """Create a texture for mmd model material."""

    bl_idname = "mmd_tools.material_remove_sphere_texture"
    bl_label = "Remove Sphere Texture"
    bl_description = "Remove sphere texture of active material"
    bl_options = {"REGISTER", "UNDO", "INTERNAL"}

    def execute(self, context: Context) -> Set[str]:
        mat = context.active_object.active_material
        if not mat:
            logger.error("No active material found")
            return {"CANCELLED"}
            
        logger.info(f"Removing sphere texture from material: {mat.name}")
        fnMat = FnMaterial(mat)
        fnMat.remove_sphere_texture()
        return {"FINISHED"}


class MoveMaterialUp(Operator):
    bl_idname = "mmd_tools.move_material_up"
    bl_label = "Move Material Up"
    bl_description = "Moves selected material one slot up"
    bl_options = {"REGISTER", "UNDO"}

    @classmethod
    def poll(cls, context: Context) -> bool:
        obj = context.active_object
        valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
        return bool(valid_mesh and obj.active_material_index > 0)

    def execute(self, context: Context) -> Set[str]:
        obj = context.active_object
        current_idx = obj.active_material_index
        prev_index = current_idx - 1
        
        logger.debug(f"Moving material {current_idx} up to position {prev_index} for object {obj.name}")
        try:
            FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
        except MaterialNotFoundError:
            logger.error(f"Materials not found for indices {current_idx} and {prev_index}")
            self.report({"ERROR"}, "Materials not found")
            return {"CANCELLED"}
        obj.active_material_index = prev_index

        return {"FINISHED"}


class MoveMaterialDown(Operator):
    bl_idname = "mmd_tools.move_material_down"
    bl_label = "Move Material Down"
    bl_description = "Moves the selected material one slot down"
    bl_options = {"REGISTER", "UNDO"}

    @classmethod
    def poll(cls, context: Context) -> bool:
        obj = context.active_object
        valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
        return bool(valid_mesh and obj.active_material_index < len(obj.material_slots) - 1)

    def execute(self, context: Context) -> Set[str]:
        obj = context.active_object
        current_idx = obj.active_material_index
        next_index = current_idx + 1
        
        logger.debug(f"Moving material {current_idx} down to position {next_index} for object {obj.name}")
        try:
            FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
        except MaterialNotFoundError:
            logger.error(f"Materials not found for indices {current_idx} and {next_index}")
            self.report({"ERROR"}, "Materials not found")
            return {"CANCELLED"}
        obj.active_material_index = next_index
        return {"FINISHED"}


class EdgePreviewSetup(Operator):
    bl_idname = "mmd_tools.edge_preview_setup"
    bl_label = "Edge Preview Setup"
    bl_description = 'Preview toon edge settings of active model using "Solidify" modifier'
    bl_options = {"REGISTER", "UNDO", "INTERNAL"}

    action: bpy.props.EnumProperty(
        name="Action",
        description="Select action",
        items=[
            ("CREATE", "Create", "Create toon edge", 0),
            ("CLEAN", "Clean", "Clear toon edge", 1),
        ],
        default="CREATE",
    )

    def execute(self, context: Context) -> Set[str]:
        from ..core.model import FnModel

        root = FnModel.find_root_object(context.active_object)
        if root is None:
            logger.error("No MMD model root found")
            self.report({"ERROR"}, "Select a MMD model")
            return {"CANCELLED"}

        if self.action == "CLEAN":
            logger.info(f"Cleaning toon edge for model: {root.name}")
            for obj in FnModel.iterate_mesh_objects(root):
                self.__clean_toon_edge(obj)
        else:
            from ..bpyutils import Props

            logger.info(f"Creating toon edge for model: {root.name}")
            scale = 0.2 * getattr(root, Props.empty_display_size)
            counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root))
            logger.info(f"Created {counts} toon edge(s)")
            self.report({"INFO"}, "Created %d toon edge(s)" % counts)
        return {"FINISHED"}

    def __clean_toon_edge(self, obj: Object) -> None:
        logger.debug(f"Cleaning toon edge for object: {obj.name}")
        if "mmd_edge_preview" in obj.modifiers:
            obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])

        if "mmd_edge_preview" in obj.vertex_groups:
            obj.vertex_groups.remove(obj.vertex_groups["mmd_edge_preview"])

        FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))

    def __create_toon_edge(self, obj: Object, scale: float = 1.0) -> int:
        logger.debug(f"Creating toon edge for object: {obj.name} with scale {scale}")
        self.__clean_toon_edge(obj)
        materials = obj.data.materials
        material_offset = len(materials)
        for m in tuple(materials):
            if m and m.mmd_material.enabled_toon_edge:
                mat_edge = self.__get_edge_material("mmd_edge." + m.name, m.mmd_material.edge_color, materials)
                materials.append(mat_edge)
            elif material_offset > 1:
                mat_edge = self.__get_edge_material("mmd_edge.disabled", (0, 0, 0, 0), materials)
                materials.append(mat_edge)
        if len(materials) > material_offset:
            mod = obj.modifiers.get("mmd_edge_preview", None)
            if mod is None:
                mod = obj.modifiers.new("mmd_edge_preview", "SOLIDIFY")
            mod.material_offset = material_offset
            mod.thickness_vertex_group = 1e-3  # avoid overlapped faces
            mod.use_flip_normals = True
            mod.use_rim = False
            mod.offset = 1
            self.__create_edge_preview_group(obj)
            mod.thickness = scale
            mod.vertex_group = "mmd_edge_preview"
        return len(materials) - material_offset

    def __create_edge_preview_group(self, obj: Object) -> None:
        vertices, materials = obj.data.vertices, obj.data.materials
        weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m}
        scale_map: Dict[int, float] = {}
        vg_scale_index = obj.vertex_groups.find("mmd_edge_scale")
        if vg_scale_index >= 0:
            scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index}
        vg_edge_preview = obj.vertex_groups.new(name="mmd_edge_preview")
        for i, mi in {v: f.material_index for f in reversed(obj.data.polygons) for v in f.vertices}.items():
            weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02
            vg_edge_preview.add(index=[i], weight=weight, type="REPLACE")

    def __get_edge_material(self, mat_name: str, edge_color: Tuple[float, float, float, float], materials: List[Material]) -> Material:
        if mat_name in materials:
            return materials[mat_name]
        mat = bpy.data.materials.get(mat_name, None)
        if mat is None:
            mat = bpy.data.materials.new(mat_name)
        mmd_mat = mat.mmd_material
        # note: edge affects ground shadow
        mmd_mat.is_double_sided = mmd_mat.enabled_drop_shadow = False
        mmd_mat.enabled_self_shadow_map = mmd_mat.enabled_self_shadow = False
        # mmd_mat.enabled_self_shadow_map = True # for blender 2.78+ BI viewport only
        mmd_mat.diffuse_color = mmd_mat.specular_color = (0, 0, 0)
        mmd_mat.ambient_color = edge_color[:3]
        mmd_mat.alpha = edge_color[3]
        mmd_mat.edge_color = edge_color
        self.__make_shader(mat)
        return mat

    def __make_shader(self, m: Material) -> None:
        m.use_nodes = True
        nodes, links = m.node_tree.nodes, m.node_tree.links

        node_shader = nodes.get("mmd_edge_preview", None)
        if node_shader is None or not any(s.is_linked for s in node_shader.outputs):
            XPOS, YPOS = 210, 110
            nodes.clear()
            node_shader = nodes.new("ShaderNodeGroup")
            node_shader.name = "mmd_edge_preview"
            node_shader.location = (0, 0)
            node_shader.width = 200
            node_shader.node_tree = self.__get_edge_preview_shader()

            node_out = nodes.new("ShaderNodeOutputMaterial")
            node_out.location = (XPOS * 2, YPOS * 0)
            links.new(node_shader.outputs["Shader"], node_out.inputs["Surface"])

        node_shader.inputs["Color"].default_value = m.mmd_material.edge_color
        node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3]

    def __get_edge_preview_shader(self) -> bpy.types.NodeTree:
        group_name = "MMDEdgePreview"
        shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
        if len(shader.nodes):
            return shader

        ng = _NodeGroupUtils(shader)

        node_input = ng.new_node("NodeGroupInput", (-5, 0))
        node_output = ng.new_node("NodeGroupOutput", (3, 0))

        ############################################################################
        node_color = ng.new_node("ShaderNodeMixRGB", (-1, -1.5))
        node_color.mute = True

        ng.new_input_socket("Color", node_color.inputs["Color1"])

        ############################################################################
        node_ray = ng.new_node("ShaderNodeLightPath", (-3, 1.5))
        node_geo = ng.new_node("ShaderNodeNewGeometry", (-3, 0))
        node_max = ng.new_math_node("MAXIMUM", (-2, 1.5))
        node_max.mute = True
        node_gt = ng.new_math_node("GREATER_THAN", (-1, 1))
        node_alpha = ng.new_math_node("MULTIPLY", (0, 1))
        node_trans = ng.new_node("ShaderNodeBsdfTransparent", (0, 0))
        node_rgb = ng.new_node("ShaderNodeBackground", (0, -0.5))
        node_mix = ng.new_node("ShaderNodeMixShader", (1, 0.5))

        links = ng.links
        links.new(node_ray.outputs["Is Camera Ray"], node_max.inputs[0])
        links.new(node_ray.outputs["Is Glossy Ray"], node_max.inputs[1])
        links.new(node_max.outputs["Value"], node_gt.inputs[0])
        links.new(node_geo.outputs["Backfacing"], node_gt.inputs[1])
        links.new(node_gt.outputs["Value"], node_alpha.inputs[0])
        links.new(node_alpha.outputs["Value"], node_mix.inputs["Fac"])
        links.new(node_trans.outputs["BSDF"], node_mix.inputs[1])
        links.new(node_rgb.outputs[0], node_mix.inputs[2])
        links.new(node_color.outputs["Color"], node_rgb.inputs["Color"])

        ng.new_input_socket("Alpha", node_alpha.inputs[1])
        ng.new_output_socket("Shader", node_mix.outputs["Shader"])

        return shader
