#!/usr/bin/python3
# -*- coding: utf-8 -*-
##===-----------------------------------------------------------------------------*- Python -*-===##
##
##                                   S E R I A L B O X
##
## This file is distributed under terms of BSD license.
## See LICENSE.txt for more information.
##
##===------------------------------------------------------------------------------------------===##
##
## This file contains the savepoint implementation of the Python Interface.
##
##===------------------------------------------------------------------------------------------===##
from abc import ABCMeta
from ctypes import c_char_p, c_void_p, c_int, Structure, POINTER, c_size_t
from .common import get_library, to_c_string
from .error import invoke, SerialboxError
from .metainfomap import MetainfoMap, MetainfoImpl
from .type import StringTypes
from .util import levenshtein
lib = get_library()
class SavepointImpl(Structure):
    """ Mapping of serialboxSavepoint_t """
    _fields_ = [("impl", c_void_p), ("ownsData", c_int)]
def register_library(library):
    library.serialboxSavepointCreate.argtypes = [c_char_p]
    library.serialboxSavepointCreate.restype = POINTER(SavepointImpl)
    library.serialboxSavepointCreateFromSavepoint.argtypes = [POINTER(SavepointImpl)]
    library.serialboxSavepointCreateFromSavepoint.restype = POINTER(SavepointImpl)
    library.serialboxSavepointDestroy.argtypes = [POINTER(SavepointImpl)]
    library.serialboxSavepointDestroy.restype = None
    library.serialboxSavepointGetName.argtypes = [POINTER(SavepointImpl)]
    library.serialboxSavepointGetName.restype = c_char_p
    library.serialboxSavepointEqual.argtypes = [POINTER(SavepointImpl), POINTER(SavepointImpl)]
    library.serialboxSavepointEqual.restype = c_int
    library.serialboxSavepointToString.argtypes = [POINTER(SavepointImpl)]
    library.serialboxSavepointToString.restype = c_char_p
    library.serialboxSavepointHash.argtypes = [POINTER(SavepointImpl)]
    library.serialboxSavepointHash.restype = c_size_t
    library.serialboxSavepointGetMetainfo.argtypes = [POINTER(SavepointImpl)]
    library.serialboxSavepointGetMetainfo.restype = POINTER(MetainfoImpl)
# ===--------------------------------------------------------------------------------------------===
#   Savepoint
# ==---------------------------------------------------------------------------------------------===
[docs]class Savepoint(object):
    """Savepoints are used within the :class:`Serializer <serialbox.Serializer>` to discriminate
    fields at different points in time. Savepoints in the :class:`Serializer <serialbox.Serializer>`
    are unique and primarily identified by their :attr:`name <serialbox.Savepoint.name>`
        >>> savepoint = Savepoint('savepoint')
        >>> savepoint.name
        'savepoint'
        >>>
    and further distinguished by their :attr:`metainfo <serialbox.Savepoint.metainfo>`
        >>> savepoint = Savepoint('savepoint', {'key': 5})
        >>> savepoint.metainfo
        <MetainfoMap {"key": 5}>
        >>>
    """
[docs]    def __init__(self, name, metainfo=None, impl=None):
        """Initialize the Savepoint.
        This method prepares the Savepoint for usage and gives a name, which is the only required
        information for the savepoint to be usable. Meta-information can be added after the
        initialization has been performed.
        :param str name: Name of the savepoint
        :param dict metainfo: {Key:value} pair dictionary used for initializing the meta-information
                              of the Savepont
        :param SavepointImpl impl: Directly set the implementation pointer [internal use]
        :raises serialbox.SerialboxError: if Savepoint could not be initialized
        """
        if impl:
            self.__savepoint = impl
        else:
            namestr = to_c_string(name)[0]
            self.__savepoint = invoke(lib.serialboxSavepointCreate, namestr)
        if metainfo:
            if isinstance(metainfo, MetainfoMap):
                metainfo = metainfo.to_dict()
            metainfomap = self.metainfo
            for key, value in metainfo.items():
                metainfomap.insert(key, value) 
    @property
    def name(self):
        """Name of the Savepoint.
            >>> s = Savepoint('savepoint')
            >>> s.name
            'savepoint'
            >>>
        :return str: Name of the savepoint
        :rtype: str
        """
        return invoke(lib.serialboxSavepointGetName, self.__savepoint).decode()
    @property
    def metainfo(self):
        """Refrence to the meta-information of the Savepoint.
            >>> s = Savepoint('savepoint', {'key': 5})
            >>> s.metainfo['key']
            5
            >>> type(s.metainfo)
            <class 'serialbox.metainfomap.MetainfoMap'>
            >>> s.metainfo.insert('key2', 'str')
            >>> s
            <MetainfoMap {"key": 5, "key2": str}>
            >>>
        :return: Refrence to the meta-information map
        :rtype: :class:`MetainfoMap <serialbox.MetainfoMap>`
        """
        return MetainfoMap(impl=invoke(lib.serialboxSavepointGetMetainfo, self.__savepoint))
[docs]    def clone(self):
        """Clone the Savepoint by performing a deepcopy.
            >>> s = Savepoint('savepoint', {'key': 5})
            >>> s_clone = s.clone()
            >>> s.metainfo.clear()
            >>> s_clone
            <Savepoint sp {"key": 5}>
            >>>
        :return: Clone of the savepoint
        :rtype: Savepoint
        """
        return Savepoint('',
                         impl=invoke(lib.serialboxSavepointCreateFromSavepoint, self.__savepoint)) 
[docs]    def __eq__(self, other):
        """Test for equality.
        Savepoints compare equal if their :attr:`names <serialbox.Savepoint.name>` and
        :attr:`metainfos <serialbox.Savepoint.metainfo>` compare equal.
            >>> s1 = Savepoint('savepoint', {'key': 'str'})
            >>> s2 = Savepoint('savepoint', {'key': 5})
            >>> s1 == s2
            False
            >>>
        :return: `True` if self == other, `False` otherwise
        :rtype: bool
        """
        return bool(invoke(lib.serialboxSavepointEqual, self.__savepoint, other.__savepoint)) 
[docs]    def __ne__(self, other):
        """Test for inequality.
        Savepoints compare equal if their :attr:`names <serialbox.Savepoint.name>` and
        :attr:`metainfos <serialbox.Savepoint.metainfo>` compare equal.
            >>> s1 = Savepoint('savepoint', {'key': 'str'})
            >>> s2 = Savepoint('savepoint', {'key': 5})
            >>> s1 != s2
            True
            >>>
        :return: `True` if self != other, `False` otherwise
        :rtype: bool
        """
        return not self.__eq__(other) 
    def impl(self):
        return self.__savepoint
    def __del__(self):
        invoke(lib.serialboxSavepointDestroy, self.__savepoint)
    def __repr__(self):
        return '<Savepoint {0}>'.format(self.__str__())
    def __str__(self):
        return invoke(lib.serialboxSavepointToString, self.__savepoint).decode()
    def __hash__(self):
        return invoke(lib.serialboxSavepointHash, self.__savepoint) 
# ===--------------------------------------------------------------------------------------------===
#   SavepointCollection
# ==---------------------------------------------------------------------------------------------===
[docs]class SavepointCollection(object, metaclass=ABCMeta):
    """Collection of savepoints. A collection can be obtained by using the
    :attr:`savepoint <serialbox.Serializer.savepoint>` attribute of the
    :class:`Serializer <serialbox.Serializer>`.
        >>> ser = Serializer(OpenModeKind.Write, '.', 'field')
        >>> ser.register_savepoint(Savepoint('s1'))
        >>> ser.register_savepoint(Savepoint('s2'))
        >>> isinstance(ser.savepoint, SavepointCollection)
        True
        >>> ser.savpoint.savepoints()
        [<Savepoint s1 {}>, <Savepoint s2 {}>]
        >>> ser.savepoint.as_savepoint()
        Traceback (most recent call last):
          File "<stdin>", line 1, in ?
          File "savepoint.py", line 227, in as_savepoint
            raise SerialboxError(errstr)
        serialbox.error.SerialboxError: Savepoint is ambiguous. Candidates are:
          s1 {}
          s2 {}
        >>>
    """
[docs]    def savepoints(self):
        """ Get the list of savepoints in this collection. The savepoints are ordered in the way
        they were inserted.
        :return: List of savepoints in the collection.
        :rtype: :class:`list` [:class:`Savepoint <serialbox.Savepoint>`]
        """
        raise NotImplementedError() 
[docs]    def as_savepoint(self):
        """ Return the unique savepoint in the list or raise an
        :class:`SerialboxError <serialbox.SerialboxError>` if the list has more than 1 element.
        :return: Unique savepoint in this collection.
        :rtype: Savepoint
        :raises serialbox.SerialboxError: if list has more than one Savepoint
        """
        num_savepoints = len(self.savepoints())
        if num_savepoints == 1:
            return self.savepoints()[0]
        if num_savepoints > 1:
            errstr = "Savepoint is ambiguous. Candidates are:\n"
            for sp in self.savepoints():
                errstr += "  {0}\n".format(str(sp))
            raise SerialboxError(errstr)
        else:
            raise SerialboxError("SavepointCollection is empty") 
    def __str__(self):
        s = "["
        for sp in self.savepoints():
            s += sp.__str__() + ", "
        return s[:-2] + "]"
    def __repr__(self):
        return '<SavepointCollection {0}>'.format(self.__str__()) 
def transformed_equal(name, key):
    """ Return True if ``name`` can be mapped to the transformed ``key`` such that ``key`` is a valid
    python identifier.
    The following transformation of ``key`` will be considered:
        ' '     ==>  '_'
        '-'     ==>  '_'
        '.'     ==>  '_'
        '[0-9]' ==> _[0-9]
    """
    key_transformed = key.replace(' ', '_').replace('-', '_').replace('.', '_')
    if key_transformed[0].isdigit():
        key_transformed = '_' + key_transformed
    return key_transformed == name
class SavepointTopCollection(SavepointCollection):
    """ Collection of all savepoints.
    """
    def __init__(self, savepoint_list):
        self.__savepoint_list = savepoint_list
    def savepoints(self):
        return self.__savepoint_list
    def __make_savepoint_collection(self, name, match_exact=False):
        savepoint_list = []
        for sp in self.__savepoint_list:
            sp_name = sp.name
            if name == sp_name:
                savepoint_list += [sp]
            elif not match_exact and transformed_equal(name, sp_name):
                savepoint_list += [sp]
        if not savepoint_list:
            errstr = "savepoint with name '%s' does not exist" % name
            # Make a suggestion if possible
            dist = []
            for sp in self.__savepoint_list:
                dist += [levenshtein(name, sp.name)]
            if min(dist) <= 3:
                errstr += ", did you mean '%s'?" % self.__savepoint_list[dist.index(min(dist))].name
            raise SerialboxError(errstr)
        return SavepointNamedCollection(savepoint_list, None)
    def __getattr__(self, name):
        """ Access a collection of savepoints identified by `name`
        :param name: Name of the savepoint
        :type name: str
        :return: Collection of savepoints sharing the same `name`
        :rtype: SavepointNamedCollection
        """
        return self.__make_savepoint_collection(name, False)
    def __getitem__(self, index):
        """ Access a collection of savepoints identified by `index`
        If `index` is an integer (`isinstance(index, int`), the method returns the unique Savepoint
        at poisition `index` in the savepoint list. Otherwise,
        :param index: Name or index of the savepoint
        :type index: str, int
        :return: Collection of savepoints sharing the same ``name`` or unique savepoint.
        :rtype: SavepointNamedCollection, Savepoint
        """
        if isinstance(index, int):
            return self.__savepoint_list[index]
        return self.__make_savepoint_collection(index, True)
class SavepointNamedCollection(SavepointCollection):
    """ Collection of Savepoints which all share the same `name`.
    """
    def __init__(self, savepoint_list, prev_key):
        self.__savepoint_list = savepoint_list
        self.__prev_key = prev_key
    def savepoints(self):
        return self.__savepoint_list
    def __make_named_savepoint_collection(self, key, match_exact=False):
        savepoint_list = []
        for sp in self.__savepoint_list:
            # Exact match
            if sp.metainfo.has_key(key):
                savepoint_list += [sp]
        if not savepoint_list:
            # Try a little harder ... we iterate now over all keys of the savepoints in the
            # collection.
            keys = []
            if not match_exact:
                for sp in self.__savepoint_list:
                    sp_keys = sp.metainfo.to_dict()
                    for k in sp_keys:
                        if transformed_equal(key, k):
                            keys += [k]
                            savepoint_list += [sp]
            # At his point we have to give up.. but not before we make a suggestion ;)
            if not savepoint_list:
                errstr = "no savepoint named '%s' has meta-info with key '%s'" % (
                    self.__savepoint_list[0].name, key)
                raise SerialboxError(errstr)
            # If we used match_exact=False and matched for example for key 'key_1': 'key 1' and
            # 'key-1', we just abort as there is no point to handle this case ...
            if keys.count(keys[0]) != len(keys):
                errstr = "ambiguous match for key '%s' for savepoint with name '%s'" % (
                    key, self.__savepoint_list[0].name)
                errstr += "Found matches:\n"
                for k in keys:
                    errstr += "  %s\n" % k
                raise SerialboxError(errstr)
            key = keys[0]
        return SavepointNamedCollection(savepoint_list, key)
    def __getattr__(self, key):
        return self.__make_named_savepoint_collection(key, False)
    def __getitem__(self, index):
        #
        # If `self.__prev_key` is not None, we have a query of the form
        # `serializer.savepoint.key[1]` meaning we access the meta-info key=value pair with
        # key=self.__prev_key and value=index. Otherwise, we have a query of the form
        # `serializer.savepoint['key1']`.
        #
        if self.__prev_key:
            savepoint_list = []
            # Check if key=value pair exists
            for sp in self.__savepoint_list:
                if sp.metainfo[self.__prev_key] == index:
                    savepoint_list += [sp]
            # Nothing found.. list the available savepoints and raise
            if not savepoint_list:
                errstr = "no savepoint named '%s' has meta-info: {\"%s\": %s}. Candidates are:\n" % (
                    self.__savepoint_list[0].name, self.__prev_key, index)
                for sp in self.savepoints():
                    errstr += "  {0}\n".format(str(sp))
                raise SerialboxError(errstr)
            return SavepointNamedCollection(savepoint_list, None)
        else:
            if not type(index) in StringTypes:
                raise SerialboxError("expected string in query for meta-info of Savepoint '%s'" %
                                     self.__savepoint_list[0].name)
            return self.__make_named_savepoint_collection(index, True)
register_library(lib)