/*
 * 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
 * MIDROSYSTEMS, 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 net.java.games.gluegen;

import java.io.*;
import java.util.*;
import java.text.MessageFormat;

import net.java.games.gluegen.cgram.types.*;

// 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            canonMap;
  private 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).
   */
  static final int ALL_STATIC = 1;
  static final int INTERFACE_AND_IMPL = 2;
  static final int INTERFACE_ONLY = 3;
  static final int IMPL_ONLY = 4;

  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 machDesc;
  
  public void readConfigurationFile(String filename) throws Exception {
    cfg = createConfig();
    cfg.read(filename);
  }

  public void setMachineDescription(MachineDescription md) {
    machDesc = md;
  }

  public void beginEmission(GlueEmitterControls controls) throws IOException
  {
    try
    {
      openWriters();
    }
    catch (Exception e)
    {
      throw new RuntimeException(
        "Unable to open files for writing", e);
    }
    
    emitAllFileHeaders();

    // Request emission of any structs requested
    for (Iterator iter = cfg.forcedStructs().iterator(); iter.hasNext(); ) {
      controls.forceStructEmission((String) iter.next());
    }
  }

  public void endEmission()
  {
    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()) {
      javaWriter().println();
    }
  }

  public void emitDefine(String name, String value, 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.
 
      if (!cfg.shouldIgnore(name)) {
        String type = null;

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

        if (type == null) {
            throw new RuntimeException(
              "Cannot emit define (2) \""+name+"\": value \""+value+
              "\" cannot be assigned to a int, long, float, or double");
        }
        if (optionalComment != null && optionalComment.length() != 0) {
          javaWriter().println("  /** " + optionalComment + " */");
        }
        javaWriter().println("  public static final " + type + " " + name + " = " + value + ";");
      }
    }
  }

  public void endDefines() throws Exception
  {
  }

  public void beginFunctions(TypeDictionary typedefDictionary,
                             TypeDictionary structDictionary,
                             Map            canonMap) throws Exception {
    this.typedefDictionary = typedefDictionary;
    this.structDictionary  = structDictionary;
    this.canonMap          = canonMap;
    if (cfg.allStatic() || cfg.emitInterface()) {
      javaWriter().println();
    }
  }

  public Iterator 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 funcsToBindSet = new HashSet(100);
    for (Iterator cIter = originalCFunctions.iterator(); cIter.hasNext(); ) {
      FunctionSymbol cFunc = (FunctionSymbol) cIter.next();
      if (!funcsToBindSet.contains(cFunc)) {
        funcsToBindSet.add(cFunc);
      }
    }

    ArrayList funcsToBind = new ArrayList(funcsToBindSet.size());
    funcsToBind.addAll(funcsToBindSet);
    // sort functions to make them easier to find in native code
    Collections.sort(
      funcsToBind,
      new Comparator() {
          public int compare(Object o1, Object o2) {
            return ((FunctionSymbol)o1).getName().compareTo(
              ((FunctionSymbol)o2).getName());
          }
          public boolean equals(Object obj) {
            return obj.getClass() == this.getClass();
          }
        });

    // Bind all the C funcs to Java methods
    ArrayList/*<FunctionEmitter>*/ methodBindingEmitters = new ArrayList(2*funcsToBind.size());
    for (Iterator iter = funcsToBind.iterator(); iter.hasNext(); ) {
      FunctionSymbol cFunc = (FunctionSymbol) iter.next();
      // Check to see whether this function should be ignored
      if (cfg.shouldIgnore(cFunc.getName())) {
        continue; // don't generate bindings for this symbol
      }
      
      Iterator allBindings = generateMethodBindingEmitters(cFunc);
      while (allBindings.hasNext()) {
        methodBindingEmitters.add(allBindings.next());
      }
    }

    // Emit all the methods
    for (int i = 0; i < methodBindingEmitters.size(); ++i) {
      FunctionEmitter emitter = (FunctionEmitter)methodBindingEmitters.get(i);      
      try {
        emitter.emit();
      } catch (Exception e) {
        throw new RuntimeException(
            "Error while emitting binding for \"" + emitter.getName() + "\"", e);
      }
      emitter.getDefaultOutput().println(); // put newline after method body
    }

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

  /**
   * Generate all appropriate Java bindings for the specified C function
   * symbols.
   */
  protected Iterator generateMethodBindingEmitters(FunctionSymbol sym) throws Exception {

    ArrayList/*<FunctionEmitter>*/ allEmitters = new ArrayList(1);

    try {
      // Get Java binding for the function
      MethodBinding mb = bindFunction(sym, null, null);
      
      // Expand all void* arguments
      List bindings = expandMethodBinding(mb);
      boolean overloaded = (bindings.size() > 1);
      if (overloaded) {
        // resize ahead of time for speed
        allEmitters.ensureCapacity(bindings.size());
      }
      
      // List of the indices of the arguments in this function that should be
      // expanded to the same type when binding functions with multiple void*
      // arguments
      List mirrorIdxs = cfg.mirroredArgs(sym.getName());
      
      for (Iterator iter = bindings.iterator(); iter.hasNext(); ) {
        MethodBinding binding = (MethodBinding) iter.next();        

        // Honor the MirrorExpandedBindingArgs directive in .cfg files
        if (mirrorIdxs != null) {
          assert(mirrorIdxs.size() >= 2); // sanity check.
          boolean typesMatch = true;
          int argIndex = ((Integer)mirrorIdxs.get(0)).intValue();
          JavaType leftArgType = binding.getJavaArgumentType(argIndex);
          for (int i = 1; i < mirrorIdxs.size(); ++i) {
            argIndex = ((Integer)mirrorIdxs.get(i)).intValue();
            JavaType rightArgType = binding.getJavaArgumentType(argIndex);
            if (!(leftArgType.equals(rightArgType))) {
              typesMatch = false;
              break;
            }
            leftArgType = rightArgType;
          }
          // Don't emit the binding if the specified args aren't the same type
          if (!typesMatch) { continue; } // skip this binding
        }

        // Try to create an NIOBuffer variant for this expanded binding. If
        // it's the same as the original binding, then we'll be able to emit
        // the binding like any normal binding because no special binding
        // generation (wrapper methods, etc) will be necessary.
        MethodBinding specialBinding = binding.createNIOBufferVariant();     
        
        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 + "\"");
        }

        boolean isUnimplemented = cfg.isUnimplemented(binding.getName());
        
        if (cfg.emitImpl()) {
          // Generate the emitter for the method which may do conversion
          // from type wrappers to NIO Buffers or which may call the
          // underlying function directly
          JavaMethodBindingImplEmitter entryPoint =
            new JavaMethodBindingImplEmitter(binding,
                                             (cfg.allStatic() ? javaWriter() : javaImplWriter()),
                                             cfg.runtimeExceptionType(),
                                             isUnimplemented);
          entryPoint.addModifier(JavaMethodBindingEmitter.PUBLIC);
          if (cfg.allStatic()) {
            entryPoint.addModifier(JavaMethodBindingEmitter.STATIC);
          }
          if (!isUnimplemented && !binding.needsBody()) {
            entryPoint.addModifier(JavaMethodBindingEmitter.NATIVE);
          }
          entryPoint.setReturnedArrayLengthExpression(cfg.returnedArrayLength(binding.getName()));
          allEmitters.add(entryPoint);
        }

        if (cfg.emitInterface()) {
          // Generate an emitter that will emit just the interface to the function
          JavaMethodBindingEmitter entryPointInterface =
            new JavaMethodBindingEmitter(binding, javaWriter(), cfg.runtimeExceptionType());
          entryPointInterface.addModifier(JavaMethodBindingEmitter.PUBLIC);
          entryPointInterface.setReturnedArrayLengthExpression(cfg.returnedArrayLength(binding.getName()));
          allEmitters.add(entryPointInterface);           
        }

        if (cfg.emitImpl() && binding.needsBody() && !isUnimplemented) {
          // Generate the method which calls the underlying function
          // after unboxing has occurred
          PrintWriter output = cfg.allStatic() ? javaWriter() : javaImplWriter();
          JavaMethodBindingEmitter wrappedEntryPoint =
            new JavaMethodBindingEmitter(specialBinding, output, cfg.runtimeExceptionType(), true);
          wrappedEntryPoint.addModifier(JavaMethodBindingEmitter.PRIVATE);
          wrappedEntryPoint.addModifier(JavaMethodBindingEmitter.STATIC); // Doesn't really matter
          wrappedEntryPoint.addModifier(JavaMethodBindingEmitter.NATIVE);
          allEmitters.add(wrappedEntryPoint); 
        }

        // If the user has stated that the function will be
        // manually implemented, then don't auto-generate a function body.
        if (cfg.emitImpl()) {
          if (!cfg.manuallyImplement(sym.getName()) && !isUnimplemented)
            {
              CMethodBindingEmitter cEmitter =
                makeCEmitter(specialBinding, 
                             overloaded,
                             (binding != specialBinding),
                             cfg.implPackageName(), cfg.implClassName(),
                             cWriter());
              allEmitters.add(cEmitter);
            }
        }
      } // end iteration over expanded bindings
    } catch (Exception e) {
      throw new RuntimeException(
        "Error while generating bindings for \"" + sym + "\"", e);
    }

    return allEmitters.iterator();
  }

    
  public void endFunctions() throws Exception
  {
    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            canonMap) throws Exception {
    this.typedefDictionary = typedefDictionary;
    this.structDictionary  = structDictionary;
    this.canonMap          = canonMap;
  }

  public void emitStruct(CompoundType structType) throws Exception {
    if (structType.getName() == null) {
      System.err.println("WARNING: skipping emission of unnamed struct \"" + structType + "\"");
      return;
    }

    if (cfg.shouldIgnore(structType.getName())) {
      return;
    }

    Type containingCType = canonicalize(new PointerType(machDesc.pointerSizeInBytes(), structType, 0));
    JavaType containingType = typeToJavaType(containingCType, false);
    String containingTypeName = containingType.getName();

    boolean needsNativeCode = false;
    for (int i = 0; i < structType.getNumFields(); i++) {
      if (structType.getField(i).getType().isFunctionPointer()) {
        needsNativeCode = true;
        break;
      }
    }

    String structClassPkg = cfg.packageForStruct(structType.getName());
    PrintWriter writer = null;
    PrintWriter cWriter = null;
    try
    {
      writer = openFile(
        cfg.javaOutputDir() + File.separator +
        CodeGenUtils.packageAsPath(structClassPkg) +
        File.separator + containingTypeName + ".java");
      CodeGenUtils.emitAutogeneratedWarning(writer, this);
      if (needsNativeCode) {
        String nRoot = cfg.nativeOutputDir();
        if (cfg.nativeOutputUsesJavaHierarchy()) {
          nRoot +=
            File.separator +
            CodeGenUtils.packageAsPath(cfg.packageName());
        }
        cWriter = openFile(nRoot + File.separator + containingTypeName + "_JNI.c");
        CodeGenUtils.emitAutogeneratedWarning(cWriter, this);
        emitCHeader(cWriter, 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 net.java.games.gluegen.runtime.*;");
    writer.println();
    List/*<String>*/ javadoc = cfg.javadocForClass(containingTypeName);
    for (Iterator iter = javadoc.iterator(); iter.hasNext(); ) {
      writer.println((String) iter.next());
    }
    writer.println();
    writer.println("public class " + containingTypeName + " {");
    writer.println("  private StructAccessor accessor;");
    writer.println();
    writer.println("  public static int size() {");
    writer.println("    return " + structType.getSize() + ";");
    writer.println("  }");
    writer.println();
    writer.println("  public " + containingTypeName + "() {");
    writer.println("    this(BufferFactory.newDirectByteBuffer(size()));");
    writer.println("  }");
    writer.println();
    writer.println("  public " + containingTypeName + "(ByteBuffer buf) {");
    writer.println("    accessor = new StructAccessor(buf);");
    writer.println("  }");
    writer.println();
    writer.println("  public ByteBuffer getBuffer() {");
    writer.println("    return accessor.getBuffer();");
    writer.println("  }");
    for (int i = 0; i < structType.getNumFields(); i++) {
      Field field = structType.getField(i);
      Type fieldType = field.getType();
      if (!cfg.shouldIgnore(structType.getName() + " " + field.getName())) {
        if (fieldType.isFunctionPointer()) {
          try {
            // Emit method call and associated native code
            FunctionType   funcType     = fieldType.asPointer().getTargetType().asFunction();
            FunctionSymbol funcSym      = new FunctionSymbol(field.getName(), funcType);
            MethodBinding  binding      = bindFunction(funcSym, containingType, containingCType);
            binding.findThisPointer(); // FIXME: need to provide option to disable this on per-function basis
            MethodBinding  specialBinding    = binding.createNIOBufferVariant();
            writer.println();

            JavaMethodBindingEmitter entryPoint = new JavaMethodBindingImplEmitter(binding, writer, cfg.runtimeExceptionType());
            entryPoint.addModifier(JavaMethodBindingEmitter.PUBLIC);
            if (!binding.needsBody() && !binding.hasContainingType()) {
              entryPoint.addModifier(JavaMethodBindingEmitter.NATIVE);
            }
            entryPoint.emit();

            JavaMethodBindingEmitter wrappedEntryPoint = new JavaMethodBindingEmitter(specialBinding, writer, cfg.runtimeExceptionType(), true);
            wrappedEntryPoint.addModifier(JavaMethodBindingEmitter.PRIVATE);
            wrappedEntryPoint.addModifier(JavaMethodBindingEmitter.NATIVE);
            wrappedEntryPoint.emit();

            CMethodBindingEmitter cEmitter =
              makeCEmitter(specialBinding,
                           false, // overloaded
                           true, // doing impl routine?
                           structClassPkg,
                           containingTypeName,
                           cWriter);
            cEmitter.emit();
          } catch (Exception e) {
            System.err.println("While processing field " + field + " of type " + structType.getName() + ":");
            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 \"" + structType.getName() + "\")");
          }
        
          writer.println();
          writer.println("  public " + fieldType.getName() + " " + field.getName() + "() {");
          writer.println("    return new " + fieldType.getName() + "(accessor.slice(" +
                         field.getOffset() + ", " + fieldType.getSize() + "));");
          writer.println("  }");

          // FIXME: add setter by autogenerating "copyTo" for all compound type wrappers
        } else if (fieldType.isArray()) {
          System.err.println("WARNING: Array fields (field \"" + field + "\" of type \"" + structType.getName() +
                             "\") not implemented yet");
        } else {
          JavaType javaType = null;
          try {
            javaType = typeToJavaType(fieldType, false);
          } catch (Exception e) {
            System.err.println("Error occurred while creating accessor for field \"" +
                               field.getName() + "\" in type \"" + structType.getName() + "\"");
            e.printStackTrace();
            throw(e);
          }
          if (javaType.isPrimitive()) {
            // Primitive type
            String externalJavaTypeName = javaType.getName();
            String internalJavaTypeName = externalJavaTypeName;
            if (isOpaque(fieldType)) {
              internalJavaTypeName = compatiblePrimitiveJavaTypeName(fieldType, javaType);
            }
            String capitalized =
              "" + Character.toUpperCase(internalJavaTypeName.charAt(0)) + internalJavaTypeName.substring(1);
            int slot = slot(fieldType, (int) field.getOffset());
            // Setter
            writer.println();
            writer.println("  public " + containingTypeName + " " + field.getName() + "(" + externalJavaTypeName + " val) {");
            writer.print  ("    accessor.set" + capitalized + "At(" + slot + ", ");
            if (!externalJavaTypeName.equals(internalJavaTypeName)) {
              writer.print("(" + internalJavaTypeName + ") ");
            }
            writer.println("val);");
            writer.println("    return this;");
            writer.println("  }");
            // Getter
            writer.println();
            writer.println("  public " + externalJavaTypeName + " " + field.getName() + "() {");
            writer.print  ("    return ");
            if (!externalJavaTypeName.equals(internalJavaTypeName)) {
              writer.print("(" + externalJavaTypeName + ") ");
            }
            writer.println("accessor.get" + capitalized + "At(" + slot + ");");
            writer.println("  }");
          } else {
            // FIXME
            String name = structType.getName();
            if (name == null) {
              name = structType.toString();
            }
            System.err.println("WARNING: Complicated fields (field \"" + field + "\" of type \"" + name +
                               "\") not implemented yet");
            //          throw new RuntimeException("Complicated fields (field \"" + field + "\" of type \"" + t +
            //                                     "\") not implemented yet");
          }
        }
      }
    }
    emitCustomJavaCode(writer, containingTypeName);
    writer.println("}");
    writer.flush();
    writer.close();
    if (needsNativeCode) {
      cWriter.flush();
      cWriter.close();
    }
  }
  public void endStructs() throws Exception {}

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

  private CMethodBindingEmitter makeCEmitter(MethodBinding binding,
                                             boolean overloaded,
                                             boolean doingImplRoutine,
                                             String bindingJavaPackageName,
                                             String bindingJavaClassName,
                                             PrintWriter output) {
    MessageFormat returnValueCapacityFormat = null;         
    JavaType javaReturnType = binding.getJavaReturnType();
    if (javaReturnType.isNIOBuffer()) {
      // See whether capacity has been specified
      String capacity = cfg.returnValueCapacity(binding.getName());
      if (capacity != null) {
        returnValueCapacityFormat = new MessageFormat(capacity);
      }
    }
    CMethodBindingEmitter cEmitter;
    if (doingImplRoutine) {
      cEmitter = new CMethodBindingImplEmitter(binding, overloaded,
                                               bindingJavaPackageName,
                                               bindingJavaClassName,
                                               cfg.allStatic(), output);
    } else {
      cEmitter = new CMethodBindingEmitter(binding, overloaded,
                                           bindingJavaPackageName,
                                           bindingJavaClassName,
                                           cfg.allStatic(), output);
    }
    if (returnValueCapacityFormat != null) {
      cEmitter.setReturnValueCapacityExpression(returnValueCapacityFormat);
    }
    cEmitter.setTemporaryCVariableDeclarations(cfg.temporaryCVariableDeclarations(binding.getName()));
    cEmitter.setTemporaryCVariableAssignments(cfg.temporaryCVariableAssignments(binding.getName()));
    return cEmitter;
  }

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

    // Opaque specifications override automatic conversions
    TypeInfo info = cfg.typeInfo(cType, typedefDictionary);
    if (info != null) {
      return info.javaType();
    }
    Type t = cType;
    if (t.isInt() || t.isEnum()) {
      switch (t.getSize()) {
       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() + " 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 || arrayDimension(t) > 0) {
        // FIXME: add option to disable generation of typed pointer ->
        // array conversions so that these will be handled with direct
        // buffers (Should this be done later? in expandMethodBinding?)
        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[]
        if (t.pointerDepth() == 1 || arrayDimension(t) == 1) {
          if (targetType.isVoid()) {
            return JavaType.createForVoidPointer();
          } else if (targetType.isInt()) {
            switch (targetType.getSize()) {
              case 1:  return javaType(ArrayTypes.byteArrayClass);
              case 2:  return javaType(ArrayTypes.shortArrayClass);
              case 4:  return javaType(ArrayTypes.intArrayClass);
              case 8:  return javaType(ArrayTypes.longArrayClass);
              default: throw new RuntimeException("Unknown integer array type of size " +
                                                  t.getSize() + " and name " + t.getName());
            }
          } else if (targetType.isFloat()) {
            return javaType(ArrayTypes.floatArrayClass);
          } else if (targetType.isDouble()) {
            return javaType(ArrayTypes.doubleArrayClass);
          } 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 (cType.getName() != null &&
                cType.getName().equals("jobject")) {
              return javaType(java.lang.Object.class);
            }

            return JavaType.createForCStruct(cfg.renameJavaType(targetType.getName()));
          } 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 || arrayDimension(t) == 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()) {
            // t is<type>**, targetType is <type>*, we need to get <type>
            targetType = targetType.asPointer().getTargetType(); 
          } else {
            // t is<type>[][], targetType is <type>[], we need to get <type>
            targetType = targetType.asArray().getElementType(); 
          }
          if (targetType.isInt()) {
            switch (targetType.getSize())
            {
              case 1:  return javaType(ArrayTypes.byteArrayArrayClass);
              // FIXME: handle 2,4,8-byte int types here
              default:
                throw new RuntimeException(
                  "Could not convert C type \"" + t + "\" to appropriate " +
                  "Java type; Currently, the only supported depth=2 " +
                  "pointer/array integer types are \"char**\" and \"char[][]\"");
            }
          } else {
            throw new RuntimeException(
              "Could not convert C type \"" + t + "\" " +
              "to appropriate Java type; need to add support for " +
              "depth=2 pointer/array types with non-integral target " +
              "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=" +
            arrayDimension(t) + " targetType=\"" + targetType + "\"]");
        }
        
      } else {
        throw new RuntimeException(
          "Could not convert C type \"" + t + "\" 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) {
    if (t.isInt()) {
      switch (t.getSize()) {
       case 1:  
       case 2:  
       case 4:  
       case 8:  return byteOffset / t.getSize();
       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 / machDesc.pointerSizeInBytes();
    } 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 int arrayDimension(Type type) {
    ArrayType arrayType = type.asArray();
    if (arrayType == null) {
      return 0;
    }
    return 1 + arrayDimension(arrayType.getElementType());
  }

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

  private String compatiblePrimitiveJavaTypeName(Type fieldType,
                                                 JavaType javaType) {
    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 (fieldType.getSize()) {
      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 =
      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 code = cfg.customJavaCodeForClass(className);
    if (code.size() == 0)
      return;

    writer.println();
    writer.println("  // --- Begin CustomJavaCode .cfg declarations"); 
    for (Iterator iter = code.iterator(); iter.hasNext(); ) {
      writer.println((String) iter.next());
    }
    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 {    
      if (cfg.allStatic() || cfg.emitInterface()) {
        String[] interfaces;
        if (cfg.emitInterface()) {
          List userSpecifiedInterfaces = cfg.extendedInterfaces(cfg.className());
          interfaces = new String[userSpecifiedInterfaces.size()];
          userSpecifiedInterfaces.toArray(interfaces);
        } else {
          interfaces = null;
        }
        
        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());
              }
            }
          };

        CodeGenUtils.emitJavaHeaders(
          javaWriter,
          cfg.packageName(),
          cfg.className(),
          cfg.allStatic() ? true : false, 
          (String[]) cfg.imports().toArray(new String[] {}),
          new String[] { "public" },
          interfaces,
          null,
          docEmitter);               
      }
    
      if (!cfg.allStatic() && cfg.emitImpl()) {
        final List/*<String>*/ implDocs = cfg.javadocForClass(cfg.className());
        CodeGenUtils.EmissionCallback docEmitter =
          new CodeGenUtils.EmissionCallback() {
            public void emit(PrintWriter w) {
              for (Iterator iter = implDocs.iterator(); iter.hasNext(); ) {
                w.println((String) iter.next());
              }
            }
          };

        CodeGenUtils.emitJavaHeaders(
          javaImplWriter,
          cfg.implPackageName(),
          cfg.implClassName(),
          true,
          new String[] { "java.nio.*", cfg.packageName() + ".*" },
          new String[] { "public" },
          new String[] { cfg.className() },
          null,
          docEmitter);                      
      }
          
      if (cfg.emitImpl()) {
        PrintWriter cWriter = cWriter();
        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();
    for (Iterator iter = cfg.customCCode().iterator(); iter.hasNext(); ) {
      cWriter.println((String) iter.next());
    }
    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);
  }

  private MethodBinding bindFunction(FunctionSymbol sym,
                                     JavaType containingType,
                                     Type containingCType) {

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

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

    for (int i = 0; i < sym.getNumArguments(); i++) {
      Type cArgType = sym.getArgumentType(i);
      JavaType mappedType = typeToJavaType(cArgType, true);
      //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(new Integer(i))) {   
        //System.out.println("Forcing conversion of " + binding.getName() + " arg #" + i + " from byte[] to String ");
        if ((mappedType.isArray() &&
             (mappedType.getJavaClass() == ArrayTypes.byteArrayClass ||
              mappedType.getJavaClass() == ArrayTypes.byteArrayArrayClass)) ||
            (mappedType.isVoidPointerType())) {
          // convert mapped type from void* and byte[] to String, or byte[][] to String[]
          if (mappedType.getJavaClass() == ArrayTypes.byteArrayArrayClass) {
            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 *\", or \"char**\" equivalent");
        }
      }
      binding.addJavaArgumentType(mappedType);
      //System.out.println("During binding of [" + sym + "], added mapping from C type: " + cArgType + " to Java type: " + mappedType);
    }

    //System.err.println("---> " + binding);
    //System.err.println("    ---> " + binding.getCSymbol());
    return binding;
  }
  
  // Expands a MethodBinding containing void pointer types into
  // multiple variants taking double arrays and NIO buffers, subject
  // to the per-function "NIO only" rule in the configuration file
  private List/*<MethodBinding>*/ expandMethodBinding(MethodBinding binding) {
    List result = new ArrayList();
    result.add(binding);
    int i = 0;
    while (i < result.size()) {
      MethodBinding mb = (MethodBinding) result.get(i);
      boolean shouldRemoveCurrent = false;
      for (int j = 0; j < mb.getNumArguments(); j++) {
        JavaType t = mb.getJavaArgumentType(j);
        if (t.isVoidPointerType()) {
          // Create variants
          MethodBinding variant = null;
          if (!cfg.nioOnly(mb.getCSymbol().getName())) {                
            variant = mb.createVoidPointerVariant(j, javaType(ArrayTypes.booleanArrayClass));
            if (! result.contains(variant)) result.add(variant); 
            variant = mb.createVoidPointerVariant(j, javaType(ArrayTypes.byteArrayClass));
            if (! result.contains(variant)) result.add(variant); 
            variant = mb.createVoidPointerVariant(j, javaType(ArrayTypes.charArrayClass));
            if (! result.contains(variant)) result.add(variant); 
            variant = mb.createVoidPointerVariant(j, javaType(ArrayTypes.shortArrayClass));
            if (! result.contains(variant)) result.add(variant); 
            variant = mb.createVoidPointerVariant(j, javaType(ArrayTypes.intArrayClass));
            if (! result.contains(variant)) result.add(variant); 
            variant = mb.createVoidPointerVariant(j, javaType(ArrayTypes.longArrayClass));
            if (! result.contains(variant)) result.add(variant); 
            variant = mb.createVoidPointerVariant(j, javaType(ArrayTypes.floatArrayClass));
            if (! result.contains(variant)) result.add(variant); 
            variant = mb.createVoidPointerVariant(j, javaType(ArrayTypes.doubleArrayClass));
            if (! result.contains(variant)) result.add(variant); 
          }
          variant = mb.createVoidPointerVariant(j, JavaType.forNIOBufferClass());
          if (! result.contains(variant)) result.add(variant); 

          // Remove original from list
          shouldRemoveCurrent = true;
        }
      }
      if (mb.getJavaReturnType().isVoidPointerType()) {
        MethodBinding variant = mb.createVoidPointerVariant(-1, JavaType.forNIOByteBufferClass());
        if (! result.contains(variant)) result.add(variant); 
        shouldRemoveCurrent = true;
      }
      if (shouldRemoveCurrent) {
        result.remove(i);
        --i;
      }
      ++i;
    }
    return result;
  }

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

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