/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.PrintStream;
import java.util.Arrays;
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 boolean mUseSimpleMethodNames = false;
    protected boolean mUseHideCommentForAPI = 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.isEGLHandle()) {
            return (jType.isArray() ? "[" : "" ) + "Landroid/opengl/" + jType.getBaseType() + ";";
        } else 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 += "J";
        } else if (baseType.equals("byte")) {
            jniName += "B";
        } else if (baseType.equals("String")) {
            jniName += "Ljava/lang/String;";
        } else if (baseType.equals("void")) {
            // nothing.
        } else {
            throw new RuntimeException("Unknown primitive basetype " + baseType);
        }
        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);
            }
            // Don't create IOBuffer versions of the EGL functions
            if (cfunc.hasEGLHandleArg()) {
                return;
            }
            // eglGetPlatformDisplay does not have any EGLHandleArgs
            // but we do not want to create IOBuffers of this, so
            // exit
            if (cfunc.getName().equals("eglGetPlatformDisplay")) {
                return;
            }
        }

        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) {
        if (mUseHideCommentForAPI) {
            out.println("    /* @hide C function " + jfunc.getCFunc().getOriginal() + " */");
            out.println();
        } else {
            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);
    }

    boolean isPointerFunc(JFunc jfunc) {
        String name = jfunc.getName();
        return (name.endsWith("Pointer") || name.endsWith("PointerOES"))
            && jfunc.getCFunc().hasPointerArg();
    }

    void emitFunctionCall(JFunc jfunc, PrintStream out, String iii, boolean grabArray) {
        boolean isVoid = jfunc.getType().isVoid();
        boolean isPointerFunc = isPointerFunc(jfunc);

        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 + ");");
    }

    // Function to automatically generate properly formatted function calls that
    // comply with clang format rules
    public static String formatFunctionCall(String indent, String functionCall) {
        final int MAXLEN = 100;
        String tokens[] = functionCall.split("\\(|\\)", 2);
        String params[] = tokens[1].split(",\\s*");
        String formatted = indent + tokens[0] + "(";
        char[] chars = new char[indent.length() + tokens[0].length() + 1];
        Arrays.fill(chars, ' ');
        String multiIndent = new String(chars);
        ArrayList<String> lines = new ArrayList<String>();
        for(int i = 0; i < params.length; i++) {
            String terminator = ((i == params.length - 1) ? "" : ",");
            if(indent.length() + formatted.length() + params[i].length() > MAXLEN) {
                lines.add(formatted);
                if (!indent.equals(multiIndent)) {
                    indent = multiIndent;
                }
                formatted = indent + params[i] + terminator;
            } else {
              formatted += (i == 0 ? "" : " ") + params[i] + terminator;
            }
        }
        lines.add(formatted);
        return String.join("\n", lines);
    }

    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 = 1;");
        out.println(iii + "        break;");
        out.println(iii + "}");

        out.println(iii + "if (" + remaining + " < _needed) {");
        out.println(iii + indent + "_exception = 1;");
        out.println(iii + indent +
                "_exceptionType = \"java/lang/IllegalArgumentException\";");
        out.println(iii + indent +
                "_exceptionMessage = \"" +
                (isBuffer ? "remaining()" : "length - " + offset) +
                " < needed\";");
        out.println(iii + indent + "goto exit;");
        out.println(iii + "}");

        needsExit = true;
    }

    boolean isNullAllowed(CFunc cfunc, String cname) {
        String[] checks = mChecker.getChecks(cfunc.getName());
        int index = 1;
        if (checks != null) {
            while (index < checks.length) {
                if (checks[index].equals("nullAllowed") &&
                    checks[index + 1].equals(cname)) {
                    return true;
                } else {
                    index = skipOneCheck(checks, index);
                }
            }
        }
        return false;
    }

    boolean hasCheckTest(CFunc cfunc) {
        String[] checks = mChecker.getChecks(cfunc.getName());
        int index = 1;
        if (checks != null) {
            while (index < checks.length) {
                if (checks[index].startsWith("check")) {
                    return true;
                } else {
                    index = skipOneCheck(checks, index);
                }
            }
        }
        return false;
    }

    boolean hasCheckTest(CFunc cfunc, String cname) {
        String[] checks = mChecker.getChecks(cfunc.getName());
        int index = 1;
        if (checks != null) {
            while (index < checks.length) {
                if (checks[index].startsWith("check") &&
                    cname != null && cname.equals(checks[index + 1])) {
                    return true;
                } else {
                    index = skipOneCheck(checks, index);
                }
            }
        }
        return false;
    }

    boolean hasIfTest(CFunc cfunc) {
        String[] checks = mChecker.getChecks(cfunc.getName());
        int index = 1;
        if (checks != null) {
            while (index < checks.length) {
                if (checks[index].startsWith("ifcheck")) {
                    return true;
                } else {
                    index = skipOneCheck(checks, index);
                }
            }
        }
        return false;
    }

    int skipOneCheck(String[] checks, int index) {
        if (checks[index].equals("return")) {
            index += 2;
        } else if (checks[index].startsWith("check")) {
            index += 3;
        } else if (checks[index].startsWith("sentinel")) {
            index += 3;
        } else if (checks[index].equals("ifcheck")) {
            index += 5;
        } else if (checks[index].equals("unsupported")) {
            index += 1;
        } else if (checks[index].equals("requires")) {
            index += 2;
        } else if (checks[index].equals("nullAllowed")) {
            index += 2;
        } else {
            System.out.println("Error: unknown keyword \"" +
                               checks[index] + "\"");
            System.exit(0);
        }

        return index;
    }

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

        if (returnType.getBaseType().startsWith("EGL")) {
            return "(" + returnType.getDeclaration() + ") 0";
        }

        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 {
                    index = skipOneCheck(checks, index);
                }
            }
        }

        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 {
                    index = skipOneCheck(checks, index);
                }
            }
        }
        return false;
    }

    String isRequiresFunc(CFunc cfunc) {
        String[] checks = mChecker.getChecks(cfunc.getName());
        int index = 1;
        if (checks != null) {
            while (index < checks.length) {
                if (checks[index].equals("requires")) {
                    return checks[index+1];
                } else {
                    index = skipOneCheck(checks, index);
                }
            }
        }
        return null;
    }

    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] + ") {");
                    out.println(iii + indent + "_exception = 1;");
                    String exceptionClassName = "java/lang/IllegalArgumentException";
                    // 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) {
                        String abbr = checks[index].substring(underscore + 1);
                        if (abbr.equals("AIOOBE")) {
                            exceptionClassName = "java/lang/ArrayIndexOutOfBoundsException";
                        } else {
                            throw new RuntimeException("unknown exception abbreviation: " + abbr);
                        }
                    }
                    out.println(iii + indent +
                                "_exceptionType = \""+exceptionClassName+"\";");
                    out.println(iii + indent +
                               "_exceptionMessage = \"" +
                               (isBuffer ? "remaining()" : "length - " +
                               offset) + " < " + checks[index + 2] +
                               " < needed\";");

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

                    needsExit = true;

                    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 {
                    index = skipOneCheck(checks, index);
                }
            }
        }

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

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

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

        int index = 1;
        if (checks != null) {
            while (index < checks.length) {
                if (checks[index].startsWith("sentinel")) {
                    if (cname != null && !cname.equals(checks[index + 1])) {
                        index += 3;
                        continue;
                    }

                    out.println(iii + cname + "_sentinel = false;");
                    out.println(iii + "for (int i = " + remaining +
                                " - 1; i >= 0; i--)  {");
                    out.println(iii + indent + "if (" + cname +
                                "[i] == " + checks[index + 2] + "){");
                    out.println(iii + indent + indent +
                                cname + "_sentinel = true;");
                    out.println(iii + indent + indent + "break;");
                    out.println(iii + indent + "}");
                    out.println(iii + "}");
                    out.println(iii +
                                "if (" + cname + "_sentinel == false) {");
                    out.println(iii + indent + "_exception = 1;");
                    out.println(iii + indent +
                                "_exceptionType = \"java/lang/IllegalArgumentException\";");
                    out.println(iii + indent + "_exceptionMessage = \"" + cname +
                                " must contain " + checks[index + 2] + "!\";");
                    out.println(iii + indent + "goto exit;");
                    out.println(iii + "}");

                    needsExit = true;
                    index += 3;
                } else {
                    index = skipOneCheck(checks, index);
                }
            }
        }
    }

    void emitStringCheck(CFunc cfunc, String cname, PrintStream out, String iii) {

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

        int index = 1;
        if (checks != null) {
            while (index < checks.length) {
                if (checks[index].startsWith("check")) {
                    if (cname != null && !cname.equals(checks[index + 1])) {
                    index += 3;
                    continue;
                }
                    out.println(iii + "_stringlen = _env->GetStringUTFLength(" + cname + ");");
                    out.println(iii + "if (" + checks[index + 2] + " > _stringlen) {");
                    out.println(iii + indent + "_exception = 1;");
                    out.println(iii + indent +
                            "_exceptionType = \"java/lang/ArrayIndexOutOfBoundsException\";");
                    out.println(iii + indent +
                            "_exceptionMessage = \"length of " + cname + " is shorter than " +
                            checks[index + 2] + " argument\";");
                    out.println(iii + indent + "goto exit;");
                    out.println(iii + "}");
                    index += 3;
                    needsExit = true;
                } else {
                    index = skipOneCheck(checks, index);
                }
            }
        }
    }

    void emitLocalVariablesForSentinel(CFunc cfunc, PrintStream out) {

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

        int index = 1;
        if (checks != null) {
            while (index < checks.length) {
                if (checks[index].startsWith("sentinel")) {
                    String cname = checks[index + 1];
                    out.println(indent + "bool " + cname + "_sentinel = false;");

                    index += 3;

                } else {
                    index = skipOneCheck(checks, index);
                }
            }
        }
    }

    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 = isPointerFunc(jfunc);

        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 + "}");
                } else if (fname.equals("glPointSizePointerOES")) {
                    out.println(iii + "if (((type == GL_FLOAT) ||");
                    out.println(iii + "     (type == GL_FIXED)) &&");
                    out.println(iii + "    (stride >= 0)) {");
                    out.println(iii + indent + "_pointSizePointerOES = pointer;");
                    out.println(iii + "}");
                } else if (fname.equals("glMatrixIndexPointerOES")) {
                    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 + "_matrixIndexPointerOES = pointer;");
                    out.println(iii + "}");
                } else if (fname.equals("glWeightPointer")) {
                    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 + "_weightPointerOES = 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 const 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 jType.isClass() ? "jobjectArray" : "j" + baseType + "Array";
        } else {
            return "jobject";
        }
    }

    String getJniDefaultReturn(JType jType) {
        if (jType.isPrimitive()) {
            String baseType = jType.getBaseType();
            if (baseType.equals("boolean")) {
                return "JNI_FALSE";
            } else {
                return "(" + getJniType(jType) + ")0";
            }
        } else {
            return "nullptr";
        }
    }

    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 = isPointerFunc(jfunc);
        boolean isPointerOffsetFunc =
            (outName.endsWith("Pointer") || outName.endsWith("PointerOES") ||
             outName.endsWith("glDrawElements") ||
             outName.endsWith("glDrawRangeElements") ||
             outName.endsWith("glTexImage2D") ||
             outName.endsWith("glTexSubImage2D") ||
             outName.endsWith("glCompressedTexImage2D") ||
             outName.endsWith("glCompressedTexSubImage2D") ||
             outName.endsWith("glTexImage3D") ||
             outName.endsWith("glTexSubImage3D") ||
             outName.endsWith("glCompressedTexImage3D") ||
             outName.endsWith("glCompressedTexSubImage3D") ||
             outName.endsWith("glReadPixels"))
            && !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('.', '_').replace('/', '_');
        if (!mUseSimpleMethodNames) {
            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>();
        List<Integer> stringArgs = new ArrayList<Integer>();
        int numBufferArgs = 0;
        List<String> bufferArgNames = new ArrayList<String>();
        List<JType> bufferArgTypes = new ArrayList<JType>();

        // 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 if (argType.isBuffer()) {
                    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);
                    bufferArgTypes.add(jfunc.getArgType(i));
                    numBufferArgs++;
                }
            }

            if (argType.isString()) {
                stringArgs.add(new Integer(i));
            }

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

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

        // 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 +
                        "jniThrowException(_env, \"java/lang/UnsupportedOperationException\",");
            out.println(indent +
                        "    \"" + cfunc.getName() + "\");");
            if (isVoid) {
                out.println(indent + "return;");
            } else {
                if (cfunc.getType().isEGLHandle()) {
                    String baseType = cfunc.getType().getBaseType().toLowerCase();
                    out.println(indent + indent + "return nullptr;");
                } else {
                    out.println(indent + indent + "return " +
                                getJniDefaultReturn(jfunc.getType()) + ";");
                }
            }
            out.println("}");
            out.println();
            return;
        }

        String requiresExtension = isRequiresFunc(cfunc);
        if (requiresExtension != null) {
            out.println(indent +
                        "if (! supportsExtension(_env, _this, have_" + requiresExtension + "ID)) {");
            out.println(indent + indent +
                        "jniThrowException(_env, \"java/lang/UnsupportedOperationException\",");
            out.println(indent + indent +
                        "    \"" + cfunc.getName() + "\");");
            if (isVoid) {
                out.println(indent + indent + "    return;");
            } else {
                String retval = getErrorReturnValue(cfunc);
                if (cfunc.getType().isEGLHandle()) {
                    String baseType = cfunc.getType().getBaseType().toLowerCase();
                    out.println(indent +
                                "return toEGLHandle(_env, " + baseType + "Class, " +
                                baseType + "Constructor, " + retval + ");");
                } else {
                    out.println(indent + "return " + retval + ";");
                }
            }
            out.println(indent + "}");
        }
        if (mUseContextPointer) {
            out.println(indent +
                "android::gl::ogles_context_t *ctx = getContext(_env, _this);");
        }

        boolean initializeReturnValue = stringArgs.size() > 0;
        boolean emitExceptionCheck = ((numArrays > 0 || numStrings > 0)
                                             && (hasNonConstArg(jfunc, cfunc, nonPrimitiveArgs)
                                                 || (cfunc.hasPointerArg() && numArrays > 0))
                                         || (numBufferArgs > 0)
                                         || hasCheckTest(cfunc)
                                         || hasIfTest(cfunc))
                                         || (stringArgs.size() > 0);
        // mChecker.getChecks(cfunc.getName()) != null
        // Emit an _exeption variable if there will be error checks
        if (emitExceptionCheck) {
            out.println(indent + "jint _exception = 0;");
            out.println(indent + "const char * _exceptionType = NULL;");
            out.println(indent + "const char * _exceptionMessage = NULL;");
        }

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

        // Emit local variable declarations for EGL Handles
        //
        // Example:
        //
        // EGLSurface surface_native = (EGLHandle)fromEGLHandle(_env, surfaceClass, surfaceConstructor, surface);
        //
        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);

                if (jfunc.getArgType(idx).isBuffer()
                   || jfunc.getArgType(idx).isArray()
                   || !jfunc.getArgType(idx).isEGLHandle())
                    continue;

                CType type = cfunc.getArgType(jfunc.getArgCIndex(idx));
                String decl = type.getDeclaration();
                out.println(indent +
                            decl + " " + cname + "_native = (" +
                            decl + ") fromEGLHandle(_env, " +
                            type.getBaseType().toLowerCase() +
                            "GetHandleID, " + jfunc.getArgName(idx) +
                            ");");
            }
        }

        // Emit local variable declarations for element/sentinel checks
        //
        // Example:
        //
        // bool attrib_list_sentinel_found = false;
        //
        emitLocalVariablesForSentinel(cfunc, out);

        // 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);

                if (!jfunc.getArgType(idx).isBuffer() && !jfunc.getArgType(idx).isArray())
                    continue;

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

            out.println();
        }

        // Emit local variable declaration for strings
        if (stringArgs.size() > 0) {
            boolean requiresStringLengthCheck = false;
            for (int i = 0; i < stringArgs.size(); i++) {
                int idx = stringArgs.get(i).intValue();
                int cIndex = jfunc.getArgCIndex(idx);
                String cname = cfunc.getArgName(cIndex);

                out.println(indent + "const char* _native" + cname + " = 0;");
                if (hasCheckTest(cfunc, cname)) {
                    requiresStringLengthCheck = true;
                }
            }

            if (requiresStringLengthCheck) {
                out.println(indent + "jsize _stringlen = 0;");
            }

            out.println();
        }

        // Null pointer checks and GetStringUTFChars
        if (stringArgs.size() > 0) {
            for (int i = 0; i < stringArgs.size(); i++) {
                int idx = stringArgs.get(i).intValue();
                int cIndex = jfunc.getArgCIndex(idx);
                String cname = cfunc.getArgName(cIndex);

                boolean nullAllowed = isNullAllowed(cfunc, cname);
                String nullAllowedIndent = nullAllowed ? indent : "";

                CType type = cfunc.getArgType(jfunc.getArgCIndex(idx));
                String decl = type.getDeclaration();

                if (nullAllowed) {
                    out.println(indent + "if (" + cname + ") {");
                } else {
                    needsExit = true;
                    out.println(indent + "if (!" + cname + ") {");
                    out.println(indent + indent + "_exception = 1;");
                    out.println(indent + indent +
                            "_exceptionType = \"java/lang/IllegalArgumentException\";");
                    out.println(indent + indent +
                            "_exceptionMessage = \"" + cname + " == null\";");
                    out.println(indent + indent + "goto exit;");
                    out.println(indent + "}");
                }

                out.println(nullAllowedIndent + indent + "_native" + cname +
                        " = _env->GetStringUTFChars(" + cname + ", 0);");

                emitStringCheck(cfunc, cname, out, nullAllowedIndent + indent);

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

            out.println();
        }

        // Emit 'GetPrimitiveArrayCritical' for non-object arrays
        // Emit 'GetPointer' calls for Buffer pointers
        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 + numBuffers) <= 1) ? "_remaining" :
                    "_" + cname + "Remaining";

                boolean nullAllowed = isNullAllowed(cfunc, cname);
                String nullAllowedIndent = nullAllowed ? indent : "";

                if (jfunc.getArgType(idx).isArray()
                       && !jfunc.getArgType(idx).isEGLHandle()) {
                    needsExit = true;

                    if (nullAllowed) {
                        out.println(indent + "if (" + cname + "_ref) {");
                    }
                    else
                    {
                        out.println(indent + "if (!" + cname + "_ref) {");
                        out.println(indent + indent + "_exception = 1;");
                        out.println(indent + indent +
                                "_exceptionType = " +
                                "\"java/lang/IllegalArgumentException\";");
                        out.println(indent + indent +
                                "_exceptionMessage = \"" + cname +
                                " == null\";");
                        out.println(indent + indent + "goto exit;");
                        out.println(indent + "}");
                    }

                    out.println(nullAllowedIndent + indent + "if (" + offset +
                            " < 0) {");
                    out.println(nullAllowedIndent + indent + indent +
                            "_exception = 1;");
                    out.println(nullAllowedIndent + indent + indent +
                            "_exceptionType = " +
                            "\"java/lang/IllegalArgumentException\";");
                    out.println(nullAllowedIndent + indent + indent +
                            "_exceptionMessage = \"" + offset +" < 0\";");
                    out.println(nullAllowedIndent + indent + indent +
                            "goto exit;");
                    out.println(nullAllowedIndent + indent + "}");

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

                    emitNativeBoundsChecks(cfunc, cname, out, false,
                            emitExceptionCheck, offset, remaining,
                            nullAllowedIndent + indent);

                    out.println(nullAllowedIndent + indent +
                                cname +
                                "_base = (" +
                                cfunc.getArgType(cIndex).getDeclaration() +
                                ")");
                    String arrayGetter = jfunc.getArgType(idx).getArrayGetterForPrimitiveArray();
                    out.println(nullAllowedIndent + indent + "    " +
                                (mUseCPlusPlus ? "_env" : "(*_env)") +
                                "->" + arrayGetter + "(" +
                                (mUseCPlusPlus ? "" : "_env, ") +
                                jfunc.getArgName(idx) +
                                "_ref, (jboolean *)0);");
                    out.println(nullAllowedIndent + indent +
                                cname + " = " + cname + "_base + " + offset + ";");

                    emitSentinelCheck(cfunc, cname, out, false,
                            emitExceptionCheck, offset, remaining,
                            nullAllowedIndent + indent);

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

                    out.println();
                } else if (jfunc.getArgType(idx).isArray()
                              && jfunc.getArgType(idx).isEGLHandle()) {
                    needsExit = true;

                    if (nullAllowed) {
                        out.println(indent + "if (" + cname + "_ref) {");
                    }
                    else
                    {
                        out.println(indent + "if (!" + cname + "_ref) {");
                        out.println(indent + indent + "_exception = 1;");
                        out.println(indent + indent + "_exceptionType = " +
                                "\"java/lang/IllegalArgumentException\";");
                        out.println(indent + indent + "_exceptionMessage = \"" +
                                cname +" == null\";");
                        out.println(indent + indent + "goto exit;");
                        out.println(indent + "}");
                    }

                    out.println(nullAllowedIndent + indent + "if (" + offset +
                            " < 0) {");
                    out.println(nullAllowedIndent + indent + indent +
                            "_exception = 1;");
                    out.println(nullAllowedIndent + indent + indent +
                            "_exceptionType = " +
                            "\"java/lang/IllegalArgumentException\";");
                    out.println(nullAllowedIndent + indent + indent +
                            "_exceptionMessage = \"" + offset +" < 0\";");
                    out.println(nullAllowedIndent + indent + indent +
                            "goto exit;");
                    out.println(nullAllowedIndent + indent + "}");

                    out.println(nullAllowedIndent + indent + remaining + " = " +
                                    (mUseCPlusPlus ? "_env" : "(*_env)") +
                                    "->GetArrayLength(" +
                                    (mUseCPlusPlus ? "" : "_env, ") +
                                    cname + "_ref) - " + offset + ";");
                    emitNativeBoundsChecks(cfunc, cname, out, false,
                            emitExceptionCheck, offset, remaining,
                            nullAllowedIndent + indent);
                    out.println(nullAllowedIndent + indent +
                                jfunc.getArgName(idx) + " = new " +
                                cfunc.getArgType(cIndex).getBaseType() +
                               "["+ remaining + "];");

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

                    out.println();
                } else if (jfunc.getArgType(idx).isBuffer()) {
                    needsExit = needsExit || (!nullAllowed && !isPointerFunc);

                    String array = numBufferArgs <= 1 ? "_array" :
                        "_" + cfunc.getArgName(cIndex) + "Array";
                    String bufferOffset = numBufferArgs <= 1 ? "_bufferOffset" :
                        "_" + cfunc.getArgName(cIndex) + "BufferOffset";

                    nullAllowed = nullAllowed || isPointerFunc;
                    if (nullAllowed) {
                        out.println(indent + "if (" + cname + "_buf) {");
                        out.print(indent);
                    }
                    else
                    {
                        out.println(indent + "if (!" + cname + "_buf) {");
                        out.println(indent + indent + "_exception = 1;");
                        out.println(indent + indent + "_exceptionType = " +
                                "\"java/lang/IllegalArgumentException\";");
                        out.println(indent + indent + "_exceptionMessage = \"" +
                                cname +" == null\";");
                        out.println(indent + indent + "goto exit;");
                        out.println(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 + indent + indent + "return;");
                        out.println(iii + indent + "}");
                    } else {
                        out.println(indent +
                                    cname +
                                    " = (" +
                                    cfunc.getArgType(cIndex).getDeclaration() +
                                    ")getPointer(_env, " +
                                    cname +
                                    "_buf, (jarray*)&" + array + ", &" + remaining + ", &" + bufferOffset +
                                    ");");
                    }

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

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

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

                if(!jfunc.getArgType(idx).isBuffer() || isPointerFunc) continue;

                String cname = cfunc.getArgName(cIndex);
                String bufferOffset = numBufferArgs <= 1 ? "_bufferOffset" :
                            "_" + cname + "BufferOffset";
                String array = numBufferArgs <= 1 ? "_array" :
                            "_" + cfunc.getArgName(cIndex) + "Array";

                boolean nullAllowed = isNullAllowed(cfunc, cname) ||
                        isPointerFunc;
                if (nullAllowed) {
                    out.println(indent + "if (" + cname + "_buf && " + cname +" == NULL) {");
                } else {
                    out.println(indent + "if (" + cname +" == NULL) {");
                }
                JType argType = jfunc.getArgType(idx);
                if (argType.isTypedBuffer()) {
                    String arrayGetter = argType.getArrayTypeForTypedBuffer().getArrayGetterForPrimitiveArray();
                    out.println(indent + indent + "char * _" + cname + "Base = (char *)_env->" + arrayGetter + "(" + array + ", (jboolean *) 0);");
                    out.println(indent + indent + cname + " = (" +cfunc.getArgType(cIndex).getDeclaration() +") (_" + cname + "Base + " + bufferOffset + ");");
                    out.println(indent + "}");
                } else {
                    out.println(indent + indent + "char * _" + cname + "Base = (char *)_env->GetPrimitiveArrayCritical(" + array + ", (jboolean *) 0);");
                    out.println(indent + indent + cname + " = (" +cfunc.getArgType(cIndex).getDeclaration() +") (_" + cname + "Base + " + bufferOffset + ");");
                    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 && isPointerOffsetFunc) {
                    typecast = "reinterpret_cast<GLvoid *>";
                } else {
                    typecast = "(" + cfunc.getArgType(i).getDeclaration() + ")";
                }
                out.print(indent + indent +
                          typecast);

                if (cfunc.getArgType(i).isConstCharPointer()) {
                    out.print("_native");
                }

                if (cfunc.getArgType(i).isEGLHandle() &&
                    !cfunc.getArgType(i).isPointer()){
                    out.print(cfunc.getArgName(i)+"_native");
                } else if (i == numArgs - 1 && isPointerOffsetFunc){
                    out.print("("+cfunc.getArgName(i)+")");
                } else {
                    out.print(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;
        }


        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() && !jfunc.getArgType(idx).isClass()) {

                    // 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) {");
                    String arrayReleaser = jfunc.getArgType(idx).getArrayReleaserForPrimitiveArray();
                    out.println(indent + indent +
                                (mUseCPlusPlus ? "_env" : "(*_env)") +
                                "->" + arrayReleaser + "(" +
                                (mUseCPlusPlus ? "" : "_env, ") +
                                jfunc.getArgName(idx) + "_ref, " +
                                "(j" + jfunc.getArgType(idx).getBaseType() + "*)" + 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) {
                        JType argType = jfunc.getArgType(idx);
                        String array = numBufferArgs <= 1 ? "_array" :
                            "_" + cfunc.getArgName(cIndex) + "Array";
                        out.println(indent + "if (" + array + ") {");
                        if (argType.isTypedBuffer()) {
                            String arrayReleaser =
                                argType.getArrayTypeForTypedBuffer().getArrayReleaserForPrimitiveArray();
                            out.println(indent + indent +
                                "_env->" + arrayReleaser + "(" + array + ", " +
                                "(j" + argType.getArrayTypeForTypedBuffer().getBaseType() + "*)" +
                                cfunc.getArgName(cIndex) +
                                ", " +
                                (cfunc.getArgType(cIndex).isConst() ?
                                    "JNI_ABORT" : (emitExceptionCheck ?
                                        "_exception ? JNI_ABORT : 0" : "0")) +
                                ");");
                        } else {
                            String bufferOffset = numBufferArgs <= 1 ? "_bufferOffset" :
                                "_" + cfunc.getArgName(cIndex) + "BufferOffset";
                            String typeCast = "(char *)" + cfunc.getArgName(cIndex);
                            String withOffset = "(void *)(" + typeCast + " - " + bufferOffset + ")";
                            String releasePointerCall = (
                                "releasePointer(_env, " + array + ", " +
                                withOffset +
                                ", " +
                                (cfunc.getArgType(cIndex).isConst() ?
                                    "JNI_FALSE" : (emitExceptionCheck ?
                                        "_exception ? JNI_FALSE : JNI_TRUE" : "JNI_TRUE")) +
                                ");");
                            out.println(formatFunctionCall(indent + indent, releasePointerCall));
                        }
                        out.println(indent + "}");
                    }
                }
            }
        }

        // Emit local variable declaration for strings
        if (stringArgs.size() > 0) {
            for (int i = 0; i < stringArgs.size(); i++) {
                int idx = stringArgs.get(i).intValue();
                int cIndex = jfunc.getArgCIndex(idx);
                String cname = cfunc.getArgName(cIndex);

                out.println(indent + "if (_native" + cname + ") {");
                out.println(indent + "    _env->ReleaseStringUTFChars(" + cname + ", _native" + cname + ");");
                out.println(indent + "}");
            }

            out.println();
        }

        // Copy results back to java arrays
       if (nonPrimitiveArgs.size() > 0) {
            for (int i = nonPrimitiveArgs.size() - 1; i >= 0; i--) {
                int idx = nonPrimitiveArgs.get(i).intValue();
                int cIndex = jfunc.getArgCIndex(idx);
                String baseType = cfunc.getArgType(cIndex).getBaseType().toLowerCase();
                if (jfunc.getArgType(idx).isArray() && jfunc.getArgType(idx).isClass()) {
                    remaining  = ((numArrays + numBuffers) <= 1) ? "_remaining" :
                                     "_" + cfunc.getArgName(cIndex) + "Remaining";
                    offset = numArrays <= 1 ? "offset" : cfunc.getArgName(cIndex) + "Offset";
                    out.println(indent +
                                "if (" + jfunc.getArgName(idx) + ") {");
                    out.println(indent + indent +
                                "for (int i = 0; i < " + remaining + "; i++) {");
                    out.println(indent + indent + indent +
                                "jobject " + cfunc.getArgName(cIndex) +
                                "_new = toEGLHandle(_env, " + baseType +
                                "Class, " + baseType + "Constructor, " +
                                cfunc.getArgName(cIndex) + "[i]);");
                    out.println(indent + indent + indent +
                                (mUseCPlusPlus ? "_env" : "(*_env)") +
                                "->SetObjectArrayElement(" +
                                (mUseCPlusPlus ? "" : "_env, ") +
                                cfunc.getArgName(cIndex) +
                                "_ref, i + " + offset + ", " +
                                cfunc.getArgName(cIndex) + "_new);");
                    out.println(indent + indent + "}");
                    out.println(indent + indent +
                                "delete[] " + jfunc.getArgName(idx) + ";");
                    out.println(indent + "}");
                }
            }
        }


        // Throw exception if there is one
        if (emitExceptionCheck) {
            out.println(indent + "if (_exception) {");
            out.println(indent + indent +
                        "jniThrowException(_env, _exceptionType, _exceptionMessage);");
            if (!isVoid) {
                if (cfunc.getType().isEGLHandle()) {
                    String baseType = cfunc.getType().getBaseType().toLowerCase();
                    out.println(indent + indent + "return nullptr;");
                } else {
                    out.println(indent + indent + "return " +
                                getJniDefaultReturn(jfunc.getType()) + ";");
                }
            }

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


        if (!isVoid) {
            if (cfunc.getType().isEGLHandle()) {
                String baseType = cfunc.getType().getBaseType().toLowerCase();
                out.println(indent +
                            "return toEGLHandle(_env, " + baseType + "Class, " +
                            baseType + "Constructor, _returnValue);");
            } else {
                out.println(indent + "return (" +
                            getJniType(jfunc.getType()) + ")_returnValue;");
            }
        }

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

}
