/* 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.utils.Utils;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;

public class CodeGenerator {
    // Buffers holding the strings to ultimately be written to the output files.
    private final StringBuilder wrapperStartupCode = new StringBuilder();
    private final StringBuilder wrapperMethodBodies = new StringBuilder();
    private final StringBuilder headerFields = new StringBuilder();
    private final StringBuilder headerMethods = new StringBuilder();

    private final HashSet<String> seenClasses = new HashSet<String>();

    private 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 CodeGenerator() {
        // Write the file header things. Includes and so forth.
        // GeneratedJNIWrappers.cpp is generated as the concatenation of wrapperStartupCode with
        // wrapperMethodBodies. Similarly, GeneratedJNIWrappers.h is the concatenation of headerFields
        // with headerMethods.
        wrapperStartupCode.append(GENERATED_COMMENT);
        wrapperStartupCode.append(
                "#include \"nsXPCOMStrings.h\"\n" +
                "#include \"AndroidBridge.h\"\n" +
                "#include \"AndroidBridgeUtilities.h\"\n" +
                "\n" +
                "#ifdef DEBUG\n" +
                "#define ALOG_BRIDGE(args...) ALOG(args)\n" +
                "#else\n" +
                "#define ALOG_BRIDGE(args...) ((void)0)\n" +
                "#endif\n" +
                "\n" +
                "using namespace mozilla;\n" +
                "void AndroidBridge::InitStubs(JNIEnv *jEnv) {\n" +
                "    initInit();\n");
        // Now we write the various GetStaticMethodID calls here...

        headerFields.append("protected:\n\n");
        headerMethods.append(GENERATED_COMMENT);
        headerMethods.append("public:\n\n");
    }

    /**
     * Append the appropriate generated code to the buffers for the method provided.
     *
     * @param aMethodTuple The Java method, plus the name for the generated method.
     * @param aClass       The class to which the Java method belongs.
     */
    public void generateMethod(MethodWithAnnotationInfo aMethodTuple, Class<?> aClass) {
        // Unpack the tuple and extract some useful fields from the Method..
        Method aMethod = aMethodTuple.method;
        String CMethodName = aMethodTuple.wrapperName;

        String javaMethodName = aMethod.getName();

        ensureClassHeaderAndStartup(aClass);

        writeHeaderField(CMethodName);
        writeStartupCode(CMethodName, javaMethodName, aMethod, aClass);

        // Get the C++ method signature for this method.
        String implementationSignature = Utils.getCImplementationMethodSignature(aMethod, CMethodName);
        String headerSignature = Utils.getCHeaderMethodSignature(aMethod, CMethodName, aMethodTuple.isStatic);

        // Add the header signature to the header file.
        headerMethods.append(headerSignature);
        headerMethods.append(";\n");

        // Use the implementation signature to generate the method body...
        writeMethodBody(implementationSignature, CMethodName, aMethod, aClass, aMethodTuple.isStatic, aMethodTuple.isMultithreaded);
    }

    /**
     * Writes the appropriate header and startup code to ensure the existence of a reference to the
     * class specified. If this is already done, does nothing.
     *
     * @param aClass The target class.
     */
    private void ensureClassHeaderAndStartup(Class<?> aClass) {
        String className = aClass.getCanonicalName();
        if (seenClasses.contains(className)) {
            return;
        }

        // Add a field to hold the reference...
        headerFields.append("\njclass ");
        headerFields.append(Utils.getClassReferenceName(aClass));
        headerFields.append(";\n");

        // Add startup code to populate it..
        wrapperStartupCode.append('\n');
        wrapperStartupCode.append(Utils.getStartupLineForClass(aClass));

        seenClasses.add(className);
    }

    /**
     * Generates the method body of the C++ wrapper function for the Java method indicated.
     *
     * @param methodSignature The previously-generated C++ method signature for the method to be
     *                        generated.
     * @param aCMethodName    The C++ method name for the method to be generated.
     * @param aMethod         The Java method to be wrapped by the C++ method being generated.
     * @param aClass          The Java class to which the method belongs.
     */
    private void writeMethodBody(String methodSignature, String aCMethodName, Method aMethod, Class<?> aClass, boolean aIsStaticBridgeMethod, boolean aIsMultithreaded) {
        Class<?>[] argumentTypes = aMethod.getParameterTypes();
        Class<?> returnType = aMethod.getReturnType();

        // The start-of-function boilerplate. Does the bridge exist? Does the env exist? etc.
        wrapperMethodBodies.append('\n');
        wrapperMethodBodies.append(methodSignature);

        wrapperMethodBodies.append(" {\n");

        // Static stubs check the bridge instance has been created before trying to run.
        if (aIsStaticBridgeMethod) {
            wrapperMethodBodies.append("    if (!sBridge) {\n" +
                                       "        ALOG_BRIDGE(\"Aborted: No sBridge - %s\", __PRETTY_FUNCTION__);\n" +
                                       "        return").append(Utils.getFailureReturnForType(returnType)).append(";\n" +
                                       "    }\n\n");
        }
        wrapperMethodBodies.append("    JNIEnv *env = ");
        if (!aIsMultithreaded) {
            wrapperMethodBodies.append("GetJNIEnv();\n");
        } else {
            wrapperMethodBodies.append("GetJNIForThread();\n");
        }
        wrapperMethodBodies.append("    if (!env) {\n" +
                                   "        ALOG_BRIDGE(\"Aborted: No env - %s\", __PRETTY_FUNCTION__);\n" +
                                   "        return").append(Utils.getFailureReturnForType(returnType)).append(";\n" +
                                   "    }\n\n");

        boolean isObjectReturningMethod = !returnType.getCanonicalName().equals("void") && Utils.doesReturnObjectType(aMethod);

        // Determine the number of local refs required for our local frame..
        // AutoLocalJNIFrame is not applicable here due to it's inability to handle return values.
        int localReferencesNeeded = Utils.enumerateReferenceArguments(aMethod);
        if (isObjectReturningMethod) {
            localReferencesNeeded++;
        }
        wrapperMethodBodies.append("    if (env->PushLocalFrame(").append(localReferencesNeeded).append(") != 0) {\n" +
                                   "        ALOG_BRIDGE(\"Exceptional exit of: %s\", __PRETTY_FUNCTION__);\n" +
                                   "        env->ExceptionDescribe();\n"+
                                   "        env->ExceptionClear();\n" +
                                   "        return").append(Utils.getFailureReturnForType(returnType)).append(";\n" +
                                   "    }\n\n");

        // Marshall arguments, if we have any.
        boolean hasArguments = argumentTypes.length != 0;

        // We buffer the arguments to the call separately to avoid needing to repeatedly iterate the
        // argument list while building this line. In the coming code block, we simultaneously
        // construct any argument marshalling code (Creation of jstrings, placement of arguments
        // into an argument array, etc. and the actual argument list passed to the function (in
        // argumentContent).
        StringBuilder argumentContent = new StringBuilder();
        if (hasArguments) {
            argumentContent.append(", ");
            // If we have >2 arguments, use the jvalue[] calling approach.
            if (argumentTypes.length > 2) {
                wrapperMethodBodies.append("    jvalue args[").append(argumentTypes.length).append("];\n");
                for (int aT = 0; aT < argumentTypes.length; aT++) {
                    wrapperMethodBodies.append("    args[").append(aT).append("].");
                    wrapperMethodBodies.append(Utils.getArrayArgumentMashallingLine(argumentTypes[aT], "a" + aT));
                }

                // The only argument is the array of arguments.
                argumentContent.append("args");
                wrapperMethodBodies.append('\n');
            } else {
                // Otherwise, use the vanilla calling approach.
                boolean needsNewline = false;
                for (int aT = 0; aT < argumentTypes.length; aT++) {
                    // If the argument is a string-esque type, create a jstring from it, otherwise
                    // it can be passed directly.
                    if (Utils.isCharSequence(argumentTypes[aT])) {
                        wrapperMethodBodies.append("    jstring j").append(aT).append(" = NewJavaString(env, a").append(aT).append(");\n");
                        needsNewline = true;
                        // Ensure we refer to the newly constructed Java string - not to the original
                        // parameter to the wrapper function.
                        argumentContent.append('j').append(aT);
                    } else {
                        argumentContent.append('a').append(aT);
                    }
                    if (aT != argumentTypes.length - 1) {
                        argumentContent.append(", ");
                    }
                }
                if (needsNewline) {
                    wrapperMethodBodies.append('\n');
                }
            }
        }

        wrapperMethodBodies.append("    ");
        if (!returnType.getCanonicalName().equals("void")) {
            if (isObjectReturningMethod) {
                wrapperMethodBodies.append("jobject");
            } else {
                wrapperMethodBodies.append(Utils.getCReturnType(returnType));
            }
            wrapperMethodBodies.append(" temp = ");
        }

        boolean isStaticJavaMethod = Utils.isMethodStatic(aMethod);

        // The call into Java
        wrapperMethodBodies.append("env->");
        wrapperMethodBodies.append(Utils.getCallPrefix(returnType, isStaticJavaMethod));
        if (argumentTypes.length > 2) {
            wrapperMethodBodies.append('A');
        }

        wrapperMethodBodies.append('(');
        // If the underlying Java method is nonstatic, we provide the target object to the JNI.
        if (!isStaticJavaMethod) {
            wrapperMethodBodies.append("aTarget, ");
        } else {
            // If the stub to be generated is static, we need to use the singleton to access the class
            // reference.
            if (aIsStaticBridgeMethod) {
                wrapperMethodBodies.append("sBridge->");
            }
            // If this is a static underlyin Java method, we need to use the class reference in our
            // call.
            wrapperMethodBodies.append(Utils.getClassReferenceName(aClass)).append(", ");
        }

        // Write the method id out..
        if (aIsStaticBridgeMethod) {
            wrapperMethodBodies.append("sBridge->");
        }
        wrapperMethodBodies.append('j');
        wrapperMethodBodies.append(aCMethodName);

        // Tack on the arguments, if any..
        wrapperMethodBodies.append(argumentContent);
        wrapperMethodBodies.append(");\n\n");

        // Check for exception and return the failure value..
        wrapperMethodBodies.append("    if (env->ExceptionCheck()) {\n" +
                                   "        ALOG_BRIDGE(\"Exceptional exit of: %s\", __PRETTY_FUNCTION__);\n" +
                                   "        env->ExceptionDescribe();\n" +
                                   "        env->ExceptionClear();\n" +
                                   "        env->PopLocalFrame(NULL);\n" +
                                   "        return").append(Utils.getFailureReturnForType(returnType)).append(";\n" +
                                   "    }\n");

        // If we're returning an object, pop the callee's stack frame extracting our ref as the return
        // value.
        if (isObjectReturningMethod) {
            wrapperMethodBodies.append("    ");
            wrapperMethodBodies.append(Utils.getCReturnType(returnType));
            wrapperMethodBodies.append(" ret = static_cast<").append(Utils.getCReturnType(returnType)).append(">(env->PopLocalFrame(temp));\n" +
                                       "    return ret;\n");
        } else if (!returnType.getCanonicalName().equals("void")) {
            // If we're a primitive-returning function, just return the directly-obtained primative
            // from the call to Java.
            wrapperMethodBodies.append("    env->PopLocalFrame(NULL);\n" +
                                       "    return temp;\n");
        } else {
            // If we don't return anything, just pop the stack frame and move on with life.
            wrapperMethodBodies.append("    env->PopLocalFrame(NULL);\n");
        }
        wrapperMethodBodies.append("}\n");
    }

    /**
     * Generates the code to get the method id of the given method on startup.
     *
     * @param aCMethodName    The C method name of the method being generated.
     * @param aJavaMethodName The name of the Java method being wrapped.
     * @param aMethod         The Java method being wrapped.
     */
    private void writeStartupCode(String aCMethodName, String aJavaMethodName, Method aMethod, Class<?> aClass) {
        wrapperStartupCode.append("    j");
        wrapperStartupCode.append(aCMethodName);
        wrapperStartupCode.append(" = get");
        if (Utils.isMethodStatic(aMethod)) {
            wrapperStartupCode.append("Static");
        }
        wrapperStartupCode.append("Method(\"");
        wrapperStartupCode.append(aJavaMethodName);
        wrapperStartupCode.append("\", \"");
        wrapperStartupCode.append(Utils.getTypeSignatureString(aMethod));
        wrapperStartupCode.append("\");\n");
    }

    /**
     * Create a method id field in the header file for the C method name provided.
     *
     * @param aMethodName C method name to generate a method id field for.
     */
    private void writeHeaderField(String aMethodName) {
        headerFields.append("jmethodID j");
        headerFields.append(aMethodName);
        headerFields.append(";\n");
    }

    /**
     * Get the finalised bytes to go into the generated wrappers file.
     *
     * @return The bytes to be written to the wrappers file.
     */
    public byte[] getWrapperFileContents() {
        wrapperStartupCode.append("}\n");
        wrapperStartupCode.append(wrapperMethodBodies);
        wrapperMethodBodies.setLength(0);
        return wrapperStartupCode.toString().getBytes();
    }

    /**
     * Get the finalised bytes to go into the generated header file.
     *
     * @return The bytes to be written to the header file.
     */
    public byte[] getHeaderFileContents() {
        headerFields.append('\n');
        headerFields.append(headerMethods);
        headerMethods.setLength(0);
        return headerFields.toString().getBytes();
    }
}
