"""Used to send commmands to RizomUV via intermediary files."""

import os
import platform
import subprocess
import time
from pathlib import Path
from subprocess import Popen
from typing import Tuple

from rizomuvbridge_ultimate import constants
from rizomuvbridge_ultimate.preferences import RizomUVBridgePreferences
from rizomuvbridge_ultimate.rizom.script_link.rizom_cmds import (
    BlenderModel,
    RizomModel,
    RizomSessionModel,
)


def check_output(pid: int, encode: str) -> bool:
    """Check if the Rizom process is still running.

    Args:
    ----
        pid: Rizom PID.
        encode: Encoding to use.

    Returns:
    -------
        bool: True if the Rizom process is still running.

    """
    output = subprocess.check_output(
        ["tasklist", "/fi", f"PID eq {pid}"], text=True, encoding="utf-8"
    )
    return str(pid) in output


class B2RRemote:
    """Used to send commands to RizomUV from Blender.

    Works by writing data to json files which are ready
    by a python script loaded by Rizom.

    """

    pid: int | None = None
    process: Popen[bytes] | None = None

    def __init__(self, prefs: "RizomUVBridgePreferences"):
        """Initialize the instance.

        Args:
        ----
            prefs: Addon preferences.

        """
        self.prefs = prefs
        self.cmd_file = Path(__file__).parent / "rizom_cmds.py"
        self.pid_file = os.path.join(self.prefs.data_path, constants.RIZOM_PID)
        self.rizom_output = os.path.join(self.prefs.data_path, constants.RIZOM_OUTPUT)
        self.blender_output = os.path.join(self.prefs.data_path, constants.BLENDER_OUTPUT)
        self.blender_model = BlenderModel()

    def check_process(self) -> bool:
        """Check if the Rizom process is still running.

        First check the store which is kept in memory, if it is not found
        then read the rizom_output.json. This is done so that the connection
        can be resumed in the unusual case of a Blender crash or close but the
        file isnt read every time.

        Returns
        -------
            True if the Rizom process is still running.

        """
        pid = self.pid

        if pid is None:
            session = RizomSessionModel.deserialise()
            pid = session.pid
            self.pid = session.pid

        if pid is not None:
            if platform.system() == "Darwin":
                try:
                    result = subprocess.run(
                        ["ps", "-p", str(pid)], stdout=subprocess.PIPE, stderr=subprocess.PIPE
                    )
                    if str(pid) in result.stdout.decode():
                        return True
                    return False
                except subprocess.SubprocessError:
                    return False

            if platform.system() == "Windows":
                try:
                    return check_output(pid, "latin-1")
                except UnicodeDecodeError:
                    return check_output(pid, "utf-8")
                except subprocess.CalledProcessError:
                    return False

        return False

    def connect(self):
        """Try to look for an existing Rizom process.

        If a running Rizom process can be found just trigger a reload.
        If not, start a new Rizom process and store its PID.

        """
        if self.check_process():
            return self.trigger_ruv_reload()

        try:
            self.process = Popen([self.prefs.rizom_path, "-cfi", self.cmd_file])
        except Exception as e:
            raise Exception(f"Error: {e}")

        self.pid = self.process.pid
        session = RizomSessionModel.deserialise()
        session.set_properties(pid=self.pid, safe=False)
        return session

    def send_data_to_rizom(self, data: BlenderModel) -> Tuple[bool, str]:
        """Serialise the Blender data model and signal Rizom to read it.

        Args:
        ----
            data: Blender data model.

        Returns:
        -------
            True if the file was sent successfully, info message.

        """
        data.serialise()

        try:
            self.connect()
        except FileNotFoundError or OSError:
            return (
                False,
                f"Rizom {self.prefs.rizom_path} could not be found, ensure the path is correct",
            )
        return True, "B2RRemote: Objects sent to RizomUV"

    def request_import(self) -> RizomModel:
        """Request the mesh from the Rizom process.

        Trigger a reload which will cause Rizom to run the
        import code in rizom_cmds.py.

        Returns
        -------
            Rizom output data.

        """
        blender_model = BlenderModel.deserialise()
        blender_model.set_properties(operation="IMPORT")
        session = self.trigger_ruv_reload()
        return self.await_rizom_update(session)

    def request_state(self) -> RizomModel:
        """Request the curretn state from the Rizom process.

        Trigger a reload which will cause Rizom to run the
        import code in rizom_cmds responsible for using the
        Rizom API to update the intermediary file.

        Returns
        -------
            RizomOutput: Rizom output data.

        """
        self.blender_model.set_properties(operation="REQUEST_STATE")
        session = self.trigger_ruv_reload()
        return self.await_rizom_update(session)

    def await_rizom_update(self, session_model: RizomSessionModel) -> RizomModel:
        """Wait for the data file to unlock.

        Rizom is responsible for unlocking the file which notifies
        Blender that the mesh is ready to be imported and the
        data is updated.

        Returns
        -------
            RizomOutput: Rizom output data.

        """
        while not session_model.safe:
            session_model.update_from_file()

        # Rizom is done writing, reset the safe flag and build data model from file
        session_model.set_properties(safe=False)
        return RizomModel.deserialise()

    def trigger_ruv_reload(self) -> RizomSessionModel:
        """Update the command file timestamp to trigger a refresh.

        Rizom remote file works by watching the command file for changes.
        Can fake a change by updating the timestamp which will trigger a refresh.

        Returns
        -------
            RizomSessionModel: Rizom session model.

        """
        session_model = RizomSessionModel.deserialise()
        session_model.set_properties(safe=False)
        new_time = time.time()
        os.utime(self.cmd_file, (new_time, new_time))
        return session_model

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

        Returns
        -------
            str: A string representation of the B2RRemote instance

        """
        return f"<B2RRemote: pid={self.pid}>)"
