#!/usr/bin/env python
# Time-stamp: <2002-01-23 19:32:21 crabbkw>
# Code and design by Casey Crabb (crabbkw@rose-hulman.edu)
# This code is licensed under the BSD license.
# See the LICENSE file for details
#
# Copyright Casey Crabb (crabbkw@rose-hulman.edu) July 2001
#

# from __future__ import nested_scopes
# from threading import *
# from IMCom import IMCom, errorCodes
from Preferences import Preferences
try:
    from xml.parsers import expat
except:
    from xml.parsers import pyexpat
    expat = pyexpat
import xml
import string
import re
#import socket
import os
import time
import sys
import getpass
import operator
import types
import signal
import traceback
import codecs
import locale
# import SocketWrapper
#try:
#    import readline
#    READLINE = 1
#except:
#    READLINE = 0

black = chr(27)+"[30;1m"
red = chr(27)+"[31;1m"
green = chr(27)+"[32;1m"
yellow = chr(27)+"[33;1m"
blue = chr(27)+"[34;1m"
purple = chr(27)+"[35;1m"
cyan = chr(27)+"[36;1m"
white = chr(27)+"[37;1m"

#
# this thing is one big hack....
# 1) xml files are contained within one large tag, so we
# simulate that by adding a <top></top> tag to the beginning
# and end of the file
#
# 2) we had a bug a while ago that made badly-formed XML files.
# Fortunately we can fix those before the parser sees them
#
# some people use buffering for performance - we use it to lie :-)
#
class FileWrapper:
    def __init__(self,file):
        self.file = file
        self.topSent = 0
        self.eof = 0
        self.leftover = "<top>\n"
        self.context = ""

    def read(self,bytes):
        if self.eof:
            return ""

        #bytes = 15

        #
        # if we have enough in the leftover buffer the use that...
        #
        if( len( self.leftover ) >= bytes ):
            out = self.leftover[:bytes]
            self.leftover = self.leftover[bytes:]
            return out

        #
        # read the max. # of bytes we could need from the file....
        #
        out = self.leftover + self.file.read( bytes - len(self.leftover) )
        self.leftover = ""

        #
        # here we fix that log format problem....
        # we use substring instead of regex because 1) we can
        # and 2) it's faster
        #
        # we search for a 2 char. substring "m'"
        # (the ' we escape before writing to the message, so that
        # guarantees that our match will be inside a tag, and nowhere
        # inside a tag should we have ' not preceded by an =)
        #
        # it will fail if the match falls on a buffer boundary. To fix
        # this we just have to keep one character of context from the
        # last buffer to temporarily prepend to our new buffer, which
        # is what we use self.context for....
        #
        out = string.replace( self.context + out, "m'", "m='" )
        # remove the context from the final output...
        out = out[len(self.context):]


        #
        # a sad and sorry excuse for a unicode hack
        # the xmlparser code apparently can't deal w/unicode
        # embedded in UTF-8 text, so we get rid of anything
        # that might confuse it. Hopefully we can take this out
        # someday....
        #
        #for x in range( len( out ) ):
        #    if( ord( out[x] ) > 127 ):
        #        out = out[:x] + " " + out[(x+1):]
        #        #print ord( out[x] )

        #
        # is out too big now?
        #
        if( len( out ) > bytes ):
            self.leftover = out[bytes:]
            out = out[:bytes]
    
        #
        # did we hit the end of the file? close the "top-level" tag
        #
        # *** NOTE ***
        # THIS IS  A CRASH WAITING TO HAPPEN
        # it's a bad idea to assume the last call to read() will ask for
        # at least 8 bytes......
        #
        if (out == ""):
            self.eof = 1
            out = "\n</top>\n"

        #
        # save our context if there is any....
        #
        if ( out != "" ):
            self.context = out[-1:]
        #print out

        return out
            
def getTerminalWidth():
    terminalWidth = 80 #linelength

class LogHandler:
    def __init__(self,cli,profile):
        if (cli == None):
            self.standalone = 1
        else:
            self.cli = cli
            self.standalone = 0

        self.messages = []
        self.JIDhash = {}
        self.NONE = 0
        self.IGNORE = 1
        self.STORE = 2
        self.expatstatus = self.NONE
    
        #
        # precompile the isInt re()
        #
        self.int_re = re.compile( r"^(\d+)$" )

        #
        # search parameters
        #
        self.mintime = -1
        self.maxtime = -1
        self.content = ""
        self.nick = ""
        self.maxmessages = 0
        self.ignorecase = 0

        #
        # our user-friendly memory
        #
        self.last_mintime = -1
        self.last_maxtime = -1
        self.last_content = "" 
        self.last_nick = "*" 
        self.last_maxmessages = 0

        #
        # the message in its pre-parsed form
        #
        self.message_time = 0
        self.message_from = ""
        self.message_message = ""
        self.current_log = ""

    def initLogHandler(self, profile):
        # saves me a bit of coding complexity
        # we'll see if this has to be fixed later....
        self.profile = profile
        self.colors = self.profile.colors.sessioncolors

    def readLogs( self, nick, mintime, maxtime, content, maxmessages, interactive ):
        if( interactive and ( mintime == None ) and ( maxtime == None ) and \
            ( content == None ) and ( maxmessages == None ) ):
            nick, mintime, maxtime, content, maxmessages = self.getArgs( \
                nick, None, None, None, None )

        if( nick == -1 ):
            #
            # data validation error. go home.
            #
            return []

        if( mintime == None ):
            mintime = -1
        if( maxtime == None ):
            maxtime = -1
        if( content == None ):
            content = "" 
        if( maxmessages == None ):
            maxmessages = 0
        if( not self.isInt( maxmessages ) ):
            self.output( self.colors["error"] + "ERROR: " +\
                self.colors["status"] + maxmessages +\
                self.colors["desc"] + ": Not a number" + \
                self.colors["default"] )
            return []

        #
        # here we parse tha arguments
        #
        self.content = content
        self.nick = nick
        self.mintime = self.parseDate( mintime )
        self.maxtime = self.parseDate( maxtime )
        self.maxmessages = int( maxmessages )
        if( ( self.mintime == -2 ) or ( self.maxtime == -2 ) ):
            return None

        self.last_mintime = self.mintime
        self.last_maxtime = self.maxtime
        self.last_content = self.content
        self.last_nick = self.nick
        self.last_maxmessages = self.maxmessages

        return self.doParsing()    

    def getArgs( self, nick, mintime, maxtime, content, maxmessages ):

        if( nick == None ):
            nick = self.last_nick
            if( nick == "*" ):
                nick = self.askuser( "Log to search", "ALL/*", 0 )
            else:
                nick = self.askuser( "Log to search", nick, 0 )
        if( nick == "ALL/*" ):
            nick = "*"
        if( nick == "" ):
            self.output( self.colors["error"] + "I need to have a nick!" \
                + self.colors["default"] )
            return -1, None, None, None, None
            
        if( mintime == None ):
            mintime = self.last_mintime
        if( mintime == -1 ):
            mintime = self.askuser( "Start time for search", "NONE/-1", 0 )
        else:
            mintime = self.askuser( "Start time for search", \
                self.getDateTime( mintime ), 0 )    

        if( ( mintime != "NONE/-1" ) ):
            tmp = self.parseDate( mintime )
            if( tmp == -2 ):
                return -1, None, None, None, None
            if( tmp == -1 ):
                self.output( "Earliest date" )
            else:
                self.output( self.getDateTime( tmp ) )
        else:
            self.output( "Earliest date" )
            mintime = -1

        if( maxtime == None ):
            maxtime = self.last_maxtime
        if( maxtime == -1 ):
            maxtime = self.askuser( "End time for search", "NOW/-1", 0 )
        else:
            maxtime = self.askuser( "End time for search", \
                self.getDateTime( maxtime ), 0 )    

        if( maxtime != "NOW/-1" ):
            tmp = self.parseDate( maxtime )
            if( tmp == -2 ):
                return -1, None, None, None, None
            self.output( self.getDateTime( tmp ) )
        else:
            self.output( "Current time" )
            maxtime = -1

        if( content == None ):
            content = self.last_content
        if( content == "" ):
            content = self.askuser( "Text to search for", "", 1 )
        else:
            content = self.askuser( "Last search text was " + \
                self.colors["status"] +\
                content + self.colors["default"] + "\n" +\
                "Text to search for", "", 1 )

        if( maxmessages == None ):
            maxmessages = self.last_maxmessages
        if( maxmessages == 0 ):
            maxmessages = self.askuser( "Number of messages to show", \
                "NO LIMIT/0", 0 )
        else:
            maxmessages = self.askuser( "Number of messages to show", \
                maxmessages, 0 )    

        if( maxmessages == "NO LIMIT/0" ):
            maxmessages = 0

        return nick, mintime, maxtime, content, maxmessages

    def getDateTime(self, timedate):
        if( timedate == -1 ):
            timedate = time.time()
        return time.strftime('%m/%d/%Y',time.localtime(timedate)) +\
            " " + time.strftime('%H:%M:%S',time.localtime(timedate))

    def askuser( self, prompt, default, emptyIsValid ):
        if( not self.standalone and hasattr( self.cli, "askuser" ) ):
            self.cli.askuser( prompt, default, emptyIsValid )
        else:
            if( emptyIsValid ):
                return raw_input( prompt + " > " ) 
            else:
                out = raw_input( prompt + " (" + str(default) + ") > " )
                if( out == "" ):
                    return default
                else:
                    return out

    def doParsing( self ):

        if( self.content != "" ):
            if( string.lower( self.content ) == self.content ):
                self.ignorecase = 1
            else:
                self.ignorecase = 0
 
                hour,min,sec = string.split( attrs[x], ":" )

        #
        # get a directory listing of the log dir...
        #
        dir = os.listdir( self.profile.logdir )

        nickstmp = string.split( self.nick, "," )
        nicks = []
        for n in nickstmp:
            if( string.find( n, "*" ) != -1 ):
                #
                # if it's a jid we only want to match jids
                # vice-versa for nicks
                #
                if( string.find( n, "@" ) == -1 ):
                    nicksonly = 1
                else:
                    nicksonly = 0

                n = string.replace( n, "*", ".*?" )
                wildcard_re = re.compile( "^" + n + "$" )

                for f in dir:
                    if( not nicksonly and string.find( f, "@" ) == -1 ):
                        continue
                    if( nicksonly and string.find( f, "@" ) != -1 ):
                        continue
                    if( wildcard_re.search( f ) != None ):
                        nicks.append( f )
            else:
                nicks.append( n )
        
        messages = []
        for n in nicks:
            try:
                self.messages = []
                f = open( self.profile.logdir + "/" + self.getJID( n ), "r")
                self.current_log = self.getJID( n )
                filewrapper = FileWrapper(f)
    
                #
                # have to init a different parser for each doc.
                #
                parser = expat.ParserCreate( "ISO-8859-1" )
                parser.StartElementHandler = self.startElement
                parser.EndElementHandler = self.endElement
                parser.CharacterDataHandler = self.characters

                parser.ParseFile(filewrapper)
                messages.extend( self.messages )
            except IOError:
                output = self.colors["error"] + "ERROR: " +\
                    self.colors["default"] + "Could not open logfile " +\
                    self.colors["desc"] + self.profile.logdir + \
                    "/" + self.getJID(self.nick) + self.colors["default"]
                self.output( output )
            except xml.parsers.expat.error:
                self.output( self.colors["error"] + "Parse error: " +\
                    self.colors["desc"] + self.current_log +\
                    ", line " + str( parser.ErrorLineNumber - 1 ) + \
                    " column ~" + str( parser.ErrorColumnNumber ) +\
                    ": " + expat.ErrorString( parser.ErrorCode ) +\
                    self.colors["default"] )

        messages.sort()
        return messages

    def startElement(self,name,attrs):
        if( name != "message" ):
            self.expatstatus = self.IGNORE
            return
        self.message_message = ""
        hour = min = sec = month = day = year = 0
        
        for x in attrs.keys():
            if( x == "time" ):
                hour,min,sec = string.split( attrs[x], ":" )
            if( x == "date" ):
                month,day,year = string.split( attrs[x], "/" )
            if( x == "from" ):
                self.message_from = attrs[x]

        year = int( year )
        month = int( month )
        day = int( day )
        hour = int( hour )
        min = int( min )
        sec = int( sec )
        self.message_time = time.mktime( [year, month, day, hour, \
            min, sec, 0, 0, -1] )

        if( ( self.mintime != -1 ) and ( self.message_time <= self.mintime ) ):
            self.expatstatus = self.IGNORE
            return

        if( ( self.maxtime != -1 ) and ( self.message_time >= self.maxtime ) ):
            self.expatstatus = self.IGNORE
            return

    def parseDate( self, text ):
        #
        # ok, so this'll get ugly :-)
        #    
        if( type( text ) == types.IntType ):
            return int( text )

        if( type( text ) == types.FloatType ):
            return int( text )

        #int_re = re.compile( r"^[+-]{0,1}(\d+)$" )
        int_re = re.compile( r"^(\d+)$" )
        if( int_re.search( str( text ) ) != None ):
            return int( text )

        if( text == "-1" ):
            return int( text )

        
        # first, fields *must* be seperated by whitespace...
        vals = string.split( text, " " )
        if( len( vals ) > 2 ):
            self.output(  self.colors["status"] + text + \
                self.colors["default"] + ": " + self.colors["desc"] + \
                "I can't understand more than two parts!" +\
                self.colors["default"] )
            return -2

        ttup = time.localtime( time.time() )    
        mytime = time.mktime( [ ttup[0], ttup[1], 
            ttup[2], 0, 0, 0, 
            -1, -1, -1 ] )
        seperator_re = re.compile( r"([;:/.-])" )
        #int_re = re.compile( r"^(\d+)$" )
        for v in vals:
            # if there's 3 fields...
            # both dates and times can have 3 fields. tsk....
            # however, if the first or third field has 4 chars. it's a date
            # those formats are: yyyy-mm-dd and mm-dd-yyyy (american format)
            # if the 2nd field has 4 chars. we can't understand the date
            #
            seperator = seperator_re.search( v )
            #if( seperator == None ):
            #    self.output(  self.colors["status"] + v + \
            #        self.colors["default"] + ": " + self.colors["desc"] + \
            #       "I can't find a seperator!" +\
            #        self.colors["default"] )
            #    return -2
            if( seperator == None or v[0] == "-" ):
                fields = [ v ]
                fields_s = [ v ]
            else:
                split_re = re.compile( re.escape( seperator.group(1) ) + "+" )
                fields = re.split( split_re, v );

            if( len( fields ) != 1 ):
                for f in fields:
                    if( int_re.search( f ) == None ):
                        self.output(  self.colors["status"] + f + \
                            self.colors["default"] + ": " + 
                            self.colors["desc"] + \
                            "Not numeric!" +\
                            self.colors["default"] )
                        return -2
                    else:
                        f = int( f )

                fields_s = []
                for x in range( len(fields) ):
                    # I *HATE* python sometimes!
                    fields_s.append( fields[x] )
                    fields[x] = int( fields[x] )
                    
            parsed = 0
            if( len( fields ) == 3 ):
                if( len( fields_s[0] ) == 4 and len( fields_s[1] ) <= 2
                    and len( fields_s[2] ) <= 2 and ( not parsed ) ):
                    parsed = 1
                    # it's an ISO date!
                    ttup = time.localtime( mytime )    
                    mytime = time.mktime( [ fields[0], fields[1], 
                        fields[2], ttup[3], ttup[4], ttup[5], 
                        -1, -1, -1 ] )

                if( not parsed and len( fields_s[0] ) <= 2 and len(
                    fields_s[1] ) <= 2 and len( fields_s[2] ) == 4 ):
                    parsed = 1
                    # it's an american-format date!
                    ttup = time.localtime( mytime )    
                    mytime = time.mktime( [ fields[2], fields[0], 
                        fields[1], ttup[3], ttup[4], ttup[5], 
                        -1, -1, -1 ] )

                if( not parsed and len( fields_s[0] ) <= 2 and
                    len( fields_s[1] ) <= 2 and 
                    len( fields_s[2] ) <= 2 ):
                    parsed = 1
                    if( seperator.group(1) == "." or
                        seperator.group(1) == ":" or
                        seperator.group(1) == ";" ):
                        # it's a time!
                        ttup = time.localtime( mytime )    
                        mytime = time.mktime( [ ttup[0], ttup[1], 
                            ttup[2], fields[0], fields[1], fields[2], 
                            -1, -1, -1 ] )
                    else:
                        # it's an american-format date!
                        ttup = time.localtime( mytime )    
                        mytime = time.mktime( [ fields[2], fields[0], 
                            fields[1], ttup[3], ttup[4], ttup[5], 
                            -1, -1, -1 ] )

            #
            # if there's 2 fields...
            # it could be a date assuming "this year", we assume month first
            # or it could be hours and minutes...
            # we assume time seperators are ":" and "."
            # date seperators are /, ., and -
            #
            if( len( fields ) == 2 ):

                if( len( fields_s[0] ) <= 2 and len( fields_s[1] ) <= 2
                    and ( not parsed ) ):
                    parsed = 1
                    if( seperator.group(1) == "." or
                        seperator.group(1) == ";" or
                        seperator.group(1) == ":" ):
                        # it's a time!
                        ttup = time.localtime( mytime )    
                        mytime = time.mktime( [ ttup[0], ttup[1], 
                            ttup[2], fields[0], fields[1], 0, 
                            -1, -1, -1 ] )
                    else:
                        # it's an american-format date!
                        ttup = time.localtime( mytime )    
                        mytime = time.mktime( [ ttup[0], fields[0], 
                            fields[1], ttup[3], ttup[4], ttup[5], 
                            -1, -1, -1 ] )

            #
            # if there's only 1 field...
            # it *must* be one of our "shortcut" specifications
            # this means that at the toplevel there can only be one field, too
            #
            # shortcut specs: yesterday, today, -5m (last 5 mins),
            # -5h (last 5 hours), -59 (last 59 minutes), 
            # -59m (last 59 minutes), -5d (last 5 days),
            # 
            if( len( fields ) == 1 ):
                fields[0] = string.lower( fields[0] )

                if( string.lower( fields[0] ) == "yesterday" ):
                     parsed = 1
                     mytime = time.time() - 86400;
                     ttup = time.localtime( mytime )    
                     mytime = time.mktime( [ ttup[0], ttup[1], 
                          ttup[2], 0, 0, 0, 
                          -1, -1, -1 ] )

                if( string.lower( fields[0] ) == "today" ):
                    parsed = 1
                    mytime = time.time();
                    ttup = time.localtime( mytime )    
                    mytime = time.mktime( [ ttup[0], ttup[1], 
                        ttup[2], 23, 59, 59, 
                        -1, -1, -1 ] )

                if( string.lower( fields[0] ) == "now" ):
                    parsed = 1
                    # breaking the rules here.....
                    return -1

                matches = re.compile( r"^-(\d+\.{0,1}\d*)([dhm])$" ).\
                    search( fields[0] )
                if( matches != None ):
                    parsed = 1
                    multdict = {
                        "d" : 86400,
                        "h" : 3600,
                        "m" : 60
                    };
                    mytime = time.time() - ( float( matches.group( 1 ) ) *
                        int( multdict[matches.group( 2 )] ) )

            if( not parsed ):
                self.output(  self.colors["status"] + text + \
                    self.colors["default"] + ": " + self.colors["desc"] + \
                    "I don't understand!" +\
                    self.colors["default"] )
                return -2

        return mytime
        
    def endElement(self,name):
        if( self.expatstatus == self.IGNORE ):
            self.expatstatus = self.NONE
            return
        if( name != "message" ):
            return

        if( self.ignorecase ):
            if( ( self.content != "" ) and ( string.find( \
                string.lower( self.message_message ), 
                string.lower( self.content ) ) == -1 ) ):
                return
        else:
            if( ( self.content != "" ) and ( string.find( \
                self.message_message, 
                self.content ) == -1 ) ):
                return

        if( self.message_from == self.profile.user + "@" + \
            self.profile.server ):
            self.message_to = self.current_log
        else:
            self.JIDhash[self.message_from] = self.current_log
            self.message_to = self.profile.user + "@" + \
                self.profile.server

        self.messages.append( [self.message_time, \
            self.message_from, \
            self.message_to, \
            self.message_message] )

        if( ( self.maxmessages != 0 ) and \
            ( len( self.messages ) > self.maxmessages ) ):
            self.messages.pop(0)

    def characters(self,data):
        if( self.expatstatus == self.IGNORE ):
            return
        self.message_message = self.message_message + data

    def setProfile( self, profile ):
        self.profile = profile
        self.colors = self.profile.colors.sessioncolors

    def getNick(self, jid):
        if self.standalone:
            if( self.JIDhash.has_key( jid ) ):
                return self.JIDhash[jid]
            return jid
        else: 
            return self.cli.getNick(jid)

    def getJID(self, nick):
        if self.standalone:
            return nick
        else: 
            return self.cli.getJID(nick)[0]

    def setParent(self, cli):
        self.cli = cli

    def output(self,message):
        if self.standalone:
            print message.encode('ISO-8859-1','replace')
        else:
            self.cli.output( message )

    def isInt( self, v ):
        if( type( v ) == types.IntType ):
            return 1
        if( self.int_re.search( v ) == None ):
            return 0
        else:
            return 1

    def displayMessage( self, message ):
        time, fromjid, tojid, text = message
        tojid = self.getNick( tojid )
        fromjid = self.getNick( fromjid )
        time = self.getDateTime( time )
        self.output( self.colors["user"] + fromjid + \
            self.colors["default"] + " -> " + \
            self.colors["user"] + tojid + \
            self.colors["default"] + " - " + \
            self.colors["time"] + "LOGGED" + \
            self.colors["default"] + " Message - " + \
            self.colors["time"] + time + \
            self.colors["messagebody"] + "\n" + text + \
            self.colors["default"] )

if(__name__ == "__main__"):
    global loghandler
    # locale.setlocale(locale.LC_ALL, "de") #("US","ISO-8859-1"))
    getTerminalWidth()

    prefs = Preferences()
    profile = prefs.getDefaultProfile()

    loghandler = LogHandler(None, profile)

    profile_n = loghandler.askuser( "Profile", profile.name, 0 )
    profile = prefs.getProfile( profile_n )
    loghandler.setProfile( profile )

    messages = loghandler.readLogs( None, None, None, None, None, 1 )
    for x in messages:
        loghandler.displayMessage( x )

    sys.exit(0)

