#!/usr/bin/env python
import string, os, sys
try:
    from xml.dom import Node
    from xml.dom.ext.reader import Sax
    Reader = Sax.FromXmlFile
except ImportError:
    print 'You need to have PyXML installed to run this program'
    sys.exit(1)


def Generate(fileName,
             output_dir=None,
             program_name=None
             ):
    output_dir = output_dir or '.'

    dom = Reader(fileName)

    header = CreateHeader(dom, program_name)
    classes = dom.getElementsByTagName('class')
    outfiles = []
    for klass in classes:
        outfiles.append(GenClassFile(klass, header, output_dir))
    return outfiles


def CreateHeader(dom, prog_name):
    result = ''
    header = dom.getElementsByTagName('header')
    if header:
        result = result + string.strip(header[0].childNodes[0].data)
        result = result + '\n\n'

    if prog_name:
        add_str = ' by ' + prog_name
    else:
        add_str = ''
    result = result + '### This file is automatically generated%s.\n' % add_str

    result = result + '### DO NOT EDIT!\n\n'

    copyright = dom.getElementsByTagName('copyright')
    if copyright:
        result = result + '"""\n'
        result = result + string.strip(copyright[0].childNodes[0].data) + '\n'
        result = result + '"""\n\n'

    return result

# Helper function for indenting Python
def indent(count, text, tab=' '*4):
    return tab*count + text

# Get/Set routines for DOMString attributes
def stringGetAttr(name, value):
    return indent(2, 'return self.getAttribute("%s")\n\n' % name)

def stringSetAttr(name):
    return indent(2, 'self.setAttribute("%s", value)\n\n' % name)

# Routines for boolean attributes
def boolGetAttr(name, value):
    return indent(2, 'return self.hasAttribute("%s")\n\n' % name)

def boolSetAttr(name):
    result =          indent(2, 'if value:\n')
    result = result + indent(3,   'self.setAttribute("%s", "%s")\n' % (name, name))
    result = result + indent(2, 'else:\n')
    result = result + indent(3,   'self.removeAttribute("%s")\n\n' % name)
    return result

# Routines for number attributes
def longGetAttr(name, value):
    result =          indent(2, 'value = self.getAttribute("%s")\n' % name)
    result = result + indent(2, 'if value:\n')
    result = result + indent(3,   'return int(value)\n')
    result = result + indent(2, 'return 0\n\n')
    return result

def longSetAttr(name):
    return indent(2, 'self.setAttribute("%s", str(value))\n\n' % name)

# Routines for value-list attributes
def listGetAttr(name, value):
    return indent(2, 'return string.capitalize(self.getAttribute("%s"))\n\n' % name)

# Routines for attributes mapped to Text nodes
def nodeGetAttr(dummy, value):
    result =          indent(2, 'if not self.firstChild:\n')
    result = result + indent(3,   'return ''\n')
    result = result + indent(2, 'if self.firstChild == self.lastChild:\n')
    result = result + indent(3,   'return self.firstChild.data\n')
    result = result + indent(2, 'self.normalize()\n')
    result = result + indent(2, 'text = filter(lambda x: x.nodeType == Node.TEXT_NODE, self.childNodes)\n')
    result = result + indent(2, 'return text[0].data\n\n')
    return result

def nodeSetAttr(dummy):
    result =          indent(2, 'text = None\n')
    result = result + indent(2, 'for node in self.childNodes:\n')
    result = result + indent(3,   'if not text and node.nodeType == Node.TEXT_NODE:\n')
    result = result + indent(4,     'text = node\n')
    result = result + indent(3,   'else:\n')
    result = result + indent(4,     'self.removeChild(node)\n')
    result = result + indent(2, 'if text:\n')
    result = result + indent(3,   'text.data = value\n')
    result = result + indent(2, 'else:\n')
    result = result + indent(3,   'text = self.ownerDocument.createTextNode(value)\n')
    result = result + indent(3,   'self.appendChild(text)\n\n')
    return result

#Routines for constant attributes
def constGetAttr(name, value):
    if not value:
        value = 'None'
    else:
        value = '"%s"' % value
    return indent(2, 'return %s\n\n' % value)

#Routines for form based classes
def formGetAttr(dummy,dummy2):
    result =          indent(2, 'parent = self.parentNode\n')
    result = result + indent(2, 'while parent:\n')
    result = result + indent(3,   'if parent.nodeName == "FORM":\n')
    result = result + indent(4,     'return parent\n')
    result = result + indent(3,   'parent = parent.parentNode\n')
    result = result + indent(2, 'return None\n\n')
    return result


g_valueTypeMap = {
    'bool' : (boolGetAttr, boolSetAttr),
    'long' : (longGetAttr, longSetAttr),
    'list' : (listGetAttr, stringSetAttr),
    'node' : (nodeGetAttr, nodeSetAttr),
    'string' : (stringGetAttr, stringSetAttr),
    'form' : (formGetAttr, None),
    'const' : (constGetAttr, None)
    }

def GenClassFile(klass, header, output_dir):
    class_name = 'HTML%sElement' % klass.getAttribute('name')
    fileName = os.path.join(output_dir,class_name + '.py')
    file = open(fileName, 'w')

    # General header stuff
    file.write(string.replace(header, '$FILE$', class_name))

    # Import statements
    file.write('import string\n')
    file.write('from xml.dom import Node\n')
    baseclass = klass.getElementsByTagName('baseclass')[0].getAttribute('name')
    base_name = string.split(baseclass, '.')[-1]
    file.write('from %s import %s\n' % (baseclass, base_name))
    file.write('\n')

    # Class declaration
    file.write('class %s(%s):\n\n' % (class_name, base_name))

    # Constructor
    file.write(indent(1, 'def __init__(self, ownerDocument, nodeName'))
    multiple = klass.getAttribute('multiple')
    tag_name = klass.getAttribute('tagname')
    if not multiple:
        if not tag_name:
            tag_name = string.upper(klass.getAttribute('name'))
        file.write('="%s"' % tag_name)
    file.write('):\n')
    file.write(indent(2, '%s.__init__(self, ownerDocument, nodeName)\n\n' % base_name))

    # Attributes
    file.write(indent(1, '### Attribute Methods ###\n\n'))
    attrs = klass.getElementsByTagName('attribute')
    read_attrs = []
    write_attrs = []
    for attr in attrs:
        dom_name = attr.getAttribute('name')
        value_type = attr.getAttribute('type')
        html_name = attr.getAttribute('htmlname')
        if not html_name:
            html_name = string.upper(dom_name)
        value = attr.getAttribute('value')    # for const value-type
        permissions = attr.getElementsByTagName('permissions')[0]
        readable = int(permissions.getAttribute('readable'))
        writeable = int(permissions.getAttribute('writeable'))

        if readable:
            file.write(indent(1, 'def _get_%s(self):\n' % dom_name))
            get_func = g_valueTypeMap[value_type][0]
            file.write(get_func(html_name, value))
            read_attrs.append(dom_name)

        if writeable:
            file.write(indent(1, 'def _set_%s(self, value):\n' % dom_name))
            set_func = g_valueTypeMap[value_type][1]
            try:
                file.write(set_func(html_name or value))
            except:
                raise "Set function '%s' in class %s, attribute %s" % (value_type, class_name, dom_name)
            write_attrs.append(dom_name)

    # Methods
    methods = klass.getElementsByTagName('method')
    if methods:
        file.write(indent(1, '### Methods ###\n\n'))
    for method in methods:
        method_name = method.getAttribute('name')
        params = method.getElementsByTagName('params')[0].childNodes
        param_list = []
        for param in params:
            arg = param.getAttribute('name')
            default = param.firstChild
            param_list.append((arg,default))
        file.write(indent(1, 'def %s(self' % method_name))
        for arg,default in param_list:
            file.write(', %s' % arg)
            if default:
                file.write('=%s' % default.data)
        file.write('):\n')

        # The function code
        code = method.getElementsByTagName('code')[0].firstChild
        if code:
            lines = string.split(string.strip(code.data), '\n')
            for line in lines:
                writeTab(file, 2, line)
        else:
            file.write(indent(2, 'pass\n'))
        file.write('\n')

    # Attribute access control
    file.write(indent(1, '### Attribute Access Mappings ###\n\n'))

    file.write(indent(1, '_readComputedAttrs = %s._readComputedAttrs.copy()\n' % base_name))
    if len(read_attrs):
        file.write(indent(1, '_readComputedAttrs.update({\n'))
        for attr in read_attrs[:-1]:
            file.write(indent(2, '"%s" : _get_%s,\n' % (attr, attr)))
        attr = read_attrs[-1]
        file.write(indent(2, '"%s" : _get_%s\n' % (attr, attr)))
        file.write(indent(2, '})\n\n'))

    file.write(indent(1, '_writeComputedAttrs = %s._writeComputedAttrs.copy()\n' % base_name))
    if len(write_attrs):
        file.write(indent(1, '_writeComputedAttrs.update({\n'))
        for attr in write_attrs[:-1]:
            file.write(indent(2, '"%s" : _set_%s,\n' % (attr, attr)))
        attr = write_attrs[-1]
        file.write(indent(2, '"%s" : _set_%s\n' % (attr, attr)))
        file.write(indent(2, '})\n\n'))

    file.write(indent(1, '_readOnlyAttrs = filter(lambda k,m=_writeComputedAttrs: not m.has_key(k),\n'))
    file.write(indent(1, '                 %s._readOnlyAttrs + _readComputedAttrs.keys())\n\n' % base_name))

    return fileName

if __name__ == '__main__':
    program_name = os.path.basename(sys.argv[0])
    output_dir = None

    if len(sys.argv) < 2:
        print 'Usage:  %s input_file [output_dir]' % program_name
        sys.exit(1)
    elif len(sys.argv) == 3:
        output_dir = sys.argv[2]

    input_file = sys.argv[1]

    Generate(input_file,output_dir,program_name)
