# Copyright (C) 2008 LottaNZB Development Team
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

"""Gives HellaNZB log messages a value beyond the simple output in a log
window. It contains rules to detect certain log messages based on their
content, extracts the information they contain and even makes them
translatable via gettext.
"""

import logging
log = logging.getLogger(__name__)

from re import compile
from gettext import ngettext

def look_up(message, level=None, classes=None):
    """Takes a raw log message and runs it through a set of detection rules.
    
    Specifying the level of the message reduces the number of detection rules
    to be used. It's also possible to directly specify the LogRecord
    subclasses to use for detection.
    
    Returns an instance of LogRecord or even better, a subclass of it, which
    contains both the extracted data and the translated message if available.
    """
    
    if not classes:
        if level in CLASSES_BY_LEVEL:
            classes = CLASSES_BY_LEVEL[level]
        else:
            classes = ALL_CLASSES
    
    level = level or logging.INFO
    
    for MessageClass in classes:
        record = MessageClass.detect(message, level)
        
        if record:
            return record
    
    # No matching message class has been found.
    return LogRecord(message, level)

class LogRecord(logging.LogRecord):
    """Represents a HellaNZB log message.
    
    This class has many subclasses all of which have a unique PATTERN and
    TEMPLATE attribute. The PATTERN attribute is a regular expression,
    which is used to check if we're dealing with the message represented by
    that class. The TEMPLATE attribute makes it possible to translate the
    message.
    """
    
    PATTERN = "^.*$"
    TEMPLATE = ""
    
    def __init__(self, orig_msg, level, data=None):
        self.orig_msg = orig_msg
        
        # It's not possible to use the native 'args' property of
        # logging.LogRecord, since the TEMPLATE is already merged with the
        # extracted data when we call the logging.LogRecord constructor.
        self.set_data(data or {})
        
        logging.LogRecord.__init__(self, None, level, "", 0, self._translate(), (), None, None)
    
    def get_message(self):
        """Just an alias of getMessage."""
        
        return self.getMessage()
    
    def set_data(self, data):
        """Stores the extracted message data as properties and as well as in
        the data dictionary.
        
        record.file_name is easier to use than record.data[0] when the
        LogRecord holds a message about the configuration file. And it makes
        it possible to rearrange the arguments in TEMPLATE if necessary.
        """
        
        for name, arg in data.iteritems():
            setattr(self, name, arg)
        
        self.data = data
    
    def _translate(self):
        """Populates TEMPLATE with the extracted data and returns the message.
        
        If no TEMPLATE is available, it just returns the original message."""
        
        if self.TEMPLATE:
            try:
                return self.TEMPLATE % self.data
            except:
                return self.orig_msg
        else:
            return self.orig_msg
    
    @classmethod
    def compile(cls):
        """Compiles the regular expression stored in PATTERN."""
        
        cls.COMPILED_PATTERN = compile(cls.PATTERN)
    
    @classmethod
    def detect(cls, message, level):
        """Checks if a message is represented by this class.
        
        This is done using the compiled regular expression in
        COMPILED_PATTERN. Returns a instance of this LogRecord subclass.
        
        Todo: I don't like the fact that the level needs to be passed to this
        method.
        """
        
        match = cls.COMPILED_PATTERN.search(message)
        
        if match:
            return cls(message, level, match.groupdict())

class _Plural:
    """Mixin class used if a LogRecord contains plural forms.
    
    The TEMPLATE_2 property contains the plural version of the message.
    This class attempts to automatically detect the number property we're
    interested in but the optional COUNT_ARG property can be used to manually
    specify it.
    """
    
    TEMPLATE_2 = ""
    COUNT_ARG = ""
    
    def _translate(self):
        if not self.COUNT_ARG:
            for name, arg in self.data.iteritems():
                try:
                    int(arg)
                except:
                    pass
                else:
                    self.COUNT_ARG = name
                    break
        
        count = int(self.data[self.COUNT_ARG])
        
        return ngettext(self.TEMPLATE, self.TEMPLATE_2, count) % self.data

class _ExceptionHandler:
    """Mixin class used to extract both the exception name and the exception
    message.
    
    HellaNZB's logging facility has an 'error' function, which optionally
    takes an exception as an argument. Both the exception name and exception
    message body is added to the actual error message.
    
    The exception name will be stored in the 'error_name' property whereas
    exception message can be found in the 'error_message' property.
    """
    
    E_NAME = r"(?P<error_name>.+)"
    E_PATTERN = r"(?P<error_message>.*)"
    E_TEMPLATE = "%(error_name)s: %(error_message)s"
    
    @classmethod
    def compile(cls):
        cls.PATTERN += r": %s('>)?: %s$" % (cls.E_NAME, cls.E_PATTERN)
        
        if cls.TEMPLATE:
            cls.TEMPLATE += ". "
        
        cls.TEMPLATE += cls.E_TEMPLATE
        cls.COMPILED_PATTERN = compile(cls.PATTERN)

class _FatalErrorHandler(_ExceptionHandler):
    """Mixin class used to detect a FatalError exception message.
    
    At several places in HellaNZB's code, FatalError exceptions are raised.
    Several of them contain information we're particularly interested in.
    This class is used to handle log messages created using the 'error'
    function with a FatalError exception passed as the second argument.
    
    The data extacted from the FatalError exception message is mixed in.
    """
    
    E_NAME = "FatalError"
    
    # We don't want 'FatalError'>' to appear in the prettified message.
    E_TEMPLATE = "%(error_message)s"
    
    def set_data(self, data):
        record = look_up(data["error_message"], classes=FATAL_ERRORS)
        
        data.update(record.data)
        data["error_message"] = record.get_message()
        data["fatal_error_class"] = record.__class__
        
        LogRecord.set_data(self, data)

from lottanzb.hellalog import core, daemon, nzbdownloader, \
    newzbindownloader, nzbqueue, postprocessor, postprocessorutil, \
    smartpar, util, nzbleecher

def _get_classes_by_level(level):
    classes = []
    
    for module in [core, daemon, nzbdownloader, newzbindownloader, nzbqueue,
        postprocessor, postprocessorutil, smartpar, util, nzbleecher,
        nzbleecher.articledecoder]:
        classes.extend([cls for name, cls in module.__dict__.iteritems() if name.endswith(level)])
    
    return classes

CLASSES_BY_LEVEL = {
    logging.INFO: _get_classes_by_level("Info"),
    logging.WARN: _get_classes_by_level("Warning"),
    logging.ERROR: _get_classes_by_level("Error")
}

ALL_CLASSES = []
FATAL_ERRORS = _get_classes_by_level("Fatal")

for classes in CLASSES_BY_LEVEL.values():
    ALL_CLASSES.extend(classes)
    
for cls in ALL_CLASSES + FATAL_ERRORS:
    cls.compile()
