/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.annotationProcessors;

import org.mozilla.gecko.annotationProcessors.classloader.AnnotatableEntity;
import org.mozilla.gecko.annotationProcessors.classloader.ClassWithOptions;
import org.mozilla.gecko.annotationProcessors.classloader.IterableJarLoadingURLClassLoader;
import org.mozilla.gecko.annotationProcessors.utils.GeneratableElementIterator;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;

public class AnnotationProcessor {
    public static final String OUTFILE = "GeneratedJNIWrappers.cpp";
    public static final String HEADERFILE = "GeneratedJNIWrappers.h";

    public static final String GENERATED_COMMENT =
            "// GENERATED CODE\n" +
            "// Generated by the Java program at /build/annotationProcessors at compile time from\n" +
            "// annotations on Java methods. To update, change the annotations on the corresponding Java\n" +
            "// methods and rerun the build. Manually updating this file will cause your build to fail.\n\n";

    public static void main(String[] args) {
        // We expect a list of jars on the commandline. If missing, whinge about it.
        if (args.length <= 1) {
            System.err.println("Usage: java AnnotationProcessor jarfiles ...");
            System.exit(1);
        }

        System.out.println("Processing annotations...");

        // We want to produce the same output as last time as often as possible. Ordering of
        // generated statements, therefore, needs to be consistent.
        Arrays.sort(args);

        // Start the clock!
        long s = System.currentTimeMillis();

        // Get an iterator over the classes in the jar files given...
        Iterator<ClassWithOptions> jarClassIterator = IterableJarLoadingURLClassLoader.getIteratorOverJars(args);

        StringBuilder headerFile = new StringBuilder(GENERATED_COMMENT);
        headerFile.append("#ifndef GeneratedJNIWrappers_h__\n" +
                          "#define GeneratedJNIWrappers_h__\n\n" +
                          "#include \"nsXPCOMStrings.h\"\n" +
                          "#include \"AndroidJavaWrappers.h\"\n" +
                          "\n" +
                          "namespace mozilla {\n" +
                          "namespace widget {\n" +
                          "namespace android {\n" +
                          "void InitStubs(JNIEnv *env);\n\n");

        StringBuilder implementationFile = new StringBuilder(GENERATED_COMMENT);
        implementationFile.append("#include \"GeneratedJNIWrappers.h\"\n" +
                                  "#include \"AndroidBridgeUtilities.h\"\n" +
                                  "#include \"nsXPCOMStrings.h\"\n" +
                                  "#include \"AndroidBridge.h\"\n" +
                                  "\n" +
                                  "namespace mozilla {\n" +
                                  "namespace widget {\n" +
                                  "namespace android {\n");

        // Used to track the calls to the various class-specific initialisation functions.
        StringBuilder stubInitialiser = new StringBuilder();
        stubInitialiser.append("void InitStubs(JNIEnv *env) {\n");

        while (jarClassIterator.hasNext()) {
            ClassWithOptions aClassTuple = jarClassIterator.next();

            CodeGenerator generatorInstance;

            // Get an iterator over the appropriately generated methods of this class
            Iterator<AnnotatableEntity> methodIterator = new GeneratableElementIterator(aClassTuple.wrappedClass);

            if (!methodIterator.hasNext()) {
                continue;
            }
            generatorInstance = new CodeGenerator(aClassTuple.wrappedClass, aClassTuple.generatedName);

            stubInitialiser.append("    ").append(aClassTuple.generatedName).append("::InitStubs(env);\n");

            // Iterate all annotated members in this class..
            while (methodIterator.hasNext()) {
                AnnotatableEntity aElementTuple = methodIterator.next();
                switch (aElementTuple.mEntityType) {
                    case METHOD:
                        generatorInstance.generateMethod(aElementTuple);
                        break;
                    case FIELD:
                        generatorInstance.generateField(aElementTuple);
                        break;
                    case CONSTRUCTOR:
                        generatorInstance.generateConstructor(aElementTuple);
                        break;
                }
            }

            headerFile.append(generatorInstance.getHeaderFileContents());
            implementationFile.append(generatorInstance.getWrapperFileContents());
        }

        implementationFile.append('\n');
        stubInitialiser.append("}");
        implementationFile.append(stubInitialiser);

        implementationFile.append("\n} /* android */\n" +
                                    "} /* widget */\n" +
                                    "} /* mozilla */\n");

        headerFile.append("\n} /* android */\n" +
                            "} /* widget */\n" +
                            "} /* mozilla */\n" +
                            "#endif\n");

        writeOutputFiles(headerFile, implementationFile);
        long e = System.currentTimeMillis();
        System.out.println("Annotation processing complete in " + (e - s) + "ms");
    }

    private static void writeOutputFiles(StringBuilder aHeaderFile, StringBuilder aImplementationFile) {
        FileOutputStream headerStream = null;
        try {
            headerStream = new FileOutputStream(OUTFILE);
            headerStream.write(aImplementationFile.toString().getBytes());
        } catch (IOException e) {
            System.err.println("Unable to write " + OUTFILE + ". Perhaps a permissions issue?");
            e.printStackTrace(System.err);
        } finally {
            if (headerStream != null) {
                try {
                    headerStream.close();
                } catch (IOException e) {
                    System.err.println("Unable to close headerStream due to "+e);
                    e.printStackTrace(System.err);
                }
            }
        }

        FileOutputStream outStream = null;
        try {
            outStream = new FileOutputStream(HEADERFILE);
            outStream.write(aHeaderFile.toString().getBytes());
        } catch (IOException e) {
            System.err.println("Unable to write " + HEADERFILE + ". Perhaps a permissions issue?");
            e.printStackTrace(System.err);
        } finally {
            if (outStream != null) {
                try {
                    outStream.close();
                } catch (IOException e) {
                    System.err.println("Unable to close outStream due to "+e);
                    e.printStackTrace(System.err);
                }
            }
        }
    }
}