# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import sys
from contextlib import contextmanager


class ErrorMessage(Exception):
    """Exception type raised from errors.error() and errors.fatal()"""


class AccumulatedErrors(Exception):
    """Exception type raised from errors.accumulate()"""


class ErrorCollector(object):
    """
    Error handling/logging class. A global instance, errors, is provided for
    convenience.

    Warnings, errors and fatal errors may be logged by calls to the following
    functions:
      - errors.warn(message)
      - errors.error(message)
      - errors.fatal(message)

    Warnings only send the message on the logging output, while errors and
    fatal errors send the message and throw an ErrorMessage exception. The
    exception, however, may be deferred. See further below.

    Errors may be ignored by calling:
      - errors.ignore_errors()

    After calling that function, only fatal errors throw an exception.

    The warnings, errors or fatal errors messages may be augmented with context
    information when a context is provided. Context is defined by a pair
    (filename, linenumber), and may be set with errors.context() used as a

    context manager:

    .. code-block:: python

        with errors.context(filename, linenumber):
            errors.warn(message)

    Arbitrary nesting is supported, both for errors.context calls:

    .. code-block:: python

        with errors.context(filename1, linenumber1):
            errors.warn(message)
            with errors.context(filename2, linenumber2):
                errors.warn(message)

    as well as for function calls:

    .. code-block:: python

        def func():
            errors.warn(message)
            with errors.context(filename, linenumber):
                func()

    Errors and fatal errors can have their exception thrown at a later time,
    allowing for several different errors to be reported at once before
    throwing. This is achieved with errors.accumulate() as a context manager:

    .. code-block:: python

        with errors.accumulate():
            if test1:
                errors.error(message1)
            if test2:
                errors.error(message2)

    In such cases, a single AccumulatedErrors exception is thrown, but doesn't
    contain information about the exceptions. The logged messages do.
    """

    out = sys.stderr
    WARN = 1
    ERROR = 2
    FATAL = 3
    _level = ERROR
    _context = []
    _count = None

    def ignore_errors(self, ignore=True):
        if ignore:
            self._level = self.FATAL
        else:
            self._level = self.ERROR

    def _full_message(self, level, msg):
        if level >= self._level:
            level = "error"
        else:
            level = "warning"
        if self._context:
            file, line = self._context[-1]
            return "%s: %s:%d: %s" % (level, file, line, msg)
        return "%s: %s" % (level, msg)

    def _handle(self, level, msg):
        msg = self._full_message(level, msg)
        if level >= self._level:
            if self._count is None:
                raise ErrorMessage(msg)
            self._count += 1
        print(msg, file=self.out)

    def fatal(self, msg):
        self._handle(self.FATAL, msg)

    def error(self, msg):
        self._handle(self.ERROR, msg)

    def warn(self, msg):
        self._handle(self.WARN, msg)

    def get_context(self):
        if self._context:
            return self._context[-1]

    @contextmanager
    def context(self, file, line):
        if file and line:
            self._context.append((file, line))
        yield
        if file and line:
            self._context.pop()

    @contextmanager
    def accumulate(self):
        assert self._count is None
        self._count = 0
        yield
        count = self._count
        self._count = None
        if count:
            raise AccumulatedErrors()

    @property
    def count(self):
        # _count can be None.
        return self._count if self._count else 0


errors = ErrorCollector()
