# 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.

import logging
log = logging.getLogger(__name__)

import gtk
import re

from time import strftime, localtime

from kiwi.ui.delegates import Delegate
from kiwi.ui.widgets.combo import ProxyComboBox

from lottanzb import hellalog
from lottanzb.core import App
from lottanzb.util import GObject, gsignal, _

class RecordSource(object):
    BOTH, LOTTA, HELLA = range(3)

class LogStore(GObject):
    """This class is used to store a list of logging.LogRecord instances (or
    subclasses of it). The records stored here have an attribute 'source',
    which is either RecordSource.LOTTA or RecordSource.HELLA.
    """
    
    gsignal("record-added", object)
    
    def __init__(self, records=None):
        self.records = records or []
        
        GObject.__init__(self)
    
    def add_record(self, record, source):
        record = self.make_log_record(record, source)
        
        self.records.append(record)
        self.emit("record-added", record)
    
    def get_record_of_type(self, cls):
        for record in self.records:
            if isinstance(record, cls):
                return record
    
    def get_records_by_source(self, source):
        return [record for record in self.records if record.source is source]
    
    @staticmethod
    def make_log_record(record, source):
        if source == RecordSource.HELLA:
            record = hellalog.look_up(record["message"], record["level"])
        
        setattr(record, "source", source)
        
        return record

class _HellaLogParser(object):
    @staticmethod
    def parse_str_level(level):
        return getattr(logging, level, logging.INFO)
    
class _HellaXMLRPCLogParser(_HellaLogParser):
    """Generates logging.LogRecords from log messages received over XML RPC.
    
    Due to the fact that HellaNZB limits the number of messages transfered over
    XML RPC, this mixin class has a quite advanced algorithm to detect which 
    of the received messages aren't part of the 'records' property yet.
    """
    
    # HellaNZB doesn't transfer more than this many log messages over XML RPC.
    LOG_LIMIT = 20
    
    def clear_hella_log(self):
        for record in self.get_records_by_source(RecordSource.HELLA):
            self.records.remove(record)
    
    def parse_hella_log(self, raw_log_entries):
        # Log messages we already have.
        existing_records = self.get_records_by_source(RecordSource.HELLA)
        
        # The messages (and their corresponding level) received by HellaNZB.
        log_entries = []
        
        # The format using which HellaNZB transfers its messages over XML RPC
        # isn't easy to work with, that's why the data structure is transformed
        # here first.
        for entry in raw_log_entries:
            for level, message in entry.iteritems():
                log_entries.append({
                    "message": message,
                    "level": self.parse_str_level(level)
                })
        
        def add_to_store(log_entries):
            for entry in log_entries:
                self.add_record(entry, RecordSource.HELLA)
        
        if existing_records:
            length_diff = len(log_entries) - len(existing_records)
            
            if len(log_entries) < self.LOG_LIMIT:
                if length_diff > 0:
                    # If the list of messages received by HellaNZB is longer
                    # than the one we already have but hasn't reached the limit
                    # yet, it's obvious which messages were newly added.
                    add_to_store(log_entries[-length_diff:])
                elif length_diff < 0:
                    # If there are less HellaNZB messages than we already have,
                    # HellaNZB must have been restarted and
                    # self.clear_hella_log() wasn't called yet. Not sure if this
                    # is necessary. Just another safety net.
                    self.clear_hella_log()
                    
                    add_to_store(log_entries)
            elif len(log_entries) == self.LOG_LIMIT:
                last_record = existing_records[-1]
                last_record_msg = last_record.orig_msg
                
                # All indices at which the last already recorded message appears
                # in the newly received log entries list. This will only contain
                # one index in common cases, but we want to handle all cases.
                # *g*
                last_msg_indices = []
                
                for index, entry in enumerate(log_entries):
                    if entry["message"] == last_record_msg:
                        last_msg_indices.append(index)
                
                # Begin with the last matching message in log_entries, because
                # this is with the utmost probability the message we already
                # have.
                last_msg_indices.reverse()
                
                if last_msg_indices:
                    for last_msg_index in last_msg_indices:
                        mismatch = False
                        
                        # Go backwards in the both lists of log messages (ours
                        # and HellaNZB's one) and check if they match.
                        for index in range(0, last_msg_index):
                            record_index = -last_msg_index + index - 1
                            msg = log_entries[index]["message"]
                            
                            if record_index < -len(existing_records) or \
                                msg != existing_records[record_index].orig_msg:
                                mismatch = True
                                break
                        
                        if not mismatch:
                            # last_msg_index is the log_entries index of the
                            # latest message we already have, as has been
                            # proved. Now add everything we don't already have
                            # to our LogStore.
                            add_to_store(log_entries[last_msg_index + 1:])
                            break
                else:
                    # This is the unprobable case when HellaNZB generates more
                    # than 20 messages during two LottaNZB update cycles.
                    add_to_store(log_entries)
        else:
            # If there is not a single HellaNZB log record in our LogStore yet,
            # we can add all messages received by HellaNZB.
            add_to_store(log_entries)

class _HellaLogFileParser(_HellaLogParser):
    """Generates logging.LogRecords from the messages in a HellaNZB log file.
    
    Since log files contain log messages from multiple sessions, this class
    just parses the most recent one, not depending whether HellaNZB has already
    been shut down or not.
    """
    
    _SESSION_PATTERN = re.compile("\n\n\n.+, exiting\.\.\n", re.M)
    _LINE_PATTERN = re.compile("^[-0-9 :,]{23} (?P<level>\S+) (?P<message>.*)$")
    
    def parse_hella_log(self, file_name):
        log_file = open(file_name, "r")
        sessions = self._SESSION_PATTERN.split(log_file.read())
        log_entries = []
        
        self.shut_down = not sessions[-1]
        
        if self.shut_down:
            sessions = sessions[:-1]
        
        if sessions:
            session_lines = sessions[-1].split("\n")
            
            for session_line in session_lines:
                match = self._LINE_PATTERN.search(session_line)
                
                if match:
                    data = match.groupdict()
                    level = data["message"]
                    message = data["message"]
                    
                    if message:
                        log_entries.append({
                            "level": self.parse_str_level(level),
                            "message": message
                        })
                elif log_entries:
                    log_entries[-1]["message"] += session_line
            
            for entry in log_entries:
                self.add_record(entry, RecordSource.HELLA)
            
        log_file.close()

class HellaLogFileStore(LogStore, _HellaLogFileParser):
    def __init__(self, file_name):
        LogStore.__init__(self)
        _HellaLogFileParser.__init__(self)
        
        self.parse_hella_log(file_name)

class HellaXMLRPCLogStore(LogStore, _HellaXMLRPCLogParser):
    def __init__(self, raw_log_entries):
        LogStore.__init__(self)
        _HellaXMLRPCLogParser.__init__(self)
        
        self.parse_hella_log(raw_log_entries)

class Log(LogStore, _HellaXMLRPCLogParser, logging.Handler):
    def __init__(self):
        LogStore.__init__(self)
        logging.Handler.__init__(self)
        _HellaXMLRPCLogParser.__init__(self)
    
    def connect_to_backend(self):
        def handle_connected_change(backend, *args):
            if backend.connected == False:
                self.clear_hella_log()
        
        def handle_updated(backend):
            self.parse_hella_log(backend.log_entries)
        
        App().backend.connect("notify::connected", handle_connected_change)
        App().backend.connect("updated", handle_updated)
    
    def emit(self, value, *args):
        if type(value) == str:
            self._emit_signal(value, *args)
        elif isinstance(value, logging.LogRecord):
            self._emit_record(value)
    
    def _emit_signal(self, name, *args):
        LogStore.emit(self, name, *args)
    
    def _emit_record(self, record):
        self.add_record(record, RecordSource.LOTTA)

class LogWindow(Delegate):
    gladefile = "log_window"
    
    def __init__(self):
        Delegate.__init__(self)
        
        # Only show debug messages by default if LottaNZB is actually executed
        # using the --debug argument.
        if App().debug:
            default_level = logging.DEBUG
        else:
            default_level = logging.INFO
        
        color = gtk.widget_get_default_colormap().alloc_color
        
        self.log = App().log
        self.textbuffer = self.content._textbuffer
        self.textbuffer.create_tag("error", foreground_gdk=color("red"))
        self.textbuffer.create_tag("debug", foreground_gdk=color("grey"))
        
        self.v_adjustment = self.scrolled_window.get_vadjustment()
        self.end_mark = self.textbuffer.create_mark("end", \
            self.textbuffer.get_end_iter(), False)
        
        self.setup_filter("level", [
            (_("Debug"), logging.DEBUG),
            (_("Info"), logging.INFO),
            (_("Warning"), logging.WARNING),
            (_("Error"), logging.ERROR)
        ], default_level)
        
        self.setup_filter("source", [
            (_("LottaNZB and HellaNZB"), RecordSource.BOTH),
            (_("LottaNZB only"), RecordSource.LOTTA),
            (_("HellaNZB only"), RecordSource.HELLA)
        ])
        
        self.show()
        self.update()
        
        self.log.connect("record-added", self.handle_log_record_added)
    
    def setup_filter(self, name, entries, preselect=None):        
        widget = ProxyComboBox()
        widget.prefill(entries)
        
        if entries:
            if preselect is None:
                preselect = entries[0][1]
            
            widget.update(preselect)
        
        widget.connect("changed", self.update)
        widget.show()
        
        self.get_widget("%s_container" % name).add(widget)
        
        setattr(self, name, widget)
    
    def get_records(self):
        level = self.level.read()
        source = self.source.read()
        store = self.log.filter_by_source(source).filter_by_level(level)
        
        return store.records
    
    def handle_log_record_added(self, log, record):
        self.display_record(record)
    
    def display_record(self, record):
        if self.source.read() == RecordSource.BOTH:
            if record.source == RecordSource.LOTTA:
                source = "LottaNZB\t"
            else:
                source = "HellaNZB\t"
        elif record.source == self.source.read():
            source = ""
        else:
            return
        
        if record.levelno < self.level.read():
            return
        
        line = strftime("%H:%M:%S", localtime(record.created)) + "\t" + \
            source + record.msg + "\n"
        
        if record.levelno >= logging.WARNING:
            tag = "error"
        elif record.levelno == logging.DEBUG:
            tag = "debug"
        else:
            tag = ""
        
        end_iter = self.textbuffer.get_end_iter()
        adjust = self.v_adjustment
        
        if tag:
            self.textbuffer.insert_with_tags_by_name(end_iter, line, tag)
        else:
            self.textbuffer.insert(end_iter, line)
        
        # Auto-scroll the log message TextView if the user has manually moved
        # the scrollbar to the bottom.
        if adjust and adjust.value and \
            adjust.value + adjust.page_size == adjust.upper:
            self.content.scroll_to_mark(self.end_mark, 0.05, True, 0.0, 1.0)
    
    def update(self, *args):
        self.content.update("")
        self.v_adjustment.set_value(0)
        
        for record in self.log.records:
            self.display_record(record)
    
    def on_save__clicked(self, widget):
        def handleResponse(dialog, response):
            if response == gtk.RESPONSE_OK:
                filename = dialog.get_filename()
                
                try:
                    logfile = open(filename, "w")
                except IOError, e:
                    logging.error(_("Could not open %s to write message log: %s") % (filename, e.strerror))
                    return
                
                try:
                    logfile.write(self.content.read())
                    logfile.close()
                except IOError, e:
                    logging.error(_("Could not write message log to %s: %s") % (filename, e.strerror))
                else:
                    logging.info(_("Message log saved to %s.") % (filename))
            
            dialog.destroy()
        
        dialog = gtk.FileChooserDialog(_("Save message log"),
            self.toplevel, gtk.FILE_CHOOSER_ACTION_SAVE,
            (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK))
        
        dialog.connect("response", handleResponse)
        dialog.set_default_response(gtk.RESPONSE_OK)
        
        dialog.show()
    
    def on_clear__clicked(self, widget):
        self.content.update("")
