package nxm.sys.lib;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintStream;
import java.util.Hashtable;
import java.util.Stack;
import java.util.Vector;

/** Utilities for working with Fortran code. These are primarily uses by CONVERT when building
    XBC or building NXMOPT. The methods in this class differ from those in {@link XmUtil} in that
    they are not specific to X-Midas interoperability and are applicable to other tasks.

    @author  J. Czechowski IV
    @since   NeXtMidas 2.1.1
 */
public final class FortranUtil {
  /** Prevent instantiation since all methods are static. */
  private FortranUtil() { }

  /** Creates the wrap methods for a ByteBuffer.
      @param file The file to process.
      @throws IOException       If an I/O error occurs.
      @throws RuntimeException  If a syntax or semantic error occurs in the file.
   */
  public static void wrapperByteBuffer(File file) throws IOException {
    wrapperByteBuffer(file,false);
  }

  /** Creates the wrap methods for a ByteBuffer.
      @param file     The file to process.
      @param isStatic Indicates if the generated methods should be static.
      @throws IOException       If an I/O error occurs.
      @throws RuntimeException  If a syntax or semantic error occurs in the file.
   */
  public static void wrapperByteBuffer(File file, boolean isStatic) throws IOException {
    (new BBMake(file,isStatic)).processFile();
  }

  /** Processes a file. <br>
      <br>
      The entire Fortran file is assumed to be case-insensitive and is read as if it was entirely
      in upper-case and without whitespace -- Fortran 77 style). However the structure name and
      variable name passed in will be treated as case sensitive with respect to the JNI .c file
      generated (since ANSI C 89 is case sensitive).

      @param inFile     The input file (a Fortran .inc file).
      @param outFile    The output file (the Java .java file).
      @param jniFile    The output file (the JNI .c file).
      @param structure  The structure name to translate (case sensitive for the .c file).
      @param varName    The variable name to get a buffer for (case sensitive for the .c file).
      @throws IOException if error occurs while processing the file
   */
  public static void wrapFortranStruct(File inFile, File outFile, File jniFile, String structure,
                                       String varName) throws IOException {
    wrapFortranStruct(inFile,outFile,jniFile,structure,varName,null,null,false);
  }

  /** Processes a file. <br>
      <br>
      The entire Fortran file is assumed to be case-insensitive and is read as if it was entirely
      in upper-case and without whitespace -- Fortran 77 style). However the structure name and
      variable name passed in will be treated as case sensitive with respect to the JNI .c file
      generated (since ANSI C 89 is case sensitive).

      @param inFile     The input file (a Fortran .inc file).
      @param outFile    The output file (the Java .java file).
      @param jniFile    The output file (the JNI .c file).
      @param structure  The structure name to translate (case sensitive for the .c file).
      @param varName    The variable name to get a buffer for (case sensitive for the .c file).
      @param subs       The substitute name for fields named "Class" since "getClass" is used by
                        {@link Object}.
      @param includes   Comma-separated list of #include files.
      @param pkgPrivate If this is true then the class will be package-private, otherwise it will
                        be public.
      @throws IOException if error occurs while processing the file
   */
  public static void wrapFortranStruct(File inFile, File outFile, File jniFile, String structure,
                                       String varName, String subs, String includes, boolean
                                       pkgPrivate) throws IOException {
    boolean isStatic = (varName != null) && (varName.trim().length() > 0);
    BBConv  bbconv   = new BBConv(subs, includes, isStatic, pkgPrivate);

    try {
      bbconv.process(inFile, outFile, jniFile, structure, varName);

    } catch(Exception e) {
      if (!bbconv.includeList.empty()) {
        throw new MidasException(bbconv.includeList.peek()+": "+e.getMessage(),e);
      }
    }

    BBMake.processFile(outFile, isStatic);
  }


  /** Generates the methods for accessing a Java NIO ByteBuffer based on the structure of the byte
      buffer as specified in a table at the top of the Java file. <br>
      <br>
      The definitions must be at the top of the Java file and must be commented out using "//"
      notation. The first line in the definition must be &lt;BBMakeDefs&gt; and the last line
      must be &lt;/BBMakeDefs&gt;. Any blank lines or lines that start with a "#" are ignored.
      All other lines have a single definition per line and are of the following format:
      <pre>
        // ByteOffset, Type, AccessorName, ModifierName, "Comment"
      </pre>
      Where:
      <pre>
        ByteOffset   - The byte offset of the field. This can be set to "~" to
                       indicate that the byte offset is relative to the the
                       previous definition.

        Type         - The data type of the field. Allowable types include:
                         byte,short,int,   --+
                         long,float,double   | Same as in Java
                         char              --+

                         AsciiChar         --  7-bit ASCII character (in a 8-bit
                                               byte) that is converted to a Java
                                               char on reading.

                         AsciiString(n)    -- An array of exactly n ASCII characters
                                              converted to a Java String on reading.

                         FortranString(n)  -- An array of 1 to n ASCII characters
                                              padded out to n bytes with spaces
                                              (same as Fortran) converted to a
                                              Java String on reading.

                         CString(n)        -- An array of 0 to n-1 ASCII characters
                                              null-terminated (same as C/C++)
                                              converted to a Java String on reading.

                       Boolean(byte)       -- An  8-bit boolean value (0=False).
                       Boolean(short)      -- An 16-bit boolean value (0=False).
                       Boolean(int)        -- An 32-bit boolean value (0=False).
                       Boolean(long)       -- An 64-bit boolean value (0=False).

        AccessorName - The name of the accessor to use (e.g. "getFoo") or
                       "&lt;none&gt;" if no accessor should be generated.

        ModifierName - The name of the modifier to use (e.g. "setFoo") or
                       "&lt;none&gt;" if no modifier should be generated.

        Comment      - A brief description of the field (should be quoted).
      </pre>
   */
  private static class BBMake {
    /** Regular expression marking the start of the definitions. */
    private static final String BBMAKE_DEFS_START    = "\\s*//\\s*[<]BBMakeDefs[>].*";

    /** Regular expression marking the end of the definitions. */
    private static final String BBMAKE_DEFS_END      = "\\s*//\\s*[<]/BBMakeDefs[>].*";

    /** Regular expression marking the start of the generated code. */
    private static final String BBMAKE_GENCODE_START = "\\s*//\\s*[<]BBMakeGenCode[>].*";

    /** Regular expression marking the end of the generated code. */
    private static final String BBMAKE_GENCODE_END   = "\\s*//\\s*[<]/BBMakeGenCode[>].*";

    /** The whitespace to use as a tab character. */
    private static final String tab                  = "  ";

    private int       insertIndex = -1;              // The code insertion index.
    private Vector<String>    fileData = new Vector<String>();    // The data from the file.
    private Vector<RecordDef> records  = new Vector<RecordDef>(); // The record definitions from the file (same as recordsMap).
    private Hashtable<String,RecordDef> recordsMap = new Hashtable<String,RecordDef>(); // The record definitions from the file (same as records).
    private Vector<String>    dumpCode = new Vector<String>();   // The code for the dump method.
    private String    index       = null;            // The the current index.
    private File      file        = null;            // The file being used.
    private boolean   isStatic;                      // Indicates if the generated methods should be static.

    /** Creates a new instance.
        @param file       The file to work with.
        @param isStatic   Indicates if the generated methods should be static.
     */
    private BBMake(File file, boolean isStatic) {
      this.file       = file;
      this.isStatic   = isStatic;
    }

    /** Processes a file.
        @param file The file to process.
        @throws IOException       If an I/O error occurs.
        @throws RuntimeException  If a syntax or semantic error occurs in the file.
     */
    public static void processFile(File file) throws IOException {
      processFile(file,false);
    }

    /** Processes a file.
        @param file     The file to process.
        @param isStatic Indicates if the generated methods should be static.
        @throws IOException       If an I/O error occurs.
        @throws RuntimeException  If a syntax or semantic error occurs in the file.
     */
    public static void processFile(File file, boolean isStatic) throws IOException {
      (new BBMake(file,isStatic)).processFile();
    }

    /** Processes the file.
        @throws IOException       If an I/O error occurs.
        @throws RuntimeException  If a syntax or semantic error occurs in the file.
     */
    private void processFile() throws IOException {
      BufferedReader in = new BufferedReader(new FileReader(file));
      readFile(in);
      in.close();

      PrintStream out = new PrintStream(new FileOutputStream(file));
      writeFile(out);
      out.close();
    }

    /** Reads the file in.
        @param in The input source.
        @throws IOException       If an I/O error occurs.
        @throws RuntimeException  If a syntax or semantic error occurs in the file.
     */
    private void readFile(BufferedReader in) throws IOException {
      int lastCloseBrace = -1;
      String line;

      while((line = in.readLine()) != null) {
        if (line.matches(BBMAKE_DEFS_START)) {
          RecordDef recdef = null;

          fileData.add(line);
          while((line = in.readLine()) != null) {
            fileData.add(line);

            if (line.matches(BBMAKE_DEFS_END)) {
              break;
            } else {
              String def = line.trim();

              if (def.startsWith("//")) { def = def.substring(2).trim(); }

              if (def.startsWith("#")) {
                /* Strip Comments */
              } else if (def.length() == 0) {
                /* Strip blank lines */
              } else if ((recdef != null) && (def.startsWith("<"))) {
                String tag = "</"+recdef.getName()+">";

                if (def.equals(tag)) {
                  recdef = null;
                } else {
                  throw new RuntimeException("Expected '"+tag+"', but found '"+def+"'.");
                }
              } else if ((recdef == null) && (def.startsWith("<"))) {
                if (def.equals("<MAIN>")) {
                  recdef = new RecordDef("MAIN", null);

                } else {
                  int index = def.indexOf(" size=");

                  if (index < 0) {
                    throw new RuntimeException("No size specified for record '"+def+"'.");
                  }

                  String name = def.substring(1,index).trim();
                  String size = def.substring(index+6,def.length()-1).trim();

                  if (size.startsWith("\"") && size.endsWith("\"")) {
                    size = size.substring(1, size.length()-1).trim();
                  }

                  if ((size.length() == 0) || (size.indexOf('"') > 0)) {
                    throw new RuntimeException("Illegal size specified for record '"+def+"'.");
                  }
                  recdef = new RecordDef(name, size);
                }
                records.add(recdef);
                recordsMap.put(recdef.getName(),recdef);

              } else if (recdef == null) {
                throw new RuntimeException("Definition is not part of a given record '"+def+"'.");

              } else {
                recdef.getRecordDefs().add(def);
              }
            }
          }
        } else if (line.matches(BBMAKE_GENCODE_START)) {
          insertIndex = fileData.size();

          /* Remove old generated code. */
          while((line = in.readLine()) != null) {
            if (line.matches(BBMAKE_GENCODE_END)) { break; }
          }
        } else {
          if (line.trim().equals("}")) { lastCloseBrace = fileData.size(); }
          fileData.add(line);
        }
      }

      if (insertIndex < 0) {
        insertIndex = lastCloseBrace;
      }

      in.close();
    }

    /** Writes the file out.
        @param out The output source.
        @throws IOException If an I/O error occurs.
     */
    private void writeFile(PrintStream out) throws IOException {
      for (int i = 0; i < fileData.size(); i++) {
        if (i == insertIndex) {
          writeCode(out);
        }
        out.println(fileData.get(i));
      }
    }

    /** Writes out the auto-generated code.
        @param out The output source.
        @throws IOException       If an I/O error occurs.
     */
    private void writeCode(PrintStream out) throws IOException {
      // The 'editor-fold' tag indicates that IDE's like NetBeans should collapse the auto-generated
      // code by default.
      out.println(tab+"// <BBMakeGenCode>//"+"GEN-"+"BEGIN:BBMakeGenCode");
      out.println(tab+"/* This next section of code generated by BBDEF2JAVA in CONVERT.");
      out.println(tab+"   DO NOT EDIT THIS CODE, it will be re-written every time");
      out.println(tab+"   BBDEF2JAVA is run and any changes will be lost.");
      out.println(tab+" */");
      out.println(tab+"//   <editor-fold defaultstate=\"collapsed\" desc=\" Generated Code \">");
      // Always do MAIN first.
      RecordDef main = recordsMap.get("MAIN");
      if (main != null) {
        writeCode(out, main, true);

        // Write out the dumpCode method
        String mods   = (isStatic)? "public static": "public";
        String spaces = "                    ";
        out.println(tab+"/** Dumps the data in the buffer to a String. This method is for");
        out.println(tab+"    debugging purposes only its syntax may change in the future");
        out.println(tab+"    without notice.");
        out.println(tab+"    ");
        out.println(tab+"    @return A string listing the current data in the buffer.");
        out.println(tab+" */");
        out.println(tab+mods+" String dumpData() {");
        out.println(tab+tab+"StringBuilder sb = new StringBuilder();");
        for (String method : dumpCode) {
          String pad = spaces.substring(Math.min(spaces.length(), method.length()));

          out.println(tab+tab+"sb.append(\""+method+pad+" = \").append("+method+"()"+pad+").append('\\n');");
        }
        out.println(tab+tab+"return sb.toString();");
        out.println(tab+"}");
      }

      // Do all the rest.
      for (RecordDef rec : records) {
        if (!rec.getName().equals("MAIN")) {
          // Start the nested class
          out.println(CLASS_DEFINITION_START.replaceAll("[$]name[$]", rec.getName()));

          // Write out the methods
          writeCode(out, rec, false);

          // write the toString method
          out.println(tab+tab+"/** Summarizes the record.");
          out.println(tab+tab+"    @return A string listing the current data in the record.");
          out.println(tab+tab+" */");
          out.println(tab+tab+"public String toString() {");
          out.println(tab+tab+tab+"StringBuilder sb = new StringBuilder();");
          out.println(tab+tab+tab+"sb.append(\""+rec.getName()+": {\");");
          for (String method : dumpCode) {
            out.println(tab+tab+tab+"sb.append(\" "+method+" = \").append("+method+"());  ");
          }
          out.println(tab+tab+tab+"sb.append(\" }\");");
          out.println(tab+tab+tab+"return sb.toString();");
          out.println(tab+tab+"}");

          // Start the nested class
          out.println(CLASS_DEFINITION_END);
        }
      }
      out.println(tab+"//   </editor-fold>");
      out.println(tab+"// </BBMakeGenCode>//"+"GEN"+"-END:BBMakeGenCode");
    }

    /** Writes out the auto-generated code for a complete record.
        @param out    The output source.
        @param def    The definition of the structure.
        @param isMain Indicates if it is the main structure for the class.
        @throws IOException       If an I/O error occurs.
     */
    private void writeCode(PrintStream out, RecordDef def, boolean isMain) throws IOException {
      Vector<String> recordDefs = def.getRecordDefs();

      setIndex("0");
      dumpCode.clear();
      for (String record : recordDefs) {
        writeMethod(out, record, isMain);
      }
    }

    /** Writes out the auto-generated code for a complete record.

        @param out    The output source.
        @param def    The definition of the structure.
        @param isMain Indicates if it is the main structure for the class.

        @throws IOException       If an I/O error occurs.
     */
    private void writeMethod(PrintStream out, String def, boolean isMain) throws IOException {
      String[] defs       = parseDefs(def);
      String   pos        = defs[0];
      String   type       = defs[1].replaceAll("[ ]", ""); //Remove extra space
      String   getName    = defs[2];
      String   setName    = defs[3];
      String   text       = defs[4];
      String   size       = null;
      String   getMethod  = null;
      String   setMethod  = null;

      if (pos.equals("~")) { pos = getIndex(); }
      else                 { setIndex(pos);    }

      if (!isMain) { pos = "(this.offset + ("+pos+"))"; }

      if (text.startsWith("\"")) { text = text.substring(1).trim(); }
      if (text.endsWith("\""))   { text = text.substring(0,text.length()-1).trim(); }
      if (text.endsWith("."))    { text = text.substring(0,text.length()-1).trim(); }

      if (getName.length() == 0) { getName = "<none>"; }
      if (setName.length() == 0) { setName = "<none>"; }
      if (text.length()    == 0) { text    = "unknown"; }

      if ((type.indexOf("(") > 0) && type.endsWith(")")) {
        String strType = type.substring(0, type.indexOf("(")).trim();
        String strSize = type.substring(type.indexOf("(")+1, type.length()-1).trim();
        type = strType;
        size = strSize;
      }

      if ((type.indexOf("[") > 0) && type.endsWith("]")) {
        String primType  = type.substring(0, type.indexOf("[")).trim();
        String arraySize = type.substring(type.indexOf("[")+1, type.length()-1).trim();
        type = primType;
        size = arraySize;
      }

      if (BBConv.getPrimitiveLength(type) != null) { // Primitive type
        String bytes = BBConv.getPrimitiveLength(type);
        String get   = null;
        String set   = null;

        if (type.equals("byte")) {
          get = "get";
          set = "put";
        } else {
          get = "get"+type.substring(0,1).toUpperCase()+type.substring(1);
          set = "put"+type.substring(0,1).toUpperCase()+type.substring(1);
        }

        if (!getName.equals("<none>")) {
          if (size == null) {
            getMethod = useTemplate(GET_PRIMITIVE_VALUE, text, getName, type, get, pos, size, bytes);
          } else {
            getMethod = useTemplate(GET_PRIMITIVE_ARRAY, text, getName, type, get, pos, size, bytes);
          }
        }
        if (!setName.equals("<none>")) {
          if (size == null) {
            setMethod = useTemplate(SET_PRIMITIVE_VALUE, text, setName, type, set, pos, size, bytes);
          } else {
            setMethod = useTemplate(SET_PRIMITIVE_ARRAY, text, setName, type, set, pos, size, bytes);
            if (setName.equals("setReadAux") && type.equals("byte")) { // since 3.1.3: for X-Midas 4.11.0 and prior (BUG 2608)
              setMethod += useTemplate(SET_PRIMITIVE_ARRAY_FROM_SHORT_TO_BYTE, text, setName, type, set, pos, size, bytes);
            }
          }
        }
        addToIndex(BBConv.getPrimitiveLength(type), size);

      } else if (type.equals("AsciiChar") && (size == null)) {
        if (!getName.equals("<none>")) {
          getMethod = useTemplate(GET_ASCII_CHAR, text, getName, null, null, pos, null, null);
        }
        if (!setName.equals("<none>")) {
          setMethod = useTemplate(SET_ASCII_CHAR, text, setName, null, null, pos, null, null);
        }
        addToIndex("1");

      } else if ((size != null) && type.equals("AsciiString")) {
        if (!getName.equals("<none>")) {
          getMethod = useTemplate(GET_ASCII_STRING, text, getName, null, null, pos, size, null);
        }
        if (!setName.equals("<none>")) {
          setMethod = useTemplate(SET_ASCII_STRING, text, setName, null, null, pos, size, null);
        }
        addToIndex(size);

      } else if ((size != null) && type.equals("CString")) {
        if (!getName.equals("<none>")) {
          getMethod = useTemplate(GET_C_STRING, text, getName, null, null, pos, size, null);
        }
        if (!setName.equals("<none>")) {
          setMethod = useTemplate(SET_C_STRING, text, setName, null, null, pos, size, null);
        }
        addToIndex(size);

      } else if ((size != null) && type.equals("FortranString")) {
        if (!getName.equals("<none>")) {
          getMethod = useTemplate(GET_FORTRAN_STRING, text, getName, null, null, pos, size, null);
        }
        if (!setName.equals("<none>")) {
          setMethod = useTemplate(SET_FORTRAN_STRING, text, setName, null, null, pos, size, null);
        }
        addToIndex(size);

      } else if ((BBConv.getPrimitiveLength(size) != null) && (type.equals("Boolean"))) {
        String get = null;
        String set = null;

        if (size.equals("byte")) {
          get = "get";
          set = "put";
        } else {
          get = "get"+size.substring(0,1).toUpperCase()+size.substring(1);
          set = "put"+size.substring(0,1).toUpperCase()+size.substring(1);
        }

        if (!getName.equals("<none>")) {
          getMethod = useTemplate(GET_BOOLEAN, text, getName, size, get, pos, null, null);
        }
        if (!setName.equals("<none>")) {
          setMethod = useTemplate(SET_BOOLEAN, text, setName, size, set, pos, null, null);
        }
        addToIndex(BBConv.getPrimitiveLength(size));

      } else if ((!type.equals("MAIN")) && (recordsMap.get(type) != null)) {
        RecordDef recdef = recordsMap.get(type);
        String    bytes  = recdef.getSize();

        if (!getName.equals("<none>")) {
          if (size == null) {
            getMethod = useTemplate(GET_RECORD_VALUE, text, getName, type, null, pos, size, bytes);
          } else {
            getMethod = useTemplate(GET_RECORD_ARRAY, text, getName, type, null, pos, size, bytes);
          }
        }
        addToIndex(bytes, size);

      } else {
        throw new RuntimeException("Invalid type '"+type+"'");
      }

      if (isMain && isStatic) {
        if (getMethod != null) { getMethod = getMethod.replaceAll("[$]mods[$]", "public static"); }
        if (setMethod != null) { setMethod = setMethod.replaceAll("[$]mods[$]", "public static"); }
      } else {
        if (getMethod != null) { getMethod = getMethod.replaceAll("[$]mods[$]", "public"); }
        if (setMethod != null) { setMethod = setMethod.replaceAll("[$]mods[$]", "public"); }

        if (!isMain) {
          if (getMethod != null) { getMethod = tab+getMethod.replaceAll("\n", "\n"+tab); }
          if (setMethod != null) { setMethod = tab+setMethod.replaceAll("\n", "\n"+tab); }
        }
      }

      if (getMethod != null) { out.println(getMethod); }
      if (setMethod != null) { out.println(setMethod); }
      if (getMethod != null) { dumpCode.add(getName);  }
    }

    /** Parses a definition into its component parts. */
    private String[] parseDefs(String def) {
      Parser parser = new Parser(def);
      parser.clean(false);

      return parser.getArray(parser.elements());
    }

    /** Gets the current byte index. */
    private String getIndex() {
      if (index == null) { index = "0"; }
      return index;
    }

    /** Sets the current byte index. */
    private void setIndex(String val) {
      index = val;
    }

    /** Adds a value to the index. */
    private void addToIndex(String val) {
      index = BBConv.add(index, val);
    }

    /** Adds a value to the index.
        @param val  The value to add.
        @param mult The multiplier.
     */
    private void addToIndex(String val, String mult) {
      addToIndex(BBConv.mul(val, mult));
    }

    /** The definition of a record. */
    private static class RecordDef {
      private final String name;                // The name of the structure.
      private final String size;                // The size of the structure.
      private final Vector<String> defs = new Vector<String>(); // The record definitions of the structure.

      /** Creates a new instance.
          @param name The name of the structure.
          @param size The size of the structure.
       */
      public RecordDef(String name, String size) {
        this.name = name;
        this.size = size;
      }

      /** Gets the name of the structure. */
      public String getName() { return name; }
      /** Gets the size of the structure. */
      public String getSize() { return size; }
      /** Gets the record definitions of the structure. */
      public Vector<String> getRecordDefs() { return defs; }
    }

    /** Gets a filled-in template. */
    private String useTemplate(String temp, String text, String name, String type,
                               String method, String pos, String size, String bytes) {
      if (text   == null) { text   = ""; }
      if (name   == null) { name   = ""; }
      if (type   == null) { type   = ""; }
      if (method == null) { method = ""; }
      if (pos    == null) { pos    = ""; }
      if (size   == null) { size   = ""; }
      if (bytes  == null) { bytes  = ""; }

      if (text.endsWith(".")      ) { text = text.substring(0, text.length()-1); }
      if (BBConv.USE_JAVADOC_BREAK) { text = text.replaceAll("[.]", "&#46;"  );  }

      return temp.replaceAll("[$]text[$]",   StringUtil.textToReplacement(text))
                 .replaceAll("[$]name[$]",   StringUtil.textToReplacement(name))
                 .replaceAll("[$]type[$]",   StringUtil.textToReplacement(type))
                 .replaceAll("[$]method[$]", StringUtil.textToReplacement(method))
                 .replaceAll("[$]pos[$]",    StringUtil.textToReplacement(pos))
                 .replaceAll("[$]size[$]",   StringUtil.textToReplacement(size))
                 .replaceAll("[$]bytes[$]",  StringUtil.textToReplacement(bytes));
    }

    //////////////////////////////////////////////////////////////////////////////////////////////////
    // METHOD TEMPLATES
    //////////////////////////////////////////////////////////////////////////////////////////////////
    private static final String SET_PRIMITIVE_VALUE
      =tab+"/** Sets the $text$.\n"
      +tab+"    @param val The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ void $name$($type$ val) {\n"
      +tab+tab+"getByteBuffer().$method$($pos$, val);\n"
      +tab+"}\n";

    private static final String SET_PRIMITIVE_ARRAY
      =tab+"/** Sets the $text$.\n"
      +tab+"    @param val The $text$ (must be an array of size $size$).\n"
      +tab+" */\n"
      +tab+"$mods$ void $name$($type$[] val) {\n"
      +tab+tab+"if (val.length != $size$) {\n"
      +tab+tab+tab+"throw new IllegalArgumentException(\"$name$: expected array of\"\n"
      +tab+tab+tab+"  + \"length $size$, given array of length \"+val.length+\".\");\n"
      +tab+tab+"}\n"
      +tab+tab+"for (int i = 0; i < val.length; i++) {\n"
      +tab+tab+tab+"int off = i * $bytes$;\n"
      +tab+tab+tab+"getByteBuffer().$method$(($pos$)+off, val[i]);\n"
      +tab+tab+"}\n"
      +tab+"}\n"
      +tab+"/** Sets the $text$ at the specified index.\n"
      +tab+"    @param val The $text$ at the specified index.\n"
      +tab+"    @since 3.1.2\n"
      +tab+" */\n"
      +tab+"$mods$ void $name$At(int index, $type$ val) {\n"
      +tab+tab+"if (index >= $size$) {\n"
      +tab+tab+tab+"throw new IllegalArgumentException(\"$name$: index [\"+index+\"] is out of range [\"+$size$+\"]\");\n"
      +tab+tab+"}\n"
      +tab+tab+"int off = index * $bytes$;\n"
      +tab+tab+"getByteBuffer().$method$(($pos$)+off, val);\n"
      +tab+"}\n";
    private static final String SET_PRIMITIVE_ARRAY_FROM_SHORT_TO_BYTE
      =tab+"/** Sets the $text$ at the specified index by an short integer value.\n"
      +tab+"    This is meant for INTERNAL USE ONLY and is subject to change/removal.\n"
      +tab+"    @param val The $text$ at the specified index (this short integer will be casted to a byte integer).\n"
      +tab+"    @since 3.1.3\n"
      +tab+" */\n"
      +tab+"$mods$ final void $name$At(int index, short val) {\n"
      +tab+tab+"$name$At(index, (byte)val);\n"
      +tab+"}\n";

    private static final String GET_PRIMITIVE_VALUE
      =tab+"/** Gets the $text$.\n"
      +tab+"    @return The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ $type$ $name$() {\n"
      +tab+tab+"return getByteBuffer().$method$($pos$);\n"
      +tab+"}\n";

    private static final String GET_PRIMITIVE_ARRAY
      =tab+"/** gets the $text$.\n"
      +tab+"    @return The $text$ (an array of size $size$).\n"
      +tab+" */\n"
      +tab+"$mods$ $type$[] $name$() {\n"
      +tab+tab+"$type$[] val = new $type$[$size$];\n"
      +tab+tab+"for (int i = 0; i < val.length; i++) {\n"
      +tab+tab+tab+"int off = i * $bytes$;\n"
      +tab+tab+tab+"val[i] = getByteBuffer().$method$(($pos$)+off);\n"
      +tab+tab+"}\n"
      +tab+tab+"return val;\n"
      +tab+"}\n"
      +tab+"/** Gets the $text$ at the specified index.\n"
      +tab+"    @return The $text$ at the specified index.\n"
      +tab+"    @since 3.1.2\n"
      +tab+" */\n"
      +tab+"$mods$ $type$ $name$At(int index) {\n"
      +tab+tab+"int off = index * $bytes$;\n"
      +tab+tab+"$type$ val = getByteBuffer().$method$(($pos$)+off);\n"
      +tab+tab+"return val;\n"
      +tab+"}";

    private static final String SET_ASCII_CHAR
      =tab+"/** Sets the $text$.\n"
      +tab+"    @param val The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ void $name$(char val) {\n"
      +tab+tab+"if (val > 0x7f) {\n"
      +tab+tab+tab+"throw new IllegalArgumentException(\"Expected ASCII character "
                  +"given '\"+val+\"'.\");\n"
      +tab+tab+"}\n"
      +tab+tab+"getByteBuffer().put($pos$, (byte)val);\n"
      +tab+"}\n";

    private static final String GET_ASCII_CHAR
      =tab+"/** Gets the $text$.\n"
      +tab+"    @return The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ char $name$() {\n"
      +tab+tab+"return (char)getByteBuffer().get($pos$);\n"
      +tab+"}\n";

    private static final String SET_ASCII_STRING
      =tab+"/** Sets the $text$.\n"
      +tab+"    @param val The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ void $name$(String val) {\n"
      +tab+tab+"for (int i = 0; i < $size$; i++) {\n"
      +tab+tab+tab+"if (i < val.length()) {\n"
      +tab+tab+tab+tab+"char letter = val.charAt(i);\n"
      +tab+tab+tab+tab+"if (letter > 0x7f) {\n"
      +tab+tab+tab+tab+tab+"throw new IllegalArgumentException(\"Expected ASCII character "
                          +"given '\"+letter+\"'.\");\n"
      +tab+tab+tab+tab+"} else {\n"
      +tab+tab+tab+tab+tab+tab+"getByteBuffer().put(($pos$)+i, (byte)letter);\n"
      +tab+tab+tab+tab+"}\n"
      +tab+tab+tab+"} else {\n"
      +tab+tab+tab+tab+"getByteBuffer().put(($pos$)+i, (byte)' ');\n"
      +tab+tab+tab+"}\n"
      +tab+tab+"}\n"
      +tab+"}\n";

    private static final String SET_FORTRAN_STRING = SET_ASCII_STRING;

    private static final String SET_C_STRING
      =tab+"/** Sets the $text$.\n"
      +tab+"    @param val The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ void $name$(String val) {\n"
      +tab+tab+"final byte NULL = 0;\n"
      +tab+tab+"for (int i = 0; i < $size$; i++) {\n"
      +tab+tab+tab+"if (i < val.length()) {\n"
      +tab+tab+tab+tab+"char letter = val.charAt(i);\n"
      +tab+tab+tab+tab+"if (letter > 0x7f) {\n"
      +tab+tab+tab+tab+tab+"throw new IllegalArgumentException(\"Expected ASCII character \"\n"
                          +"given '\"+letter+\"'.\");\n"
      +tab+tab+tab+tab+"} else {\n"
      +tab+tab+tab+tab+tab+tab+"getByteBuffer().put(($pos$)+i, (byte)letter);\n"
      +tab+tab+tab+tab+"}\n"
      +tab+tab+tab+"} else {\n"
      +tab+tab+tab+tab+"getByteBuffer().put(($pos$)+i, NULL);\n"
      +tab+tab+tab+"}\n"
      +tab+tab+"}\n"
      +tab+"}\n";

    private static final String GET_ASCII_STRING
      =tab+"/** Gets the $text$.\n"
      +tab+"    @return The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ String $name$() {\n"
      +tab+tab+"StringBuilder str = new StringBuilder();\n"
      +tab+tab+"\n"
      +tab+tab+"for (int i = 0; i < $size$; i++) {\n"
      +tab+tab+tab+"str.append((char)getByteBuffer().get(($pos$)+i));\n"
      +tab+tab+"}\n"
      +tab+tab+"return str.toString();\n"
      +tab+"}\n";

    private static final String GET_FORTRAN_STRING
      =tab+"/** Gets the $text$.\n"
      +tab+"    @return The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ String $name$() {\n"
      +tab+tab+"StringBuilder str = new StringBuilder();\n"
      +tab+tab+"StringBuilder pad = null; // Trailing spaces\n"
      +tab+tab+"\n"
      +tab+tab+"for (int i = 0; i < $size$; i++) {\n"
      +tab+tab+tab+"char letter = (char)getByteBuffer().get(($pos$)+i);\n"
      +tab+tab+tab+"\n"
      +tab+tab+tab+"if (i == 0) {\n"
      +tab+tab+tab+tab+"str.append(letter);\n"
      +tab+tab+tab+"} else if (letter == ' ') {\n"
      +tab+tab+tab+tab+"if (pad == null) {\n"
      +tab+tab+tab+tab+tab+"pad = new StringBuilder(\" \");\n"
      +tab+tab+tab+tab+"} else {\n"
      +tab+tab+tab+tab+tab+"pad.append(letter);\n"
      +tab+tab+tab+tab+"}\n"
      +tab+tab+tab+"} else {\n"
      +tab+tab+tab+tab+"if (pad != null) {\n"
      +tab+tab+tab+tab+tab+"str.append(pad);\n"
      +tab+tab+tab+tab+tab+"pad = null;\n"
      +tab+tab+tab+tab+"}\n"
      +tab+tab+tab+tab+"str.append(letter);\n"
      +tab+tab+tab+"}\n"
      +tab+tab+"}\n"
      +tab+tab+"return str.toString();\n"
      +tab+"}\n";

    private static final String GET_C_STRING
      =tab+"/** Gets the $text$.\n"
      +tab+"    @return The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ String $name$() {\n"
      +tab+tab+"final byte NULL = 0;\n"
      +tab+tab+"StringBuilder str = new StringBuilder();\n"
      +tab+tab+"\n"
      +tab+tab+"for (int i = 0; i < $size$; i++) {\n"
      +tab+tab+tab+"char letter = (char)getByteBuffer().get(($pos$)+i);\n"
      +tab+tab+tab+"\n"
      +tab+tab+tab+"if (letter == NULL) {\n"
      +tab+tab+tab+tab+"break;\n"
      +tab+tab+tab+"} else {\n"
      +tab+tab+tab+tab+"str.append(letter);\n"
      +tab+tab+tab+"}\n"
      +tab+tab+"}\n"
      +tab+tab+"return str.toString();\n"
      +tab+"}\n";

    private static final String SET_BOOLEAN
      =tab+"/** Sets the $text$.\n"
      +tab+"    @param val The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ void $name$(boolean val) {\n"
      +tab+tab+"byte num = (val)? (byte)1 : 0;\n"
      +tab+tab+"getByteBuffer().$method$($pos$, num);\n"
      +tab+"}\n";

    private static final String GET_BOOLEAN
      =tab+"/** Gets the $text$.\n"
      +tab+"    @return The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ boolean $name$() {\n"
      +tab+tab+"return (getByteBuffer().$method$($pos$) != 0);\n"
      +tab+"}\n";

    private static final String GET_RECORD_VALUE
      =tab+"/** Gets the $text$.\n"
      +tab+"    @return The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ $type$ $name$() {\n"
      +tab+tab+"return new $type$(getByteBuffer(), $pos$);\n"
      +tab+"}\n";

    private static final String GET_RECORD_ARRAY
      =tab+"/** Gets the $text$.\n"
      +tab+"    @return The $text$.\n"
      +tab+" */\n"
      +tab+"$mods$ $type$[] $name$() {\n"
      +tab+tab+"$type$[] val = new $type$[$size$];\n"
      +tab+tab+"for (int i = 0; i < val.length; i++) {\n"
      +tab+tab+tab+"int offset = i * $bytes$;\n"
      +tab+tab+tab+"val[i] = new $type$(getByteBuffer(), ($pos$)+offset);\n"
      +tab+tab+"}\n"
      +tab+tab+"return val;\n"
      +tab+"}\n";

    private static final String CLASS_DEFINITION_START
      =tab+"/** Defines the $name$ structure. */\n"
      +tab+"public static class $name$ {\n"
      +tab+tab+"/** The byte buffer. */\n"
      +tab+tab+"private final ByteBuffer byteBuffer;\n"
      +tab+tab+"\n"
      +tab+tab+"/** The offset index. */\n"
      +tab+tab+"private final int offset;\n"
      +tab+tab+"\n"
      +tab+tab+"/** Creates a new instance.\n"
      +tab+tab+"    @param byteBuffer The byte buffer.\n"
      +tab+tab+"    @param offset     The offset index.\n"
      +tab+tab+" */\n"
      +tab+tab+"private $name$(ByteBuffer byteBuffer, int offset) {\n"
      +tab+tab+tab+"this.byteBuffer = byteBuffer;\n"
      +tab+tab+tab+"this.offset     = offset;\n"
      +tab+tab+"}\n"
      +tab+tab+"/** Gets the byte buffer.\n"
      +tab+tab+"    @return The byte buffer.\n"
      +tab+tab+" */\n"
      +tab+tab+"private ByteBuffer getByteBuffer() {\n"
      +tab+tab+tab+"return byteBuffer;\n"
      +tab+tab+"}\n";

    private static final String CLASS_DEFINITION_END
      =tab+"}\n";
  }


  /** Creates a Java class (and applicable JNI code) to access a structure defined in a Fortran
      include file.
   */
  private static class BBConv {
    /** Use escapes in the Javadocs to avoid breakiterator warnings in Java 1.4.2. */
    static final boolean USE_JAVADOC_BREAK = true;

    private Vector<Struct>    structs     = new Vector<Struct>();    // The structures defined (same as structsMap).
    private Hashtable<String,Struct> structsMap  = new Hashtable<String,Struct>(); // The structures defined (same as structs).
    private Stack<InputFile>  includeList = new Stack<InputFile>();  // The files being included.

    private final String  classSubs;  // The name to substitute for "Class".
    private final String  includes;   // Comma-separated list of #include files.
    private final boolean isStatic;   // Indicates if the methods generated are static.
    private final boolean pkgPrivate; // Indicates if the class is package-private

    /** Creates a new instance.
        @param subs     The substitute name for fields named "Class" since "getClass" is used by
                        {@link Object}.
        @param includes Comma-separated list of #include files.
        @param isStatic Indicates if the methods are to be statically defined.
        @param pkgPrivate If this is true then the class will be package-private, otherwise it will
                          be public.
     */
    private BBConv(String subs, String includes, boolean isStatic, boolean pkgPrivate) {
      this.classSubs  = (subs == null)? "Class_": subs;
      this.includes   = includes;
      this.isStatic   = isStatic;
      this.pkgPrivate = pkgPrivate;
    }

    /** Processes a file. This should only be called once per instance.
        @param in         The input stream (for the Fortran .inc file).
        @param outFile    The output file (the Java .java file).
        @param jniFile    The output file (the JNI .c file).
        @param structure  The structure name to translate (case sensitive for the .c file).
        @param varName    The variable name to get a buffer for (case sensitive for the .c file).
     */
    private void process(File in, File outFile, File jniFile,
                         String structure, String varName) throws IOException {
      includeList.push(new InputFile(in));

      PrintStream out = new PrintStream(new FileOutputStream(outFile));
      PrintStream jni = (isStatic)? new PrintStream(new FileOutputStream(jniFile)): null;

      Stack<String>     unionDepth = new Stack<String>();
      Vector<JavaDef>    defs       = null;
      Vector<JavaDef>   defines    = new Vector<JavaDef>();
      Hashtable<String,JavaDef> definesMap = new Hashtable<String,JavaDef>();
      String    line       = getIn().readLine();
      String    nextLine   = getIn().readLine();
      String    structName = null;
      String    byteOffset = "0";
      String    maxOffset  = null;
      String[]  nextLines  = null;

      while (!includeList.isEmpty()) {
        line = line.replaceAll("[\t]","    ");

        if (!line.startsWith(" ")) {
          // commented out line
          nextLines = readNextLines(nextLine);
          line     = nextLines[0];
          nextLine = nextLines[1];

        } else {
          if ((nextLine != null) && nextLine.startsWith("     &")) {
            // line continuation
            line = line + nextLine.substring(6);
            nextLine = getIn().readLine();

          } else {
            String comment      = null;
            int    commentStart = line.indexOf('!');

            if (commentStart >= 0) {
              comment = line.substring(commentStart+1).replaceAll("\t", " ").trim();
              line    = line.substring(0,line.indexOf('!'));
            } else {
              comment = "";
            }

            line = line.replaceAll("[ ]","").toUpperCase(); // remove all whitespace and uppercase

            if (line.length() == 0) {
              //blank line

            } else if (line.equals("IMPLICITNONE")) {
              // ignore

            } else if (line.startsWith("EXTERNAL")) {
              // ignore

            } else if ((structName == null) && line.startsWith("STRUCTURE/") && line.endsWith("/")) {
              structName = line.substring(10, line.length()-1);
              defs       = new Vector<JavaDef>();

            } else if ((structName != null) && line.equals("ENDSTRUCTURE")) {
              Struct st = new Struct(toTitleCase(structName), max(maxOffset, byteOffset),
                                     defs.toArray(new JavaDef[0]), comment);

              structs.add(st);
              structsMap.put(st.getName(),st);

              structName = null;
              defs       = null;
              byteOffset = "0";
              maxOffset  = null;

            } else if ((structName != null) && line.equals("UNION")) {
              unionDepth.push(byteOffset);

            } else if ((structName != null) && line.equals("ENDUNION") && (unionDepth.size() > 0)) {
              unionDepth.pop();
              byteOffset = maxOffset;

            } else if ((structName != null) && line.equals("MAP") && (unionDepth.size() > 0)) {
              // Nothing to do

            } else if ((structName != null) && line.equals("ENDMAP") && (unionDepth.size() > 0)) {
              maxOffset  = max(maxOffset, byteOffset);
              byteOffset = unionDepth.peek().toString();

            } else if (structName == null) {
              if (line.startsWith("PARAMETER(") && line.endsWith(")") && (line.indexOf('=') > 0)) {
                String name  = line.substring(10, line.indexOf('='));
                String value = line.substring(line.indexOf('=')+1, line.length()-1);

                JavaDef jdef = definesMap.get(name);

                if (jdef != null) {
                  jdef.setValue(value);

                } else {
                  out.close();
                  if (jni != null) jni.close();
                  throw new RuntimeException("Can not find '"+name+"' as specified in '"+line+"'.");
                }

              } else if (line.startsWith("POINTER(") && line.endsWith(")")) {
                Shell.info("Skipping POINTER  definition not used in Java ("+line+").");

              } else if (line.startsWith("COMMON")) {
                Shell.info("Skipping COMMON   definition not used in Java ("+line+").");

              } else {
                JavaDef[] jdefs = toJavaDef(line,null,comment);
                for (JavaDef jdef : jdefs) {
                  defines.add(jdef);
                  definesMap.put(jdef.getName(), jdef);
                }
              }

            } else {
              JavaDef[] jdefs = toJavaDef(line,byteOffset,comment);

              for (JavaDef jdef : jdefs) {
                byteOffset = add(byteOffset, jdef.getByteLength());

                defs.add(jdef);

                String type = jdef.getType();

                if (jdef.getSize() != null) {
                  type = type + "[" + jdef.getSize() + "]";
                }
              }
            }

            nextLines = readNextLines(nextLine);
            line     = nextLines[0];
            nextLine = nextLines[1];
          }
        }

        if (line == null) {
          getIn().close();
          includeList.pop();
          line     = "";
          nextLine = "";
        }
      }

      // Sets the usage flags so that unused structures will be ignored.
      if ((structure == null) || (structure.length() == 0)) {
        structure = null;
      } else {
        Struct mainStruct = structsMap.get(toTitleCase(structure));
        if (mainStruct != null) {
          setUsageFlags(mainStruct);
        } else {
          out.close();
          if (jni != null) jni.close();
          throw new MidasException("Structure '"+toTitleCase(structure)+"' not found.");
        }

        // Write the Java file
        out.println("/***********************************/");
        out.println("/* THIS IS AN AUTO-GENERATED CLASS */");
        out.println("/*       !!! DO NOT EDIT !!!       */");
        out.println("/***********************************/");
        out.println("// <BBMakeDefs>");
        for (Struct struct : structs) {
          JavaDef[] params = struct.getJavaDefs();

          if (struct.isUsed()) {

            if (toTitleCase(structure).equals(struct.getName())) {
              out.println("// <MAIN>");
            } else {
              out.println("// <"+struct.getName()+" size=\""+struct.getBytes()+"\">");
            }

            for (int j = 0; j < params.length; j++) {
              String name  = toTitleCase(params[j].getName());
              String off   = params[j].getOffset();            // Actual offset

              if (j != 0) {
                String guess1 = add(params[j-1].getOffset(), params[j-1].getByteLength());
                String guess2 = add(params[j-1].getByteLength(), params[j-1].getOffset());

                if (off.equals(guess1) || off.equals(guess2)) {
                  off = "~";
                }
              }

              String str  = "//  " + StringUtil.padLeft(off, 4) + ", ";
              str = str + params[j].getType();
              if (params[j].getSize() != null) {
                str = str + "[" + params[j].getSize() + "]";
              }
              str = StringUtil.padRight(str, 30);
              str = str + ", get" + name;
              str = StringUtil.padRight(str, 55);
              str = str + ", set" + name;
              str = StringUtil.padRight(str, 80);
              str = str + ", \"" + params[j].getComment() + "\"";

              out.println(str);
            }

            if (toTitleCase(structure).equals(struct.getName())) {
              out.println("// </MAIN>");
            } else {
              out.println("// </"+struct.getName()+">");
            }
          }
        }
        out.println("// </BBMakeDefs>");
      }

      String pkgName   = outFile.getAbsolutePath();
      String className = outFile.getName();

      if (pkgName.startsWith(Shell.getNmRoot())) {
        pkgName = pkgName.substring(Shell.getNmRoot().length()).replace(File.separatorChar, '.');
      } else {
        pkgName = pkgName.replace(File.separatorChar, '.');
      }

      if (className.endsWith(".java")) {
        className = className.substring(0, className.length()-5);
      }
      if (pkgName.startsWith(".")) {
        pkgName = pkgName.substring(1);
      }
      if (pkgName.endsWith(className+".java")) {
        pkgName = pkgName.substring(0, pkgName.length() - (className+".java").length() - 1);
      }

      out.println("package "+pkgName+";");
      out.println();
      out.println("import java.nio.ByteBuffer;");
      out.println("import java.nio.ByteOrder;");
      out.println();
      out.println("import nxm.sys.lib.Shell;");
      out.println();
      out.println();
      out.println("/** A class that wraps the native "+structure+" structure for "+varName
                 +"; this is an automatically generated class. DO NOT EDIT!!!\n"
                 +"    @author Auto-Generated by: CONVERT IN="+in+" CONV=FSTRUCT2JAVA OUT="+outFile+" FORM="+structure+"\n"
                 +" */");
      if (pkgPrivate) {
        out.println("class "+className+" {");
      } else {
        out.println("public class "+className+" {");
      }
      if (structure == null) {
        // skip
      } else if (isStatic) {
        out.println("  /** Loads the JNI library. */");
        out.println("  static {");
        out.println("    Shell.loadLibrary("+className+".class.getName());");
        out.println("  }");
        out.println("  ");
        out.println("  /** The byte buffer to use. */");
        out.println("  private static ByteBuffer byteBuffer;");
        out.println("  ");
        out.println("  /** Creates a new instance. */");
        out.println("  private "+className+"() { }");
        out.println("  ");
        out.println("  /** Gets the byte buffer.");
        out.println("      @return The byte buffer.");
        out.println("   */");
        out.println("  public static ByteBuffer getByteBuffer() {");
        out.println("    if (byteBuffer == null) {");
        out.println("      byteBuffer = getByteBuffer0();");
        out.println("      byteBuffer.order(ByteOrder.nativeOrder());");
        out.println("    }");
        out.println("    return byteBuffer;");
        out.println("  }");
        out.println("  ");
        out.println("  /** Gets the byte buffer. This should only be called by getByteBuffer().<br>");
        out.println("      <br>");
        out.println("      WARNING: The byte buffer returned by this method will need to have its");
        out.println("               byte order set using byteBuffer.order(ByteOrder.nativeOrder())");
        out.println("               before it can be accessed.");
        out.println("               ");
        out.println("      @return The byte buffer.");
        out.println("   */");
        out.println("  private static native ByteBuffer getByteBuffer0();");
        out.println("  ");
      } else {
        out.println("  /** The byte buffer to use. */");
        out.println("  private final ByteBuffer byteBuffer;");
        out.println("  ");
        out.println("  /** Creates a new instance.");
        out.println("      @param byteBuffer The byte buffer to use.");
        out.println("   */");
        out.println("  public "+className+"(ByteBuffer byteBuffer) {");
        out.println("    this.byteBuffer = byteBuffer;");
        out.println("  }");
        out.println("  ");
        out.println("  /** Gets the byte buffer.");
        out.println("      @return The byte buffer.");
        out.println("   */");
        out.println("  public ByteBuffer getByteBuffer() {");
        out.println("    return byteBuffer;");
        out.println("  }");
      }

      for (JavaDef define : defines) {
        // Translate each definition:
        //   - If there is a comment write it out, otherwise use "unknown" as the comment.
        //   - If a value is given use "public static final" otherwise use "protected".
        //   - If the type matches the MAIN structure replace it as appropriate.
        String  flags  = "public static final";
        String  type   = define.getType();
        String  value  = "";
        String  cmt    = "unknown";
        boolean skip   = false;

        if ((define.getComment() != null) && (define.getComment().trim().length() > 0)) {
          cmt = define.getComment();
        }

        if (define.getValue() != null) {
          value = " = " + define.getValue();
        } else {
          if (structure == null) { skip = true; }
          flags = "protected";
        }

        if ((structure != null) && toTitleCase(structure).equals(type)) {
          type = className;

          if (value.equals("")) {
            flags = "public static final";
            value = " = new "+className+"()";
          }
        } else if (type.startsWith("Boolean(") && type.endsWith(")")) {
          type = type.substring(8, type.length()-1);
        } else if ((type.indexOf("String(") >= 0) && type.endsWith(")")) {
          // Change type to String and convert 'str' to "str"
          type = "String";
          if ((value != null) && value.startsWith(" = '") && value.endsWith("'")) {
            value = " = \"" + value.substring(4,value.length()-1) + "\"";
          }
        } else {
          // Fortran allows 1.0d7 for doubles while Java uses 1.0e7
          // Fortran allows 1.0e7 for floats  while Java uses 1.0e7f
          value = value.replaceAll("([0-9]+)([dDeE][+])([0-9]+)", "$1e$3");
          value = value.replaceAll("([0-9]+)([dDeE][-])([0-9]+)", "$1e$3");

          if (type.equals("float" )) { value = value+"f"; }
          if (type.equals("double")) { value = value+"d"; }
        }

        if ((structsMap.get(type) != null) && (!structsMap.get(type).isUsed())) {
          skip = true;
        }

        if (!skip) {
          if (BBConv.USE_JAVADOC_BREAK) { cmt = cmt.replaceAll("[.]", "&#46;"  ); }
          out.println("  /** "+cmt+" */");
          out.println("  "+flags+" "+type+" "+define.getName() + value + ";");
          out.println("  ");
        } else {
          Shell.info("Skipping VARIABLE definition not used in Java ("+define.getName()+").");
        }
      }
      out.println("}");
      out.println();


      // Write the JNI file
      String jniClassName = pkgName.replace('.', '_')+"_"+className;

      if (isStatic) {
        jni.println("");
        if ((includes != null) && (includes.trim().length() > 0)) {
          String[] includeFiles = includes.trim().split("[,]");

          for (String includeFile : includeFiles) {
            includeFile = includeFile.trim();

            if (includeFile.length() > 0) {
              if (includeFile.startsWith("<") && includeFile.endsWith(">")) {
                jni.println("#include " + includeFile);
              } else {
                jni.println("#include \"" + includeFile + "\"");
              }
            }
          }
        }
        jni.println("#include <"+jniClassName+".h>");
        jni.println("");
        jni.println("");
        jni.println("#ifdef __cplusplus");
        jni.println("extern \"C\"");
        jni.println("{");
        jni.println("#endif");
        jni.println("");
// NTN 2009-12-23 BUG 2001: don't need the below line since it should already be defined in includes (via /include= switch to CONVERT)
//        jni.println("extern "+structure+" *"+varName+";");
        jni.println("");
        jni.println("JNIEXPORT jobject JNICALL Java_"+jniClassName+ "_getByteBuffer0"
                  + "(JNIEnv * env, jclass obj) {");
        jni.println("  return (*env)->NewDirectByteBuffer(env, "+varName+", "
                  + "sizeof("+structure+"));");
        jni.println("}");
        jni.println("");
        jni.println("#ifdef __cplusplus");
        jni.println("}");
        jni.println("#endif");
        jni.close();
      }

      getIn().close();
      out.close();
    }

    /** Converts a Fortran definition to a Java definition.
        @param line    The line of code from the Fortran include file (upcase, with comments and
                       whitespace removed).
        @param offset  The byte offset.
        @param comment The comment describing the line (may be null).
        @return The applicable Java definition.
     */
    private JavaDef[] toJavaDef(String line, String offset, String comment) {
      JavaDef   jdef  = null;
      JavaDef[] jdefs = null;

      int indexStar   = line.indexOf('*');
      int indexParens = line.indexOf('(');

      String  primType = null;
      String  typeLen  = null;
      String  arrayLen = null;
      String  name     = null;


      if (line.startsWith("RECORD/")) {
        String ln  = line.substring(7);
        int    end = ln.indexOf('/');

        primType = ln.substring(0, end);
        name     = ln.substring(end+1);

        if (name.indexOf('(') > 1) {
          arrayLen = getArrayLength(name.substring(name.indexOf('(')+1,name.indexOf(')')));
          name     = name.substring(0,name.indexOf('('));
        }

        jdef = new JavaDef(toTitleCase(primType), arrayLen, name, offset, comment, null, true);


      } else if ((indexStar > 0) && ((indexParens < 0) || (indexParens > indexStar))) {
        primType = line.substring(0,indexStar);

        if (line.charAt(indexStar+1) == '(') {
          int end = line.indexOf(')');

          typeLen  = line.substring(indexStar+2,end);
          name     = line.substring(end+1);

        } else {
          int end = indexStar+1;

          while((end <= line.length()) && (Character.isDigit(line.charAt(end)))) {
            end++;
          }

          typeLen  = line.substring(indexStar+1,end);
          name     = line.substring(end);
        }


        if (name.indexOf('(') > 1) {
          arrayLen = getArrayLength(name.substring(name.indexOf('(')+1,name.indexOf(')')));
          name     = name.substring(0,name.indexOf('('));
        }

        String totalLen = null;

        if ((arrayLen != null) && (arrayLen.indexOf(',') > 0)) { // Turn 2D or 3D array into 1D array.
          String mult = null;  // The multiplier

          while(arrayLen.indexOf(',') > 0) {
            String subLength = arrayLen.substring(0, arrayLen.indexOf(','));
            arrayLen = arrayLen.substring(arrayLen.indexOf(',')+1);

            mult = mul(mult, subLength);
          }

          arrayLen = mul(mult, arrayLen);
        }

        totalLen = mul(typeLen, arrayLen);


        if (primType.equals("INTEGER")) {
               if (typeLen.equals("1")) { jdef = new JavaDef("byte", arrayLen,name,offset,comment); }
          else if (typeLen.equals("2")) { jdef = new JavaDef("short",arrayLen,name,offset,comment); }
          else if (typeLen.equals("4")) { jdef = new JavaDef("int",  arrayLen,name,offset,comment); }
          else if (typeLen.equals("8")) { jdef = new JavaDef("long", arrayLen,name,offset,comment); }
          else { throw new RuntimeException("Unknown type '"+primType+"' in '"+line+"'."); }

        } else if (primType.equals("FLOAT") || primType.equals("REAL")) {
               if (typeLen.equals("4")) { jdef = new JavaDef("float", arrayLen,name,offset,comment); }
          else if (typeLen.equals("8")) { jdef = new JavaDef("double",arrayLen,name,offset,comment); }
          else { throw new RuntimeException("Unknown type '"+primType+"' in '"+line+"'."); }

        } else if (primType.equals("COMPLEX")) {
               if (typeLen.equals("8")) { jdef = new JavaDef("float[2]", arrayLen,name,offset,comment); }
          else if (typeLen.equals("16")){ jdef = new JavaDef("double[2]",arrayLen,name,offset,comment); }
          else { throw new RuntimeException("Unknown type '"+primType+"' in '"+line+"'."); }

        } else if (primType.equals("LOGICAL")) {
               if (typeLen.equals("1")) { jdef = new JavaDef("Boolean(byte)", arrayLen,name,offset,comment); }
          else if (typeLen.equals("2")) { jdef = new JavaDef("Boolean(short)",arrayLen,name,offset,comment); }
          else if (typeLen.equals("4")) { jdef = new JavaDef("Boolean(int)",  arrayLen,name,offset,comment); }
          else if (typeLen.equals("8")) { jdef = new JavaDef("Boolean(long)", arrayLen,name,offset,comment); }
          else { throw new RuntimeException("Unknown type '"+primType+"' in '"+line+"'."); }

        } else if (primType.equals("BYTE")) {

          if (typeLen.equals("1")) { jdef = new JavaDef("byte",  arrayLen, name, offset, comment); }
          else                     { jdef = new JavaDef("byte",  totalLen, name, offset, comment); }

        } else if (primType.equals("CHARACTER")) {

          if (totalLen.equals("1")) { jdef = new JavaDef("AsciiChar",  null, name, offset, comment); }
          else { jdef = new JavaDef("FortranString("+totalLen+")", null, name, offset, comment); }

        } else {
          throw new RuntimeException("Unknown type '"+primType+"' in '"+line+"'.");
        }

      } else if (line.startsWith("BYTE")) {
        jdefs = toJavaDef("BYTE*1" + line.substring(4), offset, comment);

      } else if (line.startsWith("CHARACTER")) {
        jdefs = toJavaDef("CHARACTER*1" + line.substring(9), offset, comment);

      } else {
        throw new RuntimeException("Unknown type '"+primType+"' in '"+line+"'.");
      }

      if (jdefs == null) {
        if (jdef.getName().indexOf(',') > 1) {
          String[] varNames = jdef.getName().split(",");
          jdefs = new JavaDef[varNames.length];

          for (int i = 0; i < varNames.length; i++) {
            jdefs[i] = new JavaDef(jdef.getType(), jdef.getSize(), varNames[i],
                                   jdef.getOffset(), jdef.getComment());
          }

        } else {
          jdefs = new JavaDef[] { jdef };
        }
      }

      return jdefs;
    }


    /** Marks the given struct and all structs used by it as used. Note that this method is
        recursively applied.
        @param struct The struct to mark as used.
     */
    private void setUsageFlags(Struct struct) {
      struct.setUsed(true);

      JavaDef[] params = struct.getJavaDefs();
      for (JavaDef javaDef : params) {
        Struct type = structsMap.get(javaDef.getType());

        if ((type != null) && (!type.isUsed())) {
          setUsageFlags(type);
        }
      }
    }

    /** Computes the total array length. This will convert 2D arrays to 1D and will change specified
        ranges to be 0-indexed (the Java/C/C++ default).
        @param length The raw length of the array.
        @return The resolved array length.
     */
    private static String getArrayLength(String length) {
      String arrayLen = null;
      int    comma    = length.indexOf(',');
      int    colon    = length.indexOf(':');

      if (comma > 0) {
        arrayLen = mul(getArrayLength(length.substring(0,comma)),
                       getArrayLength(length.substring(comma+1)));
      } else if (colon > 0) {
        String first = length.substring(0, length.indexOf(':'));
        String last  = length.substring(length.indexOf(':') + 1);

             if (first.equals("1")) { arrayLen = last;                         }
        else if (first.equals("0")) { arrayLen = add(last, "1");               }
        else                       { arrayLen = add(last, "1") + "-" + first; }

      } else {
        arrayLen = length;
      }

      return arrayLen;
    }

    //////////////////////////////////////////////////////////////////////////////////////////////////
    // NESTED CLASSES
    //////////////////////////////////////////////////////////////////////////////////////////////////
    /** A Java definition. */
    private class JavaDef {
      private String  type;         // The type.
      private String  size;         // The array length (null for non-array).
      private String  name;         // The variable name.
      private String  offset;       // The variable's byte (null if not in a struct).
      private String  comment;      // The variable comment.
      private String  val;          // The variable value.
      private boolean rec;          // The variable is of a record type.

      public JavaDef(String type, String size, String name, String offset, String comment) {
        this(type,size,name,offset,comment,null,false);
      }

      public JavaDef(String type,String size,String name,String offset,String comment,String val,
                     boolean isRecord) {
        this.type    = type;
        this.size    = size;
        this.name    = name;
        this.offset  = offset;
        this.comment = comment;
        this.val     = val;
        this.rec     = isRecord;
      }

      /** The variable value. */
      private void setValue(String val) { this.val = val; }
      /** The type. */
      private String getType() { return type; }
      /** The array length (null for non-array). */
      private String getSize() { return size; }
      /** The variable name. */
      private String getName() { return name; }
      /** The variable name. */
      private String getValue() { return val; }
      /** The variable offset. */
      private String getOffset() { return offset; }
      /** The variable comment. */
      private String getComment() { return comment; }
      /** The variable is of a record type. */
      private boolean isRecord() { return rec; }

      private String getByteLength() {
        String bytes = null;

        if (isRecord()) {
          Struct struct = structsMap.get(getType());

          if (struct != null) {
            bytes = mul(struct.getBytes(), getSize());
          } else {
            throw new RuntimeException("Can not reference '"+getType()+"' before it is defined.");
          }
        } else {
          bytes = mul(getLength(getType()), getSize());
        }

        return bytes;
      }
    } // end private class JavaDef


    /** A structure definition. */
    private static class Struct {
      private String    bytes;             // The byte length.
      private String    name;              // The name.
      private JavaDef[] defs;              // The definitions.
      private String    comment;           // The variable comment.
      private boolean   used      = false; // Indicates if the struct is ever used.

      public Struct(String name, String bytes, JavaDef[] defs, String comment) {
        this.name    = name;
        this.bytes   = bytes;
        this.defs    = defs;
        this.comment = comment;
      }

      /** The structure name. */
      private String getName() { return name; }
      /** The structure comment. */
      private String getComment() { return comment; }
      /** The structure byte length. */
      private String getBytes() { return bytes; }
      /** The structure's defs. */
      private JavaDef[] getJavaDefs() { return defs; }
      /** Indicates if the struct is ever used. */
      private boolean isUsed() { return used; }
      /** Indicates if the struct is ever used. */
      private void setUsed(boolean used) { this.used = used; }
    } // end private class Struct


    //////////////////////////////////////////////////////////////////////////////////////////////////
    // STATIC FUNCTIONS SHARED WITH BBMAKE
    //////////////////////////////////////////////////////////////////////////////////////////////////
    /** Adds two positive numbers together. Wherever possible constant integer values will be
        summed together. <br>
        <br>
        Examples:
        <pre>
          add("1",   null) = "1"
          add("1",   "2" ) = "3"
          add("x",   "4" ) = "x+4" (or "4+x")
          add("x+4", "2" ) = "x+6" (or mathematically equivalent value)
        </pre>
        @param a First argument.
        @param b Second argument.
        @return The sum.
     */
    static String add(String a, String b) {
      String res = tryQuickMath(a, b, "0");

      if (res == null) {
        a = removeParens(a);
        b = removeParens(b);

        int indexA = a.lastIndexOf('+');
        int indexB = b.lastIndexOf('+');

        if (StringUtil.isInteger(a) && StringUtil.isInteger(b)) {
          res = Integer.toString(Convert.s2l(a) + Convert.s2l(b));
        } else if ((indexA > 0) && StringUtil.isInteger(a.substring(indexA+1))) {
          res = a.substring(0,indexA) + "+" + add(a.substring(indexA+1), b);
        } else if ((indexB > 0) && StringUtil.isInteger(b.substring(indexB+1))) {
          res = b.substring(0,indexB) + "+" + add(a, b.substring(indexB+1));
        } else {
          if (StringUtil.isInteger(a)) { res = addParens(b) + "+" + addParens(a); }
          else                         { res = addParens(a) + "+" + addParens(b); }
        }
      }

      return res;
    }

    /** Multiplies two positive numbers together. Wherever possible constant integer values will be
        multiplied together. <br>
        <br>
        Examples:
        <pre>
          mul("1",   null) = "1"
          mul("1",   "2" ) = "2"
          mul("x",   "4" ) = "x*4"     (or "4*x")
          mul("x+4", "2" ) = "(x+4)*2" (or mathematically equivalent value)
        </pre>
        @param a First argument.
        @param b Second argument.
        @return The product.
     */
    static String mul(String a, String b) {
      String res = tryQuickMath(a, b, "1");

      if (res == null) {
        a = removeParens(a);
        b = removeParens(b);

        if (StringUtil.isInteger(a) && StringUtil.isInteger(b)) {
          res = Integer.toString(Convert.s2l(a) * Convert.s2l(b));
        } else {
          res = addParens(a) + "*" + addParens(b);
        }
      }

      return res;
    }

    /** Finds the maximum of two positive numbers. <br>
        <br>
        Examples:
        <pre>
          max("x",   null) = "x"
          max("3",   "5" ) = "5"
          max("x",   "0" ) = "x"
          max("x",   "4" ) = "Math.max(x, 4)"   (or mathematically equivalent value)
          max("x+4", "2" ) = "Math.max(x+4, 2)" (or mathematically equivalent value)
        </pre>
        @param a First argument.
        @param b Second argument.
        @return The product.
     */
    static String max(String a, String b) {
      String res = tryQuickMath(a, b, "0");

      if (res == null) {
        a = removeParens(a);
        b = removeParens(b);

        if (a.startsWith("Math.max") && a.endsWith(")") && b.startsWith("Math.max") && b.endsWith(")")) {
          String numA = a.substring(9, a.indexOf(',')).trim();
          String strA = a.substring(a.indexOf(',')+1, a.length()-1).trim();
          String numB = b.substring(9, b.indexOf(',')).trim();
          String strB = b.substring(b.indexOf(',')+1, b.length()-1).trim();

          if (StringUtil.isNumber(numA) && StringUtil.isNumber(numB)) {
            a = max(numA, numB);
            b = max(strA, strB);
          } else if (StringUtil.isNumber(numA)) {
            a = numA;
            b = max(strA, b);
          } else if (StringUtil.isNumber(numB)) {
            a = numB;
            b = max(strB, a);
          } else {
            /* No change */
          }

        } else if (a.startsWith("Math.max") && a.endsWith(")")) {
          String numA = a.substring(9, a.indexOf(',')).trim();
          String strA = a.substring(a.indexOf(',')+1, a.length()-1).trim();

          if (StringUtil.isNumber(numA) && StringUtil.isNumber(b)) {
            a = max(numA, b);
            b = strA;
          } else if (StringUtil.isNumber(numA)) {
            a = numA;
            b = max(strA, b);
          } else {
            /* No change */
          }
        } else if (b.startsWith("Math.max") && b.endsWith(")")) {
          String numB = b.substring(9, b.indexOf(',')).trim();
          String strB = b.substring(b.indexOf(',')+1, b.length()-1).trim();

          if (StringUtil.isNumber(numB) && StringUtil.isNumber(a)) {
            a = max(numB, a);
            b = strB;
          } else if (StringUtil.isNumber(numB)) {
            a = numB;
            b = max(strB, a);
          } else {
            /* No change */
          }
        }

        if (StringUtil.isInteger(a) && StringUtil.isInteger(b)) {
          res = Integer.toString(Math.max(Convert.s2l(a), Convert.s2l(b)));
        } else {
          if (!(a.startsWith("Math.max(") && a.endsWith(")"))) { a = addParens(a); }
          if (!(b.startsWith("Math.max(") && b.endsWith(")"))) { b = addParens(b); }

          if (StringUtil.isInteger(b)) { res = "Math.max("+ b + ", " + a + ")"; }
          else                         { res = "Math.max("+ a + ", " + b + ")"; }
        }
      }

      return res;
    }

    /** Adds parenthesis to a string if required. */
    private static String addParens(String str) {
      if (!str.matches("[0-9A-Za-z_]+")) { str = "(" + str + ")"; }
      return str;
    }

    /** Removes parenthesis from a string if required. */
    private static String removeParens(String str) {
      str = str.trim();
      while(str.startsWith("(") && str.endsWith(")")) { str = str.substring(1,str.length()-1).trim(); }
      return str;
    }

    /** Try for a quick solution.
        @param a     First argument.
        @param b     Second argument.
        @param ident The identity value ("0" for add, "1" for mult).
        @return The sum or null if there is no quick solution.
     */
    private static String tryQuickMath(String a, String b, String ident) {
      String res = null;
           if (a == null)       { res = b; }
      else if (b == null)       { res = a; }
      else if (a.equals(ident)) { res = b; }
      else if (b.equals(ident)) { res = a; }
      return res;
    }

    /** Gets the length of a primitive type.
        @param type The primitive type.
        @return The length of the given type (in number of bytes).
     */
    static String getPrimitiveLength(String type) {
      String length = null;

      if (type != null) {
        if (type.equals("double")) { length = "8"; }
        if (type.equals("float" )) { length = "4"; }
        if (type.equals("long"  )) { length = "8"; }
        if (type.equals("int"   )) { length = "4"; }
        if (type.equals("short" )) { length = "2"; }
        if (type.equals("byte"  )) { length = "1"; }
        if (type.equals("char"  )) { length = "2"; }
      }

      return length;
    }

    /** Gets the length of a primitive or "semi-primitive" (Boolean, AsciiChar, String) type.
        @param type The primitive type.
        @return The length of the given type (in number of bytes).
     */
    static String getLength(String type) {
      String length = getPrimitiveLength(type);

      if (length != null) {
        /* Already set, skip the rest. */

      } else if (type.startsWith("Boolean(") && type.endsWith(")")) {
        length = getPrimitiveLength(type.substring(8, type.length()-1).trim());

      } else {
             if (type.equals("AsciiChar"))          { length = "1";                                }
        else if (type.startsWith("AsciiString("))   { length = type.substring(12,type.length()-1); }
        else if (type.startsWith("FortranString(")) { length = type.substring(14,type.length()-1); }
        else if (type.startsWith("CString("))       { length = type.substring( 8,type.length()-1); }
      }

      return length;
    }

    /** Converts the given string to title case.
        @param str The given string.
        @return The title case version of the string.
     */
    private String toTitleCase(String str) {
      if ((str == null) || (str.length() == 0)){
        /* DO NOTHING */
      } else if (str.length() == 1) {
        str = str.toUpperCase();

      } else {
        str = Character.toUpperCase(str.charAt(0)) + str.substring(1).toLowerCase();

        int index = 0;

        if (str.endsWith("_")) {
          str = str.substring(0, str.length()-1);
        }

        while((index = str.indexOf('_')) > 0) {
          if (str.length() >= index+2) {
            str = str.substring(0,index)
                + Character.toUpperCase(str.charAt(index+1))
                + str.substring(index+2);
          } else {
            str = str.substring(0,index)
                + Character.toUpperCase(str.charAt(index+1));
          }
        }
      }

      if (str.endsWith("struct")) {
        str = str.subSequence(0, str.length()-6) + "Struct";
      }

      if (str.equals("Class")) {
        str = classSubs;
      }

      return str;
    }

    /** Gets the current input file. */
    private BufferedReader getIn() {
      return includeList.peek().getReader();
    }

    /** Gets the next lines from the file.
        @param nextLine The expected next line.
        @return The next lines.
     */
    private String[] readNextLines(String nextLine) throws IOException {
      String line = null;

      String tempNextLine = (nextLine!=null)? nextLine.replaceAll("[ \t]","").toUpperCase(): "";
      if (tempNextLine.indexOf('!')>=0) {
        tempNextLine = tempNextLine.substring(0,tempNextLine.indexOf('!'));
      }

      if (tempNextLine.startsWith("INCLUDE") && tempNextLine.endsWith("'")) {
        String fname = tempNextLine.substring(8, tempNextLine.length()-1);
        String url   = Shell.getSharedMidasContext().io.findResource(fname, -1).getURL();

        if (url.startsWith("file:")) {
          includeList.push(new InputFile(new File(url.substring(5))));
        } else {
          throw new MidasException("Can't find include file '"+fname+"'");
        }

        line = "";
        nextLine = getIn().readLine();

      } else {
        line = nextLine;
        nextLine = getIn().readLine();
      }
      return new String[] { line, nextLine };
    }

    /** An input file. */
    private static class InputFile {
      private final File             file;
      private final LineNumberReader lineNum;
      private final BufferedReader   in;

      public InputFile(File inFile) throws IOException {
        file    = inFile;
        lineNum = new LineNumberReader(new FileReader(inFile));
        in      = new BufferedReader(lineNum);
      }

      /** Gets the info. */
      public String toString() {
        return file.getName() + "(" + lineNum.getLineNumber() + ")";
      }

      /** Gets the reader. */
      private BufferedReader getReader() {
        return in;
      }
    }
  }

}
