"""Singleton wrapper of the RizomUVLink module."""

import importlib
import importlib.util
import os
import sys
import uuid
from pathlib import Path
from typing import TYPE_CHECKING, Literal

from rizomuvbridge_ultimate import constants
from rizomuvbridge_ultimate.constants import RIZOM_PORTS
from rizomuvbridge_ultimate.functions.utils.math_utilts import texel_density
from rizomuvbridge_ultimate.functions.utils.path_utils import (
    get_file_as_string,
    get_path_with_doublesep,
)
from rizomuvbridge_ultimate.preferences import RizomUVBridgePreferences
from rizomuvbridge_ultimate.types import Float9

if TYPE_CHECKING:
    from RizomUVLink import CRizomUVLink


class RizomInterface:
    """Interface between Blender and RizomUV.

    Wraps the RizomUVLink module and API for
    communicating with RizomUV from Blender.

    Integrates and automates managing the connection
    using an external file to save the port number.

    """

    def __init__(self, prefs: "RizomUVBridgePreferences"):
        """Intialise the link object.

        Args:
        ----
            prefs: Addon preferences.

        """
        self.prefs = prefs
        self.port_cache = os.path.join(self.prefs.data_path, RIZOM_PORTS)
        self.link: "CRizomUVLink" = None
        self.port: int
        self.version: dict[Literal["major", "minor", "patch"], int]

        self._add_rizom_to_path()

        self.RizomUVLink = importlib.import_module("RizomUVLink")

    def get_rizom_version(self) -> dict[Literal["major", "minor", "patch"], int]:
        """Get the version of the RizomUV instance.

        Convert to dictionary for easy comparison.

        Returns
        -------
            Int representing RizomUV version.

        """
        version = self.link.RizomUVVersion()
        string_list = version.split(".")

        del string_list[-1]
        return {
            "major": int(string_list[0]),
            "minor": int(string_list[1]),
            "patch": int(string_list[2]),
        }

    def connect_to_running(self) -> bool:
        """Attempt to connect to Rizom.

        Use the port stored in the data file, this will
        connect to the running Rizom instance. If the there
        is not an instance running on the stored port it will
        return False.

        Returns
        -------
            True if the connection attempt is successful.

        """
        self.link = self.RizomUVLink.CRizomUVLink()

        with open(self.port_cache, "r", encoding="utf-8") as f:
            stored_port = f.read()
        if stored_port is not None:
            try:
                self.port = int(stored_port)
            except ValueError:
                return False

        self.link.Connect(self.port)

        return self.is_connected()

    def is_connected(self) -> bool:
        """Check if there is a connectable Rizom instance open.

        Returns
        -------
            True if connected.

        """
        if self.link.TCPPortIsOpen(self.port):
            try:
                self.version = self.get_rizom_version()
                return True
            except self.RizomUVLink.CZEx:
                return False

        return False

    def _add_rizom_to_path(self):
        """Add the RizomUVLink module to path so it can be imported.

        Args:
        ----
            rizom_path: Path to Rizom executable.

        """
        sys.path.append(constants.RIZOMUV_LINK_PACKAGE)

    def start(self):
        """Connect to existing Rizom instance or start a new one.

        First attempt to connect to Rizom. If it fails, start a new
        Rizom instance and store the port in the data file.

        """
        if self.connect_to_running():
            return True, "RizomUVLink: Connected to existing Rizom instance"

        try:
            self.port = self.link.RunRizomUV(self.prefs.rizom_path)
            self.version = self.get_rizom_version()
        except FileNotFoundError:
            return False, f"{self.prefs.rizom_path} could not be found, ensure the path is correct"
        with open(self.port_cache, "w", encoding="utf-8") as f:
            f.write(str(self.port))

        # Startup params
        self.link.Set({"Path": "Prefs.FileSuffix", "Value": ""})  # Ensure no export suffix

        return True, "RizomUVLink: Started new Rizom instance"

    def reset_uvmaps(self, uvmaps: list[str]):
        """Hacky way of welding all seams.

        Welds all seams by deleting the UVMaps and recreating them.
        This is faster than selecting all seams and welding via the API.

        Args:
        ----
            uvmaps: Names of the UV Maps to reset.

        """
        # Create new UV Map with a unique name as a dummy map
        # Switch to this while deleting all the real maps
        unique_name = str(uuid.uuid4())
        active_uvmap = self.get_current_uvmap()

        # Dummy UV map
        self.link.Uvset({"Mode": "Create", "Name": unique_name})
        self.select_uvmap(unique_name)

        for uvmap in uvmaps:
            self.link.Uvset({"Mode": "Delete", "Name": uvmap})
            self.link.Uvset({"Mode": "Create", "Name": uvmap})
            self.select_uvmap(uvmap)
            self.link.ResetTo3d({"WorkingSet": "Visible", "Rescale": True})

        # Delete the dummy
        self.select_uvmap(active_uvmap)
        self.link.Uvset({"Mode": "Delete", "Name": unique_name})

    def save_file(self):
        """Save the currently loaded file."""
        loaded_file = self.get_file()
        format = loaded_file[-3:].upper()
        self.link.Save(
            {"File": {format: {"UseUVSetNames": True}, "Path": loaded_file, "UVWProps": True}}
        )

    def get_file_type(self) -> str:
        """Get the file type of the currently loaded file."""
        loaded_file = self.get_file()
        return os.path.splitext(loaded_file)[1][1:][:3].upper()

    def get_current_uvmap(self) -> str:
        """Get the current UV map name from Rizom.

        Modify the string if it contains reserved sequence.

        Returns
        -------
            The name of the current UV map.

        """
        name = self.link.Get("Lib.CurrentUVSetName")
        for sequence, replacement in constants.RESERVED_UVMAP_SEQUENCES.items():
            if sequence in name:
                name = name.replace(sequence, replacement)
        return name

    def get_uvmaps(self) -> list[str]:
        """Get the names of all loaded UV maps in Rizom.

        Get names of UV maps loaded in RizomUV and modify the
        strings if they contain reserved sequences.

        Returns
        -------
            List of UV map names

        """
        # Regex is faster but I dont like regex and this is not a bottleneck.
        uvmaps = self.link.ItemNames("Lib.UVSets")
        for i, name in enumerate(uvmaps):
            for sequence, replacement in constants.RESERVED_UVMAP_SEQUENCES.items():
                if sequence in name:
                    uvmaps[i] = uvmaps[i].replace(sequence, replacement)

        return uvmaps

    def get_objects(self) -> list[str]:
        """Get the names of all loaded objects in Rizom.

        Returns
        -------
            List of object names

        """
        objects: list[str] = self.link.Get("Lib.Mesh.ObjectsNames")

        # DefaultObject is a fake object created By Rizom when it loads OBJ/USD format
        # Assume not a real object and delete it, exclude DefaultObject as a valid object.
        if "DefaultObject" in objects:
            objects.remove("DefaultObject")

        if self.get_file_type() == "USD":
            objects = [obj.split("/")[-1] for obj in objects]

        return objects

    def get_file(self) -> str:
        """Get the current file path from Rizom.

        Returns
        -------
            The current file path

        """
        return self.link.Get("Prefs.LastLoadedFile")

    def meets_minimum_version(self, major: int, minor: int = 0, patch: int = 0) -> bool:
        """Check if Rizom version meets the given minimum.

        Args:
        ----
            major: Major version.
            minor: Minor version.
            patch: Patch version.

        Returns:
        -------
            True if the given version is equal to or higher than the Rizom version.

        """
        return (
            self.version["major"] >= major
            or (self.version["major"] == major and self.version["minor"] >= minor)
            or (
                self.version["major"] == major
                and self.version["minor"] == minor
                and self.version["patch"] >= patch
            )
        )

    def is_below_version(self, major: int, minor: int = 0, patch: int = 0) -> bool:
        """Check if the Rizom version is below the given version.

        Args:
        ----
            major: Major version.
            minor: Minor version.
            patch: Patch version.

        Returns:
        -------
            True if the Rizom version is below the given version.

        """
        return not self.meets_minimum_version(major, minor, patch)

    def load_texture_file(self):
        """Import texture file into Rizom and set shading mode."""
        self.link.LoadUserTexture(get_path_with_doublesep(self.prefs.texture_path))

        if self.meets_minimum_version(2023) and self.is_below_version(2024):
            self.link.Set({"Path": "Vars.Viewport.TextureID", "Value": 2})
        elif self.meets_minimum_version(2024):
            self.link.Set({"Path": "Vars.Viewport.Texture", "Value": "User"})

        self.link.Set({"Path": "Vars.Viewport.ViewportUV.Textured", "Value": True})
        self.link.Set({"Path": "Vars.Viewport.Viewport3D.Textured", "Value": True})

    def load_file(self, uvmap: str | None = None) -> tuple[bool, str]:
        """Laod the file into RizomUV and select the given UVMap.

        Args:
        ----
            context: Blender context.
            uvmap: Name of the UVMap to select.

        Returns:
        -------
            True if the load was successful, info message.

        """
        print(self.prefs.export_format)
        file_path = get_file_as_string(Path(self.prefs.data_path), self.prefs.export_format)
        if not file_path:
            return False, f"RizomUVLink: {file_path} not found"

        params = {
            "File.Path": file_path,
            "File.XYZUVW": True,
            "File.UVWProps": True,
            "File.ImportGroups": True,
            "File.Normals": True,
            "File.AutoWeld": True,
            "__Focus": True,
        }
        self.link.Load(params)

        if self.prefs.export_format == "OBJ":
            self.link.Set({"Path": "Lib.Scene.Settings.Unit", "Value": "m", "UndoAble": False})
            if uvmap is not None:
                self.link.Uvset({"Mode": "Rename", "Name": uvmap})

        if uvmap:
            self.select_uvmap(uvmap)

        if self.prefs.texture_path:
            self.load_texture_file()

        return True, "RizomUVLink: File loaded into RizomUV"

    def _set_autoseams_settings(self, action: Literal["SHARP_EDGES", "MOSAIC", "PELT"]):
        """Set user configured autoseams settings based on selected preset.

        Args:
        ----
            action: The autoseams algorithm to use.

        """
        match action:
            case "SHARP_EDGES":
                self.link.Set(
                    {"Path": "Vars.AutoSelect.SharpEdges.Angle", "Value": self.prefs.sharp_angle}
                )
            case "MOSAIC":
                self.link.Set(
                    {
                        "Path": "Vars.AutoSelect.Mosaic.Developability",
                        "Value": self.prefs.mosaic_force,
                    }
                )
            case "PELT":
                self.link.Set(
                    {
                        "Path": "Vars.AutoSelect.Hierarchical.Branches",
                        "Value": self.prefs.pelt_branch,
                    }
                )
                self.link.Set(
                    {"Path": "Vars.AutoSelect.Hierarchical.Leafs", "Value": self.prefs.pelt_leaf}
                )
                self.link.Set(
                    {"Path": "Vars.AutoSelect.Hierarchical.Trunk", "Value": self.prefs.pelt_trunk}
                )

        stretch_limiter = False
        if self.prefs.stretch_quality > 0:
            stretch_limiter = True

        self.link.Set({"Path": "Vars.AutoSelect.Bijectiver", "Value": stretch_limiter})
        self.link.Set(
            {"Path": "Vars.AutoSelect.BijectiverMinQ", "Value": self.prefs.stretch_quality}
        )
        self.link.Set({"Path": "Vars.AutoSelect.CutHandles", "Value": self.prefs.cut_handles})
        self.link.Set({"Path": "Vars.AutoSelect.LinkHoles", "Value": self.prefs.link_gaps})
        self.link.Set({"Path": "Vars.AutoSelect.OpenCylinders", "Value": self.prefs.open_cylinders})

    def _autoseams_select(self, action: Literal["SHARP_EDGES", "MOSAIC", "PELT"]):
        """Select edges using the Rizom autoseams algorithms.

        Args:
        ----
            action: The autoseams algorithm to use.

        """
        self._set_autoseams_settings(action)
        print("sdasdas")
        params = {
            "PrimType": "Edge",
            "WorkingSet": "Visible&UnLocked",
            "IslandGroupMode": "Group",
            "Select": True,
            "ResetBefore": True,
            "ProtectMapName": "Protect",
            "FilterIslandVisible": True,
            "Auto": {
                "PipesCutter": self.link.Get("Vars.AutoSelect.CutHandles"),
                "HandleCutter": self.link.Get("Vars.AutoSelect.LinkHoles"),
                "QuadLoopCutter": self.link.Get("Vars.AutoSelect.OpenCylinders"),
                "StretchLimiter": self.link.Get("Vars.AutoSelect.Bijectiver"),
                "Quality": self.link.Get("Vars.AutoSelect.BijectiverMinQ"),
                "StoreCoordsUVW": True,
                "FlatteningUnfoldParams": {
                    "BorderIntersections": self.link.Get("Prefs.OverlapsOn"),
                    "TriangleFlips": self.link.Get("Prefs.TriangleFlipsOn"),
                },
            },
        }

        # Append specific algorithm settings to the autoseams dictionary
        autoseams_section = params["Auto"]
        match action:
            case "SHARP_EDGES":
                autoseams_section["SharpEdges"] = {"AngleMin": self.prefs.sharp_angle}
            case "MOSAIC":
                autoseams_section["QuasiDevelopable"] = {
                    "Developability": self.prefs.mosaic_force,
                    "IslandPolyNBMin": 1,
                    "FitCones": False,
                    "Straighten": True,
                }
            case "PELT":
                variables = (self.prefs.pelt_trunk, self.prefs.pelt_branch, self.prefs.pelt_leaf)
                seg_levels = []
                for i, pref in enumerate(variables, start=1):
                    if pref:
                        seg_levels.append(i)
                autoseams_section["Skeleton"] = {"Open": True, "SegLevels": seg_levels}

        self.link.Select(params)

    def set_rizom_settings(self):
        """Set RizomUV settings."""
        self.link.Set({"Path": "Prefs.TriangleFlipsOn", "Value": self.prefs.prevent_flips})
        self.link.Set({"Path": "Prefs.OverlapsOn", "Value": self.prefs.prevent_overlaps})
        self.link.Set({"Path": "Vars.Unwrap.FillHoles", "Value": self.prefs.fill_holes})

        self.link.IslandGroups(
            {
                "Mode": "SetGroupsProperties",
                "WorkingSet": "Visible",
                "GroupPaths": ["RootGroup"],
                "Properties": {
                    "Pack": {"MarginSize": (self.prefs.margin * (1 / self.prefs.map_res))}
                },
            }
        )
        self.link.IslandGroups(
            {
                "Mode": "SetGroupsProperties",
                "WorkingSet": "Visible",
                "GroupPaths": ["RootGroup"],
                "Properties": {
                    "Pack": {"PaddingSize": (self.prefs.padding * (1 / self.prefs.map_res))}
                },
            }
        )
        self.link.IslandGroups(
            {
                "Mode": "SetGroupsProperties",
                "WorkingSet": "Visible",
                "GroupPath": "RootGroup",
                "Properties": {"Pack": {"MapResolution": self.prefs.map_res}},
            }
        )
        self.link.IslandGroups(
            {
                "Mode": "SetGroupsProperties",
                "WorkingSet": "Visible",
                "GroupPath": "RootGroup",
                "Properties": {
                    "Pack": {
                        "Scaling": {
                            "TexelDensity": texel_density(self.prefs.td_target, self.prefs.td_unit)
                        }
                    }
                },
            }
        )
        self.link.Set(
            {
                "Path": "Lib.Scene.Settings.TexelDensityDisplayUnit",
                "Value": f"tx/{self.prefs.td_unit}",
                "UndoAble": True,
            }
        )

        self.link.IslandGroups(
            {
                "Mode": "SetGroupsProperties",
                "WorkingSet": "Visible",
                "GroupPaths": ["RootGroup"],
                "Properties": {"Pack": {"Rotate": {"Mode": int(self.prefs.orientation)}}},
            }
        )

        self.link.IslandGroups(
            {
                "Mode": "SetGroupsProperties",
                "WorkingSet": "Visible",
                "GroupPaths": ["RootGroup"],
                "Properties": {"Pack": {"Rotate": {"Step": self.prefs.step_angle}}},
            }
        )

    def run_autoseams(self, action: Literal["SHARP_EDGES", "MOSAIC", "PELT"]):
        """Use the given autoseams algorithm to automatically unwrap the mesh.

        Args:
        ----
            action: The autoseams algorithm to use.

        """
        self._autoseams_select(action)
        self.link.Cut({"PrimType": "Edge", "WorkingSet": "Visible&UnLocked"})
        self.link.Load({"Data": {"CoordsUVWInternalPath": "#Mesh.Tmp.AutoSelect.UVW"}})
        self.link.IslandGroups({"Mode": "DistributeInTilesByBBox", "WorkingSet": "Visible"})
        self.link.IslandGroups(
            {
                "Mode": "DistributeTilesContent",
                "WorkingSet": "Visible&UnLocked",
                "FreezeIslands": True,
                "UseTileLocks": True,
                "UseIslandLocks": True,
                "GroupPaths": ["RootGroup"],
            }
        )
        self.link.Pack(
            {
                "RootGroup": "RootGroup",
                "WorkingSet": "Visible",
                "ProcessTileSelection": False,
                "RecursionDepth": 1,
                "Translate": True,
                "AuxGroup": "RootGroup",
                "LayoutScalingMode": 0,
            }
        )

    def select_islands(self, islands: list[int], reset: bool = True):
        """Select the given UV islands.

        Args:
        ----
            islands: List of UV island ids to select.
            reset: Whether to reset the selection before selecting the given islands.

        """
        self.link.Select(
            {
                "PrimType": "Island",
                "WorkingSet": "Visible",
                "IslandGroupMode": "Group",
                "Select": True,
                "ResetBefore": reset,
                "IDs": islands,
                "List": True,
            }
        )

    def move_selected_islands(self, transform: Float9):
        """Move the selected UV islands by the given transform.

        Args:
        ----
            transform: List of 9 floats to offset the islands by.

        """
        if self.meets_minimum_version(2023):
            self.link.Move(
                {
                    "WorkingSet": "Visible&Selected",
                    "PrimType": "Island",
                    "IslandGroupMode": "Group",
                    "Geometrical": "Transform",
                    "Transform": transform,
                }
            )
        elif self.meets_minimum_version(2024):
            self.link.Deform(
                {
                    "WorkingSet": "Visible&Selected",
                    "PrimType": "Island",
                    "Transform": transform,
                }
            )

    def select_uvmap(self, uvmap: str):
        """Select the given UVMap.

        Args:
        ----
            uvmap: Name of the UVMap to select.

        """
        self.link.Uvset({"Mode": "SetCurrent", "Name": uvmap})

    def copy_to_all_uvsets(self, source_uvmap: str):
        """Copy the data from one UVMap to all other UVMaps.

        Args:
        ----
            source_uvmap: Name of the source UVMap.

        """
        uvsets = self.get_uvmaps()
        for uvmap in uvsets:
            if uvmap != source_uvmap:
                self.select_uvmap(uvmap)
                self.link.Uvset({"Mode": "Copy", "Name": source_uvmap})

    def get_selected_groups(self) -> tuple[list[str], list[str]]:
        """Get the selected groups in RizomUV.

        Returns
        -------
            tile_paths: List of paths to the root tiles.
            selected_groups: List of paths to selected groups (including tile).

        """
        tile_paths = [
            f"Lib.Mesh.RootGroup.Children.{tile}"
            for tile in self.link.ItemNames("Lib.Mesh.RootGroup.Children")
        ]
        return tile_paths, [
            f"{tile}.Children.{group_path}"
            for tile in tile_paths
            for group_path in self.link.ItemNames(f"{tile}.Children")
            if self.link.Get(f"{tile}.Children.{group_path}.Properties.Selected")
        ]

    def offset_stacks_for_bake(self):
        """Offset UV Islands for baking.

        Usage:
        - Group stacks, either 1 group for all stacks or 1 group for each stack.
        - Select groups (Islands dont matter, can select or not).
        - Run function.

        Reasoning behind allowing for both workflows is that RizomUV has two
        built in functions for grouping overlapping islands. One parents all stacks
        under a single group, the other parents each stack to its own individual group.

        """
        _, selected_groups = self.get_selected_groups()

        if len(selected_groups) == 1:
            island_stacks = {}
            islands = self.link.Get(f"{selected_groups[0]}.IslandIDs")
            for island in islands:
                pos = tuple(self.link.Eval(f"Lib.Mesh.Islands.{island}.GetBBoxUVW"))
                if pos in island_stacks:
                    island_stacks[pos].append(island)
                else:
                    island_stacks[pos] = [island]

            island_ids = [
                int(island)
                for stack in island_stacks.values()
                if len(stack) > 1
                for island in stack[1:]
            ]
        else:
            island_ids = []
            for group in selected_groups:
                group_islands = self.link.Get(f"{group}.IslandIDs")
                if group_islands:
                    island_ids.extend(int(island) for island in group_islands[1:])

        # Make sure groups are deselected or it will move every island in the group
        self.link.Select(
            {
                "PrimType": "IslandGroup",
                "WorkingSet": "Visible",
                "DeSelect": True,
                "Names": [os.path.splitext(group)[-1][1:] for group in selected_groups],
            }
        )
        self.select_islands(island_ids)

        one_unit_right_transform = [1, 0, 1, 0, 1, 0, 0, 0, 1]
        self.move_selected_islands(one_unit_right_transform)

    def quit(self):
        """Close the connected RizomUV instance."""
        self.link.Quit()

    def __str__(self) -> str:
        """Print override.

        Returns
        -------
            A string representation of the RizomConnection instance

        """
        return f"<RizomUVLink: port={self.link.port}>"
