import inspect
import functools

import pkg_resources

from .exceptions import ConfigurationError, CyclicDependencyError


def is_nonstr_iter(v):
    if isinstance(v, str):
        return False
    return hasattr(v, '__iter__')


def bytes_(s, encoding='latin-1', errors='strict'):
    """ This function is a copy of ``pyramid.compat.bytes_``

    If ``s`` is an instance of ``text_type``, return
    ``s.encode(encoding, errors)``, otherwise return ``s``"""
    if isinstance(s, str):  # pragma: no cover
        return s.encode(encoding, errors)
    return s


class Sentinel:
    """  This class is a copy of ``pyramid.util.Sentinel``.
    """
    def __init__(self, repr):
        self.repr = repr

    def __repr__(self):
        return self.repr

# These objects are copies of ``pyramid.util.FIRST``, ``pyramid.util.LAST``.
FIRST = Sentinel('FIRST')
LAST = Sentinel('LAST')


class TopologicalSorter:
    """  This class is a copy of ``pyramid.util.TopologicalSorter``.

    A utility class which can be used to perform topological sorts against
    tuple-like data.
    """
    def __init__(self,
                 default_before=LAST,
                 default_after=None,
                 first=FIRST,
                 last=LAST):
        self.names = []
        self.req_before = set()
        self.req_after = set()
        self.name2before = {}
        self.name2after = {}
        self.name2val = {}
        self.order = []
        self.default_before = default_before
        self.default_after = default_after
        self.first = first
        self.last = last

    def remove(self, name):
        """ Remove a node from the sort input """
        self.names.remove(name)
        del self.name2val[name]
        after = self.name2after.pop(name, [])
        if after:
            self.req_after.remove(name)
            for u in after:
                self.order.remove((u, name))
        before = self.name2before.pop(name, [])
        if before:
            self.req_before.remove(name)
            for u in before:
                self.order.remove((name, u))

    def add(self, name, val, after=None, before=None):
        """ Add a node to the sort input.  The ``name`` should be a string or
        any other hashable object, the ``val`` should be the sortable (doesn't
        need to be hashable).  ``after`` and ``before`` represents the name of
        one of the other sortables (or a sequence of such named) or one of the
        special sentinel values :attr:`solo.configurator.util.FIRST`` or
        :attr:`solo.configurator.util.LAST` representing the first or last positions
        respectively.  ``FIRST`` and ``LAST`` can also be part of a sequence
        passed as ``before`` or ``after``.  A sortable should not be added
        after LAST or before FIRST.  An example::

           sorter = TopologicalSorter()
           sorter.add('a', {'a':1}, before=LAST, after='b')
           sorter.add('b', {'b':2}, before=LAST, after='c')
           sorter.add('c', {'c':3})

           sorter.sorted() # will be {'c':3}, {'b':2}, {'a':1}

        """
        if name in self.names:
            self.remove(name)
        self.names.append(name)
        self.name2val[name] = val
        if after is None and before is None:
            before = self.default_before
            after = self.default_after
        if after is not None:
            if not is_nonstr_iter(after):
                after = (after,)
            self.name2after[name] = after
            self.order += [(u, name) for u in after]
            self.req_after.add(name)
        if before is not None:
            if not is_nonstr_iter(before):
                before = (before,)
            self.name2before[name] = before
            self.order += [(name, o) for o in before]
            self.req_before.add(name)

    def sorted(self):
        """ Returns the sort input values in topologically sorted order"""
        order = [(self.first, self.last)]
        roots = []
        graph = {}
        names = [self.first, self.last]
        names.extend(self.names)

        for a, b in self.order:
            order.append((a, b))

        def add_node(node):
            if node not in graph:
                roots.append(node)
                graph[node] = [0]  # 0 = number of arcs coming into this node

        def add_arc(fromnode, tonode):
            graph[fromnode].append(tonode)
            graph[tonode][0] += 1
            if tonode in roots:
                roots.remove(tonode)

        for name in names:
            add_node(name)

        has_before, has_after = set(), set()
        for a, b in order:
            if a in names and b in names:  # deal with missing dependencies
                add_arc(a, b)
                has_before.add(a)
                has_after.add(b)

        if not self.req_before.issubset(has_before):
            raise ConfigurationError(
                'Unsatisfied before dependencies: %s'
                % (', '.join(sorted(self.req_before - has_before)))
            )
        if not self.req_after.issubset(has_after):
            raise ConfigurationError(
                'Unsatisfied after dependencies: %s'
                % (', '.join(sorted(self.req_after - has_after)))
            )

        sorted_names = []

        while roots:
            root = roots.pop(0)
            sorted_names.append(root)
            children = graph[root][1:]
            for child in children:
                arcs = graph[child][0]
                arcs -= 1
                graph[child][0] = arcs
                if arcs == 0:
                    roots.insert(0, child)
            del graph[root]

        if graph:
            # loop in input
            cycledeps = {}
            for k, v in graph.items():
                cycledeps[k] = v[1:]
            raise CyclicDependencyError(cycledeps)

        result = []

        for name in sorted_names:
            if name in self.names:
                result.append((name, self.name2val[name]))

        return result


def viewdefaults(wrapped):
    """ This function is a copy of ``pyramid.util.viewdefaults``.

    Decorator for add_view-like methods which takes into account
    __view_defaults__ attached to view it is passed. Not a documented API but
    used by some external systems.
    """
    def wrapper(self, *arg, **kw):
        defaults = {}
        if arg:
            view = arg[0]
        else:
            view = kw.get('view')
        view = maybe_dotted(view)
        if inspect.isclass(view):
            defaults = getattr(view, '__view_defaults__', {}).copy()

        # We do not use action_method
        # Uncomment this when it's necessary
        # ----------------------------------
        # if not '_backframes' in kw:
        #     kw['_backframes'] = 1 # for action_method

        defaults.update(kw)
        return wrapped(self, *arg, **defaults)
    return functools.wraps(wrapped)(wrapper)


def _pkg_resources_style(value):
    """ This method is taken from Pyramid Web Framework.

    package.module:attr style
    """
    # Calling EntryPoint.load with an argument is deprecated.
    # See https://pythonhosted.org/setuptools/history.html#id8
    ep = pkg_resources.EntryPoint.parse('x={}'.format(value))
    if hasattr(ep, 'resolve'):
        # setuptools>=10.2
        return ep.resolve()  # pragma: NO COVER
    else:
        return ep.load(False)  # pragma: NO COVER


def maybe_dotted(dotted):
    if not isinstance(dotted, str):
        return dotted
    return _pkg_resources_style(dotted)
