package nxm.sys.prim;

import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;

import nxm.sys.inc.DataTypes;
import nxm.sys.inc.LegacyMethod;
import nxm.sys.inc.ListFile;
import nxm.sys.inc.MapKBT;
import nxm.sys.inc.Units;
import nxm.sys.lib.Args;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Convert;
import nxm.sys.lib.CsvFile;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.DataFileHeader;
import nxm.sys.lib.FileName;
import nxm.sys.lib.FileUtil;
import nxm.sys.lib.FortranUtil;
import nxm.sys.lib.ImageFile;
import nxm.sys.lib.JsonUtilities;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.Keywords;
import nxm.sys.lib.MFormat;
import nxm.sys.lib.MaskValue;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StateVector;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.TextFile;
import nxm.sys.lib.Time;
import nxm.sys.lib.Transform;
import nxm.sys.lib.XmUtil;
import nxm.sys.lib.XmlFile;
import nxm.sys.lib.XmlFile.Xml2TblConversionOptions;
import nxm.sys.libg.GraphicsUtil;
import nxm.sys.libg.MColor;

/** Convert data files from/to various formats.
    <pre>
      BUGS:  If a table OR XML text contains fields which start with the same 4
             characters, the TBL2B and XML2B conversions will create a file with
             duplicate subrecord names
    </pre>
    @author Jeff Schoen, Beth Hassler, Jay Hallam, J. Czechowski IV
*/
public class convert extends Primitive {

  /** Default delimiter for tables with duplicate keys used for converting to and from XML.
      XML keys can have the same name and NeXtMidas tables can not.   */
  private static final String DEF_DUPKEY_DELIM_TBL2XML = "__";
  private static final String DEF_DUPKEY_DELIM_TAB2XML = "_";

  /** default constructor */
  public convert () {}

  @Override
  public int open () {
    String conv = MA.getS("CONV");
         if (conv.equals("T2B"))              convertText2Blue1000or3000();
    else if (conv.startsWith("XML2TBL"))      convertXMLText2Table(conv); // Legacy
    else if (conv.startsWith("XML2TAB"))      xmlToTable(conv);           // Since 2.1.2
    else if (conv.startsWith("TAB2XML"))      tableToXML(conv);           // Since 2.5.0
    else if (conv.startsWith("TBL2XML"))      convertTable2XMLText(conv);
    else if (conv.startsWith("XML2MAPKBT"))   xmlToMapKBT(conv);
    else if (conv.startsWith("SV2KML"))       convertStateVector2KML(conv); // Since 3.1.1
    else if (conv.startsWith("POLY2KML"))     convertPoly2KML(conv);        // since 3.3.2
    else if (conv.startsWith("TEMPLATE2TBL")) convertTemplate2Table(conv);
    else if (conv.startsWith("MSGCFG2TBL"))   genmsg(XmUtil.GENMSG_TBL);
    else if (conv.startsWith("MSGCFG2INC"))   genmsg(XmUtil.GENMSG_INC);
    else if (conv.startsWith("BBDEF2JAVA"))   bbdef2java();
    else if (conv.startsWith("FSTRUCT2JAVA")) fstruct2java();
    else if (conv.startsWith("PLAT2BLUE"))    convertPlatToBlue();    // since 3.7.0 for BUG 2918
    else if (conv.startsWith("KWCONV"))       convertKeywords();      // since 3.7.0 for BUG 2918
    else if (conv.startsWith("BLUE2JSON"))    convertBlueToJson();    // since 4.1.1 for NXM-3871
    else if (conv.startsWith("BLUE2XML"))     convertToXml(conv);     // since 3.5.4 for BUG 2903
    else if (conv.startsWith("FILE2XML"))     convertToXml(conv);     // since 3.5.4 for BUG 2903
    else if (conv.startsWith("XML2BLUE"))     convertXmlToBlue(conv); // since 3.5.4 for BUG 2903 // rename to convertFromXml (?)
//  else if (conv.startsWith("XML2FILE"))     convertFromXml(conv); // TODO: for future; convertXmlToFile(conv)
    else if (conv.equals("TBL2HTML"))         convertTable2HTMLText();
    else if (conv.equals("TXT2TBL"))          convertText2Table();
    else if (conv.equals("TBL2TXT"))          convertTable2Text();
    else if (conv.equals("TBL2B"))            convertTable2BlueT3000();
    else if (conv.equals("BLUE2IMG"))         convertBlue2Img();   // since 2.7.3
    else if (conv.equals("XML2B"))            convertXMLText2Blue();
    else if (conv.equals("CSV2TBL"))          convertCSV("TABLE");
    else if (conv.equals("CSV2XML"))          convertCSV("XML");
    else if (conv.equals("BLUE"))             convertColor("BLUE"); // TBD: not implemented
    else if (conv.equals("GOLD"))             convertColor("GOLD"); // TBD: not implemented
    else if (conv.equals("OPAL"))             convertColor("OPAL"); // TBD: not implemented
    else if (conv.equals("IEEE"))             convertRep("IEEE");
    else if (conv.equals("EEEI"))             convertRep("EEEI");
    else if (conv.equals("VAX" ))             convertRep("VAX ");
    else if (conv.equals("PACKED"))           convertType(PACKED);
    else if (conv.equals("BYTE"))             convertType(BYTE);
    else if (conv.equals("INT"))              convertType(INT);
    else if (conv.equals("LONG"))             convertType(LONG);
    else if (conv.equals("FLOAT"))            convertType(FLOAT);
    else if (conv.equals("DOUBLE"))           convertType(DOUBLE);
    else if (conv.equals("ECI"))              convertRefFrame("ECI",      "CARTESIAN"); // Since 3.3.0
    else if (conv.equals("ECR"))              convertRefFrame("ECR",      "CARTESIAN"); // Since 3.3.0
    else if (conv.equals("TOP"))              convertRefFrame("TOP",      "CARTESIAN"); // Since 3.3.0
    else if (conv.equals("TOPOCENT"))         convertRefFrame("TOPOCENT", "CARTESIAN"); // Since 3.3.0
    else if (conv.equals("CARTESIAN"))        convertRefFrame(null,       "CARTESIAN"); // Since 3.3.0
    else if (conv.equals("SPHERICAL"))        convertRefFrame(null,       "SPHERICAL"); // Since 3.3.0
    else if (conv.equals("CYLINDRIC"))        convertRefFrame(null,       "CYLINDRIC"); // Since 3.3.0
    else if (conv.equals("ELLIPSOID"))        convertRefFrame(null,       "ELLIPSOID"); // Since 3.3.0
    else if (conv.equals("GEODETIC"))         convertRefFrame(null,       "GEODETIC" ); // Since 3.3.0
    else if (conv.equals("SVINTERP"))         svinterp();                               // Since 3.3.0
    else if (conv.equals("LLA2ECR"))          lla2ecr();                                // Since 3.3.0
    else if (conv.equals("LLA2ECI"))          lla2eci();                                // Since 3.3.0
    else if (conv.equals("SV2BLUE"))          sv2blue5001();                            // Since 3.3.2
    else throw new MidasException("Unsupported conversion: "+conv);

    return FINISH;
  }

  /** Method takes user-provided times specific to a Type 5000 file and retrieves the correlating
      coordinates, along with provided table data, is then exported into a KML file.
      @param conv the conversion to perform
   */
  private void convertStateVector2KML (String conv) {
    DataFile df        = MA.getDataFile("IN");
    String   outfile   = MA.getCS("OUT", df.getName().getRoot() + ".kml");
    Table    options   = MA.getTable("OPTIONS"); // Options tables of all format possibilities
    String   styleid   = options.getS("STYLEID", "styleID");      // String to hold name of style id
    StringBuilder       kmlString   = new StringBuilder(1024);    // string buffer to hold kml output
    ArrayList<Double>   beginTArray = new ArrayList<>();    // array to hold start dates for each time added to file
    ArrayList<Double>   endTArray   = new ArrayList<>();    // array to hold end dates for each time added to file
    ArrayList<double[]> pntArray    = new ArrayList<>();  // Array to hold a double[] for each points lat,lon,alt
    String  format      = options.getS("FORMAT", "Point");    // Point, LineString, gxtrack ...
    MFormat timeform    = MFormat.getDateFormatFor("yyyy-MM-dd'T'hh:mm:ss'Z'", 0);
    long    beginTime   = options.getI("BEGIN");    // Begin time for a time span
    long    endTime     = options.getL("END", 0);   // End time for a time span
    long    persist     = options.getL("PERSIST", 0); // If true, points are not given an end time
    String  prepend     = options.getS("PREPEND");  // users string input to take place of standard style format
    String  altMode     = options.getS("ALTMODE", "clampToGround");  // altitude mode component
    long    extrude     = options.getL("EXTRUDE", 0); // true clamps line to ground
    long    tess        = options.getL("TESSELLATE", 0); // true to follow terrain
    long    heading     = options.getL("HEADING", 0); // azimuth of camera
    String  href        = options.getS("HREF");       // icon file or address
    String  iconcolor   = options.getS("ICONCOLOR", "FFFFFFFF");  // color of icon
    String  labelcolor  = options.getS("LABELCOLOR", "FFFFFFFF"); // color of label
    String  linecolor   = options.getS("LINECOLOR", "FFFF0000");  // color of line
    long    tilt        = options.getL("TILT", 0);                // x-axis rotation in degrees
    long    roll        = options.getL("ROLL", 0);                // z-axis rotation in degrees
    long    iconscale   = options.getL("ICONSCALE", 1);           // Size of icon
    long    labelscale  = options.getL("LABELSCALE", 1);          // Size of label
    long    linewidth   = options.getL("LINEWIDTH", 1);           // Width of line
    long    xscale      = options.getL("XSCALE", 1); // Scale along X axis
    long    yscale      = options.getL("YSCALE", 1); // Scale along Y axis
    long    zscale      = options.getL("ZSCALE", 1); // Scale along Z axis

    // Data file is opened, times are extracted from args table, coordinates are
    // loaded into a double array and the array is added to a list for all times
    df.open();
    double size;      // Size of data to loop through
    double time  = 0; // Variable to hold time values
    int    etime = 0; // end times if there are any
    double inc   = 0; // Delta to increment times with
    double delta = options.getD("DELTA", df.getDelta()); // Delta given

    // Time data grabbed if it exists
    Data timeData = new Data();
    try {
      timeData = options.getData("TIME");
    }
    catch (Exception e1) {
      M.printStackTrace(e1);
    }
    // This if/else used to determine whether entire file, certain times or time span is used
    if (endTime != 0) {
      if (beginTime >= endTime) {M.warning("BEGIN time cannot be larger than END time.");}
      while (beginTime%300 != 0) {beginTime += 1;}

      size = (Math.floor((endTime - beginTime)/delta) + 1);
      time = beginTime;
    }
    else if (timeData != null) {
      size = timeData.spa;
    }
    else {
      size = df.getNumberOfRows();
    }

    // Create data object and load with file data
    int      dfSize = (int)df.size;
    Data     data   = df.getDataBuffer(dfSize);
    int      ndo    = (int)df.avail();
    df.read(data, ndo);

    // Loop through all times to get coordinates for each
    for (int ii = 0; ii < size; ii++) {
      if (timeData != null) {time = timeData.getIndex(ii).toD();} // Time in a double format
      else {
        time  += inc;
        inc    = delta;
      }
      // for large numbers, assume they meant to set J1950 time
      if (time  > (86400 * 100)) {time  -= (df.getXDelta() * df.getSize());}

      double   offset1  = df.getIndex(time, DataFile.IDELTA, DataFile.ADDR_ABSCISSA_AUTO);
      double   absTime1 = df.getTimeAt(offset1);
      beginTArray.add(absTime1);

      // adds dates to endTArray if TimeSpan settings are detected
      if ((endTime != 0) && (!format.equalsIgnoreCase("POINT")) && (persist == 0)) {
        etime += inc;
        if (etime > (86400 * 100)) {etime -= (df.getXDelta() * df.getSize());}
        double   offset2  = df.getIndex(time, DataFile.IDELTA, DataFile.ADDR_ABSCISSA_AUTO);
        double   absTime2 = df.getTimeAt(offset2);
        endTArray.add(absTime2);
      }
      double[] vals = StateVector.getPosVelAccAtTime(df, time, data, true);  // get the position at that time (true for LLA because KML needs geodetic coords)
      double[] pos = {vals[0],vals[1],vals[2]};              // get the LON/LAT/ALT into an array
      pntArray.add(pos);
    }
    df.close();

    /** KML to be outputted is appended to a StringBuilder */
    kmlString.append("<kml>\n");
    kmlString.append("  <Document>\n");

    int i = 0; // increment variable to step thru time array
    String  absTime = timeform.format(beginTArray.get(i)); // begin times
    if (format.equalsIgnoreCase("MODEL")) {
      for (double[] d : pntArray) {
        if (endTArray.size() > 0) {}
        kmlString.append("    <Placemark>\n");
        kmlString.append("      <TimeSpan>\n");
        kmlString.append("        <begin>").append(absTime).append("</begin>\n");
        if (persist == 1) {
          if (endTArray.isEmpty()) {
            M.warning("PERSIST ERROR: END option not defined");
          } else {
            kmlString.append("        <end>").append(timeform.format(endTArray.get(i))).append("</end>\n");
          }
        }
        kmlString.append("      </TimeSpan>\n");
        kmlString.append("      <Model id=\"").append(styleid).append("\">\n");
        kmlString.append("        <altitudeMode>").append(altMode).append("</altitudeMode>\n");
        kmlString.append("        <Location>\n");
        kmlString.append("          <longitude>").append(d[0]).append("</longitude>\n");
        kmlString.append("          <latitude>" ).append(d[1]).append("</latitude>\n");
        kmlString.append("          <altitude>" ).append(d[2]).append("</altitude>\n");
        kmlString.append("        </Location>\n");
        kmlString.append("        <Orientation>\n");
        kmlString.append("          <heading>").append(heading).append("</heading>\n");
        kmlString.append("          <tilt>").append(tilt).append("</tilt>\n");
        kmlString.append("          <roll>").append(roll).append("</roll>\n");
        kmlString.append("        </Orientation>\n");
        kmlString.append("        <Scale>\n");
        kmlString.append("          <x>").append(xscale).append("</x>\n");
        kmlString.append("          <y>").append(yscale).append("</y>\n");
        kmlString.append("          <z>").append(zscale).append("</z>\n");
        kmlString.append("        </Scale>\n");
        kmlString.append("        <Link>\n");
        kmlString.append("          <href>").append(href).append("</href>\n");
        kmlString.append("        </Link>\n");
        kmlString.append("      </Model>\n");
        kmlString.append("    </Placemark>\n");
        i++;
      }
    } else {
      if (prepend != null) {kmlString.append(prepend);}
      else {
        kmlString.append("    <Style id=\"").append(styleid).append("\">\n");
        kmlString.append("      <IconStyle>\n");
        kmlString.append("        <color>"  ).append(iconcolor).append("</color>\n");
        kmlString.append("        <scale>"  ).append(iconscale).append("</scale>\n");
        kmlString.append("        <heading>").append(heading  ).append("</heading>\n");
        kmlString.append("        <Icon>\n");
        kmlString.append("          <href>").append(href).append("</href>\n");
        kmlString.append("        </Icon>\n");
        kmlString.append("      </IconStyle>\n");
        kmlString.append("      <LabelStyle>\n");
        kmlString.append("        <color>").append(labelcolor).append("</color>\n");
        kmlString.append("        <scale>").append(labelscale).append("</scale>\n");
        kmlString.append("      </LabelStyle>\n");
        kmlString.append("      <LineStyle>\n");
        kmlString.append("        <color>").append(linecolor).append("</color>\n");
        kmlString.append("        <width>").append(linewidth).append("</width>\n");
        kmlString.append("      </LineStyle>\n");
        kmlString.append("    </Style>\n");

        if (format.equalsIgnoreCase("POINT")) {
          for (double[] d : pntArray) {
            kmlString.append("    <Placemark>\n");
            kmlString.append("      <TimeStamp>\n");
            kmlString.append("        <when>").append(absTime).append("</when>\n");
            kmlString.append("      </TimeStamp>\n");
            kmlString.append("      <styleUrl>#").append(styleid).append("</styleUrl>\n");
            kmlString.append("      <Point>\n");
            kmlString.append("        <extrude>"     ).append(extrude).append("</extrude>\n");
            kmlString.append("        <altitudeMode>").append(altMode).append("</altitudeMode>\n");
            kmlString.append("        <coordinates>").append(d[0]).append(",")
                                                     .append(d[1]).append(",")
                                                     .append(d[2]).append("</coordinates>\n");
            kmlString.append("      </Point>\n");
            kmlString.append("    </Placemark>\n");
            i++;
          }
        } else {
          kmlString.append("    <Placemark>\n");
          kmlString.append("      <TimeSpan>\n");
          kmlString.append("        <begin>").append(timeform.format(beginTArray.get(i))).append("</begin>\n");
          kmlString.append("        <end>"  ).append(timeform.format(beginTArray.get(beginTArray.size() - 1))).append("</end>\n");
          kmlString.append("      </TimeSpan>\n");
          kmlString.append("      <styleUrl>#").append(styleid).append("</styleUrl>\n");
          kmlString.append("      <LineString>\n");
          kmlString.append("        <extrude>"     ).append(extrude).append("</extrude>\n");
          kmlString.append("        <tessellate>"  ).append(tess   ).append("</tessellate>\n");
          kmlString.append("        <altitudeMode>").append(altMode).append("</altitudeMode>\n");
          kmlString.append("        <coordinates>\n");
          for (double[] d : pntArray) {
            kmlString.append("          ").append(d[0]).append(",")
                                          .append(d[1]).append(",")
                                          .append(d[2]).append("\n");
          }
          kmlString.append("        </coordinates>\n");
          kmlString.append("      </LineString>\n");
          kmlString.append("    </Placemark>\n");
        }
      }
    }
    kmlString.append("  </Document>\n");
    kmlString.append("</kml>");

    if (outfile == null || outfile.length() == 0) {
      M.error("CONVERT: No output result specified for conversion SV2KML");
    }

    /** Below if/else statement allows output to terminal for debug purposes */
    try {
      if (conv.indexOf("/S") > 0) {
        String resname = MA.getU("OUT");
        if (resname.length() == 0) {M.type(kmlString);}
        else {MR.put(resname, kmlString.toString());}
      }
      else {
        TextFile tf = new TextFile(M, outfile);
        tf.open(TextFile.OUTPUT);
        tf.write(kmlString.toString());
        tf.close();
      }
    }
    catch (Exception e) {
      M.printStackTrace("CONVERT: Unable to convert table to KML (internal error: " + e + ").", e);
    }
  }

  /** Convert a type 1999 LayerPoly file to KML
      @since NeXtMidas 3.3.2
   */
  private void convertPoly2KML (String conv) {
    DataFile df        = MA.getDataFile("IN");
    String   outfile   = MA.getCS("OUT", df.getName().getRoot() + ".kml");
    Table    options   = MA.getTable("OPTIONS"); // Options tables of all format possibilities
    boolean  toString  = conv.indexOf("/S") >= 0;

    double[]            coords;                           // single set of coords
    Data                row;
    int                 objSize;
    StringBuilder       kmlString   = new StringBuilder(2048);    // string buffer to hold kml output
    int                 lineWidth   = 2;
    Color               color;       // color per lineString
    int                 colorNum;    // value of the color in the Poly file
    int                 colorIndex;  // index in the colorMap of the colorNum
    String              scolor;      // color in String form for KML entry
    String              styleid;
    int                 counter;

    if (options!=null && options.containsKey("LINEWIDTH")) lineWidth = options.getL("LINEWIDTH",lineWidth);

    Color[] cmap = MColor.getColorMap(MColor.MAPLINE, -1);  // default to XRAMP
    if (options!=null && options.containsKey("CMAP")) {
      cmap = MColor.getColorMap(options.getS("CMAP"),-1);
    }

    df.open();

    int                 dim     = df.getMode();                 // usually 2
    double[]            rowData = new double[dim];              // single row of data

    // KML output is appended to a StringBuilder
    kmlString.append("<kml>\n");
    kmlString.append("  <Document>\n");

    // parse the data in the file and put it in the KML <LineString> with the color indicated
    for (int index=0; index < df.getNumberOfRows(); index++) {
      row = df.getData(index);
      row.toArray(rowData);
      objSize = (int)rowData[0];
      colorNum = (int)rowData[1];
      if (colorNum==-998 || colorNum==-999) { index +=2; continue; }

      if (colorNum<1) color = MColor.RGBtoBGR(MColor.getSpecialXMidasColor(colorNum));
      else            {
        colorIndex = ((colorNum-1)%(cmap.length-1)) +1; // for 1..(color.length-1)
        color = MColor.RGBtoBGR(cmap[colorIndex]);
      }

      scolor = MColor.toString(color);
      if (scolor.indexOf("0X")>-1) scolor = scolor.substring(scolor.indexOf("0X")+2);
      index += 2;  // skip the first two rows of each section, since they are informational
      double stopPoint = index+objSize;    // how far to go into the data for this line string

      // create a KML Style from the color and line width
      styleid = "Style_"+index;
      kmlString.append("    <Style id=\"").append(styleid).append("\">\n");
      kmlString.append("      <LineStyle>\n");
      kmlString.append("        <color>").append(scolor).append("</color>\n");
      kmlString.append("        <width>").append(lineWidth).append("</width>\n");
      kmlString.append("      </LineStyle>\n");
      kmlString.append("    </Style>\n");

      coords = new double[objSize*dim];
      counter = 0;

      kmlString.append("    <Placemark>\n");
      kmlString.append("      <styleUrl>#").append(styleid).append("</styleUrl>\n");
      kmlString.append("      <LineString>\n");
      kmlString.append("        <coordinates>\n");

      while (index < stopPoint) {
        row = df.getData(++index);
        row.toArray(rowData);
        coords[counter++] = rowData[0];
        coords[counter++] = rowData[1];
        if (dim==3) coords[counter++] = rowData[2];
        kmlString.append("          ").append(rowData[0]).append(",")
                                      .append(rowData[1]);
        if (dim==3) kmlString.append(",").append(rowData[2]).append("\n");
        else        kmlString.append("\n");        // standard case (dim==2)
      }
      kmlString.append("        </coordinates>\n");
      kmlString.append("      </LineString>\n");
      kmlString.append("    </Placemark>\n");
    }
    kmlString.append("  </Document>\n");
    kmlString.append("</kml>");

    df.close();

    // toString is useful for debug purposes
    if (toString) {
      String resname = MA.getU("OUT");
      if (resname.isEmpty()) M.type(kmlString);
      else                   MR.put(resname, kmlString.toString());
    } else {
      TextFile tf = new TextFile(M, outfile);
      tf.open(TextFile.OUTPUT);
      tf.write(kmlString.toString());
      tf.close();
    }

  }

  /** Convert a text file to a BLUE file (Type 1000 or 3000) */
  private int convertText2Blue1000or3000 () { // formerly named convertT2B

    String line;
    int maxrec = 100;
    String formats = "ABILFDXZ";
    boolean parse = MA.getState("/PARSE");

    TextFile tf = MA.getTextFile("IN"); tf.setExtDefault("txt");
    DataFile df = MA.getDataFile("OUT","3000","NH",0);

    String format  = MA.getU("FORM");
    int skip       = MA.getL("SKIP");

    Parser fp = new Parser(format,'|');

    int nfp = fp.elements();
    int[] offsets = new int[maxrec];
    int[] lengths = new int[maxrec];
    char[] types = new char[maxrec];
    int[] offs = new int[maxrec];

    int off,mult,length;
    int offset = 0;
    char type = '?';
    int irec = 0;

    for (int n=1; n<=nfp; n++) {
      String ff = fp.get(n);   // field format
      off = -1;
      for (int i=0; i<formats.length() && off<0; i++) {
        type = formats.charAt(i);
        off = ff.indexOf(type);
      }
      if (off<0) M.error("Unsupported type in format string: "+format);
      // get the field multiplier
      mult=1; if (off>0) mult = Convert.s2l(ff.substring(0,off));
      // get the field length
      length=1; if (off<ff.length()-1) length = Convert.s2l(ff.substring(off+1));
      // handle the spacer format
      if (type==DataTypes.XLONG) { offset += mult*length; continue; }
      // build the type 3000 record structure
      for (int i=0; i<mult; i++) {
        String recName = df.getUniqueRecName("TR00",irec+1,null);
        String mode;
        int origBPS = Data.getBPS(type);
        // get number of 8by fields UNLESS the data type is more than 8 bytes
        // in that case, figure out the multiplier based on original data type (NXM-3570)
        int la      = (origBPS <= 8) ?  ((length+7)/8) : ((length+(origBPS-1))/origBPS);
        if (!Data.isString((byte)type)) mode = String.valueOf((char)Data.SCALAR); // for numeric types
        else if (la>10) mode = String.valueOf((char)Data.TRANSMATRIX);
        else if (la==10) mode = String.valueOf((char)Data.XMODE);
        else mode = "" + la;
        offs[irec] = df.getRecLength();
        df.addSubRec(recName,mode+type);
        offsets[irec] = offset;
        lengths[irec] = length;
        types[irec] = type;
        offset += length;
        irec++;
      }
    }
    String pad=""; for (int i=0; i<offset; i++) pad+=' ';

    if (irec==1) df.setType(1000);
    df.open(BaseFile.OUTPUT);
    Data data = df.getDataBuffer(1);

    tf.open(TextFile.INPUT|TextFile.FORCEABORT);
    while ( (line=tf.read()) != null) {
      Parser p = null;
      if (skip-- > 0) continue;  // skip header lines
      // pad the line to the required length to make later processing cleaner
      if (parse) p = new Parser (line);
      else if (line.length()<pad.length()) line += pad.substring(line.length());
      for (int i=0; i<irec; i++) {
        String sline = (parse)? p.get(i+1)
                              : line.substring(offsets[i],offsets[i]+lengths[i]);
        switch (types[i]) {
          case DOUBLE:   data.packD(offs[i],       Convert.s2d(sline)); break;
          case FLOAT:    data.packF(offs[i],(float)Convert.s2d(sline)); break;
          case LONG:     data.packL(offs[i],       Convert.s2l(sline)); break;
          case INT:      data.packI(offs[i],(short)Convert.s2l(sline)); break;
          case BYTE:     data.packB(offs[i],(byte) Convert.s2l(sline)); break;
          case ASCII:    data.packS(offs[i],lengths[i],sline); break;
          case ASCII256: data.packS(offs[i],lengths[i],sline); break; // 3.9.4 NXM-3570
        }
      }
      df.write(data);
    }
    tf.close();
    df.close();

    return NORMAL;
  }

  /** Convert the header flavor of a data file. TODO: this is not implemented. */
  private int convertColor (String color) {
    return NORMAL;
  }

  /** Convert the data/header representation of a BLUE file */
  private int convertRep (String conversion) {

    /* Get the input/output data files.  If no output is specified,
       use the input file as the output file. */
    DataFile dfi = MA.getDataFile("IN");
    String key="OUT";
    if (MA.getLength(key)<=0) {
      key="IN";
      dfi.open(BaseFile.INOUT);
    } else {
      dfi.open(BaseFile.INPUT);
    }
    if (!dfi.isOpen()) {
      M.error("Unable to open "+dfi.getFileName().getFullName());
      return (FINISH);
    }

    DataFile dfo = MA.getDataFile(key,dfi,0);
    byte convertTo, convertFrom;
    Data dfdata;

    convertTo = DataFile.testRep(conversion);
    convertFrom = DataFile.testRep(dfi.getDataRep());

    // Convert Data
    if (convertTo != convertFrom || !dfo.getURL().equals(dfi.getURL())) {
      dfo.setDataRep(conversion);
      dfo.open(BaseFile.OUTPUT);
      xfer = MA.getL("/TL", Math.max(1,(int)(bufSize/dfi.dbpe)) );
      dfdata = dfi.getDataBuffer(xfer);
      for (int n=xfer; n>0; ) {
        n = dfi.read(dfdata);
        if (n>0) dfo.write(dfdata,n);
      }
      dfi.close();
      dfo.convertHeaderRep(conversion);
      dfo.update();
      dfo.close();
      return NORMAL;
    }

    convertFrom = DataFile.testRep(dfi.getHeadRep());

    if (convertTo != convertFrom) {     // just Convert Header
      dfi.convertHeaderRep(conversion);
      dfi.update();
      dfi.close();
    }

    return NORMAL;
  }

  /** Convert {@code <IN>} file to given type in place or as {@code <OUT>} if specified */
  private int convertType (byte convertTo) {

    /* Get the input/output data files.  If no output is specified,
       use the input file as the output file. */
    DataFile df = MA.getDataFile("IN");
    String key="OUT";
    if (MA.getLength(key)<=0) {
      key="IN";
      df.open(DataFile.INOUT);
    } else {
      df.open(BaseFile.INPUT);
    }
    if (!df.isOpen()) {
      M.error("Unable to open "+df.getFileName().getFullName());
      return FINISH;
    }
    DataFile dfOut = MA.getDataFile(key,df,0);
    Data dfdata,dfdata2;


    if (df.getProtected()) {
      df.close();
      M.error("File is protected.");
      return FINISH;
    }
    if (convertTo != df.dataType) {
      // Logic update in 3.9.1 for finding the size based on frame size.
      // previously did not account for frame size.
      dfdata = df.getDataBuffer((int)df.size);
      int df2 = (df.getFrameSize() > 0) ? (int)(df.size*df.getFrameSize()) : (int)df.size;
      dfdata2 = new Data(df.getFormat().substring(0,1)+(char)convertTo,df2);
      df.read(dfdata);
      Convert.type(dfdata.buf,0,df.dataType,dfdata2.buf,0,convertTo,(int)df.size*df.ape*df.spa);
      dfOut.open();
      dfOut.seek(0.0);
      dfOut.setFormatType(convertTo);
      dfOut.setSize((df.size));
      dfOut.write(dfdata2,(int)df.size);
    }

    df.close();
    dfOut.close();

    return NORMAL;
  }

  private void convertText2Table() {
    TextFile tf = MA.getTextFile("IN");
    Table t = new Table(tf);
    String out = MA.getU("OUT");
    if (out == null || out.length() == 0) {
      M.error("CONVERT: No output specified for conversion of text to table of " +tf.getName());
    } else {
      MR.put(MA.getU("OUT"),t);
    }
  }

  private void convertTable2Text() {
    Table t = MA.getTable("IN");
    TextFile tf = MA.getTextFile("OUT");
    tf.open(TextFile.OUTPUT|TextFile.FORCEABORT);
    String s = t.toString();
    tf.write(s);
    tf.close();
  }

  /** Convert a table (from the command line) into a Type 3000 "BLUE" file. */
  private int convertTable2BlueT3000 () { // formerly named convertTable2Blue
    // Get command line information and set flags accordingly
    Table inTable           = MA.getTable("IN");
    DataFile df             = MA.getDataFile("OUT");
    String formatString     = MA.getU("FORM");
    char formatDelim        = MA.getS("/DELIM","|").charAt(0);
    boolean defaultRecNames = MA.getState("/DEFAULTNAMES",false);

    // Clean up the string by making it all caps and replacing any delimiter with a comma.
    //formatString.toUpperCase(); // <-- despite the above comments, this line never did anything!
    if (formatDelim != ',') formatString = formatString.replace(formatDelim, ',');

    // Perform the conversion
    convertTable2BlueT3000(inTable, formatString, defaultRecNames, df);

    return NORMAL;
  }

  /* Convert a table object into a Midas Type 3000 "Blue" file. */
  private int convertTable2BlueT3000 (Table inTable, String formatString, boolean defaultRecNames, DataFile df) {
    Table.Iterator ti = inTable.iterator();
    ti.getNext();
    Object obj = ti.value;
    if (obj instanceof Table) {
      Table recTable = Convert.o2t(obj);

      // Set the data formats needed to form the file header
      // This throws a class cast exception if it fails
      String formatList[] = getFormatList (formatString, recTable);

      // The record names are derived from the table key names or
      // defaulted to a one-up string
      String recordNameList[] = null;
      if (!defaultRecNames) {
        recordNameList = getRecNamesFromTable (recTable);
      }

      // Make the header
      make3000Header (formatList, recordNameList, df);

      if ( df.open(BaseFile.OUTPUT) ) {               // Open file for output
        writeTableTo3000 (inTable, formatList, df);   // Write the data
        df.close();                                   // Close file for output
      }
      else {                                          // Unable to open file
        M.error("Unable to open file "+df.getURL());
      }
    }
    else {
      M.error("There should be sub-Table(s) within input Table!");
    }
    return 0;
  }

  /** @since NeXtMidas 2.7.3 */
  private int convertBlue2Img () {
    DataFile df       = MA.getDataFile("IN");                // input file
    String   fileName = MA.getS("OUT",df.getName().getRoot()+".png"); // output File
    Table    t        = MA.getTable("IMGOPTS", new Table());
    String   fmt      = MA.getS("FORM","PNG");             // PNG, GIF, or JPG
    String   cmap     = t.getS("CMAP", "RAMP");            // Colormap
    String   colormap = t.getS("COLORS", null);            // list of colors
    int      ncolors  = t.getL("NC",256);                  // number of colors [def=256]
    double   minVal   = t.getD("ZMIN", -Double.MAX_VALUE); // min allowable value
    double   maxVal   = t.getD("ZMAX", Double.MAX_VALUE);  // max allowable value
    minVal            = t.getD("Z1", minVal);              // convenience
    maxVal            = t.getD("Z2", maxVal);              // convenience
    boolean  cthru    = t.getState("CTHRU", false);        // clear background
    int      cnt      = t.getL("CNT", 50);                 // contrast
    int      brt      = t.getL("BRT", 50);                 // brightness
    int      alpha    = t.getL("ALPHA", -1);               // transparency
    boolean grayscale = t.getState("GRAYSCALE",false);     // grayscale
    grayscale         = t.getState("GREYSCALE",grayscale); // convenience
    String  cxMode    = t.getS("CXMODE","MAG");            // complex mode should match GraphicsUtils Mode Listing (since 3.1.0)

    boolean terrain   = (cmap.equals("TERRAIN")) ;          // whether or not the data is terrain-based
    String  cols   = (colormap!=null)? colormap: cmap;      // Use list of colors if given, if not defined cmap
    Color[] colors = MColor.getColorMap(cols, ncolors);     // get colormap as array of colors

    convertBlue2Img(df,colors,fileName, minVal, maxVal, cthru, cnt, brt, alpha, grayscale, fmt, cxMode, terrain);

    return 0;
  }

  /** @since NeXtMidas 2.7.3 */
  private int convertBlue2Img (DataFile df, Color[] colors, String fileName, double minVal, double maxVal,
      boolean cthru, int cnt, int brt, int alpha, boolean grayscale, String fmt, String cxMode, boolean terrain) {

    ImageFile file = new ImageFile(M, fileName);
    GraphicsUtil.createImageFile(df,colors,minVal,maxVal,fmt,file,cnt,brt,alpha,grayscale,cxMode,terrain);

    return 0;
  }

  /**
   * Convert a Platinum file to a BLUE file by converting keywords to JICD 4.2 ICD Keywords
   * @since NeXtMidas 3.7.0 for Bug 2918
   */
  private void convertPlatToBlue () {
    FileName inFileName  = MA.getFileName("IN");
    FileName outFileName = MA.getFileName("OUT");
    Table plat2blueTable = Table.fromTableFile(this, "nxm.sys.dat.kwconv_plat2blue.tbl");
    Table overridingOptionsTbl = MA.getTable("OPTIONS"); // overrides in OPTIONS= parameter
    Table mergedTable;
    if (overridingOptionsTbl != null) {
      mergedTable = Table.merge(plat2blueTable, overridingOptionsTbl, true);
    }
    else {
      mergedTable = plat2blueTable;
    }

    FileUtil.convertKeywords(this, inFileName, outFileName, mergedTable);
  }

  /**
   * Convert keywords based in a conversion table
   * @since NeXtMidas 3.7.0 for Bug 2918
   */
  private void convertKeywords () {
    FileName inFileName  = MA.getFileName("IN");
    FileName outFileName = MA.getFileName("OUT");
    Table conversionTable = MA.getTable("OPTIONS"); // conversion table in OPTIONS= parameter
    if (conversionTable != null) {
      FileUtil.convertKeywords(this, inFileName, outFileName, conversionTable);
    }
    else {
      throw new MidasException("No conversion table found");
    }
  }

  private void convertBlueToJson () {
    FileName inFileName  = MA.getFileName("IN");
    FileName outFileName = MA.getFileName("OUT");
    boolean omitData     = MA.isPresent("/OMITDATA");
    String tableString   = MA.getCS("OPTIONS","{}");
    Table conversionTable;
    if (tableString.startsWith("{") ) {
      conversionTable = new Table(tableString,Table.CASESENSITIVE);
    }
    else {
      TextFile tf = new TextFile(tableString);
      conversionTable = new Table(tf,Table.CASESENSITIVE);
    }

    if (!conversionTable.isEmpty()) { // perform the specified mapping
      FileUtil.convertBlue2JSON(this.M, inFileName, outFileName, conversionTable);
    }
    else { // just put the existing mappings in JSON
      DataFile df = new DataFile(M, inFileName);
      df.open();
      DataFileHeader dfHeader = new DataFileHeader(df);
      Table table4JSON = dfHeader.getHeaderAndKeywordsAsTableCS();
      if (!omitData) {
        if (df.isRecordBased()) { // include the record data, if this is a record based file
          table4JSON.put("FileData",FileUtil.getRecordDataAsList(df));
        }
        else {
          int numRead;
          int xf = df.getXFrame();
          int tl = (xf > 1)? 1 : (int)Math.min(df.size, 32);
          Data data = df.getDataBuffer(tl);
          if (!data.isString()) {
            if (xf > 1) {
              if (df.spa == 1) {
                List<List<Number>> oneSPA = new ArrayList<>();
                while ((numRead = df.read(data, data.size)) >= 0) {
                  oneSPA.add(data.toList());
                }
                table4JSON.put("FileData",oneSPA);
              }
              else {
                List<List<List<Number>>> multSPA = new ArrayList<>();
                while ((numRead = df.read(data, data.size)) >= 0) {
                  multSPA.add(data.toListSPA());
                }
                table4JSON.put("FileData",multSPA);
              }
            }
            else {
              if (df.spa == 1) {
                List<Number> oneSPA = new ArrayList<>();
                while ((numRead = df.read(data, data.size)) >= 0) {
                  oneSPA.addAll(data.toList());
                }
                table4JSON.put("FileData",oneSPA);
              }
              else {
                List<List<Number>> multSPA = new ArrayList<>();
                while ((numRead = df.read(data, data.size)) >= 0) {
                  multSPA.addAll(data.toListSPA());
                }
                table4JSON.put("FileData",multSPA);
              }
            }
          }
          else {
            if (xf > 1) {
              List<List<String>> strList = new ArrayList<>();
              while ((numRead = df.read(data, data.size)) >= 0) {
                strList.add(data.toStringList());
              }
              table4JSON.put("FileData",strList);
            }
            else {
              List<String> strList = new ArrayList<>();
              while ((numRead = df.read(data, data.size)) >= 0) {
                strList.addAll(data.toStringList());
              }
              table4JSON.put("FileData",strList);
            }
          }
        }
      }
      df.close();
      String json = JsonUtilities.toJSON(table4JSON).toString();
      TextFile out = new TextFile(outFileName);
      out.open(BaseFile.OUTPUT);
      out.write(json);
      out.close();
    }
  }

  /** Converts a file (BLUE, CSV, e.g. any file that implements {@link ListFile} or {@link nxm.sys.lib.PlotFile}) to XML (file or results String).
   *  If conv == "BLUE2XML", then input file MUST be a DataFile.
   *  @param conv The conversion (either "BLUE2XML" or more generically "FILE2XML")
   *  @since NeXtMidas 3.5.4
   */
  private void convertToXml (String conv) {
    BaseFile inFile;
    Table    options = MA.getTable("OPTIONS");

         if (conv.startsWith("BLUE2XML")) inFile = MA.getDataFile("IN");
    else if (conv.startsWith("FILE2XML")) inFile = MA.getFile("IN");
    else throw new MidasException("Unsupported conversion: "+conv);

    inFile.open(BaseFile.INPUT);
    CharSequence xml = XmlFile.fileToXml(inFile, options);
    inFile.close();

    if (conv.contains("/S")) {
      String outRes = MA.getU("OUT");
      MR.put(outRes, xml.toString());
    }
    else {
      TextFile outFile = MA.getTextFile("OUT");
      outFile.open(TextFile.OUTPUT);
      outFile.write(xml.toString());
      outFile.close();
    }
  }

  /** Converts an XML file or XML String (results) to a BLUE file.
   *  This is the reverse of {@link #convertToXml(String)}.
   *  @since NeXtMidas 3.5.4
   */
  private void convertXmlToBlue (String conv) {
    Table options = MA.getTable("OPTIONS");
    DataFile outDataFile = MA.getDataFile("OUT");

    if (conv.contains("/S")) { // IN arg is an XML string
      String xmlstr = MA.getU("IN");
      XmlFile.xmlToFile(xmlstr, outDataFile, options);
    }
    else {
      TextFile inTextFile = MA.getTextFile("IN", Args.REQUIRED);
      String mimeType = TextFile.getMimeType(null, inTextFile.filename);
      if (!mimeType.startsWith("text")) { // since 3.6.0 for Bug 2903: check input file type
        M.error("Input file must be of type TEXT (wrong conversion direction?)");
      }
      else {
        XmlFile.xmlToFile(inTextFile, outDataFile, options);
      }
    }
  }

  /** Helper function for writing a table to a type 3000 file. inTable is
      a table with 1..n tables of the same structure */
  private void writeTableTo3000 (Table inTable, String [] formatList,
                                 DataFile df) {
    double df_offset = 0.0;
    Data dfdata = new Data();

    dfdata.setSize (df.getRecLength());
    dfdata.setFormatType (df.dataType);
    dfdata.setRecLength (df.getRecLength());

    for (Table.Iterator ti=inTable.iterator(); ti.getNext(); ) {
      int boff = 0;
      Table.Iterator dti = (Convert.o2t(ti.value)).iterator();
      for (int i=0 ; i<dti.size ; i++) {
        String dataString = ""+dti.values[i];
        String format     = formatList[i];
        int    width      = Data.getBPA(format);

        // TODO: The code below is missing a number of data types and is incorrectly
        //       doing an .indexOf(..) rather than checking the proper type letter.
        //       This will do the WRONG thing if type 'X' support is added and the
        //       format is "XA" (80-char ASCII not XLong).
        if      (format.indexOf('A') > 0) dfdata.packS(boff, width, dataString);
        else if (format.indexOf('D') > 0) dfdata.packD(boff, Convert.s2d(dataString));
        else if (format.indexOf('L') > 0) dfdata.packL(boff, Convert.s2l(dataString));
        else if (format.indexOf('F') > 0) dfdata.packF(boff, Convert.o2f(Convert.s2o(dataString,'F',M)));
        else if (format.indexOf('I') > 0) dfdata.packI(boff, Convert.o2i(Convert.s2o(dataString,'I',M)));
        else if (format.indexOf('B') > 0) dfdata.packB(boff, Convert.o2b(Convert.s2o(dataString,'B',M)));
        boff += width;
      }
      df.write (dfdata, df_offset, 1);
      df_offset++;
    }
  }

  /** Helper function to form record name list for creating 3000 files */
  private String [] getRecNamesFromTable (Table recTable) {
    final int MAX_RECORD_NAME_LENGTH = 4;

    String nameList[] = new String[recTable.size()];
    short fieldNum = 0;
    for (Table.Iterator ti=recTable.iterator(); ti.getNext(); ) {
      String keyStr = ti.key;
      int len = MAX_RECORD_NAME_LENGTH;
      if (keyStr.length() < MAX_RECORD_NAME_LENGTH) {
        len = keyStr.length();
      }
      nameList [fieldNum] = keyStr.substring(0,len);
      fieldNum++;
    }
    return nameList;
  }

  /** Helper function to form format string array used to create 3000 files */
  private String [] getFormatList (String formatString, Table recTable)
                    throws ClassCastException {
    String formatList[] = new String[recTable.size()];

    boolean useDefaultFormats = formatString == null;
    short fieldNum = 0;
    for (Table.Iterator ti=recTable.iterator(); ti.getNext(); ) {
      String keyStr = ti.key;
      Data data;
      try {
        data = recTable.getData(keyStr);
      } catch (Exception e) {
        // Data in this table that can not be converted to a primitive type
        throw new ClassCastException();
      }
      String recFormat = null;
      if (!useDefaultFormats) {
        String formatKeyValString = StringUtil.getWordByString (formatString, keyStr);
        if (formatKeyValString != null &&
            !formatKeyValString.startsWith (keyStr) ) {
          formatKeyValString = null;
        }
        if (formatKeyValString != null) {
          recFormat = getKeyValue (formatKeyValString);
        }
      }
      if (useDefaultFormats || recFormat == null) {
        // In this mode use double or String (ascii) types for all data
        try {
          double d = Double.parseDouble(""+data);
          formatList [fieldNum] = "SD";
        } catch (Exception e) {
          formatList [fieldNum] = "SA";
        }
      } else {
        formatList [fieldNum] = recFormat;
      }
      fieldNum++;
    }  // end While recTable.more

    return formatList;
  }

  /** Helper function for getting value from KEY=VALUE string */
  private String getKeyValue (String s) {
    int delimIndex = s.indexOf('=');
    return s.substring (delimIndex+1);
  }

  /** Helper function for forming a type 3000 header.  Record names defaulted if record name list = null */
  private void make3000Header (String formatList[],
                               String recNameList[],
                               DataFile df) {
    df.setType(3000);
    String recName;
    for (int i=0; i<formatList.length; i++) {
      if (recNameList != null) recName = recNameList[i];
      else recName = df.getUniqueRecName("TR00",i+1,null);
      df.addSubRec (recName, formatList[i]);
    }
  }

  /** Convert a comma-separated-value (CSV) text file into a Table or an XML text file */
  private int convertCSV(String toWhat) {
    String csvFileName = MA.getFileName("IN").toString();
    CsvFile csvFile = new CsvFile(M,csvFileName);
    csvFile.open(CsvFile.INPUT);
    if (toWhat.equals("TABLE")) {
      String outputResultName = MA.getU("OUT");
      Table tab = csvFile.toTable();
      MR.put(outputResultName,tab);
    } else {
     // TextFile tf = MA.getTextFile("OUT");   // Changed to 3 lines below for Bug 1972
      String outFile = MA.getCS("OUT");
      if (outFile != null && outFile.indexOf('.')==-1) outFile += ".xml"; // Since 2.9.0 (Bug 1972)
      TextFile tf = new TextFile(M, outFile);
      int indent = 2;
      if (tf !=null) {
        tf.open(TextFile.OUTPUT|TextFile.FORCEABORT);
        tf.writeln("<XML>");
        tf.writeln(csvFile.toXmlBrief(indent));
        tf.writeln("</XML>");
        tf.close();
      } else { // TODO: Dead code
        M.error("Need an output file name to convert CSV file "+ csvFileName);
      }
    }
    csvFile.close();
    return NORMAL;
  }

  /** Converts XML to a table. */
  private int xmlToTable(String conv) {

    // This default is intentionally different from the default for XML2TBL
    String  delim = MA.getS("/DELIM", DEF_DUPKEY_DELIM_TAB2XML);
    String  cname = "CDATA"; // No switch yet
    int     start = MA.getL("/BASE", 1);
    boolean comp  = MA.getState("/COMP");
    // Switch for using root XML tag name as the results key (since 3.3.3 for BUG 2752)
    boolean useRootForRes = MA.getState("/USEROOTFORRES");
    // OUT parameter is mandatory unless /useRootFor Res switch is set
    String resName = MA.getS("OUT");

    // Check for missing OUT name (since NXM 2.7.2)
    if ((resName == null || resName.length() == 0) && !useRootForRes) {
      M.error("CONVERT: No output table name or useRootForRes switch for XML2TAB conversion of " +MA.getFileName("IN"));
    } else {

      try {

        // Create MaskValue containing the options for converting xml to table
        MaskValue<Xml2TblConversionOptions> xml2TblconversionOptions = MaskValue.forType(Xml2TblConversionOptions.class);
        if (comp) {
          xml2TblconversionOptions.set(Xml2TblConversionOptions.Compression);
        }
        if (useRootForRes) {
          xml2TblconversionOptions.set(Xml2TblConversionOptions.IncludeRootName);
        }

        // Convert Xml to Table
        Table tbl;
        if (conv.indexOf("/S") > 0) {
          tbl = XmlFile.fastXmlToTable(MA.getS("IN"), delim, cname, start, xml2TblconversionOptions);
        } else {
          TextFile tf = MA.getTextFile("IN");
          tbl = XmlFile.fastXmlFileToTable(tf, delim, cname, start, xml2TblconversionOptions);
        }

        // If using Root Name for Result key, get root name from table and
        // remove the key value pair.  @see documentation in XmlFile
        if (useRootForRes) {
          resName = tbl.getS(XmlFile.KEY_ROOTNAME);
          if (!MA.getState("/KEEPROOTKEY")) { // since 3.5.4
            tbl.remove(XmlFile.KEY_ROOTNAME);
          }
        }

        if (tbl != null) {
          if (XmlFile.getDotKeyFixEnabled()) tbl.hasDotsInKeys(resName);
          MR.put(resName, tbl);
        } else {
          M.warning("CONVERT: Unable to convert XML to table (missing close tag?).");
        }
      } catch (MidasException me) {
        throw new MidasException(me);
      } catch (Exception e) {
        M.printStackTrace("CONVERT: Unable to convert XML to table.", e);
      }
    }

    return NORMAL;
  }

  /** Converts a NeXtMidas table into XML. */
  private int tableToXML(String conv) {
    Table  inputTable   = MA.getTable("IN");
    String mainKey = MA.getS("/XMLKEY","XML");
    // This is intentionally different from the default for XML2TBL
    String  delim = MA.getS("/DELIM", DEF_DUPKEY_DELIM_TAB2XML);
    String  cname = "CDATA"; // No switch yet
    boolean comp  = MA.getState("/COMP");
    boolean newLines  = MA.getState("/NEWLINES",true);
    try {
      if (conv.indexOf("/S") > 0) {
        String xml = XmlFile.fastTableToXml(inputTable, mainKey, delim, cname, comp, newLines);
        if (xml != null) {
          String resName = MA.getU("OUT");
          MR.put(resName, xml);
        } else {
          M.warning("CONVERT: Unable to convert table to XML.");
        }
      } else {
        String outFile = MA.getCS("OUT");
        if (outFile != null && outFile.indexOf('.')==-1) outFile += ".xml";  // since 2.9.0 add .xml extension (Bug 1972)
        TextFile tf = new TextFile(M, outFile);
        XmlFile.fastTableToXmlFile(tf, inputTable, mainKey, delim, cname, comp, newLines);
      }
    } catch (Exception e) {
      M.printStackTrace("CONVERT: Unable to convert table to XML.", e);
    }
    return NORMAL;
  }

  /** Converts XML to a table. */
  private int xmlToMapKBT(String conv) {

    // Switch for using root XML tag name as the results key (since 3.3.3 for BUG 2752)
    boolean useRootForRes = MA.getState("/USEROOTFORRES");
    // OUT parameter is mandatory unless /useRootFor Res switch is set
    String resName = MA.getS("OUT");

    // Check for missing OUT name (since NXM 2.7.2)
    if ((resName == null || resName.length() == 0) && !useRootForRes) {
      M.error("CONVERT: No output table name or useRootForRes switch for XML2TAB conversion of " +MA.getFileName("IN"));
    } else {

      try {

        // Convert Xml to MapKBT
        MapKBT mapkbt;
        if (conv.indexOf("/S") > 0) {
          mapkbt = XmlFile.fromXML(MA.getS("IN"));
        } else {
          TextFile tf = MA.getTextFile("IN");
          tf.open();
          String xml = tf.readAll();
          mapkbt = XmlFile.fromXML(xml);
          tf.close();
        }

        // If using Root Name for Result key, get root name from table and
        // remove the key value pair.  @see documentation in XmlFile
        if (useRootForRes) {
          resName = mapkbt.getS(XmlFile.KEY_ROOTNAME);
          if (!MA.getState("/KEEPROOTKEY")) { // since 3.5.4
            mapkbt.remove(XmlFile.KEY_ROOTNAME);
          }
        }

        if (mapkbt != null) {
          MR.put(resName, mapkbt);
        } else {
          M.warning("CONVERT: Unable to convert XML to MapKBT (missing close tag?).");
        }
      } catch (MidasException me) {
        throw new MidasException(me);
      } catch (Exception e) {
        M.printStackTrace("CONVERT: Unable to convert XML to MapKBT.", e);
      }
    }

    return NORMAL;
  }

  /** Convert an XML text file into a Midas BLUE type 3000 file */
  private int convertXMLText2Blue() {
    TextFile xmltf = MA.getTextFile("IN");
    DataFile dfout = MA.getDataFile("OUT");
    String subTableKeys = MA.getS("/XMLKEY","XML");
    String formatString  = MA.getU("FORM");
    char delim           = MA.getS("/DELIM","|").charAt(0);
    boolean defaultRecNames = MA.getState("/DEFAULTNAMES",false);

    // Clean up the format string by making it all caps and replacing any
    // delimiter with a comma.
    //formatString.toUpperCase(); // <-- This line never did anything!
    if (delim != ',') formatString = formatString.replace(delim, ',');

    // A key (or keys) for the table of interest, make sure the keys are
    // upper case as well
    if (subTableKeys != null) subTableKeys = subTableKeys.toUpperCase();

    // Convert the XML text into a table
    Table mainTable = convertXMLText2Table (xmltf);
    if (mainTable != null) {
      // Get to the table (sub-table) of interest
      StringTokenizer subkeyST = new StringTokenizer (subTableKeys, ""+delim);
      Table subTable = mainTable;
      String token = null;
      while (subkeyST.hasMoreTokens()) {
        token = subkeyST.nextToken();
        if (token != null && !token.isEmpty()) {
          subTable = subTable.getTable(token);
        }
      }

      // Now convert this "FLAT" table into a type-3000 BLUE file
      if (subTable != null) {
        convertTable2BlueT3000 (subTable, formatString, defaultRecNames, dfout);
        //convertTable2Blue (subTable, dfout);
      } else {
        // Table is null
        M.error ("FINAL TABLE: key = "+token+" is null");
      }
    } else {
      M.error ("Unable to convert table to BLUE file");
    }
    return NORMAL;
  }

  /** Wrapper to the static method for doing all the work */
  private int convertTable2XMLText (String conv) {
    boolean formString = (conv.indexOf("/S") > 0);
    Table tin = MA.getTable("IN");
    String outResult = MA.getS("OUT");
    if (outResult == null || outResult.length() == 0) {
      M.error("CONVERT: No output result specified for conversion TBL2XML");
    }
    String mainKey = MA.getS("/XMLKEY","XML");
    String  delim = MA.getS("/DELIM", DEF_DUPKEY_DELIM_TBL2XML);
    boolean newLines = MA.getState("/NEWLINES", false);
    int status = convertTable2XMLText(tin, formString, mainKey, outResult, MR, delim, newLines, M);
    if (status < 0) {
      M.error("Must specify table name. Use /TABLE argument.");
    }
    return NORMAL;
  }

  /** Convert a Midas table into an XML text file or String with a default delimiter for duplicate keys
   *  @param tin         Midas input table
   *  @param formString  Output a String result if true, a text file otherwise
   *  @param mainKey     The top-level key like &lt;XML
   *  @param outResult   The output result name or file name
   *  @param MR          The midas results table
   *  @return            -1 for error or 0 otherwise
   */
  public static int convertTable2XMLText (Table tin, boolean formString, String mainKey, String outResult,
                                          nxm.sys.lib.Results MR) {
    return convertTable2XMLText (tin, formString,  mainKey, outResult, MR, DEF_DUPKEY_DELIM_TBL2XML, false);
  }

  /** Convert a Midas table into an XML text file or String
    @param tin The input table
    @param formString Output a String result if true, a text file otherwise
    @param mainKey The top-level key like &lt;XML
    @param outResult The output result name or file name
    @param MR The midas results table
    @param duplicateKeyDelim Delimiter for case where XML has duplicates
    @param newLines append new line after each token
    @return -1 for error or 0 otherwise
  */
  public static int convertTable2XMLText (Table tin, boolean formString, String mainKey, String outResult,
                                          nxm.sys.lib.Results MR, String duplicateKeyDelim, boolean newLines) {
    return convertTable2XMLText(tin,formString, mainKey,outResult,MR,duplicateKeyDelim, newLines, Shell.getMidasContext());
  }

  private static int convertTable2XMLText (Table tin, boolean formString, String mainKey, String outResult,
        nxm.sys.lib.Results MR, String duplicateKeyDelim, boolean newLines, Midas M) {
    if (tin != null) {
      StringBuffer sbout = new StringBuffer();
      // If there are no inner tables we want to write XML of the form
      //   <XML ATTR1="NICE" ATTR2="SLOPPY" ... />.
      // otherwise, with inner tables we want to write XML of the form
      //   <XML><INNER ATTR1="NICE" ATTR2="SLOPPY" .../></XML>

      sbout.append("<").append(mainKey);// the closing '>' is written if there are inner tables
      int retval = innerTablesToXML(tin, sbout, duplicateKeyDelim, newLines, true);
      if (retval == 0) {
        String attrs = getXMLAttributes(tin);
        sbout.append(attrs).append("/>");
      }
      else {
        sbout.append("</").append(mainKey).append(">");
      }
      if (newLines) sbout.append("\n");

      if (formString) {
        if (outResult != null && outResult.length() > 0) {
          MR.put(outResult,""+sbout);
        }
      }
      else if (outResult.length()>0) {
        TextFile tf = new TextFile(M,outResult); // NXM-4097
        // Output to specified file
        tf.open(TextFile.OUTPUT|TextFile.FORCEABORT);
        tf.write(sbout.toString());
        tf.close();
      }
      else {
        // Output to screen
        Shell.getSharedMidasContext().println(sbout.toString()); // since 3.7.1 use M.println instead of M.terminal.writeln to allow logging of interactive commands NXM-2813
      }
    }
    else {
      return -1;
    }
    return 0;
  }

  /* Convert a Midas table into an HTML text file */
  private int convertTable2HTMLText () {
    Table tin = MA.getTable("IN");
    TextFile tf = MA.getTextFile("OUT");
    String mainKey = MA.getS("/HTMLKEY","HTML");
    String duplicateKeyDelim = MA.getS("/DELIM",DEF_DUPKEY_DELIM_TBL2XML);
    // If no extension is given, set to html.
    FileName fileName = tf.getFileName();
    if (fileName.getExt() == null || fileName.getExt().length() == 0) {
      fileName.setExt("html");
    }
    if (tin != null) {
      if (tf.filename.length()>0) {
        // Output to specified file
        createHTMLOutput(tin, mainKey, tf, duplicateKeyDelim);
      } else {
        // Output to screen
        createHTMLOutput(tin, mainKey, null, duplicateKeyDelim);
      }
    } else {
      M.error("Must specify table name.  Use /TABLE argument.");
    }
    return NORMAL;
  }

  /** Convert an XML text file or String into a Midas table */
  private int convertXMLText2Table(String conv) {
    String inString = MA.getS("IN").toUpperCase();
    String mainKey = MA.getS("/XMLKEY","XML");
    String tout    = MA.getU("OUT");
    if (tout == null || tout.length() == 0) {
      M.error("CONVERT: No output specified for XML to Table conversion.");
      return NORMAL;
    }
    Table output;
    Table out;
    if (conv.indexOf("/S") >= 0) {
      //XML is a string
      output = new Table();
      try {
        fromXML(inString, output);
      } catch (NoSuchElementException e) {
        M.warning("Unable to convert XML, '"+inString+"'"); // stack trace not needed
      }
    } else {
      //XML is contained in a Text File
      TextFile tf = MA.getTextFile("IN");
      output = convertXMLText2Table(tf); // may return null on error
      if (output == null) {
        M.warning("CONVERT: An error occurred while reading '"+tf+"'.");
        output = new Table();
      }
    }
    if (output != null) {
      out = output.getTable(mainKey);
      if (out != null) MR.put(tout, out);
    }
    return NORMAL;
  }

  /** Convert an XML text file object into a table object
      @param tf A NeXtMidas TextFile containing XML
      @return A table or null if unable to open file
   */
  public static Table convertXMLText2Table (TextFile tf) {
    StringBuilder data   = new StringBuilder();
    Table         output = new Table();

    try {
      tf.open(TextFile.INPUT|TextFile.FORCEABORT);
      String line;
      while (tf.isOpen && (line=tf.read())!=null) {
        if (data.length() == 0) {
          data.append(line.trim());
        } else {
          data.append(" ").append(line.trim());
        }
      }
      tf.close();
    } catch (Exception e) {
      Shell.printStackTrace(e);
      return null;
    }

    String xmlString = data.toString();
    if (xmlString.indexOf ("<XML") <  0) {
      Shell.warning("No XML tag, will attempt to convert anyway. xml=" + xmlString);
    }
    fromXML(xmlString, output);
    return output;
  }

  /** Supporting method for HTML conversions */
  private void createHTMLOutput (Table table, String key, TextFile tf, String duplicateKeyDelim) {
    if (tf !=null) tf.open(TextFile.OUTPUT|TextFile.FORCEABORT);
    textWriteln(tf,"<"+key+">");
    textWriteln(tf,"<table border=\"1\" cellpadding=\"0\" cellspacing=\"0\" width=\"50%\" BGCOLOR=\"CCCCCC\" >");
    getHTMLInnerTables(table, tf, duplicateKeyDelim, true);
    textWriteln(tf,"</table>");
    textWriteln(tf,"</"+key+">");
    if (tf !=null) {tf.close();}
  }

  /** Checks the innertable name for __ on the end and removes it if present
   @param _innerTable name of the inner table
   @return inner table XML with delimiter stripped if it was present */
  private static String fixXmlInnerTableName(String _innerTable, String duplicateKeyDelim) {
     String innerTableXMLname;
     int backnum = _innerTable.lastIndexOf(duplicateKeyDelim);
     if (backnum>0) innerTableXMLname=_innerTable.substring(0,backnum);
     else innerTableXMLname=_innerTable;
     return innerTableXMLname;
  }

  private static int innerTablesToXML(Table table, StringBuffer sbout, String duplicateKeyDelim,
                                      boolean newLines, boolean init) {
    int retval=1;
    StringBuilder sb = new StringBuilder();
    for (Table.Iterator ti=table.iterator(); ti.getNext(); ) {
      String keyval = ti.key;
      Object obj = ti.value;
      if (obj instanceof Table) {
        sb.append(keyval).append(",");
      }
    }
    String nameStr = sb.toString();
    if (nameStr.length()>0) {
      nameStr=nameStr.substring(0,nameStr.length()-1);
      sbout.append(">");
      if (newLines)sbout.append("\n");
      StringTokenizer mainST = new StringTokenizer(nameStr,",",false);
      while (mainST.hasMoreTokens()) {
        String innerTable = mainST.nextToken();
        // check inner table name for __# on the end and remove
        String innerTableXML = fixXmlInnerTableName(innerTable, duplicateKeyDelim);
        sbout.append("<").append(innerTableXML);
        Table t = table.getTable(innerTable);
        String attrs = getXMLAttributes(t);
        if (attrs.length()>0) sbout.append(" ").append(attrs);
        int ret = innerTablesToXML(t, sbout, duplicateKeyDelim, newLines, false);
        if (ret==1) {
          sbout.append("</").append(innerTableXML).append(">");
          if (newLines) sbout.append("\n");
        }
      }
    } else {
      if (init) {
        sbout.append(" ");
      } else {
        sbout.append("/>");
        if (newLines) sbout.append("\n");
      }
      retval = 0;
    }
    return retval;
  }

  /** Write to TextFile or if TextFile is null, write to screen */
  private void textWriteln (TextFile tf, String s) {
    textWrite(tf,s+"\n");
  }

  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  private void textWrite (TextFile tf, String s) {
    if (tf == null) M.terminal.print(s);
    else            tf.write(s);
  }

  /** Supporting methods for table-to-html TBL2HTML conversions */
  private int getHTMLInnerTables(Table table, TextFile tf, String duplicateKeyDelim, boolean init) {
    int retval=1;
    StringBuilder sb = new StringBuilder();

    for (Table.Iterator ti=table.iterator(); ti.getNext(); ) {
      String keyval = ti.key;
      Object obj = ti.value;
      if (obj instanceof Table) {
        sb.append(keyval).append(",");
      }
    }
    String nameStr = sb.toString();
    if (nameStr.length()>0) {
      nameStr=nameStr.substring(0,nameStr.length()-1);
      if (!init) textWrite(tf,">");
      StringTokenizer mainST = new StringTokenizer(nameStr,",",false);
      while (mainST.hasMoreTokens()) {
        String innerTable = mainST.nextToken();
        // check inner table name for __# on the end and remove
        String innerTableHTML = fixXmlInnerTableName(innerTable, duplicateKeyDelim);
        textWrite(tf,"<tr><td border=\"1\"><i> "+innerTableHTML + "</i></td>");

        Table t = table.getTable(innerTable);
        String attrs = getHTMLAttributes(t);
        if (attrs.length()>0) textWrite(tf," "+attrs);
        int ret = getHTMLInnerTables(t, tf, duplicateKeyDelim, false);
        if (ret==1) textWrite(tf,"</"+innerTableHTML+">");
      }
    } else {
      retval = 0;
    }
    return retval;
  }

  /** de-serialize from an XML format
   *  @param bufferString string
   *  @param output       Table with deserialized XML
   *  @throws NoSuchElementException thrown if unable to serialize element
   */
  @LegacyMethod("use lib.XmlFile.fromXML(String, Table) - functionality moved to library Class in 3.9.2")
  public static void fromXML(String bufferString, Table output) throws NoSuchElementException {
    XmlFile.fromXML(bufferString, output); // Moved logic to XMLFile.fromXML since 3.9.2 (jdk11 modularity)
  }

  /** de-serialize from an XML format
   *  @param bufferString            XML string
   *  @param output                  Table with deserialized XML
   *  @param duplicateKeyDelim       duplicate key delimiter
   *  @throws NoSuchElementException thrown if unable to serialize element
   */
  @LegacyMethod("use XmlFile.fromXML(String, Table, String) - functionality moved to library Class in 3.9.2")
  public static void fromXML(String bufferString, Table output, String duplicateKeyDelim)
         throws NoSuchElementException {
    XmlFile.fromXML(bufferString, output, duplicateKeyDelim); // Moved logic to XMLFile.fromXML since 3.9.2 (jdk11 modularity)
  }

  /** Makes an XML table from the given String
   *  @param st string with tokens to convert to XML table
   *  @return XML Table
   */
  @LegacyMethod("use XmlFile.makeXMLTableFromLine(StringTokenizer) - functionality moved to library Class in 3.9.2")
  public static Table makeXMLTableFromLine (StringTokenizer st) {
    return XmlFile.makeXMLTableFromLine(st); // Moved logic to XMLFile.makeXMLTableFromLine since 3.9.2 (jdk11 modularity)
  }

  /** Get a String object containing the table information as XML attributes
   *  @param t table containing information to be converted to XML
   *  @return String object containing the table information as XML attributes
   */
  public static String getXMLAttributes (Table t) {
    StringBuilder sb = new StringBuilder();
    for (Table.Iterator ti=t.iterator(); ti.getNext(); ) {
      String keyval = ti.key;
      Object obj = ti.value;
      if (!(obj instanceof Table)) {
        sb.append(keyval).append("=").append('"').append(obj).append('"').append(" ");
      }
    }
    return sb.toString();
  }

  /** Get a String object containing the table information as HTML attributes
   *  @param t table containing information to be converted to HTML
   *  @return String object containing the table information as HTML attributes
   */
  public String getHTMLAttributes (Table t) {
    StringBuilder sb = new StringBuilder();
    for (Table.Iterator ti=t.iterator(); ti.getNext(); ) {
      String keyval = ti.key;
      Object obj = ti.value;
      if (obj instanceof Table) {
        // do nothing
      }
      else {
        String cellColor = "CCCCCC";
        if (obj.toString().equals("-1")) cellColor = "F00000";
        if (obj.toString().startsWith("FAILED SOME TESTS")) cellColor = "F00000";
        if (obj.toString().equals("1")) cellColor = "009900";
        if (obj.toString().startsWith("PASSED ALL TESTS")) cellColor = "009900";
        String borderString = " BORDER = \"1\"";
        String bgColorString = " BGCOLOR = \"" + cellColor + "\"";
        sb.append("<tr><td ").append(borderString).append(bgColorString).append(">")
          .append(keyval).append("</td><td").append(borderString).append(bgColorString)
          .append(" >").append(obj).append("</td></tr><tr><td>");
      }
    }
    return sb.toString();
  }

  /** Converts a X-Midas XDATALIST template to a table template. */
  private void convertTemplate2Table(String conv) {
    Table tbl = convertTemplate2Table(MA.getDataFile("IN"));

    if(conv.equals("TEMPLATE2TBL/F")) {
      tbl.toTextFile(MA.getTextFile("OUT"));
    } else {
      MR.put(MA.getU("OUT"), tbl);
    }
  }

  /** Converts the X-Midas XDATALIST template (used by LIST) to a table template that can be used by LIST2.
      @param template The datafile with the template.
      @return The template table.
      @since NeXtMidas 2.1.0
   */
  public static Table convertTemplate2Table(DataFile template) {
    template.open();

    Keywords keywds = template.keywords;
    Table    tbl    = new Table();
    Keywords.Iterator kwi = keywds.iterator();
    Keywords.Key      val = null;

    while (kwi.hasNext()) {
      val = kwi.next();

      if (val.name.equals("SECTION") && val.value.equals("XDATALIST")) {
        break;
      }
    }

    if (val != null) {
      Table    global = new Table();
      int      fields = -1;

      tbl.setKey("_GLOBAL_", global);
      global.setKey("TYPE", "XDATALIST");


      while (kwi.hasNext()) {
        val = kwi.next();

        if (val.name.equals("FIELD")) {
          break;
        } else {
          global.setKey(val.name, val.value);
          if (val.name.equals("FIELDS")) {
            fields = Convert.o2l(val.value);
          }
        }
      }

      for (int i = 1; i <= fields; i++) {
        String label = "_"+i+"_";
        Table  field = new Table();

        if (keywds.setScope("FIELD="+i) == 1) { //found
          kwi = keywds.iterator();

          while (kwi.hasNext()) {
            val = kwi.next();

            if (val.name.equals("SUBR")) {
              label = Convert.o2s(val.value);
            } else if (val.name.equals("SECTION")) {
              /* Remove SECTION=END tag that is on the last one enrty. */
            } else {
              field.setKey(val.name, val.value);
            }
          } // end while loop
        }

        tbl.setKey(label, field);
      } // end for loop

    } else {
      tbl = null;
    }

    template.close();
    return tbl;
  }

  /** Converts a ByteBuffer definition to Java code. */
  private void bbdef2java() {
    String fname = MA.getFileName("IN").toString();
    try {
      FortranUtil.wrapperByteBuffer(new java.io.File(fname));
    } catch(Exception e) {
      throw new MidasException("BYTEBUFFER: Error running MAKE on '"+fname+"'.", e);
    }
  }

  /** Converts a Fortran structure into a Java class. */
  private void fstruct2java() {
    String  inFile     = MA.getFileName("IN").toString();
    String  outFile    = MA.getFileName("OUT").toString();
    String  struct     = MA.getS("FORM",      null);
    String  varName    = MA.getS("/VARIABLE", null);
    String  classSub   = MA.getS("/CLASSSUB", null);
    String  includes   = MA.getS("/INCLUDE",  null);
    boolean pkgPrivate = MA.getState("/PACKAGE");

    if((inFile == null) || (inFile.trim().length() == 0)) {
      M.error("No input file specified.");
    }

    if((outFile == null) || (outFile.trim().length() == 0)) {
      M.error("No output file specified.");
    } else if(!outFile.endsWith(".java")) {
      M.error("Name of output file must end in '.java', given '"+outFile+"'.");
    }

    String jniFile = outFile.substring(0, outFile.lastIndexOf('.')) + ".c";

    try {
      FortranUtil.wrapFortranStruct(new java.io.File(inFile), new java.io.File(outFile),
                                    new java.io.File(jniFile), struct, varName, classSub, includes,
                                    pkgPrivate);
    } catch(Exception e) {
      throw new MidasException("BYTEBUFFER: Error running TRANSF: "+e.getMessage(),e);
    }
  }

  /** Provides the functionality of the X-Midas GENMSGING for XBC and also creates the TBL files
      required for converting NeXtMidas to/from X-Midas messages.
      @param fmt The output format.
   */
  private void genmsg(int fmt) {
    String opt    = MA.getS("IN");
    String path   = MA.getS("/PATH", MR.getS("OPT.PATH"));
    String xmdisk = MA.getS("/XMDISK");

    if ((xmdisk == null) || (xmdisk.length() == 0)) xmdisk = null;

    XmUtil.genmsg(M, xmdisk, opt, path, fmt);
  }

  /** Converts reference frame and type. Since 3.3.0. */
  private void convertRefFrame (String outRefFrame, String outType) {
    DataFile    in         = MA.getDataFile("IN", "5000","NH|VD", DataFile.INPUT ); in.open();
    DataFile    out        = MA.getDataFile("OUT", in,            DataFile.OUTPUT); out.open();
    Table       inRecDefs  = in.getRecordDefs();
    Table       outRecDefs = out.getRecordDefs();
    Table       qw         = MA.getTable("QUADWORDS", new Table());
    Time        epoch      = MA.getTime("EPOCH", out.getTime());
    StateVector sv         = new StateVector(this, in.getName());

    if (outRefFrame == null) {
      // Just doing a type conversion
      outRefFrame = in.getReferenceFrame();
    }

    if (inRecDefs.containsKey("POS")) outRecDefs.put("POS.TYPE", outType);
    if (inRecDefs.containsKey("VEL")) outRecDefs.put("VEL.TYPE", outType);
    if (inRecDefs.containsKey("ACC")) outRecDefs.put("ACC.TYPE", outType);

    out.setRecordDefs(outRecDefs);
    out.setReferenceFrame(outRefFrame);
    if (qw.containsKey("ALT" )) out.setQuadword(DataFile.Q_ALT,  qw.getD("ALT"));
    if (qw.containsKey("LAT" )) out.setQuadword(DataFile.Q_LAT,  qw.getD("LAT"));
    if (qw.containsKey("LON" )) out.setQuadword(DataFile.Q_LON,  qw.getD("LON"));
    if (qw.containsKey("AZIM")) out.setQuadword(DataFile.Q_AZIM, qw.getD("AZIM"));
    if (qw.containsKey("ELEV")) out.setQuadword(DataFile.Q_ELEV, qw.getD("ELEV"));
    if (qw.containsKey("ROLL")) out.setQuadword(DataFile.Q_ROLL, qw.getD("ROLL"));
    out.setQuadword(DataFile.Q_YEAR, epoch.getYear());
    out.setQuadword(DataFile.Q_SEC,  epoch.getSoY());
    out.setQuadword(DataFile.Q_GHA,  epoch.getGHA());

    // By going through the Transform class we "clean up" the quadwords and
    // avoid setting quadwords that are not applicable.
    Transform xform = new Transform(out);
    xform.toDataFile(out);

    if (inRecDefs.containsKey("TIME")) {
      List<Time> times = new ArrayList<>((int)in.getSize());
      for (int i = 0; i < in.getSize(); i++) {
        times.add(Time.toTime(in.getData(i, "TIME")));
      }
      for (Table row : sv.toFileEntries(times, out)) {
        out.writeDataTable(row);
      }
    }
    else {
      for (Table row : sv.toFileEntries(out)) {
        out.writeDataTable(row);
      }
    }

    in.close();
    out.close();
  }

  /** StateVector interpolation (takes the place of X-Midas commands like SVINTERP_TC). Since 3.3.0. */
  private void svinterp () {
    DataFile    in         = MA.getDataFile("IN",  "5000","NH|VD", DataFile.INPUT ); in.open();
    DataFile    out        = MA.getDataFile("OUT", in,             DataFile.OUTPUT); out.open();
    StateVector sv         = new StateVector(this, in.getName());
    List<Time>  times      = new ArrayList<>();
    Table       options    = MA.getTable("OPTIONS");
    Table       outRecDefs = out.getRecordDefs();
    DataFile    inTimes = null;
    boolean generateTimes = false; //bug2822 (when true times are user specified)

    KeyObject.setKeys(sv, options, this, 0);

    if (!outRecDefs.containsKey("TIME")) {
      outRecDefs.put("TIME", FileUtil.recDef("TIME", "SD", null, Units.SCALAR, Units.TIME_S));
      out.setRecordDefs(outRecDefs);
    }

    if (MA.find("FUNC") && !MA.find("TIMES")) { // allow positional argument
      inTimes = MA.getDataFile("FUNC",  "3000|5000|6000","", DataFile.INPUT);
    }
    //BUG 2822 Provide options for specifying start/stop/delta for interpolation
    else if (MA.find("TIMESPEC")) {
      generateTimes = true;
    }
    else {
      inTimes = MA.getDataFile("TIMES", "3000|5000|6000","", DataFile.INPUT);
    }

    if ( generateTimes ) {
      // BUG 2822 Provide options for specifying start/stop/delta for interpolation
      // Not all end users do their own ephemeris interpolation
      // Allow start time, stop time, time delta to be specified for SVINTERP
      Table timeSpec = MA.getTable( "TIMESPEC" );
      Time startTime = Time.toTime( timeSpec.get("START"));
      Time stopTime = Time.toTime( timeSpec.get("STOP"));
      double timeDelta = timeSpec.getD("DELTA");
      //alternate
      double deltaWSec = Math.floor(timeDelta);
      double deltaFSec = timeDelta % 1;
      int row_minus_1 = 0;
      Time currentTime = new Time(startTime);
      while ( currentTime.getSec() < stopTime.getSec()){
        times.add(currentTime);
        currentTime = new Time(startTime);
        // To keep as much precision as possible:
        //.-calculate total offset from start time, rather than repeated adds
        //.-add in separately the contributions from the fractional delta & whole delta

        currentTime.addSec(deltaFSec * ++row_minus_1);
        currentTime.addSec(deltaWSec * row_minus_1);
      }
    }
    else {
      inTimes.open();

      if (inTimes.findRec("TIME") >= 0) {
        Table row = new Table();
        while (inTimes.readDataTable(row) > 0) {
        times.add(new Time(row.getD("TIME")));
        }
      }
      else if ((inTimes.findRec("TCWS") >= 0) && (inTimes.findRec("TCFS") >= 0)) {
        // ICEPacket or the like with whole seconds and fractional seconds
        Table row = new Table();
        while (inTimes.readDataTable(row) > 0) {
          Time t = new Time(row.getD("TCWS"), row.getD("TCFS"));

          if (row.containsKey("TCO")) {
            t.addSec(-1.0 * row.getD("TCO") * inTimes.getDelta());
          }
          times.add(t);
        }
      }
      else {
        throw new MidasException("No TIME column found in "+inTimes.getName());
      }
    }

    for (Table row : sv.toFileEntries(times, null)) {
      out.writeDataTable(row);
    }

    if (times.size() > 1) {
      // Matches SVINTERP_TC in setting delta to match the avg. time delta
      double dtTotal = 0.0;
      double dtConst = times.get(1).diff(times.get(0));
      for (int i = 1; i < times.size(); i++) {
        dtTotal += times.get(i).diff(times.get(i-1));
      }
      double dt = dtTotal / (times.size()-1);
      if (Math.abs(dt-dtConst) < 1e-12) {
        // Difference from a predicted constant dt is <1ps so it is probably
        // safe to assume the dt is constant and the delta is rounding error.
        // This case can be seein with %TEST_CONVERT_SVINTERP with the test
        // that uses a delta of 17.0123.
        dt = dtConst;
      }
      out.setDelta(dt);
    }
    else {
      // Degenerate case in SVINTERP_TC which results in XD=0.0, which causes
      // follow-on problems, so set to 1.0 in this case.
      out.setDelta(1.0);
    }

    out.setUnits(Units.TIME_S);
    out.setStart(0.0);           // <-- not set in SVINTERP_TC
    out.setTime(times.get(0));   // <-- not set in SVINTERP_TC
    if (!generateTimes ) {
      inTimes.close();
    }
    in.close();
    out.close();
  }

  /** Converts StateVector (BLUE, SP3, KEYWORDS, etc) to a BLUE (Type 5001) file. Since 3.3.2. */
  private void sv2blue5001 () {
    FileName    in         = MA.getFileName("IN");
    DataFile    out        = MA.getDataFile("OUT");
    StateVector sv         = new StateVector(this, in);
    Table       options    = MA.getTable("OPTIONS");
    Table       quals      = out.getQualifiers();
    Double      xd         = quals.getD("XDELTA",    0); // user-specified delta (0=not given)
    Double      size       = quals.getD("SIZE",      0); // user-specified size  (0=not given)
    String      timeName   = quals.getS("TIME_NAME", "NONE"); // Has time column?

    KeyObject.setKeys(sv, options, this, 0);

    // ==== INIT BLUE HEADER (1 of 2) ==========================================
    if (xd   == 0) xd   = null;
    if (size == 0) size = null;

    quals.addIfNotPresent("TYPE",   5001);
    quals.addIfNotPresent("TIME",   sv.getStartTime());
    quals.addIfNotPresent("XSTART", 0);
    quals.addIfNotPresent("XDELTA", 60);
    quals.addIfNotPresent("XUNITS", Units.TIME_S);

    out.open(DataFile.OUTPUT);
    sv.getTransform().toDataFile(out);

    // ==== GENERATE DATA ======================================================
    Time        startTime = out.getTimeAtCurrent();
    Time        endTime   = ((xd==null)||(size==null))? null : new Time(startTime).addSec(size*xd);
    List<Table> entries   = sv.toFileEntries(startTime, endTime, xd, null);

    // ==== INIT BLUE HEADER (2 of 2) ==========================================
    Table outRecDefs = out.getRecordDefs();
    Table svRecDefs  = sv.toFileRecDefs();

    if (outRecDefs.getSize() == 0) {
      if (!timeName.equals("TIME")) svRecDefs.remove("TIME");
      out.setRecordDefs(svRecDefs);
    }

    if ((xd == null) && (entries.size() >= 2)) {
      Time t0 = (Time)entries.get(0).getO("TIME");
      Time t1 = (Time)entries.get(1).getO("TIME");
      out.setXDelta(t1.diff(t0));
    }

    // ==== WRITE DATA =========================================================
    for (Table row : entries) {
      out.writeDataTable(row);
    }

    out.close();
  }

  /** Converts reference LON/LAT/ALT to ECR state-vector. Since 3.3.0. */
  private void lla2ecr () { lla2xxx(Transform.toTransform("ECR")); }

  /** Converts reference LON/LAT/ALT to ECR state-vector. Since 3.3.0. */
  private void lla2eci () { lla2xxx(Transform.toTransform("ECI")); }

  /** Converts reference LON/LAT/ALT to ECR or ECI state-vector. Since 3.3.0. */
  private void lla2xxx (Transform t) {
    Table       lla      = MA.getTable("IN",      new Table());
    Table       options  = MA.getTable("OPTIONS", new Table());
    DataFile    out      = MA.getDataFile("OUT");
    Time        epoch    = MA.getTime("EPOCH", out.getTime());
    double      start    = options.getD("START", epoch.getSec());
    double      startSec = start % 86400;
    double      startDay = start - startSec;
    double      delta    = options.getD("DELTA", 60);
    StateVector sv       = new StateVector();
    int         size     = options.getL("ELEM", (int)(86400/out.getDelta() + 1));

    sv.setTransform(Transform.toTransform("ECR"));

    out.open(DataFile.OUTPUT);
    out.setType(5001);
    out.setRecordDefs(sv.toFileRecDefs());
    out.setTimeCode(startDay);
    out.setStart(startSec);
    out.setDelta(delta);
    out.setSize(size);
    sv.setGeo(lla.getD("ALT"), lla.getD("LAT"), lla.getD("LON"));
    t.setEpoch(epoch);
    t.toDataFile(out);

    for (Table row : sv.toFileEntries(out)) {
      out.writeDataTable(row);
    }
    out.close();
  }
}
