# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import traceback
from collections.abc import Set as AbstractSet
from os import environ
from pathlib import Path
from typing import TYPE_CHECKING

import bpy
from bpy.app.translations import pgettext
from bpy.props import BoolProperty, CollectionProperty, StringProperty
from bpy.types import (
    Armature,
    Context,
    Event,
    Operator,
    Panel,
    PropertyGroup,
    SpaceFileBrowser,
)
from bpy_extras.io_utils import ImportHelper

from ..common import ops, version
from ..common.error_dialog import show_error_dialog
from ..common.logger import get_logger
from ..common.preferences import (
    ImportPreferencesProtocol,
    copy_import_preferences,
    create_import_preferences_dict,
    draw_import_preferences_layout,
    get_preferences,
)
from ..editor import search
from ..editor.extension import get_armature_extension
from ..editor.ops import VRM_OT_open_url_in_web_browser, layout_operator
from ..editor.property_group import CollectionPropertyProtocol, StringPropertyGroup
from .abstract_base_vrm_importer import AbstractBaseVrmImporter, parse_vrm_json
from .license_validation import LicenseConfirmationRequiredError
from .uni_vrm_vrm_animation_importer import UniVrmVrmAnimationImporter
from .vrm0_importer import Vrm0Importer
from .vrm1_importer import Vrm1Importer

logger = get_logger(__name__)


class LicenseConfirmation(PropertyGroup):
    message: StringProperty()  # type: ignore[valid-type]
    url: StringProperty()  # type: ignore[valid-type]
    json_key: StringProperty()  # type: ignore[valid-type]

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        message: str  # type: ignore[no-redef]
        url: str  # type: ignore[no-redef]
        json_key: str  # type: ignore[no-redef]


def import_vrm_update_addon_preferences(
    import_op: "IMPORT_SCENE_OT_vrm", context: Context
) -> None:
    if import_op.use_addon_preferences:
        copy_import_preferences(source=import_op, destination=get_preferences(context))


class IMPORT_SCENE_OT_vrm(Operator, ImportHelper):
    bl_idname = "import_scene.vrm"
    bl_label = "Open"
    bl_description = "import VRM"
    bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}

    filename_ext = ".vrm"
    filter_glob: StringProperty(  # type: ignore[valid-type]
        default="*.vrm",
        options={"HIDDEN"},
    )

    use_addon_preferences: BoolProperty(  # type: ignore[valid-type]
        name="Import using add-on preferences",
        description="Import using add-on preferences instead of operator arguments",
    )

    extract_textures_into_folder: BoolProperty(  # type: ignore[valid-type]
        name="Extract texture images into the folder",
        update=import_vrm_update_addon_preferences,
        default=False,
    )
    make_new_texture_folder: BoolProperty(  # type: ignore[valid-type]
        name="Don't overwrite existing texture folder",
        update=import_vrm_update_addon_preferences,
        default=True,
    )
    set_shading_type_to_material_on_import: BoolProperty(  # type: ignore[valid-type]
        name='Set shading type to "Material"',
        update=import_vrm_update_addon_preferences,
        default=True,
    )
    set_view_transform_to_standard_on_import: BoolProperty(  # type: ignore[valid-type]
        name='Set view transform to "Standard"',
        update=import_vrm_update_addon_preferences,
        default=True,
    )
    set_armature_display_to_wire: BoolProperty(  # type: ignore[valid-type]
        name='Set an imported armature display to "Wire"',
        update=import_vrm_update_addon_preferences,
        default=True,
    )
    set_armature_display_to_show_in_front: BoolProperty(  # type: ignore[valid-type]
        name='Set an imported armature display to show "In-Front"',
        update=import_vrm_update_addon_preferences,
        default=True,
    )
    set_armature_bone_shape_to_default: BoolProperty(  # type: ignore[valid-type]
        name="Set an imported bone shape to default",
        update=import_vrm_update_addon_preferences,
        default=True,
    )
    enable_mtoon_outline_preview: BoolProperty(  # type: ignore[valid-type]
        name="Enable MToon Outline Preview",
        default=True,
    )

    def execute(self, context: Context) -> set[str]:
        try:
            filepath = Path(self.filepath)
            if not filepath.is_file():
                return {"CANCELLED"}

            if self.use_addon_preferences:
                copy_import_preferences(
                    source=get_preferences(context), destination=self
                )

            license_error = None
            try:
                return import_vrm(
                    filepath,
                    self,
                    context,
                    license_validation=True,
                )
            except LicenseConfirmationRequiredError as e:
                license_error = e  # Prevent traceback dump on another exception
        except Exception:
            show_error_dialog(
                pgettext("Failed to import VRM."),
                traceback.format_exc(),
            )
            raise

        logger.warning(license_error.description())

        execution_context = "INVOKE_DEFAULT"
        import_anyway = False
        if environ.get("BLENDER_VRM_AUTOMATIC_LICENSE_CONFIRMATION") == "true":
            execution_context = "EXEC_DEFAULT"
            import_anyway = True

        return ops.wm.vrm_license_warning(
            execution_context,
            import_anyway=import_anyway,
            license_confirmations=license_error.license_confirmations(),
            filepath=str(filepath),
            **create_import_preferences_dict(self),
        )

    def invoke(self, context: Context, event: Event) -> set[str]:
        self.use_addon_preferences = True
        copy_import_preferences(source=get_preferences(context), destination=self)

        if "gltf" not in dir(bpy.ops.import_scene):
            return ops.wm.vrm_gltf2_addon_disabled_warning("INVOKE_DEFAULT")
        return ImportHelper.invoke(self, context, event)

    def draw(self, _context: Context) -> None:
        pass  # Is needed to get panels available

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        filter_glob: str  # type: ignore[no-redef]
        use_addon_preferences: bool  # type: ignore[no-redef]
        extract_textures_into_folder: bool  # type: ignore[no-redef]
        make_new_texture_folder: bool  # type: ignore[no-redef]
        set_shading_type_to_material_on_import: bool  # type: ignore[no-redef]
        set_view_transform_to_standard_on_import: bool  # type: ignore[no-redef]
        set_armature_display_to_wire: bool  # type: ignore[no-redef]
        set_armature_display_to_show_in_front: bool  # type: ignore[no-redef]
        set_armature_bone_shape_to_default: bool  # type: ignore[no-redef]
        enable_mtoon_outline_preview: bool  # type: ignore[no-redef]


class VRM_PT_import_file_browser_tool_props(Panel):
    bl_idname = "VRM_PT_import_file_browser_tool_props"
    bl_space_type = "FILE_BROWSER"
    bl_region_type = "TOOL_PROPS"
    bl_parent_id = "FILE_PT_operator"
    bl_label = ""
    bl_options: AbstractSet[str] = {"HIDE_HEADER"}

    @classmethod
    def poll(cls, context: Context) -> bool:
        space_data = context.space_data
        if not isinstance(space_data, SpaceFileBrowser):
            return False
        return space_data.active_operator.bl_idname == "IMPORT_SCENE_OT_vrm"

    def draw(self, context: Context) -> None:
        space_data = context.space_data
        if not isinstance(space_data, SpaceFileBrowser):
            return

        operator = space_data.active_operator
        if not isinstance(operator, IMPORT_SCENE_OT_vrm):
            return

        layout = self.layout
        draw_import_preferences_layout(operator, layout)


class VRM_PT_import_unsupported_blender_version_warning(Panel):
    bl_idname = "VRM_PT_import_unsupported_blender_version_warning"
    bl_space_type = "FILE_BROWSER"
    bl_region_type = "TOOL_PROPS"
    bl_parent_id = "FILE_PT_operator"
    bl_label = ""
    bl_options: AbstractSet[str] = {"HIDE_HEADER"}

    @classmethod
    def poll(cls, context: Context) -> bool:
        space_data = context.space_data
        if not isinstance(space_data, SpaceFileBrowser):
            return False
        if space_data.active_operator.bl_idname != "IMPORT_SCENE_OT_vrm":
            return False
        return bool(version.panel_warning_message())

    def draw(self, _context: Context) -> None:
        warning_message = version.panel_warning_message()
        if warning_message is None:
            return

        box = self.layout.box()
        warning_column = box.column(align=True)
        for index, warning_line in enumerate(warning_message.splitlines()):
            warning_column.label(
                text=warning_line,
                translate=False,
                icon="NONE" if index else "ERROR",
            )


class WM_OT_vrm_license_confirmation(Operator):
    bl_label = "VRM License Confirmation"
    bl_idname = "wm.vrm_license_warning"
    bl_options: AbstractSet[str] = {"REGISTER"}

    filepath: StringProperty()  # type: ignore[valid-type]

    license_confirmations: CollectionProperty(type=LicenseConfirmation)  # type: ignore[valid-type]
    import_anyway: BoolProperty(  # type: ignore[valid-type]
        name="Import Anyway",
    )

    extract_textures_into_folder: BoolProperty()  # type: ignore[valid-type]
    make_new_texture_folder: BoolProperty()  # type: ignore[valid-type]
    set_shading_type_to_material_on_import: BoolProperty()  # type: ignore[valid-type]
    set_view_transform_to_standard_on_import: BoolProperty()  # type: ignore[valid-type]
    set_armature_display_to_wire: BoolProperty()  # type: ignore[valid-type]
    set_armature_display_to_show_in_front: BoolProperty()  # type: ignore[valid-type]
    set_armature_bone_shape_to_default: BoolProperty()  # type: ignore[valid-type]
    enable_mtoon_outline_preview: BoolProperty()  # type: ignore[valid-type]

    def execute(self, context: Context) -> set[str]:
        try:
            filepath = Path(self.filepath)
            if not filepath.is_file():
                return {"CANCELLED"}
            if not self.import_anyway:
                return {"CANCELLED"}
            return import_vrm(
                filepath,
                self,
                context,
                license_validation=False,
            )
        except Exception:
            show_error_dialog(
                pgettext("Failed to import VRM."),
                traceback.format_exc(),
            )
            raise

    def invoke(self, context: Context, _event: Event) -> set[str]:
        return context.window_manager.invoke_props_dialog(self, width=600)

    def draw(self, _context: Context) -> None:
        layout = self.layout
        layout.label(text=self.filepath, translate=False)
        for license_confirmation in self.license_confirmations:
            box = layout.box()
            for line in license_confirmation.message.split("\n"):
                box.label(text=line, translate=False, icon="INFO")
            if license_confirmation.json_key:
                box.label(
                    text=pgettext("For more information please check following URL.")
                )
                if VRM_OT_open_url_in_web_browser.supported(license_confirmation.url):
                    split = box.split(factor=0.85, align=True)
                    split.prop(
                        license_confirmation,
                        "url",
                        text=license_confirmation.json_key,
                        translate=False,
                    )
                    op = layout_operator(split, VRM_OT_open_url_in_web_browser)
                    op.url = license_confirmation.url
                else:
                    box.prop(
                        license_confirmation,
                        "url",
                        text=license_confirmation.json_key,
                        translate=False,
                    )

        layout.prop(self, "import_anyway")

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        filepath: str  # type: ignore[no-redef]
        license_confirmations: CollectionPropertyProtocol[  # type: ignore[no-redef]
            LicenseConfirmation
        ]
        import_anyway: bool  # type: ignore[no-redef]
        extract_textures_into_folder: bool  # type: ignore[no-redef]
        make_new_texture_folder: bool  # type: ignore[no-redef]
        set_shading_type_to_material_on_import: bool  # type: ignore[no-redef]
        set_view_transform_to_standard_on_import: bool  # type: ignore[no-redef]
        set_armature_display_to_wire: bool  # type: ignore[no-redef]
        set_armature_display_to_show_in_front: bool  # type: ignore[no-redef]
        set_armature_bone_shape_to_default: bool  # type: ignore[no-redef]
        enable_mtoon_outline_preview: bool  # type: ignore[no-redef]


def import_vrm(
    filepath: Path,
    preferences: ImportPreferencesProtocol,
    context: Context,
    *,
    license_validation: bool,
) -> set[str]:
    parse_result = parse_vrm_json(filepath, license_validation=license_validation)
    if parse_result.spec_version_number >= (1,):
        vrm_importer: AbstractBaseVrmImporter = Vrm1Importer(
            context,
            parse_result,
            preferences,
        )
    else:
        vrm_importer = Vrm0Importer(
            context,
            parse_result,
            preferences,
        )

    vrm_importer.import_vrm()

    return {"FINISHED"}


def menu_import(
    menu_op: Operator, _context: Context
) -> None:  # Same as test/blender_io.py for now
    vrm_import_op = layout_operator(
        menu_op.layout, IMPORT_SCENE_OT_vrm, text="VRM (.vrm)"
    )
    vrm_import_op.use_addon_preferences = True

    vrma_import_op = layout_operator(
        menu_op.layout, IMPORT_SCENE_OT_vrma, text="VRM Animation (.vrma)"
    )
    vrma_import_op.armature_object_name = ""


class IMPORT_SCENE_OT_vrma(Operator, ImportHelper):
    bl_idname = "import_scene.vrma"
    bl_label = "Open"
    bl_description = "Import VRM Animation"
    bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}

    filename_ext = ".vrma"
    filter_glob: StringProperty(  # type: ignore[valid-type]
        default="*.vrma",
        options={"HIDDEN"},
    )

    armature_object_name: StringProperty(  # type: ignore[valid-type]
        options={"HIDDEN"},
    )

    def execute(self, context: Context) -> set[str]:
        try:
            errors = WM_OT_vrma_import_prerequisite.detect_errors(
                context, self.armature_object_name
            )
            if errors:
                for error in errors:
                    logger.error(error)
                return {"CANCELLED"}

            filepath = Path(self.filepath)
            if not filepath.is_file():
                return {"CANCELLED"}

            armature = None
            if self.armature_object_name:
                armature = context.blend_data.objects.get(self.armature_object_name)
                if not armature:
                    return {"CANCELLED"}

            if not armature:
                armature = search.current_armature(context)

            if not armature:
                added = ops.icyp.make_basic_armature()
                if added != {"FINISHED"}:
                    return added
                armature = search.current_armature(context)
                if not armature:
                    return {"CANCELLED"}
                armature_data = armature.data
                if not isinstance(armature_data, Armature):
                    return {"CANCELLED"}
                ext = get_armature_extension(armature_data)
                ext.spec_version = ext.SPEC_VERSION_VRM1

            return UniVrmVrmAnimationImporter.execute(context, filepath, armature)
        except Exception:
            show_error_dialog(
                pgettext("Failed to import VRM Animation."),
                traceback.format_exc(),
            )
            raise

    def invoke(self, context: Context, event: Event) -> set[str]:
        if WM_OT_vrma_import_prerequisite.detect_errors(
            context, self.armature_object_name
        ):
            return ops.wm.vrma_import_prerequisite(
                "INVOKE_DEFAULT",
                armature_object_name=self.armature_object_name,
            )
        return ImportHelper.invoke(self, context, event)

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        filter_glob: str  # type: ignore[no-redef]
        armature_object_name: str  # type: ignore[no-redef]


class WM_OT_vrma_import_prerequisite(Operator):
    bl_label = "VRM Animation Import Prerequisite"
    bl_idname = "wm.vrma_import_prerequisite"
    bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}

    armature_object_name: StringProperty(  # type: ignore[valid-type]
        options={"HIDDEN"},
    )
    armature_object_name_candidates: CollectionProperty(  # type: ignore[valid-type]
        type=StringPropertyGroup,
        options={"HIDDEN"},
    )

    @staticmethod
    def detect_errors(context: Context, armature_object_name: str) -> list[str]:
        error_messages: list[str] = []

        if not armature_object_name:
            armature = search.current_armature(context)
        else:
            armature = context.blend_data.objects.get(armature_object_name)

        if not armature:
            return error_messages

        armature_data = armature.data
        if not isinstance(armature_data, Armature):
            return error_messages

        ext = get_armature_extension(armature_data)
        if get_armature_extension(armature_data).is_vrm1():
            humanoid = ext.vrm1.humanoid
            if not humanoid.human_bones.all_required_bones_are_assigned():
                error_messages.append(pgettext("Please assign required human bones"))
        else:
            error_messages.append(pgettext("Please set the version of VRM to 1.0"))

        return error_messages

    def execute(self, _context: Context) -> set[str]:
        return ops.import_scene.vrma(
            "INVOKE_DEFAULT", armature_object_name=self.armature_object_name
        )

    def invoke(self, context: Context, _event: Event) -> set[str]:
        if not self.armature_object_name:
            armature_object = search.current_armature(context)
            if armature_object:
                self.armature_object_name = armature_object.name
        self.armature_object_name_candidates.clear()
        for obj in context.blend_data.objects:
            if obj.type != "ARMATURE":
                continue
            candidate = self.armature_object_name_candidates.add()
            candidate.value = obj.name
        return context.window_manager.invoke_props_dialog(self, width=800)

    def draw(self, context: Context) -> None:
        layout = self.layout

        layout.label(
            text="VRM Animation import requires a VRM 1.0 armature",
            icon="INFO",
        )

        error_messages = WM_OT_vrma_import_prerequisite.detect_errors(
            context, self.armature_object_name
        )

        layout.prop_search(
            self,
            "armature_object_name",
            self,
            "armature_object_name_candidates",
            icon="OUTLINER_OB_ARMATURE",
            text="Armature to be animated",
        )

        if error_messages:
            error_column = layout.box().column(align=True)
            for error_message in error_messages:
                error_column.label(text=error_message, icon="ERROR", translate=False)

        open_op = layout_operator(
            layout,
            VRM_OT_open_url_in_web_browser,
            icon="URL",
            text="Open help in a Web Browser",
        )
        open_op.url = pgettext("https://vrm-addon-for-blender.info/en-us/animation/")

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        armature_object_name: str  # type: ignore[no-redef]
        armature_object_name_candidates: CollectionPropertyProtocol[  # type: ignore[no-redef]
            StringPropertyGroup
        ]
