# Copyright (c) 2000-2001 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# 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; either version 2 of the License, or (at your option) any later
# version.
#
# 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.,
# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

"""bibal module.

Library of functions used by the Engine and the User Interfaces
"""

__revision__ = "$Id: bibal.py,v 1.77 2002/02/14 10:08:30 alf Exp $"

import os, stat
from os.path import normpath,join
import sys,time

from narval.lib import AL_NS, get_encoding, dyn_import
from narval import config
from narval.NarvalRC import NarvalRC

def tell(str) :
    sys.stdout.write(str)
    sys.stdout.flush()


# Load Memory and Recipes ##################################################

def load_modules(fact, callback) :
    NARVAL_HOME = config.get_home()
    if NARVAL_HOME not in sys.path:
        sys.path.insert(0,NARVAL_HOME)
    modpath = normpath(join(NARVAL_HOME,'modules'))
    from imp import find_module,load_module
    mp_file,mp_filename,mp_desc = find_module('modules')
    modules_package = load_module('modules',mp_file,mp_filename,mp_desc)
    black_list = NarvalRC().black_list['actions-module']
    print 'Loading Modules: ',

    for file in [f for f in os.listdir(modpath)
                 if f[-3:] == '.py' and not f[:-3] in black_list]:
        if file == '__init__.py' : continue
        file = file[:-3]
        load_date = str(os.stat(modpath+'/'+file+'.py')[8])
        tell("[%s"% file)
        try:
            m_file, m_filename, m_desc = find_module(file, modules_package.__path__)
            m = load_module(file, m_file, m_filename, m_desc)
            elements = fact.elements_from_string(m.MOD_XML)
            for action in elements:
                action.group = file
                action.load_date = load_date
                callback(action)
        except Exception, e:
            tell(' // error: %s' % str(e))
        tell('] ')
    print

def reload_module(fact, callback, group, name) :
    print 'Reloading Module: ',
    
    tell("[%s"% group)
    modpath = normpath(join(config.get_home(), 'modules', '%s.py'%group))
    load_date = str(os.stat(modpath)[8])#, '%s.py'%group)[8])  
    from imp import find_module,load_module
    m_file, m_filename, m_desc = find_module('modules/%s'%group)#, modpath)
    m = load_module('modules.%s'%group, m_file, m_filename, m_desc)
    for action in fact.elements_from_string(m.MOD_XML):
        action.group = group
        action.load_date = load_date
        callback(action)
        if action.name == name:
            target = action
    tell('] ')
    print
    return target

def load_transforms(fact, callback):
    transformpath =  normpath(join(config.get_home(),'transforms'))
    print 'Loading Transforms: ',
    black_list = NarvalRC().black_list['transforms-module']
    for subdir in [f for f in os.listdir(transformpath)
                   if not f in black_list]:
        pathname = transformpath+os.sep+subdir 
        if stat.S_ISDIR(os.stat(pathname)[stat.ST_MODE]) :
            tell('[%s'%subdir)
            for file in filter(lambda x: x[-5:] == '.xslt',
                               os.listdir(pathname)) :
                try:
                    element = fact.element_from_file(os.path.join(pathname, file))
                    element.group = subdir
                    element.name = file[:-5]
                    element.load_date = str(os.stat(pathname+'/'+file)[8])
                    
                    callback(element)
                except Exception, e:
                    tell(' // error in %s: %s'%(file,str(e)))
                    import traceback
                    traceback.print_exc()
            tell('] ')
    print


def reload_transform(fact, callback, group, name) :
    transformpath =  normpath(join(config.get_home(),'transforms', group))
    print 'Reloading Transform: ',
    tell("[%s"% name)
    filename = normpath('%s/%s.xslt'% (transformpath, name))
    f = open(filename)
    element = fact.element_from_stream(f)
    f.close()
    element.group = group
    element.name = name
    element.load_date = str(os.stat(filename)[8])
    callback(element)
    tell('] ')
    print
    return element


def save_transforms(memory) :
    transformpath =  normpath(join(config.get_home(),'transforms'))
    files = {}
    errors = []
    for element in memory.get_transforms():
        l = files.setdefault(element.group, [])
        l.append(element)
    for group, list in files.items() :
        dir = join(transformpath,group)
        print 'saving transforms',dir
        for x in list :
            name = x.name+'.xslt'
            filename = join(dir,name)
            try:
                f = open(filename, 'w')
                f.write("<?xml version='1.0' encoding='%s'?>\n"%get_encoding())
                f.write(e.as_xml(encoding=get_encoding()))
                f.close()
            except IOError,e:
                errors.append(filename)
                print 'Could not open file',filename
    return errors


def load_recipes(fact, callback) :
    recipepath = normpath(join(config.get_home(),'recipes'))
    print 'Loading Cookbooks: ',
    black_list = NarvalRC().black_list['cookbook']
    for file in [f for f in os.listdir(recipepath)
                 if f[-4:] == '.xml' and not f[:-4] in black_list]:
        tell('[%s'%file[:-4])
        try:
            path = os.path.join(recipepath, file)
            elements = fact.elements_from_file(path)
            date = str(os.stat(path)[8])
            for e in elements:
                e.group = file[:-4]
                e.load_date = date
                callback(e)
        except Exception, e:
            tell(' // error: %s' % str(e))
            import traceback
            traceback.print_exc()
        tell('] ')
    print

def reload_recipe(fact, callback, group, name):
    recipepath = normpath(join(config.get_home(),'recipes', '%s.xml' % group))
    print 'Reloading Cookbook: ',
    tell('[%s'%group)
    elements = fact.elements_from_file(recipepath)
    date = str(os.stat(recipepath)[8])
    for e in elements:
        e.group = group
        e.load_date = date
        callback(e)
        if e.name == name:
            target = e
    tell('] ')
    print
    return target

def save_recipes(memory) :
    recipepath = normpath(join(config.get_home(),'recipes'))
    files = {}
    errors = []
    for element in memory.get_recipes():
        l = files.setdefault(element.group, [])
        l.append(element)
    for name, list in files.items() :
        print 'Saving recipes',name
        filename = join(recipepath, '%s.xml'%name)
        try:
            f = open(filename,'w')
        except IOError:
            print 'Could not open file',filename
            errors.append(filename)
        else:
            f.write("<?xml version='1.0' encoding='%s'?>\n" % get_encoding())
            f.write("<al:cookbook xmlns:al='%s'>\n"%AL_NS)
            for e in list :
                f.write(e.as_xml(encoding=get_encoding()))
            f.write("</al:cookbook>")
            f.close()
    return errors

def load_memory(fact, callback, filepath=None) :
    try:
        print "Loading Memory..."
        memorypath = filepath or normpath(join(config.get_home(),'data/memory.xml'))
        f = open(memorypath)
        elements = fact.elements_from_stream(f)
        f.close()
        map(callback, elements)
        
    except Exception, e:
        import traceback
        traceback.print_exc()
        print "Problem loading memory !"


def check_for_home() :
    should_update_catalog = 0
    try:
        os.listdir(config.get_home())
    except OSError:
        try:
            import shutil
            shutil.copytree(config.get_share(), config.get_home())
            should_update_catalog=1
        except OSError, e:
            print "Fatal Error: unable to create home directory by copying "+\
                  "%s to %s" % (config.get_share(), config.get_home())
            print e
            sys.exit(1)
    else:
        for directory in ['data', 'dtd', 'extensions', 'graphics',
                      'modules', 'recipes', 'transforms']:
            try:
                os.listdir(normpath(join(config.get_home(),directory)))
            except OSError :
                import shutil
                shutil.copytree(normpath(join(config.get_share(),directory)),
                                join(config.get_home(),directory))
##                if directory == 'dtd':
##                    should_update_catalog = 1

##    if should_update_catalog:
##        update_catalog()
        
##def update_catalog():
##    cat_file = normpath(join(config.get_home(),'dtd/CATALOG'))
##    mode = os.stat(cat_file)[stat.ST_MODE]
##    os.chmod(cat_file,mode | 0200) # give write access to the owner
##    good_cat = open(cat_file,'w')
##    source_cat = open(normpath(join(config.get_share(),'dtd/CATALOG')),'r')
##    base_str = normpath(join(config.get_home(),'dtd')) + '/'
##    if sys.platform == 'win32' and base_str[1]==':':
##        base_str=base_str[0]+'|'+base_str[2:]
##    good_cat.write('BASE "%s"\n' % base_str)
##    good_cat.write(source_cat.read())
##    good_cat.close()
##    source_cat.close()

# mk xml traceback error #######################################################
def mk_traceback_error(plan, info) :
    """Create error element from python traceback.
    """
    import traceback
    from cStringIO import StringIO
    from xml.sax.saxutils import escape  
    typ, value, tbck = info
    buf = StringIO()
    encod = get_encoding()
    buf.write('<?xml version="1.0" encoding="%s"?>' % (get_encoding()))
    buf.write('''<al:error xmlns:al="%s"><exception type="%s" value="%s"><stack>
''' % (AL_NS, typ, escape(str(value))))
    stacktb = traceback.extract_tb(tbck)
    for stackentry in stacktb :
        buf.write('<stack_entry filename="%s" line_number="%s" function_name="%s"' %\
        (escape(stackentry[0]), stackentry[1], stackentry[2]))
        if stackentry[3]:
            buf.write('>%s</stack_entry>\n' % escape(stackentry[3]).encode(get_encoding()))
        else:
            buf.write('/>\n')
    buf.write('</stack></exception>%s</al:error>' % plan.as_xml(get_encoding()))
    buf.seek(0)
    #print '#'*80
    #print buf.getvalue()
    #print '#'*80
    error = plan.memory.fact.element_from_stream(buf)
    #buf.close()
    #plan.add_elements((error,))
    return error


# shallow calendar #############################################################
class WrapError(Exception):
    pass

class shallow_calendar:
    """simple calendar used when scheduling time conditions"""

    def __init__(self,(seconds,minutes,hours,monthdays,months,weekdays)):
        self.seconds   = self.build_time_list(seconds,0,60)
        self.minutes   = self.build_time_list(minutes,0,60)
        self.hours     = self.build_time_list(hours,0,24)
        self.monthdays = self.build_time_list(monthdays,1,61)
        self.months    = self.build_time_list(months,1,13)
        self.weekdays  = self.build_time_list(weekdays,0,7)

        self.year=None
        self.month_idx = None
        self.monthday_idx = None
        self.hours_idx = None
        self.minutes_idx = None
        self.seconds_idx = None
    

    def get_next_date(self,date=None):
        date = date or time.localtime(time.time())
        return self.next_date(date) 

    def next_date(self,date):
        y,m,d,h,mm,s,week,jul,dst = date
        exact_date=1
        self.year=y
        try:
            self.month_idx = self.find_next_in_ordered_list(self.months,m)
        except WrapError:
            self.year=self.year+1
            self.month_idx = 0
            exact_date=0

        try:
            self.monthday_idx = self.find_next_in_ordered_list(self.monthdays,d)
        except WrapError:
            self.next_month()
            self.monthday_idx = -1
            self.next_day()
            exact_date=0

        try:
            self.hour_idx = self.find_next_in_ordered_list(self.hours,h)
        except WrapError:
            self.next_day()
            self.hour_idx=0
            exact_date=0
            
        try:
            self.minute_idx = self.find_next_in_ordered_list(self.minutes,mm)
        except WrapError:
            self.next_hour()
            self.minute_idx=0
            exact_date=0
            print self.hour_idx,self.minute_idx
            
        try:
            self.second_idx = self.find_next_in_ordered_list(self.seconds,s)
        except WrapError:
            self.next_minute()
            self.second_idx=0
            exact_date=0
            print self.minute_idx,self.second_idx

        if exact_date:
            self.next_second()

        #print self.monthday_idx
        # FIXME : handle day of week
        #print self.year,self.months[self.month_idx],self.monthdays[self.monthday_idx],self.hours[self.hour_idx],self.minutes[self.minute_idx],self.seconds[self.second_idx]
        return time.mktime((self.year,
                            self.months[self.month_idx],
                            self.monthdays[self.monthday_idx],
                            self.hours[self.hour_idx],
                            self.minutes[self.minute_idx],
                            self.seconds[self.second_idx],
                            -1,-1,-1))
                            

        
    def next_month(self):
        self.month_idx = self.month_idx+1
        if self.month_idx==len(self.months):
            self.month_idx = 0
            self.year = self.year+1

    def next_day(self):
        self.monthday_idx = self.monthday_idx+1
        monthday = self.monthdays[self.monthday_idx]
        if self.monthday_idx==len(self.monthdays) or \
           not self.__exist_day_in_month(monthday,self.months[self.month_idx]):
            self.monthday_idx = 0
            self.next_month()
            if not self.__exist_day_in_month(self.monthdays[self.monthday_idx],
                                             self.months[self.month_idx]):
                self.next_month() # the new month has 31 days, so no further testing is required

    def next_hour(self):
        self.hour_idx = self.hour_idx+1
        if self.hour_idx == len(self.hours):
            self.hour_idx=0
            self.next_day()

    def next_minute(self):
        self.minute_idx = self.minute_idx+1
        if self.minute_idx == len(self.minutes):
            self.minute_idx=0
            self.next_hour()
        

    def next_second(self):
        self.second_idx = self.second_idx+1
        if self.second_idx == len(self.seconds):
            self.second_idx=0
            self.next_minute()

    def find_next_in_ordered_list(self,list,elt):
        for i in range(len(list)):
            if list[i]>=elt:
                return i
        raise WrapError('Carry on!')
    

    def __exist_day_in_month(self,day,month):
        if day==31 and month not in (1,3,5,7,8,10,12):
            return 0
        if day==30 and month == 2:
            return 0
        if day==29 and month == 2 and self.__february(self.year)==28:
            return 0
        return 1
        
    def __february(self,year=None):
        if not year:
            date = time.localtime(time.time())
            if date[1]>2:
                year = date[0]+1
            else:
                year = date[0]

        if year%4 or (year%100 and not year%400):
            return 28
        else:
            return 29
        
        
    def build_time_list(self,cron_string,min,max):
        if cron_string == '*':
            return range(min,max)
        from string import split
        answer = []
        l = split(cron_string,',')
        for r  in l:
            limit = split(r,'-')
            if len(limit)==1:
                answer.append(int(limit[0]))
            elif len(limit)>=2:
                answer = answer + range(int(limit[0]),int(limit[1])+1)
            else:
                print 'ignoring weird value in range',range
        answer.sort()
        return answer

        

def mk_time_stamp() :
    return str(time.time())




# InternalEngineError #########################################

class InternalEngineError(Exception):
    """InternalEngineError"""
    pass


def ASSERT(condition, msg=None, exceptionClass=InternalEngineError):
    if not condition:
        raise exceptionClass('ASSERTION FAILED %s' % (msg or ''))

def WARN(condition,message):
    if not condition:
        print '+'
        print '+ WARNING',message
        print '+'

if __name__ == "__main__" :
    sc = shallow_calendar(('0','0','*','1,10,20,30','*','*'))
    print sc.get_next_date()
