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

from typing import Optional, TYPE_CHECKING, List

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

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):
        # 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.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):
        return self._owner

    def updateOwner(self, owner):
        if self._owner != owner:
            self._owner = owner
            self.ownerChanged.emit()

    @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 self._time_total

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

    @pyqtProperty(int, notify = timeElapsedChanged)
    def timeRemaining(self) -> int:
        # Never get a negative time remaining
        return 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):
        if self._time_total != new_time_total:
            self._time_total = new_time_total
            self.timeTotalChanged.emit()

    def updateTimeElapsed(self, new_time_elapsed):
        if self._time_elapsed != new_time_elapsed:
            self._time_elapsed = new_time_elapsed
            self.timeElapsedChanged.emit()

    def updateState(self, new_state):
        if self._state != new_state:
            self._state = new_state
            self.stateChanged.emit()

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