import json
import os
from collections import defaultdict

from PySide6 import QtCore, QtGui, QtWidgets

from sasmodels.sasview_model import load_standard_models

from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
from sas.qtgui.Utilities.GuiUtils import communicator

from .UI.CategoryManagerUI import Ui_CategoryManagerUI
from .UI.ChangeCategoryUI import Ui_ChangeCategoryUI


class ToolTippedItemModel(QtGui.QStandardItemModel):
    """
    Subclass from QStandardItemModel to allow displaying tooltips in
    QTableView model.
    """
    def __init__(self, parent=None):
        QtGui.QStandardItemModel.__init__(self, parent)

    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        """
        Displays tooltip for each column's header
        :param section:
        :param orientation:
        :param role:
        :return:
        """
        if role == QtCore.Qt.ToolTipRole:
            if orientation == QtCore.Qt.Horizontal:
                return str(self.header_tooltips[section])

        return QtGui.QStandardItemModel.headerData(self, section, orientation, role)

class Categories:
    """
    Container class for accessing model categories
    """
    def __init__(self):
        self.master_category_dict = defaultdict(list)
        self.by_model_dict = defaultdict(list)
        self.model_enabled_dict = defaultdict(bool)
        self.models = {}

        # Prepare the master category list
        self.readCategoryInfo()

        # Prepare model->category lookup
        self.setupModelDict()

    def readCategoryInfo(self):
        """
        Reads the categories in from file
        """
        categorization_file = CategoryInstaller.get_user_file()
        if not os.path.isfile(categorization_file):
            categorization_file = CategoryInstaller.get_default_file()
        with open(categorization_file, 'rb') as cat_file:
            self.master_category_dict = json.load(cat_file)
            self.regenerateModelDict()

        self.category_list = sorted(self.master_category_dict.keys())

    def saveCategories(self):
        """
        Serializes categorization info to file
        """
        self.regenerateMasterDict()
        with open(CategoryInstaller.get_user_file(), 'w') as cat_file:
            json.dump(self.master_category_dict, cat_file)

    def setupModelDict(self):
        """
        create a dictionary for model->category mapping
        """
        # Load the model dict
        models = load_standard_models()
        for model in models:
            # {model name -> model object}
            self.models[model.name] = model

    def regenerateModelDict(self):
        """
        Regenerates self.by_model_dict which has each model name as the
        key and the list of categories belonging to that model
        along with the enabled mapping
        """
        self.by_model_dict = defaultdict(list)
        for category in self.master_category_dict:
            for (model, enabled) in self.master_category_dict[category]:
                self.by_model_dict[model].append(category)
                self.model_enabled_dict[model] = enabled

    def regenerateMasterDict(self):
        """
        regenerates self.master_category_dict from
        self.by_model_dict and self.model_enabled_dict
        """
        self.master_category_dict = defaultdict(list)
        for model in self.by_model_dict:
            for category in self.by_model_dict[model]:
                self.master_category_dict[category].append\
                    ((model, self.model_enabled_dict[model]))

    def modelToCategory(self):
        """
        Getter for the model->category dict
        """
        return self.by_model_dict

    def modelDict(self):
        """
        Getter for the model list
        """
        return self.models

    def categoryDict(self):
        """
        Getter for the category dict
        """
        return self.master_category_dict

    def categoryList(self):
        """
        Getter for the category list
        """
        return self.category_list


class CategoryManager(QtWidgets.QDialog, Ui_CategoryManagerUI):
    def __init__(self, parent=None, manager=None):
        super(CategoryManager, self).__init__(parent)
        self.setupUi(self)

        self.communicator = communicator

        self.manager = manager

        self.setWindowTitle("Category Manager")

        self.initializeGlobals()

        self.initializeModels()

        self.initializeSignals()

    def initializeGlobals(self):
        """
        Initialize global variables used in this class
        """
        # Default checked state
        self.chkEnable.setCheckState(QtCore.Qt.Checked)

        # Modify is disabled by default
        self.cmdModify.setEnabled(False)

        # Data for chosen model
        self.model_data = None

        # Categories object
        self.categories = Categories()

    def initializeModels(self):
        """
        Set up models and views
        """
        # Set the main models
        self._category_model = ToolTippedItemModel()
        self.lstCategory.setModel(self._category_model)
        # Proxy model for showing a subset of model content
        self.model_proxy = QtCore.QSortFilterProxyModel(self)
        self.model_proxy.setSourceModel(self._category_model)
        self.lstCategory.setModel(self.model_proxy)

        self.initializeModelList()

        self.setTableProperties(self.lstCategory)

        self.lstCategory.setAlternatingRowColors(True)
        # self.lstCategory.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        # self.lstCategory.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False)

    def initializeModelList(self):
        """
        Model category combo setup
        """
        self._category_model.clear()
        for ind, model in enumerate(self.categories.modelDict()):
            item = QtGui.QStandardItem(model)
            empty_item = QtGui.QStandardItem()
            empty_item.setEditable(False)
            # Add a checkbox to it
            item.setCheckable(True)
            checked = self.categories.model_enabled_dict[model]
            item.setCheckState(QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked)
            item.setEditable(False)
            current_category = self.categories.modelToCategory()[model]
            self._category_model.appendRow([item, empty_item])
            self._category_model.item(ind, 1).setText(', '.join(i for i in current_category))

    def initializeSignals(self):
        """
        :return:
        """
        self.cmdOK.clicked.connect(self.onClose)
        self.cmdModify.clicked.connect(self.onModify)
        self.cmdReset.clicked.connect(self.onReset)
        self.cmdHelp.clicked.connect(self.displayHelp)

        self.chkEnable.toggled.connect(self.onEnableAll)

        # every change in txtSearch
        self.txtSearch.textChanged.connect(self.onSearch)

        # Signals from the list
        selectionModel = self.lstCategory.selectionModel()
        selectionModel.selectionChanged.connect(self.onListSelection)
        self._category_model.itemChanged.connect(self.onListChanged)


    def onClose(self):
        """
        Save the category file before exiting
        """
        self.categories.saveCategories()
        # Ask the fitting widget to reload the comboboxes
        self.communicator.updateModelCategoriesSignal.emit()

        self.close()

    def onListChanged(self, item):
        # remember the state of the checkbox
        self.categories.model_enabled_dict[item.text()] = item.checkState() == QtCore.Qt.Checked

    def selectedModels(self):
        """
        Returns a list of selected models
        """
        selected_models = []
        selectionModel = self.lstCategory.selectionModel()
        selectedRows = selectionModel.selectedRows()
        for row in selectedRows:
            model_index = self.model_proxy.mapToSource(row)
            current_text = self._category_model.itemFromIndex(model_index).text()
            selected_models.append(self.categories.modelDict()[current_text])
        return selected_models

    def onListSelection(self):
        """
        Respond to row selection and update GUI
        """
        selected_items = self.selectedModels()
        self.cmdModify.setEnabled(len(selected_items) == 1)

    def onReset(self):
        """
        Reload the saved categories
        """
        self.initializeGlobals()
        # Reload the Categories object
        self.categories = Categories()
        # Reload the model
        self.initializeModelList()
        self.setTableProperties(self.lstCategory)
        self.lstCategory.setAlternatingRowColors(True)

    def displayHelp(self):
        location = "/user/qtgui/Perspectives/Fitting/fitting_help.html#category-manager"
        self.manager.showHelp(location)

    def onEnableAll(self, isChecked):
        """
        Respond to the Enable/Disable All checkbox
        """
        select = QtCore.Qt.Checked if isChecked else QtCore.Qt.Unchecked
        for row in range(self._category_model.rowCount()):
            self._category_model.item(row).setCheckState(select)

    def onSearch(self):
        """
        Respond to text entered in search field
        """
        if self.txtSearch.isModified():
            input_to_check = str(self.txtSearch.text())

        # redefine the proxy model
        self.model_proxy.setFilterRegularExpression(QtCore.QRegularExpression(input_to_check,
            QtCore.QRegularExpression.CaseInsensitiveOption |
            QtCore.QRegularExpression.PatternOption.NoPatternOption))

    def onModify(self):
        """
        Show the Change Category dialog - modal
        """
        # Selected category:
        selected_models = self.selectedModels()
        if len(selected_models) > 1:
            # Somehow we got more than one model - complain!
            raise AttributeError("Please select only one model.")
        change_dialog = ChangeCategory(parent=self, categories=self.categories, model=selected_models[0])

        if change_dialog.exec_() != QtWidgets.QDialog.Accepted:
            return
        # Reload the model
        self.initializeModelList()
        self.setTableProperties(self.lstCategory)
        self.lstCategory.setAlternatingRowColors(True)

    def setTableProperties(self, table):
        """
        Setting table properties
        """
        # Table properties
        table.verticalHeader().setVisible(False)
        table.setAlternatingRowColors(True)
        #table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
        table.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        table.resizeColumnsToContents()

        # Header
        header = table.horizontalHeader()
        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
        header.ResizeMode(QtWidgets.QHeaderView.Interactive)


        self._category_model.setHeaderData(0, QtCore.Qt.Horizontal, "Model")
        self._category_model.setHeaderData(1, QtCore.Qt.Horizontal, "Category")

        self._category_model.header_tooltips = ['Select model',
                                                'Change or create new category']

class ChangeCategory(QtWidgets.QDialog, Ui_ChangeCategoryUI):
    """
    Dialog for adding/removing categories for a single model
    """
    def __init__(self, parent=None, categories=None, model=None):
        super(ChangeCategory, self).__init__(parent)
        self.setupUi(self)

        self.model = model
        self.parent = parent
        self.categories = categories

        self.initializeElements()

        self.initializeList()

        self.initializeSignals()

    def initializeElements(self):
        """
        Initialize local GUI elements with information from the Categories object
        """
        self.cbCategories.addItems(self.categories.categoryList())
        self.setWindowTitle("Change Category for: "+ self.model.name)
        self.lblTitle.setText("Current categories for " + self.model.name)
        self.rbExisting.setChecked(True)
        self.onAddChoice()

    def initializeList(self):
        """
        Initialize the category list for the given model
        """
        current_categories = self.categories.modelToCategory()[self.model.name]
        for cat in current_categories:
            self.lstCategories.addItem(cat)

    def initializeSignals(self):
        """
        Initialize signals for UI elements
        """
        self.cmdAdd.clicked.connect(self.onAdd)
        self.cmdRemove.clicked.connect(self.onRemove)
        self.cmdOK.clicked.connect(self.onOK)
        self.cmdCancel.clicked.connect(self.close)

        # Signals from the list
        self.lstCategories.itemSelectionChanged.connect(self.onListSelection)

        # Signals from the radio buttons
        self.rbExisting.toggled.connect(self.onAddChoice)

    def onAddChoice(self):
        """
        Respond to the type selection for new category
        """
        isNew = self.rbNew.isChecked()
        self.cbCategories.setEnabled(not isNew)
        self.txtNewCategory.setEnabled(isNew)

    def onListSelection(self):
        """
        Respond to selection in the category list view
        """
        selected_items = self.selectedModels()
        self.cmdRemove.setEnabled(len(selected_items) > 0)

    def selectedModels(self):
        """
        Returns a list of selected models
        """
        selected_categories = []
        selectedRows = self.lstCategories.selectedItems()
        selected_categories = [str(row.text()) for row in selectedRows]

        return selected_categories

    def onAdd(self):
        """
        Add the chosen category to the list
        """
        if self.rbExisting.isChecked():
            new_category = self.cbCategories.currentText()
        else:
            new_category = self.txtNewCategory.text()
        # Display the current value as txt
        if new_category:
            self.lstCategories.addItem(new_category)

    def onRemove(self):
        """
        Remove selected categories in the list
        """
        selectedRows = self.lstCategories.selectedItems()
        if not selectedRows:
            return
        for row in selectedRows:
            self.lstCategories.takeItem(self.lstCategories.row(row))

    def onOK(self):
        """
        Accept the new categories for the model
        """
        # Read in the categories
        self.categories.modelToCategory()[self.model.name] = self.listCategories()
        self.accept()

    def listCategories(self):
        """
        Returns the list of categories from the QListWidget
        """
        return [str(self.lstCategories.item(i).text()) for i in range(self.lstCategories.count())]
