import platform
import random
import re
import sys
import textwrap
import threading
from typing import Any
from typing import AnyStr
from typing import Optional
from typing import Text
from typing import Union

import six


__all__ = [
    "httplib",
    "iteritems",
    "PY2",
    "Queue",
    "stringify",
    "StringIO",
    "urlencode",
    "parse",
    "reraise",
    "maybe_stringify",
]

PYTHON_VERSION_INFO = sys.version_info
PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3

# Infos about python passed to the trace agent through the header
PYTHON_VERSION = platform.python_version()
PYTHON_INTERPRETER = platform.python_implementation()

try:
    StringIO = six.moves.cStringIO
except ImportError:
    StringIO = six.StringIO  # type: ignore[misc]

httplib = six.moves.http_client
urlencode = six.moves.urllib.parse.urlencode
parse = six.moves.urllib.parse
Queue = six.moves.queue.Queue
iteritems = six.iteritems
reraise = six.reraise
reload_module = six.moves.reload_module

ensure_text = six.ensure_text
stringify = six.text_type
string_type = six.string_types[0]
binary_type = six.binary_type
msgpack_type = six.binary_type
# DEV: `six` doesn't have `float` in `integer_types`
numeric_types = six.integer_types + (float,)

# `six.integer_types` cannot be used for typing as we need to define a type
# alias for
# see https://mypy.readthedocs.io/en/latest/common_issues.html#variables-vs-type-aliases
if PY3:
    NumericType = Union[int, float]
else:
    NumericType = Union[long, int, float]  # noqa: F821

# Pattern class generated by `re.compile`
if PYTHON_VERSION_INFO >= (3, 7):
    pattern_type = re.Pattern
else:
    pattern_type = re._pattern_type  # type: ignore[misc,attr-defined]


def is_integer(obj):
    # type: (Any) -> bool
    """Helper to determine if the provided ``obj`` is an integer type or not"""
    # DEV: We have to make sure it is an integer and not a boolean
    # >>> type(True)
    # <class 'bool'>
    # >>> isinstance(True, int)
    # True
    return isinstance(obj, six.integer_types) and not isinstance(obj, bool)


try:
    from time import time_ns
except ImportError:
    from time import time as _time

    def time_ns():
        # type: () -> int
        return int(_time() * 10e5) * 1000


try:
    from time import monotonic
except ImportError:
    from ddtrace.vendor.monotonic import monotonic


try:
    from time import monotonic_ns
except ImportError:

    def monotonic_ns():
        # type: () -> int
        return int(monotonic() * 1e9)


try:
    from time import process_time_ns
except ImportError:
    from time import clock as _process_time  # type: ignore[attr-defined]

    def process_time_ns():
        # type: () -> int
        return int(_process_time() * 1e9)


if sys.version_info.major < 3:
    getrandbits = random.SystemRandom().getrandbits
else:
    getrandbits = random.getrandbits


if sys.version_info.major < 3:
    if isinstance(threading.current_thread(), threading._MainThread):  # type: ignore[attr-defined]
        main_thread = threading.current_thread()
    else:
        main_thread = threading._shutdown.im_self  # type: ignore[attr-defined]
else:
    main_thread = threading.main_thread()


if PYTHON_VERSION_INFO[0:2] >= (3, 4):
    from asyncio import iscoroutinefunction

    # Execute from a string to get around syntax errors from `yield from`
    # DEV: The idea to do this was stolen from `six`
    #   https://github.com/benjaminp/six/blob/15e31431af97e5e64b80af0a3f598d382bcdd49a/six.py#L719-L737
    six.exec_(
        textwrap.dedent(
            """
    import functools
    import asyncio


    def make_async_decorator(tracer, coro, *params, **kw_params):
        \"\"\"
        Decorator factory that creates an asynchronous wrapper that yields
        a coroutine result. This factory is required to handle Python 2
        compatibilities.

        :param object tracer: the tracer instance that is used
        :param function f: the coroutine that must be executed
        :param tuple params: arguments given to the Tracer.trace()
        :param dict kw_params: keyword arguments given to the Tracer.trace()
        \"\"\"
        @functools.wraps(coro)
        @asyncio.coroutine
        def func_wrapper(*args, **kwargs):
            with tracer.trace(*params, **kw_params):
                result = yield from coro(*args, **kwargs)  # noqa: E999
                return result

        return func_wrapper
    """
        )
    )

else:
    # asyncio is missing so we can't have coroutines; these
    # functions are used only to ensure code executions in case
    # of an unexpected behavior
    def iscoroutinefunction(fn):  # type: ignore
        return False

    def make_async_decorator(tracer, fn, *params, **kw_params):
        return fn


# DEV: There is `six.u()` which does something similar, but doesn't have the guard around `hasattr(s, 'decode')`
def to_unicode(s):
    # type: (AnyStr) -> Text
    """Return a unicode string for the given bytes or string instance."""
    # No reason to decode if we already have the unicode compatible object we expect
    # DEV: `six.text_type` will be a `str` for python 3 and `unicode` for python 2
    # DEV: Double decoding a `unicode` can cause a `UnicodeEncodeError`
    #   e.g. `'\xc3\xbf'.decode('utf-8').decode('utf-8')`
    if isinstance(s, six.text_type):
        return s

    # If the object has a `decode` method, then decode into `utf-8`
    #   e.g. Python 2 `str`, Python 2/3 `bytearray`, etc
    if hasattr(s, "decode"):
        return s.decode("utf-8")

    # Always try to coerce the object into the `six.text_type` object we expect
    #   e.g. `to_unicode(1)`, `to_unicode(dict(key='value'))`
    return six.text_type(s)


def get_connection_response(
    conn,  # type: httplib.HTTPConnection
):
    # type: (...) -> httplib.HTTPResponse
    """Returns the response for a connection.

    If using Python 2 enable buffering.

    Python 2 does not enable buffering by default resulting in many recv
    syscalls.

    See:
    https://bugs.python.org/issue4879
    https://github.com/python/cpython/commit/3c43fcba8b67ea0cec4a443c755ce5f25990a6cf
    """
    if PY2:
        return conn.getresponse(buffering=True)
    else:
        return conn.getresponse()


try:
    import contextvars  # noqa
except ImportError:
    from ddtrace.vendor import contextvars  # type: ignore  # noqa

    CONTEXTVARS_IS_AVAILABLE = False
else:
    CONTEXTVARS_IS_AVAILABLE = True


try:
    from pep562 import Pep562  # noqa

    def ensure_pep562(module_name):
        # type: (str) -> None
        if sys.version_info < (3, 7):
            Pep562(module_name)


except ImportError:

    def ensure_pep562(module_name):
        # type: (str) -> None
        pass


def maybe_stringify(obj):
    # type: (Any) -> Optional[str]
    if obj is not None:
        return stringify(obj)
    return None
