"""
A module to make the interface to the mmpref library more Pythonic.  It contains
a single class that can be created once and then used to access preference data.
This class also provides data type-free .get(), .set() and .getAllPreferences()
methods.
Simple script usage might look like this::
    import preferences
    # Initialize the preference handler
    pref_handler = preferences.Preferences(preferences.SCRIPTS)
    # Store and get all preferences from a group specific to this script
    pref_handler.beginGroup('my_script')
    # Set the preference 'hosts' to 5
    pref_handler.set('hosts', 5)
    # Get the value of the hosts preference
    num_hosts = pref_handler.get('hosts', default=8)
    # Get the value of the jobname preference, with myjob as the default
    # name if no preference is set
    jobname = pref_handler.get('jobname', default='myjob')
Slightly more complex script usage using groups might look like this::
    import preferences
    # Initialize the preference handler
    pref_handler = preferences.Preferences(preferences.SCRIPTS)
    # Store and get all preferences from a group specific to this script
    pref_handler.beginGroup('my_script')
    # Set the preference 'hosts' to 5
    pref_handler.set('hosts', 5)
    # Switch to the group 'my_script/dialogs'
    pref_handler.beginGroup('dialogs')
    # Set the value of the 'my_script/dialogs/import_directory preference
    pref_handler.set('import_directory', directory)
    # Reset the current group to the 'my_script' group
    pref_handler.resetGroup(retain=1)
    # Get the value of the hosts preference
    num_hosts = pref_handler.get('hosts', default=8)
    # Change the value of the 'my_script/dialogs/import_directory' preference
    pref_handler.set('dialogs/import_directory', directory2)
    # Switch to the group 'my_script/dialogs'
    pref_handler.beginGroup('dialogs')
    # Get the import directory
    imdir = pref_handler.get('import_directory')
"""
from contextlib import contextmanager
from typing import Union
import pymmlibs
from schrodinger.infra import mm
CANVAS = pymmlibs.CANVAS
COMBIGLIDE = pymmlibs.COMBIGLIDE
DESMOND = pymmlibs.DESMOND
EPIK = pymmlibs.EPIK
IMPACT = pymmlibs.IMPACT
JAGUAR = pymmlibs.JAGUAR
KNIME = pymmlibs.KNIME
MACROMODEL = pymmlibs.MACROMODEL
MAESTRO = pymmlibs.MAESTRO
PHASE = pymmlibs.PHASE
PSP = pymmlibs.PSP
PYMOL = pymmlibs.PYMOL
QIKPROP = pymmlibs.QIKPROP
SCRIPTS = pymmlibs.SCRIPTS
SHARED = pymmlibs.SHARED
WATERMAP = pymmlibs.WATERMAP
OK = pymmlibs.MMPREF_OK
NO_TYPE = pymmlibs.MMPREF_TYPE_NONE
INT = pymmlibs.MMPREF_TYPE_INT
STRING = pymmlibs.MMPREF_TYPE_STRING
FLOAT = pymmlibs.MMPREF_TYPE_DOUBLE
BOOL = pymmlibs.MMPREF_TYPE_BOOL
DATA_TYPES = [BOOL, INT, STRING, FLOAT]
ALL_TYPES = DATA_TYPES + [NO_TYPE]
# Create a unique marker that won't be used as a default value by a caller.
NODEFAULT = object()
TMPDIR_KEY = pymmlibs.MMPREF_TMPDIR_KEY
[docs]class Preferences:
    """
    A class that allows Pythonic access to the mmpref library.
    """
[docs]    def __init__(self, product):
        """
        Create a new Preferences object
        :type product: constant
        :param product: A product name constant supplied in this module or one
                        from the pymmlibs module.
        :raise ValueError: If product is not a recognized constant
        :raise IOError: If the preferences file is inaccessible.
        :raise IOError: If the preferences file is inaccessible.
        :raise RuntimeError: If another error is encountered.
        Constants supplied in this module:
        - CANVAS
        - COMBIGLIDE
        - DESMOND
        - EPIK
        - IMPACT
        - JAGUAR
        - KNIME
        - MACROMODEL
        - MAESTRO
        - PHASE
        - PSP
        - PYMOL
        - QIKPROP
        - SCRIPTS
        - SHARED
        - WATERMAP
        """
        self.product = product
        self.status, self.handle = pymmlibs.mmpref_new(product)
        self._check_pref_status() 
    def __del__(self, _hasattr=hasattr, _delete=pymmlibs.mmpref_delete):
        """
        Delete the handle and write the preferences to the preference file.
        """
        # attributes are saved at function level to protect against deletion
        # at interpreter exit
        # handle might be collected, or fail to initialize
        if _hasattr(self, 'handle') and self.handle is not None:
            _delete(self.handle)
[docs]    def sync(self):
        """
        Sync the settings to file and reload settings modified by other
        threads/process.
        """
        self.status = pymmlibs.mmpref_sync(self.handle)
        self._check_pref_status() 
[docs]    def clear(self):
        """
        This will clear all preferences associated with this handle.
        """
        pymmlibs.mmpref_clear(self.handle) 
[docs]    def getKeyType(self, key):
        """
        Gets the type of data stored for this key.
        :type key: str
        :param key: key or group to remove
        :rtype: constant
        :return: A constant indicating the data type stored for this key.
                 Valid constants are INT, STRING, FLOAT, BOOL.
        If the key is not found, None is returned.
        """
        for ptype in ALL_TYPES:
            # mmpref_remove returns the number of keys removed, so if it
            # evaluates to False, no key was removed
            if self.contains(key, key_type=ptype):
                return ptype
        return None 
[docs]    def remove(self, key, key_type=None):
        """
        Remove the key or group and its values.
        To remove a group and its associated keys, set type to NO_TYPE.
        If a group is set using `beginGroup`, the search will happen
        within the group.
        :type key: str
        :param key: key or group to remove
        :type key_type: constant
        :param key_type: A constant indicating the data type of this key or if
                         it is a group. If not given, an attempt will be made
                         to remove key if it is found as a key and if not it
                         will be treated as a group.  Valid constants are (INT,
                         STRING, FLOAT, BOOL, NO_TYPE (groups))
        :rtype: int
        :return: The number of keys removed
        :raise ValueError: If key_type is not a recognized type
        """
        total_removed = 0
        if key_type is None:
            key_type = self.getKeyType(key)
            if key_type is None:
                # No key found, treat as a group
                key_type = NO_TYPE
        if key_type in ALL_TYPES:
            total_removed = pymmlibs.mmpref_remove(self.handle, key, key_type)
        else:
            raise ValueError('Unknown value for key_type argument')
        return total_removed 
[docs]    def contains(self, key, key_type=None):
        """
        Check for the presence of key.
        Check if the given key is available matching the give datatype.
        If `beginGroup` is called before this function, the given key will
        be searched for relative to the group.
        Keys specific to the running platform and non-platform specific keys
        attribute will be considered in the search.
        :type key: str
        :param key: key to search for
        :type key_type: constant
        :param key_type: A constant indicating the data type of this key. If
                         not given, an attempt will be made to find a key of
                         any data type. Valid key_type are: (INT, STRING,
                         FLOAT, BOOL).
        :rtype: bool
        :return: True if the key is found, False if not
        :raise ValueError: If key_type is not a valid type
        """
        if key_type is None:
            for ptype in DATA_TYPES:
                if pymmlibs.mmpref_contains(self.handle, key, ptype) == OK:
                    return True
        elif key_type in DATA_TYPES:
            if pymmlibs.mmpref_contains(self.handle, key, key_type) == OK:
                return True
        else:
            raise ValueError('Unknown value for key_type argument')
        return False 
[docs]    def beginGroup(self, group, toplevel=False):
        """
        Indicate the group for future key searches by appending group to the
        current group path.  This group will be used for future group/key
        searches until `endGroup` is called.  This is useful to avoid typing
        the common path again and again to query sets of keys.
        A product's keys can be grouped together under various paths.  For
        instance, the SCRIPT product may have a PoseExplorer group, and that
        group may have Paths and Dialogs subgroups.  One could access the
        'import' key under the PoseExplorer/Paths group via::
            handler = Preferences(SCRIPTS)
            handler.beginGroup('PoseExplorer')
            handler.beginGroup('Paths')
            import_dir = handler.get('import')
        or::
            handler = PreferenceHandler(SCRIPTS)
            handler.beginGroup('PoseExplorer/Paths')
            import_dir = handler.get('import')
        or::
            handler = Preferences(SCRIPTS)
            import_dir = handler.get('PoseExplorer/Paths/import')
        :type group: str
        :param group: the group to append to the current group path
        :type toplevel: bool
        :param toplevel: If True, treat this group as a toplevel group - i.e.
            the group path will simply be 'group' after this.  If False (default),
            this group should be appended to the current group path.
        :raise ValueError:
                if key contain invalid character
        :raise MmException:
                otherwise
        """
        if toplevel:
            self.resetGroup()
        try:
            pymmlibs.mmpref_begin_group(self.handle, group)
        except mm.MmException as e:
            if e.rc == mm.MMPREF_KEY_INVALID_CHAR:
                raise ValueError(e)
            raise 
[docs]    def getGroup(self):
        """
        Get the current group set via the `beginGroup` method.
        :rtype: str
        :return: The current group, or "" if no group is set
        """
        return pymmlibs.mmpref_get_group(self.handle) 
[docs]    def endGroup(self):
        """
        Remove the current group set via the `beginGroup` method from the
        group search path.  The new group path will be the same as it was before
        the last invocation of `beginGroup`.
        """
        return pymmlibs.mmpref_end_group(self.handle) 
[docs]    @contextmanager
    def changeGroup(self, group, toplevel=False):
        """
        Indicate the group to use for key searches by appending group to the
        current group path.
        :param str group: the group to append to the current group path
        :param bool toplevel: If True, treat this group as a toplevel group - i.e.
            the group path will simply be 'group' after this.  If False (default),
            this group should be appended to the current group path.
        """
        self.beginGroup(group, toplevel=toplevel)
        # Preference is yielded with the applied group path here
        yield
        self.endGroup() 
[docs]    def resetGroup(self, retain=0):
        """
        Unset the group path so that searches begin at the top level for the
        product.
        :type retain: int
        :param retain: The number of groups in the path to retain below the top
            level.  For instance, if the current group is a/b/c/d,
            self.resetGroup(retain=1) would set the current group to a.
        """
        current_group = self.getGroup()
        retain = current_group.split('/')[:retain]
        while self.getGroup():
            self.endGroup()
        for group in retain:
            self.beginGroup(group) 
[docs]    def set(self,
            key: str,
            value: Union[int, bool, float, str],
            os_specific: bool = False):
        """
        Set the value of key.  Note that the underlying library stores keys in a
        type specific way, but this function and the `get` function abstract
        that away.
        If key does not exist, it is created.
        If the group path has been set via `beginGroup`, it is honored.
        :param key: The key to set the value for. Can be a key name or a group
                    path/key name.
        :param value: The value to set key to
        :param os_specific: Adds the key with os specific attribute if set to True.
                            This ties the key to the platform it is invoked.
        :raise ValueError: if key contain invalid character
        :raise TypeError: if type of existing key is being changed
        :raise MmException: otherwise
        """
        try:
            if os_specific:
                mm.mmpref_begin_os_attribute(self.handle)
            if isinstance(value, str):
                mm.mmpref_set_string(self.handle, key, value)
            elif isinstance(value, bool):
                mm.mmpref_set_bool(self.handle, key, value)
            elif isinstance(value, int):
                mm.mmpref_set_int(self.handle, key, value)
            elif isinstance(value, float):
                mm.mmpref_set_double(self.handle, key, value)
            else:
                msg = (
                    'Key value %s is a %s, but may only be a str, bool, int, '
                    'or float' % (value, type(value)))
                raise TypeError(msg)
        except mm.MmException as e:
            if e.rc == mm.MMPREF_KEY_INVALID_CHAR:
                raise ValueError(e)
            elif e.rc == mm.MMPREF_ERR:
                raise TypeError(e)
            else:
                raise
        finally:
            mm.mmpref_end_os_attribute(self.handle) 
[docs]    def get(self, key, default=NODEFAULT):
        """
        Get the value of key.  Note that the underlying library stores keys in a
        type specific way, but this function and the `set` function abstract
        that away.
        If key does not exist, the value of the default argument is returned if
        it is given.  If the key does not exist and default is not given, a
        KeyError is raised.
        If the group path has been set via `beginGroup`, it is honored.
        If the OS has been set via `beginOS`, it is honored.
        OS-specific key values get higher preference when searching for keys.
        The user-specific preference file is checked for the key first, then the
        default installation files are checked.
        :type key: str
        :param key: The key to get the value for.  Can be a key name or a group
            path/key name.
        :raise KeyError: If the key is not found and default is not given
        """
        status, value = pymmlibs.mmpref_get_string(self.handle, key)
        if status == OK:
            return value
        status, value = pymmlibs.mmpref_get_bool(self.handle, key)
        if status == OK:
            return value
        status, value = pymmlibs.mmpref_get_int(self.handle, key)
        if status == OK:
            return value
        status, value = pymmlibs.mmpref_get_double(self.handle, key)
        if status == OK:
            return value
        # Key was never found
        if default is NODEFAULT:
            raise KeyError('Key not found and no default specified')
        else:
            return default 
[docs]    def getAllPreferences(self, group=""):
        """
        Get a dictionary of all preferences in the current group path.  The
        dictionary keys will be preference names and the dictionary values will
        be preference values.  Note that preference names will include the
        relative group path starting at the current group.
        If the OS has been set via `beginOS`, it is honored.
        OS-specific key values get higher preference when searching for keys.
        :type group: str
        :param group: Append this group path to the current group path before
            searching for keys.  Note that this persists during this operation only,
            after returning the dictionary the group path returns to its previous
            value.
        :rtype: dict
        :return: dictionary of preferences with preference names as keys and
            preference values as values.
        """
        pref_dict = {}
        # Cycle through all the various types and merge them together
        status, type_dict = pymmlibs.mmpref_get_string_list(self.handle, group)
        pref_dict.update(type_dict)
        status, type_dict = pymmlibs.mmpref_get_bool_list(self.handle, group)
        pref_dict.update(type_dict)
        status, type_dict = pymmlibs.mmpref_get_int_list(self.handle, group)
        pref_dict.update(type_dict)
        status, type_dict = pymmlibs.mmpref_get_double_list(self.handle, group)
        pref_dict.update(type_dict)
        return pref_dict 
    def _check_pref_status(self):
        """
        Raise appropriate exception based on status.
        """
        if self.status == pymmlibs.MMPREF_UNRECOGNIZED_PRODUCT:
            raise ValueError('Unrecognized product "%s"' % self.product)
        elif self.status == pymmlibs.MMPREF_FORMAT_ERROR:
            raise SyntaxError('Format error in using settings file.')
        elif self.status == pymmlibs.MMPREF_ACCESS_ERROR:
            raise OSError("Access error in using settings file. This might be "
                          "due to permissions or a full disk.")
        elif self.status != OK:
            raise RuntimeError('Unknown error: %s' % self.status) 
[docs]def get_preference(key, group, product=SCRIPTS, default=NODEFAULT):
    """
    Get the value of key in the specified group under product. See documentation for
    `Preferences.get` for more information.
    :param str key: The key to get the value for. Can be a key name or a group
        path/key name.
    :param str group: the group path to append to the product
    :param constant product: A product name constant supplied in this module or one
        from the pymmlibs module.
    :type default: any
    :param default: The default value to return if the key does not exist.
    :rtype: bool, int, float, str, or default
    :return: The value for the key.
    :raise KeyError: If the key is not found and default is not given
    """
    handler = Preferences(product)
    handler.beginGroup(group)
    value = handler.get(key, default=default)
    handler.endGroup()
    return value 
[docs]def set_preference(key, group, value, product=SCRIPTS):
    """
    Set the value of key in the specified group under product. See documentation
    for `Preferences.set` for more information.
    :param str key: The key to set the value for. Can be a key name or a group
        path/key name.
    :param str group: the group path to append to the product
    :type value: bool, int, float, or str
    :param value: The value to set key to
    :param constant product: A product name constant supplied in this module or one
        from the pymmlibs module.
    """
    handler = Preferences(product)
    handler.beginGroup(group)
    handler.set(key, value)
    handler.endGroup()