#!/usr/bin/python

MENUDIR="/usr/lib/menu"

import sys
import glob
import fileinput
import re
import os.path

# XXX: I really hate using these globals, (and even worse letting
# menu-methods modify them)
compat="menu-tiny"
preoutput=""
postoutput=""
startmenu=""
endmenu=""
submenutitle=""
treewalk="c(m)"
genmenu=""
preouput="#Automatically generated file. Do not edit (see the documentation for the menu-tiny package)\n\n"
postoutput=""
prerun=""
preruntest=""
postrun=""

def indent(level):
    for i in range(level-1):
        print "\t",

def canonize(str):
    str = str.strip()
    def escseq_repl(mo):
        s = mo.group(0)
        if s == '\\b': return '\b'
        if s == '\\t': return '\t'
        if s == '\\n': return '\n'
        raise "escseq_repl: Invalid argument: "+s

    if not str:
        return str
    if str[0] != '"':
        return str
    str = re.sub(r'\\[bnt]', escseq_repl, str)
    if str[0] == '"': str = str[1:]
    if str[-1] == '"': str = str[:-1]
    #str = re.sub(r'^"?(.*?)"?$', lambda mo: mo.group(1), str)
        
    return str

class CLFileInput(fileinput.FileInput):
    def __getitem__(self, i):
        cl = ""
        while (1):
            cl = cl[:] + fileinput.FileInput.__getitem__(self, i);
            if cl.endswith("\\\n"):
                cl = cl[:-2]
                self._lineno = self._lineno - 1
                continue
            else:
                break
        return cl

class MenuParseException:
    def __init__(self, msg):
        self.msg = msg
    def __str__(self):
        return 'MenuParseException: '+self.msg

class MenuEntry:
    def __init__(self, str):
        self.attr={
            'icon':"",
            'sort':""
            }
        mo = re.match(r'^\?\s*package\(\s*(.*?)\)\s*:', str)
        if not mo:
            raise MenuParseException, "Invalid menu entry `"+str+"' (should be `?package(foobar):...'"
        self.package = mo.group(1)
        str = str[mo.end():]
        while str:
            if str.isspace():
                break;

            mo = re.match(r'\s*([a-zA-Z._+-]+)=\s*("(\\"|[^"])*"|\S+)', str)
            if not mo:
                raise MenuParseException, "Invalid menu entry: bad name-value pair: "+str
            name = mo.group(1)
            value=canonize(mo.group(2))
            if name=='needs':
                value = value.lower()
            self.attr[name]=value
            str = str[mo.end():]

    def __getattr__(self, name):
        return self.attr[name]

    def __cmp__(self, other):
        result = strcmpn(self.sort, other.sort)
        if result == 0:
            result = strcmpn(self.title, other.title)
        if result == 0:
            result = cmp(id(self), id(other))
        return result

    def display(self, level):
        indent(level)
        for k in self.attr.keys():
            if self.attr.get(k):
                print "%s=%s" % (k, self.attr[k]),
        print

    def render(self, mm):
        G = globals()
        return mm.evaluate(G[self.needs],self)


# Compare two strings just like strcmp, but preserve decimal integer
# sorting order, i.e. "2" < "10" or "iso8859-2" < "iso8859-10" <
# "iso10646-1". Strings are sorted as if sequences of digits were
# prefixed by a length indicator (i.e., does not ignore leading zeroes).
#
# Markus Kuhn <Markus.Kuhn@cl.cam.ac.uk>
# Munged from C to python by Carl Worth <cworth@handhelds.org>
def strcmpn(s1, s2):
    predigits = 0
    i1 = 0
    i2 = 0
    while 1:
        if (i1 >= len(s1)):
            if (i2 >= len(s2)):
                return 0
            else:
                return -1
        elif (i2 >= len(s2)):
            return 1
        digits = s1[i1].isdigit() and s2[i2].isdigit()
        if (digits and not predigits):
            d1 = i1
            d2 = i2
            while (s1[d1].isdigit() and s2[d2].isdigit()):
                d1 += 1
                d2 += 1
                if (d1 >= len(s1)):
                    if (d2 >= len(s2)):
                        return cmp(s1[d1-1], s2[d2-1])
                    else:
                        return -1
                elif (d2 >= len(s2)):
                    return 1
            if (not s1[d1].isdigit() and s2[d2].isdigit()):
                return -1
            if (s1[d1].isdigit() and not s2[d2].isdigit()):
                return 1;
        if (s1[i1] < s2[i2]):
            return -1;
        if (s1[i1] > s2[i2]):
            return 1;
        predigits = digits;
        i1 += 1
        i2 += 1

class MenuNode:

    def __init__(self, title, parent=None, level=0):
        self.title=title
        self.parent=parent
        self.level=level

        self.icon=""
        self.longtitle=""
        self.sort=""
	self.children=[]
        self.submenus={}

    def __cmp__(self, other):
        result = strcmpn(self.sort, other.sort)
        if result == 0:
            result = strcmpn(self.title, other.title)
        if result == 0:
            result = cmp(id(self), id(other))
        return result

    def add(self, entry):
        section = entry.attr['section']
        if section and not section.isspace():
            try:
                (section, entry.attr['section']) = section.split('/',1)
            except ValueError:
                entry.attr['section'] = None

            if not self.submenus.get(section):
                self.submenus[section] = MenuNode(section, self, self.level+1)
            self.submenus[section].add(entry)
        else:
            entry.level = self.level + 1
            self.children.append(entry)

    def display(self, level=0):
        if self.title:
            indent(level)
            print '['+self.title+']'
        for c in self.children:
            c.display(level)
        for m in self.submenus.values():
            m.display(level+1)

    def prune_to_supported(self, sup):
        for k in self.submenus.keys():
            m=self.submenus[k]
            if not m.prune_to_supported(sup):
                del self.submenus[k]
        favorite_children={}
        for c in self.children[:]:
            if not c.needs in sup:
                self.children.remove(c)
            else:
                favorite_c = favorite_children.get(c.title)
                if favorite_c:
                    if sup.index(favorite_c.needs) > sup.index(c.needs):
                        self.children.remove(favorite_c)
                        favorite_children[c.title] = c
                    else:
                        self.children.remove(c)
                else:
                    favorite_children[c.title] = c

        return len(self.submenus) + len(self.children)

    def render(self, mm):
        ret = ""
        if not len(self.children) and not len(self.submenus.keys()):
            return
        if self.level:
            ret += mm.evaluate(submenutitle, self)
        for i in list(treewalk):
            if i == '(':
                ret += mm.evaluate(startmenu, self)
            elif i == 'c':
                self.children.sort()
                for c in self.children:
                    ret = ret + c.render(mm)
            elif i == 'm':
                sm = self.submenus.values()
                sm.sort()
                for m in sm:
                    ret += m.render(mm)
            elif i == 'M':
                things = []
                if len(self.children):
                    things = self.children
                sm = self.submenus.values()
                if len(sm):
                    things = things + sm
                things.sort()
                for t in things:
                    ret += t.render(mm)
            elif i == ')':
                ret += mm.evaluate(endmenu, self)
        return ret

class Menu:
    def __init__(self, menudir):
        self.level = 0
        self.root = MenuNode("")
        files = glob.glob(menudir+'/*')
        if files:
            clfi = CLFileInput(files)
            for line in clfi:
                if line.isspace():
                    continue
                try:
                    entry = MenuEntry(line)
                except MenuParseException, msg:
                    print "%s: at %s:%d" % (msg, clfi.filename(), clfi.filelineno())
                else:
                    self.root.add(entry)

    def render(self, mm):
        if len(preruntest):
            exit_status = os.system(preruntest)
            if (exit_status & 0xff or exit_status >> 8):
                return
        if len(prerun):
            os.system(prerun)
            
        self.root.prune_to_supported(map(lambda x: x.__name__, Supported));
        output = open(rootprefix + genmenu, "w")
        output.write(mm.evaluate(preoutput, self))
        output.write(self.root.render(mm))
        output.write(mm.evaluate(postoutput, self))
        output.close()
        if len(postrun):
            os.system(postrun)

    def display(self):
        self.root.display()

def nstring(count, str):
    return str * count

def ifelse(test, a, b):
    if test:
        return a
    else:
        return b

def ifnempty(test, str):
    if test:
        return str
    else:
        return ""

def esc(str, special):
    return re.sub(r'(['+re.escape(special)+r'])', lambda mo: '\\'+mo.group(1), str)

def level():
    return 0

def include(file):
    dir = os.path.dirname(file)
    if dir:
        os.chdir(os.path.dirname(file))
    # XXX: Gaping security hole here... maybe only exec if file is root-owned?
    execfile(os.path.basename(file), globals(), globals())

class MenuMethod:

    class SubAttr:
        def __init__(self, name, dict):
            self.name = name
            self.dict = dict

    def __init__(self, file):
        self.attr={
            'preoutput':'',
            'postoutput':'',
            'submenutitle':'',
            }
        self.mainattr = MenuMethod.SubAttr("", self.attr)
        self.subattr  = self.mainattr
        self.parsefile(file)

    def __getattr__(self, name):
        return self.attr[name]

    def parsefile(self, file):
        include(file)

    def level(self):
        return 0

    def evaluate(self, thing, arg):
        if callable(thing):
            str = apply(thing,(arg,))
        else:
            str = thing
        return str

def usage():
    print "usage: install-menu menu-method file"
    sys.exit(1)

if len(sys.argv) < 2:
    usage()

menu=Menu(MENUDIR)
#menu.display()

for file in sys.argv[1:]:
    mm = MenuMethod(file)
    menu.render(mm)

