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

from collections import OrderedDict

from PyQt5.QtCore import Qt

from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel

from cura.QualityManager import QualityManager
from cura.Settings.ExtruderManager import ExtruderManager

from typing import List, TYPE_CHECKING

if TYPE_CHECKING:
    from cura.Settings.ExtruderStack import ExtruderStack


##  QML Model for listing the current list of valid quality profiles.
#
class ProfilesModel(InstanceContainersModel):
    LayerHeightRole = Qt.UserRole + 1001
    LayerHeightWithoutUnitRole = Qt.UserRole + 1002
    AvailableRole = Qt.UserRole + 1003

    def __init__(self, parent = None):
        super().__init__(parent)
        self.addRoleName(self.LayerHeightRole, "layer_height")
        self.addRoleName(self.LayerHeightWithoutUnitRole, "layer_height_without_unit")
        self.addRoleName(self.AvailableRole, "available")

        Application.getInstance().globalContainerStackChanged.connect(self._update)
        Application.getInstance().getMachineManager().activeVariantChanged.connect(self._update)
        Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
        Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update)

    # Factory function, used by QML
    @staticmethod
    def createProfilesModel(engine, js_engine):
        return ProfilesModel.getInstance()

    ##  Get the singleton instance for this class.
    @classmethod
    def getInstance(cls) -> "ProfilesModel":
        # Note: Explicit use of class name to prevent issues with inheritance.
        if not ProfilesModel.__instance:
            ProfilesModel.__instance = cls()
        return ProfilesModel.__instance

    __instance = None   # type: "ProfilesModel"

    ##  Fetch the list of containers to display.
    #
    #   See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
    def _fetchInstanceContainers(self):
        global_container_stack = Application.getInstance().getGlobalContainerStack()
        if global_container_stack is None:
            return []

        global_stack_definition = global_container_stack.definition

        # Get the list of extruders and place the selected extruder at the front of the list.
        extruder_stacks = self._getOrderedExtruderStacksList()
        materials = [extruder.material for extruder in extruder_stacks]

        # Fetch the list of usable qualities across all extruders.
        # The actual list of quality profiles come from the first extruder in the extruder list.
        result = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)

        # The usable quality types are set
        quality_type_set = set([x.getMetaDataEntry("quality_type") for x in result])

        # Fetch all qualities available for this machine and the materials selected in extruders
        all_qualities = QualityManager.getInstance().findAllQualitiesForMachineAndMaterials(global_stack_definition, materials)

        # If in the all qualities there is some of them that are not available due to incompatibility with materials
        # we also add it so that they will appear in the slide quality bar. However in recomputeItems will be marked as
        # not available so they will be shown in gray
        for quality in all_qualities:
            if quality.getMetaDataEntry("quality_type") not in quality_type_set:
                result.append(quality)

        # if still profiles are found, add a single empty_quality ("Not supported") instance to the drop down list
        if len(result) == 0:
            # If not qualities are found we dynamically create a not supported container for this machine + material combination
            not_supported_container = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0]
            result.append(not_supported_container)

        return result

    ##  Re-computes the items in this model, and adds the layer height role.
    def _recomputeItems(self):

        # Some globals that we can re-use.
        global_container_stack = Application.getInstance().getGlobalContainerStack()
        if global_container_stack is None:
            return

        extruder_stacks = self._getOrderedExtruderStacksList()
        container_registry = ContainerRegistry.getInstance()

        # Get a list of usable/available qualities for this machine and material
        qualities = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)

        unit = global_container_stack.getBottom().getProperty("layer_height", "unit")
        if not unit:
            unit = ""

        # group all quality items according to quality_types, so we know which profile suits the currently
        # active machine and material, and later yield the right ones.
        tmp_all_quality_items = OrderedDict()
        for item in super()._recomputeItems():
            profile = container_registry.findContainers(id=item["id"])
            quality_type = profile[0].getMetaDataEntry("quality_type") if profile else ""

            if quality_type not in tmp_all_quality_items:
                tmp_all_quality_items[quality_type] = {"suitable_container": None, "all_containers": []}

            tmp_all_quality_items[quality_type]["all_containers"].append(item)
            if tmp_all_quality_items[quality_type]["suitable_container"] is None:
                tmp_all_quality_items[quality_type]["suitable_container"] = item

        # reverse the ordering (finest first, coarsest last)
        all_quality_items = OrderedDict()
        for key in reversed(tmp_all_quality_items.keys()):
            all_quality_items[key] = tmp_all_quality_items[key]

        # First the suitable containers are set in the model
        containers = []
        for data_item in all_quality_items.values():
            suitable_item = data_item["suitable_container"]
            if suitable_item is not None:
                containers.append(suitable_item)

        # Once the suitable containers are collected, the rest of the containers are appended
        for data_item in all_quality_items.values():
            for item in data_item["all_containers"]:
                if item not in containers:
                    containers.append(item)

        # Now all the containers are set
        for item in containers:
            profile = container_registry.findContainers(id = item["id"])

            # When for some reason there is no profile container in the registry
            if not profile:
                self._setItemLayerHeight(item, "", "")
                item["available"] = False
                yield item
                continue

            profile = profile[0]

            # When there is a profile but it's an empty quality should. It's shown in the list (they are "Not Supported" profiles)
            if profile.getId() == "empty_quality":
                self._setItemLayerHeight(item, "", "")
                item["available"] = True
                yield item
                continue

            item["available"] = profile in qualities

            # Easy case: This profile defines its own layer height.
            if profile.hasProperty("layer_height", "value"):
                self._setItemLayerHeight(item, profile.getProperty("layer_height", "value"), unit)
                yield item
                continue

            machine_manager = Application.getInstance().getMachineManager()

            # Quality-changes profile that has no value for layer height. Get the corresponding quality profile and ask that profile.
            quality_type = profile.getMetaDataEntry("quality_type", None)
            if quality_type:
                quality_results = machine_manager.determineQualityAndQualityChangesForQualityType(quality_type)
                for quality_result in quality_results:
                    if quality_result["stack"] is global_container_stack:
                        quality = quality_result["quality"]
                        break
                else:
                    # No global container stack in the results:
                    if quality_results:
                        # Take any of the extruders.
                        quality = quality_results[0]["quality"]
                    else:
                        quality = None
                if quality and quality.hasProperty("layer_height", "value"):
                    self._setItemLayerHeight(item, quality.getProperty("layer_height", "value"), unit)
                    yield item
                    continue

            # Quality has no value for layer height either. Get the layer height from somewhere lower in the stack.
            skip_until_container = global_container_stack.material
            if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer():  # No material in stack.
                skip_until_container = global_container_stack.variant
                if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer():  # No variant in stack.
                    skip_until_container = global_container_stack.getBottom()
            self._setItemLayerHeight(item, global_container_stack.getRawProperty("layer_height", "value", skip_until_container = skip_until_container.getId()), unit)  # Fall through to the currently loaded material.
            yield item

    ## Get a list of extruder stacks with the active extruder at the front of the list.
    @staticmethod
    def _getOrderedExtruderStacksList() -> List["ExtruderStack"]:
        extruder_manager = ExtruderManager.getInstance()
        extruder_stacks = extruder_manager.getActiveExtruderStacks()
        active_extruder = extruder_manager.getActiveExtruderStack()

        if active_extruder in extruder_stacks:
            extruder_stacks.remove(active_extruder)
            extruder_stacks = [active_extruder] + extruder_stacks

        return extruder_stacks

    @staticmethod
    def _setItemLayerHeight(item, value, unit):
        item["layer_height"] = str(value) + unit
        item["layer_height_without_unit"] = str(value)
