/*
 * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * - Redistribution of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistribution in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * Neither the name of Sun Microsystems, Inc. or the names of
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
 * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
 * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
 * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
 * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
 * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
 * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed or intended for use
 * in the design, construction, operation or maintenance of any nuclear
 * facility.
 *
 * Sun gratefully acknowledges that this software was originally authored
 * and developed by Kenneth Bradley Russell and Christopher John Kline.
 */

package com.sun.gluegen;

import com.jogamp.common.nio.Buffers;
import com.jogamp.common.os.DynamicLookupHelper;
import java.io.*;
import java.util.*;
import java.text.MessageFormat;

import com.sun.gluegen.cgram.types.*;
import java.nio.Buffer;
import java.util.logging.Logger;

import static java.util.logging.Level.*;
import static com.sun.gluegen.JavaEmitter.MethodAccess.*;

// PROBLEMS:
//  - what if something returns 'const int *'? Could we
//    return an IntBuffer that has read-only behavior? Or do we copy the array
//    (but we don't know its size!). What do we do if it returns a non-const
//    int*? Should the user be allowed to write back to the returned pointer?
//
//  - Non-const array types must be properly released with JNI_COMMIT
//    in order to see side effects if the array was copied.


public class JavaEmitter implements GlueEmitter {

  private StructLayout layout;
  private TypeDictionary typedefDictionary;
  private TypeDictionary structDictionary;
  private Map<Type, Type> canonMap;
  protected JavaConfiguration cfg;

  /**
   * Style of code emission. Can emit everything into one class
   * (AllStatic), separate interface and implementing classes
   * (InterfaceAndImpl), only the interface (InterfaceOnly), or only
   * the implementation (ImplOnly).
   */
  public enum EmissionStyle {AllStatic, InterfaceAndImpl, InterfaceOnly, ImplOnly};

  /**
   * Access control for emitted Java methods.
   */
  public enum MethodAccess {PUBLIC, PROTECTED, PRIVATE, PACKAGE_PRIVATE, PUBLIC_ABSTRACT}

  private PrintWriter javaWriter; // Emits either interface or, in AllStatic mode, everything
  private PrintWriter javaImplWriter; // Only used in non-AllStatic modes for impl class
  private PrintWriter cWriter;
  private MachineDescription machDesc32;
  private MachineDescription machDesc64;

  protected final static Logger LOG = Logger.getLogger(JavaEmitter.class.getPackage().getName());

  public void readConfigurationFile(String filename) throws Exception {
    cfg = createConfig();
    cfg.read(filename);
  }

  public void setMachineDescription(MachineDescription md32, MachineDescription md64) {

    if ((md32 == null) && (md64 == null)) {
      throw new RuntimeException("Must specify at least one MachineDescription");
    }

    machDesc32 = md32;
    machDesc64 = md64;
  }

  class ConstantRenamer implements SymbolFilter {

    private List<ConstantDefinition> constants;

    public void filterSymbols(List<ConstantDefinition> constants, List<FunctionSymbol> functions) {
      this.constants = constants;
      doWork();
    }

    public List<ConstantDefinition> getConstants() {
      return constants;
    }

    public List<FunctionSymbol> getFunctions() {
      return null;
    }

    private void doWork() {
      List<ConstantDefinition> newConstants = new ArrayList<ConstantDefinition>();
      JavaConfiguration cfg = getConfig();
      for (ConstantDefinition def : constants) {
        def.rename(cfg.getJavaSymbolRename(def.getName()));
        newConstants.add(def);
      }
      constants = newConstants;
    }
  }

    public void beginEmission(GlueEmitterControls controls) throws IOException {

        // Request emission of any structs requested
        for (String structs : cfg.forcedStructs()) {
            controls.forceStructEmission(structs);
        }

        if (!cfg.structsOnly()) {
            try {
                openWriters();
            } catch (Exception e) {
                throw new RuntimeException("Unable to open files for writing", e);
            }
            emitAllFileHeaders();

            // Handle renaming of constants
            controls.runSymbolFilter(new ConstantRenamer());
        }
    }

    public void endEmission() {
        if (!cfg.structsOnly()) {
            emitAllFileFooters();

            try {
                closeWriters();
            } catch (Exception e) {
                throw new RuntimeException("Unable to close open files", e);
            }
        }
    }

  public void beginDefines() throws Exception {
    if ((cfg.allStatic() || cfg.emitInterface()) && !cfg.structsOnly()) {
      javaWriter().println();
    }
  }

  protected static int getJavaRadix(String name, String value)  {
    // FIXME: need to handle when type specifier is in last char (e.g.,
    // "1.0d or 2759L", because parseXXX() methods don't allow the type
    // specifier character in the string.
    //
    //char lastChar = value.charAt(value.length()-1);

    try {
      // see if it's a long or int
      int radix;
      String parseValue;
      // FIXME: are you allowed to specify hex/octal constants with
      // negation, e.g. "-0xFF" or "-056"? If so, need to modify the
      // following "if(..)" checks and parseValue computation
      if (value.startsWith("0x") || value.startsWith("0X")) {
        radix = 16;
        parseValue = value.substring(2);
      }
      else if (value.startsWith("0") && value.length() > 1) {
        // TODO: is "0" the prefix in C to indicate octal???
        radix = 8;
        parseValue = value.substring(1);
      }
      else {
        radix = 10;
        parseValue = value;
      }
      //System.err.println("parsing " + value + " as long w/ radix " + radix);
      long longVal = Long.parseLong(parseValue, radix);
      return radix;
    } catch (NumberFormatException e) {
      try {
        // see if it's a double or float
        double dVal = Double.parseDouble(value);
        return 10;
      } catch (NumberFormatException e2) {
        throw new RuntimeException(
          "Cannot emit define \""+name+"\": value \""+value+
          "\" cannot be assigned to a int, long, float, or double", e2);
      }
    }
  }

  protected static Object getJavaValue(String name, String value) {

    // "calculates" the result type of a simple expression
    // example: (2+3)-(2.0f-3.0) -> Double
    // example: (1 << 2) -> Integer

    Scanner scanner = new Scanner(value).useDelimiter("[+-/*/></(/)]");

    Object resultType = null;

    while (scanner.hasNext()) {

        String t = scanner.next().trim();

        if(0<t.length()) {
            Object type = getJavaValue2(name, t);

            //fast path
            if(type instanceof Double)
                return type;

            if(resultType != null) {

                if(resultType instanceof Integer) {
                    if(type instanceof Long || type instanceof Float || type instanceof Double)
                        resultType = type;
                }else if(resultType instanceof Long) {
                    if(type instanceof Float || type instanceof Double)
                        resultType = type;
                }else if(resultType instanceof Float) {
                    if(type instanceof Float)
                        resultType = type;
                }
            }else{
                resultType = type;
            }

            //fast path
            if(resultType instanceof Double)
                return type;
        }
    }

    return resultType;
  }

  private static Object getJavaValue2(String name, String value) {
    // FIXME: need to handle when type specifier is in last char (e.g.,
    // "1.0d or 2759L", because parseXXX() methods don't allow the type
    // specifier character in the string.
    //
    char lastChar = value.charAt(value.length()-1);

    try {
      // see if it's a long or int
      int radix;
      String parseValue;
      // FIXME: are you allowed to specify hex/octal constants with
      // negation, e.g. "-0xFF" or "-056"? If so, need to modify the
      // following "if(..)" checks and parseValue computation
      if (value.startsWith("0x") || value.startsWith("0X")) {
        radix = 16;
        parseValue = value.substring(2);
      } else if (value.startsWith("0") && value.length() > 1) {
        // TODO: is "0" the prefix in C to indicate octal???
        radix = 8;
        parseValue = value.substring(1);
      } else {
        radix = 10;
        parseValue = value;
      }
      if(lastChar == 'u' || lastChar == 'U') {
          parseValue = parseValue.substring(0, parseValue.length()-1);
      }

      //System.err.println("parsing " + value + " as long w/ radix " + radix);
      long longVal = Long.parseLong(parseValue, radix);
      // if constant is small enough, store it as an int instead of a long
      if (longVal > Integer.MIN_VALUE && longVal < Integer.MAX_VALUE) {
        return (int)longVal;
      }
      return longVal;

    } catch (NumberFormatException e) {
      try {
        // see if it's a double or float
        double dVal = Double.parseDouble(value);
        double absVal = Math.abs(dVal);
        // if constant is small enough, store it as a float instead of a double
        if (absVal < Float.MIN_VALUE || absVal > Float.MAX_VALUE) {
            return new Double(dVal);
        }
        return new Float((float) dVal);
      } catch (NumberFormatException e2) {
        throw new RuntimeException(
          "Cannot emit define \""+name+"\": value \""+value+
          "\" cannot be assigned to a int, long, float, or double", e2);
      }
    }
  }


  protected static String getJavaType(String name, String value) {
    Object oval = getJavaValue(name, value);
    return getJavaType(name, oval);
  }

  protected static String getJavaType(String name, Object oval) {
    if(oval instanceof Integer) {
        return "int";
    } else if(oval instanceof Long) {
        return "long";
    } else if(oval instanceof Float) {
        return "float";
    } else if(oval instanceof Double) {
        return "double";
    }

    throw new RuntimeException(
      "Cannot emit define (2) \""+name+"\": value \""+oval+
      "\" cannot be assigned to a int, long, float, or double");
  }

  public void emitDefine(ConstantDefinition def, String optionalComment) throws Exception  {

    if (cfg.allStatic() || cfg.emitInterface()) {
      // TODO: Some defines (e.g., GL_DOUBLE_EXT in gl.h) are defined in terms
      // of other defines -- should we emit them as references to the original
      // define (not even sure if the lexer supports this)? Right now they're
      // emitted as the numeric value of the original definition. If we decide
      // emit them as references we'll also have to emit them in the correct
      // order. It's probably not an issue right now because the emitter
      // currently only emits only numeric defines -- if it handled #define'd
      // objects it would make a bigger difference.

      String name = def.getName();
      String value = def.getValue();

      if (!cfg.shouldIgnoreInInterface(name)) {
        String type = getJavaType(name, value);
        if (optionalComment != null && optionalComment.length() != 0) {
          javaWriter().println("  /** " + optionalComment + " */");
        }
        String suffix = "";
        if(!value.endsWith(")")) {
            if (type.equals("float") && !value.endsWith("f")) {
                suffix = "f";
            }else if(value.endsWith("u") || value.endsWith("U")) {
                value = value.substring(0, value.length()-1);
            }
        }

        javaWriter().println("  public static final " + type + " " + name + " = " + value + suffix + ";");
      }
    }
  }

  public void endDefines() throws Exception {
  }

  public void beginFunctions(TypeDictionary typedefDictionary,
                             TypeDictionary structDictionary,
                             Map<Type, Type> canonMap) throws Exception {

    this.typedefDictionary = typedefDictionary;
    this.structDictionary  = structDictionary;
    this.canonMap          = canonMap;

    if ((cfg.allStatic() || cfg.emitInterface()) && !cfg.structsOnly()) {
      javaWriter().println();
    }
  }

  public Iterator<FunctionSymbol> emitFunctions(List<FunctionSymbol> originalCFunctions) throws Exception {

    // Sometimes headers will have the same function prototype twice, once
    // with the argument names and once without. We'll remember the signatures
    // we've already processed we don't generate duplicate bindings.
    //
    // Note: this code assumes that on the equals() method in FunctionSymbol
    // only considers function name and argument types (i.e., it does not
    // consider argument *names*) when comparing FunctionSymbols for equality
    Set<FunctionSymbol> funcsToBindSet = new HashSet<FunctionSymbol>(100);
    for (FunctionSymbol cFunc : originalCFunctions) {
      if (!funcsToBindSet.contains(cFunc)) {
        funcsToBindSet.add(cFunc);
      }
    }

    //    validateFunctionsToBind(funcsToBindSet);

    ArrayList<FunctionSymbol> funcsToBind = new ArrayList<FunctionSymbol>(funcsToBindSet);
    // sort functions to make them easier to find in native code
    Collections.sort(funcsToBind, new Comparator<FunctionSymbol>() {
            public int compare(FunctionSymbol o1, FunctionSymbol o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });

    // Bind all the C funcs to Java methods
    HashSet<MethodBinding> methodBindingSet = new HashSet<MethodBinding>();
    ArrayList<FunctionEmitter> methodBindingEmitters = new ArrayList<FunctionEmitter>(2*funcsToBind.size());
    for (FunctionSymbol cFunc : funcsToBind) {
      // Check to see whether this function should be ignored
      if (!cfg.shouldIgnoreInImpl(cFunc.getName())) {
          methodBindingEmitters.addAll(generateMethodBindingEmitters(methodBindingSet, cFunc));
      }

    }

    // Emit all the methods
    for (FunctionEmitter emitter : methodBindingEmitters) {
      try {
        if (!emitter.isInterface() || !cfg.shouldIgnoreInInterface(emitter.getName())) {
            emitter.emit();
            emitter.getDefaultOutput().println(); // put newline after method body
        }
      } catch (Exception e) {
        throw new RuntimeException(
            "Error while emitting binding for \"" + emitter.getName() + "\"", e);
      }
    }

    // Return the list of FunctionSymbols that we generated gluecode for
    return funcsToBind.iterator();
  }

  /**
   * Create the object that will read and store configuration information for
   * this JavaEmitter.
   */
  protected JavaConfiguration createConfig() {
    return new JavaConfiguration();
  }

  /**
   * Get the configuration information for this JavaEmitter.
   */
  protected JavaConfiguration getConfig() {
    return cfg;
  }

  /**
   * Generates the public emitters for this MethodBinding which will
   * produce either simply signatures (for the interface class, if
   * any) or function definitions with or without a body (depending on
   * whether or not the implementing function can go directly to
   * native code because it doesn't need any processing of the
   * outgoing arguments).
   */
  protected void generatePublicEmitters(MethodBinding binding,
                                        List<FunctionEmitter> allEmitters,
                                        boolean signatureOnly) {
    PrintWriter writer = ((signatureOnly || cfg.allStatic()) ? javaWriter() : javaImplWriter());

    if (cfg.manuallyImplement(binding.getName()) && !signatureOnly) {
      // We only generate signatures for manually-implemented methods;
      // user provides the implementation
      return;
    }

    MethodAccess accessControl = cfg.accessControl(binding.getName());
    // We should not emit anything except public APIs into interfaces
    if (signatureOnly && (accessControl != PUBLIC)) {
      return;
    }

    // It's possible we may not need a body even if signatureOnly is
    // set to false; for example, if the routine doesn't take any
    // arrays or buffers as arguments
    boolean isUnimplemented = cfg.isUnimplemented(binding.getName());
    List<String> prologue = cfg.javaPrologueForMethod(binding, false, false);
    List<String> epilogue = cfg.javaEpilogueForMethod(binding, false, false);
    boolean needsBody = (isUnimplemented ||
                         (binding.needsNIOWrappingOrUnwrapping() ||
                          binding.signatureUsesJavaPrimitiveArrays()) ||
                         (prologue != null) ||
                         (epilogue != null));

    JavaMethodBindingEmitter emitter =
      new JavaMethodBindingEmitter(binding,
                                   writer,
                                   cfg.runtimeExceptionType(),
                                   cfg.unsupportedExceptionType(),
                                   !signatureOnly && needsBody,
                                   cfg.tagNativeBinding(),
                                   false,
                                   cfg.nioDirectOnly(binding.getName()),
                                   false,
                                   false,
                                   false,
                                   isUnimplemented,
                                   signatureOnly,
                                   cfg);
    switch (accessControl) {
      case PUBLIC:     emitter.addModifier(JavaMethodBindingEmitter.PUBLIC); break;
      case PROTECTED:  emitter.addModifier(JavaMethodBindingEmitter.PROTECTED); break;
      case PRIVATE:    emitter.addModifier(JavaMethodBindingEmitter.PRIVATE); break;
      default: break; // package-private adds no modifiers
    }
    if (cfg.allStatic()) {
      emitter.addModifier(JavaMethodBindingEmitter.STATIC);
    }
    if (!isUnimplemented && !needsBody && !signatureOnly) {
      emitter.addModifier(JavaMethodBindingEmitter.NATIVE);
    }
    emitter.setReturnedArrayLengthExpression(cfg.returnedArrayLength(binding.getName()));
    emitter.setPrologue(prologue);
    emitter.setEpilogue(epilogue);
    allEmitters.add(emitter);
  }

  /**
   * Generates the private emitters for this MethodBinding. On the
   * Java side these will simply produce signatures for native
   * methods. On the C side these will create the emitters which will
   * write the JNI code to interface to the functions. We need to be
   * careful to make the signatures all match up and not produce too
   * many emitters which would lead to compilation errors from
   * creating duplicated methods / functions.
   */
  protected void generatePrivateEmitters(MethodBinding binding,
                                         List<FunctionEmitter> allEmitters) {
    if (cfg.manuallyImplement(binding.getName())) {
      // Don't produce emitters for the implementation class
      return;
    }

    boolean hasPrologueOrEpilogue =
        ((cfg.javaPrologueForMethod(binding, false, false) != null) ||
         (cfg.javaEpilogueForMethod(binding, false, false) != null));

    // If we already generated a public native entry point for this
    // method, don't emit another one
    if (!cfg.isUnimplemented(binding.getName()) &&
        (binding.needsNIOWrappingOrUnwrapping() ||
         binding.signatureUsesJavaPrimitiveArrays() ||
         hasPrologueOrEpilogue)) {
      PrintWriter writer = (cfg.allStatic() ? javaWriter() : javaImplWriter());

      // If the binding uses primitive arrays, we are going to emit
      // the private native entry point for it along with the version
      // taking only NIO buffers
      if (!binding.signatureUsesJavaPrimitiveArrays()) {
        // (Always) emit the entry point taking only direct buffers
        JavaMethodBindingEmitter emitter =
          new JavaMethodBindingEmitter(binding,
                                       writer,
                                       cfg.runtimeExceptionType(),
                                       cfg.unsupportedExceptionType(),
                                       false,
                                       cfg.tagNativeBinding(),
                                       true,
                                       cfg.nioDirectOnly(binding.getName()),
                                       true,
                                       true,
                                       false,
                                       false,
                                       false,
                                       cfg);
        emitter.addModifier(JavaMethodBindingEmitter.PRIVATE);
        if (cfg.allStatic()) {
          emitter.addModifier(JavaMethodBindingEmitter.STATIC);
        }
        emitter.addModifier(JavaMethodBindingEmitter.NATIVE);
        emitter.setReturnedArrayLengthExpression(cfg.returnedArrayLength(binding.getName()));
        allEmitters.add(emitter);
      }
    }

    // Now generate the C emitter(s). We need to produce one for every
    // Java native entry point (public or private). The only
    // situations where we don't produce one are (a) when the method
    // is unimplemented, and (b) when the signature contains primitive
    // arrays, since the latter is handled by the method binding
    // variant taking only NIO Buffers.
    if (!cfg.isUnimplemented(binding.getName()) &&
        !binding.signatureUsesJavaPrimitiveArrays()) {
      // See whether we need an expression to help calculate the
      // length of any return type
      MessageFormat returnValueCapacityFormat = null;
      MessageFormat returnValueLengthFormat = null;
      JavaType javaReturnType = binding.getJavaReturnType();
      if (javaReturnType.isNIOBuffer() ||
          javaReturnType.isCompoundTypeWrapper()) {
        // See whether capacity has been specified
        String capacity = cfg.returnValueCapacity(binding.getName());
        if (capacity != null) {
          returnValueCapacityFormat = new MessageFormat(capacity);
        }
      } else if (javaReturnType.isArray() ||
                 javaReturnType.isArrayOfCompoundTypeWrappers()) {
        // NOTE: adding a check here because the CMethodBindingEmitter
        // also doesn't yet handle returning scalar arrays. In order
        // to implement this, return the type as a Buffer instead
        // (i.e., IntBuffer, FloatBuffer) and add code as necessary.
        if (javaReturnType.isPrimitiveArray()) {
          throw new RuntimeException("Primitive array return types not yet supported");
        }

        // See whether length has been specified
        String len = cfg.returnValueLength(binding.getName());
        if (len != null) {
          returnValueLengthFormat = new MessageFormat(len);
        }
      }

      CMethodBindingEmitter cEmitter;
      // Generate a binding without mixed access (NIO-direct, -indirect, array)
      cEmitter =
          new CMethodBindingEmitter(binding,
                                    cWriter(),
                                    cfg.implPackageName(),
                                    cfg.implClassName(),
                                    true, // NOTE: we always disambiguate with a suffix now, so this is optional
                                    cfg.allStatic(),
                                    (binding.needsNIOWrappingOrUnwrapping() || hasPrologueOrEpilogue),
                                    !cfg.nioDirectOnly(binding.getName()),
                                    machDesc64);
      if (returnValueCapacityFormat != null) {
          cEmitter.setReturnValueCapacityExpression(returnValueCapacityFormat);
      }
      if (returnValueLengthFormat != null) {
          cEmitter.setReturnValueLengthExpression(returnValueLengthFormat);
      }
      cEmitter.setTemporaryCVariableDeclarations(cfg.temporaryCVariableDeclarations(binding.getName()));
      cEmitter.setTemporaryCVariableAssignments(cfg.temporaryCVariableAssignments(binding.getName()));
      allEmitters.add(cEmitter);
    }
  }

  /**
   * Generate all appropriate Java bindings for the specified C function
   * symbols.
   */
  protected List<? extends FunctionEmitter> generateMethodBindingEmitters(Set<MethodBinding> methodBindingSet, FunctionSymbol sym) throws Exception {

    ArrayList<FunctionEmitter> allEmitters = new ArrayList<FunctionEmitter>();

    try {
      // Get Java binding for the function
      MethodBinding mb = bindFunction(sym, null, null, machDesc64);

      // JavaTypes representing C pointers in the initial
      // MethodBinding have not been lowered yet to concrete types
      List<MethodBinding> bindings = expandMethodBinding(mb);

      for (MethodBinding binding : bindings) {

        if(!methodBindingSet.add(binding)) {
            // skip .. already exisiting binding ..
            continue;
        }

        if (cfg.allStatic() && binding.hasContainingType()) {
          // This should not currently happen since structs are emitted using a different mechanism
          throw new IllegalArgumentException("Cannot create binding in AllStatic mode because method has containing type: \"" +
                                             binding + "\"");
        }

        // The structure of the generated glue code looks something like this:
        // Simple method (no arrays, void pointers, etc.):
        //   Interface class:
        //     public void fooMethod();
        //   Implementation class:
        //     public native void fooMethod();
        //
        // Method taking void* argument:
        //   Interface class:
        //     public void fooMethod(Buffer arg);
        //   Implementation class:
        //     public void fooMethod(Buffer arg) {
        //       ... bounds checks, etc. ...
        //
        //       boolean arg_direct = arg != null && Buffers.isDirect(arg);
        //
        //       fooMethod0(arg_direct?arg:Buffers.getArray(arg),
        //                  arg_direct?Buffers.getDirectBufferByteOffset(arg):Buffers.getIndirectBufferByteOffset(arg),
        //                  arg_direct,
        //                  ... );
        //     }
        //     private native void fooMethod1(Object arg, int arg_byte_offset, boolean arg_is_direct, ...);
        //
        // Method taking primitive array argument:
        //   Interface class:
        //     public void fooMethod(int[] arg, int arg_offset);
        //     public void fooMethod(IntBuffer arg);
        //   Implementing class:
        //     public void fooMethod(int[] arg, int arg_offset) {
        //       ... range checks, etc. ...
        //       fooMethod1(arg, SIZEOF_INT * arg_offset);
        //     }
        //     public void fooMethod(IntBuffer arg) {
        //       ... bounds checks, etc. ...
        //
        //       boolean arg_direct = BufferFactory.isDirect(arg);
        //
        //       fooMethod1(arg_direct?arg:BufferFactory.getArray(arg),
        //                  arg_direct?BufferFactory.getDirectBufferByteOffset(arg):BufferFactory.getIndirectBufferByteOffset(arg),
        //                  arg_direct,
        //                  ... );
        //     }
        //     private native void fooMethod1(Object arg, int arg_byte_offset, boolean arg_is_direct, ...);
        //
        // Note in particular that the public entry point taking an
        // array is merely a special case of the indirect buffer case.

        if (cfg.emitInterface()) {
          generatePublicEmitters(binding, allEmitters, true);
        }
        if (cfg.emitImpl()) {
          generatePublicEmitters(binding, allEmitters, false);
          generatePrivateEmitters(binding, allEmitters);
        }
      } // end iteration over expanded bindings
    } catch (Exception e) {
      throw new RuntimeException("Error while generating bindings for \"" + sym + "\"", e);
    }

    return allEmitters;
  }


  public void endFunctions() throws Exception {
    if (!cfg.structsOnly()) {
        if (cfg.allStatic() || cfg.emitInterface()) {
            emitCustomJavaCode(javaWriter(), cfg.className());
        }
        if (!cfg.allStatic() && cfg.emitImpl()) {
            emitCustomJavaCode(javaImplWriter(), cfg.implClassName());
        }
    }
  }

  public void beginStructLayout() throws Exception {}
  public void layoutStruct(CompoundType t) throws Exception {
    getLayout().layout(t);
  }
  public void endStructLayout() throws Exception {}

  public void beginStructs(TypeDictionary typedefDictionary,
                           TypeDictionary structDictionary,
                           Map<Type, Type> canonMap) throws Exception {
    this.typedefDictionary = typedefDictionary;
    this.structDictionary  = structDictionary;
    this.canonMap          = canonMap;
  }

  public void emitStruct(CompoundType structType, String alternateName) throws Exception {
    // Emit abstract base class delegating to 32-bit or 64-bit implementations
    emitStructImpl(structType, alternateName, machDesc32, machDesc64, true, false);
    // Emit concrete implementing class for each variant
    emitStructImpl(structType, alternateName, machDesc32, machDesc64, false, true);
    emitStructImpl(structType, alternateName, machDesc32, machDesc64, false, false);
  }

  public void emitStructImpl(CompoundType structType,
                             String alternateName,
                             MachineDescription md32,
                             MachineDescription md64,
                             boolean doBaseClass,
                             boolean do32Bit) throws Exception {
    String name = structType.getName();
    if (name == null && alternateName != null) {
      name = alternateName;
    }

    if (name == null) {
        LOG.log(WARNING, "skipping emission of unnamed struct \"{0}\"", structType);
        return;
    }

    if (cfg.shouldIgnoreInInterface(name)) {
      return;
    }

    Type containingCType = canonicalize(new PointerType(SizeThunk.POINTER, structType, 0));
    JavaType containingType = typeToJavaType(containingCType, false, null);
    if (!containingType.isCompoundTypeWrapper()) {
      return;
    }
    String containingTypeName = containingType.getName();

    if ((md32 == null) || (md64 == null)) {
      throw new RuntimeException("Must supply both 32- and 64-bit MachineDescriptions to emitStructImpl");
    }
    String suffix = "";

    // The "external" MachineDescription is the one used to determine
    // the sizes of the primitive types seen in the public API. For
    // example, if a C long is an element of a struct, it is the size
    // of a Java int on a 32-bit machine but the size of a Java long
    // on a 64-bit machine. To support both of these sizes with the
    // same API, the abstract base class must take and return a Java
    // long from the setter and getter for this field. However the
    // implementation on a 32-bit platform must downcast this to an
    // int and set only an int's worth of data in the struct. The
    // "internal" MachineDescription is the one used to determine how
    // much data to set in or get from the struct and exactly from
    // where it comes.
    //
    // Note that the 64-bit MachineDescription is always used as the
    // external MachineDescription.

    MachineDescription extMachDesc = md64;
    MachineDescription intMachDesc = null;

    if (!doBaseClass) {
      if (do32Bit) {
        intMachDesc = md32;
        suffix = "32";
      } else {
        intMachDesc = md64;
        suffix = "64";
      }
    }

    boolean needsNativeCode = false;
    // Native code for calls through function pointers gets emitted
    // into the abstract base class; Java code which accesses fields
    // gets emitted into the concrete classes
    if (doBaseClass) {
      for (int i = 0; i < structType.getNumFields(); i++) {
        if (structType.getField(i).getType().isFunctionPointer()) {
          needsNativeCode = true;
          break;
        }
      }
    }

    String structClassPkg = cfg.packageForStruct(name);
    PrintWriter writer = null;
    PrintWriter newWriter = null;
    try  {
      writer = openFile(
        cfg.javaOutputDir() + File.separator +
        CodeGenUtils.packageAsPath(structClassPkg) +
        File.separator + containingTypeName + suffix + ".java");
      CodeGenUtils.emitAutogeneratedWarning(writer, this);
      if (needsNativeCode) {
        String nRoot = cfg.nativeOutputDir();
        if (cfg.nativeOutputUsesJavaHierarchy()) {
          nRoot += File.separator + CodeGenUtils.packageAsPath(cfg.packageName());
        }
        newWriter = openFile(nRoot + File.separator + containingTypeName + "_JNI.c");
        CodeGenUtils.emitAutogeneratedWarning(newWriter, this);
        emitCHeader(newWriter, containingTypeName);
      }
    } catch(Exception e)   {
      throw new RuntimeException("Unable to open files for emission of struct class", e);
    }

    writer.println();
    writer.println("package " + structClassPkg + ";");
    writer.println();
    writer.println("import java.nio.*;");
    writer.println();

    writer.println("import " + cfg.gluegenRuntimePackage() + ".*;");
    writer.println("import " + DynamicLookupHelper.class.getPackage().getName() + ".*;");
    writer.println("import " + Buffers.class.getPackage().getName() + ".*;");
    writer.println();
    List<String> imports = cfg.imports();
    for (String str : imports) {
      writer.print("import ");
      writer.print(str);
      writer.println(";");
    }
    writer.println();
    List<String> javadoc = cfg.javadocForClass(containingTypeName);
    for (String doc : javadoc) {
      writer.println(doc);
    }
    writer.print((doBaseClass ? "public " : "") + (doBaseClass ? "abstract " : "") + "class " + containingTypeName + suffix + " ");
    if (!doBaseClass) {
      writer.print("extends " + containingTypeName + " ");
    }
    boolean firstIteration = true;
    List<String> userSpecifiedInterfaces = cfg.implementedInterfaces(containingTypeName);
    for (String userInterface : userSpecifiedInterfaces) {
      if (firstIteration) {
        writer.print("implements ");
      }
      firstIteration = false;
      writer.print(userInterface);
      writer.print(" ");
    }
    writer.println("{");
    writer.println();
    if (doBaseClass) {
      writer.println("  StructAccessor accessor;");
      writer.println();
    }

    writer.println("  public static int size() {");
    if (doBaseClass) {
      writer.println("    if (Platform.is32Bit()) {");
      writer.println("      return " + containingTypeName + "32" + ".size();");
      writer.println("    } else {");
      writer.println("      return " + containingTypeName + "64" + ".size();");
      writer.println("    }");
    } else {
      writer.println("    return " + structType.getSize(intMachDesc) + ";");
    }
    writer.println("  }");
    writer.println();
    if (doBaseClass) {
      writer.println("  public static " + containingTypeName + " create() {");
      writer.println("    return create(Buffers.newDirectByteBuffer(size()));");
      writer.println("  }");
      writer.println();
      writer.println("  public static " + containingTypeName + " create(java.nio.ByteBuffer buf) {");
      writer.println("    if (Platform.is32Bit()) {");
      writer.println("      return new " + containingTypeName + "32(buf);");
      writer.println("    } else {");
      writer.println("      return new " + containingTypeName + "64(buf);");
      writer.println("    }");
      writer.println("  }");
      writer.println();
      writer.println("  " + containingTypeName + "(java.nio.ByteBuffer buf) {");
      writer.println("    accessor = new StructAccessor(buf);");
      writer.println("  }");
      writer.println();
      writer.println("  public java.nio.ByteBuffer getBuffer() {");
      writer.println("    return accessor.getBuffer();");
      writer.println("  }");
    } else {
      writer.println("  " + containingTypeName + suffix + "(java.nio.ByteBuffer buf) {");
      writer.println("    super(buf);");
      writer.println("  }");
      writer.println();
    }
    for (int i = 0; i < structType.getNumFields(); i++) {

      Field field = structType.getField(i);
      Type fieldType = field.getType();

      if (!cfg.shouldIgnoreInInterface(name + " " + field.getName())) {

        String renamed = cfg.getJavaSymbolRename(field.getName());
        String fieldName = renamed==null ? field.getName() : renamed;

        if (fieldType.isFunctionPointer()) {

          if (doBaseClass) {
            try {
              // Emit method call and associated native code
              FunctionType   funcType     = fieldType.asPointer().getTargetType().asFunction();
              FunctionSymbol funcSym      = new FunctionSymbol(fieldName, funcType);
              MethodBinding  binding      = bindFunction(funcSym, containingType, containingCType, machDesc64);
              binding.findThisPointer(); // FIXME: need to provide option to disable this on per-function basis
              writer.println();

              // Emit public Java entry point for calling this function pointer
              JavaMethodBindingEmitter emitter =
                new JavaMethodBindingEmitter(binding,
                                             writer,
                                             cfg.runtimeExceptionType(),
                                             cfg.unsupportedExceptionType(),
                                             true,
                                             cfg.tagNativeBinding(),
                                             false,
                                             true, // FIXME: should unify this with the general emission code
                                             false,
                                             false, // FIXME: should unify this with the general emission code
                                             false, // FIXME: should unify this with the general emission code
                                             false, // FIXME: should unify this with the general emission code
                                             false,
                                             cfg);
              emitter.addModifier(JavaMethodBindingEmitter.PUBLIC);
              emitter.emit();

              // Emit private native Java entry point for calling this function pointer
              emitter =
                new JavaMethodBindingEmitter(binding,
                                             writer,
                                             cfg.runtimeExceptionType(),
                                             cfg.unsupportedExceptionType(),
                                             false,
                                             cfg.tagNativeBinding(),
                                             true,
                                             true, // FIXME: should unify this with the general emission code
                                             true,
                                             true, // FIXME: should unify this with the general emission code
                                             false, // FIXME: should unify this with the general emission code
                                             false, // FIXME: should unify this with the general emission code
                                             false,
                                             cfg);
              emitter.addModifier(JavaMethodBindingEmitter.PRIVATE);
              emitter.addModifier(JavaMethodBindingEmitter.NATIVE);
              emitter.emit();

              // Emit (private) C entry point for calling this function pointer
              CMethodBindingEmitter cEmitter =
                new CMethodBindingEmitter(binding,
                                          newWriter,
                                          structClassPkg,
                                          containingTypeName,
                                          true, // FIXME: this is optional at this point
                                          false,
                                          true,
                                          false, // FIXME: should unify this with the general emission code
                                          machDesc64);
              cEmitter.emit();
            } catch (Exception e) {
              System.err.println("While processing field " + field + " of type " + name + ":");
              throw(e);
            }
          }
        } else if (fieldType.isCompound()) {
          // FIXME: will need to support this at least in order to
          // handle the union in jawt_Win32DrawingSurfaceInfo (fabricate
          // a name?)
          if (fieldType.getName() == null) {
            throw new RuntimeException("Anonymous structs as fields not supported yet (field \"" +
                                       field + "\" in type \"" + name + "\")");
          }

          writer.println();
          generateGetterSignature(writer, doBaseClass, fieldType.getName(), capitalizeString(fieldName));
          if (doBaseClass) {
            writer.println(";");
          } else {
            writer.println(" {");
            writer.println("    return " + fieldType.getName() + ".create(accessor.slice(" +
                           field.getOffset(intMachDesc) + ", " + fieldType.getSize(intMachDesc) + "));");
            writer.println("  }");
          }

        } else if (fieldType.isArray()) {

            Type baseElementType = field.getType().asArray().getBaseElementType();

            if(!baseElementType.isPrimitive())
                break;

            String paramType = typeToJavaType(baseElementType, false, extMachDesc).getName();
            String capitalized = capitalizeString(fieldName);

            int slot = -1;
            if(!doBaseClass) {
              slot = slot(fieldType, (int) field.getOffset(intMachDesc), intMachDesc);
            }

            // Setter
            writer.println();
            generateSetterSignature(writer, doBaseClass, containingTypeName, capitalized, paramType+"[]");
            if (doBaseClass) {
              writer.println(";");
            } else {
              writer.println(" {");
              writer.print  ("    accessor.set" + capitalizeString(paramType) + "sAt(" + slot + ", ");
              writer.println("val);");
              writer.println("    return this;");
              writer.println("  }");
            }
            writer.println();
            // Getter
            generateGetterSignature(writer, doBaseClass, paramType+"[]", capitalized);
            if (doBaseClass) {
              writer.println(";");
            } else {
              writer.println(" {");
              writer.print  ("    return ");
              writer.println("accessor.get" + capitalizeString(paramType) + "sAt(" + slot + ", new " +paramType+"["+fieldType.asArray().getLength()+"]);");
              writer.println("  }");
            }

        } else {
          JavaType internalJavaType = null;
          JavaType externalJavaType = null;

          try {
            externalJavaType = typeToJavaType(fieldType, false, extMachDesc);
            if (!doBaseClass) {
              internalJavaType = typeToJavaType(fieldType, false, intMachDesc);
            }
          } catch (Exception e) {
            System.err.println("Error occurred while creating accessor for field \"" +
                               field.getName() + "\" in type \"" + name + "\"");
            throw(e);
          }
          if (externalJavaType.isPrimitive()) {
            // Primitive type
            String externalJavaTypeName = null;
            String internalJavaTypeName = null;
            externalJavaTypeName = externalJavaType.getName();
            if (!doBaseClass) {
              internalJavaTypeName = internalJavaType.getName();
            }
            if (isOpaque(fieldType)) {
              externalJavaTypeName = compatiblePrimitiveJavaTypeName(fieldType, externalJavaType, extMachDesc);
              if (!doBaseClass) {
                internalJavaTypeName = compatiblePrimitiveJavaTypeName(fieldType, internalJavaType, intMachDesc);
              }
            }
            String capitalized = null;
            if (!doBaseClass) {
              capitalized = capitalizeString(internalJavaTypeName);
            }
            int slot = -1;
            if (!doBaseClass) {
              slot = slot(fieldType, (int) field.getOffset(intMachDesc), intMachDesc);
            }
            writer.println();
            String capitalizedFieldName = capitalizeString(fieldName);
            // Setter
            generateSetterSignature(writer, doBaseClass, containingTypeName, capitalizedFieldName, externalJavaTypeName);
            if (doBaseClass) {
              writer.println(";");
            } else {
              writer.println(" {");
              writer.print  ("    accessor.set" + capitalized + "At(" + slot + ", ");
              if (!externalJavaTypeName.equals(internalJavaTypeName)) {
                writer.print("(" + internalJavaTypeName + ") ");
              }
              writer.println("val);");
              writer.println("    return this;");
              writer.println("  }");
            }
            writer.println();
            // Getter
            generateGetterSignature(writer, doBaseClass, externalJavaTypeName, capitalizedFieldName);
            if (doBaseClass) {
              writer.println(";");
            } else {
              writer.println(" {");
              writer.print  ("    return ");
              if (!externalJavaTypeName.equals(internalJavaTypeName)) {
                writer.print("(" + externalJavaTypeName + ") ");
              }
              writer.println("accessor.get" + capitalized + "At(" + slot + ");");
              writer.println("  }");
            }
          } else {
            // FIXME
            LOG.log(WARNING, "Complicated fields (field \"{0}\" of type \"{1}\") not implemented yet", new Object[]{field, name});
            //          throw new RuntimeException("Complicated fields (field \"" + field + "\" of type \"" + t +
            //                                     "\") not implemented yet");
          }
        }
      }
    }
    if (doBaseClass) {
      emitCustomJavaCode(writer, containingTypeName);
    }
    writer.println("}");
    writer.flush();
    writer.close();
    if (needsNativeCode) {
      newWriter.flush();
      newWriter.close();
    }
  }
  public void endStructs() throws Exception {}

  public static int addStrings2Buffer(StringBuilder buf, String sep, String first, Collection<String> col) {
    int num = 0;
    if(null==buf) {
        buf = new StringBuilder();
    }

    Iterator<String> iter = col.iterator();
    if(null!=first) {
        buf.append(first);
        if( iter.hasNext() ) {
            buf.append(sep);
        }
        num++;
    }
    while( iter.hasNext() ) {
        buf.append(iter.next());
        if( iter.hasNext() ) {
            buf.append(sep);
        }
        num++;
    }
    return num;
  }

  //----------------------------------------------------------------------
  // Internals only below this point
  //

    private void generateGetterSignature(PrintWriter writer, boolean baseClass, String returnTypeName, String capitalizedFieldName) {
        writer.print("  public " + (baseClass ? "abstract " : "") + returnTypeName + " get" + capitalizedFieldName + "()");
    }

    private void generateSetterSignature(PrintWriter writer, boolean baseClass, String returnTypeName, String capitalizedFieldName, String paramTypeName) {
        writer.print("  public " + (baseClass ? "abstract " : "") + returnTypeName + " set" + capitalizedFieldName + "(" + paramTypeName + " val)");
    }

  private JavaType typeToJavaType(Type cType, boolean outgoingArgument, MachineDescription curMachDesc) {
    // Recognize JNIEnv* case up front
    PointerType opt = cType.asPointer();
    if ((opt != null) &&
        (opt.getTargetType().getName() != null) &&
        (opt.getTargetType().getName().equals("JNIEnv"))) {
      return JavaType.createForJNIEnv();
    }
    Type t = cType;

    // Opaque specifications override automatic conversions
    // in case the identity is being used .. not if ptr-ptr
    TypeInfo info = cfg.typeInfo(t, typedefDictionary);
    if (info != null) {
      boolean isPointerPointer = false;
      if (t.pointerDepth() > 0 || t.arrayDimension() > 0) {
        Type targetType; // target type
        if (t.isPointer()) {
          // t is <type>*, we need to get <type>
          targetType = t.asPointer().getTargetType();
        } else {
          // t is <type>[], we need to get <type>
          targetType = t.asArray().getElementType();
        }
        if (t.pointerDepth() == 2 || t.arrayDimension() == 2) {
          // Get the target type of the target type (targetType was computer earlier
          // as to be a pointer to the target type, so now we need to get its
          // target type)
          if (targetType.isPointer()) {
            isPointerPointer = true;

            // t is<type>**, targetType is <type>*, we need to get <type>
            Type bottomType = targetType.asPointer().getTargetType();
            LOG.log(INFO, "Opaque Type: {0}, targetType: {1}, bottomType: {2} is ptr-ptr", new Object[]{t, targetType, bottomType});
          }
        }
      }
      if(!isPointerPointer) {
          return info.javaType();
      }
    }

    if (t.isInt() || t.isEnum()) {
      switch ((int) t.getSize(curMachDesc)) {
       case 1:  return javaType(Byte.TYPE);
       case 2:  return javaType(Short.TYPE);
       case 4:  return javaType(Integer.TYPE);
       case 8:  return javaType(Long.TYPE);
       default: throw new RuntimeException("Unknown integer type of size " +
                                           t.getSize(curMachDesc) + " and name " + t.getName());
      }
    } else if (t.isFloat()) {
      return javaType(Float.TYPE);
    } else if (t.isDouble()) {
      return javaType(Double.TYPE);
    } else if (t.isVoid()) {
      return javaType(Void.TYPE);
    } else {
      if (t.pointerDepth() > 0 || t.arrayDimension() > 0) {
        Type targetType; // target type
        if (t.isPointer()) {
          // t is <type>*, we need to get <type>
          targetType = t.asPointer().getTargetType();
        } else {
          // t is <type>[], we need to get <type>
          targetType = t.asArray().getElementType();
        }

        // Handle Types of form pointer-to-type or array-of-type, like
        // char* or int[]; these are expanded out into Java primitive
        // arrays, NIO buffers, or both in expandMethodBinding
        if (t.pointerDepth() == 1 || t.arrayDimension() == 1) {
          if (targetType.isVoid()) {
            return JavaType.createForVoidPointer();
          } else if (targetType.isInt()) {
            switch ((int) targetType.getSize(curMachDesc)) {
              case 1:  return JavaType.createForCCharPointer();
              case 2:  return JavaType.createForCShortPointer();
              case 4:  return JavaType.createForCInt32Pointer();
              case 8:  return JavaType.createForCInt64Pointer();
              default: throw new RuntimeException("Unknown integer array type of size " +
                                                  t.getSize(curMachDesc) + " and name " + t.getName());
            }
          } else if (targetType.isFloat()) {
            return JavaType.createForCFloatPointer();
          } else if (targetType.isDouble()) {
            return JavaType.createForCDoublePointer();
          } else if (targetType.isCompound()) {
            if (t.isArray()) {
              throw new RuntimeException("Arrays of compound types not handled yet");
            }
            // Special cases for known JNI types (in particular for converting jawt.h)
            if (t.getName() != null &&
                t.getName().equals("jobject")) {
              return javaType(java.lang.Object.class);
            }

            String name = targetType.getName();
            if (name == null) {
              // Try containing pointer type for any typedefs
              name = t.getName();
              if (name == null) {
                throw new RuntimeException("Couldn't find a proper type name for pointer type " + t);
              }
            }

            return JavaType.createForCStruct(cfg.renameJavaType(name));
          } else {
            throw new RuntimeException("Don't know how to convert pointer/array type \"" +
                                       t + "\"");
          }
        }
        // Handle Types of form pointer-to-pointer-to-type or
        // array-of-arrays-of-type, like char** or int[][]
        else if (t.pointerDepth() == 2 || t.arrayDimension() == 2) {
          // Get the target type of the target type (targetType was computer earlier
          // as to be a pointer to the target type, so now we need to get its
          // target type)
          Type bottomType;
          if (targetType.isPointer()) {
            // t is<type>**, targetType is <type>*, we need to get <type>
            bottomType = targetType.asPointer().getTargetType();
            return JavaType.forNIOPointerBufferClass();
          } else {
            // t is<type>[][], targetType is <type>[], we need to get <type>
            bottomType = targetType.asArray().getElementType();
            LOG.log(WARNING, "typeToJavaType(ptr-ptr): {0}, targetType: {1}, bottomType: {2} -> Unhandled!", new Object[]{t, targetType, bottomType});
          }

          // Warning: The below code is not backed up by an implementation,
          //          the only working variant is a ptr-ptr type which results in a PointerBuffer.
          //
          if (bottomType.isPrimitive()) {
            if (bottomType.isInt()) {
              switch ((int) bottomType.getSize(curMachDesc)) {
                case 1: return javaType(ArrayTypes.byteBufferArrayClass);
                case 2: return javaType(ArrayTypes.shortBufferArrayClass);
                case 4: return javaType(ArrayTypes.intBufferArrayClass);
                case 8: return javaType(ArrayTypes.longBufferArrayClass);
                default: throw new RuntimeException("Unknown two-dimensional integer array type of element size " +
                                                    bottomType.getSize(curMachDesc) + " and name " + bottomType.getName());
              }
            } else if (bottomType.isFloat()) {
              return javaType(ArrayTypes.floatBufferArrayClass);
            } else if (bottomType.isDouble()) {
              return javaType(ArrayTypes.doubleBufferArrayClass);
            } else {
              throw new RuntimeException("Unexpected primitive type " + bottomType.getName() +
                                         " in two-dimensional array");
            }
          } else if (bottomType.isVoid()) {
            return javaType(ArrayTypes.bufferArrayClass);
          } else if (targetType.isPointer() && (targetType.pointerDepth() == 1) &&
                     targetType.asPointer().getTargetType().isCompound()) {
            // Array of pointers; convert as array of StructAccessors
            return JavaType.createForCArray(bottomType);
          } else {
            throw new RuntimeException(
              "Could not convert C type \"" + t + "\" " +
              "to appropriate Java type; need to add more support for " +
              "depth=2 pointer/array types [debug info: targetType=\"" +
              targetType + "\"]");
          }
        } else {
          // can't handle this type of pointer/array argument
          throw new RuntimeException(
            "Could not convert C pointer/array \"" + t + "\" to " +
            "appropriate Java type; types with pointer/array depth " +
            "greater than 2 are not yet supported [debug info: " +
            "pointerDepth=" + t.pointerDepth() + " arrayDimension=" +
            t.arrayDimension() + " targetType=\"" + targetType + "\"]");
        }

      } else {
        throw new RuntimeException(
          "Could not convert C type \"" + t + "\" (class " +
          t.getClass().getName() + ") to appropriate Java type");
      }
    }
  }

  private static boolean isIntegerType(Class<?> c) {
    return ((c == Byte.TYPE) ||
            (c == Short.TYPE) ||
            (c == Character.TYPE) ||
            (c == Integer.TYPE) ||
            (c == Long.TYPE));
  }

  private int slot(Type t, int byteOffset, MachineDescription curMachDesc) {
    if (t.isInt()) {
      switch ((int) t.getSize(curMachDesc)) {
       case 1:
       case 2:
       case 4:
       case 8:  return byteOffset / (int) t.getSize(curMachDesc);
       default: throw new RuntimeException("Illegal type");
      }
    } else if (t.isFloat()) {
      return byteOffset / 4;
    } else if (t.isDouble()) {
      return byteOffset / 8;
    } else if (t.isPointer()) {
      return byteOffset / curMachDesc.pointerSizeInBytes();
    } else if (t.isArray()) {
      return slot(t.asArray().getBaseElementType(), byteOffset, curMachDesc);
    } else {
      throw new RuntimeException("Illegal type " + t);
    }
  }

  private StructLayout getLayout() {
    if (layout == null) {
      layout = StructLayout.createForCurrentPlatform();
    }
    return layout;
  }

  protected PrintWriter openFile(String filename) throws IOException {
    //System.out.println("Trying to open: " + filename);
    File file = new File(filename);
    String parentDir = file.getParent();
    if (parentDir != null)
    {
      File pDirFile = new File(parentDir);
      pDirFile.mkdirs();
    }
    return new PrintWriter(new BufferedWriter(new FileWriter(file)));
  }

  private boolean isOpaque(Type type) {
    return (cfg.typeInfo(type, typedefDictionary) != null);
  }

  private String compatiblePrimitiveJavaTypeName(Type fieldType,
                                                 JavaType javaType,
                                                 MachineDescription curMachDesc) {
    Class<?> c = javaType.getJavaClass();
    if (!isIntegerType(c)) {
      // FIXME
      throw new RuntimeException("Can't yet handle opaque definitions of structs' fields to non-integer types (byte, short, int, long, etc.)");
    }
    switch ((int) fieldType.getSize(curMachDesc)) {
      case 1:  return "byte";
      case 2:  return "short";
      case 4:  return "int";
      case 8:  return "long";
      default: throw new RuntimeException("Can't handle opaque definitions if the starting type isn't compatible with integral types");
    }
  }

  private void openWriters() throws IOException {
    String jRoot = null;
    if (cfg.allStatic() || cfg.emitInterface()) {
      jRoot = cfg.javaOutputDir() + File.separator +
        CodeGenUtils.packageAsPath(cfg.packageName());
    }
    String jImplRoot = null;
    if (!cfg.allStatic()) {
      jImplRoot =
        cfg.javaOutputDir() + File.separator +
        CodeGenUtils.packageAsPath(cfg.implPackageName());
    }
    String nRoot = cfg.nativeOutputDir();
    if (cfg.nativeOutputUsesJavaHierarchy())
    {
      nRoot +=
        File.separator + CodeGenUtils.packageAsPath(cfg.packageName());
    }

    if (cfg.allStatic() || cfg.emitInterface()) {
      javaWriter = openFile(jRoot + File.separator + cfg.className() + ".java");
    }
    if (!cfg.allStatic() && cfg.emitImpl()) {
      javaImplWriter = openFile(jImplRoot + File.separator + cfg.implClassName() + ".java");
    }
    if (cfg.emitImpl()) {
      cWriter = openFile(nRoot + File.separator + cfg.implClassName() + "_JNI.c");
    }

    if (javaWriter != null) {
      CodeGenUtils.emitAutogeneratedWarning(javaWriter, this);
    }
    if (javaImplWriter != null) {
      CodeGenUtils.emitAutogeneratedWarning(javaImplWriter, this);
    }
    if (cWriter != null) {
      CodeGenUtils.emitAutogeneratedWarning(cWriter, this);
    }
  }

  protected PrintWriter javaWriter() {
    if (!cfg.allStatic() && !cfg.emitInterface()) {
      throw new InternalError("Should not call this");
    }
    return javaWriter;
  }

  protected PrintWriter javaImplWriter() {
    if (cfg.allStatic() || !cfg.emitImpl()) {
      throw new InternalError("Should not call this");
    }
    return javaImplWriter;
  }

  protected PrintWriter cWriter() {
    if (!cfg.emitImpl()) {
      throw new InternalError("Should not call this");
    }
    return cWriter;
  }

  private void closeWriter(PrintWriter writer) throws IOException {
    writer.flush();
    writer.close();
  }

  private void closeWriters() throws IOException {
    if (javaWriter != null) {
      closeWriter(javaWriter);
    }
    if (javaImplWriter != null) {
      closeWriter(javaImplWriter);
    }
    if (cWriter != null) {
      closeWriter(cWriter);
    }
    javaWriter = null;
    javaImplWriter = null;
    cWriter = null;
  }

  /**
   * Returns the value that was specified by the configuration directive
   * "JavaOutputDir", or the default if none was specified.
   */
  protected String getJavaOutputDir() {
    return cfg.javaOutputDir();
  }

  /**
   * Returns the value that was specified by the configuration directive
   * "Package", or the default if none was specified.
   */
  protected String getJavaPackageName() {
    return cfg.packageName();
  }

  /**
   * Returns the value that was specified by the configuration directive
   * "ImplPackage", or the default if none was specified.
   */
  protected String getImplPackageName() {
    return cfg.implPackageName();
  }

  /**
   * Emit all the strings specified in the "CustomJavaCode" parameters of
   * the configuration file.
   */
  protected void emitCustomJavaCode(PrintWriter writer, String className) throws Exception  {
    List<String> code = cfg.customJavaCodeForClass(className);
    if (code.isEmpty())
      return;

    writer.println();
    writer.println("  // --- Begin CustomJavaCode .cfg declarations");
    for (String line : code) {
      writer.println(line);
    }
    writer.println("  // ---- End CustomJavaCode .cfg declarations");
  }

  /**
   * Write out any header information for the output files (class declaration
   * and opening brace, import statements, etc).
   */
  protected void emitAllFileHeaders() throws IOException {
    try {
        List<String> imports = new ArrayList<String>(cfg.imports());
        imports.add(cfg.gluegenRuntimePackage()+".*");
        imports.add(DynamicLookupHelper.class.getPackage().getName()+".*");
        imports.add(Buffers.class.getPackage().getName()+".*");
        imports.add(Buffer.class.getPackage().getName()+".*");

      if (cfg.allStatic() || cfg.emitInterface()) {

        String[] interfaces;
        List<String> userSpecifiedInterfaces = null;
        if (cfg.emitInterface()) {
          userSpecifiedInterfaces = cfg.extendedInterfaces(cfg.className());
        } else {
          userSpecifiedInterfaces = cfg.implementedInterfaces(cfg.className());
        }
        interfaces = new String[userSpecifiedInterfaces.size()];
        userSpecifiedInterfaces.toArray(interfaces);

        final List<String> intfDocs = cfg.javadocForClass(cfg.className());
        CodeGenUtils.EmissionCallback docEmitter =
          new CodeGenUtils.EmissionCallback() {
            public void emit(PrintWriter w) {
              for (Iterator iter = intfDocs.iterator(); iter.hasNext(); ) {
                w.println((String) iter.next());
              }
            }
          };

        String[] accessModifiers = null;
        if(cfg.accessControl(cfg.className()) == PUBLIC_ABSTRACT) {
            accessModifiers = new String[] { "public", "abstract" };
        } else {
            accessModifiers = new String[] { "public" };
        }

        CodeGenUtils.emitJavaHeaders(
          javaWriter,
          cfg.packageName(),
          cfg.className(),
          cfg.allStatic() ? true : false,
          imports,
          accessModifiers,
          interfaces,
          cfg.extendedParentClass(cfg.className()),
          docEmitter);
      }

      if (!cfg.allStatic() && cfg.emitImpl()) {
        final List<String> implDocs = cfg.javadocForClass(cfg.implClassName());
        CodeGenUtils.EmissionCallback docEmitter =
          new CodeGenUtils.EmissionCallback() {
            public void emit(PrintWriter w) {
              for (Iterator iter = implDocs.iterator(); iter.hasNext(); ) {
                w.println((String) iter.next());
              }
            }
          };

        String[] interfaces;
        List<String> userSpecifiedInterfaces = null;
        userSpecifiedInterfaces = cfg.implementedInterfaces(cfg.implClassName());
        int additionalNum = 0;
        if (cfg.className() != null) {
          additionalNum = 1;
        }
        interfaces = new String[additionalNum + userSpecifiedInterfaces.size()];
        userSpecifiedInterfaces.toArray(interfaces);
        if (additionalNum == 1) {
          interfaces[userSpecifiedInterfaces.size()] = cfg.className();
        }

        String[] accessModifiers = null;
        if(cfg.accessControl(cfg.implClassName()) == PUBLIC_ABSTRACT) {
            accessModifiers = new String[] { "public", "abstract" };
        } else {
            accessModifiers = new String[] { "public" };
        }

        CodeGenUtils.emitJavaHeaders(
          javaImplWriter,
          cfg.implPackageName(),
          cfg.implClassName(),
          true,
          imports,
          accessModifiers,
          interfaces,
          cfg.extendedParentClass(cfg.implClassName()),
          docEmitter);
      }

      if (cfg.emitImpl()) {
        emitCHeader(cWriter(), cfg.implClassName());
      }
    } catch (Exception e) {
      throw new RuntimeException(
        "Error emitting all file headers: cfg.allStatic()=" + cfg.allStatic() +
        " cfg.emitImpl()=" + cfg.emitImpl() + " cfg.emitInterface()=" + cfg.emitInterface(),
        e);
    }

  }

  protected void emitCHeader(PrintWriter cWriter, String className) {
    cWriter.println("#include <jni.h>");
    cWriter.println("#include <stdlib.h>");
    cWriter.println();

    if (getConfig().emitImpl()) {
      cWriter.println("#include <assert.h>");
      cWriter.println();
    }

    for (String code : cfg.customCCode()) {
      cWriter.println(code);
    }
    cWriter.println();
  }

  /**
   * Write out any footer information for the output files (closing brace of
   * class definition, etc).
   */
  protected void emitAllFileFooters(){
    if (cfg.allStatic() || cfg.emitInterface()) {
      javaWriter().println();
      javaWriter().println("} // end of class " + cfg.className());
    }
    if (!cfg.allStatic() && cfg.emitImpl())  {
      javaImplWriter().println();
      javaImplWriter().println("} // end of class " + cfg.implClassName());
    }
  }

  private JavaType javaType(Class<?> c) {
    return JavaType.createForClass(c);
  }

  /** Maps the C types in the specified function to Java types through
      the MethodBinding interface. Note that the JavaTypes in the
      returned MethodBinding are "intermediate" JavaTypes (some
      potentially representing C pointers rather than true Java types)
      and must be lowered to concrete Java types before creating
      emitters for them. */
  private MethodBinding bindFunction(FunctionSymbol sym,
                                     JavaType containingType,
                                     Type containingCType,
                                     MachineDescription curMachDesc) {

    MethodBinding binding = new MethodBinding(sym, containingType, containingCType);

    binding.renameMethodName(cfg.getJavaSymbolRename(sym.getName()));

    // System.out.println("bindFunction(0) "+sym.getReturnType());

    if (cfg.returnsString(binding.getName())) {
      PointerType prt = sym.getReturnType().asPointer();
      if (prt == null ||
          prt.getTargetType().asInt() == null ||
          prt.getTargetType().getSize(curMachDesc) != 1) {
        throw new RuntimeException(
          "Cannot apply ReturnsString configuration directive to \"" + sym +
          "\". ReturnsString requires native method to have return type \"char *\"");
      }
      binding.setJavaReturnType(javaType(java.lang.String.class));
    } else {
      binding.setJavaReturnType(typeToJavaType(sym.getReturnType(), false, curMachDesc));
    }

    // System.out.println("bindFunction(1) "+binding.getJavaReturnType());

    // List of the indices of the arguments in this function that should be
    // converted from byte[] or short[] to String
    List<Integer> stringArgIndices = cfg.stringArguments(binding.getName());

    for (int i = 0; i < sym.getNumArguments(); i++) {
      Type cArgType = sym.getArgumentType(i);
      JavaType mappedType = typeToJavaType(cArgType, true, curMachDesc);
      // System.out.println("C arg type -> \"" + cArgType + "\"" );
      // System.out.println("      Java -> \"" + mappedType + "\"" );

      // Take into account any ArgumentIsString configuration directives that apply
      if (stringArgIndices != null && stringArgIndices.contains(i)) {
        // System.out.println("Forcing conversion of " + binding.getName() + " arg #" + i + " from byte[] to String ");
        if (mappedType.isCVoidPointerType() ||
            mappedType.isCCharPointerType() ||
            mappedType.isCShortPointerType() ||
            mappedType.isNIOPointerBuffer() ||
            (mappedType.isArray() &&
             (mappedType.getJavaClass() == ArrayTypes.byteBufferArrayClass) ||
             (mappedType.getJavaClass() == ArrayTypes.shortBufferArrayClass))) {
          // convert mapped type from:
          //   void*, byte[], and short[] to String
          //   ByteBuffer[] and ShortBuffer[] to String[]
          if (mappedType.isArray() || mappedType.isNIOPointerBuffer()) {
            mappedType = javaType(ArrayTypes.stringArrayClass);
          } else {
            mappedType = javaType(String.class);
          }
        }
        else {
        throw new RuntimeException(
          "Cannot apply ArgumentIsString configuration directive to " +
          "argument " + i + " of \"" + sym + "\": argument type is not " +
          "a \"void*\", \"char *\", \"short *\", \"char**\", or \"short**\" equivalent");
        }
      }
      binding.addJavaArgumentType(mappedType);
      //System.out.println("During binding of [" + sym + "], added mapping from C type: " + cArgType + " to Java type: " + mappedType);
    }

    // System.out.println("---> " + binding);
    // System.out.println("    ---> " + binding.getCSymbol());
    // System.out.println("bindFunction(3) "+binding);
    return binding;
  }

  private MethodBinding lowerMethodBindingPointerTypes(MethodBinding inputBinding,
                                                       boolean convertToArrays,
                                                       boolean[] canProduceArrayVariant) {
    MethodBinding result = inputBinding;
    boolean arrayPossible = false;

    // System.out.println("lowerMethodBindingPointerTypes(0): "+result);

    for (int i = 0; i < inputBinding.getNumArguments(); i++) {
      JavaType t = inputBinding.getJavaArgumentType(i);
      if (t.isCPrimitivePointerType()) {
        if (t.isCVoidPointerType()) {
          // These are always bound to java.nio.Buffer
          result = result.replaceJavaArgumentType(i, JavaType.forNIOBufferClass());
        } else if (t.isCCharPointerType()) {
          arrayPossible = true;
          if (convertToArrays) {
            result = result.replaceJavaArgumentType(i, javaType(ArrayTypes.byteArrayClass));
          } else {
            result = result.replaceJavaArgumentType(i, JavaType.forNIOByteBufferClass());
          }
        } else if (t.isCShortPointerType()) {
          arrayPossible = true;
          if (convertToArrays) {
            result = result.replaceJavaArgumentType(i, javaType(ArrayTypes.shortArrayClass));
          } else {
            result = result.replaceJavaArgumentType(i, JavaType.forNIOShortBufferClass());
          }
        } else if (t.isCInt32PointerType()) {
          arrayPossible = true;
          if (convertToArrays) {
            result = result.replaceJavaArgumentType(i, javaType(ArrayTypes.intArrayClass));
          } else {
            result = result.replaceJavaArgumentType(i, JavaType.forNIOIntBufferClass());
          }
        } else if (t.isCInt64PointerType()) {
          arrayPossible = true;
          if (convertToArrays) {
            result = result.replaceJavaArgumentType(i, javaType(ArrayTypes.longArrayClass));
          } else {
            result = result.replaceJavaArgumentType(i, JavaType.forNIOInt64BufferClass());
          }
        } else if (t.isCFloatPointerType()) {
          arrayPossible = true;
          if (convertToArrays) {
            result = result.replaceJavaArgumentType(i, javaType(ArrayTypes.floatArrayClass));
          } else {
            result = result.replaceJavaArgumentType(i, JavaType.forNIOFloatBufferClass());
          }
        } else if (t.isCDoublePointerType()) {
          arrayPossible = true;
          if (convertToArrays) {
            result = result.replaceJavaArgumentType(i, javaType(ArrayTypes.doubleArrayClass));
          } else {
            result = result.replaceJavaArgumentType(i, JavaType.forNIODoubleBufferClass());
          }
        } else {
          throw new RuntimeException("Unknown C pointer type " + t);
        }
      }
    }

    // System.out.println("lowerMethodBindingPointerTypes(1): "+result);

    // Always return primitive pointer types as NIO buffers
    JavaType t = result.getJavaReturnType();
    if (t.isCPrimitivePointerType()) {
      if (t.isCVoidPointerType()) {
        result = result.replaceJavaArgumentType(-1, JavaType.forNIOByteBufferClass());
      } else if (t.isCCharPointerType()) {
        result = result.replaceJavaArgumentType(-1, JavaType.forNIOByteBufferClass());
      } else if (t.isCShortPointerType()) {
        result = result.replaceJavaArgumentType(-1, JavaType.forNIOShortBufferClass());
      } else if (t.isCInt32PointerType()) {
        result = result.replaceJavaArgumentType(-1, JavaType.forNIOIntBufferClass());
      } else if (t.isCInt64PointerType()) {
        result = result.replaceJavaArgumentType(-1, JavaType.forNIOInt64BufferClass());
      } else if (t.isCFloatPointerType()) {
        result = result.replaceJavaArgumentType(-1, JavaType.forNIOFloatBufferClass());
      } else if (t.isCDoublePointerType()) {
        result = result.replaceJavaArgumentType(-1, JavaType.forNIODoubleBufferClass());
      } else {
        throw new RuntimeException("Unknown C pointer type " + t);
      }
    }

    // System.out.println("lowerMethodBindingPointerTypes(2): "+result);

    if (canProduceArrayVariant != null) {
      canProduceArrayVariant[0] = arrayPossible;
    }

    return result;
  }

  // Expands a MethodBinding containing C primitive pointer types into
  // multiple variants taking Java primitive arrays and NIO buffers, subject
  // to the per-function "NIO only" rule in the configuration file
  protected List<MethodBinding> expandMethodBinding(MethodBinding binding) {

    List<MethodBinding> result = new ArrayList<MethodBinding>();
    // Indicates whether it is possible to produce an array variant
    // Prevents e.g. char* -> String conversions from emitting two entry points
    boolean[] canProduceArrayVariant = new boolean[1];

    if (binding.signatureUsesCPrimitivePointers() ||
        binding.signatureUsesCVoidPointers() ||
        binding.signatureUsesCArrays()) {

      result.add(lowerMethodBindingPointerTypes(binding, false, canProduceArrayVariant));

      // FIXME: should add new configuration flag for this
      if (canProduceArrayVariant[0] && (binding.signatureUsesCPrimitivePointers() || binding.signatureUsesCArrays()) &&
          !cfg.nioDirectOnly(binding.getName()) && !cfg.nioOnly(binding.getName())) {
        result.add(lowerMethodBindingPointerTypes(binding, true, null));
      }
    } else {
      result.add(binding);
    }

    return result;
  }

  private String resultName() {
    return "_res";
  }

  private Type canonicalize(Type t) {
    Type res = canonMap.get(t);
    if (res != null) {
      return res;
    }
    canonMap.put(t, t);
    return t;
  }

  /**
   * Converts first letter to upper case.
   */
  private final String capitalizeString(String string) {
      return Character.toUpperCase(string.charAt(0)) + string.substring(1);
  }

}