from __future__ import nested_scopes, generators, division, absolute_import, with_statement, \
   print_function, unicode_literals

from . import compatibility

compatibility.backport()  # noqa

import builtins

import os  # noqa
import sys  # noqa

from io import UnsupportedOperation  # noqa


from collections import OrderedDict  # noqa
from unicodedata import normalize  # noqa

import re  # noqa
import inspect  # noqa
from keyword import iskeyword  # noqa
from urllib.parse import urljoin

# region Compatibility Conditionals

# The following detects the presence of the typing library

try:
    from typing import Union, Optional, Iterable, Tuple, Any, Callable, AnyStr, Iterator  # noqa
except ImportError:
    Union = Optional = Iterable = Tuple = Any = Callable = AnyStr = Iterator = None


# Before `collections.abc` existed, the definitions we use from this module were in `collections`
try:
    import collections.abc as collections_abc
    import collections
except ImportError:
    import collections
    collections_abc = collections

# Earlier versions of the `collections` library do not include the `Generator` class, so when this class is missing--
# we employ a workaround.

if hasattr(collections_abc, 'Generator'):
    Generator = collections_abc.Generator
else:
    Generator = type(n for n in (1, 2, 3))

# endregion

try:

    from inspect import signature

    getargspec = None

except ImportError:

    signature = None

    try:
        from inspect import getfullargspec
    except ImportError:
        from inspect import getargspec as getfullargspec


_Module = type(re)


def qualified_name(type_):
    # type: (Union[type, _Module]) -> str
    """
    >>> print(qualified_name(qualified_name))
    qualified_name

    >>> from sob import model
    >>> print(qualified_name(model.marshal))
    sob.model.marshal
    """

    if hasattr(type_, '__qualname__'):
        type_name = '.'.join(name_part for name_part in type_.__qualname__.split('.') if name_part[0] != '<')
    else:
        type_name = type_.__name__

    if isinstance(type_, _Module):

        if type_name in (
            'builtins', '__builtin__', '__main__', '__init__'
        ):
            type_name = None

    else:

        if type_.__module__ not in (
            'builtins', '__builtin__', '__main__', '__init__'
        ):
            type_name = type_.__module__ + '.' + type_name

    return type_name


def calling_functions_qualified_names(depth=1):
    # type: (int) -> Iterator[str]
    """
    >>> def my_function_a(): return calling_functions_qualified_names()
    >>> def my_function_b(): return my_function_a()
    >>> print(my_function())
    ['my_function_b', 'my_function_a']
    """

    depth += 1
    name = calling_function_qualified_name(depth=depth)
    names = []

    while name:
        if name and not (names and names[0] == name):
            names.insert(0, name)
        depth += 1
        name = calling_function_qualified_name(depth=depth)

    return names


def calling_function_qualified_name(depth=1):
    # type: (int) -> Optional[str]
    """
    >>> def my_function(): return calling_function_qualified_name()
    >>> print(my_function())
    """

    if not isinstance(depth, int):

        depth_representation = repr(depth)

        raise TypeError(
            'The parameter `depth` for `sob.utilities.calling_function_qualified_name` must be an `int`, not' +
            (
                (':\n%s' if '\n' in depth_representation else ' %s.') %
                depth_representation
            )
        )
    try:
        stack = inspect.stack()
    except IndexError:
        return None

    if len(stack) < (depth + 1):
        return None

    name_list = []
    frame_info = stack[depth]  # type: inspect.FrameInfo

    try:
        frame_function = frame_info.function
    except AttributeError:
        frame_function = frame_info[3]

    if frame_function != '<module>':

        try:
            frame = frame_info.frame
        except AttributeError:
            frame = frame_info[0]

        name_list.append(frame_function)
        arguments, _, _, frame_locals = inspect.getargvalues(frame)

        if arguments:

            argument = arguments[0]
            argument_value = frame_locals[argument]
            argument_value_type = type(argument_value)

            if (
                hasattr(argument_value_type, '__name__') and
                hasattr(argument_value_type, '__module__') and
                (
                    (argument_value_type.__name__ not in dir(builtins)) or
                    (getattr(builtins, argument_value_type.__name__) is not argument_value_type)
                )
            ):
                name_list.append(qualified_name(argument_value_type))

    if len(name_list) < 2:

        try:
            file_name = frame_info.filename
        except AttributeError:
            file_name = frame_info[1]

        module_name = inspect.getmodulename(file_name)

        if (module_name is not None) and (module_name not in sys.modules):

            path_parts = list(os.path.split(file_name))
            path_parts.pop()

            while path_parts:

                parent = path_parts.pop()
                module_name = parent + '.' + module_name

                if module_name in sys.modules:
                    break

        if module_name is None:
            raise ValueError('The path "%s" is not a python module' % file_name)
        else:
            if module_name in sys.modules:
                qualified_module_name = qualified_name(sys.modules[module_name])
                name_list.append(qualified_module_name)

    return '.'.join(reversed(name_list))


def property_name(string):
    # type: (str) -> str
    """
    Converts a "camelCased" attribute/property name, or a name which conflicts with a python keyword, to a
    pep8-compliant property name.

    >>> print(property_name('theBirdsAndTheBees'))
    the_birds_and_the_bees

    >>> print(property_name('FYIThisIsAnAcronym'))
    fyi_this_is_an_acronym

    >>> print(property_name('in'))
    in_

    >>> print(property_name('id'))
    id_
    """
    pn = re.sub(
        r'__+',
        '_',
        re.sub(
            r'[^\w]+',
            '',
            re.sub(
                r'([a-zA-Z])([0-9])',
                r'\1_\2',
                re.sub(
                    r'([0-9])([a-zA-Z])',
                    r'\1_\2',
                    re.sub(
                        r'([A-Z])([A-Z])([a-z])',
                        r'\1_\2\3',
                        re.sub(
                            r'([a-z])([A-Z])',
                            r'\1_\2',
                            re.sub(
                                r'([^\x20-\x7F]|\s)+',
                                '_',
                                normalize('NFKD', string)
                            )
                        )
                    )
                )
            )
        )
    ).lower()
    if iskeyword(pn) or (pn in dir(builtins)):
        pn += '_'
    return pn


def class_name(string):
    """
    >>> print(class_name('the birds and the bees'))
    TheBirdsAndTheBees

    >>> print(class_name('the-birds-and-the-bees'))
    TheBirdsAndTheBees

    >>> print(class_name('**the - birds - and - the - bees**'))
    TheBirdsAndTheBees

    >>> print(class_name('FYI is an acronym'))
    FYIIsAnAcronym

    >>> print(class_name('in-you-go'))
    InYouGo

    >>> print(class_name('False'))
    False_

    >>> print(class_name('True'))
    True_

    >>> print(class_name('ABC Acronym'))
    ABCAcronym
    """
    return camel(string, capitalize=True)


def camel(string, capitalize=False):
    # type: (str, bool) -> str
    """
    >>> print(camel('the birds and the bees'))
    theBirdsAndTheBees

    >>> print(camel('the-birds-and-the-bees'))
    theBirdsAndTheBees

    >>> print(camel('**the - birds - and - the - bees**'))
    theBirdsAndTheBees

    >>> print(camel('FYI is an acronym'))
    fyiIsAnAcronym

    >>> print(camel('in-you-go'))
    inYouGo

    >>> print(camel('False'))
    false

    >>> print(camel('True'))
    true

    >>> print(camel('in'))
    in_
    """
    string = normalize('NFKD', string)
    characters = []
    if not capitalize:
        string = string.lower()
    capitalize_next = capitalize
    for s in string:
        if s in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789':
            if capitalize_next:
                if capitalize or characters:
                    s = s.upper()
            characters.append(s)
            capitalize_next = False
        else:
            capitalize_next = True
    cn = ''.join(characters)
    if iskeyword(cn) or (cn in dir(builtins)):
        cn += '_'
    return cn


def get_source(o):
    # type: (object) -> str
    if hasattr(o, '_source') and isinstance(o._source, str):
        result = o._source
    else:
        result = inspect.getsource(o)
    return result


def camel_split(string):
    # test: (str) -> str
    """
    >>> print('(%s)' % ', '.join("'%s'" % s for s in camel_split('theBirdsAndTheBees')))
    ('the', 'Birds', 'And', 'The', 'Bees')
    >>> print('(%s)' % ', '.join("'%s'" % s for s in camel_split('theBirdsAndTheBees123')))
    ('the', 'Birds', 'And', 'The', 'Bees', '123')
    >>> print('(%s)' % ', '.join("'%s'" % s for s in camel_split('theBirdsAndTheBeesABC123')))
    ('the', 'Birds', 'And', 'The', 'Bees', 'ABC', '123')
    >>> print('(%s)' % ', '.join("'%s'" % s for s in camel_split('the-Birds-And-The-Bees-ABC--123')))
    ('the', '-', 'Birds', '-', 'And', '-', 'The', '-', 'Bees', '-', 'ABC', '--', '123')
    >>> print('(%s)' % ', '.join("'%s'" % s for s in camel_split('THEBirdsAndTheBees')))
    ('THE', 'Birds', 'And', 'The', 'Bees')
    """
    words = []
    character_type = None
    acronym = False
    for s in string:
        if s in '0123456789':
            if character_type == 0:
                words[-1].append(s)
            else:
                words.append([s])
            character_type = 0
            acronym = False
        elif s in 'abcdefghijklmnopqrstuvwxyz':
            if character_type == 1:
                words[-1].append(s)
            elif character_type == 2:
                if acronym:
                    words.append([words[-1].pop()] + [s])
                else:
                    words[-1].append(s)
            else:
                words.append([s])
            character_type = 1
            acronym = False
        elif s in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
            if character_type == 2:
                words[-1].append(s)
                acronym = True
            else:
                words.append([s])
                acronym = False
            character_type = 2
        else:
            if character_type == 3:
                words[-1].append(s)
            else:
                words.append([s])
            character_type = 3
    return tuple(
        ''.join(w) for w in words
    )


def properties_values(o):
    # type: (object) -> Sequence[Tuple[AnyStr, Any]]
    for a in dir(o):
        if a[0] != '_':
            v = getattr(o, a)
            if not callable(v):
                yield a, v


UNDEFINED = None


class Undefined(object):

    def __init__(self):

        if UNDEFINED is not None:
            raise RuntimeError(
                '%s may only be defined once.' % repr(self)
            )

    def __repr__(self):
        return (
            'UNDEFINED'
            if self.__module__ in ('__main__', 'builtins', '__builtin__', __name__) else
            '%s.UNDEFINED' % self.__module__
        )

    def __bool__(self):
        return False

    def __hash__(self):
        return 0

    def __eq__(self, other):
        # type: (Any) -> bool
        return other is self


UNDEFINED = Undefined()


def parameters_defaults(function):
    # type: (Callable) -> OrderedDict
    """
    Returns an ordered dictionary mapping a function's argument names to default values, or `UNDEFINED` in the case of
    positional arguments.

    >>> class X(object):
    ...
    ...    def __init__(self, a, b, c, d=1, e=2, f=3):
    ...        pass
    ...
    >>> print(list(parameters_defaults(X.__init__).items()))
    [('self', UNDEFINED), ('a', UNDEFINED), ('b', UNDEFINED), ('c', UNDEFINED), ('d', 1), ('e', 2), ('f', 3)]
    """
    pd = OrderedDict()
    if signature is None:
        spec = getfullargspec(function)
        i = - 1
        for a in spec.args:
            pd[a] = UNDEFINED
        for a in reversed(spec.args):
            try:
                pd[a] = spec.defaults[i]
            except IndexError:
                break
            i -= 1
    else:
        for pn, p in signature(function).parameters.items():
            if p.default is inspect.Parameter.empty:
                pd[pn] = UNDEFINED
            else:
                pd[pn] = p.default
    return pd


def read(data):
    # type: (Union[str, IOBase, addbase]) -> Any
    if (
        (hasattr(data, 'readall') and callable(data.readall)) or
        (hasattr(data, 'read') and callable(data.read))
    ):
        if hasattr(data, 'seek') and callable(data.seek):
            try:
                data.seek(0)
            except UnsupportedOperation:
                pass
        if hasattr(data, 'readall') and callable(data.readall):
            try:
                data = data.readall()
            except UnsupportedOperation:
                data = data.read()
        else:
            data = data.read()
        return data
    else:
        raise TypeError(
            '%s is not a file-like object' % repr(data)
        )


def get_io_url(file_like_object):
    # type: (IOBase) -> Optional[str]
    """
    Get the URL from which an input-outpuy (file-like) object was sourced
    """
    if hasattr(file_like_object, 'url'):
        url = file_like_object.url
    elif hasattr(file_like_object, 'name'):
        url = urljoin('file:', file_like_object.name)
    return url


def indent(text, number_of_spaces=4, start=1, stop=None):
    # type: (str, int, int, int, Optional[int]) -> str
    """
    Indent text by `number_of_spaces` starting at line index `start` and stopping at line index `stop`.
    """

    indented_text = text

    if ('\n' in text) or start == 0:
        lines = text.split('\n')
        if stop:
            if stop < 0:
                stop = len(lines) - stop
        else:
            stop = len(lines)
        for i in range(start, stop):
            lines[i] = (' ' * number_of_spaces) + lines[i]
        indented_text = '\n'.join(lines)

    return indented_text


_URL_DIRECTORY_AND_FILE_NAME_RE = re.compile(r'^(.*/)([^/]*)')


def url_directory_and_file_name(url):
    # type: (str) -> str
    """
    Split a URL into
    """
    return _URL_DIRECTORY_AND_FILE_NAME_RE.match(url).groups()


def url_relative_to(absolute_url, base_url):
    # type: (str) -> str
    """
    Returns a relative URL given an absolute URL and a base URL
    """

    # If no portion of the absolute URL is shared with the base URL--the absolute URL will be returned
    relative_url = absolute_url  # type: str

    base_url = url_directory_and_file_name(base_url)[0]

    if base_url:

        relative_url = ''

        # URLs are not case-sensitive
        base_url = base_url.lower()
        lowercase_absolute_url = absolute_url.lower()

        while base_url and base_url.lower() != lowercase_absolute_url[:len(base_url)]:
            relative_url = '../' + relative_url
            base_url = url_directory_and_file_name(base_url[:-1])[0]

        relative_url += absolute_url[len(base_url):]

    return relative_url


if __name__ == '__main__':
    import doctest
    doctest.testmod()
