#!/usr/bin/env python
# Julian Rockey 2003
# Generate marshall/demarshal functions from marshal_funcs.data file

import sys
import re

def cap_first(str):
    """Capitalise first letter of string."""
    return str[0].upper() + str[1:]

def set_method(attr):
    """Return the name for a QT class setter method for an attribute."""
    return "set" + cap_first(attr)

class DictMaker:
    """Generate code for marshalling/demarshalling types using Python dictionaries."""

    supported_types = ['string']
    re_dictmap = re.compile("%dict\-map(.*)")
    re_dictmap_constructor = re.compile("%constructor (.+)")

    def __init__(self):
        self.attr_list = []
        self.current_type = None
        self.operation = None
        self.constructor = None

        self.type_handlers = {}
        for type in self.supported_types:
            self.type_handlers[type] = (eval('self.handle_%s_marsh' % type),
                                        eval('self.handle_%s_demarsh' % type))

    def handle_string_marsh(self, attribute):
        """Handle marshalling of string item from the dictionary."""
        return ["if (%s && !PyBytes_Check(%s)) return false;" % (attribute, attribute),
                "if (%s) { qobj.%s(TQString::fromUtf8(PyBytes_AS_STRING(%s)));" % (attribute, set_method(attribute), attribute),
                "PyDict_DelItemString(dict,(char*)\"%s\"); } " % (attribute)]

    def handle_string_demarsh(self, attribute):
        """Handle demarshalling of string items into the dictionary."""
        return ["PyObject *%s = PyBytes_FromString(qobj.%s().utf8().data() );" % (attribute ,attribute),
                "PyDict_SetItemString(dict, (char*)\"%s\", %s);" % (attribute, attribute)
                ]
    
    def pre_code_for(self, operation, attribute):
        
        if operation==MARSHAL:
            return ["PyObject *%s = PyDict_GetItemString(dict,(char*)\"%s\");" % (attribute, attribute) ]
        
        return []

    def post_code_for(self, operation, attribute):
        return []

    def code_for(self, operation, type, attribute):
        if operation!=None and (type in self.type_handlers):
            return self.pre_code_for(operation, attribute) + \
                   self.type_handlers[type][not not operation](attribute) + \
                   self.post_code_for(operation, attribute)
        
        return []

    def set_current_type(self, current_type):
        self.current_type = current_type
        self.constructor = "";

    def set_operation(self, operation):
        if operation in [None, MARSHAL, DEMARSHAL]:
            self.operation = operation

    def check_dictmap(self, line):

        if self.operation not in [MARSHAL,DEMARSHAL]: return []

        m=self.re_dictmap_constructor.match(line)
        if m:
            self.constructor = m.groups()[0]
            return ['']

        m=self.re_dictmap.match(line)
        if not m: return []

        if self.operation==MARSHAL:
            result = ["{",
                      "if (!PyDict_Check(obj)) return false;",
                      "%s qobj%s;" % (self.current_type,self.constructor),
                      "PyObject *dict = PyDict_Copy(obj);"
                      ]
        if self.operation==DEMARSHAL:
            result = ["{",
                      "PyObject *dict = PyDict_New();",
                      "if (!dict) return NULL;",
                      "%s qobj%s;" % (self.current_type,self.constructor),
                      "(*str) >> qobj;"
                      ]

        if m.groups()[0].strip():
            self.attr_list = [tuple(x.split(':')) for x in m.groups()[0].strip().split(',') ]

        for attribute, type in self.attr_list:
            result += self.code_for(self.operation, type, attribute)

        if self.operation==MARSHAL:
            result += ["if (str) (*str) << qobj;",
                       "Py_DECREF(dict);",
                       "return true;",
                       "}"
                       ]
        if self.operation==DEMARSHAL:
            result += ["return dict;",
                       "}"
                       ]
            
        return result

class DocType:
    """A class to hold documentation information for each type."""
    
    def __init__(self, type):
        self.type = type
        self.demarshal_asme = None
        self.asme = []
        self.info = []

    def add_asme(self, asme):
        if self.demarshal_asme == None: self.demarshal_asme = asme
        self.asme += [asme]

    def add_info(self,info):
        self.info += [info]

    def xml(self):
        return ['<type dcoptype="%s">' % self.type,
                '  <demarshal-asme>%s</demarshal-asme>' % self.demarshal_asme] + \
               ['  <marshal-asme>%s</marshal-asme>' % asme for asme in self.asme ] + \
               ['  <info>%s</info>' % info for info in self.info ] + \
               ['</type>']


MARSHAL, DEMARSHAL, TOPYOBJ, FROMPYOBJ = 0,1,2,3

if len(sys.argv)!=4:
    print("Use: gen_marshal_code.py <input file> <output file> <doc-xml-output file>")
    raise RuntimeError

nowt, in_name, code_name, doc_xml_name = tuple(sys.argv)

##in_name, code_name, doc_xml_name = "marshal_funcs.data", "marshal_funcs.h", "marshal_funcs_doc.xml"

gen_code_comments = ['/*',
                     ' * This code was generated by gen_marshal_code.py',
                     ' * Please do not modify, or it\'ll be overwritten!',
                     ' */',
                     ' ',
                     ]

re_type = re.compile(r"type\: *([^\s]+).*")
re_marshDemarsh = re.compile("%% *(de)?marshal *.*")
re_tofromPyobj = re.compile("%% *(to|from)_pyobj *.*")
re_defaultCode = re.compile("%defaultcode *.*")
re_docInfo = re.compile("%doc *([^ ]+) *(.*)")

in_file = open(in_name,"r")
code = []

types = {}
doc_types = {}
current_operation = None

dict_maker = DictMaker()

for l in in_file.readlines():
    l=l[:-1]

    # match a "type:" line
    m=re_type.match(l)
    if m:
        current_type = m.groups()[0]
        types[current_type]={}
        doc_types[current_type] = DocType(current_type)
        dict_maker.set_current_type(current_type)
        continue

    m=re_docInfo.match(l)
    if m:
        doc_cmd, rest = m.groups()
        if doc_cmd=="as":
            doc_types[current_type].add_asme(rest)
        if doc_cmd=="info":
            doc_types[current_type].add_info(rest)
        continue

    # match a "%% marshal" or "%% demarshal" line
    m=re_marshDemarsh.match(l)
    if m:
        if m.groups()[0]:
            current_operation = DEMARSHAL
            code.append("PyObject *demarshal_" + current_type + \
                        "(TQDataStream *str)")
        else:
            current_operation = MARSHAL
            code.append("bool marshal_" + current_type + \
                        "(PyObject *obj, TQDataStream *str)")
        dict_maker.set_operation(current_operation)
        continue

    m=re_tofromPyobj.match(l)
    if m:
        if m.groups()[0]=='to':
            current_operation = TOPYOBJ
            code += ["PyObject *toPyObject_%s(%s val)" % (current_type,current_type)]
        elif m.groups()[0]=='from':
            current_operation = FROMPYOBJ
            code += ["%s fromPyObject_%s(PyObject *obj, bool *ok)" % (current_type,current_type)]
        continue
            
            
    if l.strip()=='%%':
        current_operation = None
        dict_maker.set_operation(current_operation)

    if current_operation!=None:
        types[current_type][current_operation]=1

        dict_code = dict_maker.check_dictmap(l)
        if dict_code:
            code += dict_code
            continue

        m=re_defaultCode.match(l)
        if m:
            if current_operation==MARSHAL:
                code += [
                    "{",
                    "  bool ok;",
                    "  %s qobj=fromPyObject_%s(obj,&ok);" % (current_type,current_type),
                    "  if (ok && str) (*str) << qobj;",
                    "  return ok;",
                    "}"
                    ]
                continue
            if current_operation==DEMARSHAL:
                code += [
                    "{",
                    "  %s qobj;" % current_type,
                    "  (*str) >> qobj;",
                    "  return toPyObject_%s(qobj);" % current_type,
                    "}"
                    ]
                continue

        code.append(l)
        
in_file.close()

code.append("void Marshaller::initFuncs() {")
for t in types:
    if MARSHAL in types[t]:
        code.append("m_marsh_funcs[\"" + t + "\"]=marshal_" + t + ";")
    if DEMARSHAL in types[t]:
        code.append("m_demarsh_funcs[\"" + t + "\"]=demarshal_" + t + ";")
code.append("}")

out_file = open(code_name,"w")
out_file.writelines([x + '\n' for x in gen_code_comments])
out_file.writelines([x + '\n' for x in code])
out_file.close()

xml_file = open(doc_xml_name,"w")
print('<?xml version="1.0" ?>', file=xml_file)
print('<!-- This file was auto-generated by gen_marshal_code.py. Changes will be lost! -->', file=xml_file)
print("<types>", file=xml_file)
[ [xml_file.write(x+"\n") for x in doc.xml()] for doc in list(doc_types.values()) ] # silly one-liner
print("</types>", file=xml_file)
xml_file.close()
