/*******************************************************************************
 * Copyright (C) 2009-2011 FuseSource Corp.
 * Copyright (c) 2003, 2006 IBM Corporation and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 *******************************************************************************/
package org.fusesource.hawtjni.generator;

import java.io.*;
import java.util.*;

/**
 * Produces the java classes mapping to XPCOM Mozilla objects.
 * 
 * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
 */
public class MozillaGenerator {

    static boolean DEBUG = false;

    FileReader r = null;
    FileWriter w = null;
    int maxLines = 1000;
    int cntLines = 0;
    int n = 0;
    String[] b = null;
    String body = null;
    int nMethods = 0;
    String uuidName;
    String uuidValue;
    String className;
    String parentName;
    String[] constantNames;
    String[] constantValues;
    String[] methodNames;
    String[][] argTypes;
    String[][] argNames;
    String bodyOrder;
    TreeMap<Integer, TreeSet<String>> vtbls = new TreeMap<Integer, TreeSet<String>>();


    // Contains the characters found before a method name
    // Useful to extract the method name. e.g.
    // NS_IMETHOD QueryInterface(const nsIID & uuid, void * *result) = 0;
    // NS_IMETHOD_(nsrefcnt) AddRef(void) = 0;
    // method name follows: QueryInterface, AddRef etc.
    static String[] BEFORE_METHOD_NAME = { "  NS_IMETHOD ", "  NS_IMETHOD_(nsrefcnt) ", "  NS_IMETHOD_(void *) ", "  NS_IMETHOD_(void) ", "  NS_IMETHOD_(nsresult) ",
            "  NS_SCRIPTABLE NS_IMETHOD ", "  NS_SCRIPTABLE NS_IMETHOD_(nsrefcnt) ", "  NS_SCRIPTABLE NS_IMETHOD_(void *) ", "  NS_SCRIPTABLE NS_IMETHOD_(void) ",
            "  NS_SCRIPTABLE NS_IMETHOD_(nsresult) ", };

    static String NO_SUPER_CLASS = "SWT_NO_SUPER_CLASS";

    static String[][] TYPES_C2JAVA = { { "PRBool *", "int[]" },
            { "nsIID &", "nsID" },
            { "nsCID &", "nsID" },
            { "nsCID * *", "int /*long*/" }, // nsID[] not supported by jnigen
            { "* *", "int /*long*/[]" }, { "**", "int /*long*/[]" }, { "* &", "int /*long*/[]" }, { "PRUint32 *", "int[]" }, { "PRInt32 *", "int[]" }, { "PRInt64 *", "long[]" },
            { "PRUnichar *", "char[]" }, { "char *", "byte[]" }, { "float *", "float[]" }, { "PRUint16 *", "short[]" }, { "nativeWindow *", "int /*long*/[]" },
            { "nsWriteSegmentFun", "int /*long*/" }, { "nativeWindow", "int /*long*/" },

            { "*", "int /*long*/" }, // c type containing one or more * (and any
                                     // other character, and did not match
                                     // previous patterns) is a simple pointer
            { "&", "int /*long*/" },

            { "PRUint32", "int" }, { "PRInt32", "int" }, { "PRInt64", "long" }, { "nsresult", "int" }, { "PRBool", "int" }, { "float", "float" }, { "PRUint16", "short" },
            { "size_t", "int" }, };

    static String GECKO = "/bluebird/teamhawtjni/hawtjni-builddir/mozilla/1.4/linux_gtk2/mozilla/dist/include/";

    static String TARGET_FOLDER = "/bluebird/teamhawtjni/chrisx/amd64/workspace/org.eclipse.hawtjni/Eclipse SWT Mozilla/common/org/eclipse/hawtjni/internal/mozilla/";

    static String[] XPCOM_HEADERS = { "profile/nsIProfile.h", "widget/nsIAppShell.h", "widget/nsIBaseWindow.h", "xpcom/nsIComponentManager.h", "xpcom/nsIComponentRegistrar.h",
            "webbrwsr/nsIContextMenuListener.h", "docshell/nsIDocShell.h", "dom/nsIDOMEvent.h", "dom/nsIDOMMouseEvent.h", "dom/nsIDOMUIEvent.h", "dom/nsIDOMWindow.h",
            "uriloader/nsIDownload.h",
            "webbrwsr/nsIEmbeddingSiteWindow.h",
            "xpcom/nsIFactory.h",
            "xpcom/nsIFile.h",
            "helperAppDlg/nsIHelperAppLauncherDialog.h",
            "exthandler/nsIExternalHelperAppService.h", // contains
                                                        // nsIHelperAppLauncher
            "xpcom/nsIInputStream.h", "xpcom/nsIInterfaceRequestor.h", "necko/nsIIOService.h", "xpcom/nsILocalFile.h", "xpcom/nsIMemory.h", "progressDlg/nsIProgressDialog.h",
            "windowwatcher/nsIPromptService.h", "xpcom/nsIServiceManager.h", "xpcom/nsISupports.h", "webbrwsr/nsITooltipListener.h", "necko/nsIURI.h",
            "uriloader/nsIURIContentListener.h", "xpcom/nsIWeakReference.h", "webbrwsr/nsIWebBrowser.h", "webbrwsr/nsIWebBrowserChrome.h", "webbrwsr/nsIWebBrowserChromeFocus.h",
            "webbrwsr/nsIWebBrowserFocus.h", "docshell/nsIWebNavigation.h", "uriloader/nsIWebProgress.h", "uriloader/nsIWebProgressListener.h", "embed_base/nsIWindowCreator.h",
            "windowwatcher/nsIWindowWatcher.h" };

    public static void main(String[] args) {
        MozillaGenerator x = new MozillaGenerator();
        for (int i = 0; i < XPCOM_HEADERS.length; i++)
            x.parse(GECKO + XPCOM_HEADERS[i], TARGET_FOLDER);
        x.outputVtblCall();
        System.out.println("done");
    }


    /** Write callbacks */
    public void write(String data) {
        if (DEBUG) {
            System.out.print(data);
            return;
        }
        try {
            w.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void writeLine() {
        if (DEBUG) {
            System.out.println();
            return;
        }
        write("\r\n");
    }

    public void writeLine(String data) {
        if (DEBUG) {
            System.out.println(data);
            return;
        }
        write(data + "\r\n");
    }

    public void writeCopyrights() {
        writeLine(COPYRIGHTS);
    }

    public void writePackageDeclaration() {
        writeLine(PACKAGE_DECLARATION);
    }

    public void writeClassDeclaration(String className, String parentName) {
        String line = "public class " + className;
        if (!parentName.equals(NO_SUPER_CLASS))
            line += " extends " + parentName;
        line += " {";
        writeLine(line);
    }

    public void writeLastMethodId(String parentName, int nMethods) {
        String line = "\tstatic final int LAST_METHOD_ID = ";
        if (!parentName.equals(NO_SUPER_CLASS))
            line += parentName + ".LAST_METHOD_ID + " + nMethods + ";";
        else
            line += "" + (nMethods - 1) + ";"; // zero indexed
        writeLine(line);
    }

    public void writeIID(String uuidName, String uuidValue) {
        writeLine("\tpublic static final String " + uuidName + " =");
        writeLine("\t\t\"" + uuidValue + "\";");
        writeLine();
        String iid = uuidName.substring(0, uuidName.indexOf("_STR"));
        writeLine("\tpublic static final nsID " + iid + " =");
        writeLine("\t\tnew nsID(" + uuidName + ");");
    }

    public void writeAddressField() {
        writeLine("\tint /*long*/ address;");
    }

    public void writeConstructor(String className, String parentName) {
        writeLine("\tpublic " + className + "(int /*long*/ address) {");
        if (!parentName.equals(NO_SUPER_CLASS)) {
            writeLine("\t\tsuper(address);");
        } else {
            writeLine("\t\tthis.address = address;");
        }
        writeLine("\t}");
    }

    public void writeAddressGetter() {
        writeLine("\tpublic int /*long*/ getAddress() {");
        writeLine("\t\treturn this.address;");
        writeLine("\t}");
    }

    public void writeConstant(String name, String value) {
        writeLine("\tpublic static final int " + name + " = " + value + ";");
    }

    public void writeMethod(String name, String parentName, int methodIndex, String[] argTypes, String[] argNames) {
        write("\tpublic int " + name + "(");
        for (int i = 0; i < argTypes.length; i++) {
            write(argTypes[i] + " " + argNames[i]);
            if (i < argTypes.length - 1)
                write(", ");
        }
        write(") {");
        writeLine();
        String line = "\t\treturn XPCOM.VtblCall(";
        if (!parentName.equals(NO_SUPER_CLASS))
            line += parentName + ".LAST_METHOD_ID + " + (methodIndex + 1) + ", getAddress()";
        else
            line += methodIndex + ", getAddress()"; // zero indexed
        write(line);
        if (argTypes.length > 0)
            write(", ");
        for (int i = 0; i < argTypes.length; i++) {
            write(argNames[i]);
            if (i < argTypes.length - 1)
                write(", ");
        }
        writeLine(");");
        writeLine("\t}");
    }

    public void writeClassEnd() {
        write("}");
    }

    public void logVtblCall(String[] argTypes) {
        String vtbl = "static final native int VtblCall(int fnNumber, int /*long*/ ppVtbl";
        if (argTypes.length > 0)
            vtbl += ", ";
        for (int i = 0; i < argTypes.length; i++) {
            vtbl += argTypes[i] + " arg" + i;
            if (i < argTypes.length - 1)
                vtbl += ", ";
        }
        vtbl += ");";
        Integer key = new Integer(argTypes.length);
        TreeSet<String> list = vtbls.get(key);
        if (list == null) {
            list = new TreeSet<String>();
            vtbls.put(key, list);
        }
        boolean duplicate = false;
        
        for (String s : list) {
            if (vtbl.equals(s)) {
                duplicate = true;
                break;
            }
        }
        if (!duplicate)
            list.add(vtbl);
    }

    public void outputVtblCall() {
        Collection<TreeSet<String>> values = vtbls.values();
        for (TreeSet<String> elts : values) {
            for (String elt : elts) {
                System.out.println(elt);
            }
        }
    }

    /** Parsing invoking write callbacks */

    /*
     * Convert a C header file into a Java source file matching SWT Mozilla
     * binding.
     */
    public void parse(String src, String destPath) {
        if (DEBUG)
            writeLine("*** PARSING <" + src + "> to folder " + destPath);
        b = new String[maxLines];
        cntLines = 0;
        try {
            r = new FileReader(src);
            BufferedReader br = new BufferedReader(r);
            while ((b[cntLines] = br.readLine()) != null) {
                cntLines++;
            }
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        n = 0;
        boolean lookForClasses = true;
        while (lookForClasses) {
            /* parsing */
            lookForClasses = parse();

            String destFile = destPath + className + ".java";
            try {
                w = new FileWriter(destFile);
                if (DEBUG)
                    writeLine("** CREATED JAVA FILE <" + destFile + ">");
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }

            /* writing */
            writeCopyrights();
            writePackageDeclaration();
            writeLine();
            writeClassDeclaration(className, parentName);
            writeLine();
            writeLastMethodId(parentName, nMethods);
            writeLine();
            writeIID(uuidName, uuidValue);
            writeLine();
            if (parentName.equals(NO_SUPER_CLASS)) {
                writeAddressField();
                writeLine();
            }
            writeConstructor(className, parentName);
            writeLine();

            if (parentName.equals(NO_SUPER_CLASS)) {
                writeAddressGetter();
                writeLine();
            }

            int constantIndex = 0, methodIndex = 0;
            for (int i = 0; i < bodyOrder.length(); i++) {
                if (bodyOrder.charAt(i) == 'C') {
                    writeConstant(constantNames[constantIndex], constantValues[constantIndex]);
                    if (i < bodyOrder.length() - 1)
                        writeLine();
                    constantIndex++;
                } else if (bodyOrder.charAt(i) == 'M') {
                    writeMethod(methodNames[methodIndex], parentName, methodIndex, argTypes[methodIndex], argNames[methodIndex]);
                    if (i < bodyOrder.length() - 1)
                        writeLine();
                    methodIndex++;
                }
            }

            writeClassEnd();

            try {
                w.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public String getPackages() {
        return "package org.eclipse.hawtjni.internal.mozilla;";
    }

    public boolean parse() {
        if (!jumpToUuidDeclaration())
            return false;
        uuidName = getUuidName(b[n]);
        if (DEBUG)
            System.out.println("UUID name: <" + uuidName + ">");
        uuidValue = getUuidValue(b[n]);
        if (DEBUG)
            System.out.println("UUID value: <" + uuidValue + ">");
        jumpToInterfaceDeclaration();
        className = getClassName(b[n]);
        if (DEBUG)
            System.out.println("Interface name: <" + className + ">");
        parentName = getParentName(b[n]);
        if (DEBUG)
            System.out.println("parentName: <" + parentName + ">");
        parseBody();
        return true;
    }

    boolean jumpToUuidDeclaration() {
        // jump to line matching:
        // "#define NS_IWEBBROWSERCHROME_IID_STR "ba434c60-9d52-11d3-afb0-00a024ffc08c""
        while (!(b[n].startsWith("#define ") && b[n].indexOf("_IID_STR \"") != -1)) {
            n++;
            if (n >= cntLines)
                return false;
        }
        return true;
    }

    // assume a declaration matching:
    // "#define NS_IWEBBROWSERCHROME_IID_STR "ba434c60-9d52-11d3-afb0-00a024ffc08c""
    // returns NS_IWEBBROWSERCHROME_IID_STR
    String getUuidName(String declaration) {
        return declaration.substring(declaration.indexOf("#define ") + "#define ".length(), declaration.indexOf(" \""));
    }

    // assume a declaration matching:
    // "#define NS_IWEBBROWSERCHROME_IID_STR "ba434c60-9d52-11d3-afb0-00a024ffc08c""
    // returns ba434c60-9d52-11d3-afb0-00a024ffc08c
    String getUuidValue(String declaration) {
        return declaration.substring(declaration.indexOf("_IID_STR \"") + "_IID_STR \"".length(), declaration.lastIndexOf('"'));
    }

    void jumpToInterfaceDeclaration() {
        // jump to line matching:
        // "class NS_NO_VTABLE nsIWebBrowserChrome : public nsISupports {"
        while (!(b[n].startsWith("class NS_NO_VTABLE "))) {
            n++;
        }
    }

    // Assume a declaration matching:
    // "class NS_NO_VTABLE nsIWebBrowserChrome : public nsISupports {"
    // or
    // "class NS_NO_VTABLE NS_SCRIPTABLE nsIWebBrowserChrome : public nsISupports {"
    // returns nsIWebBrowserChrome.
    // Special case for nsISupports that has no super class: class NS_NO_VTABLE
    // nsISupports {
    String getClassName(String declaration) {
        int endIndex = declaration.indexOf(" :");
        // nsISupports special case (no super class)
        if (endIndex == -1)
            endIndex = declaration.indexOf(" {");
        String searchString = "class NS_NO_VTABLE NS_SCRIPTABLE";
        int startIndex = declaration.indexOf(searchString);
        if (startIndex == -1) {
            searchString = "class NS_NO_VTABLE ";
            startIndex = declaration.indexOf(searchString);
        }
        return declaration.substring(startIndex + searchString.length(), endIndex);
    }

    // assume a declaration matching:
    // "class NS_NO_VTABLE nsIWebBrowserChrome : public nsISupports {"
    // returns nsISupports
    // special case for nsISupports that has no super class: class NS_NO_VTABLE
    // nsISupports {
    String getParentName(String declaration) {
        if (declaration.indexOf(" :") == -1)
            return NO_SUPER_CLASS;
        return declaration.substring(declaration.indexOf(": public ") + ": public ".length(), declaration.indexOf(" {"));
    }

    // parse methods and constants declarations starting at the current index
    // out:
    // .String body - contains the corresponding java content
    // .n - set to the end of the interface body declaration ( line with the
    // enclosing "};" )
    // .nMethods - set to the number of methods parsed
    void parseBody() {
        body = "";
        bodyOrder = "";
        int nConstants = 0;
        nMethods = 0;

        int tmp_n = n;
        while (true) {
            int type = jumpToNextConstantOrMethod();
            if (type == CONSTANT)
                nConstants++;
            if (type == METHOD)
                nMethods++;
            if (type == END_BODY)
                break;
            n++;
        }
        n = tmp_n;
        constantNames = new String[nConstants];
        constantValues = new String[nConstants];
        methodNames = new String[nMethods];
        argTypes = new String[nMethods][];
        argNames = new String[nMethods][];
        int constantIndex = 0, methodIndex = 0;
        while (true) {
            int type = jumpToNextConstantOrMethod();
            if (type == CONSTANT) {
                parseConstant(b[n], constantIndex);
                bodyOrder += "C";
                constantIndex++;
            }
            if (type == METHOD) {
                parseMethod(b[n], methodIndex);
                logVtblCall(argTypes[methodIndex]);
                bodyOrder += "M";
                methodIndex++;
            }
            if (type == END_BODY)
                return;
            n++;
        }
    }

    static int CONSTANT = 0;

    static int METHOD = 1;

    static int END_BODY = 2;

    boolean isEndOfInterfaceBody() {
        return b[n].startsWith("};");
    }

    int jumpToNextConstantOrMethod() {
        while (!isEndOfInterfaceBody()) {
            if (b[n].startsWith("  enum { ")) {
                return CONSTANT;
            }
            if (methodNameStartIndexOf(b[n]) != -1) {
                return METHOD;
            }
            n++;
        }
        return END_BODY;
    }

    void parseConstant(String constant, int constantIndex) {
        String constantName = constant.substring(constant.indexOf(" enum { ") + " enum { ".length(), constant.indexOf(" ="));
        if (DEBUG)
            writeLine("constantName <" + constantName + ">");
        constantNames[constantIndex] = constantName;

        // most constants values have a trailing U
        // enum { APP_TYPE_UNKNOWN = 0U };
        int endIndex = constant.indexOf("U };");
        // a few others don't
        // enum { ENUMERATE_FORWARDS = 0 };
        if (endIndex == -1)
            endIndex = constant.indexOf(" };");
        String constantValue = constant.substring(constant.indexOf(" = ") + " = ".length(), endIndex);
        if (DEBUG)
            writeLine("constantValue <" + constantValue + ">");
        constantValues[constantIndex] = constantValue;
    }

    // NS_IMETHOD SetStatus(PRUint32 statusType, const PRUnichar *status) = 0;
    // identify:
    // method name: <SetStatus>
    // Nbr of arguments: 2
    // Type of argument 0: PRUint32
    // Name of argument 0: statusType
    // Type of argument 1: const PRUnichar *
    // Name of argument 1: status
    void parseMethod(String line, int methodIndex) {
        int start = methodNameStartIndexOf(line);
        int end = methodNameEndIndexOf(line);
        String methodName = line.substring(start, end);
        if (DEBUG)
            writeLine("method name: <" + methodName + ">");
        methodNames[methodIndex] = methodName;
        int argStart = end + "(".length();
        int argEnd = line.indexOf(")", argStart);
        parseArgs(line.substring(argStart, argEnd), methodIndex);
    }

    // Given a line, returns the start of the method name or -1
    // if the line does not contain a method declaration.
    int methodNameStartIndexOf(String line) {
        for (int i = 0; i < BEFORE_METHOD_NAME.length; i++) {
            int index = line.indexOf(BEFORE_METHOD_NAME[i]);
            if (index != -1)
                return index + BEFORE_METHOD_NAME[i].length();
        }
        return -1;
    }

    int methodNameEndIndexOf(String line) {
        int startIndex = methodNameStartIndexOf(line);
        return line.indexOf("(", startIndex);
    }

    void parseArgs(String args, int methodIndex) {
        int nArgs = -1;
        // methods with no args look like: () or (void)
        String[] noArgs = new String[] { "", "void" };
        for (int i = 0; i < noArgs.length; i++) {
            if (args.equals(noArgs[i])) {
                nArgs = 0;
                break;
            }
        }
        if (nArgs == -1)
            nArgs = count(args, ", ") + 1;
        String[] argTypes = new String[nArgs];
        this.argTypes[methodIndex] = argTypes;
        String[] argNames = new String[nArgs];
        this.argNames[methodIndex] = argNames;
        int typeStart = 0;

        // name is separated from its type by either of the following (sorted by
        // decreasing size to find the most complete pattern */
        String[] typeNameSep = new String[] { " * *", " **", " * & ", " * ", " *", " & ", " " };
        for (int i = 0; i < nArgs; i++) {
            /* get the type */
            int nextTypeStart = i < nArgs - 1 ? args.indexOf(", ", typeStart) + ", ".length() : args.length();
            int typeNameSepIndex = 0;
            int separatorIndex = 0;
            for (; typeNameSepIndex < typeNameSep.length; typeNameSepIndex++) {
                separatorIndex = args.indexOf(typeNameSep[typeNameSepIndex], typeStart);
                if (separatorIndex != -1 && separatorIndex < nextTypeStart)
                    break;
            }
            String separator = typeNameSep[typeNameSepIndex];
            argTypes[i] = getC2JavaType(args.substring(typeStart, separatorIndex + separator.length()));
            if (DEBUG)
                writeLine("arg type" + i + ": <" + argTypes[i] + ">");
            /* get the name */
            int nameStart = separatorIndex + separator.length();
            int nameEnd = i < nArgs - 1 ? args.indexOf(", ", nameStart) : args.length();
            argNames[i] = args.substring(nameStart, nameEnd);
            if (DEBUG)
                writeLine("arg name" + i + ": <" + argNames[i] + ">");

            typeStart = nextTypeStart;
        }
    }

    String getC2JavaType(String cType) {
        for (int i = 0; i < TYPES_C2JAVA.length; i++) {
            if (cType.indexOf(TYPES_C2JAVA[i][0]) != -1)
                return TYPES_C2JAVA[i][1];
        }
        return "!ERROR UNKNOWN C TYPE <" + cType + ">!";
    }

    // how many times part can be found in s
    static int count(String s, String part) {
        int index = -1, cnt = 0;
        while ((index = s.indexOf(part, index + 1)) != -1)
            cnt++;
        return cnt;
    }

    static String COPYRIGHTS = "/* ***** BEGIN LICENSE BLOCK *****\r\n" + " * Version: MPL 1.1\r\n" + " *\r\n"
            + " * The contents of this file are subject to the Mozilla Public License Version\r\n"
            + " * 1.1 (the \"License\"); you may not use this file except in compliance with\r\n" + " * the License. You may obtain a copy of the License at\r\n"
            + " * http://www.mozilla.org/MPL/\r\n" + " *\r\n" + " * Software distributed under the License is distributed on an \"AS IS\" basis,\r\n"
            + " * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License\r\n" + " * for the specific language governing rights and limitations under the\r\n"
            + " * License.\r\n" + " *\r\n" + " * The Original Code is Mozilla Communicator client code, released March 31, 1998.\r\n" + " *\r\n"
            + " * The Initial Developer of the Original Code is\r\n" + " * Netscape Communications Corporation.\r\n"
            + " * Portions created by Netscape are Copyright (C) 1998-1999\r\n" + " * Netscape Communications Corporation.  All Rights Reserved.\r\n" + " *\r\n"
            + " * Contributor(s):\r\n" + " *\r\n" + " * IBM\r\n" + " * -  Binding to permit interfacing between Mozilla and SWT\r\n"
            + " * -  Copyright (C) 2003, 2009 IBM Corp.  All Rights Reserved.\r\n" + " *\r\n" + " * ***** END LICENSE BLOCK ***** */";

    static String PACKAGE_DECLARATION = "package org.eclipse.hawtjni.internal.mozilla;";

}
