#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Various utilities functions
"""
from collections.abc import MutableSet

from types import GeneratorType


def find_all(string, sub, start=None, end=None, ignore_case=False, **kwargs):
    """
    Return all indices in string s where substring sub is
    found, such that sub is contained in the slice s[start:end].

    >>> list(find_all('The quick brown fox jumps over the lazy dog', 'fox'))
    [16]

    >>> list(find_all('The quick brown fox jumps over the lazy dog', 'mountain'))
    []

    >>> list(find_all('The quick brown fox jumps over the lazy dog', 'The'))
    [0]

    >>> list(find_all(
    ... 'Carved symbols in a mountain hollow on the bank of an inlet irritated an eccentric person',
    ... 'an'))
    [44, 51, 70]

    >>> list(find_all(
    ... 'Carved symbols in a mountain hollow on the bank of an inlet irritated an eccentric person',
    ... 'an',
    ... 50,
    ... 60))
    [51]

    :param string: the input string
    :type string: str
    :param sub: the substring
    :type sub: str
    :return: all indices in the input string
    :rtype: __generator[str]
    """
    #pylint: disable=unused-argument
    if ignore_case:
        sub = sub.lower()
        string = string.lower()
    while True:
        start = string.find(sub, start, end)
        if start == -1:
            return
        yield start
        start += len(sub)


def get_first_defined(data, keys, default_value=None):
    """
    Get the first defined key in data.
    :param data:
    :type data:
    :param keys:
    :type keys:
    :param default_value:
    :type default_value:
    :return:
    :rtype:
    """
    for key in keys:
        if key in data:
            return data[key]
    return default_value


def is_iterable(obj):
    """
    Are we being asked to look up a list of things, instead of a single thing?
    We check for the `__iter__` attribute so that this can cover types that
    don't have to be known by this module, such as NumPy arrays.

    Strings, however, should be considered as atomic values to look up, not
    iterables.

    We don't need to check for the Python 2 `unicode` type, because it doesn't
    have an `__iter__` attribute anyway.
    """
    # pylint: disable=consider-using-ternary
    return hasattr(obj, '__iter__') and not isinstance(obj, str) or isinstance(obj, GeneratorType)


def extend_safe(target, source):
    """
    Extends source list to target list only if elements doesn't exists in target list.
    :param target:
    :type target: list
    :param source:
    :type source: list
    """
    for elt in source:
        if elt not in target:
            target.append(elt)


class _Ref:
    """
    Reference for IdentitySet
    """
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        return self.value is other.value

    def __hash__(self):
        return id(self.value)


class IdentitySet(MutableSet):  # pragma: no cover
    """
    Set based on identity
    """
    def __init__(self, items=None):  # pylint: disable=super-init-not-called
        if items is None:
            items = []
        self.refs = set(map(_Ref, items))

    def __contains__(self, elem):
        return _Ref(elem) in self.refs

    def __iter__(self):
        return (ref.value for ref in self.refs)

    def __len__(self):
        return len(self.refs)

    def add(self, value):
        self.refs.add(_Ref(value))

    def discard(self, value):
        self.refs.discard(_Ref(value))

    def update(self, iterable):
        """
        Update set with iterable
        :param iterable:
        :type iterable:
        :return:
        :rtype:
        """
        for elem in iterable:
            self.add(elem)

    def __repr__(self):  # pragma: no cover
        return f"{type(self).__name__}({list(self)})"
