from __future__ import absolute_import

import contextlib
import unittest
import inspect
import warnings
import functools
import sys
from collections import namedtuple

import gi
from gi import PyGIDeprecationWarning
from gi.repository import GLib
from gi._compat import StringIO


ExceptionInfo = namedtuple("ExceptionInfo", ["type", "value", "traceback"])
"""The type used for storing exceptions used by capture_exceptions()"""


@contextlib.contextmanager
def capture_exceptions():
    """Installs a temporary sys.excepthook which records all exceptions
    instead of printing them.
    """

    exceptions = []

    def custom_excepthook(*args):
        exceptions.append(ExceptionInfo(*args))

    old_hook = sys.excepthook
    sys.excepthook = custom_excepthook
    try:
        yield exceptions
    finally:
        sys.excepthook = old_hook


def ignore_gi_deprecation_warnings(func_or_class):
    """A unittest class and function decorator which makes them ignore
    PyGIDeprecationWarning.
    """

    if inspect.isclass(func_or_class):
        assert issubclass(func_or_class, unittest.TestCase)
        cls = func_or_class
        for name, value in cls.__dict__.items():
            if callable(value) and name.startswith("test_"):
                new_value = ignore_gi_deprecation_warnings(value)
                setattr(cls, name, new_value)
        return cls
    else:
        func = func_or_class

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            with capture_gi_deprecation_warnings():
                return func(*args, **kwargs)

        return wrapper


@contextlib.contextmanager
def capture_gi_deprecation_warnings():
    """Temporarily suppress PyGIDeprecationWarning output and record them"""

    with warnings.catch_warnings(record=True) as warn:
        warnings.simplefilter('always', category=PyGIDeprecationWarning)
        yield warn


@contextlib.contextmanager
def capture_glib_warnings(allow_warnings=False, allow_criticals=False):
    """Temporarily suppress glib warning output and record them.

    The test suite is run with G_DEBUG="fatal-warnings fatal-criticals"
    by default. Setting allow_warnings and allow_criticals will temporarily
    allow warnings or criticals without terminating the test run.
    """

    old_mask = GLib.log_set_always_fatal(GLib.LogLevelFlags(0))

    new_mask = old_mask
    if allow_warnings:
        new_mask &= ~GLib.LogLevelFlags.LEVEL_WARNING
    if allow_criticals:
        new_mask &= ~GLib.LogLevelFlags.LEVEL_CRITICAL

    GLib.log_set_always_fatal(GLib.LogLevelFlags(new_mask))

    GLibWarning = gi._gi.Warning
    try:
        with warnings.catch_warnings(record=True) as warn:
            warnings.filterwarnings('always', category=GLibWarning)
            yield warn
    finally:
        GLib.log_set_always_fatal(old_mask)


@contextlib.contextmanager
def capture_glib_deprecation_warnings():
    """Temporarily suppress glib deprecation warning output and record them"""

    GLibWarning = gi._gi.Warning
    with warnings.catch_warnings(record=True) as warn:
        warnings.filterwarnings(
            'always', category=GLibWarning,
            message=".+ is deprecated and shouldn't be used anymore\\. "
                    "It will be removed in a future version\\.")
        yield warn


@contextlib.contextmanager
def capture_output():
    """
    with capture_output() as (stdout, stderr):
        some_action()
    print(stdout.getvalue(), stderr.getvalue())
    """

    err = StringIO()
    out = StringIO()
    old_err = sys.stderr
    old_out = sys.stdout
    sys.stderr = err
    sys.stdout = out

    try:
        yield (out, err)
    finally:
        sys.stderr = old_err
        sys.stdout = old_out
