# Copyright 2014 MMD Tools authors
# This file is part of MMD Tools.

"""Properties for MMD model root object"""

import bpy

from ..bpyutils import FnContext
from ..core.material import FnMaterial
from ..core.model import FnModel
from ..core.sdef import FnSDEF
from . import patch_library_overridable
from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph
from .translations import MMDTranslation

IS_BLENDER_50_UP = bpy.app.version >= (5, 0)


def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1):
    d = constraint.driver_add(path, index)
    variables = d.driver.variables
    for x in reversed(variables):
        variables.remove(x)
    return d.driver, variables


def __add_single_prop(variables, id_obj, data_path, prefix):
    var = variables.new()
    var.name = prefix + str(len(variables))
    var.type = "SINGLE_PROP"
    target = var.targets[0]
    target.id_type = "OBJECT"
    target.id = id_obj
    target.data_path = data_path
    return var


def _toggleUsePropertyDriver(self: "MMDRoot", _context):
    root_object: bpy.types.Object = self.id_data
    armature_object = FnModel.find_armature_object(root_object)

    if armature_object is None:
        ik_map = {}
    else:
        bones = armature_object.pose.bones
        ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones}

    if self.use_property_driver:
        for ik, (b, c) in ik_map.items():
            driver, variables = __driver_variables(c, "influence")
            driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name
            b = b if c.use_tail else b.parent
            for b in ([b] + b.parent_recursive)[: c.chain_count]:
                c = next((c for c in b.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None)
                if c:
                    driver, variables = __driver_variables(c, "influence")
                    driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name
        for i in FnModel.iterate_mesh_objects(root_object):
            for prop_hide in ("hide_viewport", "hide_render"):
                driver, variables = __driver_variables(i, prop_hide)
                driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name
    else:
        for ik, (b, c) in ik_map.items():
            c.driver_remove("influence")
            b = b if c.use_tail else b.parent
            for b in ([b] + b.parent_recursive)[: c.chain_count]:
                c = next((c for c in b.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None)
                if c:
                    c.driver_remove("influence")
        for i in FnModel.iterate_mesh_objects(root_object):
            for prop_hide in ("hide_viewport", "hide_render"):
                i.driver_remove(prop_hide)


# ===========================================
# Callback functions
# ===========================================


def _toggleUseToonTexture(self: "MMDRoot", _context):
    use_toon = self.use_toon_texture
    for i in FnModel.iterate_mesh_objects(self.id_data):
        for m in i.data.materials:
            if m:
                FnMaterial(m).use_toon_texture(use_toon)


def _toggleUseSphereTexture(self: "MMDRoot", _context):
    use_sphere = self.use_sphere_texture
    for i in FnModel.iterate_mesh_objects(self.id_data):
        for m in i.data.materials:
            if m:
                FnMaterial(m).use_sphere_texture(use_sphere, i)


def _toggleUseSDEF(self: "MMDRoot", _context):
    mute_sdef = not self.use_sdef
    for i in FnModel.iterate_mesh_objects(self.id_data):
        FnSDEF.mute_sdef_set(i, mute_sdef)


def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context):
    root = self.id_data
    hide = not self.show_meshes
    for i in FnModel.iterate_mesh_objects(self.id_data):
        i.hide_set(hide)
        i.hide_render = hide
    if hide and context.active_object is None:
        FnContext.set_active_object(context, root)


def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context):
    root = self.id_data
    hide = not self.show_rigid_bodies
    for i in FnModel.iterate_rigid_body_objects(root):
        i.hide_set(hide)
    if hide and context.active_object is None:
        FnContext.set_active_object(context, root)


def _toggleVisibilityOfJoints(self: "MMDRoot", context):
    root_object = self.id_data
    hide = not self.show_joints
    for i in FnModel.iterate_joint_objects(root_object):
        i.hide_set(hide)
    if hide and context.active_object is None:
        FnContext.set_active_object(context, root_object)


def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context):
    root_object: bpy.types.Object = self.id_data
    hide = not self.show_temporary_objects
    with FnContext.temp_override_active_layer_collection(context, root_object):
        for i in FnModel.iterate_temporary_objects(root_object):
            i.hide_set(hide)
    if hide and context.active_object is None:
        FnContext.set_active_object(context, root_object)


def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context):
    root = self.id_data
    show_names = root.mmd_root.show_names_of_rigid_bodies
    for i in FnModel.iterate_rigid_body_objects(root):
        i.show_name = show_names


def _toggleShowNamesOfJoints(self: "MMDRoot", _context):
    root = self.id_data
    show_names = root.mmd_root.show_names_of_joints
    for i in FnModel.iterate_joint_objects(root):
        i.show_name = show_names


def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool):
    root = prop.id_data
    arm = FnModel.find_armature_object(root)
    if arm is None:
        return
    if not v and bpy.context.active_object == arm:
        FnContext.set_active_object(bpy.context, root)
    arm.hide_set(not v)


def _getVisibilityOfMMDRigArmature(prop: "MMDRoot"):
    if prop.id_data.mmd_type != "ROOT":
        return False
    arm = FnModel.find_armature_object(prop.id_data)
    return arm is not None and not arm.hide_get()


def _setActiveRigidbodyObject(prop: "MMDRoot", v: int):
    obj = FnContext.get_scene_objects(bpy.context)[v]
    if FnModel.is_rigid_body_object(obj):
        FnContext.set_active_and_select_single_object(bpy.context, obj)
    prop["active_rigidbody_object_index"] = v


def _getActiveRigidbodyObject(prop: "MMDRoot"):
    context = bpy.context
    active_obj = FnContext.get_active_object(context)
    if FnModel.is_rigid_body_object(active_obj):
        prop["active_rigidbody_object_index"] = FnContext.get_scene_objects(context).find(active_obj.name)
    return prop.get("active_rigidbody_object_index", 0)


def _setActiveJointObject(prop: "MMDRoot", v: int):
    obj = FnContext.get_scene_objects(bpy.context)[v]
    if FnModel.is_joint_object(obj):
        FnContext.set_active_and_select_single_object(bpy.context, obj)
    prop["active_joint_object_index"] = v


def _getActiveJointObject(prop: "MMDRoot"):
    context = bpy.context
    active_obj = FnContext.get_active_object(context)
    if FnModel.is_joint_object(active_obj):
        prop["active_joint_object_index"] = FnContext.get_scene_objects(context).find(active_obj.name)
    return prop.get("active_joint_object_index", 0)


def _setActiveMorph(prop: "MMDRoot", v: bool):
    if "active_morph_indices" not in prop:
        prop["active_morph_indices"] = [0] * 5
    prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v


def _getActiveMorph(prop: "MMDRoot"):
    if "active_morph_indices" in prop:
        return prop["active_morph_indices"][prop.get("active_morph_type", 3)]
    return 0


def _setActiveMeshObject(prop: "MMDRoot", v: int):
    obj = FnContext.get_scene_objects(bpy.context)[v]
    if FnModel.is_mesh_object(obj):
        FnContext.set_active_and_select_single_object(bpy.context, obj)
    prop["active_mesh_index"] = v


def _getActiveMeshObject(prop: "MMDRoot"):
    context = bpy.context
    active_obj = FnContext.get_active_object(context)
    if FnModel.is_mesh_object(active_obj):
        prop["active_mesh_index"] = FnContext.get_scene_objects(context).find(active_obj.name)
    return prop.get("active_mesh_index", -1)


# ===========================================
# Property classes
# ===========================================


class MMDDisplayItem(bpy.types.PropertyGroup):
    """PMX 表示項目(表示枠内の1項目)"""

    type: bpy.props.EnumProperty(
        name="Type",
        description="Select item type",
        items=[
            ("BONE", "Bone", "", 1),
            ("MORPH", "Morph", "", 2),
        ],
    )

    morph_type: bpy.props.EnumProperty(
        name="Morph Type",
        description="Select morph type",
        items=[
            ("material_morphs", "Material", "Material Morphs", 0),
            ("uv_morphs", "UV", "UV Morphs", 1),
            ("bone_morphs", "Bone", "Bone Morphs", 2),
            ("vertex_morphs", "Vertex", "Vertex Morphs", 3),
            ("group_morphs", "Group", "Group Morphs", 4),
        ],
        default="vertex_morphs",
    )


class MMDDisplayItemFrame(bpy.types.PropertyGroup):
    """PMX 表示枠

    PMXファイル内では表示枠がリストで格納されています。
    """

    name_e: bpy.props.StringProperty(
        name="Name(Eng)",
        description="English Name",
        default="",
    )

    # 特殊枠フラグ
    # 特殊枠はファイル仕様上の固定枠(削除、リネーム不可)
    is_special: bpy.props.BoolProperty(
        name="Special",
        description="Is special",
        default=False,
    )

    # 表示項目のリスト
    data: bpy.props.CollectionProperty(
        name="Display Items",
        type=MMDDisplayItem,
    )

    # 現在アクティブな項目のインデックス
    active_item: bpy.props.IntProperty(
        name="Active Display Item",
        min=0,
        default=0,
    )


class MMDRoot(bpy.types.PropertyGroup):
    """MMDモデルデータ

    モデルルート用に作成されたEmtpyオブジェクトで使用します
    """

    name: bpy.props.StringProperty(
        name="Name",
        description="The name of the MMD model",
        default="",
    )

    name_e: bpy.props.StringProperty(
        name="Name (English)",
        description="The english name of the MMD model",
        default="",
    )

    comment_text: bpy.props.StringProperty(
        name="Comment",
        description="The text datablock of the comment",
        default="",
    )

    comment_e_text: bpy.props.StringProperty(
        name="Comment (English)",
        description="The text datablock of the english comment",
        default="",
    )

    ik_loop_factor: bpy.props.IntProperty(
        name="MMD IK Loop Factor",
        description="Scaling factor of MMD IK loop",
        min=1,
        soft_max=10,
        max=100,
        default=1,
    )

    # TODO: Replace to driver for NLA
    show_meshes: bpy.props.BoolProperty(
        name="Show Meshes",
        description="Show all meshes of the MMD model",
        # get=_show_meshes_get,
        # set=_show_meshes_set,
        update=_toggleVisibilityOfMeshes,
        default=True,
    )

    show_rigid_bodies: bpy.props.BoolProperty(
        name="Show Rigid Bodies",
        description="Show all rigid bodies of the MMD model",
        update=_toggleVisibilityOfRigidBodies,
    )

    show_joints: bpy.props.BoolProperty(
        name="Show Joints",
        description="Show all joints of the MMD model",
        update=_toggleVisibilityOfJoints,
    )

    show_temporary_objects: bpy.props.BoolProperty(
        name="Show Temps",
        description="Show all temporary objects of the MMD model",
        update=_toggleVisibilityOfTemporaryObjects,
    )

    show_armature: bpy.props.BoolProperty(
        name="Show Armature",
        description="Show the armature object of the MMD model",
        get=_getVisibilityOfMMDRigArmature,
        set=_setVisibilityOfMMDRigArmature,
    )

    show_names_of_rigid_bodies: bpy.props.BoolProperty(
        name="Show Rigid Body Names",
        description="Show rigid body names",
        update=_toggleShowNamesOfRigidBodies,
    )

    show_names_of_joints: bpy.props.BoolProperty(
        name="Show Joint Names",
        description="Show joint names",
        update=_toggleShowNamesOfJoints,
    )

    show_japanese_name: bpy.props.BoolProperty(
        name="Japanese name",
        description="Toggle Japanese name display",
        default=True,
    )

    show_english_name: bpy.props.BoolProperty(
        name="English name",
        description="Toggle English name display",
        default=True,
    )

    use_toon_texture: bpy.props.BoolProperty(
        name="Use Toon Texture",
        description="Use toon texture",
        update=_toggleUseToonTexture,
        default=True,
    )

    use_sphere_texture: bpy.props.BoolProperty(
        name="Use Sphere Texture",
        description="Use sphere texture",
        update=_toggleUseSphereTexture,
        default=True,
    )

    use_sdef: bpy.props.BoolProperty(
        name="Use SDEF",
        description="Use SDEF",
        update=_toggleUseSDEF,
        default=True,
    )

    use_property_driver: bpy.props.BoolProperty(
        name="Use Property Driver",
        description="Setup drivers for MMD property animation (Visibility and IK toggles)",
        update=_toggleUsePropertyDriver,
        default=False,
    )

    is_built: bpy.props.BoolProperty(
        name="Is Built",
    )

    active_rigidbody_index: bpy.props.IntProperty(
        name="Active Rigidbody Index",
        min=0,
        get=_getActiveRigidbodyObject,
        set=_setActiveRigidbodyObject,
    )

    active_joint_index: bpy.props.IntProperty(
        name="Active Joint Index",
        min=0,
        get=_getActiveJointObject,
        set=_setActiveJointObject,
    )

    # *************************
    # Display Items
    # *************************
    display_item_frames: bpy.props.CollectionProperty(
        name="Display Frames",
        type=MMDDisplayItemFrame,
    )

    active_display_item_frame: bpy.props.IntProperty(
        name="Active Display Item Frame",
        min=0,
        default=0,
    )

    # *************************
    # Bone
    # *************************
    active_bone_index: bpy.props.IntProperty(
        name="Active Bone Index",
        description="Index of the active bone in the armature",
        default=0,
    )

    # *************************
    # Morph
    # *************************
    material_morphs: bpy.props.CollectionProperty(
        name="Material Morphs",
        type=MaterialMorph,
    )
    uv_morphs: bpy.props.CollectionProperty(
        name="UV Morphs",
        type=UVMorph,
    )
    bone_morphs: bpy.props.CollectionProperty(
        name="Bone Morphs",
        type=BoneMorph,
    )
    vertex_morphs: bpy.props.CollectionProperty(name="Vertex Morphs", type=VertexMorph)
    group_morphs: bpy.props.CollectionProperty(
        name="Group Morphs",
        type=GroupMorph,
    )
    active_morph_type: bpy.props.EnumProperty(
        name="Active Morph Type",
        description="Select current morph type",
        items=[
            ("material_morphs", "Material", "Material Morphs", 0),
            ("uv_morphs", "UV", "UV Morphs", 1),
            ("bone_morphs", "Bone", "Bone Morphs", 2),
            ("vertex_morphs", "Vertex", "Vertex Morphs", 3),
            ("group_morphs", "Group", "Group Morphs", 4),
        ],
        default="vertex_morphs",
    )
    active_morph: bpy.props.IntProperty(
        name="Active Morph",
        min=0,
        set=_setActiveMorph,
        get=_getActiveMorph,
    )
    morph_panel_show_settings: bpy.props.BoolProperty(
        name="Morph Panel Show Settings",
        description="Show Morph Settings",
        default=True,
    )
    active_mesh_index: bpy.props.IntProperty(
        name="Active Mesh",
        min=0,
        set=_setActiveMeshObject,
        get=_getActiveMeshObject,
    )

    # *************************
    # Translation
    # *************************
    translation: bpy.props.PointerProperty(
        name="Translation",
        type=MMDTranslation,
    )

    @staticmethod
    def __get_select(prop: bpy.types.Object) -> bool:
        # TODO: Object.select is deprecated since v4.0.0, use Object.select_get() method instead
        # utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_get() method instead")
        return prop.select_get()

    @staticmethod
    def __set_select(prop: bpy.types.Object, value: bool) -> None:
        # TODO: Object.select is deprecated since v4.0.0, use Object.select_set() method instead
        # utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_set() method instead")
        prop.select_set(value)

    @staticmethod
    def __get_hide(prop: bpy.types.Object) -> bool:
        # TODO: Object.hide is deprecated since v4.0.0, use Object.hide_get() method instead
        # utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_get() method instead")
        return prop.hide_get()

    @staticmethod
    def __set_hide(prop: bpy.types.Object, value: bool) -> None:
        # TODO: Object.hide is deprecated since v4.0.0, use Object.hide_set() method instead
        # utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_set() method instead")
        prop.hide_set(value)
        if prop.hide_viewport != value:
            prop.hide_viewport = value

    @staticmethod
    def __get_pose_bone_select(prop: bpy.types.PoseBone) -> bool:
        return prop.bone.select

    @staticmethod
    def __set_pose_bone_select(prop: bpy.types.PoseBone, value: bool) -> None:
        prop.bone.select = value

    @staticmethod
    def register():
        bpy.types.Object.mmd_type = patch_library_overridable(
            bpy.props.EnumProperty(
                name="Type",
                description="Internal MMD type of this object (DO NOT CHANGE IT DIRECTLY)",
                default="NONE",
                items=[
                    ("NONE", "None", "", 1),
                    ("ROOT", "Root", "", 2),
                    ("RIGID_GRP_OBJ", "Rigid Body Grp Empty", "", 3),
                    ("JOINT_GRP_OBJ", "Joint Grp Empty", "", 4),
                    ("TEMPORARY_GRP_OBJ", "Temporary Grp Empty", "", 5),
                    ("PLACEHOLDER", "Place Holder", "", 6),
                    ("CAMERA", "Camera", "", 21),
                    ("JOINT", "Joint", "", 22),
                    ("RIGID_BODY", "Rigid body", "", 23),
                    ("LIGHT", "Light", "", 24),
                    ("TRACK_TARGET", "Track Target", "", 51),
                    ("NON_COLLISION_CONSTRAINT", "Non Collision Constraint", "", 52),
                    ("SPRING_CONSTRAINT", "Spring Constraint", "", 53),
                    ("SPRING_GOAL", "Spring Goal", "", 54),
                ],
            ),
        )
        bpy.types.Object.mmd_root = patch_library_overridable(bpy.props.PointerProperty(type=MMDRoot))

        bpy.types.Object.select = patch_library_overridable(
            bpy.props.BoolProperty(
                get=MMDRoot.__get_select,
                set=MMDRoot.__set_select,
                options={
                    "SKIP_SAVE",
                    "ANIMATABLE",
                    "LIBRARY_EDITABLE",
                },
            ),
        )
        bpy.types.Object.hide = patch_library_overridable(
            bpy.props.BoolProperty(
                get=MMDRoot.__get_hide,
                set=MMDRoot.__set_hide,
                options={
                    "SKIP_SAVE",
                    "ANIMATABLE",
                    "LIBRARY_EDITABLE",
                },
            ),
        )

        if not IS_BLENDER_50_UP:
            bpy.types.PoseBone.select = patch_library_overridable(
                bpy.props.BoolProperty(
                    name="Select",
                    description="Pose bone selection state (compatibility layer for Blender 4.x, forwards to bone.select)",
                    get=MMDRoot.__get_pose_bone_select,
                    set=MMDRoot.__set_pose_bone_select,
                    options={
                        "SKIP_SAVE",
                        "ANIMATABLE",
                        "LIBRARY_EDITABLE",
                    },
                ),
            )

    @staticmethod
    def unregister():
        del bpy.types.Object.hide
        del bpy.types.Object.select
        del bpy.types.Object.mmd_root
        del bpy.types.Object.mmd_type
        if not IS_BLENDER_50_UP:
            del bpy.types.PoseBone.select
