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

from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from typing import Any, Dict, List, Optional, Set, Tuple, TYPE_CHECKING

from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer

import cura.CuraApplication
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.cura_empty_instance_containers import empty_intent_container

if TYPE_CHECKING:
    from UM.Settings.InstanceContainer import InstanceContainer


##  Front-end for querying which intents are available for a certain
#   configuration.
class IntentManager(QObject):
    __instance = None

    ##  This class is a singleton.
    @classmethod
    def getInstance(cls):
        if not cls.__instance:
            cls.__instance = IntentManager()
        return cls.__instance

    intentCategoryChanged = pyqtSignal() #Triggered when we switch categories.

    ##  Gets the metadata dictionaries of all intent profiles for a given
    #   configuration.
    #
    #   \param definition_id ID of the printer.
    #   \param nozzle_name Name of the nozzle.
    #   \param material_base_file The base_file of the material.
    #   \return A list of metadata dictionaries matching the search criteria, or
    #   an empty list if nothing was found.
    def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]:
        intent_metadatas = []  # type: List[Dict[str, Any]]
        try:
            materials = ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials
        except KeyError:
            Logger.log("w", "Unable to find the machine %s or the variant %s", definition_id, nozzle_name)
            materials = {}
        if material_base_file not in materials:
            return intent_metadatas

        material_node = materials[material_base_file]
        for quality_node in material_node.qualities.values():
            for intent_node in quality_node.intents.values():
                intent_metadatas.append(intent_node.getMetadata())
        return intent_metadatas

    ##  Collects and returns all intent categories available for the given
    #   parameters. Note that the 'default' category is always available.
    #
    #   \param definition_id ID of the printer.
    #   \param nozzle_name Name of the nozzle.
    #   \param material_id ID of the material.
    #   \return A set of intent category names.
    def intentCategories(self, definition_id: str, nozzle_id: str, material_id: str) -> List[str]:
        categories = set()
        for intent in self.intentMetadatas(definition_id, nozzle_id, material_id):
            categories.add(intent["intent_category"])
        categories.add("default") #The "empty" intent is not an actual profile specific to the configuration but we do want it to appear in the categories list.
        return list(categories)

    ##  List of intents to be displayed in the interface.
    #
    #   For the interface this will have to be broken up into the different
    #   intent categories. That is up to the model there.
    #
    #   \return A list of tuples of intent_category and quality_type. The actual
    #   instance may vary per extruder.
    def getCurrentAvailableIntents(self) -> List[Tuple[str, str]]:
        application = cura.CuraApplication.CuraApplication.getInstance()
        global_stack = application.getGlobalContainerStack()
        if global_stack is None:
            return [("default", "normal")]
            # TODO: We now do this (return a default) if the global stack is missing, but not in the code below,
            #       even though there should always be defaults. The problem then is what to do with the quality_types.
            #       Currently _also_ inconsistent with 'currentAvailableIntentCategories', which _does_ return default.
        quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
        available_quality_types = {quality_group.quality_type for quality_group in quality_groups.values() if quality_group.node_for_global is not None}

        final_intent_ids = set()  # type: Set[str]
        current_definition_id = global_stack.definition.getId()
        for extruder_stack in global_stack.extruderList:
            if not extruder_stack.isEnabled:
                continue
            nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
            material_id = extruder_stack.material.getMetaDataEntry("base_file")
            final_intent_ids |= {metadata["id"] for metadata in self.intentMetadatas(current_definition_id, nozzle_name, material_id) if metadata.get("quality_type") in available_quality_types}

        result = set()  # type: Set[Tuple[str, str]]
        for intent_id in final_intent_ids:
            intent_metadata = application.getContainerRegistry().findContainersMetadata(id = intent_id)[0]
            result.add((intent_metadata["intent_category"], intent_metadata["quality_type"]))
        return list(result)

    ##  List of intent categories available in either of the extruders.
    #
    #   This is purposefully inconsistent with the way that the quality types
    #   are listed. The quality types will show all quality types available in
    #   the printer using any configuration. This will only list the intent
    #   categories that are available using the current configuration (but the
    #   union over the extruders).
    #   \return List of all categories in the current configurations of all
    #   extruders.
    def currentAvailableIntentCategories(self) -> List[str]:
        global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
        if global_stack is None:
            return ["default"]
        current_definition_id = global_stack.definition.getId()
        final_intent_categories = set()  # type: Set[str]
        for extruder_stack in global_stack.extruderList:
            if not extruder_stack.isEnabled:
                continue
            nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
            material_id = extruder_stack.material.getMetaDataEntry("base_file")
            final_intent_categories.update(self.intentCategories(current_definition_id, nozzle_name, material_id))
        return list(final_intent_categories)

    ##  The intent that gets selected by default when no intent is available for
    #   the configuration, an extruder can't match the intent that the user
    #   selects, or just when creating a new printer.
    def getDefaultIntent(self) -> InstanceContainer:
        return empty_intent_container

    @pyqtProperty(str, notify = intentCategoryChanged)
    def currentIntentCategory(self) -> str:
        application = cura.CuraApplication.CuraApplication.getInstance()
        active_extruder_stack = application.getMachineManager().activeStack
        if active_extruder_stack is None:
            return ""
        return active_extruder_stack.intent.getMetaDataEntry("intent_category", "")

    ##  Apply intent on the stacks.
    @pyqtSlot(str, str)
    def selectIntent(self, intent_category: str, quality_type: str) -> None:
        Logger.log("i", "Attempting to set intent_category to [%s] and quality type to [%s]", intent_category, quality_type)
        old_intent_category = self.currentIntentCategory
        application = cura.CuraApplication.CuraApplication.getInstance()
        global_stack = application.getGlobalContainerStack()
        if global_stack is None:
            return
        current_definition_id = global_stack.definition.getId()
        machine_node = ContainerTree.getInstance().machines[current_definition_id]
        for extruder_stack in global_stack.extruderList:
            nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
            material_id = extruder_stack.material.getMetaDataEntry("base_file")

            material_node = machine_node.variants[nozzle_name].materials[material_id]

            # Since we want to switch to a certain quality type, check the tree if we have one.
            quality_node = None
            for q_node in material_node.qualities.values():
                if q_node.quality_type == quality_type:
                    quality_node = q_node

            if quality_node is None:
                Logger.log("w", "Unable to find quality_type [%s] for extruder [%s]", quality_type, extruder_stack.getId())
                continue

            # Check that quality node if we can find a matching intent.
            intent_id = None
            for id, intent_node in quality_node.intents.items():
                if intent_node.intent_category == intent_category:
                    intent_id = id
            intent = application.getContainerRegistry().findContainers(id = intent_id)
            if intent:
                extruder_stack.intent = intent[0]
            else:
                extruder_stack.intent = self.getDefaultIntent()
        application.getMachineManager().setQualityGroupByQualityType(quality_type)
        if old_intent_category != intent_category:
            self.intentCategoryChanged.emit()
