import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

public class JniCodeEmitter {

    static final boolean mUseCPlusPlus = true;
    protected boolean mUseContextPointer = true;
    protected boolean mUseStaticMethods = false;
    protected String mClassPathName;
    protected ParameterChecker mChecker;
    protected List<String> nativeRegistrations = new ArrayList<String>();
    boolean needsExit;
    protected static String indent = "    ";
    HashSet<String> mFunctionsEmitted = new HashSet<String>();

    public static String getJniName(JType jType) {
        String jniName = "";
        if (jType.isClass()) {
            return "L" + jType.getBaseType() + ";";
        } else if (jType.isArray()) {
            jniName = "[";
        }

        String baseType = jType.getBaseType();
        if (baseType.equals("int")) {
            jniName += "I";
        } else if (baseType.equals("float")) {
            jniName += "F";
        } else if (baseType.equals("boolean")) {
            jniName += "Z";
        } else if (baseType.equals("short")) {
            jniName += "S";
        } else if (baseType.equals("long")) {
            jniName += "L";
        } else if (baseType.equals("byte")) {
            jniName += "B";
        }
        return jniName;
    }


    public void emitCode(CFunc cfunc, String original,
            PrintStream javaInterfaceStream,
            PrintStream javaImplStream,
            PrintStream cStream) {
        JFunc jfunc;
        String signature;
        boolean duplicate;

        if (cfunc.hasTypedPointerArg()) {
            jfunc = JFunc.convert(cfunc, true);

            // Don't emit duplicate functions
            // These may appear because they are defined in multiple
            // Java interfaces (e.g., GL11/GL11ExtensionPack)
            signature = jfunc.toString();
            duplicate = false;
            if (mFunctionsEmitted.contains(signature)) {
                duplicate = true;
            } else {
                mFunctionsEmitted.add(signature);
            }

            if (!duplicate) {
                emitNativeDeclaration(jfunc, javaImplStream);
                emitJavaCode(jfunc, javaImplStream);
            }
            if (javaInterfaceStream != null) {
                emitJavaInterfaceCode(jfunc, javaInterfaceStream);
            }
            if (!duplicate) {
                emitJniCode(jfunc, cStream);
            }
        }

        jfunc = JFunc.convert(cfunc, false);

        signature = jfunc.toString();
        duplicate = false;
        if (mFunctionsEmitted.contains(signature)) {
            duplicate = true;
        } else {
            mFunctionsEmitted.add(signature);
        }

        if (!duplicate) {
            emitNativeDeclaration(jfunc, javaImplStream);
        }
        if (javaInterfaceStream != null) {
            emitJavaInterfaceCode(jfunc, javaInterfaceStream);
        }
        if (!duplicate) {
            emitJavaCode(jfunc, javaImplStream);
            emitJniCode(jfunc, cStream);
        }
    }

    public void emitNativeDeclaration(JFunc jfunc, PrintStream out) {
        out.println("    // C function " + jfunc.getCFunc().getOriginal());
        out.println();

        emitFunction(jfunc, out, true, false);
    }

    public void emitJavaInterfaceCode(JFunc jfunc, PrintStream out) {
        emitFunction(jfunc, out, false, true);
    }

    public void emitJavaCode(JFunc jfunc, PrintStream out) {
        emitFunction(jfunc, out, false, false);
    }

    void emitFunctionCall(JFunc jfunc, PrintStream out, String iii, boolean grabArray) {
        boolean isVoid = jfunc.getType().isVoid();
        boolean isPointerFunc = jfunc.getName().endsWith("Pointer") &&
            jfunc.getCFunc().hasPointerArg();

        if (!isVoid) {
            out.println(iii +
                        jfunc.getType() + " _returnValue;");
        }
        out.println(iii +
                    (isVoid ? "" : "_returnValue = ") +
                    jfunc.getName() +
                    (isPointerFunc ? "Bounds" : "" ) +
                    "(");

        int numArgs = jfunc.getNumArgs();
        for (int i = 0; i < numArgs; i++) {
            String argName = jfunc.getArgName(i);
            JType argType = jfunc.getArgType(i);

            if (grabArray && argType.isTypedBuffer()) {
                String typeName = argType.getBaseType();
                typeName = typeName.substring(9, typeName.length() - 6);
                out.println(iii + indent + "get" + typeName + "Array(" + argName + "),");
                out.print(iii + indent + "getOffset(" + argName + ")");
            } else {
                out.print(iii + indent + argName);
            }
            if (i == numArgs - 1) {
                if (isPointerFunc) {
                    out.println(",");
                    out.println(iii + indent + argName + ".remaining()");
                } else {
                    out.println();
                }
            } else {
                out.println(",");
            }
        }

        out.println(iii + ");");
    }

    void printIfcheckPostamble(PrintStream out, boolean isBuffer, boolean emitExceptionCheck,
            String iii) {
                printIfcheckPostamble(out, isBuffer, emitExceptionCheck,
                                      "offset", "_remaining", iii);
            }

    void printIfcheckPostamble(PrintStream out, boolean isBuffer, boolean emitExceptionCheck,
            String offset, String remaining, String iii) {
                out.println(iii + "    default:");
                out.println(iii + "        _needed = 0;");
                out.println(iii + "        break;");
                out.println(iii + "}");

                out.println(iii + "if (" + remaining + " < _needed) {");
                if (emitExceptionCheck) {
                    out.println(iii + indent + "_exception = 1;");
                }
                out.println(iii + indent +
                            (mUseCPlusPlus ? "_env" : "(*_env)") +
                            "->ThrowNew(" +
                            (mUseCPlusPlus ? "" : "_env, ") +
                            "IAEClass, " +
                            "\"" +
                            (isBuffer ?
                             "remaining()" : "length - " + offset) +
                            " < needed\");");
                out.println(iii + indent + "goto exit;");
                needsExit = true;
                out.println(iii + "}");
            }

    boolean isNullAllowed(CFunc cfunc) {
        String[] checks = mChecker.getChecks(cfunc.getName());
        int index = 1;
        if (checks != null) {
            while (index < checks.length) {
                if (checks[index].equals("return")) {
                    index += 2;
                } else if (checks[index].startsWith("check")) {
                    index += 3;
                } else if (checks[index].equals("ifcheck")) {
                    index += 5;
                } else if (checks[index].equals("unsupported")) {
                    index += 1;
                } else if (checks[index].equals("nullAllowed")) {
                    return true;
                } else {
                    System.out.println("Error: unknown keyword \"" +
                                       checks[index] + "\"");
                    System.exit(0);
                }
            }
        }
        return false;
    }

    String getErrorReturnValue(CFunc cfunc) {
        CType returnType = cfunc.getType();
        boolean isVoid = returnType.isVoid();
        if (isVoid) {
            return null;
        }

        String[] checks = mChecker.getChecks(cfunc.getName());

        int index = 1;
        if (checks != null) {
            while (index < checks.length) {
                if (checks[index].equals("return")) {
                    return checks[index + 1];
                } else if (checks[index].startsWith("check")) {
                    index += 3;
                } else if (checks[index].equals("ifcheck")) {
                    index += 5;
                } else if (checks[index].equals("unsupported")) {
                    index += 1;
                } else if (checks[index].equals("nullAllowed")) {
                    index += 1;
                } else {
                    System.out.println("Error: unknown keyword \"" +
                                       checks[index] + "\"");
                    System.exit(0);
                }
            }
        }

        return null;
    }

    boolean isUnsupportedFunc(CFunc cfunc) {
        String[] checks = mChecker.getChecks(cfunc.getName());
        int index = 1;
        if (checks != null) {
            while (index < checks.length) {
                if (checks[index].equals("unsupported")) {
                    return true;
                } else if (checks[index].equals("return")) {
                    index += 2;
                } else if (checks[index].startsWith("check")) {
                    index += 3;
                } else if (checks[index].equals("ifcheck")) {
                    index += 5;
                } else if (checks[index].equals("nullAllowed")) {
                    index += 1;
                } else {
                    System.out.println("Error: unknown keyword \"" +
                                       checks[index] + "\"");
                    System.exit(0);
                }
            }
        }
        return false;
    }

    void emitNativeBoundsChecks(CFunc cfunc, String cname, PrintStream out,
            boolean isBuffer, boolean emitExceptionCheck, String offset, String remaining, String iii) {

                String[] checks = mChecker.getChecks(cfunc.getName());

                boolean lastWasIfcheck = false;

                int index = 1;
                if (checks != null) {
                    while (index < checks.length) {
                        if (checks[index].startsWith("check")) {
                            if (lastWasIfcheck) {
                                printIfcheckPostamble(out, isBuffer, emitExceptionCheck,
                                                      offset, remaining, iii);
                            }
                            lastWasIfcheck = false;
                            if (cname != null && !cname.equals(checks[index + 1])) {
                                index += 3;
                                continue;
                            }
                            out.println(iii + "if (" + remaining + " < " +
                                        checks[index + 2] +
                                        ") {");
                            if (emitExceptionCheck) {
                                out.println(iii + indent + "_exception = 1;");
                            }
                    String exceptionClassName = "IAEClass";
                    // If the "check" keyword was of the form
                    // "check_<class name>", use the class name in the
                    // exception to be thrown
                    int underscore = checks[index].indexOf('_');
                    if (underscore >= 0) {
                    exceptionClassName = checks[index].substring(underscore + 1) + "Class";
                    }
                            out.println(iii + indent +
                                        (mUseCPlusPlus ? "_env" : "(*_env)") +
                                        "->ThrowNew(" +
                                        (mUseCPlusPlus ? "" : "_env, ") +
                        exceptionClassName + ", " +
                                        "\"" +
                                        (isBuffer ?
                                         "remaining()" : "length - " + offset) +
                                        " < " + checks[index + 2] +
                                        "\");");

                            out.println(iii + indent + "goto exit;");
                            needsExit = true;
                            out.println(iii + "}");

                            index += 3;
                        } else if (checks[index].equals("ifcheck")) {
                            String[] matches = checks[index + 4].split(",");

                            if (!lastWasIfcheck) {
                                out.println(iii + "int _needed;");
                                out.println(iii +
                                            "switch (" +
                                            checks[index + 3] +
                                            ") {");
                            }

                            for (int i = 0; i < matches.length; i++) {
                                out.println("#if defined(" + matches[i] + ")");
                                out.println(iii +
                                            "    case " +
                                            matches[i] +
                                            ":");
                                out.println("#endif // defined(" + matches[i] + ")");
                            }
                            out.println(iii +
                                        "        _needed = " +
                                        checks[index + 2] +
                                        ";");
                            out.println(iii +
                                        "        break;");

                            lastWasIfcheck = true;
                            index += 5;
                        } else if (checks[index].equals("return")) {
                            // ignore
                            index += 2;
                        } else if (checks[index].equals("unsupported")) {
                            // ignore
                            index += 1;
                        } else if (checks[index].equals("nullAllowed")) {
                            // ignore
                            index += 1;
                        } else {
                            System.out.println("Error: unknown keyword \"" +
                                               checks[index] + "\"");
                            System.exit(0);
                        }
                    }
                }

                if (lastWasIfcheck) {
                    printIfcheckPostamble(out, isBuffer, emitExceptionCheck, iii);
                }
            }

    boolean hasNonConstArg(JFunc jfunc, CFunc cfunc, List<Integer> nonPrimitiveArgs) {
        if (nonPrimitiveArgs.size() > 0) {
            for (int i = nonPrimitiveArgs.size() - 1; i >= 0; i--) {
                int idx = nonPrimitiveArgs.get(i).intValue();
                int cIndex = jfunc.getArgCIndex(idx);
                if (jfunc.getArgType(idx).isArray()) {
                    if (!cfunc.getArgType(cIndex).isConst()) {
                        return true;
                    }
                } else if (jfunc.getArgType(idx).isBuffer()) {
                    if (!cfunc.getArgType(cIndex).isConst()) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    /**
     * Emit a function in several variants:
     *
     * if nativeDecl: public native <returntype> func(args);
     *
     * if !nativeDecl:
     *   if interfaceDecl:  public <returntype> func(args);
     *   if !interfaceDecl: public <returntype> func(args) { body }
     */
    void emitFunction(JFunc jfunc, PrintStream out, boolean nativeDecl, boolean interfaceDecl) {
        boolean isPointerFunc =
            jfunc.getName().endsWith("Pointer") &&
            jfunc.getCFunc().hasPointerArg();

        if (!nativeDecl && !interfaceDecl && !isPointerFunc) {
            // If it's not a pointer function, we've already emitted it
            // with nativeDecl == true
            return;
        }

        String maybeStatic = mUseStaticMethods ? "static " : "";

        if (isPointerFunc) {
            out.println(indent +
                        (nativeDecl ? "private " + maybeStatic +"native " :
                         (interfaceDecl ? "" : "public ") + maybeStatic) +
                        jfunc.getType() + " " +
                        jfunc.getName() +
                        (nativeDecl ? "Bounds" : "") +
                        "(");
        } else {
            out.println(indent +
                        (nativeDecl ? "public " + maybeStatic +"native " :
                         (interfaceDecl ? "" : "public ") + maybeStatic) +
                        jfunc.getType() + " " +
                        jfunc.getName() +
                        "(");
        }

        int numArgs = jfunc.getNumArgs();
        for (int i = 0; i < numArgs; i++) {
            String argName = jfunc.getArgName(i);
            JType argType = jfunc.getArgType(i);

            out.print(indent + indent + argType + " " + argName);
            if (i == numArgs - 1) {
                if (isPointerFunc && nativeDecl) {
                    out.println(",");
                    out.println(indent + indent + "int remaining");
                } else {
                    out.println();
                }
            } else {
                out.println(",");
            }
        }

        if (nativeDecl || interfaceDecl) {
            out.println(indent + ");");
        } else {
            out.println(indent + ") {");

            String iii = indent + indent;

            // emitBoundsChecks(jfunc, out, iii);
            emitFunctionCall(jfunc, out, iii, false);

            // Set the pointer after we call the native code, so that if
            // the native code throws an exception we don't modify the
            // pointer. We assume that the native code is written so that
            // if an exception is thrown, then the underlying glXXXPointer
            // function will not have been called.

            String fname = jfunc.getName();
            if (isPointerFunc) {
                // TODO - deal with VBO variants
                if (fname.equals("glColorPointer")) {
                    out.println(iii + "if ((size == 4) &&");
                    out.println(iii + "    ((type == GL_FLOAT) ||");
                    out.println(iii + "     (type == GL_UNSIGNED_BYTE) ||");
                    out.println(iii + "     (type == GL_FIXED)) &&");
                    out.println(iii + "    (stride >= 0)) {");
                    out.println(iii + indent + "_colorPointer = pointer;");
                    out.println(iii + "}");
                } else if (fname.equals("glNormalPointer")) {
                    out.println(iii + "if (((type == GL_FLOAT) ||");
                    out.println(iii + "     (type == GL_BYTE) ||");
                    out.println(iii + "     (type == GL_SHORT) ||");
                    out.println(iii + "     (type == GL_FIXED)) &&");
                    out.println(iii + "    (stride >= 0)) {");
                    out.println(iii + indent + "_normalPointer = pointer;");
                    out.println(iii + "}");
                } else if (fname.equals("glTexCoordPointer")) {
                    out.println(iii + "if (((size == 2) ||");
                    out.println(iii + "     (size == 3) ||");
                    out.println(iii + "     (size == 4)) &&");
                    out.println(iii + "    ((type == GL_FLOAT) ||");
                    out.println(iii + "     (type == GL_BYTE) ||");
                    out.println(iii + "     (type == GL_SHORT) ||");
                    out.println(iii + "     (type == GL_FIXED)) &&");
                    out.println(iii + "    (stride >= 0)) {");
                    out.println(iii + indent + "_texCoordPointer = pointer;");
                    out.println(iii + "}");
                } else if (fname.equals("glVertexPointer")) {
                    out.println(iii + "if (((size == 2) ||");
                    out.println(iii + "     (size == 3) ||");
                    out.println(iii + "     (size == 4)) &&");
                    out.println(iii + "    ((type == GL_FLOAT) ||");
                    out.println(iii + "     (type == GL_BYTE) ||");
                    out.println(iii + "     (type == GL_SHORT) ||");
                    out.println(iii + "     (type == GL_FIXED)) &&");
                    out.println(iii + "    (stride >= 0)) {");
                    out.println(iii + indent + "_vertexPointer = pointer;");
                    out.println(iii + "}");
                }
            }

            boolean isVoid = jfunc.getType().isVoid();

            if (!isVoid) {
                out.println(indent + indent + "return _returnValue;");
            }
            out.println(indent + "}");
        }
        out.println();
    }

    public void addNativeRegistration(String s) {
        nativeRegistrations.add(s);
    }

    public void emitNativeRegistration(String registrationFunctionName,
            PrintStream cStream) {
        cStream.println("static const char *classPathName = \"" +
                        mClassPathName +
                        "\";");
        cStream.println();

        cStream.println("static JNINativeMethod methods[] = {");

        cStream.println("{\"_nativeClassInit\", \"()V\", (void*)nativeClassInit },");

        Iterator<String> i = nativeRegistrations.iterator();
        while (i.hasNext()) {
            cStream.println(i.next());
        }

        cStream.println("};");
        cStream.println();


        cStream.println("int " + registrationFunctionName + "(JNIEnv *_env)");
        cStream.println("{");
        cStream.println(indent +
                        "int err;");

        cStream.println(indent +
                        "err = android::AndroidRuntime::registerNativeMethods(_env, classPathName, methods, NELEM(methods));");

        cStream.println(indent + "return err;");
        cStream.println("}");
    }

    public JniCodeEmitter() {
        super();
    }

    String getJniType(JType jType) {
        if (jType.isVoid()) {
            return "void";
        }

        String baseType = jType.getBaseType();
        if (jType.isPrimitive()) {
            if (baseType.equals("String")) {
                return "jstring";
            } else {
                return "j" + baseType;
            }
        } else if (jType.isArray()) {
            return "j" + baseType + "Array";
        } else {
            return "jobject";
        }
    }

    String getJniMangledName(String name) {
        name = name.replaceAll("_", "_1");
        name = name.replaceAll(";", "_2");
        name = name.replaceAll("\\[", "_3");
        return name;
    }

    public void emitJniCode(JFunc jfunc, PrintStream out) {
        CFunc cfunc = jfunc.getCFunc();

        // Emit comment identifying original C function
        //
        // Example:
        //
        // /* void glClipPlanef ( GLenum plane, const GLfloat *equation ) */
        //
        out.println("/* " + cfunc.getOriginal() + " */");

        // Emit JNI signature (name)
        //
        // Example:
        //
        // void
        // android_glClipPlanef__I_3FI
        //

        String outName = "android_" + jfunc.getName();
        boolean isPointerFunc = outName.endsWith("Pointer") &&
            jfunc.getCFunc().hasPointerArg();
        boolean isVBOPointerFunc = (outName.endsWith("Pointer") ||
            outName.endsWith("DrawElements")) &&
            !jfunc.getCFunc().hasPointerArg();
        if (isPointerFunc) {
            outName += "Bounds";
        }

        out.print("static ");
        out.println(getJniType(jfunc.getType()));
        out.print(outName);

        String rsignature = getJniName(jfunc.getType());

        String signature = "";
        int numArgs = jfunc.getNumArgs();
        for (int i = 0; i < numArgs; i++) {
            JType argType = jfunc.getArgType(i);
            signature += getJniName(argType);
        }
        if (isPointerFunc) {
            signature += "I";
        }

        // Append signature to function name
        String sig = getJniMangledName(signature).replace('.', '_');
        out.print("__" + sig);
        outName += "__" + sig;

        signature = signature.replace('.', '/');
        rsignature = rsignature.replace('.', '/');

        out.println();
        if (rsignature.length() == 0) {
            rsignature = "V";
        }

        String s = "{\"" +
            jfunc.getName() +
            (isPointerFunc ? "Bounds" : "") +
            "\", \"(" + signature +")" +
            rsignature +
            "\", (void *) " +
            outName +
            " },";
        nativeRegistrations.add(s);

        List<Integer> nonPrimitiveArgs = new ArrayList<Integer>();
        int numBufferArgs = 0;
        List<String> bufferArgNames = new ArrayList<String>();

        // Emit JNI signature (arguments)
        //
        // Example:
        //
        // (JNIEnv *_env, jobject this, jint plane, jfloatArray equation_ref, jint offset) {
        //
        out.print("  (JNIEnv *_env, jobject _this");
        for (int i = 0; i < numArgs; i++) {
            out.print(", ");
            JType argType = jfunc.getArgType(i);
            String suffix;
            if (!argType.isPrimitive()) {
                if (argType.isArray()) {
                    suffix = "_ref";
                } else {
                    suffix = "_buf";
                }
                nonPrimitiveArgs.add(new Integer(i));
                if (jfunc.getArgType(i).isBuffer()) {
                    int cIndex = jfunc.getArgCIndex(i);
                    String cname = cfunc.getArgName(cIndex);
                    bufferArgNames.add(cname);
                    numBufferArgs++;
                }
            } else {
                suffix = "";
            }

            out.print(getJniType(argType) + " " + jfunc.getArgName(i) + suffix);
        }
        if (isPointerFunc) {
            out.print(", jint remaining");
        }
        out.println(") {");

        int numArrays = 0;
        int numBuffers = 0;
        for (int i = 0; i < nonPrimitiveArgs.size(); i++) {
            int idx = nonPrimitiveArgs.get(i).intValue();
            if (jfunc.getArgType(idx).isArray()) {
                ++numArrays;
            }
            if (jfunc.getArgType(idx).isBuffer()) {
                ++numBuffers;
            }
        }

        // Emit method body

        // Emit local variable declarations for _exception and _returnValue
        //
        // Example:
        //
        // android::gl::ogles_context_t *ctx;
        //
        // jint _exception;
        // GLenum _returnValue;
        //
        CType returnType = cfunc.getType();
        boolean isVoid = returnType.isVoid();

        boolean isUnsupported = isUnsupportedFunc(cfunc);
        if (isUnsupported) {
            out.println(indent +
                        "_env->ThrowNew(UOEClass,");
            out.println(indent +
                        "    \"" + cfunc.getName() + "\");");
            if (!isVoid) {
                String retval = getErrorReturnValue(cfunc);
                out.println(indent + "return " + retval + ";");
            }
            out.println("}");
            out.println();
            return;
        }

        if (mUseContextPointer) {
            out.println(indent +
                "android::gl::ogles_context_t *ctx = getContext(_env, _this);");
        }

        boolean emitExceptionCheck = (numArrays > 0 || numBuffers > 0) &&
            hasNonConstArg(jfunc, cfunc, nonPrimitiveArgs);
        // mChecker.getChecks(cfunc.getName()) != null

        // Emit an _exeption variable if there will be error checks
        if (emitExceptionCheck) {
            out.println(indent + "jint _exception = 0;");
        }

        // Emit a single _array or multiple _XXXArray variables
        if (numBufferArgs == 1) {
                out.println(indent + "jarray _array = (jarray) 0;");
        } else {
            for (int i = 0; i < numBufferArgs; i++) {
                out.println(indent + "jarray _" + bufferArgNames.get(i) +
                            "Array = (jarray) 0;");
            }
        }
        if (!isVoid) {
            String retval = getErrorReturnValue(cfunc);
            if (retval != null) {
                out.println(indent + returnType.getDeclaration() +
                            " _returnValue = " + retval + ";");
            } else {
                out.println(indent + returnType.getDeclaration() +
                            " _returnValue;");
            }
        }

        // Emit local variable declarations for pointer arguments
        //
        // Example:
        //
        // GLfixed *eqn_base;
        // GLfixed *eqn;
        //
        String offset = "offset";
        String remaining = "_remaining";
        if (nonPrimitiveArgs.size() > 0) {
            for (int i = 0; i < nonPrimitiveArgs.size(); i++) {
                int idx = nonPrimitiveArgs.get(i).intValue();
                int cIndex = jfunc.getArgCIndex(idx);
                String cname = cfunc.getArgName(cIndex);

                CType type = cfunc.getArgType(jfunc.getArgCIndex(idx));
                String decl = type.getDeclaration();
                if (jfunc.getArgType(idx).isArray()) {
                    out.println(indent +
                                decl +
                                (decl.endsWith("*") ? "" : " ") +
                                jfunc.getArgName(idx) +
                                "_base = (" + decl + ") 0;");
                }
                remaining = (numArrays <= 1 && numBuffers <= 1) ? "_remaining" :
                    "_" + cname + "Remaining";
                out.println(indent +
                            "jint " + remaining + ";");
                out.println(indent +
                            decl +
                            (decl.endsWith("*") ? "" : " ") +
                            jfunc.getArgName(idx) +
                            " = (" + decl + ") 0;");
            }

            out.println();
        }

        // Emit 'GetPrimitiveArrayCritical' for arrays
        // Emit 'GetPointer' calls for Buffer pointers
        int bufArgIdx = 0;
        if (nonPrimitiveArgs.size() > 0) {
            for (int i = 0; i < nonPrimitiveArgs.size(); i++) {
                int idx = nonPrimitiveArgs.get(i).intValue();
                int cIndex = jfunc.getArgCIndex(idx);

                String cname = cfunc.getArgName(cIndex);
                offset = numArrays <= 1 ? "offset" :
                    cname + "Offset";
                remaining = (numArrays <= 1 && numBuffers <= 1) ? "_remaining" :
                    "_" + cname + "Remaining";

                if (jfunc.getArgType(idx).isArray()) {
                    out.println(indent +
                                "if (!" +
                                cname +
                                "_ref) {");
                    if (emitExceptionCheck) {
                        out.println(indent + indent + "_exception = 1;");
                    }
                    out.println(indent + "    " +
                                (mUseCPlusPlus ? "_env" : "(*_env)") +
                                "->ThrowNew(" +
                                (mUseCPlusPlus ? "" : "_env, ") +
                                "IAEClass, " +
                                "\"" + cname +
                                " == null\");");
                    out.println(indent + "    goto exit;");
                    needsExit = true;
                    out.println(indent + "}");

                    out.println(indent + "if (" + offset + " < 0) {");
                    if (emitExceptionCheck) {
                        out.println(indent + indent + "_exception = 1;");
                    }
                    out.println(indent + "    " +
                                (mUseCPlusPlus ? "_env" : "(*_env)") +
                                "->ThrowNew(" +
                                (mUseCPlusPlus ? "" : "_env, ") +
                                "IAEClass, " +
                                "\"" + offset + " < 0\");");
                    out.println(indent + "    goto exit;");
                    needsExit = true;
                    out.println(indent + "}");

                    out.println(indent + remaining + " = " +
                                    (mUseCPlusPlus ? "_env" : "(*_env)") +
                                    "->GetArrayLength(" +
                                    (mUseCPlusPlus ? "" : "_env, ") +
                                    cname + "_ref) - " + offset + ";");

                    emitNativeBoundsChecks(cfunc, cname, out, false,
                                           emitExceptionCheck,
                                           offset, remaining, "    ");

                    out.println(indent +
                                cname +
                                "_base = (" +
                                cfunc.getArgType(cIndex).getDeclaration() +
                                ")");
                    out.println(indent + "    " +
                                (mUseCPlusPlus ? "_env" : "(*_env)") +
                                "->GetPrimitiveArrayCritical(" +
                                (mUseCPlusPlus ? "" : "_env, ") +
                                jfunc.getArgName(idx) +
                                "_ref, (jboolean *)0);");
                    out.println(indent +
                                cname + " = " + cname + "_base + " + offset +
                                ";");
                    out.println();
                } else {
                    String array = numBufferArgs <= 1 ? "_array" :
                        "_" + bufferArgNames.get(bufArgIdx++) + "Array";

                    boolean nullAllowed = isNullAllowed(cfunc) || isPointerFunc;
                    if (nullAllowed) {
                        out.println(indent + "if (" + cname + "_buf) {");
                        out.print(indent);
                    }

                    if (isPointerFunc) {
                        out.println(indent +
                                cname +
                                " = (" +
                                cfunc.getArgType(cIndex).getDeclaration() +
                                ") getDirectBufferPointer(_env, " +
                                cname + "_buf);");
                        String iii = "    ";
                        out.println(iii + indent + "if ( ! " + cname + " ) {");
                        out.println(iii + iii + indent + "return;");
                        out.println(iii + indent + "}");
                    } else {
                        out.println(indent +
                                    cname +
                                    " = (" +
                                    cfunc.getArgType(cIndex).getDeclaration() +
                                    ")getPointer(_env, " +
                                    cname +
                                    "_buf, &" + array + ", &" + remaining +
                                    ");");
                    }

                    emitNativeBoundsChecks(cfunc, cname, out, true,
                                           emitExceptionCheck,
                                           offset, remaining, nullAllowed ? "        " : "    ");

                    if (nullAllowed) {
                        out.println(indent + "}");
                    }
                }
            }
        }

        if (!isVoid) {
            out.print(indent + "_returnValue = ");
        } else {
            out.print(indent);
        }
        String name = cfunc.getName();

        if (mUseContextPointer) {
            name = name.substring(2, name.length()); // Strip off 'gl' prefix
            name = name.substring(0, 1).toLowerCase() +
                name.substring(1, name.length());
            out.print("ctx->procs.");
        }

        out.print(name + (isPointerFunc ? "Bounds" : "") + "(");

        numArgs = cfunc.getNumArgs();
        if (numArgs == 0) {
            if (mUseContextPointer) {
                out.println("ctx);");
            } else {
                out.println(");");
            }
        } else {
            if (mUseContextPointer) {
                out.println("ctx,");
            } else {
                out.println();
            }
            for (int i = 0; i < numArgs; i++) {
                String typecast;
                if (i == numArgs - 1 && isVBOPointerFunc) {
                    typecast = "const GLvoid *";
                } else {
                    typecast = cfunc.getArgType(i).getDeclaration();
                }
                out.print(indent + indent +
                          "(" +
                          typecast +
                          ")" +
                          cfunc.getArgName(i));

                if (i == numArgs - 1) {
                    if (isPointerFunc) {
                        out.println(",");
                        out.println(indent + indent + "(GLsizei)remaining");
                    } else {
                        out.println();
                    }
                } else {
                    out.println(",");
                }
            }
            out.println(indent + ");");
        }

        if (needsExit) {
            out.println();
            out.println("exit:");
            needsExit = false;
        }

        bufArgIdx = 0;
        if (nonPrimitiveArgs.size() > 0) {
            for (int i = nonPrimitiveArgs.size() - 1; i >= 0; i--) {
                int idx = nonPrimitiveArgs.get(i).intValue();

                int cIndex = jfunc.getArgCIndex(idx);
                if (jfunc.getArgType(idx).isArray()) {

                    // If the argument is 'const', GL will not write to it.
                    // In this case, we can use the 'JNI_ABORT' flag to avoid
                    // the need to write back to the Java array
                    out.println(indent +
                                "if (" + jfunc.getArgName(idx) + "_base) {");
                    out.println(indent + indent +
                                (mUseCPlusPlus ? "_env" : "(*_env)") +
                                "->ReleasePrimitiveArrayCritical(" +
                                (mUseCPlusPlus ? "" : "_env, ") +
                                jfunc.getArgName(idx) + "_ref, " +
                                cfunc.getArgName(cIndex) +
                                "_base,");
                    out.println(indent + indent + indent +
                                (cfunc.getArgType(cIndex).isConst() ?
                                 "JNI_ABORT" :
                                 "_exception ? JNI_ABORT: 0") +
                                ");");
                    out.println(indent + "}");
                } else if (jfunc.getArgType(idx).isBuffer()) {
                    if (! isPointerFunc) {
                        String array = numBufferArgs <= 1 ? "_array" :
                            "_" + bufferArgNames.get(bufArgIdx++) + "Array";
                        out.println(indent + "if (" + array + ") {");
                        out.println(indent + indent +
                                    "releasePointer(_env, " + array + ", " +
                                    cfunc.getArgName(cIndex) +
                                    ", " +
                                    (cfunc.getArgType(cIndex).isConst() ?
                                     "JNI_FALSE" : "_exception ? JNI_FALSE :" +
                                             " JNI_TRUE") +
                                    ");");
                        out.println(indent + "}");
                    }
                }
            }
        }

        if (!isVoid) {
            out.println(indent + "return _returnValue;");
        }

        out.println("}");
        out.println();
    }

}
