# listdict.py - kantag dictionary of lists.
# Copyright (C) 2018 David Gasaway
# https://bitbucket.org/dgasaway/kantag/

# 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,
# see <http://www.gnu.org/licenses>.
class ListDict(dict):
    """
    Data container that extends dict with convenience methods for storing a list with each
    dictionary slot.
    """
    # ----------------------------------------------------------------------------------------------
    @staticmethod
    def _list_diff(a, b):
        """
        Return a list of values that are in list a but not in list b, preserving element order.
        """
        return [value for value in a if not value in b]

    # ----------------------------------------------------------------------------------------------
    @staticmethod
    def _list_int(a, b):
        """
        Return a list of values that are in both list and list b, preserving element order.
        """
        return [value for value in a if value in b]

    # ----------------------------------------------------------------------------------------------
    @staticmethod
    def _list_remove(lst, value):
        """
        Remove all occurences of value from the list in-line.
        """
        i = len(lst) - 1
        while i >= 0:
            if lst[i] == value:
                lst.pop(i)
            i -= 1

    # ----------------------------------------------------------------------------------------------
    @staticmethod
    def _list_replace(lst, replace, replacement):
        """
        Replace all values matching values in-line in a list.  Return whether any values were
        replaced.
        """
        replaced = False
        for i, v in enumerate(lst):
            if v == replace:
                lst[i] = replacement
                replaced = True
        return replaced

    # ----------------------------------------------------------------------------------------------
    @staticmethod
    def _list_replace_ci(lst, replace, replacement):
        """
        Replace all values case-insensitive-matching values in-line in a list.  Return whether any
        values were replaced.
        """
        replaced = False
        for i, v in enumerate(lst):
            if v.lower() == replace.lower():
                lst[i] = replacement
                replaced = True
        return replaced

    # ----------------------------------------------------------------------------------------------
    def append(self, key, value):
        """
        Append a value to the list at the key.
        """
        if key in self:
            self[key].append(value)
        else:
            # Create a new sequence at the key.
            self[key] = [value]

    # ----------------------------------------------------------------------------------------------
    def append_unique(self, key, value):
        """
        Append a value to the list at they key, provided the value is not already in the list.
        """
        if key in self:
            l = self[key]
            if value not in l:
                l.append(value)
        else:
            # Create a new sequence at the key.
            self[key] = [value]

    # ----------------------------------------------------------------------------------------------
    def extend(self, key, values):
        """
        Append values to the list at the key.
        """
        for value in values:
            self.append(key, value)

    # ----------------------------------------------------------------------------------------------
    def extend_unique(self, key, values):
        """
        Append values to the list at the key, provided the values are not already in the list.
        """
        for value in values:
            self.append_unique(key, value)

    # ----------------------------------------------------------------------------------------------
    def merge_unique(self, dct):
        """
        Add values from a dictionary that are not already in the object.
        """
        for key, values in dct.iteritems():
            self.extend_unique(key, values)

    # ----------------------------------------------------------------------------------------------
    def replace(self, key, replace, replacement):
        """
        Replace matching values in the list at the key.
        """
        if key in self:
            #self[key] = map((lambda v: replacement if v == replace else v), self[key])
            ListDict._list_replace(self[key], replace, replacement)

    # ----------------------------------------------------------------------------------------------
    def replace_ci(self, key, replace, replacement):
        """
        Replace case-insensitive-matching values in the list at the key.
        """
        if key in self:
            #self[key] = map((lambda v: replacement if v == replace else v), self[key])
            ListDict._list_replace_ci(self[key], replace, replacement)

    # ----------------------------------------------------------------------------------------------
    def remove(self, key, value):
        """
        Remove a value from the list at key; if the list is empty after removing the value, remove
        the key.
        """
        if key in self:
            l = [v for v in self[key] if v != value]
            if len(l) == 0:
                del self[key]
            else:
                self[key] = l

    # ----------------------------------------------------------------------------------------------
    def append_replace(self, key, replace, value):
        """
        Replace matching values in the list at the key with another value.  If neither the replaced
        value nor replacement value is in the list, then append the replacement value.
        """
        # Note we want this code to preserve the order of the items in the list.  That's why we use
        # a replace when possible rather than a simpler remove/append.
        if key in self:
            l = self[key]
            replaced = ListDict._list_replace(l, replace, value)
            # As an optimization, avoid attempting to append if there was a replace.
            if not replaced and value not in l:
                l.append(value)
        else:
            self[key] = [value]

    # ----------------------------------------------------------------------------------------------
    def remove_dict(self, remove):
        """
        Remove all the values from the dictionary that are present in a second dictionary.
        """
        for key, values in remove.iteritems():
            for value in values:
                self.remove(key, value)

    # ----------------------------------------------------------------------------------------------
    def move_values(self, a, b):
        """
        Provided there is a list with key a, replace the list at key b with the list at key a and
        remove key a.
        """
        if a in self:
            self[b] = self[a]
            del self[a]

    # ----------------------------------------------------------------------------------------------
    def contains(self, key, value):
        """
        Return whether the list at the given key contains the given value.
        """
        return key in self and value in self[key]

    # ----------------------------------------------------------------------------------------------
    def apply_to_all(self, key, func):
        """
        Apply a function to each value in the list at key.
        """
        if key in self:
            self[key] = [func(value) for value in self[key]]

    # ----------------------------------------------------------------------------------------------
    @staticmethod
    def get_common_values(dicts):
        """
        Return a new ListDict that contains the values common to all the ListDicts passed.
        """
        result = ListDict()

        for key, values in dicts[0].iteritems():
            common_values = values[:]
            for dict2 in dicts[1:]:
                if key in dict2:
                    common_values = ListDict._list_int(common_values, dict2[key])
                else:
                    common_values = []
            if len(common_values) > 0:
                result[key] = common_values

        return result
