# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.

from typing import Optional, TYPE_CHECKING, List

from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot, QUrl
from PyQt6.QtGui import QImage

from cura.CuraApplication import CuraApplication

if TYPE_CHECKING:
    from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
    from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
    from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel


class PrintJobOutputModel(QObject):
    stateChanged = pyqtSignal()
    timeTotalChanged = pyqtSignal()
    timeElapsedChanged = pyqtSignal()
    nameChanged = pyqtSignal()
    keyChanged = pyqtSignal()
    assignedPrinterChanged = pyqtSignal()
    ownerChanged = pyqtSignal()
    configurationChanged = pyqtSignal()
    previewImageChanged = pyqtSignal()
    compatibleMachineFamiliesChanged = pyqtSignal()

    def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent = None) -> None:
        super().__init__(parent)
        self._output_controller = output_controller
        self._state = ""
        self._time_total = 0
        self._time_elapsed = 0
        self._name = name  # Human readable name
        self._key = key  # Unique identifier
        self._assigned_printer = None  # type: Optional[PrinterOutputModel]
        self._owner = ""  # Who started/owns the print job?

        self._configuration = None  # type: Optional[PrinterConfigurationModel]
        self._compatible_machine_families = []  # type: List[str]
        self._preview_image_id = 0

        self._preview_image = None  # type: Optional[QImage]

    @pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
    def compatibleMachineFamilies(self) -> List[str]:
        # Hack; Some versions of cluster will return a family more than once...
        return list(set(self._compatible_machine_families))

    def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
        if self._compatible_machine_families != compatible_machine_families:
            self._compatible_machine_families = compatible_machine_families
            self.compatibleMachineFamiliesChanged.emit()

    @pyqtProperty(QUrl, notify=previewImageChanged)
    def previewImageUrl(self):
        self._preview_image_id += 1
        # There is an image provider that is called "print_job_preview". In order to ensure that the image qml object, that
        # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
        # as new (instead of relying on cached version and thus forces an update.
        temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
        return QUrl(temp, QUrl.ParsingMode.TolerantMode)

    def getPreviewImage(self) -> Optional[QImage]:
        return self._preview_image

    def updatePreviewImage(self, preview_image: Optional[QImage]) -> None:
        if self._preview_image != preview_image:
            self._preview_image = preview_image
            self.previewImageChanged.emit()

    @pyqtProperty(QObject, notify=configurationChanged)
    def configuration(self) -> Optional["PrinterConfigurationModel"]:
        return self._configuration

    def updateConfiguration(self, configuration: Optional["PrinterConfigurationModel"]) -> None:
        if self._configuration != configuration:
            self._configuration = configuration
            self.configurationChanged.emit()

    @pyqtProperty(str, notify = ownerChanged)
    def owner(self) -> str:
        return self._owner

    def updateOwner(self, owner: str) -> None:
        if self._owner != owner:
            self._owner = owner
            self.ownerChanged.emit()

    @pyqtProperty(bool, notify = ownerChanged)
    def isMine(self) -> bool:
        """
        Returns whether this print job was sent by the currently logged in user.

        This checks the owner of the print job with the owner of the currently
        logged in account. Both of these are human-readable account names which
        may be duplicate. In practice the harm here is limited, but it's the
        best we can do with the information available to the API.
        """
        return self._owner == CuraApplication.getInstance().getCuraAPI().account.userName

    @pyqtProperty(QObject, notify=assignedPrinterChanged)
    def assignedPrinter(self):
        return self._assigned_printer

    def updateAssignedPrinter(self, assigned_printer: Optional["PrinterOutputModel"]) -> None:
        if self._assigned_printer != assigned_printer:
            old_printer = self._assigned_printer
            self._assigned_printer = assigned_printer
            if old_printer is not None:
                # If the previously assigned printer is set, this job is moved away from it.
                old_printer.updateActivePrintJob(None)
            self.assignedPrinterChanged.emit()

    @pyqtProperty(str, notify=keyChanged)
    def key(self):
        return self._key

    def updateKey(self, key: str):
        if self._key != key:
            self._key = key
            self.keyChanged.emit()

    @pyqtProperty(str, notify = nameChanged)
    def name(self):
        return self._name

    def updateName(self, name: str):
        if self._name != name:
            self._name = name
            self.nameChanged.emit()

    @pyqtProperty(int, notify = timeTotalChanged)
    def timeTotal(self) -> int:
        return int(self._time_total)

    @pyqtProperty(int, notify = timeElapsedChanged)
    def timeElapsed(self) -> int:
        return int(self._time_elapsed)

    @pyqtProperty(int, notify = timeElapsedChanged)
    def timeRemaining(self) -> int:
        # Never get a negative time remaining
        return int(max(self.timeTotal - self.timeElapsed, 0))

    @pyqtProperty(float, notify = timeElapsedChanged)
    def progress(self) -> float:
        result = float(self.timeElapsed) / max(self.timeTotal, 1.0)  # Prevent a division by zero exception.
        return min(result, 1.0)  # Never get a progress past 1.0

    @pyqtProperty(str, notify=stateChanged)
    def state(self) -> str:
        return self._state

    @pyqtProperty(bool, notify=stateChanged)
    def isActive(self) -> bool:
        inactive_states = [
            "pausing",
            "paused",
            "resuming",
            "wait_cleanup"
        ]
        if self.state in inactive_states and self.timeRemaining > 0:
            return False
        return True

    def updateTimeTotal(self, new_time_total: int) -> None:
        if self._time_total != new_time_total:
            self._time_total = new_time_total
            self.timeTotalChanged.emit()

    def updateTimeElapsed(self, new_time_elapsed: int) -> None:
        if self._time_elapsed != new_time_elapsed:
            self._time_elapsed = new_time_elapsed
            self.timeElapsedChanged.emit()

    def updateState(self, new_state: str) -> None:
        if self._state != new_state:
            self._state = new_state
            self.stateChanged.emit()

    @pyqtSlot(str)
    def setState(self, state):
        self._output_controller.setJobState(self, state)
