# Copyright (C) 2008-2009 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
import re

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 = logging.LogRecord(None, record["level"], "", 0, \
                record["message"], (), None, None)
        
        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.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].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)
        
        App().connect("notify::backend", self._connect_to_backend)
    
    def _connect_to_backend(self, *args):
        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_async("notify::connected", handle_connected_change)
        App().backend.connect_async("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)
