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

from ...core.logging_setup import logger
import os
import re
import string
from typing import Callable, Optional, Set

import bpy
import numpy as np

from .bpyutils import FnContext


# 指定したオブジェクトのみを選択状態かつアクティブにする
def selectAObject(obj):
    try:
        bpy.ops.object.mode_set(mode="OBJECT")
    except Exception:
        pass
    bpy.ops.object.select_all(action="DESELECT")
    FnContext.select_object(FnContext.ensure_context(), obj)
    FnContext.set_active_object(FnContext.ensure_context(), obj)


# 現在のモードを指定したオブジェクトのEdit Modeに変更する
def enterEditMode(obj):
    selectAObject(obj)
    if obj.mode != "EDIT":
        bpy.ops.object.mode_set(mode="EDIT")


def setParentToBone(obj, parent, bone_name):
    selectAObject(obj)
    FnContext.set_active_object(FnContext.ensure_context(), parent)
    bpy.ops.object.mode_set(mode="POSE")
    parent.data.bones.active = parent.data.bones[bone_name]
    bpy.ops.object.parent_set(type="BONE", xmirror=False, keep_transform=False)
    bpy.ops.object.mode_set(mode="OBJECT")


def selectSingleBone(context, armature, bone_name, reset_pose=False):
    try:
        bpy.ops.object.mode_set(mode="OBJECT")
    except Exception as e:
        logger.warning(f"Failed to set object mode: {e}")
    for i in context.selected_objects:
        i.select_set(False)
    FnContext.set_active_object(context, armature)
    bpy.ops.object.mode_set(mode="POSE")
    if reset_pose:
        for p_bone in armature.pose.bones:
            p_bone.matrix_basis.identity()

    for p_bone in armature.pose.bones:
        is_target = p_bone.name == bone_name
        p_bone.select = is_target
        if is_target:
            armature.data.bones.active = p_bone.bone
            p_bone.bone.hide = False


__CONVERT_NAME_TO_L_REGEXP = re.compile(r"^(.*)左(.*)$")
__CONVERT_NAME_TO_R_REGEXP = re.compile(r"^(.*)右(.*)$")


# 日本語で左右を命名されている名前をblender方式のL(R)に変更する
def convertNameToLR(name, use_underscore=False):
    m = __CONVERT_NAME_TO_L_REGEXP.match(name)
    delimiter = "_" if use_underscore else "."
    if m:
        name = m.group(1) + m.group(2) + delimiter + "L"
    m = __CONVERT_NAME_TO_R_REGEXP.match(name)
    if m:
        name = m.group(1) + m.group(2) + delimiter + "R"
    return name


__CONVERT_L_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[lL])(?P<after>($|(?P=separator)))")
__CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[rR])(?P<after>($|(?P=separator)))")


def convertLRToName(name):
    match = __CONVERT_L_TO_NAME_REGEXP.search(name)
    if match:
        return f"左{name[0:match.start()]}{match['after']}{name[match.end():]}"

    match = __CONVERT_R_TO_NAME_REGEXP.search(name)
    if match:
        return f"右{name[0:match.start()]}{match['after']}{name[match.end():]}"

    return name


# src_vertex_groupのWeightをdest_vertex_groupにaddする
def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name):
    mesh = meshObj.data
    src_vertex_group = meshObj.vertex_groups[src_vertex_group_name]
    dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name]

    vtxIndex = src_vertex_group.index
    for v in mesh.vertices:
        try:
            gi = [i.group for i in v.groups].index(vtxIndex)
            dest_vertex_group.add([v.index], v.groups[gi].weight, "ADD")
        except ValueError:
            pass


def separateByMaterials(meshObj: bpy.types.Object, keep_normals: bool = False):
    meshData = meshObj.data
    if len(meshData.materials) < 2:
        selectAObject(meshObj)
        return

    dummy_parent = None
    try:
        dummy_parent = bpy.data.objects.new(name="tmp", object_data=None)
        matrix_parent_inverse = meshObj.matrix_parent_inverse.copy()
        prev_parent = meshObj.parent
        meshObj.parent = dummy_parent
        meshObj.active_shape_key_index = 0
        mmd_normal_name = None  # To avoid conflict ("mmd_normal.001", etc.)
        if keep_normals:
            existing_custom_normal = meshData.attributes.get("custom_normal")
            if existing_custom_normal:
                if existing_custom_normal.data_type == "INT16_2D":
                    normals_data = np.empty(len(meshData.loops) * 2, dtype=np.int16)
                    existing_custom_normal.data.foreach_get("value", normals_data)
                    mmd_normal = meshData.attributes.new("mmd_normal", "INT16_2D", "CORNER")
                    mmd_normal_name = mmd_normal.name
                    mmd_normal.data.foreach_set("value", normals_data)
                else:
                    raise TypeError(f"Unsupported custom_normal data type: '{existing_custom_normal.data_type}'. Supported types: 'INT16_2D'")

        try:
            enterEditMode(meshObj)
            bpy.ops.mesh.separate(type="MATERIAL")
        finally:
            bpy.ops.object.mode_set(mode="OBJECT")

        for i in dummy_parent.children:
            materials = i.data.materials
            i.name = getattr(materials[0], "name", "None") if len(materials) else "None"
            i.parent = prev_parent
            i.matrix_parent_inverse = matrix_parent_inverse

            if keep_normals and mmd_normal_name:
                mmd_normal = i.data.attributes.get(mmd_normal_name)
                if mmd_normal:
                    if mmd_normal.data_type == "INT16_2D":
                        normals_data = np.empty(len(i.data.loops) * 2, dtype=np.int16)
                        mmd_normal.data.foreach_get("value", normals_data)
                        custom_normal_attr = i.data.attributes.get("custom_normal")
                        if not custom_normal_attr:
                            custom_normal_attr = i.data.attributes.new("custom_normal", "INT16_2D", "CORNER")
                        custom_normal_attr.data.foreach_set("value", normals_data)
                    else:
                        raise TypeError(f"Unsupported custom_normal data type: '{mmd_normal.data_type}'. Supported types: 'INT16_2D'")
                    i.data.attributes.remove(mmd_normal)
    finally:
        if dummy_parent and dummy_parent.name in bpy.data.objects:
            bpy.data.objects.remove(dummy_parent)


def clearUnusedMeshes():
    meshes_to_delete = [mesh for mesh in bpy.data.meshes if mesh.users == 0]

    for mesh in meshes_to_delete:
        bpy.data.meshes.remove(mesh)


# Boneのカスタムプロパティにname_jが存在する場合、name_jの値を
# それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成
def makePmxBoneMap(armObj):
    # Maintain backward compatibility with mmd_tools_local v0.4.x or older.
    return {(i.mmd_bone.name_j or i.get("mmd_bone_name_j", i.get("name_j", i.name))): i for i in armObj.pose.bones}


__REMOVE_PREFIX_DIGITS_REGEXP = re.compile(r"\.\d{1,}$")


def unique_name(name: str, used_names: Set[str]) -> str:
    """Generate a unique name from the given name.
    This function is a limited and simplified version of bpy_extras.io_utils.unique_name.

    Args:
        name (str): The name to make unique.
        used_names (Set[str]): A set of names that are already used.

    Returns:
        str: The unique name, formatted as "{name}.{number:03d}".
    """
    if name not in used_names:
        return name
    count = 1
    new_name = orig_name = __REMOVE_PREFIX_DIGITS_REGEXP.sub("", name)
    while new_name in used_names:
        new_name = f"{orig_name}.{count:03d}"
        count += 1
    return new_name


def int2base(x, base, width=0):
    """
    Convert an int to a base
    Source: http://stackoverflow.com/questions/2267362
    """
    digs = string.digits + string.ascii_uppercase
    assert 2 <= base <= len(digs)
    digits, negtive = "", False
    if x <= 0:
        if x == 0:
            return "0" * max(1, width)
        x, negtive, width = -x, True, width - 1
    while x:
        digits = digs[x % base] + digits
        x //= base
    digits = "0" * (width - len(digits)) + digits
    if negtive:
        digits = "-" + digits
    return digits


def saferelpath(path, start, strategy="inside"):
    """
    On Windows relpath will raise a ValueError
    when trying to calculate the relative path to a
    different drive.
    This method will behave different depending on the strategy
    choosen to handle the different drive issue.
    Strategies:
    - inside: this will just return the basename of the path given
    - outside: this will prepend '..' to the basename
    - absolute: this will return the absolute path instead of a relative.
    See http://bugs.python.org/issue7195
    """
    if strategy == "inside":
        return os.path.basename(path)

    if strategy == "absolute":
        return os.path.abspath(path)

    if strategy == "outside" and os.name == "nt":
        d1, _ = os.path.splitdrive(path)
        d2, _ = os.path.splitdrive(start)
        if d1 != d2:
            return ".." + os.sep + os.path.basename(path)

    return os.path.relpath(path, start)


class ItemOp:
    @staticmethod
    def get_by_index(items, index):
        if 0 <= index < len(items):
            return items[index]
        return None

    @staticmethod
    def resize(items: bpy.types.bpy_prop_collection, length: int):
        count = length - len(items)
        if count > 0:
            for i in range(count):
                items.add()
        elif count < 0:
            for i in range(-count):
                items.remove(length)

    @staticmethod
    def add_after(items, index):
        index_end = len(items)
        index = max(0, min(index_end, index + 1))
        items.add()
        items.move(index_end, index)
        return items[index], index


class ItemMoveOp:
    type: bpy.props.EnumProperty(
        name="Type",
        description="Move type",
        items=[
            ("UP", "Up", "", 0),
            ("DOWN", "Down", "", 1),
            ("TOP", "Top", "", 2),
            ("BOTTOM", "Bottom", "", 3),
        ],
        default="UP",
    )

    @staticmethod
    def move(items, index, move_type, index_min=0, index_max=None):
        if index_max is None:
            index_max = len(items) - 1
        else:
            index_max = min(index_max, len(items) - 1)
        index_min = min(index_min, index_max)

        if index < index_min:
            items.move(index, index_min)
            return index_min
        if index > index_max:
            items.move(index, index_max)
            return index_max

        index_new = index
        if move_type == "UP":
            index_new = max(index_min, index - 1)
        elif move_type == "DOWN":
            index_new = min(index + 1, index_max)
        elif move_type == "TOP":
            index_new = index_min
        elif move_type == "BOTTOM":
            index_new = index_max

        if index_new != index:
            items.move(index, index_new)
        return index_new


def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None):
    """Mark a function as deprecated.
    Args:
        deprecated_in (Optional[str]): Version in which the function was deprecated.
        details (Optional[str]): Additional details about the deprecation.
    Returns:
        Callable: The decorated function.
    """

    def _function_wrapper(function: Callable):
        def _inner_wrapper(*args, **kwargs):
            warn_deprecation(function.__name__, deprecated_in, details)
            return function(*args, **kwargs)

        return _inner_wrapper

    return _function_wrapper


def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, details: Optional[str] = None) -> None:
    """Report a deprecation warning.
    Args:
        function_name (str): Name of the deprecated function.
        deprecated_in (Optional[str]): Version in which the function was deprecated.
        details (Optional[str]): Additional details about the deprecation.
    """
    logger.warning(
        "%s is deprecated%s%s",
        function_name,
        f" since {deprecated_in}" if deprecated_in else "",
        f": {details}" if details else "",
        stack_info=True,
        stacklevel=4,
    )

    # import warnings  # pylint: disable=import-outside-toplevel

    # warnings.warn(f"""{function_name}is deprecated{f" since {deprecated_in}" if deprecated_in else ""}{f": {details}" if details else ""}""", category=DeprecationWarning, stacklevel=2)
