/* 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 com.android.tools.lint.checks.ApiLookup;
import com.android.tools.lint.LintCliClient;

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 org.mozilla.gecko.annotationProcessors.utils.Utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Properties;
import java.util.Scanner;
import java.util.Vector;
import java.net.URL;
import java.net.URLClassLoader;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class SDKProcessor {
    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 Javamethods and rerun the build. Manually updating this file\n" +
            "// will cause your build to fail.\n" +
            "\n";

    private static ApiLookup sApiLookup;
    private static int sMaxSdkVersion;

    public static void main(String[] args) throws Exception {
        // We expect a list of jars on the commandline. If missing, whinge about it.
        if (args.length < 5) {
            System.err.println("Usage: java SDKProcessor sdkjar classlistfile outdir fileprefix max-sdk-version");
            System.exit(1);
        }

        System.out.println("Processing platform bindings...");

        String sdkJar = args[0];
        Vector classes = getClassList(args[1]);
        String outdir = args[2];
        String generatedFilePrefix = args[3];
        sMaxSdkVersion = Integer.parseInt(args[4]);

        LintCliClient lintClient = new LintCliClient();
        sApiLookup = ApiLookup.get(lintClient);

        // 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 " + generatedFilePrefix + "_h__\n" +
                "#define " + generatedFilePrefix + "_h__\n" +
                "\n" +
                "#include \"mozilla/jni/Refs.h\"\n" +
                "\n" +
                "namespace mozilla {\n" +
                "namespace java {\n" +
                "namespace sdk {\n" +
                "\n");

        StringBuilder implementationFile = new StringBuilder(GENERATED_COMMENT);
        implementationFile.append(
                "#include \"" + generatedFilePrefix + ".h\"\n" +
                "#include \"mozilla/jni/Accessors.h\"\n" +
                "\n" +
                "namespace mozilla {\n" +
                "namespace java {\n" +
                "namespace sdk {\n" +
                "\n");

        // Used to track the calls to the various class-specific initialisation functions.
        ClassLoader loader = null;
        try {
            loader = URLClassLoader.newInstance(new URL[] { new URL("file://" + sdkJar) },
                                                SDKProcessor.class.getClassLoader());
        } catch (Exception e) {
            throw new RuntimeException(e.toString());
        }

        for (Iterator<String> i = classes.iterator(); i.hasNext(); ) {
            String className = i.next();
            System.out.println("Looking up: " + className);

            generateClass(Class.forName(className, true, loader),
                          implementationFile,
                          headerFile);
        }

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

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

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

    private static int getAPIVersion(Class<?> cls, Member m) {
        if (m instanceof Method || m instanceof Constructor) {
            return sApiLookup.getCallVersion(
                    cls.getName().replace('.', '/'),
                    Utils.getMemberName(m),
                    Utils.getSignature(m));
        } else if (m instanceof Field) {
            return sApiLookup.getFieldVersion(
                    Utils.getClassDescriptor(m.getDeclaringClass()), m.getName());
        } else {
            throw new IllegalArgumentException("expected member to be Method, Constructor, or Field");
        }
    }

    private static Member[] sortAndFilterMembers(Class<?> cls, Member[] members) {
        Arrays.sort(members, new Comparator<Member>() {
            @Override
            public int compare(Member a, Member b) {
                return a.getName().compareTo(b.getName());
            }
        });

        ArrayList<Member> list = new ArrayList<>();
        for (Member m : members) {
            // Sometimes (e.g. Bundle) has methods that moved to/from a superclass in a later SDK
            // version, so we check for both classes and see if we can find a minimum SDK version.
            int version = getAPIVersion(cls, m);
            final int version2 = getAPIVersion(m.getDeclaringClass(), m);
            if (version2 > 0 && version2 < version) {
                version = version2;
            }
            if (version > sMaxSdkVersion) {
                System.out.println("Skipping " + m.getDeclaringClass().getName() + "." + m.getName() +
                    ", version " + version + " > " + sMaxSdkVersion);
                continue;
            }

            // Sometimes (e.g. KeyEvent) a field can appear in both a class and a superclass. In
            // that case we want to filter out the version that appears in the superclass, or
            // we'll have bindings with duplicate names.
            try {
                if (m instanceof Field && !m.equals(cls.getField(m.getName()))) {
                    // m is a field in a superclass that has been hidden by
                    // a field with the same name in a subclass.
                    System.out.println("Skipping " + m.getName() +
                                       " from " + m.getDeclaringClass());
                    continue;
                }
            } catch (final NoSuchFieldException e) {
            }

            list.add(m);
        }

        return list.toArray(new Member[list.size()]);
    }

    private static void generateClass(Class<?> clazz,
                                      StringBuilder implementationFile,
                                      StringBuilder headerFile) {
        String generatedName = clazz.getSimpleName();

        CodeGenerator generator = new CodeGenerator(new ClassWithOptions(clazz, generatedName));

        generator.generateMembers(sortAndFilterMembers(clazz, clazz.getConstructors()));
        generator.generateMembers(sortAndFilterMembers(clazz, clazz.getMethods()));
        generator.generateMembers(sortAndFilterMembers(clazz, clazz.getFields()));

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

    private static Vector<String> getClassList(String path) {
        Scanner scanner = null;
        try {
            scanner = new Scanner(new FileInputStream(path));

            Vector lines = new Vector();
            while (scanner.hasNextLine()) {
                lines.add(scanner.nextLine());
            }
            return lines;
        } catch (Exception e) {
            System.out.println(e.toString());
            return null;
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
    }

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

        FileOutputStream headerStream = null;
        try {
            headerStream = new FileOutputStream(new File(aOutputDir, aPrefix + ".h"));
            headerStream.write(aHeaderFile.toString().getBytes());
        } catch (IOException e) {
            System.err.println("Unable to write " + aOutputDir + ". 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);
                }
            }
        }
    }
}