"""Operator for importing mesh items from RizomUV."""

from time import time
from typing import Tuple

import bpy
from bpy.types import Context, Object

from rizomuvbridge_ultimate.functions.bpy_funcs.get_prefs import get_addon_prefs
from rizomuvbridge_ultimate.functions.bpy_funcs.selection import (
    change_selection,
    get_local_meshes,
    get_selected_meshes,
)
from rizomuvbridge_ultimate.functions.bpy_funcs.state_management import (
    restore_names,
    restore_visibility,
    unhide_layers,
    unhide_objects,
)
from rizomuvbridge_ultimate.functions.bpy_funcs.uv_transfer import (
    process_transfers,
    set_uvmap_names,
)
from rizomuvbridge_ultimate.functions.logging.reporter import Reporter
from rizomuvbridge_ultimate.functions.logging.tracker import UVTransferTracker
from rizomuvbridge_ultimate.functions.rizom_functions.interface_helpers import (
    connect_to_rizom,
    request_import_from_rizom,
)
from rizomuvbridge_ultimate.functions.rizom_functions.model_processing import (
    find_and_prepare_target_objects,
)
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 RizomModel


class ImportFromRizom(bpy.types.Operator):
    """Import meshes from RizomUV and transfer UV maps."""

    bl_description = "Import meshes from RizomUV and transfer UV maps"
    bl_idname = "ruv.rizom_import"
    bl_label = "Import (RizomUV)"
    bl_options = {"REGISTER", "INTERNAL"}

    def __init__(self):
        """Initialise data structures to store information for output report later."""
        self.transfer_tracker = UVTransferTracker()

    @staticmethod
    def import_file(model: RizomModel) -> Tuple[bool, str]:
        """Import file from RizomUV into Blender.

        Args:
        ----
            prefs: Addon preferences.
            model: Rizom data model.


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

        """

        def attempt_import(format, file_path) -> bool:
            if format == "FBX":
                bpy.ops.import_scene.fbx(filepath=file_path)
            elif format == "OBJ":
                if bpy.app.version >= (3, 3, 0):
                    bpy.ops.wm.obj_import(filepath=file_path)
                else:
                    bpy.ops.import_scene.obj(filepath=file_path)
            elif format == "USD":
                bpy.ops.wm.usd_import(filepath=file_path)
            return True

        format = model.get_format()
        max_attempts = 10
        success_status = False

        for _ in range(max_attempts):
            try:
                success_status = attempt_import(format, model.file_path)
                break
            except RuntimeError:
                success_status = False
                time.sleep(2.5)

        if success_status:
            return success_status, "RizomUV Bridge: File imported successfully"
        return success_status, "RizomUV Bridge: Failed to import file"

    @staticmethod
    def scene_cleanup(delete_objs: list[Object]):
        """Delete data created by the addon from the Blender scene.

        Args:
        ----
            delete_objs: List of objects to delete.

        """
        for obj in delete_objs:
            for material in obj.data.materials.items():
                # Prevent duplicate materials
                bpy.data.materials.remove(material[1])
            bpy.data.objects.remove(obj, do_unlink=True)

        for mesh in bpy.data.meshes:
            if mesh.users == 0:
                bpy.data.meshes.remove(mesh)

    def print_report(self, prefs: RizomUVBridgePreferences):
        """Print output report for the user.

        Args:
        ----
            prefs: Addon preferences.

        """
        tracker = self.transfer_tracker
        if prefs.replace_objects:
            self.report(
                {"INFO"}, "Objects imported from RizomUV, original objects have been replaced"
            )
            return

        if tracker.invalid_geometry:
            self.report(
                {"WARNING"},
                (
                    "Some objects did not have matching geometry and will likely not have "
                    "UVs transferred, see console for logs"
                ),
            )

        elif tracker.updated_uvmaps and not any(
            (tracker.missing_objects, tracker.missing_uvmaps, tracker.invalid_geometry)
        ):
            self.report({"INFO"}, "UV Maps updated, no problems encountered. See console for logs")

        elif tracker.updated_uvmaps and tracker.missing_objects:
            self.report({"WARNING"}, "UV Maps updated, with some problems. See console for logs")

        elif tracker.updated_uvmaps and tracker.missing_uvmaps and not prefs.add_uvsets:
            self.report({"WARNING"}, "UV Maps updated, with some problems. See console for logs")

        elif tracker.updated_uvmaps and tracker.invalid_geometry:
            self.report({"WARNING"}, "UV Maps updated, with some problems. See console for logs")

        else:
            self.report({"INFO"}, "UV Maps updated, no problems encountered. See console for logs.")

        reporter = Reporter(prefs, tracker)
        reporter.main()

    @staticmethod
    def incorrect_duplicate_transfer_bug_workaround(
        context: Context, matched_objects: list[Object]
    ):
        """Workaround for a Blender bug.

        Since Blender 3.6 when transferring UVs to duplicate objects
        that havent been edited the UVs will not transfer
        to the correct object. Something to do with how Blender
        internally handles duplicated mesh data probably.

        Bug can be reproduced by making a cube and duplicating it multiple
        times, export to Rizom and edit UVs. When you transfer the UVs some of the
        imported UVs will be overlapping.

        Args:
        ----
            context: Current Blender context.
            matched_objects: List of objects which will be going through UV transfers.

        """
        if not context.active_object:
            context.view_layer.objects.active = context.scene.objects[0]

        bpy.ops.object.mode_set(mode="EDIT")
        for obj in matched_objects:
            obj.select_set(True)
        bpy.ops.mesh.select_all(action="SELECT")
        bpy.ops.object.mode_set(mode="OBJECT")

    @staticmethod
    def post_processing(
        context: Context, prefs: RizomUVBridgePreferences, matched_objects: list[Object]
    ):
        """Post processing after import.

        Run various operations on objects after import.
        - Mark seams and sharp edges from UV boundaries.

        Args:
        ----
            context: Current Blender context.
            prefs: Addon preferences.
            matched_objects: List of objects which have had UVs transferred.

        """
        change_selection(context, matched_objects)
        bpy.ops.object.mode_set(mode="EDIT")

        if prefs.mark_seams or prefs.mark_sharp:
            bpy.ops.mesh.select_all(action="SELECT")
            bpy.ops.uv.select_all(action="SELECT")
            if prefs.mark_sharp:
                bpy.ops.mesh.mark_sharp(clear=True)
            if prefs.mark_seams:
                bpy.ops.mesh.mark_seam(clear=True)

            bpy.ops.uv.seams_from_islands(mark_seams=prefs.mark_seams, mark_sharp=prefs.mark_sharp)
        bpy.ops.object.mode_set(mode="OBJECT")

    @staticmethod
    def get_rizom_interface(prefs: "RizomUVBridgePreferences"):
        """Check if the RizomUV Link is available, if not, use the custom remote link.

        Args:
        ----
            prefs: Addon preferences.

        Returns:
        -------
            RizomInterface: RizomUV Link interface, either B2RRemote or RizomInterface.

        """
        livelink_available = prefs.attempt_livelink and prefs.livelink_allowed
        if livelink_available:
            return RizomInterface(prefs)
        return B2RRemote(prefs)

    @staticmethod
    def restore_local_view(
        context: Context,
        objects: list[Object],
        active_object: Object,
        selected_objects: list[Object],
    ):
        """Restore local view state.

        Args:
        ----
            context: Current Blender context.
            objects: List of objects were in localview mode.
            active_object: Active object before the operation.
            selected_objects: List of objects that were selected before the operation.

        """
        bpy.ops.object.select_all(action="DESELECT")
        for obj in objects:
            obj.select_set(True)
        if active_object:
            context.view_layer.objects.active = active_object

        bpy.ops.view3d.localview(frame_selected=False)
        bpy.ops.object.select_all(action="DESELECT")
        for obj in selected_objects:
            obj.select_set(True)

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

        ruv_interface = self.get_rizom_interface(prefs)

        success_status, message = connect_to_rizom(ruv_interface)
        if not success_status:
            self.report({"ERROR"}, message)
            return {"CANCELLED"}
        print(message)

        selected_objects = get_selected_meshes()
        active_object = context.active_object

        local_view = context.space_data.local_view
        local_objects = []

        if local_view:
            local_objects = get_local_meshes()
            bpy.ops.view3d.localview(frame_selected=False)

        rizom_model = request_import_from_rizom(ruv_interface)

        matched_objects, missing_objects = find_and_prepare_target_objects(context, rizom_model)
        if missing_objects:
            self.transfer_tracker.add_missing_objects(missing_objects)

        if not matched_objects:
            self.report(
                {"ERROR"},
                "No matching objects found in the Blender scene. Objects are matched by name",
            )
            return {"CANCELLED"}

        layer_visibility_history = unhide_layers(context, list(matched_objects.values()))
        object_visibility_history = unhide_objects(list(matched_objects.values()))

        self.incorrect_duplicate_transfer_bug_workaround(context, list(matched_objects.values()))
        success_status, message = self.import_file(rizom_model)

        imported_objects = [context.scene.objects[obj_name] for obj_name in matched_objects.keys()]

        # When using obj format the uvmap name does not survive import.
        if rizom_model.get_format() == "OBJ":
            set_uvmap_names(imported_objects, rizom_model.uvmap, 0)

        if not prefs.replace_objects:
            process_transfers(self.transfer_tracker, prefs, matched_objects, imported_objects)

        self.scene_cleanup(
            list(matched_objects.values()) if prefs.replace_objects else imported_objects
        )
        self.post_processing(
            context,
            prefs,
            list(matched_objects.values()) if not prefs.replace_objects else imported_objects,
        )

        # Restore object names
        if not prefs.replace_objects:
            restore_names(matched_objects)

            if not prefs.reveal_hidden:
                restore_visibility(layer_visibility_history, object_visibility_history)
                if local_view and selected_objects:
                    self.restore_local_view(context, local_objects, active_object, selected_objects)

            else:
                change_selection(context, list(matched_objects.values()))

        self.print_report(prefs)

        return {"FINISHED"}
