package nxm.sys.intr;

import java.io.FileInputStream;

import nxm.sys.inc.DataTypes;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.Midas;
import nxm.sys.lib.StringUtil;

/**
  Modifies the contents of a Midas file header.

  @author Jeff Schoen
*/
public class Headermod extends Intrinsic {

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

  public int process () {

    // Get switches
    boolean create = MA.getState("/CREATE");
    boolean attach = MA.getState("/ATTACH");
    boolean detach = MA.getState("/DETACH");
    boolean view   = MA.getState("/VIEW");

    if (attach) M.error("/ATTACH not implemented yet - see IMPORT/EXPORT");

    // Since 3.5.2: Warn user if user is using invalid /REP instead of DR or HR (BUG 2869)
    // Deprecated: Warning allowed for backwards compatibility (doing nothing). TODO: Error out starting in 3.9.x?
    if (MA.isPresent("/REP")) {
      M.warning("HEADERMOD: /REP=... not supported: use DATAREP= or HEADREP=");
    }

    // Get the file
    DataFile hcb = MA.getDataFile ("FILE");

    if (view) {
      viewHeader(M, hcb);
    }
    else {
      if (detach) hcb.setDetached(1); // since 3.1.3: added support for /DETACH (BUG 2612)
      if (create) hcb.eraseFileName(false);
      else hcb.open (DataFile.INOUT|DataFile.APPEND);

      // Set named fields from arguments
      for (int n=2; n<=MA.numberOf(); n++) {
        String key = MA.getKey(n);
        // Since 3.5.2: Error out if user is using invalid X-Midas tags: REP or T (BUG 2869)
        if (key.equalsIgnoreCase("REP")) {
          M.error("HEADERMOD: REP=... not supported: use DATAREP= or HEADREP=");
        }
        else if (key.equalsIgnoreCase("T")) { // not supporting since it could be either TIME or TYPE
          M.error("HEADERMOD: T=... not supported: use TYPE=");
        }
        else {
          Object value = MA.getO(key);
          KeyObject.setKey (hcb,key,value,M);
        }
      }

      if (create) {
        hcb.open (DataFile.OUTPUT);
        hcb.seek (hcb.size); // insure EOF write
      }

      // update header information
      hcb.update();
      hcb.close();
    }
    return FINISH;
  }

  /** The common header portion. <br>
      <br>
      Format: <code>t:Description</code> <br>
      <br>
      Where <code>t</code> is the type specifier for a numeric type (in caps) or
      <code>nS</code> for an ASCII entry that is <code>n</code> characters
      long or <code>#</code> for a Type 3000 subrecord. <br>
      <br>
      <code>null</code> entries indicate a suggested line break location.
   */
  private static final String[] COMMON_HEADER_DEFINITION
    = { "4S:Header Version",
        "4S:Header Format",
        "4S:Data Format",
        "L:Detached Header",
        null,
        "L:Protection",
        "L:Pipe Mode",
        "L:Ext Header Start",
        "L:Ext Header Size",
        null,
        "D:Data Start",
        "D:Data Size",
        "L:File Type",
        "2S:Data Format",
        null,
        "I:Flag Mask",
        "D:Time Code",
        "I:Inlet Owner",
        "I:Num Outlets",
        null,
        "L:Outlet Asynch Mask",
        "L:Pipe Location",
        "L:Pipe Size (Bytes)",
        "D:Next In Byte",
        "D:Next Out Byte (Cum)",
        null,
        "D:Next Out Byte (Out1)",
        "D:              (Out2)",
        "D:              (Out3)",
        "D:              (Out4)",
        "D:              (Out5)",
        "D:              (Out6)",
        "D:              (Out7)",
        "D:              (Out8)",
        null,
        "L:Keyword Length",
        null,
        "8S:Keywords", /*  0 */
        "8S:Keywords", /*  8 */
        "8S:Keywords", /* 16 */
        "8S:Keywords", /* 24 */
        "8S:Keywords", /* 32 */
        "8S:Keywords", /* 40 */
        "8S:Keywords", /* 48 */
        "8S:Keywords", /* 56 */
        "8S:Keywords", /* 64 */
        "8S:Keywords", /* 72 */
        "8S:Keywords", /* 80 */
        "8S:Keywords", /* 88 */
        "4S:Keywords", /* 92 */
      };

  /** The Type 1000/2000 adjunct header.
      @see #COMMON_HEADER_DEFINITION
  */
  private static final String[] TYPE1000_ADJUNCT_DEFINITION
    = { "D:Abscissa Start",
        "D:Abscissa Delta",
        "L:Abscissa Units",
        "L:Frame Size",
        null,
        "D:Secondary Start",
        "D:Secondary Delta",
        "L:Secondary Units",
        "L:Buffer ID",
      };

  /** The Type 3000 adjunct header.
      @see #COMMON_HEADER_DEFINITION
  */
  private static final String[] TYPE3000_ADJUNCT_DEFINITION
    = { "D:Abscissa Start",
        "D:Abscissa Delta",
        "L:Abscissa Units",
        "L:Num Subrecords",
        null,
        "D:Secondary Start",
        "D:Secondary Delta",
        "L:Secondary Units",
        "L:Record Length",
        null,
        "#:SubRec  1", "#:SubRec  2", "#:SubRec  3", "#:SubRec  4", null,
        "#:SubRec  5", "#:SubRec  6", "#:SubRec  7", "#:SubRec  8", null,
        "#:SubRec  9", "#:SubRec 10", "#:SubRec 11", "#:SubRec 12", null,
        "#:SubRec 13", "#:SubRec 14", "#:SubRec 15", "#:SubRec 16", null,
        "#:SubRec 17", "#:SubRec 18", "#:SubRec 19", "#:SubRec 20", null,
        "#:SubRec 21", "#:SubRec 22", "#:SubRec 23", "#:SubRec 24", null,
        "#:SubRec 25", "#:SubRec 26"
      };

  /** Prints out the header for a file to the terminal. This is useful for debugging file I/O.
      @param midas    The midas context to use.
      @param dataFile The data file to use. (Must be an actual file on the LOCAL disk).
   */
  private static void viewHeader (final Midas midas, final DataFile dataFile) {
    /* Get the "native data" */
    dataFile.open(DataFile.INPUT);
    dataFile.convertHeaderRep(midas.getResults().getS("ENV.OSREP"));
    final byte[] dfHeader = new byte[dataFile.hb.length];
    System.arraycopy(dataFile.hb, 0, dfHeader, 0, dfHeader.length);
    dataFile.close();

    /* The URL of the file. */
    String fName = dataFile.ioh.getURL();

    if (fName.startsWith("file:")) {
      fName = fName.substring(5);
    } else {
      fName = null;
    }

    /* Get the "file data" */
    byte[] fileHeader = new byte[dfHeader.length];

    try {
      final FileInputStream in = new FileInputStream(fName);
      final int bytes = in.read(fileHeader);

      if (bytes != fileHeader.length) {
      in.close();
        throw new Exception("Unable to read header from " + fName);
      }

      in.close();
    } catch(Exception e) {
      fileHeader = null;
    }

    /* Print out the header. */
    int type  = 0; // The file type

    if (fileHeader != null) {
      midas.type("");
      midas.type("Reading File: " + fName);
      midas.type("Using Format: " + toString(fileHeader,  4, "4S"));
      midas.type("Using Type:   " + toString(fileHeader, 48, "L" ));

      try {
        type = Integer.parseInt(toString(fileHeader, 48, "L" ));
      } catch(Exception e) {
        midas.error(e);
      }
    } else {
      midas.type("");
      midas.type("File not found, reading from pipe.");
      type = dataFile.getType();
    }

    /* Common portion */
    midas.type("");
    midas.type("COMMON HEADER       "                          + "  |  "
              + "           FILE AS READ FROM DISK           " + "  |  "
              + "      FILE AFTER BEING READ BY DATAFILE     ");
    print(midas, '-');
    print(midas, COMMON_HEADER_DEFINITION, fileHeader, dfHeader, 0);

    /* Type Specific */
    midas.type("");
    midas.type("");
    midas.type("TYPE " + type + " ADJUNCT   "                  + "  |  "
              + "           FILE AS READ FROM DISK           " + "  |  "
              + "      FILE AFTER BEING READ BY DATAFILE     ");
    print(midas, '-');

    if (type < 1000) {
      midas.warning("Adjunct header info not available for Type " + type
                  + " files");
    } else if (type < 3000) { /* 1000 & 2000 */
      print(midas, TYPE1000_ADJUNCT_DEFINITION, fileHeader, dfHeader, 256);

    } else if (type < 4000) { /* 3000 */
      print(midas, TYPE3000_ADJUNCT_DEFINITION, fileHeader, dfHeader, 256);


    } else {
      midas.warning("Adjunct header info not available for Type " + type + " files");
    }
  }

  /** Gets a type char from a type definition string as used in the header definitions.
      @param type The definition string.
      @return The type character.
  */
  private static char getTypeChar (String type) {
    if (type.indexOf("S") == 1) {
      return 'S';
    }
    else {
      return type.charAt(0);
    }
  }

  /** Gets the number of bytes per element from a type definition string as
      used in the header definitions.
      @param type The type definition string.
      @return The number of bytes per element.
  */
  private static int getBPE (String type) {
    switch (getTypeChar(type)) {
      case 'S':   return (type.charAt(0) - 48);
      case '#':   return 8;
      default:    return Data.getBPS(type.charAt(0));
    }
  }

  /** Prints the header information to the terminal.
      @param midas         The Midas context to use.
      @param definition    The header/adjunct definition array
                           (see {@link #COMMON_HEADER_DEFINITION}).
      @param fileHeader    The file header as read directly from the file.
      @param nativeHeader  The file header as read by {@link DataFile} and
                           then converted to the local byte representation
                           (also done by {@link DataFile}).
      @param index         The index into the header definition to start from.
  */
  private static void print (final Midas  midas,      final String[] definition,
                             final byte[] fileHeader, final byte[] nativeHeader,
                             int index) {
    for (String def : definition) {
      if (def != null) {
        final String[] entry = def.split(":");

        print(midas, entry[1], fileHeader, nativeHeader, index, entry[0]);

        index += getBPE(entry[0]);
      }
      else {
        print(midas, ' ');
      }
    }
  }

  /** Prints a header entry to the terminal.
      @param midas         The Midas context to use.
      @param title         The title of the entry.
      @param fileHeader    The file header as read directly from the file.
      @param nativeHeader  The file header as read by {@link DataFile} and
                           then converted to the local byte representation
                           (also done by {@link DataFile}).
      @param offset        The offset into the header buffer to read from.
      @param type          The type definition string.
   */
  private static void print (final Midas  midas,      final String title,
                             final byte[] fileHeader, final byte[] nativeHeader,
                             final int    offset,     final String type) {
    if (fileHeader != null) {
      print(midas, title,
            bytesToString(fileHeader,   offset, getBPE(type)),
            bytesToString(nativeHeader, offset, getBPE(type)),
            toString(fileHeader,   offset, type),
            toString(nativeHeader, offset, type));
    } else {
      print(midas, title,
            "",
            bytesToString(nativeHeader, offset, getBPE(type)),
            "",
            toString(nativeHeader, offset, type));
    }
  }

  /** Converts a byte array to a string.
      @param buffer  The byte buffer.
      @param offset  The offset into the buffer to read from.
      @param type    The type definition string.
      @return The string conversion of the entry in the buffer.
  */
  private static String toString (final byte[] buffer, final int offset,
                                  final String type) {
    final char basicType = getTypeChar(type);

    switch (basicType) {
      case DataTypes.STRING:
        String str = "'";

        for (int i = 0; i < (type.charAt(0) - 48); i++) {
          char val = (char)buffer[offset + i];
          str += ((val < 0x20) || (val > 0x7E))? '.': val;
        }
        str += "'";

        return str;

      case '#':
        return StringUtil.padRight(
            toString(buffer, offset + 0, "4S")  /* "4S:Name" */
            + "  "
            + toString(buffer, offset + 4, "2S")  /* "2S:Format" */
            + "  "
            + toString(buffer, offset + 6, "I"),  /* "I:Offset" */
            20);

      case DataTypes.DOUBLE: case DataTypes.FLOAT:
        return "";

      case DataTypes.XLONG: case DataTypes.LONG: case DataTypes.INT: case DataTypes.BYTE:
        return toIntStr(buffer, offset, type);

      default:
        throw new RuntimeException("Unknown data type '" + type + "'.");
    }
  }

  /** Converts a byte array to a string for a number. <br>
      <br>
      This is used ONLY from within {@link #toString(byte[],int,String)}.
      @param buffer  The byte buffer.
      @param offset  The offset into the buffer to read from.
      @param type    The type definition string.
      @return The string conversion of the entry in the buffer.
  */
  @SuppressWarnings("fallthrough")
  private static String toIntStr (final byte[] buffer, final int offset,
                                  final String type) {
    final char   basicType = getTypeChar(type);
    final String rep       = new String(buffer, 4, 4);

    long value = 0;

    if (rep.equals("EEEI")) {
      switch(basicType) {
        case DataTypes.XLONG:
          value += (0xFF & buffer[offset + 7]) << 56;
          value += (0xFF & buffer[offset + 6]) << 48;
          value += (0xFF & buffer[offset + 5]) << 40;
          value += (0xFF & buffer[offset + 4]) << 32;
          /* FALLTHROUGH */
        case DataTypes.LONG:
          value += (0xFF & buffer[offset + 3]) << 24;
          value += (0xFF & buffer[offset + 2]) << 16;
          /* FALLTHROUGH */
        case DataTypes.INT:
          value += (0xFF & buffer[offset + 1]) <<  8;
          /* FALLTHROUGH */
        case DataTypes.BYTE:
          value += (0xFF & buffer[offset + 0]) <<  0;
          break;
        default:
          throw new RuntimeException("Unknown data type '" + type + "'.");
      }
    } else if (rep.equals("IEEE")) {
      switch(basicType) {
        case DataTypes.XLONG:
          value += (0xFF & buffer[offset + 4]) << 56;
          value += (0xFF & buffer[offset + 5]) << 48;
          value += (0xFF & buffer[offset + 6]) << 40;
          value += (0xFF & buffer[offset + 7]) << 32;
          /* FALLTHROUGH */
        case DataTypes.LONG:
          value += (0xFF & buffer[offset + 0]) << 24;
          value += (0xFF & buffer[offset + 1]) << 16;
          value += (0xFF & buffer[offset + 2]) <<  8;
          value += (0xFF & buffer[offset + 3]) <<  0;
          break;
        case DataTypes.INT:
          value += (0xFF & buffer[offset + 0]) <<  8;
          value += (0xFF & buffer[offset + 1]) <<  0;
        case DataTypes.BYTE:
          value += (0xFF & buffer[offset + 0]) <<  0;
          break;
        default:
          throw new RuntimeException("Unknown data type '" + type + "'.");
      }
    } else {
      throw new RuntimeException("Unknown data rep '" + rep + "'.");
    }

    return Long.toString(value);
  }

  /** Converts a set of bytes to a readable string.
      @param buffer  The byte buffer.
      @param offset  The offset into the buffer to read from.
      @param bytes   The number of bytes to include.
      @return The string conversion of the entry in the buffer.
  */
  private static String bytesToString (final byte[] buffer,
                                       final int    offset,
                                       final int    bytes) {
    final StringBuilder str = new StringBuilder();

    for (int i = 0; i < bytes; i++) {
      int value = ((int)buffer[i + offset]) & 0xFF;

      if (value < 0x10) {
        str.append('0');
      }

      str.append(Integer.toHexString(value));

      if (((i % 2) == 1) && (i != (bytes - 1))){
        str.append(' ');
      }
    }

    return str.toString();
  }

  /** Prints a header entry to the terminal.
      @param midas       The Midas context to use.
      @param title       The title of the entry.
      @param fileData    The bytes as read from the file.
      @param nativeData  The bytes as read via {@link DataFile}.
      @param fileInfo    The bytes after from the file after conversion.
      @param nativeInfo  The bytes after via {@link DataFile} after conversion.
  */
  private static void print (final Midas  midas,    final String title,
                             final String fileData, final String nativeData,
                             final String fileInfo, final String nativeInfo) {
    StringBuilder str = new StringBuilder(120);

    str.append(StringUtil.padRight(title,      20));
    str.append("  |  ");
    str.append(StringUtil.padRight(fileData,   20));
    str.append("  (");
    str.append(StringUtil.padLeft(fileInfo,  20));
    str.append(")  |  ");
    str.append(StringUtil.padRight(nativeData, 20));
    str.append("  (");
    str.append(StringUtil.padLeft(nativeInfo,  20));
    str.append(")");

    midas.type(str);
  }

  /** Prints a blank header entry to the terminal.
      @param midas  The Midas context to use.
      @param spacer The spacer character to use (usually <code>' '</code> or <code>'-'</code>).
  */
  private static void print (final Midas midas, final char spacer) {
    midas.type(StringUtil.padRight("", 22, spacer)
             + '|'
             + StringUtil.padRight("", 48, spacer)
             + '|'
             + StringUtil.padRight("", 46, spacer));
  }
}
