import logging
from contextlib import suppress
from typing import List, NamedTuple, Optional

from qtpy.QtCore import QMessageLogContext, QtMsgType, qInstallMessageHandler


class Record(NamedTuple):
    level: int
    message: str
    ctx: dict


class QMessageHandler:
    """A context manager to intercept messages from Qt.

    Parameters
    ----------
    logger : logging.Logger, optional
        If provided, intercepted messages will be logged with `logger` at the
        corresponding python log level, by default None

    Attributes
    ----------
    records: list of tuple
        Captured messages. This is a 3-tuple of:
        `(log_level: int, message: str, context: dict)`

    Examples
    --------

    >>> handler = QMessageHandler()
    >>> handler.install()  # now all Qt output will be available at mh.records

    >>> with QMessageHandler() as handler:  # temporarily install
    ...     ...

    >>> logger = logging.getLogger(__name__)
    >>> with QMessageHandler(logger):  # re-reoute Qt messages to a python logger.
    ...    ...
    """

    _qt2loggertype = {
        QtMsgType.QtDebugMsg: logging.DEBUG,
        QtMsgType.QtInfoMsg: logging.INFO,
        QtMsgType.QtWarningMsg: logging.WARNING,
        QtMsgType.QtCriticalMsg: logging.ERROR,  # note
        QtMsgType.QtFatalMsg: logging.CRITICAL,  # note
        QtMsgType.QtSystemMsg: logging.CRITICAL,
    }

    def __init__(self, logger: Optional[logging.Logger] = None):
        self.records: List[Record] = []
        self._logger = logger
        self._previous_handler: Optional[object] = "__uninstalled__"

    def install(self):
        """Install this handler (override the current QtMessageHandler)."""
        self._previous_handler = qInstallMessageHandler(self)

    def uninstall(self):
        """Uninstall this handler, restoring the previous handler."""
        if self._previous_handler != "__uninstalled__":
            qInstallMessageHandler(self._previous_handler)

    def __repr__(self):
        n = type(self).__name__
        return f"<{n} object at {hex(id(self))} with {len(self.records)} records>"

    def __enter__(self):
        """Enter a context with this handler installed"""
        self.install()
        return self

    def __exit__(self, *args):
        self.uninstall()

    def __call__(self, msgtype: QtMsgType, context: QMessageLogContext, message: str):
        level = self._qt2loggertype[msgtype]

        # PyQt seems to throw an error if these are simply empty
        ctx = dict.fromkeys(["category", "file", "function", "line"])
        with suppress(UnicodeDecodeError):
            ctx["category"] = context.category
        with suppress(UnicodeDecodeError):
            ctx["file"] = context.file
        with suppress(UnicodeDecodeError):
            ctx["function"] = context.function
        with suppress(UnicodeDecodeError):
            ctx["line"] = context.line

        self.records.append(Record(level, message, ctx))
        if self._logger is not None:
            self._logger.log(level, message, extra=ctx)
