/* 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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.util.Arrays;
import java.util.Iterator;

public class AnnotationProcessor {
    private static final String NATIVES_NAME = "Natives";
    private static final String WRAPPERS_NAME = "Wrappers";
    private static final String EXPORT_PREFIX = "mozilla/java/";

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

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

        final String OUTPUT_PREFIX = args[0];
        final String QUALIFIER = OUTPUT_PREFIX + "JNI";

        (new File(QUALIFIER)).mkdir();

        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.
        final String[] jars = Arrays.copyOfRange(args, 1, args.length);
        Arrays.sort(jars);

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

        int ret = 0;

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

        while (jarClassIterator.hasNext()) {
            final ClassWithOptions annotatedClass = jarClassIterator.next();
            if (!annotatedClass.hasGenerated()) {
                continue;
            }

            final String sourceFileName = QUALIFIER + annotatedClass.generatedName + WRAPPERS_NAME + ".cpp";
            final String headerFileName = QUALIFIER + File.separator + annotatedClass.generatedName + WRAPPERS_NAME + ".h";
            final String headerExportedFileName = EXPORT_PREFIX + annotatedClass.generatedName + WRAPPERS_NAME + ".h";
            final String nativesFileName = QUALIFIER + File.separator + annotatedClass.generatedName + NATIVES_NAME + ".h";
            final String nativesExportedFileName = EXPORT_PREFIX + annotatedClass.generatedName + NATIVES_NAME + ".h";

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

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

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

            nativesFile.append(
                    "#ifndef " + getHeaderGuardName(nativesExportedFileName) + "\n" +
                    "#define " + getHeaderGuardName(nativesExportedFileName) + "\n" +
                    "\n" +
                    "#ifndef MOZ_PREPROCESSOR\n" +
                    "#include \"" + headerExportedFileName + "\"\n" +
                    "#include \"mozilla/jni/Natives.h\"\n" +
                    "#endif\n" +
                    "\n" +
                    "namespace mozilla {\n" +
                    "namespace java {\n" +
                    "\n");

            generateClass(annotatedClass, headerFile, implementationFile, nativesFile);

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

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

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

            ret |= writeOutputFile(sourceFileName, implementationFile);
            ret |= writeOutputFile(headerFileName, headerFile);
            ret |= writeOutputFile(nativesFileName, nativesFile);
        }
        long e = System.currentTimeMillis();
        System.out.println("Annotation processing complete in " + (e - s) + "ms");

        System.exit(ret);
    }

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

        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, headerFile, implementationFile, nativesFile);
        }
    }

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

    private static int writeOutputFile(final String name, final StringBuilder content) {
        final byte[] contentBytes = content.toString().getBytes(StandardCharsets.UTF_8);

        try {
            final byte[] existingBytes = Files.readAllBytes(new File(name).toPath());
            if (Arrays.equals(contentBytes, existingBytes)) {
                return 0;
            }
        } catch (FileNotFoundException e) {
            // Pass.
        } catch (NoSuchFileException e) {
            // Pass.
        } catch (IOException e) {
            System.err.println("Unable to read " + name + ". Perhaps a permissions issue?");
            e.printStackTrace(System.err);
            return 1;
        }

        try (FileOutputStream outStream = new FileOutputStream(name)) {
            outStream.write(contentBytes);
        } catch (IOException e) {
            System.err.println("Unable to write " + name + ". Perhaps a permissions issue?");
            e.printStackTrace(System.err);
            return 1;
        }

        return 0;
    }
}
