# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import functools
from dataclasses import dataclass
from typing import Final, Optional

import bpy
from bpy.types import Armature, Context

from ..common import ops
from ..common.logger import get_logger
from ..common.preferences import get_preferences
from ..common.version import get_addon_version
from .extension import (
    VrmAddonArmatureExtensionPropertyGroup,
    VrmAddonSceneExtensionPropertyGroup,
    get_armature_extension,
)
from .mtoon1 import migration as mtoon1_migration
from .spring_bone1 import migration as spring_bone1_migration
from .vrm0 import migration as vrm0_migration
from .vrm1 import migration as vrm1_migration

logger = get_logger(__name__)


@dataclass
class State:
    blend_file_compatibility_warning_shown: bool = False
    blend_file_addon_compatibility_warning_shown: bool = False


state: Final = State()


def is_unnecessary(armature_data: Armature) -> bool:
    ext = get_armature_extension(armature_data)
    return tuple(
        ext.addon_version
    ) >= get_addon_version() and vrm0_migration.is_unnecessary(ext.vrm0)


def defer_migrate(armature_object_name: str) -> bool:
    context = bpy.context

    armature = context.blend_data.objects.get(armature_object_name)
    if not armature:
        return False
    armature_data = armature.data
    if not isinstance(armature_data, Armature):
        return False
    if is_unnecessary(armature_data):
        return True
    bpy.app.timers.register(
        functools.partial(
            migrate_timer_callback,
            armature_object_name,
        )
    )
    return False


def migrate_timer_callback(armature_object_name: str) -> None:
    """Match the type of migrate() to bpy.app.timers.register."""
    context = bpy.context  # Context cannot span frames, so get it anew
    migrate(context, armature_object_name)


def migrate(context: Optional[Context], armature_object_name: str) -> bool:
    if context is None:
        context = bpy.context

    armature = context.blend_data.objects.get(armature_object_name)
    if not armature:
        return False
    armature_data = armature.data
    if not isinstance(armature_data, Armature):
        return False

    if is_unnecessary(armature_data):
        return True

    ext = get_armature_extension(armature_data)

    vrm0_migration.migrate(context, ext.vrm0, armature)
    vrm1_migration.migrate(context, ext.vrm1, armature)
    spring_bone1_migration.migrate(context, armature)

    if (
        ext.has_vrm_model_metadata(armature)
        and tuple(ext.addon_version) < (3, 14, 0)
        and not ext.get("spec_version")
    ):
        ext.spec_version = ext.SPEC_VERSION_VRM0

    updated_addon_version = get_addon_version()
    logger.info(
        "Upgrade armature %s %s to %s",
        armature_object_name,
        tuple(ext.addon_version),
        updated_addon_version,
    )
    ext.addon_version = updated_addon_version
    return True


def migrate_all_objects(
    context: Context,
    *,
    skip_non_migrated_armatures: bool = False,
    show_progress: bool = False,
) -> None:
    for obj in context.blend_data.objects:
        if obj.type == "ARMATURE" and isinstance(armature_data := obj.data, Armature):
            if skip_non_migrated_armatures:
                ext = get_armature_extension(armature_data)
                if (
                    tuple(ext.addon_version)
                    == VrmAddonArmatureExtensionPropertyGroup.INITIAL_ADDON_VERSION
                ):
                    continue
            migrate(context, obj.name)

    VrmAddonSceneExtensionPropertyGroup.update_vrm0_material_property_names(
        context, context.scene.name
    )
    mtoon1_migration.migrate(context, show_progress=show_progress)
    validate_blend_file_compatibility(context)
    validate_blend_file_addon_compatibility(context)

    preferences = get_preferences(context)

    updated_addon_version = get_addon_version()
    logger.debug(
        "Upgrade preferences %s to %s",
        tuple(preferences.addon_version),
        updated_addon_version,
    )

    if tuple(preferences.addon_version) != preferences.INITIAL_ADDON_VERSION and tuple(
        preferences.addon_version
    ) < (2, 34, 0):
        preferences.enable_advanced_preferences = True
        preferences.export_gltf_animations = True

    preferences.addon_version = updated_addon_version


def validate_blend_file_compatibility(context: Context) -> None:
    """Warn when attempting to edit a file created in newer Blender with older Blender.

    Due to add-on version support issues, there are often reports of users trying to
    edit files created in newer Blender with older Blender, which can cause shape keys
    to break. This warning alerts users to be careful.
    """
    if not context.blend_data.filepath:
        return
    if not have_vrm_model(context):
        return

    blend_file_major_minor_version = (
        context.blend_data.version[0],
        context.blend_data.version[1],
    )
    current_major_minor_version = (bpy.app.version[0], bpy.app.version[1])
    if blend_file_major_minor_version <= current_major_minor_version:
        return

    file_version_str = ".".join(map(str, blend_file_major_minor_version))
    app_version_str = ".".join(map(str, current_major_minor_version))

    logger.error(
        "Opening incompatible file: file_blender_version=%s running_blender_version=%s",
        file_version_str,
        app_version_str,
    )

    if not state.blend_file_compatibility_warning_shown:
        state.blend_file_compatibility_warning_shown = True
        # Use timer because dialog disappears automatically if not executed with
        # timer in Blender 4.2.0
        bpy.app.timers.register(
            functools.partial(
                show_blend_file_compatibility_warning,
                file_version_str,
                app_version_str,
            ),
            first_interval=0.1,
        )


def show_blend_file_compatibility_warning(file_version: str, app_version: str) -> None:
    ops.vrm.show_blend_file_compatibility_warning(
        "INVOKE_DEFAULT",
        file_version=file_version,
        app_version=app_version,
    )


def validate_blend_file_addon_compatibility(context: Context) -> None:
    """Warn when attempting to edit a file created with a newer VRM add-on.

    This warning is for files using an older VRM add-on.
    """
    if not context.blend_data.filepath:
        return
    installed_addon_version = get_addon_version()

    # TODO: It might be better to store the version in Scene or similar
    up_to_date = True
    file_addon_version: tuple[int, ...] = (0, 0, 0)
    for armature in context.blend_data.armatures:
        file_addon_version = tuple(get_armature_extension(armature).addon_version)
        if file_addon_version > installed_addon_version:
            up_to_date = False
            break
    if up_to_date:
        return

    file_addon_version_str = ".".join(map(str, file_addon_version))
    installed_addon_version_str = ".".join(map(str, installed_addon_version))

    logger.error(
        "Opening incompatible VRM add-on version: file=%s installed=%s",
        file_addon_version_str,
        installed_addon_version_str,
    )

    if not state.blend_file_compatibility_warning_shown:
        state.blend_file_compatibility_warning_shown = True
        # Use timer because dialog disappears automatically if not executed with
        # timer in Blender 4.2.0
        bpy.app.timers.register(
            functools.partial(
                show_blend_file_addon_compatibility_warning,
                file_addon_version_str,
                installed_addon_version_str,
            ),
            first_interval=0.1,
        )


def show_blend_file_addon_compatibility_warning(
    file_addon_version: str, installed_addon_version: str
) -> None:
    ops.vrm.show_blend_file_addon_compatibility_warning(
        "INVOKE_DEFAULT",
        file_addon_version=file_addon_version,
        installed_addon_version=installed_addon_version,
    )


def have_vrm_model(context: Context) -> bool:
    return any(
        map(
            VrmAddonArmatureExtensionPropertyGroup.has_vrm_model_metadata,
            context.blend_data.objects,
        )
    )
