/* 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 java.io.File;
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;
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;

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