"""
Brian's logging module.

Preferences
-----------
.. document_brian_prefs:: logging
"""

import atexit
import logging
import logging.handlers
import os
import shutil
import sys
import tempfile
import time
from logging.handlers import RotatingFileHandler
from warnings import warn

import numpy

try:
    import scipy
except ImportError:
    scipy = None
import sympy

import brian2
from brian2.core.preferences import BrianPreference, prefs

from .environment import running_from_ipython

__all__ = ["get_logger", "BrianLogger", "std_silent"]

# ===============================================================================
# Logging preferences
# ===============================================================================


def log_level_validator(log_level):
    log_levels = ("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "DIAGNOSTIC")
    return log_level.upper() in log_levels


#: Our new log level for more detailed debug output (mostly useful for debugging
#: Brian itself, not for user scripts)
DIAGNOSTIC = 5

#: Translation from string representation to number
LOG_LEVELS = {
    "CRITICAL": logging.CRITICAL,
    "ERROR": logging.ERROR,
    "WARNING": logging.WARNING,
    "INFO": logging.INFO,
    "DEBUG": logging.DEBUG,
    "DIAGNOSTIC": DIAGNOSTIC,
}
logging.addLevelName(DIAGNOSTIC, "DIAGNOSTIC")

if "logging" not in prefs.pref_register:
    # Duplicate import of this module can happen when the documentation is built
    prefs.register_preferences(
        "logging",
        "Logging system preferences",
        delete_log_on_exit=BrianPreference(
            default=True,
            docs="""
            Whether to delete the log and script file on exit.

            If set to ``True`` (the default), log files (and the copy of the main
            script) will be deleted after the brian process has exited, unless an
            uncaught exception occurred. If set to ``False``, all log files will be
            kept.
            """,
        ),
        file_log_level=BrianPreference(
            default="DEBUG",
            docs="""
            What log level to use for the log written to the log file.

            In case file logging is activated (see `logging.file_log`), which log
            level should be used for logging. Has to be one of CRITICAL, ERROR,
            WARNING, INFO, DEBUG or DIAGNOSTIC.
            """,
            validator=log_level_validator,
        ),
        console_log_level=BrianPreference(
            default="INFO",
            docs="""
            What log level to use for the log written to the console.

            Has to be one of CRITICAL, ERROR, WARNING, INFO, DEBUG or DIAGNOSTIC.
            """,
            validator=log_level_validator,
        ),
        file_log=BrianPreference(
            default=True,
            docs="""
            Whether to log to a file or not.

            If set to ``True`` (the default), logging information will be written
            to a file. The log level can be set via the `logging.file_log_level`
            preference.
            """,
        ),
        file_log_max_size=BrianPreference(
            default=10000000,
            docs="""
            The maximum size for the debug log before it will be rotated.

            If set to any value ``> 0``, the debug log will be rotated once
            this size is reached. Rotating the log means that the old debug log
            will be moved into a file in the same directory but with suffix ``".1"``
            and the a new log file will be created with the same pathname as the
            original file. Only one backup is kept; if a file with suffix ``".1"``
            already exists when rotating, it will be overwritten.
            If set to ``0``, no log rotation will be applied.
            The default setting rotates the log file after 10MB.
            """,
        ),
        save_script=BrianPreference(
            default=True,
            docs="""
            Whether to save a copy of the script that is run.

            If set to ``True`` (the default), a copy of the currently run script
            is saved to a temporary location. It is deleted after a successful
            run (unless `logging.delete_log_on_exit` is ``False``) but is kept after
            an uncaught exception occured. This can be helpful for debugging,
            in particular when several simulations are running in parallel.
            """,
        ),
        std_redirection=BrianPreference(
            default=True,
            docs="""
            Whether or not to redirect stdout/stderr to null at certain places.

            This silences a lot of annoying compiler output, but will also hide
            error messages making it harder to debug problems. You can always
            temporarily switch it off when debugging. If
            `logging.std_redirection_to_file` is set to ``True`` as well, then the
            output is saved to a file and if an error occurs the name of this file
            will be printed.
            """,
        ),
        std_redirection_to_file=BrianPreference(
            default=True,
            docs="""
            Whether to redirect stdout/stderr to a file.

            If both ``logging.std_redirection`` and this preference are set to
            ``True``, all standard output/error (most importantly output from
            the compiler) will be stored in files and if an error occurs the name
            of this file will be printed. If `logging.std_redirection` is ``True``
            and this preference is ``False``, then all standard output/error will
            be completely suppressed, i.e. neither be displayed nor stored in a
            file.

            The value of this preference is ignore if `logging.std_redirection` is
            set to ``False``.
            """,
        ),
        display_brian_error_message=BrianPreference(
            default=True,
            docs="""
            Whether to display a text for uncaught errors, mentioning the location
            of the log file, the mailing list and the github issues.

            Defaults to ``True``.""",
        ),
    )

# ===============================================================================
# Initial setup
# ===============================================================================


def _encode(text):
    """Small helper function to encode unicode strings as UTF-8."""
    return text.encode("UTF-8")


UNHANDLED_ERROR_MESSAGE = (
    "Brian 2 encountered an unexpected error. "
    "If you think this is a bug in Brian 2, please report this issue either to the "
    "discourse forum at <http://brian.discourse.group/>, "
    "or to the issue tracker at <https://github.com/brian-team/brian2/issues>."
)


def brian_excepthook(exc_type, exc_obj, exc_tb):
    """
    Display a message mentioning the debug log in case of an uncaught
    exception.
    """
    # Do not catch Ctrl+C
    if exc_type == KeyboardInterrupt:
        return
    logger = logging.getLogger("brian2")
    BrianLogger.exception_occured = True

    if not prefs["logging.display_brian_error_message"]:
        # Put the exception message in the log file, but do not log to the
        # console
        if BrianLogger.console_handler is not None:
            logger.removeHandler(BrianLogger.console_handler)
        logger.exception("An exception occured", exc_info=(exc_type, exc_obj, exc_tb))
        if BrianLogger.console_handler is not None:
            logger.addHandler(BrianLogger.console_handler)
        # Run the default except hook
        return sys.__excepthook__(exc_type, exc_obj, exc_tb)

    message = UNHANDLED_ERROR_MESSAGE
    if BrianLogger.tmp_log is not None:
        message += (
            " Please include this file with debug information in your "
            f"report: {BrianLogger.tmp_log} "
        )
    if BrianLogger.tmp_script is not None:
        message += (
            " Additionally, you can also include a copy "
            "of the script that was run, available "
            f"at: {BrianLogger.tmp_script}"
        )
    if hasattr(std_silent, "dest_fname_stdout"):
        stdout = std_silent.dest_fname_stdout
        stderr = std_silent.dest_fname_stderr
        message += (
            " You can also include a copy of the "
            "redirected std stream outputs, available at "
            f"'{stdout}' and '{stderr}'."
        )

    message += " Thanks!"  # very important :)
    logger.error(message, exc_info=(exc_type, exc_obj, exc_tb))


def clean_up_logging():
    """
    Shutdown the logging system and delete the debug log file if no error
    occured.
    """
    BrianLogger.initialized = False
    logging.shutdown()
    if not BrianLogger.exception_occured and prefs["logging.delete_log_on_exit"]:
        if BrianLogger.tmp_log is not None:
            try:
                os.remove(BrianLogger.tmp_log)
            except OSError as exc:
                warn(f"Could not delete log file: {exc}")
            # Remove log files that have been rotated (currently only one)
            rotated_log = f"{BrianLogger.tmp_log}.1"
            if os.path.exists(rotated_log):
                try:
                    os.remove(rotated_log)
                except OSError as exc:
                    warn(f"Could not delete log file: {exc}")
        if BrianLogger.tmp_script is not None:
            try:
                os.remove(BrianLogger.tmp_script)
            except OSError as exc:
                warn(f"Could not delete copy of script file: {exc}")
        std_silent.close()


atexit.register(clean_up_logging)


class HierarchyFilter:
    """
    A class for suppressing all log messages in a subtree of the name hierarchy.
    Does exactly the opposite as the `logging.Filter` class, which allows
    messages in a certain name hierarchy to *pass*.

    Parameters
    ----------
    name : str
        The name hiearchy to suppress. See `BrianLogger.suppress_hierarchy` for
        details.
    """

    def __init__(self, name):
        self.orig_filter = logging.Filter(name)

    def filter(self, record):
        """
        Filter out all messages in a subtree of the name hierarchy.
        """
        # do the opposite of what the standard filter class would do
        return not self.orig_filter.filter(record)


class NameFilter:
    """
    A class for suppressing log messages ending with a certain name.

    Parameters
    ----------
    name : str
        The name to suppress. See `BrianLogger.suppress_name` for details.
    """

    def __init__(self, name):
        self.name = name

    def filter(self, record):
        """
        Filter out all messages ending with a certain name.
        """
        # The last part of the name
        record_name = record.name.split(".")[-1]
        return self.name != record_name


class RemoveBrian2Filter(logging.Filter):
    """
    A class for removing the ``brian2`` prefix from log messages.
    Will be used for extension packages that use the Brian logging system.
    """

    def filter(self, record):
        assert record.name.startswith("brian2."), record.name
        record.name = record.name[7:]
        return True


class BrianLogger:
    """
    Convenience object for logging. Call `get_logger` to get an instance of
    this class.

    Parameters
    ----------
    name : str
        The name used for logging, normally the name of the module. If the name
        does not start with ``brian2.``, it will be prepended automatically when
        interacting with the `logging` module. This means that from the logging's
        system point of view, it will use the configuration for the logger in the
        ``brian2`` hierachy. However, when displaying the name in the log messages,
        the ``brian2.`` prefix will be removed. This is useful for extension
        modules, which can use the Brian logging system, but will be displayed as
        ``myextension`` instead of ``brian2.myextension``. This is also used in
        Brian's test suite, which only considers log messages starting with
        ``brian2``.
    """

    #: Global flag to know whether the logging system is in a usable state
    initialized = False

    #: Class attribute to remember whether any exception occured
    exception_occured = False

    #: Class attribute for remembering log messages that should only be
    #: displayed once
    _log_messages = set()

    #: The name of the temporary log file (by default deleted after the run if
    #: no exception occurred), if any
    tmp_log = None

    #: The `logging.FileHandler` responsible for logging to the temporary log
    #: file
    file_handler = None

    #: The `logging.StreamHandler` responsible for logging to the console
    console_handler = None

    #: The name of the temporary copy of the main script file (by default
    #: deleted after the run if no exception occurred), if any
    tmp_script = None

    #: The pid of the process that initialized the logger – used to switch off file logging in
    #: multiprocessing contexts
    _pid = None

    def __init__(self, name):
        if not name.startswith("brian2."):
            self.name = "brian2." + name
            self.filter_name = True
        else:
            self.name = name
            self.filter_name = False

    def _log(self, log_level, msg, name_suffix, once):
        """
        Log an entry.

        Parameters
        ----------
        log_level : {'debug', 'info', 'warn', 'error'}
            The level with which to log the message.
        msg : str
            The log message.
        name_suffix : str
            A suffix that will be added to the logger name.
        once : bool
            Whether to suppress identical messages if they are logged again.
        """
        if not BrianLogger.initialized:
            # Prevent logging errors on exit
            return
        name = self.name
        if name_suffix:
            name += f".{name_suffix}"

        # Switch off file logging when using multiprocessing
        if BrianLogger.tmp_log is not None and BrianLogger._pid != os.getpid():
            BrianLogger.tmp_log = None
            logging.getLogger("brian2").removeHandler(BrianLogger.file_handler)
            BrianLogger.file_handler = None

        if once:
            # Check whether this exact message has already been displayed
            log_tuple = (name, log_level, msg)
            if log_tuple in BrianLogger._log_messages:
                return
            else:
                BrianLogger._log_messages.add(log_tuple)

        the_logger = logging.getLogger(name)
        if self.filter_name:
            filter = RemoveBrian2Filter()
            the_logger.addFilter(filter)
        the_logger.log(LOG_LEVELS[log_level], msg)
        if self.filter_name:
            the_logger.removeFilter(filter)

    def diagnostic(self, msg, name_suffix=None, once=False):
        """
        Log a diagnostic message.

        Parameters
        ----------
        msg : str
            The message to log.
        name_suffix : str, optional
            A suffix to add to the name, e.g. a class or function name.
        once : bool, optional
            Whether this message should be logged only once and not repeated
            if sent another time.
        """
        self._log("DIAGNOSTIC", msg, name_suffix, once)

    def debug(self, msg, name_suffix=None, once=False):
        """
        Log a debug message.

        Parameters
        ----------
        msg : str
            The message to log.
        name_suffix : str, optional
            A suffix to add to the name, e.g. a class or function name.
        once : bool, optional
            Whether this message should be logged only once and not repeated
            if sent another time.
        """
        self._log("DEBUG", msg, name_suffix, once)

    def info(self, msg, name_suffix=None, once=False):
        """
        Log an info message.

        Parameters
        ----------
        msg : str
            The message to log.
        name_suffix : str, optional
            A suffix to add to the name, e.g. a class or function name.
        once : bool, optional
            Whether this message should be logged only once and not repeated
            if sent another time.
        """
        self._log("INFO", msg, name_suffix, once)

    def warn(self, msg, name_suffix=None, once=False):
        """
        Log a warn message.

        Parameters
        ----------
        msg : str
            The message to log.
        name_suffix : str, optional
            A suffix to add to the name, e.g. a class or function name.
        once : bool, optional
            Whether this message should be logged only once and not repeated
            if sent another time.
        """
        self._log("WARNING", msg, name_suffix, once)

    warning = warn

    def error(self, msg, name_suffix=None, once=False):
        """
        Log an error message.

        Parameters
        ----------
        msg : str
            The message to log.
        name_suffix : str, optional
            A suffix to add to the name, e.g. a class or function name.
        once : bool, optional
            Whether this message should be logged only once and not repeated
            if sent another time.
        """
        self._log("ERROR", msg, name_suffix, once)

    @staticmethod
    def _suppress(filterobj, filter_log_file):
        """
        Apply a filter object to log messages.

        Parameters
        ----------
        filterobj : `logging.Filter`
            A filter object to apply to log messages.
        filter_log_file : bool
            Whether the filter also applies to log messages in the log file.
        """
        BrianLogger.console_handler.addFilter(filterobj)

        if filter_log_file:
            BrianLogger.file_handler.addFilter(filterobj)

    @staticmethod
    def suppress_hierarchy(name, filter_log_file=False):
        """
        Suppress all log messages in a given hiearchy.

        Parameters
        ----------
        name : str
            Suppress all log messages in the given `name` hierarchy. For
            example, specifying ``'brian2'`` suppresses all messages logged
            by Brian, specifying ``'brian2.codegen'`` suppresses all messages
            generated by the code generation modules.
        filter_log_file : bool, optional
            Whether to suppress the messages also in the log file. Defaults to
            ``False`` meaning that suppressed messages are not displayed on
            the console but are still saved to the log file.
        """

        suppress_filter = HierarchyFilter(name)

        BrianLogger._suppress(suppress_filter, filter_log_file)

    @staticmethod
    def suppress_name(name, filter_log_file=False):
        """
        Suppress all log messages with a given name.

        Parameters
        ----------
        name : str
            Suppress all log messages ending in the given `name`. For
            example, specifying ``'resolution_conflict'`` would suppress
            messages with names such as
            ``brian2.equations.codestrings.CodeString.resolution_conflict`` or
            ``brian2.equations.equations.Equations.resolution_conflict``.
        filter_log_file : bool, optional
            Whether to suppress the messages also in the log file. Defaults to
            ``False`` meaning that suppressed messages are not displayed on
            the console but are still saved to the log file.
        """
        suppress_filter = NameFilter(name)

        BrianLogger._suppress(suppress_filter, filter_log_file)

    @staticmethod
    def log_level_diagnostic():
        """
        Set the log level to "diagnostic".
        """
        BrianLogger.console_handler.setLevel(DIAGNOSTIC)

    @staticmethod
    def log_level_debug():
        """
        Set the log level to "debug".
        """
        BrianLogger.console_handler.setLevel(logging.DEBUG)

    @staticmethod
    def log_level_info():
        """
        Set the log level to "info".
        """
        BrianLogger.console_handler.setLevel(logging.INFO)

    @staticmethod
    def log_level_warn():
        """
        Set the log level to "warn".
        """
        BrianLogger.console_handler.setLevel(logging.WARN)

    @staticmethod
    def log_level_error():
        """
        Set the log level to "error".
        """
        BrianLogger.console_handler.setLevel(logging.ERROR)

    @staticmethod
    def initialize():
        """
        Initialize Brian's logging system. This function will be called
        automatically when Brian is imported.
        """
        # get the main logger
        logger = logging.getLogger("brian2")
        logger.propagate = False
        logger.setLevel(LOG_LEVELS["DIAGNOSTIC"])

        # Log to a file
        if prefs["logging.file_log"]:
            try:
                # Temporary filename used for logging
                with tempfile.NamedTemporaryFile(
                    prefix="brian_debug_", suffix=".log", delete=False
                ) as tmp_f:
                    BrianLogger.tmp_log = tmp_f.name
                # Remove any previously existing file handler
                if BrianLogger.file_handler is not None:
                    BrianLogger.file_handler.close()
                    logger.removeHandler(BrianLogger.file_handler)
                # Rotate log file after prefs['logging.file_log_max_size'] bytes and keep one copy
                BrianLogger.file_handler = RotatingFileHandler(
                    BrianLogger.tmp_log,
                    mode="a",
                    maxBytes=prefs["logging.file_log_max_size"],
                    backupCount=1,
                    encoding="utf-8",
                )
                BrianLogger.file_handler.setLevel(
                    LOG_LEVELS[prefs["logging.file_log_level"].upper()]
                )
                BrianLogger.file_handler.setFormatter(
                    logging.Formatter(
                        "%(asctime)s %(levelname)-10s %(name)s: %(message)s"
                    )
                )
                logger.addHandler(BrianLogger.file_handler)
                BrianLogger._pid = os.getpid()
            except OSError as ex:
                warn(f"Could not create log file: {ex}")

        # Save a copy of the script
        BrianLogger.tmp_script = None
        if prefs["logging.save_script"]:
            if (
                len(sys.argv[0])
                and not running_from_ipython()
                and os.path.isfile(sys.argv[0])
            ):
                try:
                    tmp_file = tempfile.NamedTemporaryFile(
                        prefix="brian_script_", suffix=".py", delete=False
                    )
                    with tmp_file:
                        # Timestamp
                        tmp_file.write(_encode(f"# {time.asctime()}\n"))
                        # Command line arguments
                        tmp_file.write(_encode(f"# Run as: {' '.join(sys.argv)}\n\n"))
                        # The actual script file
                        # TODO: We are copying the script file as it is, this might clash
                        # with the encoding we used for the comments added above
                        with open(os.path.abspath(sys.argv[0]), "rb") as script_file:
                            shutil.copyfileobj(script_file, tmp_file)
                        BrianLogger.tmp_script = tmp_file.name
                except OSError as ex:
                    warn(f"Could not copy script file to temp directory: {ex}")

        if BrianLogger.console_handler is not None:
            logger.removeHandler(BrianLogger.console_handler)

        # create console handler with a higher log level
        BrianLogger.console_handler = logging.StreamHandler()
        BrianLogger.console_handler.setLevel(
            LOG_LEVELS[prefs["logging.console_log_level"]]
        )
        BrianLogger.console_handler.setFormatter(
            logging.Formatter("%(levelname)-10s %(message)s [%(name)s]")
        )

        # add the handler to the logger
        logger.addHandler(BrianLogger.console_handler)

        # We want to log all warnings
        logging.captureWarnings(True)  # pylint: disable=E1101
        # Manually connect to the warnings logger so that the warnings end up in
        # the log file. Note that connecting to the console handler here means
        # duplicated warning messages in the ipython notebook, but not doing so
        # would mean that they are not displayed at all in the standard ipython
        # interface...
        warn_logger = logging.getLogger("py.warnings")
        warn_logger.addHandler(BrianLogger.console_handler)

        # Put some standard info into the log file
        logger.log(
            logging.DEBUG,
            f"Logging to file: {BrianLogger.tmp_log}, copy of main script saved as:"
            f" {BrianLogger.tmp_script}",
        )
        logger.log(logging.DEBUG, f"Python interpreter: {sys.executable}")
        logger.log(logging.DEBUG, f"Platform: {sys.platform}")
        version_infos = {
            "brian": brian2.__version__,
            "numpy": numpy.__version__,
            "scipy": scipy.__version__ if scipy else "not installed",
            "sympy": sympy.__version__,
            "python": sys.version,
        }
        for _name, _version in version_infos.items():
            logger.log(logging.DEBUG, f"{_name} version is: {str(_version)}")
        # Handle uncaught exceptions
        sys.excepthook = brian_excepthook

        BrianLogger.initialized = True


def get_logger(module_name="brian2"):
    """
    Get an object that can be used for logging.

    Parameters
    ----------
    module_name : str
        The name used for logging, should normally be the module name as
        returned by ``__name__``.

    Returns
    -------
    logger : `BrianLogger`
    """

    return BrianLogger(module_name)


class catch_logs:
    """
    A context manager for catching log messages. Use this for testing the
    messages that are logged. Defaults to catching warning/error messages and
    this is probably the only real use case for testing. Note that while this
    context manager is active, *all* log messages are suppressed. Using this
    context manager returns a list of (log level, name, message) tuples.

    Parameters
    ----------
    log_level : int or str, optional
        The log level above which messages are caught.
    only_from : list, optional
        A list of module names from which messages are caught. Defaults to
        the ``brian2`` module.
    Examples
    --------
    >>> logger = get_logger('brian2.logtest')
    >>> logger.warn('An uncaught warning') # doctest: +SKIP
    WARNING  brian2.logtest: An uncaught warning
    >>> with catch_logs() as l:
    ...    logger.warn('a caught warning')
    ...    print('l contains: %s' % l)
    ...
    l contains: [('WARNING', 'brian2.logtest', 'a caught warning')]

    """

    _entered = False

    def __init__(self, log_level=logging.WARN, only_from=("brian2",)):
        self.log_list = []
        self.handler = LogCapture(self.log_list, log_level, only_from=only_from)
        self._entered = False

    def __enter__(self):
        if self._entered:
            raise RuntimeError(f"Cannot enter {self!r} twice")
        self._entered = True
        return self.log_list

    def __exit__(self, *exc_info):
        if not self._entered:
            raise RuntimeError(f"Cannot exit {self!r} without entering first")
        self.handler.uninstall()


class LogCapture(logging.Handler):
    """
    A class for capturing log warnings. This class is used by
    `~brian2.utils.logger.catch_logs` to allow testing in a similar
    way as with `warnings.catch_warnings`.
    """

    def __init__(self, log_list, log_level=logging.WARN, only_from=("brian2",)):
        logging.Handler.__init__(self, level=log_level)
        self.log_list = log_list
        self.only_from = only_from
        # make a copy of the previous handlers
        self.handlers = list(logging.getLogger("brian2").handlers)
        self.install()

    def emit(self, record):
        # Append a tuple consisting of (level, name, msg) to the list of
        # log messages
        if any(
            record.name == name or record.name.startswith(name + ".")
            for name in self.only_from
        ):
            self.log_list.append((record.levelname, record.name, record.msg))

    def install(self):
        """
        Install this handler to catch all warnings. Temporarily disconnect all
        other handlers.
        """
        the_logger = logging.getLogger("brian2")
        for handler in self.handlers:
            the_logger.removeHandler(handler)
        the_logger.addHandler(self)

    def uninstall(self):
        """
        Uninstall this handler and re-connect the previously installed
        handlers.
        """
        the_logger = logging.getLogger("brian2")
        for handler in self.handlers:
            the_logger.addHandler(handler)


# See http://stackoverflow.com/questions/26126160/redirecting-standard-out-in-err-back-after-os-dup2
# for an explanation of how this function works. Note that 1 and 2 are the file
# numbers for stdout and stderr
class std_silent:
    """
    Context manager that temporarily silences stdout and stderr but keeps the
    output saved in a temporary file and writes it if an exception is raised.
    """

    dest_stdout = None
    dest_stderr = None

    def __init__(self, alwaysprint=False):
        self.alwaysprint = alwaysprint or not prefs["logging.std_redirection"]
        self.redirect_to_file = prefs["logging.std_redirection_to_file"]
        if (
            not self.alwaysprint
            and self.redirect_to_file
            and std_silent.dest_stdout is None
        ):
            std_silent.dest_fname_stdout = tempfile.NamedTemporaryFile(
                prefix="brian_stdout_", suffix=".log", delete=False
            ).name
            std_silent.dest_fname_stderr = tempfile.NamedTemporaryFile(
                prefix="brian_stderr_", suffix=".log", delete=False
            ).name
            std_silent.dest_stdout = open(std_silent.dest_fname_stdout, "w")
            std_silent.dest_stderr = open(std_silent.dest_fname_stderr, "w")

    def __enter__(self):
        if not self.alwaysprint and self.redirect_to_file:
            sys.stdout.flush()
            sys.stderr.flush()
            self.orig_out_fd = os.dup(1)
            self.orig_err_fd = os.dup(2)
            os.dup2(std_silent.dest_stdout.fileno(), 1)
            os.dup2(std_silent.dest_stderr.fileno(), 2)

    def __exit__(self, exc_type, exc_value, traceback):
        if not self.alwaysprint and self.redirect_to_file:
            std_silent.dest_stdout.flush()
            std_silent.dest_stderr.flush()
            if exc_type is not None:
                with open(std_silent.dest_fname_stdout) as f:
                    out = f.read()
                with open(std_silent.dest_fname_stderr) as f:
                    err = f.read()
            os.dup2(self.orig_out_fd, 1)
            os.dup2(self.orig_err_fd, 2)
            os.close(self.orig_out_fd)
            os.close(self.orig_err_fd)
            if exc_type is not None:
                sys.stdout.write(out)
                sys.stderr.write(err)

    @classmethod
    def close(cls):
        if std_silent.dest_stdout is not None:
            std_silent.dest_stdout.close()
            if prefs["logging.delete_log_on_exit"]:
                try:
                    os.remove(std_silent.dest_fname_stdout)
                except OSError:
                    # TODO: this happens quite frequently - why?
                    # The file objects are closed as far as Python is concerned,
                    # but maybe Windows is still hanging on to them?
                    pass
        if std_silent.dest_stderr is not None:
            std_silent.dest_stderr.close()
            if prefs["logging.delete_log_on_exit"]:
                try:
                    os.remove(std_silent.dest_fname_stderr)
                except OSError:
                    pass
