"""
Functions and classes to record and print out log messages.

This module provides a own logger class as well as specific functions to
improve Pythons logging facility.

wxGlade uses the python logging instance with three log handler attached.

The first handler L{StringHandler} is used to cache messages later
displaying calling getBufferAsList() or getBufferAsString().

The second handler C{logging.StreamHandler} to print error messages to
sys.stderr.

The third handler C{logging.FileHandler} writes all messages into a file. This
behaviour is useful to store logged exceptions permanently.

@note: Python versions older then 2.6.6 (released 24th August 2010) contains
logging implementation that are not Unicode aware.
The Python bug report #8924 contains the details. A fix has been committed
in revision 81919 (27.12.2010) in the public Python repository.

@todo: Integrate Unicode logging fix.

@copyright: 2013-2014 Carsten Grohmann
@license: MIT (see license.txt) - THIS PROGRAM COMES WITH NO WARRANTY
"""

import atexit
import StringIO
import datetime
import inspect
import locale
import logging
import logging.handlers
import os
import pprint
import sys
import types

import config


stringLoggerInstance = None
"""\
Reference to the active StringHandler instance
"""

_orig_exec_handler = None
"""\
Contains the original exception handler

@see: L{installExceptionHandler()}
@see: L{deinstallExceptionHandler()}
"""


class StringHandler(logging.handlers.MemoryHandler):
    """\
    Stores the log records as a list of strings.

    @ivar buffer: The message buffer itself
    @type buffer: list[str]
    """

    storeAsUnicode = True
    """\
    Stores the log records as unicode strings

    @type: bool
    """

    encoding = sys.stdout.encoding or sys.getfilesystemencoding()
    """\
    Encoding of all character strings

    The default encoding is used to convert character strings into unicode
    strings.

    @see: L{storeAsUnicode}
    """

    def __init__(self, storeAsUnicode=True):
        """\
        Constructor

        @param storeAsUnicode: Store recorded log records as unicode strings
        """
        self.buffer = []
        logging.handlers.MemoryHandler.__init__(self, sys.maxint, 99)
        self.storeAsUnicode = storeAsUnicode

    def _toUnicode(self, msg):
        """\
        Convert a non unicode string into a unicode string

        @return: Unicode string
        @see: L{self.encoding}
        """
        # return msg if is already a unicode string or None
        if type(msg) in [types.UnicodeType, types.NoneType]:
            return msg

        # convert character string into a unicode string
        if not isinstance(msg, unicode):
            msg = msg.decode(self.encoding, 'replace')
        return msg

    def getBufferAsList(self, clean=True):
        """\
        Returns all buffered messages

        @param clean: Clean the internal message buffer
        @return:      Message buffer
        @rtype:       list[str]

        @see: L{getBufferAsString()}
        """
        self.acquire()
        try:
            messages = self.buffer[:]
            if clean:
                self.flush()
        finally:
            self.release()
        return messages

    def getBufferAsString(self, clean=True):
        """\
        Returns all buffered messages

        @param clean: Clean the internal message buffer
        @return:      Concatenated messages
        @rtype:       String

        @see: L{getBufferAsList()}
        """
        msg_list = self.getBufferAsList(clean)
        if self.storeAsUnicode:
            return u'\n'.join(msg_list)
        return '\n'.join(msg_list)

    def emit(self, record):
        """\
        Emit a record.

        Add a formatted log record to the buffer.
        """
        msg = self.format(record)
        if self.storeAsUnicode:
            msg = self._toUnicode(msg)
        self.buffer.append(msg)
        if self.shouldFlush(record):
            self.flush()

    def flush(self):
        """\
        Empty the buffer
        """
        self.buffer = []

# end of class StringHandler


class ExceptionFormatter(logging.Formatter):
    """\
    Extended formatter to include more exception details automatically.
    """

    def formatException(self, ei):
        """
        Returns a detailed exception

        @param ei: Tuple or list of exc_type, exc_value, exc_tb
        @return: Formatted exception
        @rtype:  String
        """
        context = None
        exc_tb = ei[2]
        exc_type = ei[0]
        exc_value = ei[1]
        filename = None
        frame = None
        func_args = None
        func_name = None
        index = None
        lineno = None
        loc_langcode = None
        loc_encoding = None
        loc_filesystem = None
        sio = StringIO.StringIO()
        stack_level = 0
        stack_list = []
        var = None
        var_name = None
        var_type = None
        var_value = None
        try:
            try:
                # log exception details
                now = datetime.datetime.now().isoformat()
                py_version = getattr(config, 'py_version', 'not found')
                wx_version = getattr(config, 'wx_version', 'not found')
                platform = getattr(config, 'platform', 'not found')
                app_version = getattr(config, 'version', 'not found')
                loc_langcode, loc_encoding = locale.getlocale()
                loc_filesystem = sys.getfilesystemencoding()

                sio.write('An unexpected error occurred!\n')
                sio.write('\n')
                sio.write('Date and time:       %s\n' % now)
                sio.write('Python version:      %s\n' % py_version)
                sio.write('wxPython version:    %s\n' % wx_version)
                sio.write('wxWidgets platform:  %s\n' % platform)
                sio.write('wxGlade version:     %s\n' % app_version)
                sio.write('Language code:       %s\n' % loc_langcode)
                sio.write('Encoding:            %s\n' % loc_encoding)
                sio.write('Filesystem encoding: %s\n' % loc_filesystem)
                sio.write('\n')
                sio.write('Exception type:      %s\n' % exc_type)
                sio.write('Exception details:   %s\n' % exc_value)
                sio.write('Application stack trace:\n')

                # leave the exception handler if no traceback is available
                if not exc_tb:
                    return

                # get stack frames
                stack_list = inspect.getinnerframes(exc_tb, 7)
                stack_list.reverse()
                stack_level = -1

                for frame, filename, lineno, func_name, context, index in \
                        stack_list:

                    stack_level += 1
                    try:
                        func_args = inspect.formatargvalues(
                            *inspect.getargvalues(frame)
                        )
                    except:
                        # sometimes frames contains non-printable values:
                        # e.g. TypeError: __repr__ returned non-string
                        # (type NoneType)
                        func_args = '(<unknown arguments>)'

                    msg = 'Stack frame at level %d' % stack_level
                    sio.write('%s\n' % msg)
                    msg = '=' * len(msg)
                    sio.write('%s\n' % msg)
                    sio.write('  File "%s", line %d\n' % (filename, lineno))
                    sio.write('  Function "%s%s"\n' % (func_name, func_args))
                    sio.write('  Source code context:\n')

                    pos = 0
                    for line in context:
                        line = line.rstrip()
                        if pos == index:
                            sio.write('  ->  %s\n' % line)
                        else:
                            sio.write('      %s\n' % line)
                        pos += 1

                    if frame.f_locals:
                        sio.write('  Local variables:\n')
                        for var_name in frame.f_locals:
                            # convert name and value to ascii characters
                            var = frame.f_locals[var_name]
                            var_type = type(var)
                            if isinstance(var_type, types.UnicodeType):
                                var_value = frame.f_locals[var_name]
                                var_value = var_value.encode('unicode_escape')
                            elif isinstance(var_type, types.StringType):
                                var_value = frame.f_locals[var_name]
                                var_value = var_value.encode('string-escape')
                            else:
                                try:
                                    var_value = pprint.pformat(
                                        frame.f_locals[var_name])
                                    var_value = var_value.encode(
                                        'ascii', 'replace')
                                except:
                                    var_value = '<unknown content>'
                            sio.write('  -> %s (%s): %s\n' % (
                                var_name, var_type, var_value)
                            )
                    else:
                        sio.write('  No local variables\n')
                    sio.write('\n')
            except Exception, e:
                # This code should NEVER be executed!
                logging.error('An exception has been raised inside the '
                              'exception handler: %s', e)
                sys.exit(1)

        # delete local references of trace backs or part of them  to avoid
        # circular references
        finally:
            del context
            del ei
            del exc_tb
            del exc_type,
            del exc_value,
            del filename
            del frame
            del func_args
            del func_name
            del index
            del lineno
            del loc_encoding
            del loc_filesystem
            del loc_langcode
            del stack_level
            del stack_list
            del var
            del var_name
            del var_type
            del var_value

        s = sio.getvalue()
        sio.close()
        if s[-1] == "\n":
            s = s[:-1]
        return s

# end of class ExceptionFormatter


def init(filename='wxglade.log', encoding='utf-8', level=None):
    """\
    Initialise the logging facility

    Initialise and configure the logging itself as well as the handlers
    described above.

    Our own exception handler will be installed finally.

    The file logger won't be instantiate if not file name is given.

    @param filename: Name of the log file
    @type filename:  str
    @param encoding: Encoding of the log file
    @type encoding:  str
    @param level:    Verbosity of messages written in log file e.g. "INFO"
    @type level:     str

    @see: L{StringHandler}
    @see: L{stringLoggerInstance}
    @see: L{installExceptionHandler()}
    """
    default_formatter = ExceptionFormatter('%(levelname)-8s: %(message)s')
    file_formatter = ExceptionFormatter(
        '%(asctime)s %(name)s %(levelname)s: %(message)s'
        )
    logger = logging.getLogger()

    # check for installed handlers and remove them
    for handler in logger.handlers[:]:
        logger.removeHandler(handler)

    # set newline sequence
    if os.name == 'nt':
        logging.StreamHandler.terminator = '\r\n'
    elif os.name == 'mac':
        logging.StreamHandler.terminator = '\r'
    else:
        logging.StreamHandler.terminator = '\n'

    # instantiate console handler
    console_logger = logging.StreamHandler()
    console_logger.setLevel(logging.INFO)
    console_logger.setFormatter(default_formatter)
    logger.addHandler(console_logger)

    # instantiate file handler
    if filename:
        file_logger = logging.handlers.RotatingFileHandler(
            filename,
            maxBytes=100 * 1024,
            encoding=encoding,
            backupCount=1,
            )
        file_logger.setFormatter(file_formatter)
        file_logger.setLevel(logging.NOTSET)
        logger.addHandler(file_logger)

    # instantiate string handler
    string_logger = StringHandler(storeAsUnicode=False)
    string_logger.setLevel(logging.WARNING)
    string_logger.setFormatter(default_formatter)
    logger.addHandler(string_logger)

    # store string logger globally
    global stringLoggerInstance
    stringLoggerInstance = string_logger

    # don't filter log levels in root logger
    logger.setLevel(logging.NOTSET)

    # Set log level for root logger only
    if level:
        if level.upper() in logging._levelNames:                 # pylint: disable=W0212
            logger.setLevel(logging._levelNames[level.upper()])  # pylint: disable=W0212
        else:
            logging.warning(
                _('Invalid log level "%s". Use "WARNING" instead.'),
                level.upper(),
                )
            logger.setLevel(logging.WARNING)
    else:
        logger.setLevel(logging.NOTSET)

    # Install own exception handler at the end because it can raise a debug
    # messages. This debug messages triggers the creation of a default
    # StreamHandler if no log handler exists.
    # There is a period of time with no log handler during this log
    # initialisation.
    installExceptionHandler()


def deinit():
    """\
    Reactivate system exception handler

    @see: L{deinstallExceptionHandler()}
    """
    deinstallExceptionHandler()
    if deinit in atexit._exithandlers:
        atexit._exithandlers.remove(deinit)


def setDebugLevel():
    """\
    Set the log level to DEBUG for all log handlers
    """
    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)


def getBufferAsList(clean=True):
    """\
    Returns all messages buffered by L{stringLoggerInstance}.

    @param clean: Clean the internal message buffer
    @return:      Message buffer
    @rtype:       list[str]

    @see: L{StringHandler.getBufferAsList()}
    @see: L{stringLoggerInstance}
    """
    return stringLoggerInstance.getBufferAsList(clean)


def getBufferAsString(clean=True):
    """\
    Returns all messages buffered by L{stringLoggerInstance}.

    @param clean: Clean the internal message buffer
    @return:      Concatenated messages
    @rtype:       String

    @see: L{StringHandler.getBufferAsString()}
    @see: L{stringLoggerInstance}
    """
    return stringLoggerInstance.getBufferAsString(clean)


def flush():
    """\
    Empty the buffer of the L{stringLoggerInstance}.

    @see: L{StringHandler.flush()}
    @see: L{stringLoggerInstance}
    """
    stringLoggerInstance.flush()


def installExceptionHandler():
    """\
    Install own exception handler

    The original exception handler is saved in L{_orig_exec_handler}.

    @see: L{_orig_exec_handler}
    @see: L{deinstallExceptionHandler()}
    """
    global _orig_exec_handler
    if _orig_exec_handler:
        logging.debug(
            _('The exception handler has been installed already.'),
            )
        return

    _orig_exec_handler = sys.excepthook
    sys.excepthook = exceptionHandler


def deinstallExceptionHandler():
    """\
   Restore the original exception handler

   The original exception handler has been saved in L{_orig_exec_handler}.

   @see: L{_orig_exec_handler}
   @see: L{installExceptionHandler()}
    """
    global _orig_exec_handler
    if not _orig_exec_handler:
        logging.debug(
            _('The exception handler has not been installed.'
              'Thereby it can not be deinstalled.'),
            )
        return

    sys.excepthook = _orig_exec_handler
    _orig_exec_handler = None


def exceptionHandler(exc_type, exc_value, exc_tb):
    """\
    Log detailed information about uncaught exceptions

    @param exc_type:  Type of the exception (normally a class object)
    @param exc_value: The "value" of the exception
    @param exc_tb:    Call stack of the exception
    """
    logging.error(
        _("An unhandled exception occurred"),
        exc_info=(exc_type, exc_value, exc_tb)
    )


def getMessage(self):
    """\
    Return the message for this LogRecord.

    Return the message for this LogRecord after merging any user-supplied
    arguments with the message.

    This specific version tries to handle Unicode user-supplied arguments.
    """
    if not hasattr(types, "UnicodeType"):  # if no unicode support...
        msg = str(self.msg)
    else:
        msg = self.msg
        if type(msg) not in (types.UnicodeType, types.StringType):
            try:
                msg = str(self.msg)
            except UnicodeError:
                msg = self.msg      # Defer encoding till later
    if self.args:
        try:
            msg = msg % self.args
        except UnicodeError:
            # TODO it's still an hack :-/
            logging.error(_('Unknown format of arguments: %s'), self.args)
        except TypeError:
            # Errors caused by wrong message formatting
            logging.exception(_('Wrong format of a log message'))
    return msg

# inject own (improved) function
logging.LogRecord.getMessage = getMessage
