# 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 common.utils import utils

__all__ = ['ParamsCollection', 'ParameterInfo', 'ParameterHeader']

# *****************************************************************************
class ParameterInfo:
    """Description of a parameter to be stored in an INI file and possibly
    edited via a dialog box.

    Attributes:
      - 'groupName': INI file group name for the parameter (an ASCII string).
      - 'dialogLabel': label to denote the parameter in a settings dialog
        box, or 'None' if there should be no label for this parameter.
      - 'attributeName': name of the attribute of a parameters collection
        class instance to store the data in. This same string will be used as
        the parameter's name in the INI file.
      - 'dataType': Python datatype of the attribute that will hold the data:
        'int', 'float', 'bool', 'str' or 'unicode'.
      - 'defaultValue': parameter's default value (may be 'None').
      - 'admissibleValues': a complete sequence of allowable parameter values
        or 'None' if the set of values is not fixed.
      - 'minValue', 'maxValue': minimum and maximum values for numeric data
        types or 'None' for boundaries that are not defined.
      - 'isMinStrict', 'isMaxStrict': 'True' if the respective boundary values
        themselves are not acceptable.
      - 'minLength', 'maxLength': minimum and maximum lengths for 'str' and
        'unicode' data types or 'None' if string length is not restricted."""

    def __init__(self, groupName, dialogLabel, attributeName,
        dataType, defaultValue, admissibleValues = None,
        minValue = None, maxValue = None,
        isMinStrict = False, isMaxStrict = False,
        minLength = None, maxLength = None):

        # Empty group names (used to indicate the root group in QSettings) are
        # not allowed.
        assert isinstance(groupName, str) and len(groupName) > 0
        assert isinstance(attributeName, str) and len(attributeName) > 0
        assert defaultValue is None or isinstance(defaultValue, dataType)
        assert admissibleValues is None or all(isinstance(item, dataType)
            for item in admissibleValues)
        assert minValue is None or maxValue is None or minValue < maxValue
        assert minValue is None or isinstance(minValue, dataType)
        assert maxValue is None or isinstance(maxValue, dataType)
        assert minLength is None or minLength > 0
        assert maxLength is None or maxLength > 0
        assert minLength is None or maxLength is None or minLength <= maxLength

        self.groupName = groupName
        self.dialogLabel = dialogLabel
        self.attributeName = attributeName
        self.dataType = dataType
        self.defaultValue = defaultValue
        self.admissibleValues = admissibleValues
        self.minValue = minValue
        self.maxValue = maxValue
        self.isMinStrict = isMinStrict
        self.isMaxStrict = isMaxStrict
        self.minLength = minLength
        self.maxLength = maxLength

    def saveToSettings(self, settings, value):
        """Save a parameter specified by 'value' in the given 'QSettings'
        instance in an appropriate way as specified by the 'self' instance."""

        with utils.SettingsGrouper(settings, self.groupName):
            settings.setValue(self.attributeName, value)

    def loadFromSettings(self, settings):
        """Load a parameter from the given 'QSettings' instance and return its
        value converted to the required data type.

        If the value is missing from the 'QSettings' instance or does not meet
        the constraints, return 'self.defaultValue'."""

        with utils.SettingsGrouper(settings, self.groupName):

            # Return the default value if the required data is missing.
            if not settings.contains(self.attributeName):
                return self.defaultValue

            variantValue = settings.value(self.attributeName)

        # Convert the data to its Python type.
        if self.dataType is float:
            (value, conversionResult) = variantValue.toDouble()
        elif self.dataType is int:
            (value, conversionResult) = variantValue.toInt()
        elif self.dataType is bool:
            value = variantValue.toBool()
            conversionResult = True
        elif self.dataType is unicode:
            value = unicode(variantValue.toString())
            conversionResult = True
        elif self.dataType is str:
            try:
                value = str(variantValue.toString())
            except UnicodeEncodeError:
                conversionResult = False
            else:
                conversionResult = True
        else:
            assert False, 'The data type is not supported'

        # Check the value against the additional constraints.
        if conversionResult:
            if (self.admissibleValues is not None and
                value not in self.admissibleValues):
                conversionResult = False

            if self.minValue is not None:
                if (value < self.minValue or
                    self.isMinStrict and value == self.minValue):
                    conversionResult = False
            if self.maxValue is not None:
                if (value > self.maxValue or
                    self.isMaxStrict and value == self.maxValue):
                    conversionResult = False

            if self.minLength is not None and len(value) < self.minLength:
                conversionResult = False
            if self.maxLength is not None and len(value) > self.maxLength:
                conversionResult = False

        # Return the default value if the data is invalid or does not meet the
        # constraints.
        if conversionResult:
            return value
        else:
            return self.defaultValue

# *****************************************************************************
class ParameterHeader:
    """Description of a settings dialog box header to be used to denote a
    subgroup of parameters within a group of the given name.

    Attributes:
      - 'groupName': INI file group name (an ASCII string).
      - 'dialogLabel': label to denote a subgroup of parameters in a settings
        dialog box."""

    def __init__(self, groupName, dialogLabel):

        # Empty group names (used to indicate the root group in QSettings) are
        # not allowed.
        assert isinstance(groupName, str) and len(groupName) > 0
        assert dialogLabel is not None

        self.groupName = groupName
        self.dialogLabel = dialogLabel

# *****************************************************************************
class ParamsCollection:
    """Base class for parameter collections to be stored in text INI files and
    possibly edited via a dialog box.

    Redefine 'getParameterList' virtual method to specify the list of the
    parameters along with constraints that may apply. Specify the name of the
    INI file to hold the data by redefining 'getSettingsFilePath', and then use
    'saveToFile' and 'loadFromFile' methods for persistent data storage.

    Use ordinary attributes of the derived class to access the data."""

    # ---- Public overridable methods -----------------------------------------
    @classmethod
    def getParameterList(cls):
        """Return an ordered list of 'ParameterInfo' instances (possibly
        intervened by a number of 'ParameterHeader's) describing properties of
        the parameters (to be stored as attributes in instances of the derived
        class), as well as their appearance and order in a settings dialog box.

        In a dialog box, as well as in an INI file, parameters will be grouped
        according to 'groupName's specified in 'ParameterInfo's and
        'ParameterHeader's (so that parameters with a common 'groupName' are
        displayed side by side). Within a group, parameter order is determined
        by the order of elements in the list returned by this function. In the
        dialog box, each parameter may be denoted with a separate label, as
        specified by 'ParameterInfo.dialogLabel'. By means of introducing
        intervening 'ParameterHeader' instances into the list, it's also
        possible to insert informative labels between consecutive parameter
        fields in the dialog box. Otherwise, 'ParameterHeader' elements of the
        list will be ignored.

        Use 'ParamsDialog' to interactively edit attributes of the derived
        class in a dialog box."""

        return []

    @classmethod
    def getSettingsFilePath(cls):
        """Return INI file path to be used by 'saveToFile' and
        'loadFromFile'."""

        return None

    # ---- Public methods -----------------------------------------------------
    def __init__(self):
        """Construct an all-default params instance."""

        self.reset()

    def reset(self):
        """Set the parameters to their default values."""

        for param in self.getParameterList():
            if isinstance(param, ParameterInfo):
                setattr(self, param.attributeName, param.defaultValue)

    def copy(self):
        """Return a new instance of this class holding the same values."""

        # Create an all-default instance.
        copy = self.__class__()
        # Copy all the attribute values from 'self'.
        for attributeName in self.getAttributeNames():
            setattr(copy, attributeName, getattr(self, attributeName))
        return copy

    @classmethod
    def getAttributeNames(cls, groupName = None):
        """Return the list of class instance attribute names that belong to the
        specified INI file group, or the complete list of class instance
        attributes if 'groupName' is 'None'.

        Order of the returned attribute names will correspond to the required
        parameter sequence in a dialog box (as well as in an INI file)."""

        attributeNames = []

        for param in cls.getParameterList():
            if isinstance(param, ParameterInfo):
                if groupName is None or param.groupName == groupName:
                    attributeNames.append(param.attributeName)

        return attributeNames

    @classmethod
    def getParameterInfoByName(cls, attributeName):
        """Return 'ParameterInfo' instance that corresponds to the given class
        instance attribute."""

        for param in cls.getParameterList():
            if isinstance(param, ParameterInfo):
                if param.attributeName == attributeName:
                    return param

    def saveToFile(self):
        """Save the current settings in a text file located at the path defined
        by 'getSettingsFilePath'."""

        settings = QSettings(self.getSettingsFilePath(), QSettings.IniFormat)

        # Save the values in the order defined by 'parameterList'.
        for param in self.getParameterList():
            if isinstance(param, ParameterInfo):

                # Don't provide a default value to 'getattr', as all the values
                # have to be initialized in the constructor.
                paramValue = getattr(self, param.attributeName)
                param.saveToSettings(settings, paramValue)

    @classmethod
    def loadFromFile(cls):
        """Return params instance loaded from a text file located at the path
        defined by 'getSettingsFilePath'.

        Invalid or absent values, if any, are replaced with defaults."""

        settings = QSettings(cls.getSettingsFilePath(), QSettings.IniFormat)

        # Construct an all-default class instance.
        params = cls()

        for param in cls.getParameterList():
            if isinstance(param, ParameterInfo):

                value = param.loadFromSettings(settings)
                setattr(params, param.attributeName, value)

        return params
