"""File which is loaded into Rizom. Rizom has access to anything inside this file.

functions with the ruv prefix are only available in Rizom.
Rizom is Python 3.7 so ensure all functions are compatible.

This file is a little unorganized because it cannot have any external dependencies as Rizom
will not be able to resolve them. Any code that Rizom needs to be able to run must go in here.

"""

import json
import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING, List, Tuple, Type, TypeVar, Union

# Just doing this to shut the linter up, don't have any type stubs for Rizom API.
if TYPE_CHECKING:
    from types import ModuleType

    App = ModuleType("App")


def resolve_mac_files() -> Tuple[Path, Path, Path]:
    """Find the path to the data files needed to transfer data between Rizom and Blender.

    macOS only.

    Returns
    -------
        Tuple[Path, Path, Path]: Path to the rizom_output, blender_output and rizom_session files.

    """
    if "Rizom" in sys.executable:
        remote_cmds = App.Get("Prefs.RemoteControlFilePath")
        rizom_json = Path(remote_cmds).parents[2] / "data" / "rizom_output.json"
        blender_json = Path(remote_cmds).parents[2] / "data" / "blender_output.json"
        session_json = Path(remote_cmds).parents[2] / "data" / "rizom_session.json"

    else:
        rizom_json = Path(__file__).parents[2] / "data" / "rizom_output.json"
        blender_json = Path(__file__).parents[2] / "data" / "blender_output.json"
        session_json = Path(__file__).parents[2] / "data" / "rizom_session.json"

    return rizom_json, blender_json, session_json


def resolve_windows_files() -> Tuple[Path, Path, Path]:
    """Find the path to the data files needed to transfer data between Rizom and Blender.

    Windows only.

    Returns
    -------
        Tuple[Path, Path, Path]: Path to the rizom_output, blender_output and rizom_session files.

    """
    app_data = Path(os.getenv("USERPROFILE")) / "Documents"
    if app_data is None:
        raise RuntimeError("Documents directory not found")  # TODO: Replace with proper exception

    app_data_path = Path(app_data)

    rizom_json = app_data_path / "RizomUV Bridge" / "rizom_output.json"
    blender_json = app_data_path / "RizomUV Bridge" / "blender_output.json"
    session_json = app_data_path / "RizomUV Bridge" / "rizom_session.json"

    return rizom_json, blender_json, session_json


if os.name == "nt":
    rizom_json, blender_json, session_json = resolve_windows_files()
elif os.name == "posix":
    rizom_json, blender_json, session_json = resolve_mac_files()
else:
    raise RuntimeError(
        "Unsupported platform"
    )  # TODO: Replace with propper exception for unsupported platform


T = TypeVar("T", bound="DataModel")


class DataModel:
    """Data model base class.

    Used to represent data and provide an interface between
    Rizom and Blender.

    """

    json_path: Union[str, Path]

    def __init__(self):
        """Intialise the data model."""
        self.json_path = self.__class__.json_path

    def serialise(self):
        """Save the current state of the data model to a json file."""
        data = {k: v for k, v in self.__dict__.items() if k != "json_path"}
        with open(self.json_path, "w") as f:
            json.dump(data, f)

    @classmethod
    def deserialise(cls: Type[T]) -> T:
        """Fetch existing data from the json file and intialise the data model.

        Returns
        -------
            DataModel: Model instance.

        """
        try:
            with open(cls.json_path, "r") as f:
                data = json.load(f)
        except json.JSONDecodeError:
            data = {}

        instance = cls(**data)
        instance.serialise()  # Save the updated data back to the file
        return instance

    def set_properties(self, **kwargs):
        """Update the data model and the backing json file.

        Update the class properties and write to the json file
        to persist the changes across the Rizom and Blender sessions.

        No autocompletion because this needs to be compatible with
        Python 3.7 for RizomUV, meaning no TypedDict.

        Args:
        ----
            **kwargs: The properties to update.

        """
        for k, v in kwargs.items():
            self.__dict__[k] = v

        try:
            with open(self.json_path, "r") as f:
                data = json.load(f)
        except json.JSONDecodeError:
            data = {}

        data.update(kwargs)

        with open(self.json_path, "w") as f:
            json.dump(data, f)

    def update_from_file(self):
        """Update existing data model from the json file."""
        try:
            with open(self.json_path, "r") as f:
                data = json.load(f)
                self.__dict__.update(**data)
        except json.JSONDecodeError:
            pass


class RizomModel(DataModel):
    """Intialise the Rizom data model."""

    json_path = rizom_json

    def __init__(
        self,
        file_path: Union[str, Path] = "",
        uvmap: str = "",
        loaded_objects: List[str] = [],
    ):
        """Initialise the Rizom data model.

        Args:
        ----
            file_path (str): Path to the mesh file loaded in Rizom.
            uvmap (str): Name of the active UVMap in Rizom.
            meshes (list[str]): Names of the meshes in Rizom.

        """
        super().__init__()
        if isinstance(file_path, Path):
            self.file_path = str(file_path)
        else:
            self.file_path = file_path
        self.uvmap = uvmap
        self.loaded_objects = loaded_objects

    def sync_state_from_rizom(self):
        """Sync model with current state from Rizom and serialise."""
        self.file_path = App.Get("Prefs.LastLoadedFile")
        self.uvmap = App.Get("Lib.CurrentUVSetName")
        loaded_objects = App.Get("Lib.Mesh.ObjectsNames")
        if "DefaultObject" in loaded_objects:
            loaded_objects.remove("DefaultObject")
        if self.get_format() == "USD":
            loaded_objects = [obj.split("/")[-1] for obj in loaded_objects]

        self.loaded_objects = loaded_objects
        self.serialise()

    def get_format(self) -> str:
        """Return the format of the mesh file.

        Formats the extension into 3 letter suffix matching addon enums,
        eg .usdc becomes USD.

        Returns
        -------
            str: Format suffix of the mesh file. (FBX | OBJ | USD)

        """
        return os.path.splitext(self.file_path)[1][1:][:3].upper()

    def __str__(self):
        """Return string representation of the model."""
        model = "RizomModel:"
        file_str = f" - file_path: {self.file_path}"
        uvmap_str = f" - uvmap: {self.uvmap}"
        meshes_str = f" - loaded_objects: {self.loaded_objects}"
        return f"\n{model}\n{file_str}\n{uvmap_str}\n{meshes_str}"


class BlenderModel(DataModel):
    """Intialise the Blender data model."""

    json_path = blender_json

    def __init__(self, file_path: Union[str, Path] = "", uvmap: str = "", operation: str = ""):
        """Initialise the Blender data model.

        Args:
        ----
            file_path (str, optional): _description_. Defaults to "".
            uvmap (str, optional): _description_. Defaults to "".
            operation (str, optional): _description_. Defaults to "".

        """
        super().__init__()
        self.file_path = str(file_path)
        self.uvmap = uvmap
        self.operation = operation


class RizomSessionModel(DataModel):
    """Intialise the session data model.

    Contains data pertaining to the connection itself.

    """

    json_path = session_json
    print(json_path)

    def __init__(self, pid: int = -1, safe: bool = False):
        """Initialise the session data model."""
        super().__init__()
        self.pid = pid
        self.safe = safe


def ruv_load_mesh(model: BlenderModel):
    """Load the mesh file into RizomUV.

    Args:
    ----
        data (BlenderModel): Blender data model.

    """
    App.Load(
        {
            "File": {
                "Path": model.file_path,
                "ImportGroups": True,
                "XYZUVW": True,
                "FBX": {"UseUVSetNames": True},
                "AutoWeld": True,
                "Meta": True,
                "Normals": True,
            },
            "__Focus": True,
        }
    )


# Code that RizomUV runs when it loads.
# Contextually run different code based on the operation type written to Blender output file.

if "Rizom" in sys.executable:
    blender_model: BlenderModel = BlenderModel.deserialise()
    rizom_model = RizomModel()
    session_model = RizomSessionModel.deserialise()

    remote_file = App.Get("Prefs.RemoteControlFilePath")
    if not remote_file:
        raise FileNotFoundError

    if blender_model.operation in ["EXPORT", "LOAD"]:
        ruv_load_mesh(blender_model)

        if blender_model.operation == "EXPORT":
            # OBJ format causes the UV Map to lose its name, so restore it here.
            if blender_model.file_path.lower().endswith(".obj"):
                App.Uvset({"Mode": "Rename", "Name": blender_model.uvmap})

        rizom_model.sync_state_from_rizom()

    elif blender_model.operation == "IMPORT":
        rizom_model.sync_state_from_rizom()
        App.Save(
            {
                "File": {
                    rizom_model.get_format(): {"UseUVSetNames": True},
                    "Path": rizom_model.file_path,
                    "UVWProps": True,
                }
            }
        )
        session_model.set_properties(safe=True)

    elif blender_model.operation == "REQUEST_STATE":
        rizom_model.sync_state_from_rizom()
        session_model.set_properties(safe=True)
