#
#   configfile.py
#
#   -Python Darkness Bot-
#          Module for parsing of config files
#
#   Copyright by Jannis A. Schnitzer
#   Created by Jannis A. Schnitzer on 2008-11-02
#
# This library 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 library 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 library; if not, write to the Free Software Foundation, Inc., 59
# Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#

import re
import sys
import os
import time

class __empty:
    def __init__(self):
        self.counter = -1
    def next(self):
        self.counter += 1
        return self.counter
    
kinds = __empty()
kinds.variable = kinds.next()
kinds.array    = kinds.next()
kinds.section  = kinds.next()
kinds.func     = kinds.next()

kinds.names = {}
kinds.names[kinds.variable] = 'variable'
kinds.names[kinds.array]    = 'array'
kinds.names[kinds.section]  = 'section'
kinds.names[kinds.func]     = 'func'

dl = __empty()
dl.debug  = dl.next()
dl.info   = dl.next()
dl.syntax = dl.next()
dl.error  = dl.next()
dl.fatal  = dl.next()
dl.names  = {}
dl.names[dl.info]   = '(info)'
dl.names[dl.syntax] = '(syntax error)'
dl.names[dl.error]  = '(error)'
dl.names[dl.fatal]  = '(fatal error)'

dprint_threshold = dl.syntax

def dprint(s, caller = "", filename = "", line = -1, level = dl.info):
    if level < dprint_threshold: return None
    print time.ctime(),
    if dl.names.has_key(level):
        print dl.names[level],
    else:
        print "(info)",
    if caller: print caller+":",
    if filename:
        l = ""
        if line > -1:
            l = ":"+str(line)
        print "("+filename+l+")",
    print s
    

def parsevalue(rawvalue):
    if not rawvalue: return None
    if rawvalue[:2].lower() == "0x":
        try:
            value = int(rawvalue, 16)
        except ValueError: return False # ignore
        return value
    try:
        value = int(rawvalue)
        return value
    except ValueError: pass
    try:
        value = float(rawvalue)
        return value
    except ValueError: pass
    # ok, not a hex number, not an int, not a float
    # I think we say it's a string as fallback
    if rawvalue == "true":
        return True
    if rawvalue == "false":
        return False
    value = rawvalue
    if len(value) > 1 and value[0] == value[-1] \
        and (value[0] == chr(34) or value[0] == chr(39)):
        # chr(34) is one "",  chr(39) '' - my syntax highlighter plays crazy with 
        # those characters...
        value = value[1:-1]
        
    return value

builtins = {}
def include(parent, args, caller):
    "Include a config file or a directory of config files."
    class_directory = "directory"
    if not args:
        dprint("No arguments", "include", caller.filename, caller.lineno, dl.syntax)
        return None
    filename = args[0]
    try:
        sectionclass = args[1]
    except IndexError:
        dprint("sectionclass is empty", "include", caller.filename, caller.lineno, dl.debug)
        sectionclass = ""
    try:
        altname = args[2]
    except IndexError:
        dprint("altname is empty", "include", caller.filename, caller.lineno, dl.debug)
        altname = filename
    if os.path.isdir(filename):
        dirname = filename
        for thedir in os.walk(dirname):
            subdir = thedir[0]
            # there MIGHT be several trailing slashes
            while subdir[-1] == "/": subdir = subdir[:-1]
            if not sectionclass:
                sectionclass = os.path.basename(subdir)
            files  = thedir[2]
            thenode = node("["+subdir+"] "+class_directory, sectionclass+"/", 0)
            if thenode.invalid:
                dprint("invalid node", "include", thenode.filename, thenode.lineno, dl.syntax)
                return None # TODO: we ignore invalid names
            # TODO: we might want to make a validize(string) function that
            # TODO: makes a string valid for a sectionclass name
            olddir = changedir(subdir+'/')
            for file in files:
                dprint("Recursively including "+file+" ...", "include", caller.filename, caller.lineno)
                include(thenode, [file, sectionclass], caller)
            parent.add_child(thenode)
            os.chdir(olddir)
    elif os.path.isfile(filename):
        thenode = parse(filename, filename)
        if thenode.invalid:
            dprint("invalid node", "include", thenode.filename, thenode.lineno, dl.syntax)
            return None
        thenode.sclass = sectionclass
        thenode.name   = altname
        dprint("included "+filename+" and appended to "+parent.name, "include", caller.filename, caller.lineno)
        parent.add_child(thenode)
    else:
        dprint(filename+" is neither a file nor a directory", "include", caller.filename, caller.lineno, dl.syntax)
    
builtins['include'] = include
builtins['import']  = include

class node:
    def __init__(self, line, parent = None, filename = "", lineno = -1):
        self.raw = line
        self.invalid = False
        self.parent  = parent
        self.can_have_content = True
        self.filename = filename
        self.lineno = lineno
        # we parse the line here.
        line = line.strip()
        if not line:
            self.invalid = True
            return None # the caller discards us
        # first check if we've got a string here
        value = None
        splices = re.findall("(\S+) = (\".*?\"|'.*?')", line)
        if splices:
            value = splices[0][1][1:-1] # strip away those "" and ''
            # we currently don't support escaping with \. Perhaps later.
        else:
            # check for comment
            if line[0] == '#':
                self.invalid = True
                return None
            pos = line.find (" #")
            if pos > 0: # cannot be 0, because we check for line[0] and stripped line before
                line = line[:pos].rstrip() # chop chop
                
        if line == "[]":
            self.name = ""
            self.kind = kinds.section
            self.depth = 0
            self.sclass = ""
            self.sections = {}
            self.snames   = {}
            self.values   = {}
            self.indices  = {}
            self.can_have_content = False
            return None
        splices = re.findall("^([a-zA-Z_][a-zA-Z_0-9]*)((?:\[[a-zA-Z_0-9]*\])?)\s*=\s*(.+)", line)
        if splices:
            splices = splices[0]
            rawvalue = splices[2]
            name = splices[0]
            array = splices[1]
            self.array = None # we check if this is None or a string
            if array:
                self.array = name
                self.name  = array[1:-1] # key
                self.kind  = kinds.array
            else:
                self.name = name
                self.kind = kinds.variable
            # value...
            if value != None: # we already have a string
                self.value = value
                return None
            self.value = parsevalue(rawvalue)
            if self.value == None:
                self.invalid = True
            return None
        # not a name = value pair
        tmpre = "^\$([a-zA-Z_][a-zA-Z_0-9]*)\s*\((.+)\)"
        splices = re.findall (tmpre, line)
        if splices:
            splices = splices[0]
            name = splices[0]
            arglist = splices[1]
            argsre = "(\".*?\"|'.*?'|[^,]+)(?:,\s*)?"
            args = re.findall(argsre, arglist)
            arglist = []
            for arg in args:
                arglist.append(parsevalue(arg))
            # args is now a nice list :-)
            try:
                self.kind = kinds.func
                builtins[name](self.parent, arglist, self)
                return None
            except (KeyError, TypeError, NameError):
                dprint("Exception "+sys.exc_info()[0].__name__+" while executing builtin "+name,
                    "node.__init__", self.filename, self.lineno, dl.error)
                self.invalid = True
                return None
        splices = re.findall("(\[+)([^\[\]]+)(\]+)(?:\s+(\S+))?", line)
        if splices:
            splices = splices[0]
            leftbraces = splices[0]
            rightbraces = splices[2]
            name = splices[1]
            sectionclass = splices[3]
            self.name = name
            self.kind = kinds.section
            self.depth = len(leftbraces) # we usually don't care about this,
                        # just to check if it's lower than the current depth
            if not len(leftbraces) == len(rightbraces):
                # TODO: do something funny
                pass
            self.sclass = sectionclass
            self.sections = {} # sections stored like class   => section
            self.snames   = {} # sections stored like section => class
            self.values   = {} # normal variables here
            self.indices  = {} # array indices (counts)
            self.can_have_content = False
            return None
        self.invalid = True
        return None
    def add_child(self, child):
        if not self.kind == kinds.section: return None
        if not (type(child) == list or child.__class__ == node):
            # TODO: log/debug
            try:
                dprint("type(child) is "+str(type(child)), "node.add_child", child.filename, child.lineno, dl.error)
            except AttributeError:
                dprint("type(child) is "+str(type(child)), "node.add_child", level = dl.error)
            return None
        if child.kind == kinds.section: # subsection
            if self.snames.has_key(child.name):
                oldclass = self.snames[child.name]
                oldsect  = self.sections[oldclass][child.name]
                # TODO: overwrite... merge? Something else?
                # TODO: only overwriting if can_have_contents
                del (self.sections[oldclass][child.name])
                self.snames[child.name] = oldclass
                if not child.can_have_contents:
                    oldclass.name   = child.name
                    oldclass.sclass = child.sclass
                    child = oldclass
            self.snames[child.name] = child.sclass
            if not self.sections.has_key(child.sclass):
                self.sections[child.sclass] = {}
            self.sections[child.sclass][child.name] = child
            child.depth  = self.depth+1
        elif child.kind == kinds.array:
            if not self.values.has_key(child.array) or \
                type(self.values[child.array]) != dict: # TODO: overwrite!
                self.values[child.array] = {}
            if child.name == "":
                if not self.indices.has_key(child.array):
                    self.indices[child.array] = 0
                child.name = self.indices[child.array]
                self.indices[child.array] += 1
            self.values[child.array][child.name] = child.value
        elif child.kind == kinds.variable:
            # TODO: overwrite?
            self.values[child.name] = child.value
        elif type(child) == list:
            for i in child:
                self.add_child(i)
        child.parent = self
        return child
    def s(self, name):
        if not self.kind == kinds.section: return None
        if self.snames.has_key(name):
            return self.sections[self.snames[name]][name]
        return None
    
    def v(self, name):
        if not self.kind == kinds.section: return None
        if self.values.has_key(name):
            return self.values[name]
        return None
        
    def l(self):
        if not self.kind == kinds.section: return None
        retval = {}
        if self.snames.keys():
            retval['sections'] = self.snames.keys()
        if self.values:
            retval['variables'] = self.values
        return retval
        
    def tree(self, level = 0, last = {0:False}):
        "Totally useless tree display of the options under this node"
        sf = " |  "
        se = "    "
        if not self.kind == kinds.section: return None
        if level:
            s = ""
            for i in xrange(level-1):
                if last[i]:
                    s += se
                else:
                    s += sf
            print s,
            if last[level-1]: print '`-',
            else: print '+-',
        print "["+self.name+"]", self.sclass
        count = 0
        thelist = self.values.items()
        thelist.extend([(thekey, None) for thekey in self.snames.iterkeys()])
        length = len(thelist)
        for (name, value) in thelist:
            if count == length-1: last[level] = True
            else: last[level] = False
            if value == None:
                self.s(name).tree(level+1, last)
                count += 1
                continue
            elif type(value) == dict:
                c = 0
                l = len(value)
                for (n, v) in value.items():
                    s = ""
                    for i in xrange(level):
                        if last[i]:
                            s += se
                        else:
                            s += sf
                    print s,
                    if last[level] and c == l-1: print '`-',
                    else: print '+-',
                    print name+"["+str(n)+"]", "=", v
                    c += 1
            else:
                s = ""
                for i in xrange(level):
                    if last[i]:
                        s += se
                    else:
                        s += sf
                print s,
                if last[level]: print '`-',
                else: print '+-',
                print name, "=", value
            count += 1
        
    def __str__(self):
        if self.invalid: return "INVALID"
        s = "["+kinds.names[self.kind]+"] "+self.name
        try:   return s+" = "+str(self.value)
        except AttributeError: return s
        
def changedir(filename):
    olddir = os.path.abspath(".")
    newdir = os.path.dirname(filename)
    if newdir:
        os.chdir(newdir)
        dprint("changed from "+olddir+" to "+os.path.abspath(newdir), "changedir", filename)
    return olddir

        
def parse(filename, rootname = ""):
    try:
        cf = open(filename, "r")
    except IOError:
        # TODO: logger! (On command line, of course)
        dprint("IOError!", "parse", filename, dl.fatal)
        return None
    root = node("[section]")
    root.name  = rootname # this is the root section.
    root.depth = 0    # ^-- read this
    current = root
    olddir = changedir(filename)
    stack = [root]
    dprint("parsing "+filename+" from "+os.path.abspath(".")+" with root "+rootname, "parse", filename)
    lineno = 0
    for line in cf.xreadlines():
        lineno += 1
        n = node(line, stack[-1], filename, lineno)
        if n.invalid: continue
        if n.kind == kinds.section:
            if n.name == "": # special case: revert to root node
                stack = [root]
                current = root
                continue
            while stack and stack[-1].depth >= n.depth:
                stack.pop()
            if stack: retval = stack[-1].add_child(n)
            current = n
            stack.append(n)
        # end section
        elif n.kind == kinds.array or \
            n.kind == kinds.variable:
            current.add_child(n)
        elif n.kind == kinds.func:
            # ignore
            continue
        else:
            dprint("Unknown node kind "+str(n.kind)+" ("+kinds.names[n.kind]+")", "parse", filename, lineno, dl.syntax)
            pass
    os.chdir(olddir)
    cf.close()
    return root
    