"""Util functions/classes for airbrake-python."""

try:
    from queue import Queue, Full, Empty
except ImportError:
    # Py2 legacy fix
    from Queue import Queue, Full, Empty

import os
import traceback
import types
import json
import subprocess

try:
    TypeType = types.TypeType
except AttributeError:
    # For >= Python 3
    TypeType = type

try:
    # For >= Python 3
    from subprocess import DEVNULL
except ImportError:
    DEVNULL = open(os.devnull, 'wb')


class FailProofJSONEncoder(json.JSONEncoder):
    """Uses object's representation for unsupported types."""

    def default(self, o):  # pylint: disable=E0202
        # E0202 ignored in favor of compliance with documentation:
        # https://docs.python.org/2/library/json.html#json.JSONEncoder.default
        """Return object's repr when not JSON serializable."""
        try:
            return repr(o)
        except Exception:  # pylint: disable=W0703
            return super(FailProofJSONEncoder, self).default(o)


class CheckableQueue(Queue):

    """Checkable FIFO Queue which makes room for new items."""

    def __init__(self, maxsize=1000):
        """Queue constructor."""
        Queue.__init__(self, maxsize=maxsize)

    def __contains__(self, item):
        """Check the Queue for the existence of an item."""
        try:
            # pylint: disable=not-context-manager
            with self.mutex:
                return item in self.queue
        except AttributeError:
            return item in self.queue

    def put(self, item, block=False, timeout=1):
        """Add an item to the Queue."""
        try:
            Queue.put(self, item, block=block, timeout=timeout)
        except Full:
            try:
                self.get_nowait()
            except Empty:
                pass
            self.put(item)


def is_exc_info_tuple(exc_info):
    """Determine whether 'exc_info' is an exc_info tuple.

    Note: exc_info tuple means a tuple of exception related values
    as returned by sys.exc_info().
    """
    try:
        errtype, value, tback = exc_info
        if all([x is None for x in exc_info]):
            return True
        elif all((isinstance(errtype, TypeType),
                  isinstance(value, Exception),
                  hasattr(tback, 'tb_frame'),
                  hasattr(tback, 'tb_lineno'),
                  hasattr(tback, 'tb_next'))):
            return True
    except (TypeError, ValueError):
        pass
    return False


def pytb_lastline(excinfo=None):
    """Return the actual last line of the (current) traceback.

    To provide exc_info, rather than allowing this function
    to read the stack frame automatically, this function
    may be called like so:

        ll = pytb_lastline(sys.exc_info())

    OR
        try:
            1/0
        except Exception as err:
            ll = pytb_lastline(err)
    """
    lines = None
    if excinfo:
        if isinstance(excinfo, Exception):
            # pylint: disable=using-constant-test
            kls = getattr(excinfo, '__class__', '')
            if kls:
                kls = str(getattr(kls, '__name__', ''))
                kls = ("%s: " % kls) if kls else ''
            lines = [kls + str(excinfo)]
        else:
            try:
                lines = traceback.format_exception(*excinfo)
                lines = "\n".join(lines).split('\n')
            except (TypeError, AttributeError) as err:
                err.message = (
                    "Incorrect argument(s) [%s] passed to pytb_lastline(). "
                    "Should be sys.exc_info() or equivalent. | %s"
                    % (excinfo, err.message))
                raise err.__class__(err.message)
    if not lines:
        # uses sys.exc_info()
        lines = traceback.format_exc().split('\n')
    # strip whitespace, Falsy values,
    # and the string 'None', sometimes returned by the traceback module
    lines = [line.strip() for line in lines if line]
    lines = [line for line in lines if str(line).lower() != 'none']
    if lines:
        return lines[-1]


def non_empty_keys(data):
    """Strip out empty keys from a dict.

    :param dict data: the dict to copy
    :return:
    """
    non_empty = {}
    for (key, val) in data.items():
        if isinstance(val, dict):
            data = non_empty_keys(val)
            if data:
                non_empty[key] = data
        elif val and val != 'None':
            non_empty[key] = val
    return non_empty


def get_local_git_revision():
    """Find the commit hash of the latest local commit."""
    rev = _git_revision_with_binary()
    if not rev:
        rev = _git_revision_from_file()
    return rev


def _git_revision_with_binary():
    """Get the latest git hash using the git binary."""
    try:
        rev = subprocess.check_output(["git", "rev-parse", "HEAD"],
                                      stderr=DEVNULL)
        return rev.decode('utf-8').strip()
    except (OSError, subprocess.CalledProcessError):
        return None


def _git_revision_from_file():
    """Get the latest git hash from file in .git/refs/heads/master."""
    path = _get_git_path()
    if os.path.exists(path):
        rev = _get_git_ref_revision(path)
        if rev is None:
            return None
        return str(rev)


def _get_git_ref_revision(path):
    """Get the latest git hash from file."""
    head_ref_path_file = os.path.join(path, "HEAD")
    if not os.path.exists(head_ref_path_file):
        return None
    with open(head_ref_path_file, 'r') as ref_path_file:
        ref = ref_path_file.read().strip()
        if 'ref:' in ref:
            ref_path = ref.partition('ref:')[-1].strip()
            rev_file = os.path.join(path, ref_path)
            if not os.path.exists(rev_file):
                return None
            with open(rev_file, 'r') as rev_file_handler:
                return rev_file_handler.read().strip()
        elif len(ref) == 40:
            return ref
        else:
            return None


def _get_git_path():
    """Get the path to the local git repo."""
    package_dir = os.path.dirname(__file__)
    root_dir = os.path.normpath(os.path.join(package_dir, os.pardir))
    return os.path.join(root_dir, '.git')