"""Operator for exporting mesh items to RizomUV."""

from pathlib import Path
from typing import Callable, Literal

import bpy
from bpy.types import Context, Event, Object

from rizomuvbridge_ultimate.functions.bpy_funcs import validation
from rizomuvbridge_ultimate.functions.bpy_funcs.export import export_to_file
from rizomuvbridge_ultimate.functions.bpy_funcs.get_prefs import get_addon_prefs, get_uvmaps_prop
from rizomuvbridge_ultimate.functions.bpy_funcs.object_editing import (
    flatten_object_hierarchy,
    triangulate_objects,
    unwrap_objects,
)
from rizomuvbridge_ultimate.functions.bpy_funcs.selection import get_filtered_selection
from rizomuvbridge_ultimate.functions.utils.path_utils import get_file_as_string
from rizomuvbridge_ultimate.preferences import RizomUVBridgePreferences
from rizomuvbridge_ultimate.rizom.live_link.rizom_link import RizomInterface
from rizomuvbridge_ultimate.rizom.script_link.b2r_remote import B2RRemote
from rizomuvbridge_ultimate.rizom.script_link.rizom_cmds import BlenderModel


class ExportToRizom(bpy.types.Operator):
    """Export selected mesh items and open the file in RizomUV."""

    bl_description = "Export selected objects to a file and opens it in RizomUV"
    bl_idname = "ruv.rizom_export"
    bl_label = "Export (RizomUV)"
    bl_options = {"REGISTER", "INTERNAL"}

    @classmethod
    def poll(cls, context: Context) -> bool:
        """Check if there is an active mesh object.

        Returns
        -------
            True if there is an active mesh object

        """
        return context.active_object is not None and context.active_object.type == "MESH"

    def validate_objects(
        self,
        act_obj: Object,
        objs_for_export: list[Object],
        format: Literal["OBJ", "FBX", "USD"],
        fix_names: bool,
    ) -> bool:
        """Validate given objects for Rizom specific expectations.

        Check objects won't be rejected or cause problems in Rizom.
        Runs following tests in order:

        - Check that active object UV maps follow naming rules (no rizom special sequences).
        - Check that selected objects have the same UV sets as the active object.
        - When exporting OBJ format check that object names do not contain periods
        this is because Rizom converts these to colons which makes name matching hard.

        Args:
        ----
            act_obj: The active object
            objs_for_export: List of objects to validate.
            format: The format of the exported file.
            fix_names: Should the names of the objects be fixed.

        Returns:
        -------
            True if objects are valid, False otherwise

        """
        is_valid, message = validation.validate_uvmap_names(act_obj)
        if is_valid is not True:
            self.report({"ERROR"}, message)
            return False

        is_valid, message = validation.validate_uvmaps_against_valid(act_obj, objs_for_export)
        if is_valid is not True:
            self.report({"ERROR"}, message)
            return False

        if format in ("OBJ", "USD"):
            is_valid, message = validation.validate_object_names(objs_for_export, fix_names)
            if is_valid is not True:
                self.report({"ERROR"}, message)
                return False

        return True

    @staticmethod
    def send_to_rizom_link(rizom_link: RizomInterface, uvmap: str) -> tuple[bool, str]:
        """Send the user exported object file to RizomUV using RizomUVLink.

        Args:
        ----
            context: Blender context.
            rizom_link: Instance of the RizomUVLink wrapper class.
            uvmap: The name of the UV Map to be selected in Rizom.

        Returns:
        -------
            True for success, info message.

        """
        success, message = rizom_link.start()
        if not success:
            return success, message

        return rizom_link.load_file(uvmap)

    @staticmethod
    def send_to_rizom_b2r(
        rizom_remote: B2RRemote, prefs: "RizomUVBridgePreferences", uvmap: str
    ) -> tuple[bool, str]:
        """Send the user exported object file to RizomUV using B2RRemote.

        Args:
        ----
            rizom_remote: Instance of the B2RRemote class.
            prefs: Addon preferences.
            uvmap: The UV Map to be selected in Rizom.

        Returns:
        -------
            True for success, info message.

        """
        file_path = get_file_as_string(Path(prefs.data_path), prefs.export_format)

        operation = "EXPORT"
        data_model = BlenderModel(file_path, uvmap, operation)

        return rizom_remote.send_data_to_rizom(data_model)

    @staticmethod
    def preprocess_objects(
        context: Context,
        prefs: "RizomUVBridgePreferences",
        objects: list[Object],
        target_uvmap: str,
    ):
        """Preprocess objects before sending to Rizom.

        Run various user selected operations on objects before sending to Rizom.
        - Triangulate objects
        - Unwrap objects

        Args:
        ----
            context: Blender context.
            prefs: Addon preferences.
            objects: List of objects to run operations on.
            target_uvmap: The name of the UV Map to be selected in Rizom.

        """
        if prefs.export_actions == "USE_SEAMS" or prefs.use_seams:
            unwrap_objects(context, objects, target_uvmap)
        if prefs.triangulate:
            triangulate_objects(objects)

    @staticmethod
    def postprocessing(prefs: "RizomUVBridgePreferences", uvmap: str):
        """Extra commands to run after loading objects in Rizom.

        Args:
        ----
            prefs: Addon preferences.
            uvmap: Target UV Map.

        """
        if prefs.apply_rizom_settings:
            bpy.ops.ruv.rizom_action(
                action="APPLY_RIZOM_SETTINGS",
                target_uvmap=uvmap,
                apply_to_all=prefs.apply_to_all_uvmaps_action,
            )

        if prefs.export_actions not in ("EXPORT"):
            bpy.ops.ruv.rizom_action(
                action=prefs.export_actions,
                target_uvmap=uvmap,
                apply_to_all=prefs.apply_to_all_uvmaps_action,
            )

    def execute(self, context: Context) -> set[str]:
        """Execute the operator."""
        prefs = get_addon_prefs()
        uvmap_prop = get_uvmaps_prop()

        livelink_available = prefs.attempt_livelink and prefs.livelink_allowed

        if livelink_available:
            ruv_interface = RizomInterface(prefs)
        else:
            ruv_interface = B2RRemote(prefs)

        act_obj, target_objs = get_filtered_selection(context, prefs.exclude_clones)

        if not self.validate_objects(act_obj, target_objs, prefs.export_format, prefs.fix_names):
            return {"CANCELLED"}

        self.preprocess_objects(context, prefs, target_objs, uvmap_prop.uv_maps)

        parent_child_hierarchy = flatten_object_hierarchy(target_objs)

        # Override the context because the fbx sometimes doesn't see selected objects
        ctx_override = context.copy()
        ctx_override["selected_objects"] = target_objs
        with context.temp_override(**ctx_override):
            export_to_file(prefs.data_path, prefs.export_format, prefs.apply_modifiers)

        # Restore the original scene hierarchy
        for obj in target_objs:
            obj.parent = parent_child_hierarchy[obj]

            if obj.parent:
                # Restore transforms
                obj.matrix_parent_inverse = obj.parent.matrix_world.inverted()

        uvmap = uvmap_prop.uv_maps if uvmap_prop.uv_maps else act_obj.data.uv_layers.active.name

        # Safe to ignore the type because instance is created based on livelink_available earlier
        if livelink_available:
            assert isinstance(ruv_interface, RizomInterface)
            success, message = self.send_to_rizom_link(ruv_interface, uvmap)
        else:
            assert isinstance(ruv_interface, B2RRemote)
            success, message = self.send_to_rizom_b2r(ruv_interface, prefs, uvmap)

        if not success:
            self.report({"ERROR"}, message)
            return {"CANCELLED"}

        self.report({"INFO"}, message)

        # Post export actions (if any)
        if livelink_available:
            self.postprocessing(prefs, uvmap)

        return {"FINISHED"}

    def invoke(self, context: Context, event: Event) -> Callable:
        """Run before execution of the operator.

        Used to enable a confirmation dialog before execution.

        Returns
        -------
            Confirmation dialog or execute the operator.

        """
        prefs = get_addon_prefs()

        if prefs.confirm_export:
            return context.window_manager.invoke_confirm(
                self,
                event,
                title="Confirm Export",
                message=f"This will overwrite the existing RizomUV {prefs.export_format} file",
            )

        return self.execute(context)
