/* 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 SOURCE_FILE = "GeneratedJNIWrappers.cpp";
    public static final String HEADER_FILE = "GeneratedJNIWrappers.h";
    public static final String NATIVES_FILE = "GeneratedJNINatives.h";

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

    private static final StringBuilder headerFile = new StringBuilder(GENERATED_COMMENT);
    private static final StringBuilder implementationFile = new StringBuilder(GENERATED_COMMENT);
    private static final StringBuilder nativesFile = new StringBuilder(GENERATED_COMMENT);

    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);

        headerFile.append(
                "#ifndef " + getHeaderGuardName(HEADER_FILE) + "\n" +
                "#define " + getHeaderGuardName(HEADER_FILE) + "\n" +
                "\n" +
                "#include \"mozilla/jni/Refs.h\"\n" +
                "\n" +
                "namespace mozilla {\n" +
                "namespace java {\n" +
                "\n");

        implementationFile.append(
                "#include \"GeneratedJNIWrappers.h\"\n" +
                "#include \"mozilla/jni/Accessors.h\"\n" +
                "\n" +
                "namespace mozilla {\n" +
                "namespace java {\n" +
                "\n");

        nativesFile.append(
                "#ifndef " + getHeaderGuardName(NATIVES_FILE) + "\n" +
                "#define " + getHeaderGuardName(NATIVES_FILE) + "\n" +
                "\n" +
                "#include \"GeneratedJNIWrappers.h\"\n" +
                "#include \"mozilla/jni/Natives.h\"\n" +
                "\n" +
                "namespace mozilla {\n" +
                "namespace java {\n" +
                "\n");

        while (jarClassIterator.hasNext()) {
            generateClass(jarClassIterator.next());
        }

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

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

        nativesFile.append(
                "} /* widget */\n" +
                "} /* mozilla */\n" +
                "#endif // " + getHeaderGuardName(NATIVES_FILE) + "\n");

        writeOutputFile(SOURCE_FILE, implementationFile);
        writeOutputFile(HEADER_FILE, headerFile);
        writeOutputFile(NATIVES_FILE, nativesFile);

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

    private static void generateClass(final ClassWithOptions annotatedClass) {
        // Get an iterator over the appropriately generated methods of this class
        final GeneratableElementIterator methodIterator
                = new GeneratableElementIterator(annotatedClass);
        final ClassWithOptions[] innerClasses = methodIterator.getInnerClasses();

        if (!methodIterator.hasNext() && innerClasses.length == 0) {
            return;
        }

        final CodeGenerator generatorInstance = new CodeGenerator(annotatedClass);
        generatorInstance.generateClasses(innerClasses);

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

        headerFile.append(generatorInstance.getHeaderFileContents());
        implementationFile.append(generatorInstance.getWrapperFileContents());
        nativesFile.append(generatorInstance.getNativesFileContents());

        for (ClassWithOptions innerClass : innerClasses) {
            generateClass(innerClass);
        }
    }

    private static String getHeaderGuardName(final String name) {
        return name.replaceAll("\\W", "_");
    }

    private static void writeOutputFile(final String name,
                                        final StringBuilder content) {
        FileOutputStream outStream = null;
        try {
            outStream = new FileOutputStream(name);
            outStream.write(content.toString().getBytes());
        } catch (IOException e) {
            System.err.println("Unable to write " + name + ". 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);
                }
            }
        }
    }
}
