# Copyright (c) 2011-2014, B.I.Stepanov Institute of Physics, National Academy
# of Sciences of Belarus.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

from PyQt4.QtCore import *
from PyQt4.QtGui import *

from common.utils import gui
from common.utils import txt

from common.EditWidgets import *
from common.ParamsCollection import *

__all__ = ['ParamsDialog', 'ParameterGroupInfo']

# *****************************************************************************
class ParameterGroupInfo:
    """Description of appearance of a group of parameters of a
    'ParamsCollection' instance in a settings dialog box.

    Attributes:
      - 'groupName': INI file group name (the same value as that specified in
        the respective 'ParameterInfo' instances describing the parameters).
      - 'groupBoxCaption': caption of the group box to hold the parameter edit
        widgets in a dialog box.
      - 'startsNewColumn': 'True' if this group box should be placed in a new
        column with respect to the previous one. Group boxes (corresponding to
        the INI file parameters groups) are arranged into columns in settings
        dialog boxes."""

    def __init__(self, groupName, groupBoxCaption, startsNewColumn = False):

        self.groupName = groupName
        self.groupBoxCaption = groupBoxCaption
        self.startsNewColumn = startsNewColumn

# *****************************************************************************
class ParamsDialog(QDialog):
    """Base class for dialogs intended for interactive modification of
    'ParamsCollection' instances.

    Redefine 'getGroupInfoList' method to specify which INI file groups should
    be represented in the dialog. Redefine 'updateCustomErrorMessages' to
    implement any constraint checking procedures that involve several parameter
    values at once (and hence may not be implemented at the level of a single
    editing widget). Redefine 'updateEditEnableStatus' to restrict availability
    of certain edit controls under certain circumstances. Redefine
    'checkSavePossibility' to perform some additional checkings when the 'Save'
    button is pressed, if required.

    Use 'getValue' to access current values of the parameters being edited, and
    'setErrorMessage' to inform the user of problems with these additional
    constraints. Use 'setEnabled' to dynamically alter availability to the user
    of edit controls associated with certain parameters.

    Pass an instance of a class derived from 'ParamsCollection' to the
    constructor, then run the dialog with 'exec_'. When (and only if) the user
    presses the 'Save' button (and 'checkSavePossibility' returns 'True'), the
    passed parameters instance is modified, and its 'saveToFile' method is
    invoked."""

    # ---- Public overridable methods -----------------------------------------
    def __init__(self, parent, params):
        QDialog.__init__(self, parent)

        # ---- Members ----
        assert isinstance(params, ParamsCollection)
        self.params = params

        # This will map attribute names of the parameters edited by the dialog
        # to the respective 'EditWidget' instances.
        self.editWidgets = {}

        # ---- Main layout ----
        layout = QHBoxLayout()

        # Current (the rightmost) column of the dialog's layout.
        lastColumnLayout = QVBoxLayout()

        for group in self.getGroupInfoList():

            if group.startsNewColumn:

                # Finish the current parameter column.
                lastColumnLayout.addStretch()
                # Set stretch factor to 1 to prevent column width changes
                # induced by warning icons in 'EditWidget's.
                layout.addLayout(lastColumnLayout, 1)

                lastColumnLayout = QVBoxLayout()

            lastColumnLayout.addWidget(self.createParameterGroupBox(
                group.groupName, group.groupBoxCaption))

        # Append the button box to the rightmost column of the dialog's layout.
        lastColumnLayout.addStretch()

        # ---- Button box ----
        self.saveButton = QPushButton(self.getSaveButtonName())
        self.saveButton.setAutoDefault(False)
        self.saveButton.clicked.connect(self.onSaveButtonClicked)

        self.cancelButton = QPushButton('Cancel')
        self.cancelButton.setAutoDefault(False)
        self.cancelButton.clicked.connect(self.onCancelButtonClicked)

        buttonBoxLayout = QHBoxLayout()
        buttonBoxLayout.addStretch()
        buttonBoxLayout.addWidget(self.saveButton)
        buttonBoxLayout.addWidget(self.cancelButton)

        lastColumnLayout.addLayout(buttonBoxLayout)

        # Set stretch factor to 1 to prevent column width changes induced by
        # warning icons in 'EditWidget's.
        layout.addLayout(lastColumnLayout, 1)
        self.setLayout(layout)

        self.setWindowTitle(self.getWindowTitle())
        self.setWindowIcon(gui.loadIcon('parameters'))

        self.setWindowFlags(
            self.windowFlags() & ~Qt.WindowContextHelpButtonHint)

        # ---- Initialization ----
        # Set custom error messages and update enabled state of edit controls
        # and the save button.
        self.onValueChanged()

    # ---- Protected overridable methods --------------------------------------
    @classmethod
    def getGroupInfoList(cls):
        """Return a list of 'ParameterGroupInfo' instances enumerating
        parameter groups that have to be present in the dialog box, along with
        the corresponding group box captions."""

        return []

    @classmethod
    def getWindowTitle(cls):
        """Return a string to be used as the dialog's title."""
        return 'Parameters'

    @classmethod
    def getSaveButtonName(cls):
        """Return a string to be displayed on the save button."""
        return '&Save'

    def updateCustomErrorMessages(self):
        return

    def updateEditEnableStatus(self):
        return

    def checkSavePossibility(self):
        return True

    # ---- Protected methods --------------------------------------------------
    def getValue(self, attributeName):
        """Return the current value (as displayed in the corresponding edit
        widget) for a parameter with the given attribute name or 'None' if the
        value does not meet some of the constraints."""

        return self.editWidgets[attributeName].getValue()

    def setErrorMessage(self, attributeName, message):
        """Set or clear custom error message for a parameter with the given
        attribute name.

        'message' has to be a string to set the error message or 'None' to
        clear it."""

        if message is not None:
            self.editWidgets[attributeName].setCustomErrorMessage(message)
        else:
            self.editWidgets[attributeName].clearCustomErrorMessage()

    def setEnabled(self, attributeName, isEnabled):
        """Enable or disable interactive editing for a parameter with the given
        attribute name."""

        self.editWidgets[attributeName].setEnabled(isEnabled)

    # ---- Private methods ----------------------------------------------------
    def createParameterGroupBox(self, groupName, groupBoxCaption):
        """Create and return a group box widget holding edit fields for a
        parameter group of the given name. Initialize edit fields with values
        stored in 'self.params', connect all the required slots associated with
        them and save references to the created edit widgets in
        'self.editWidgets'."""

        groupBox = QGroupBox(groupBoxCaption)
        groupBoxLayout = QGridLayout()

        for param in self.params.getParameterList():

            if param.groupName != groupName:
                continue

            currentRow = groupBoxLayout.rowCount()

            # A subgroup header will occupy the whole line of the group box.
            if isinstance(param, ParameterHeader):
                label = QLabel(param.dialogLabel)
                label.setTextFormat(Qt.RichText)
                groupBoxLayout.addWidget(label, currentRow, 0, 1, 2)

            # Otherwise, an edit widget is placed on the right and optionally
            # a descriptive label on the left.
            elif isinstance(param, ParameterInfo):

                editWidget = self.createEditWidget(param)

                # If 'dataType' is 'bool', the descriptive text is incorporated
                # into the 'editWidget' checkbox, so the label is superfluous.
                if (param.dialogLabel is not None and
                    param.dataType is not bool):

                    label = QLabel(param.dialogLabel)
                    label.setTextFormat(Qt.RichText)
                    label.setAlignment(Qt.AlignRight)
                    groupBoxLayout.addWidget(label, currentRow, 0)
                    groupBoxLayout.addWidget(editWidget, currentRow, 1)
                else:
                    groupBoxLayout.addWidget(editWidget, currentRow, 0, 1, 2)

                # Disconnect the 'onValueChanged' slot to prevent state updates
                # for the not yet existent 'Save' button as well as superfluous
                # constraint checkings with 'updateCustomErrorMessage'.
                with gui.SignalBlocker(editWidget):
                    editWidget.setValue(
                        getattr(self.params, param.attributeName))

                # Place a reference to the edit control in 'self.editWidgets'.
                self.editWidgets[param.attributeName] = editWidget

            else:
                assert False

        groupBox.setLayout(groupBoxLayout)
        return groupBox

    def createEditWidget(self, paramInfo):
        """Create and return and 'EditWidget' instance suitable for editing of
        a parameter described with the given 'ParameterInfo' instance."""

        if paramInfo.admissibleValues is not None:
            # 'int' and 'float' data types are currently not supported for
            # parameters with fixed sets of values.
            assert paramInfo.dataType is str or paramInfo.dataType is unicode

            # If the set of possible values for a parameter is fixed, then its
            # value is selected using a combo box.
            editWidget = ComboBoxEditWidget(paramInfo.admissibleValues)

        elif paramInfo.dataType is float or paramInfo.dataType is int:
            # Initialize the edit widget with minimal and maximal values
            # defined in the 'ParameterInfo' instance.
            editWidget = NumberEditWidget(
                paramInfo.minValue, paramInfo.maxValue,
                paramInfo.isMinStrict, paramInfo.isMaxStrict,
                isFloatingPoint = paramInfo.dataType is float)

        elif paramInfo.dataType is bool:
            # Dialog label is incorporated into the checkbox in this case.
            editWidget = BoolEditWidget(paramInfo.dialogLabel)

        else:
            # 'unicode' strings are currently not supported.
            assert paramInfo.dataType is str
            editWidget = StringEditWidget(
                paramInfo.minLength, paramInfo.maxLength)

        editWidget.valueChanged.connect(self.onValueChanged)
        editWidget.returnPressed.connect(self.onReturnPressed)

        return editWidget

    # ---- Private slots ------------------------------------------------------
    def onReturnPressed(self):
        """Pass focus to the edit control below the current one in the current
        parameter group, looping to the group's first parameter in the
        bottom."""

        # Find the parameter that's currently being edited.
        for attributeName in self.editWidgets:
            if self.editWidgets[attributeName].getEditWidget().hasFocus():
                curParamName = attributeName
                break

        groupName = self.params.getParameterInfoByName(curParamName).groupName

        # List of the group's attributes, in the order of their appearance in
        # the dialog.
        groupParamNames = self.params.getAttributeNames(groupName)

        for i in range(len(groupParamNames) - 1):
            if groupParamNames[i] == curParamName:
                nextParamName = groupParamNames[i + 1]
                break
        else:
            nextParamName = groupParamNames[0]

        self.editWidgets[nextParamName].getEditWidget().setFocus()

    def onValueChanged(self):
        """Update button state and custom error messages."""

        # Clear any previous custom error messages.
        for attributeName in self.editWidgets:
            self.setErrorMessage(attributeName, None)

        self.updateCustomErrorMessages()
        self.updateEditEnableStatus()

        # Deny saving of the malformed data.
        self.saveButton.setEnabled(all(
            self.editWidgets[attributeName].hasValue()
            for attributeName in self.editWidgets))

    def onSaveButtonClicked(self):
        """Save changes to the INI file, update the 'self.params' instance and
        close the dialog box, unless 'self.checkSavePossibility' returns
        'False'."""

        if not self.checkSavePossibility():
            return

        # Modify the instance being edited and save the changes to disk.
        for attributeName in self.editWidgets:
            setattr(self.params, attributeName,
                self.editWidgets[attributeName].getValue())

        self.params.saveToFile()
        self.accept()

    def onCancelButtonClicked(self):
        self.reject()
