import collections.abc
import dataclasses

from abc import ABC, abstractmethod
from copy import deepcopy
from enum import Enum
from typing import Callable, Container, Generic, Iterable, Iterator, List, Optional, Sequence, Sized, TypeVar, Union, \
    Tuple, Any, Dict

T = TypeVar('T')


class UniqueList(list, Generic[T]):
    """
    Simple list that maintains uniqueness of elements.
    In comparison to 'set': preserves list interface and element ordering.

    But this class is better to use only for short lists because of linear complexity of uniqueness lookup.
    Behaves optimally for <= 4 items, good enough for <= 20 items.

    List addition and multiplication are not overloaded and return a standard list.
    But addition with assignment (+=) behaves as UniqueList.extend.

    :param iterable: list of elements to turn into distinct
    """

    def __init__(self, iterable: Optional[Iterable[T]] = None):
        iterable = iterable or ()
        super().__init__(dict.fromkeys(iterable).keys())  # preserves order and uniqueness in the newly created list

    def append(self, value: T):
        """
        Adds ``value`` to the end of the list in case it is not present there

        :param value: will be added to the list or ignored if it's already there
        """
        if value not in super().__iter__():
            super().append(value)

    def extend(self, iterable: Iterable[T]):
        """
        Extends current list with the ``iterable`` elements that aren't present in the list yet

        :param iterable: sequence of elements to be added to the list in case they aren't there
        """
        impl = super()
        impl.extend(element for element in iterable
                    if element not in impl.__iter__())

    def insert(self, index: int, value: T):
        """
        Inserts specified ``value`` to the provided ``index`` in the list in case it's unique value

        :param index: position for inserting into the list
        :param value: will be inserted at the specified ``index`` if it's unique
        """
        if value not in super().__iter__():
            super().insert(index, value)

    def __setitem__(self, key: Union[int, slice], value: Union[T, Iterable[T]]):
        """
        Sets specified ``value`` at the specified index or slice if it's unique value

        :param key: for pointing to the elements from the list
        :param value: element(s) to be set by the specified ``key``
        """
        if value not in super().__iter__():
            super().__setitem__(key, value)

    def __iadd__(self, other: Iterable[T]) -> 'UniqueList':
        """
        Extends current list with the ``iterable`` elements that aren't present in the list yet

        :param other: sequence of elements to be added to the list in case they aren't there

        :return: this class instance
        """
        self.extend(other)
        return self


def remove_items(collection: List[T], items_to_remove: Container[T]):
    """
    Removes all specified items from the list. Modifies original collection

    :param collection: list of elements to be filtered
    :param items_to_remove: filter list of unwanted elements

    :return: modified ``collection`` parameter
    """
    if collection:
        collection[:] = [item for item in collection if item not in items_to_remove]
    return collection


def are_same_length(collections: Iterable[Sized]) -> bool:
    """
    Checks if all arguments have the same length

    :param collections: collection of collections

    :return: does collections inside ``collections`` have the same length
    """
    it = collections.__iter__()
    first = next(it, None)
    if first is not None:
        first = len(first)
        for elem in it:
            if len(elem) != first:
                return False
    return True


def ensure_wrapped_in_sequence(
        obj: Optional[Union[T, Iterable[T]]],
        sequence_factory: Callable[[Iterable[T]], Sequence[T]] = list
) -> Optional[Sequence[T]]:
    """
    Makes sure given ``obj`` is of type that acts like sequence and converts ``obj`` to it otherwise

    :param obj: any object to be ensured or wrapped into sequence type
    :param sequence_factory: sequence factory for wrapping ``obj`` into in case it is not of any sequence type

    :return: the same object if it's of type sequence (or None)
        or ``obj`` wrapped in type provided by ``sequence_factory``
    """
    if obj is None:
        return obj
    elif isinstance(obj, str) or not isinstance(obj, collections.abc.Iterable):
        return sequence_factory([obj])
    elif not isinstance(obj, collections.abc.Sequence) or not isinstance(obj, sequence_factory):
        return sequence_factory(obj)
    return obj


class ComparableEnum(Enum):
    """
    The Enum implementation that allows to avoid the multi-module enum comparison problem
    (https://stackoverflow.com/questions/26589805/python-enums-across-modules)
    """

    def __eq__(self, other: 'Enum'):
        """
        Compares this enum with the ``other_graph``

        :param other: another enum

        :return: is it equal to ``other`` in terms of the string representation
        """
        return str(self) == str(other)

    def __hash__(self):
        """
        Gets hashcode of this enum

        :return: hashcode of string representation of this enum
        """
        return hash(str(self))


class Copyable:
    """Provides default implementations for `copy` & `deepcopy`."""

    def __copy__(self):
        cls = self.__class__
        result = cls.__new__(cls)
        result.__dict__.update(self.__dict__)
        return result

    def __deepcopy__(self, memo=None):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, deepcopy(v, memo))
        return result


class Comparable(ABC):
    @abstractmethod
    def __eq__(self, other: 'Comparable') -> bool:
        """
        Compares this object of :class:`Comparable` interface with the ``other`` comparable

        :param other: other object implementing :class:`Comparable` interface

        :return: is it equal to ``other`` in terms of the comparables
        """
        raise NotImplementedError()

    @abstractmethod
    def __lt__(self, other: 'Comparable') -> bool:
        """
        Compares this object of :class:`Comparable` interface with the ``other`` comparable

        :param other: other object implementing :class:`Comparable` interface

        :return: is less than ``other`` in terms of the comparables
        """
        raise NotImplementedError()

    def __ne__(self, other: 'Comparable') -> bool:
        """
        Compares this object of :class:`Comparable` interface with the ``other`` comparable

        :param other: other object implementing :class:`Comparable` interface

        :return: is it not equal to ``other`` in terms of the comparables
        """
        return not self.__eq__(other)

    def __le__(self, other) -> bool:
        """
        Compares this object of :class:`Comparable` interface with the ``other`` comparable

        :param other: other object implementing :class:`Comparable` interface

        :return: is less than or equal to ``other`` in terms of the comparables
        """
        return self.__lt__(other) or self.__eq__(other)

    def __gt__(self, other) -> bool:
        """
        Compares this object of :class:`Comparable` interface with the ``other`` comparable

        :param other: other object implementing :class:`Comparable` interface

        :return: is greater than ``other`` in terms of the comparables
        """
        return not self.__le__(other)

    def __ge__(self, other) -> bool:
        """
        Compares this object of :class:`Comparable` interface with the ``other`` comparable

        :param other: other object implementing :class:`Comparable` interface

        :return: is greater than or equal to ``other`` in terms of the comparables
        """
        return not self.__lt__(other)


class BidirectionalIterator(Iterator[T]):
    @abstractmethod
    def has_prev(self) -> bool:
        """
        Checks if this iterator has implemented previous item getter

        :return: whether this iterator has previous item getter
        """
        raise NotImplementedError()

    @abstractmethod
    def has_next(self) -> bool:
        """
        Checks if this iterator has implemented next item getter

        :return: whether this iterator has next item getter
        """
        raise NotImplementedError()

    @abstractmethod
    def next(self) -> T:
        """
        Gets next item of this iterator

        :return: next item
        """
        raise NotImplementedError()

    @abstractmethod
    def prev(self) -> T:
        """
        Gets previous item of this iterator

        :return: previous item
        """
        raise NotImplementedError()

    @abstractmethod
    def current(self) -> T:
        """
        Get current value pointed to by the iterator

        :return: current item
        """
        raise NotImplementedError()

    def __next__(self):
        """
        Gets next item of this iterator

        :raises StopIteration: the end of this iterator

        :return: next item
        """
        if self.has_next():
            return self.next()
        raise StopIteration()

    def __iter__(self):
        """
        Returns this iterator

        :return: this iterator
        """
        return self


def unzip(tuples: Iterable[Tuple]) -> Tuple[Sequence, ...]:
    return tuple(zip(*tuples))


def update_dataclass(base_dc: Any, update_dc: Union[Any, Dict]) -> Any:
    update_dict = dataclasses.asdict(update_dc) if dataclasses.is_dataclass(update_dc) else update_dc
    new_base = dataclasses.replace(base_dc, **update_dict)
    return new_base
