# Copyright (c) 2008-2009 Aryeh Leib Taurog, all rights reserved.
# Released under the New BSD license.
"""
This module contains a base type which provides list-style mutations
without specific data storage methods.

See also http://static.aryehleib.com/oldsite/MutableLists.html

Author: Aryeh Leib Taurog.
"""
from functools import total_ordering


@total_ordering
class ListMixin:
    """
    A base class which provides complete list interface.
    Derived classes must call ListMixin's __init__() function
    and implement the following:

    function _get_single_external(self, i):
        Return single item with index i for general use.
        The index i will always satisfy 0 <= i < len(self).

    function _get_single_internal(self, i):
        Same as above, but for use within the class [Optional]
        Note that if _get_single_internal and _get_single_internal return
        different types of objects, _set_list must distinguish
        between the two and handle each appropriately.

    function _set_list(self, length, items):
        Recreate the entire object.

        NOTE: items may be a generator which calls _get_single_internal.
        Therefore, it is necessary to cache the values in a temporary:
            temp = list(items)
        before clobbering the original storage.

    function _set_single(self, i, value):
        Set the single item at index i to value [Optional]
        If left undefined, all mutations will result in rebuilding
        the object using _set_list.

    function __len__(self):
        Return the length

    int _minlength:
        The minimum legal length [Optional]

    int _maxlength:
        The maximum legal length [Optional]

    type or tuple _allowed:
        A type or tuple of allowed item types [Optional]
    """

    _minlength = 0
    _maxlength = None

    # ### Python initialization and special list interface methods ###

    def __init__(self, *args, **kwargs):
        if not hasattr(self, '_get_single_internal'):
            self._get_single_internal = self._get_single_external

        if not hasattr(self, '_set_single'):
            self._set_single = self._set_single_rebuild
            self._assign_extended_slice = self._assign_extended_slice_rebuild

        super().__init__(*args, **kwargs)

    def __getitem__(self, index):
        "Get the item(s) at the specified index/slice."
        if isinstance(index, slice):
            return [self._get_single_external(i) for i in range(*index.indices(len(self)))]
        else:
            index = self._checkindex(index)
            return self._get_single_external(index)

    def __delitem__(self, index):
        "Delete the item(s) at the specified index/slice."
        if not isinstance(index, (int, slice)):
            raise TypeError("%s is not a legal index" % index)

        # calculate new length and dimensions
        origLen = len(self)
        if isinstance(index, int):
            index = self._checkindex(index)
            indexRange = [index]
        else:
            indexRange = range(*index.indices(origLen))

        newLen = origLen - len(indexRange)
        newItems = (self._get_single_internal(i)
                    for i in range(origLen)
                    if i not in indexRange)

        self._rebuild(newLen, newItems)

    def __setitem__(self, index, val):
        "Set the item(s) at the specified index/slice."
        if isinstance(index, slice):
            self._set_slice(index, val)
        else:
            index = self._checkindex(index)
            self._check_allowed((val,))
            self._set_single(index, val)

    # ### Special methods for arithmetic operations ###
    def __add__(self, other):
        'add another list-like object'
        return self.__class__([*self, *other])

    def __radd__(self, other):
        'add to another list-like object'
        return other.__class__([*other, *self])

    def __iadd__(self, other):
        'add another list-like object to self'
        self.extend(other)
        return self

    def __mul__(self, n):
        'multiply'
        return self.__class__(list(self) * n)

    def __rmul__(self, n):
        'multiply'
        return self.__class__(list(self) * n)

    def __imul__(self, n):
        'multiply'
        if n <= 0:
            del self[:]
        else:
            cache = list(self)
            for i in range(n - 1):
                self.extend(cache)
        return self

    def __eq__(self, other):
        olen = len(other)
        for i in range(olen):
            try:
                c = self[i] == other[i]
            except IndexError:
                # self must be shorter
                return False
            if not c:
                return False
        return len(self) == olen

    def __lt__(self, other):
        olen = len(other)
        for i in range(olen):
            try:
                c = self[i] < other[i]
            except IndexError:
                # self must be shorter
                return True
            if c:
                return c
            elif other[i] < self[i]:
                return False
        return len(self) < olen

    # ### Public list interface Methods ###
    # ## Non-mutating ##
    def count(self, val):
        "Standard list count method"
        count = 0
        for i in self:
            if val == i:
                count += 1
        return count

    def index(self, val):
        "Standard list index method"
        for i in range(0, len(self)):
            if self[i] == val:
                return i
        raise ValueError('%s not found in object' % val)

    # ## Mutating ##
    def append(self, val):
        "Standard list append method"
        self[len(self):] = [val]

    def extend(self, vals):
        "Standard list extend method"
        self[len(self):] = vals

    def insert(self, index, val):
        "Standard list insert method"
        if not isinstance(index, int):
            raise TypeError("%s is not a legal index" % index)
        self[index:index] = [val]

    def pop(self, index=-1):
        "Standard list pop method"
        result = self[index]
        del self[index]
        return result

    def remove(self, val):
        "Standard list remove method"
        del self[self.index(val)]

    def reverse(self):
        "Standard list reverse method"
        self[:] = self[-1::-1]

    def sort(self, key=None, reverse=False):
        "Standard list sort method"
        self[:] = sorted(self, key=key, reverse=reverse)

    # ### Private routines ###
    def _rebuild(self, newLen, newItems):
        if newLen and newLen < self._minlength:
            raise ValueError('Must have at least %d items' % self._minlength)
        if self._maxlength is not None and newLen > self._maxlength:
            raise ValueError('Cannot have more than %d items' % self._maxlength)

        self._set_list(newLen, newItems)

    def _set_single_rebuild(self, index, value):
        self._set_slice(slice(index, index + 1, 1), [value])

    def _checkindex(self, index):
        length = len(self)
        if 0 <= index < length:
            return index
        if -length <= index < 0:
            return index + length
        raise IndexError('invalid index: %s' % index)

    def _check_allowed(self, items):
        if hasattr(self, '_allowed'):
            if False in [isinstance(val, self._allowed) for val in items]:
                raise TypeError('Invalid type encountered in the arguments.')

    def _set_slice(self, index, values):
        "Assign values to a slice of the object"
        try:
            valueList = list(values)
        except TypeError:
            raise TypeError('can only assign an iterable to a slice')

        self._check_allowed(valueList)

        origLen = len(self)
        start, stop, step = index.indices(origLen)

        # CAREFUL: index.step and step are not the same!
        # step will never be None
        if index.step is None:
            self._assign_simple_slice(start, stop, valueList)
        else:
            self._assign_extended_slice(start, stop, step, valueList)

    def _assign_extended_slice_rebuild(self, start, stop, step, valueList):
        'Assign an extended slice by rebuilding entire list'
        indexList = range(start, stop, step)
        # extended slice, only allow assigning slice of same size
        if len(valueList) != len(indexList):
            raise ValueError('attempt to assign sequence of size %d '
                             'to extended slice of size %d'
                             % (len(valueList), len(indexList)))

        # we're not changing the length of the sequence
        newLen = len(self)
        newVals = dict(zip(indexList, valueList))

        def newItems():
            for i in range(newLen):
                if i in newVals:
                    yield newVals[i]
                else:
                    yield self._get_single_internal(i)

        self._rebuild(newLen, newItems())

    def _assign_extended_slice(self, start, stop, step, valueList):
        'Assign an extended slice by re-assigning individual items'
        indexList = range(start, stop, step)
        # extended slice, only allow assigning slice of same size
        if len(valueList) != len(indexList):
            raise ValueError('attempt to assign sequence of size %d '
                             'to extended slice of size %d'
                             % (len(valueList), len(indexList)))

        for i, val in zip(indexList, valueList):
            self._set_single(i, val)

    def _assign_simple_slice(self, start, stop, valueList):
        'Assign a simple slice; Can assign slice of any length'
        origLen = len(self)
        stop = max(start, stop)
        newLen = origLen - stop + start + len(valueList)

        def newItems():
            for i in range(origLen + 1):
                if i == start:
                    yield from valueList

                if i < origLen:
                    if i < start or i >= stop:
                        yield self._get_single_internal(i)

        self._rebuild(newLen, newItems())
