#!/usr/bin/env python3
#
# Copyright (c) ZeroC, Inc. All rights reserved.
#

import os, sys, shutil, re, signal, time, pprint

from xml.sax import make_parser
from xml.sax.handler import feature_namespaces
from xml.sax.handler import ContentHandler
from xml.sax import saxutils
from xml.sax import SAXException

from xml.dom.minidom import parse

progname = os.path.basename(sys.argv[0])
contentHandler = None
propertyClasses = {}

commonPreamble = """//
// Copyright (c) ZeroC, Inc. All rights reserved.
//
"""

commonPreamble = commonPreamble + "// Generated by " + progname + " from file %(inputfile)s, " + time.ctime()
commonPreamble = commonPreamble + """

// IMPORTANT: Do not edit this file -- any edits made here will be lost!
"""

cppHeaderPreamble = commonPreamble + """
#ifndef ICE_INTERNAL_%(classname)s_H
#define ICE_INTERNAL_%(classname)s_H

#include <Ice/Config.h>

namespace IceInternal
{

struct Property
{
    const char* pattern;
    bool deprecated;
    const char* deprecatedBy;

    Property(const char* n, bool d, const char* b) :
        pattern(n),
        deprecated(d),
        deprecatedBy(b)
    {
    }

    Property() :
        pattern(0),
        deprecated(false),
        deprecatedBy(0)
    {
    }

};

struct PropertyArray
{
    const Property* properties;
    const int length;

    PropertyArray(const Property* p, size_t len) :
        properties(p),
        length(static_cast<int>(len))
    {
    }
};

class %(classname)s
{
public:

"""

cppHeaderPostamble = """
    static const PropertyArray validProps[];
    static const char * clPropNames[];
};

}

#endif
"""

cppSrcPreamble = commonPreamble + """
#include <Ice/%(classname)s.h>

"""

javaPreamble = commonPreamble + """
package com.zeroc.IceInternal;

public final class %(classname)s
{
"""

javaCompatPreamble = commonPreamble + """
package IceInternal;

public final class %(classname)s
{
"""

csPreamble = commonPreamble + """
namespace IceInternal
{
    public sealed class %(classname)s
    {
"""

jsPreamble = commonPreamble + """
/* eslint comma-dangle: "off" */
/* eslint array-bracket-newline: "off" */
/* eslint no-useless-escape: "off" */

const Ice = require("../Ice/Property").Ice;
const %(classname)s = {};
const Property = Ice.Property;
"""

jsEpilogue = \
"""
Ice.%(classname)s = %(classname)s;
module.exports.Ice = Ice;
"""

def usage():
    global progname
    print >> sys.stderr, "Usage: " + progname + " [--{cpp|java|java-compat|csharp|js} file]"

def progError(msg):
    global progname
    print >> sys.stderr, progname + ": " + msg

#
# Currently the processing of PropertyNames.xml is going to take place
# in two parts. One is using DOM to extract the property 'classes' such
# as 'proxy', 'objectadapter', etc. The other part uses SAX to create
# the language mapping source code.
#

class PropertyClass:
    def __init__(self, prefixOnly , childProperties):
        self.prefixOnly = prefixOnly
        self.childProperties = childProperties

    def getChildren(self):
        return self.childProperties

    def isPrefixOnly(self):
        return self.prefixOnly

    def __repr__(self):
        return repr((repr(self.preifxOnly), repr(self.childProperties)))

def initPropertyClasses(filename):
    doc = parse(filename)
    propertyClassNodes = doc.getElementsByTagName("class")
    global propertyClasses
    propertyClasses = {}
    for n in propertyClassNodes:
        className = n.attributes["name"].nodeValue
        classType = n.attributes["prefix-only"].nodeValue
        properties = []
        for a in n.childNodes:
            if a.localName == "suffix" and a.hasAttributes():
                """Convert minidom maps to hashtables """
                attmap = {}
                for i in range(0, a.attributes.length):
                    attmap[a.attributes.item(i).name] = a.attributes.item(i).value
                properties.append(attmap)

        propertyClasses[className] = PropertyClass(classType.lower() == "true", properties)

#
# SAX part.
#

def handler(signum, frame):
    """Installed as signal handler. Should cause an files that are in
    use to be closed and removed"""
    global contentHandler
    contentHandler.cleanup()
    sys.exit(128 + signum)

class UnknownElementException(Exception):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)

class PropertyHandler(ContentHandler):

    def __init__(self, inputfile, className):
        self.start = False
        self.properties = {}
        self.inputfile = inputfile
        self.className = className
        self.currentSection = None
        self.sectionPropertyCount = 0
        self.sections = []
        self.cmdLineOptions = []

    def cleanup(self):
        """Needs to be overridden in derived class"""
        pass

    def startFiles(self):
        """Needs to be overridden in derived class"""
        pass

    def closeFiles(self):
        """Needs to be overridden in derived class"""
        pass

    def deprecatedImpl(self, propertyName):
        """Needs to be overridden in derived class"""
        pass

    def deprecatedImplWithReplacementImpl(self, propertyName, deprecatedBy):
        """Needs to be overridden in derived class"""
        pass

    def propertyImpl(self, propertyName):
        """Needs to be overridden in derived class"""
        pass

    def newSection(self, sectionName):
        """Needs to be overridden in derived class"""
        pass

    def moveFiles(self, location):
        """Needs to be overridden in derived class"""
        pass

    def handleNewSection(self, sectionName, noCmdLine):
        self.currentSection = sectionName
        self.sectionPropertyCount = 0
        if noCmdLine == "false":
            self.cmdLineOptions.append(sectionName)
        self.sections.append(sectionName)
        self.newSection()

    def handleDeprecated(self, propertyName):
        self.properties[propertyName] = None
        self.deprecatedImpl(propertyName)

    def handleDeprecatedWithReplacement(self, propertyName, deprecatedBy):
        self.properties[propertyName] = deprecatedBy
        self.deprecatedImplWithReplacementImpl(propertyName, deprecatedBy)

    def handleProperty(self, propertyName):
        self.properties[propertyName] = ""
        self.propertyImpl(propertyName)

    def startElement(self, name, attrs):
        if name == "properties":
            self.start = True
            self.startFiles()
            return

        if not self.start:
            return

        if name == "section":
            noCmdLine = attrs.get("noCmdLine", "false")
            self.handleNewSection(attrs.get("name"), noCmdLine)

        elif name == "property":
            propertyName = attrs.get("name", None)
            if "class" in attrs:
                c = propertyClasses[attrs["class"]]
                for p in c.getChildren():
                    if propertyName == None:
                        self.startElement(name, p)
                    else:
                        t = dict(p)

                        # deprecatedBy properties in property classes
                        # are special. deprecatedBy attributes are
                        # usually absolute or 'raw', but in the case of
                        # a property class, they need to be expanded.
                        if "deprecatedBy" in t:
                            t["deprecatedBy"] = "%s.%s.%s" % (self.currentSection, propertyName, t["deprecatedBy"])
                        t['name'] =  "%s.%s" % (propertyName, p['name'])
                        self.startElement(name, t)
                if c.isPrefixOnly():
                    return

            #
            # != None implies deprecated == true
            #
            deprecatedBy = attrs.get("deprecatedBy", None)
            if deprecatedBy != None:
                self.handleDeprecatedWithReplacement(propertyName, deprecatedBy)
            elif attrs.get("deprecated", "false").lower() == "true" :
                self.handleDeprecated(propertyName)
            else:
                self.handleProperty(propertyName)

    def endElement(self, name):
        if name == "properties":
            self.closeFiles()
        elif name == "section":
            self.closeSection()

class CppPropertyHandler(PropertyHandler):

    def __init__(self, inputfile, c):
        PropertyHandler.__init__(self, inputfile, c)
        self.hFile = None
        self.cppFile = None

    def cleanup(self):
        if self.hFile != None:
            self.hFile.close()
            if os.path.exists(self.className + ".h"):
                os.remove(self.className + ".h")
        if self.cppFile != None:
            self.cppFile.close()
            if os.path.exists(self.className + ".cpp"):
                os.remove(self.className + ".cpp")

    def startFiles(self):
        self.hFile = open(self.className + ".h", "w")
        self.cppFile = open(self.className + ".cpp", "w")
        self.hFile.write(cppHeaderPreamble % {'inputfile' : self.inputfile, 'classname' : self.className})
        self.cppFile.write(cppSrcPreamble % {'inputfile' : self.inputfile, 'classname' : self.className})

    def closeFiles(self):
        self.hFile.write(cppHeaderPostamble % {'classname' : self.className})
        self.cppFile.write("const IceInternal::PropertyArray "\
                        "IceInternal::%(classname)s::validProps[] =\n" % \
                {'classname' : self.className})

        self.cppFile.write("{\n")
        for s in self.sections:
            self.cppFile.write("    %sProps,\n" % s)
        self.cppFile.write("    IceInternal::PropertyArray(0,0)\n");
        self.cppFile.write("};\n\n")

        self.cppFile.write("const char* IceInternal::%(classname)s::clPropNames[] =\n" % \
                {'classname' : self.className})
        self.cppFile.write("{\n")
        for s in self.cmdLineOptions:
            self.cppFile.write("    \"%s\",\n" % s)
        self.cppFile.write("    0\n")
        self.cppFile.write("};\n")
        self.hFile.close()
        self.cppFile.close()

    def fix(self, propertyName):
        return propertyName.replace("[any]", "*")

    def deprecatedImpl(self, propertyName):
        self.cppFile.write("    IceInternal::Property(\"%s.%s\", true, 0),\n" % (self.currentSection, \
                self.fix(propertyName)))

    def deprecatedImplWithReplacementImpl(self, propertyName, deprecatedBy):
        self.cppFile.write("    IceInternal::Property(\"%s.%s\", true, \"%s\"),\n" % (self.currentSection, \
                self.fix(propertyName), deprecatedBy))

    def propertyImpl(self, propertyName):
        self.cppFile.write("    IceInternal::Property(\"%s.%s\", false, 0),\n" % \
                (self.currentSection, self.fix(propertyName)))

    def newSection(self):
        self.hFile.write("    static const PropertyArray %sProps;\n" % self.currentSection)
        self.cppFile.write("const IceInternal::Property %sPropsData[] =\n" % self.currentSection)
        self.cppFile.write("{\n")

    def closeSection(self):
        self.cppFile.write("};\n")
        self.cppFile.write("""
const IceInternal::PropertyArray
    IceInternal::%(className)s::%(section)sProps(%(section)sPropsData,
                                                sizeof(%(section)sPropsData)/sizeof(%(section)sPropsData[0]));

""" % { 'className' : self.className, 'section': self.currentSection })

    def moveFiles(self, location):
        dest = os.path.join(location, "cpp", "src", "Ice")
        if os.path.exists(os.path.join(dest, self.className + ".h")):
            os.remove(os.path.join(dest, self.className + ".h"))
        if os.path.exists(os.path.join(dest, self.className + ".cpp")):
            os.remove(os.path.join(dest, self.className + ".cpp"))
        shutil.move(self.className + ".h", dest)
        shutil.move(self.className + ".cpp", dest)

class JavaPropertyHandler(PropertyHandler):
    def __init__(self, inputfile, c):
        PropertyHandler.__init__(self, inputfile, c)
        self.srcFile = None

    def cleanup(self):
        if self.srcFile != None:
            self.srcFile.close()
            if os.path.exists(self.className + ".java"):
                os.remove(self.className + ".java")

    def startFiles(self):
        self.srcFile = open(self.className + ".java", "w")
        self.srcFile.write(javaPreamble % {'inputfile' : self.inputfile, 'classname' : self.className})

    def closeFiles(self):
        self.srcFile.write("    public static final Property[] validProps[] =\n")

        self.srcFile.write("    {\n")
        for s in self.sections:
            self.srcFile.write("        %sProps,\n" % s)
        self.srcFile.write("        null\n")
        self.srcFile.write("    };\n")

        self.srcFile.write("\n    public static final String clPropNames[] =\n")
        self.srcFile.write("    {\n")
        for s in self.cmdLineOptions:
            self.srcFile.write("        \"%s\",\n" % s)
        self.srcFile.write("        null\n")
        self.srcFile.write("    };\n")
        self.srcFile.write("}\n")
        self.srcFile.close()

    def fix(self, propertyName):
        #
        # The Java property strings are actually regexp's that will be passed to Java's regexp facitlity.
        #
        return propertyName.replace(".", "\\\\.").replace("[any]", "[^\\\\s]+")

    def deprecatedImpl(self, propertyName):
        self.srcFile.write("        new Property(\"%(section)s\\\\.%(pattern)s\", " \
                "true, null),\n" % \
                {"section" : self.currentSection, "pattern": self.fix(propertyName)})

    def deprecatedImplWithReplacementImpl(self, propertyName, deprecatedBy):
        self.srcFile.write("        new Property(\"%(section)s\\\\.%(pattern)s\", "\
                "true, \"%(deprecatedBy)s\"),\n"  % \
                {"section" : self.currentSection, "pattern": self.fix(propertyName),
                    "deprecatedBy" : deprecatedBy})

    def propertyImpl(self, propertyName):
        self.srcFile.write("        new Property(\"%(section)s\\\\.%(pattern)s\", " \
                "false, null),\n" % \
                {"section" : self.currentSection, "pattern": self.fix(propertyName)} )

    def newSection(self):
        self.srcFile.write("    public static final Property %sProps[] =\n" % self.currentSection)
        self.srcFile.write("    {\n")

    def closeSection(self):
        self.srcFile.write("        null\n")
        self.srcFile.write("    };\n\n")

    def moveFiles(self, location):
        dest = os.path.join(location, "java", "src", "Ice", "src", "main", "java", "com", "zeroc", "IceInternal")
        if os.path.exists(os.path.join(dest, self.className + ".java")):
            os.remove(os.path.join(dest, self.className + ".java"))
        shutil.move(self.className + ".java", dest)

class JavaCompatPropertyHandler(JavaPropertyHandler):
    def __init__(self, inputfile, c):
        JavaPropertyHandler.__init__(self, inputfile, c)

    def startFiles(self):
        self.srcFile = open(self.className + "-compat.java", "w")
        self.srcFile.write(javaCompatPreamble % {'inputfile' : self.inputfile, 'classname' : self.className})

    def moveFiles(self, location):
        dest = os.path.join(location, "java-compat", "src", "Ice", "src", "main", "java", "IceInternal")
        if os.path.exists(os.path.join(dest, self.className + ".java")):
            os.remove(os.path.join(dest, self.className + ".java"))
        shutil.move(self.className + "-compat.java", os.path.join(dest, self.className + ".java"))

class CSPropertyHandler(PropertyHandler):
    def __init__(self, inputfile, c):
        PropertyHandler.__init__(self, inputfile, c)
        self.srcFile = None

    def cleanup(self):
        if self.srcFile != None:
            self.srcFile.close()
            if os.path.exists(self.className + ".cs"):
                os.remove(self.className + ".cs")

    def startFiles(self):
        self.srcFile = open(self.className + ".cs", "w")
        self.srcFile.write(csPreamble % {'inputfile' : self.inputfile, 'classname' : self.className})

    def closeFiles(self):
        self.srcFile.write("        public static Property[][] validProps =\n")

        self.srcFile.write("        {\n")
        for s in self.sections:
            self.srcFile.write("            %sProps,\n" % s)
        self.srcFile.write("            null\n")
        self.srcFile.write("        };\n\n")

        self.srcFile.write("        public static string[] clPropNames =\n")
        self.srcFile.write("        {\n")
        for s in self.cmdLineOptions:
            self.srcFile.write("            \"%s\",\n" % s)
        self.srcFile.write("            null\n")
        self.srcFile.write("        };\n")
        self.srcFile.write("    }\n")
        self.srcFile.write("}\n")
        self.srcFile.close()

    def fix(self, propertyName):
        return propertyName.replace(".", "\\.").replace("[any]", "[^\\s]+")

    def deprecatedImpl(self, propertyName):
        self.srcFile.write("             new Property(@\"^%s\.%s$\", true, null),\n" % (self.currentSection, \
                self.fix(propertyName)))

    def deprecatedImplWithReplacementImpl(self, propertyName, deprecatedBy):
        self.srcFile.write("             new Property(@\"^%s\.%s$\", true, @\"%s\"),\n" % \
                (self.currentSection, self.fix(propertyName), deprecatedBy))

    def propertyImpl(self, propertyName):
        self.srcFile.write("             new Property(@\"^%s\.%s$\", false, null),\n" % (self.currentSection, \
                self.fix(propertyName)))

    def newSection(self):
        self.srcFile.write("        public static Property[] %sProps =\n" % self.currentSection);
        self.srcFile.write("        {\n")

    def closeSection(self):
        self.srcFile.write("             null\n")
        self.srcFile.write("        };\n")
        self.srcFile.write("\n")

    def moveFiles(self, location):
        dest = os.path.join(location, "csharp", "src", "Ice")
        if os.path.exists(os.path.join(dest, self.className + ".cs")):
            os.remove(os.path.join(dest, self.className + ".cs"))
        shutil.move(self.className + ".cs", dest)

class JSPropertyHandler(PropertyHandler):
    def __init__(self, inputfile, c):
        PropertyHandler.__init__(self, inputfile, c)
        self.srcFile = None
        self.validSections = ["Ice"]

    def cleanup(self):
        if self.srcFile != None:
            self.srcFile.close()
            if os.path.exists(self.className + ".js"):
                os.remove(self.className + ".js")

    def startFiles(self):
        self.srcFile = open(self.className + ".js", "w")
        self.srcFile.write(jsPreamble % {'inputfile' : self.inputfile, 'classname' : self.className})

    def closeFiles(self):
        self.srcFile.write("%s.validProps =\n" % (self.className))
        self.srcFile.write("[\n")
        for s in self.sections:
            if s in self.validSections:
                self.srcFile.write("    %s.%sProps,\n" % (self.className, s))
        self.srcFile.write("];\n\n")

        self.srcFile.write("%s.clPropNames =\n" % (self.className))
        self.srcFile.write("[\n")
        for s in self.cmdLineOptions:
            if s in self.validSections:
                self.srcFile.write("    \"%s\",\n" % s)
        self.srcFile.write("];\n")

        self.srcFile.write(jsEpilogue % {'classname' : self.className});
        self.srcFile.close()

    def fix(self, propertyName):
        return propertyName.replace(".", "\\.").replace("[any]", ".")

    def deprecatedImpl(self, propertyName):
        if self.currentSection in self.validSections:
            self.srcFile.write("    new Property(\"/^%s\.%s/\", true, null),\n" % (self.currentSection, \
                    self.fix(propertyName)))

    def deprecatedImplWithReplacementImpl(self, propertyName, deprecatedBy):
        if self.currentSection in self.validSections:
            self.srcFile.write("    new Property(\"/^%s\.%s/\", true, \"%s\"),\n" % \
                    (self.currentSection, self.fix(propertyName), deprecatedBy))

    def propertyImpl(self, propertyName):
        if self.currentSection in self.validSections:
            self.srcFile.write("    new Property(\"/^%s\.%s/\", false, null),\n" % (self.currentSection, \
                    self.fix(propertyName)))

    def newSection(self):
        if self.currentSection in self.validSections:
            self.skipSection = False
            self.srcFile.write("%s.%sProps =\n" % (self.className, self.currentSection));
            self.srcFile.write("[\n")

    def closeSection(self):
        if self.currentSection in self.validSections:
            self.srcFile.write("];\n")
            self.srcFile.write("\n")

    def moveFiles(self, location):
        dest = os.path.join(location, "js", "src", "Ice")
        if os.path.exists(os.path.join(dest, self.className + ".js")):
            os.remove(os.path.join(dest, self.className + ".js"))
        shutil.move(self.className + ".js", dest)

class MultiHandler(PropertyHandler):
    def __init__(self, inputfile, c):
        self.handlers = []
        PropertyHandler.__init__(self, inputfile, c)

    def cleanup(self):
        for f in self.handlers:
            f.cleanup()

    def addHandlers(self, handlers):
        self.handlers.extend(handlers)

    def startFiles(self):
        for f in self.handlers:
            f.startFiles()

    def closeFiles(self):
        for f in self.handlers:
            f.closeFiles()

    def newSection(self):
        for f in self.handlers:
            f.newSection()

    def closeSection(self):
        for f in self.handlers:
            f.closeSection()

    def handleNewSection(self, sectionName, cmdLine):
        for f in self.handlers:
            f.handleNewSection(sectionName, cmdLine)

    def handleDeprecated(self, propertyName):
        for f in self.handlers:
            f.handleDeprecated(propertyName)

    def handleDeprecatedWithReplacement(self, propertyName, deprecatedBy):
        for f in self.handlers:
            f.handleDeprecatedWithReplacement(propertyName, deprecatedBy)

    def handleProperty(self, propertyName):
        for f in self.handlers:
            f.handleProperty(propertyName)

    def startElement(self, name, attrs):
        for f in self.handlers:
            f.startElement(name, attrs)

    def moveFiles(self, location):
        for f in self.handlers:
            f.moveFiles(location)

def main():
    if len(sys.argv) != 1 and len(sys.argv) != 3:
        usage()
        sys.exit(1)

    infile = None
    lang = None

    #
    # Find the root of the tree.
    #
    for toplevel in [".", "..", "../..", "../../..", "../../../.."]:
        toplevel = os.path.normpath(toplevel)
        if os.path.exists(os.path.join(toplevel, "config", "makeprops.py")):
            break
    else:
        progError("cannot find top-level directory")
        sys.exit(1)

    if len(sys.argv) == 1:
        infile = os.path.join(toplevel, "config", "PropertyNames.xml")
    else:
        option = sys.argv[1]
        if option == "--cpp":
            lang = "cpp"
        elif option == "--java":
            lang = "java"
        elif option == "--java-compat":
            lang = "java-compat"
        elif option == "--csharp":
            lang = "csharp"
        elif option == "--js":
            lang = "js"
        elif option in ["-h", "--help", "-?"]:
            usage()
            sys.exit(0)
        else:
            usage()
            sys.exit(1)
        infile = sys.argv[2]

    className, ext = os.path.splitext(os.path.basename(infile))
    global contentHandler
    if lang == None:
        contentHandler = MultiHandler(infile, "")
        contentHandler.addHandlers([CppPropertyHandler(infile, className),
            JavaPropertyHandler(infile, className),
            JavaCompatPropertyHandler(infile, className),
            CSPropertyHandler(infile, className),
            JSPropertyHandler(infile, className)])
    else:
        if lang == "cpp":
            contentHandler = CppPropertyHandler(infile, className)
        elif lang == "java":
            contentHandler = JavaPropertyHandler(infile, className)
        elif lang == "java-compat":
            contentHandler = JavaCompatPropertyHandler(infile, className)
        elif lang == "csharp":
            contentHandler = CSPropertyHandler(infile, className)
        elif lang == "js":
            contentHandler = JSPropertyHandler(infile, className)

    #
    # Install signal handler so we can remove the output files if we are interrupted.
    #
    signal.signal(signal.SIGINT, handler)
    # signal.signal(signal.SIGHUP, handler)
    signal.signal(signal.SIGTERM, handler)
    initPropertyClasses(infile)

    parser = make_parser()
    parser.setFeature(feature_namespaces, 0)
    parser.setContentHandler(contentHandler)
    pf = open(infile)
    try:
        parser.parse(pf)
        contentHandler.moveFiles(toplevel)
    except IOError as ex:
        progError(str(ex))
        contentHandler.cleanup()
    except SAXException as ex:
        progError(str(ex))
        contentHandler.cleanup()

if __name__ == "__main__":
    main()
