package nxm.sys.intr;

import static java.lang.Math.abs;
import static java.lang.Math.rint;
import static nxm.sys.inc.Installation.ApplyBugFix.KEYWORD_FSET_INTO_EXTENDED_ONLY;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import nxm.sys.inc.ListFile;
import nxm.sys.inc.Units;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.FileName;
import nxm.sys.lib.ICEPacket;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.Keywords;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StateVector;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.Table.Iterator;
import nxm.sys.lib.Terminal;
import nxm.sys.lib.TextFile;
import nxm.sys.lib.Time;
import nxm.sys.lib.Transform;

/** Manipulates contents of a Midas file's keyword block.

    @author Jeff Schoen / Jay Hallam / J. Czechowski IV
 */
public class Keyword extends Intrinsic {
  private static final boolean OLD_LISTING_FORMAT = false;

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

  @Override
  @SuppressWarnings("deprecation")  // for M.terminal - since 3.7.1 for NXM-2813
  public int process() {
    boolean warn = MA.getState("/WARN", true);
    // Set the default value based on the CoreIO option. Default is true, if the coreIO option is set, it will be false. Since 3.9.1
    boolean flat = MA.getState("/FLAT", !(Shell.getMidasContext().io.getOptions().indexOf("SetKeywordDefaultFlatValueToFalse") > -1));

    // function to perform
    String origFunc = MA.getU("FUNC");
    String func     = origFunc.replace("SET", "PUT");

    // Open the file
    DataFile hcb = MA.getDataFile ("FILE");
    if ((func.indexOf("PUT")>=0) || (func.indexOf("ADD")>=0) || (func.indexOf("DEL")==0)) {
      hcb.open(DataFile.INOUT|DataFile.HEADERONLY);
    } else {
      hcb.open(DataFile.INPUT|DataFile.HEADERONLY);
    }
    if (!hcb.isOpen) return FINISH;

    // set the scope
    String scope = MA.getU("/SCOPE");
    if (scope.length()>0) {
      if (hcb.keywords.setScope(scope)==0) {
        M.error("Could not set scope to "+scope);
      }
    }

    // set the Keywords flags (since 3.1.1)
    boolean hasFlags = MA.isPresent("/FLAGS");
    if (hasFlags) {
      hcb.keywords.setFlags(MA.getU("/FLAGS"));
    }
    boolean useOldListing = MA.getState("/LEGACYLIST");       // since 3.5.4: use old (pre 3.3.0) listing format which also does paging
    if (useOldListing && func.equals("LIST")) {               // display keywords to terminal (old/legacy list format)
      if (!hasFlags) hcb.keywords.setFlags(Keywords.NOABORT); // since 3.1.1 (try to continue pass bad keyword blocks)
      String name = MA.getS("NAME",null);
      M.more(Terminal.INIT);  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
      for (Keywords.Iterator kwi=hcb.keywords.iterator(); kwi.getNext() && M.more();) {
        if (name==null || StringUtil.isWildcardMatch(name,kwi.name)) M.more( kwi.list() );
      }
      M.more(Terminal.EXIT);
    }
    else if (func.equals("LIST") || func.startsWith("LIST/")) { // display sorted keywords to terminal
      // This is the updated format that supports sorting and "pretty printing"
      // which was added in NeXtMidas 3.3.0 for BUG 2692.
      if (!hasFlags) hcb.keywords.setFlags(Keywords.NOABORT); // since 3.1.1 (try to continue pass bad keyword blocks)
      String  filter = MA.getS("NAME",null);
      boolean sort   = func.contains("/S");
      int mainKeywordsListed = 0;
      int keywordsListed     = 0;

      M.more(Terminal.INIT);  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
      if ((scope == null) || scope.isEmpty() || scope.equals("ALL")) {
        // MAIN HEADER
        // since 3.7.1 using M.println instead of M.terminal.writeln to allow logging - NXM-1813
        M.println("==== MAIN HEADER ==================================");
        for (Map.Entry<String,Object> kwd : hcb.keywords.getAll("MAIN", sort)) {
          if (filter==null || StringUtil.isWildcardMatch(filter,kwd.getKey())) {
            M.println(Keywords.list(kwd, "MAIN", 24, Keywords.UNITS_LIST));
            mainKeywordsListed++;
          }
        }

        // EXTENDED HEADER
        // since 3.7.1 using M.println instead of M.terminal.writeln to allow logging - NXM-1813
        M.println("");
        M.println("==== EXTENDED HEADER ==============================");
        for (Map.Entry<String,Object> kwd : hcb.keywords.getAll("EXT", sort)) {
          if (filter==null || StringUtil.isWildcardMatch(filter,kwd.getKey())) {
            M.println(Keywords.list(kwd, "EXT", 24, Keywords.UNITS_LIST));
            keywordsListed++;
          }
        }
      }
      else {
        for (Map.Entry<String,Object> kwd : hcb.keywords.getAll(scope, sort)) {
          if (filter==null || StringUtil.isWildcardMatch(filter,kwd.getKey())) {
            M.println(Keywords.list(kwd, scope, 24, Keywords.UNITS_LIST));
            keywordsListed++;
          }
        }
      }
      M.more(Terminal.EXIT);  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
      // since 3.5.4: display keyword listing statistics
      M.println("");  // since 3.7.1 using M.println instead of M.terminal.writeln to allow logging - NXM-1813
      M.println("==== KEYWORD STATS ================================");
      if ((scope == null) || scope.isEmpty() || scope.equals("ALL")) {  // use M.println instead of M.terminal.write since 3.7.1 for NXM-2813 log interactive cmds
        M.println("Number of keywords listed: MAIN = " + mainKeywordsListed + "  EXT = " + keywordsListed);
      } else {
        M.println("Number of keywords listed: "+scope + " = " + keywordsListed);
      }
      // end since 3.5.4 display keyword listing statistics
    }
    else if (func.equals("GET")) {                 // get a keyword (s) into a result(s)
      int     argCount = 3;
      boolean moreArgs = true;
      while (moreArgs && (argCount <= MA.getNArgs())) {
        String name = MA.getU(argCount); argCount++;
        moreArgs = !name.isEmpty();
        if (moreArgs) {
          String label = MA.getU(argCount); argCount++;
          moreArgs = !label.isEmpty();
          if (moreArgs) {
            if (scope.equals("MAIN")) { MR.put(label,hcb.keywords.getMain(name)); }
            else                      { MR.put(label,hcb.keywords.get(name));     }
          } else {
            M.warning("Unbalanced keyword NAME = '"+name+"'. Missing LABEL pairing for GET");
          }
        }
      }
    }
    else if (func.equals("PUT") ) {  // set a keyword(s) from a result(s)
      insert("PUT",scope,hcb,warn);  // code moved to insert method created in 3.1.3 for Bug 2614
    }
    else if (func.equals("ADD") ) {  // add a keyword from a result
      insert("ADD",scope,hcb,warn);  // code moved to insert method created in 3.1.3 for Bug 2614
    }
    else if (func.startsWith("DEL")) {  // delete a keyword
      String name = MA.getU("NAME");
      if (scope.equals("MAIN")) { hcb.keywords.deleteMain(name); }
      else                      { hcb.keywords.delete(name);     }
    }
    else if (func.equals("FGET")) {    // get keywords into text file
      TextFile tf;
      if (MA.getLength("NAME")>0) {
        tf = MA.getTextFile("NAME");
      } else {
        FileName fn = MA.getFileName("FILE"); fn.setExt("key");
        tf = new TextFile(this,fn);
      }
      tf.open(TextFile.OUTPUT|TextFile.FORCEABORT);
      hcb.keywords.dump(tf);
      tf.close();
    }
    else if (func.equals("FPUT")) {    // put/set keywords from text file
      TextFile tf;
      if (MA.getLength("NAME")>0) {
        tf = MA.getTextFile("NAME");
      } else {
        FileName fn = MA.getFileName("FILE"); fn.setExt("key");
        tf = new TextFile(this,fn);
      }
      tf.open(TextFile.INPUT|TextFile.FORCEABORT);
      hcb.keywords.clear();
      if (MA.getState("/EXTONLY",KEYWORD_FSET_INTO_EXTENDED_ONLY)) hcb.keywords.loadExt(tf);
      else hcb.keywords.load(tf); // since 3.1.2: load into MAIN for = and EXTENDED for == (BUG 2599)
      tf.close();
    }
    else if (func.startsWith("SVGET")) { // Since NeXtMidas 3.3.0
      svget(func, hcb);
    }
    else if (func.startsWith("SVPUT")) { // Since NeXtMidas 3.3.0
      svset(func, hcb);
    }
    else if (func.startsWith("TCGET")) { // Since NeXtMidas 3.3.0
      tcget(func, hcb);
    }
    else if (func.startsWith("TCPUT")) { // Since NeXtMidas 3.3.0
      tcset(func, hcb);
    }
    else if (func.startsWith("TCDEL")) { // Since NeXtMidas 3.9.0
      tcdel(hcb);
    }
    else if (func.startsWith("NTCDEL")) { // Since NeXtMidas 3.9.4
      ntcdel(hcb);
    }
    else if (func.startsWith("TGET")) {  // get keywords into table
      String name = MA.getU("NAME", hcb.getFileName().getTag());  // create the table name, default to Filename
      nxm.sys.lib.Table tb = hcb.keywords.makeTable(flat);  // create the table
      if (func.indexOf("/F") >= 0) {
        tb.toTextFile(MA.getTextFile("NAME"));
      } else {
        MR.put(name, tb);                                   // store the table
      }
    }
    else if (func.startsWith("TPUT")) { // NXM-4094 support adding keywords via a Table
      Table tbl = MA.getTable("NAME");
      for (Iterator ti=tbl.iterator(); ti.getNext(); ) {
        hcb.keywords.put(ti.key,  ti.value);
      }
    }
    else {
      M.warning("Unsupported function: "+origFunc);
    }

    hcb.close();

    return FINISH;
  }

  /** Common code called by PUT/SET and ADD
      @param func "PUT" or "ADD"
      @param scope MAIN or not
      @param hcb The DataFile being updated
      @param warn Result of /WARN
      @since NeXtMidas 3.1.3 (Bug 2614)
   */
  private void insert (String func, String scope, DataFile hcb, boolean warn) {
    int     argCount = 3;
    boolean moreArgs = true;

    while (moreArgs && (argCount <= MA.getNArgs())) {
      String name = MA.getU(argCount); argCount++;
      moreArgs = !name.isEmpty();
      if (moreArgs) {
        Object value = MA.getO(argCount); argCount++;
        moreArgs = (value != null);
        if (moreArgs) {
          if (scope.equals("MAIN")) {
            hcb.keywords.putMain(name,value.toString(), warn);
          }
          else {
            if (func.equals("PUT")) {
              hcb.keywords.put(name,value);
            }
            else if (func.equals("ADD")) {
              hcb.keywords.add(name,value);
            }
            else {
              M.warning("KEYWORD: Invalid FUNC = '"+func+"'. Function must be 'PUT' or 'ADD'.");
            }
          }
        } else {
          M.warning("Unbalanced keyword NAME = '"+name+"'. Missing VALUE pairing for PUT");
        }
      }
    }
  }

  /** Implements the SVGET function. Since NeXtMidas 3.3.0. */
  private void svget (String func, DataFile hcb) {
    StateVector        sv       = new StateVector();
    String             modifier = func.substring(5);
    StateVector.SVMode svMode;

         if (modifier.isEmpty()   ) svMode = null;
    else if (modifier.equals("/E")) svMode = StateVector.SVMode.KeywordsEph;
    else if (modifier.equals("/L")) svMode = StateVector.SVMode.KeywordsLLA;
    else if (modifier.equals("/P")) svMode = StateVector.SVMode.KeywordsPlat;
    else if (modifier.equals("/V")) svMode = StateVector.SVMode.KeywordsVec;
    else throw new MidasException("Expected only /E, /L, /P, or /V given "+modifier);

    sv.setFile(this, hcb.getName(), hcb.getFlags(), svMode);

    Transform t = sv.getTransform();
    ListFile lf = MA.getFile("NAME", ListFile.class);
    boolean  t5 = false; // Special handling for Type 5000 files?
    Time     t0;
    Double   dt = (MA.find("/DT"))? MA.getD("/DT") : null;
    if ((dt != null) && (dt < 0)) dt = null;

    lf.open(BaseFile.OUTPUT|BaseFile.TRUNCATE);

    if (lf instanceof DataFile) {
      DataFile df = (DataFile)lf;
      if (df.getType() < 3000) df.setType(5001);
      if (df.getTypeCodeClass() == 5) {
        t.toDataFile(df); // <-- set quadwords
        t5 = true;
      }
    }
    if (lf.getRecordDefCount() == 0) {
      lf.setRecordDefs(sv.toFileRecDefs());
    }

    List<nxm.sys.lib.Table> rows = (t5 || (dt != null))? sv.toFileEntries(null, null, dt, "ECR")
                                                       : sv.toFileEntries("ECR");
    if (rows.size() < 2) {
      t0 = Time.toTime(rows.get(0).get("TIME"));
      dt = 60.0;
    }
    else {
      Time t1;

      t0 = Time.toTime(rows.get(0).get("TIME"));
      t1 = Time.toTime(rows.get(1).get("TIME"));
      dt = t1.diff(t0);

      Time lastTime = t1;
      for (int i = 2; i < rows.size(); i++) {
        Time   thisTime = Time.toTime(rows.get(i).get("TIME"));
        double delta    = thisTime.diff(lastTime);

        if (abs(dt - delta) > 1e-6) {
          throw new MidasException("StateVector.toFileEntries(..) returned bad data!");
        }
        lastTime = thisTime;
      }
    }

    if (lf instanceof DataFile) {
      DataFile df = (DataFile)lf;
      if (t5) {
        // Type 5000 file: TimeCode = Date, XStart = TimeOfDay
        // (use setTime(..) not setTimeCode(..) to ensure TC_PREC= is correct)
        double wsod = t0.getWSec() % 86400;
        double fsod = t0.getFSec();
        df.setTime(new Time(t0.getWSec() - wsod, 0.0));
        t0 = new Time(wsod, fsod);
      }
      df.setStart(t0.getSec());
      df.setDelta(dt);
      df.setUnits(Units.TIME_S);
      df.setSize(0.0);
    }

    for (int i = 0; i < rows.size(); i++) {
      lf.setData(i, rows.get(i));
    }
    lf.close();
  }

  /** Implements the SVSET function. Since NeXtMidas 3.3.0. */
  private void svset (String func, DataFile hcb) {
    Keywords    keys      = hcb.getKeywordsObject();
    StateVector sv        = new StateVector();
    String      modifiers = func.substring(5);

    if (modifiers.isEmpty()) {
      throw new MidasException("Must specify output format(s) via modifier(s) on SVSET.");
    }

    sv.setFile(this, MA.getFileName("NAME"), 0, null);

    if (modifiers.contains("/E")) keys.putAll(sv.toKeywordsEph());
    if (modifiers.contains("/L")) keys.putAll(sv.toKeywordsLLA(hcb));
    if (modifiers.contains("/P")) keys.putAll(sv.toKeywordsPlat(hcb));
    if (modifiers.contains("/V")) keys.putAll(sv.toKeywordsVec());
  }

  /** Implements the TCGET function. Since NeXtMidas 3.3.0. */
  private void tcget (String func, DataFile hcb) {
    String modifiers = func.substring(5);

    if (modifiers.isEmpty()) {
      throw new MidasException("Must specify format via modifier(s) on TCGET.");
    }
    else if (modifiers.equals("/I/X") || modifiers.equals("/X/I")) {
      tcgetICE(hcb);
    }
    else {
      throw new MidasException("Unsupported format modifier(s) '"+modifiers+"' on TCGET.");
    }
  }

  /** Implements the TCSET function. Since NeXtMidas 3.3.0. */
  private void tcset (String func, DataFile hcb) {
    String modifiers = func.substring(5);

    if (modifiers.isEmpty()) {
      throw new MidasException("Must specify format via modifier(s) on SVSET.");
    }
    else if (modifiers.equals("/I/X") || modifiers.equals("/X/I")) {
      tcsetICE(hcb);
    }
    else {
      throw new MidasException("Unsupported format modifier(s) '"+modifiers+"' on SVSET.");
    }
  }

  /** Implements the TCGET function (X-Midas keywords to ICE packets). Since NeXtMidas 3.3.0. */
  private void tcgetICE (DataFile hcb) {
    // Most of the algorithm in this function is based on the old X-Midas
    // TCKEYS_TO_PKT code which exists in a number of X-Midas option trees.
    Keywords        kwds    = hcb.getKeywordsObject();
    List<ICEPacket> pkts    = new LinkedList<>();

    // READ ALL OF THE KEYWORDS IN =============================================
    ICEPacket lastPkt    = null;
    double    lastSample = 0;
    double    lastTcoff  = 0;
    int       allSize    = 0;
    double    totSize    = 0;
    int       count      = 0;

    for (int i = 1; (kwds.find("TC_WHOLE_"+i) != null); i++) {
      ICEPacket pkt    = new ICEPacket(hcb.getFormat(), 0, 0);
      double    sample = kwds.getD("TCSAMPLE_" +i, 0.0    ); // File sample index (1-based)
      double    wsec   = kwds.getD("TC_WHOLE_" +i, 0.0    ); // Whole seconds for time stamp (J1950)
      double    fsec   = kwds.getD("TC_FRAC_"  +i, 0.0    ); // Fract seconds for time stamp (J1950)
      double    tcoff  = kwds.getD("TC_OFFSET_"+i, 0.0    ); // Time code offset [OPTIONAL]
                count  = kwds.getL("PACKET_"   +i, count+1); // Packet count     [OPTIONAL]

      if (i > 1) {
        // Although sample is 1-based, we can ignore this fact in the below
        // computation since we are always computing the delta between two
        // sample values so the -1's in "(sample-1)" and "(lastSample-1)"
        // cancel out. The sign values on tcoff and lastTcoff are as they
        // are since it is just an expansion of "-(tcoff - lastTcoff)".
        double actSize  = sample - lastSample - tcoff + lastTcoff;
        double residual = totSize - rint(totSize);
        int    size     = (int)rint(actSize + residual);
        double error    = actSize - size;

        lastPkt.setSize(size);
        totSize += actSize;

        if ((error >= 0.0000001) || (error <= -0.0000001)) {
          // Error exceeds typical rounding error
          M.warning("Bad packet size for TCSAMPLE_"+i+" "+actSize+" using "+size);
        }

        if (i    == 2      ) allSize = size;
        if (size != allSize) allSize = -size;
      }
      lastPkt    = pkt;
      lastSample = sample;
      lastTcoff  = tcoff;

      pkt.setCount(count);
      pkt.setChannel(0);     // <-- This matches the X-Midas TCKEYS_TO_PKT
      pkt.setTC(0, 1, tcoff, wsec, fsec);

      pkts.add(pkt);
    }

    if (pkts.size() < 2) {
      // Matches X-Midas usage where output requires a minimum of two entries
      M.warning("Found less than two complete sets of TC keys");
      kwds.clear();
    }
    else if (allSize > 0) {
      lastPkt.setSize(allSize);
    }
    else {
      // Handle inconsistently-sized packets by issuing a warning and assuming
      // the last packet ends at the end of the file. This is differs from the
      // X-Midas version which always assumes consistently-sized packets and
      // computes the size based on the first packet's TCO and TCSAMPLE_2.
      M.warning("Inconsistent packet size");
      lastPkt.setSize((int)rint(hcb.getSize() - totSize));
    }

    // WRITE THE OUTPUT FILE ===================================================
    DataFile out = MA.getDataFile("NAME", "3000", "NH", DataFile.OUTPUT);
    out.open();
    out.setSubRecords(ICEPacket.SUBRECORDS_NORMAL);
    out.setXStart(0.0);
    if (allSize > 0) out.setXDelta(hcb.getXDelta() * allSize);
    out.setXUnits(Units.TIME_S);
    out.setSize(pkts.size());

    for (ICEPacket pkt : pkts) {
      out.write(pkt.buf, 0, ICEPacket.headerLength);
    }
    out.close();
  }

  /** Implements the TCSET function (ICE packets to X-Midas keywords). Since NeXtMidas 3.3.0. */
  private void tcsetICE (DataFile hcb) {
    // Most of the algorithm in this function is based on the old X-Midas
    // PKT_TO_TCKEYS code which exists in a number of X-Midas option trees.

    // READ THE PACKETS IN =====================================================
    DataFile  in         = MA.getDataFile("NAME", "3000", "NH", DataFile.INPUT);
    Keywords  kwds       = hcb.getKeywordsObject();
    ICEPacket pkt        = new ICEPacket();
    int       i          = 1;
    double    lastSample = 0;
    double    lastTCOff  = 0;

    in.open();
    while (in.read(pkt.buf, 0, ICEPacket.headerLength) == ICEPacket.headerLength) {
      Time   tc;
      double tcoff;

      if (pkt.hasSync() && (pkt.getTCStatus() > 0)) {
        tc    = pkt.getTC();
        tcoff = pkt.getTCO();
      }
      else {
        tc    = new Time(0.0, 0.0);
        tcoff = 0.0;
      }

      // JC4: The use of pkt.getUser() in the following computation matches the
      // X-Midas version, but I think it is bogus. However since all the test
      // cases I am familiar with have user=0, I can not say for sure.
      double sample = (i == 1)? tcoff + 1
                              : lastSample + (tcoff - lastTCOff)
                                           + (pkt.getSize() - pkt.getUser());

      kwds.put("D:TC_WHOLE_" +i, tc.getWSec());
      kwds.put("D:TC_FRAC_"  +i, tc.getFSec());
      kwds.put("D:TC_OFFSET_"+i, tcoff);
      kwds.put("D:TCSAMPLE_" +i, sample);

      i++;
      lastSample = sample;
      lastTCOff  = tcoff;
    }
    in.close();
  }

  /** Deletes all TimeLine keywords in the DataFile.
   *  @param hcb  Datafile in which to perform the removal of the TimeLine keywords
   *  @since NeXtMidas 3.9.0
   */
  private void tcdel (DataFile hcb) {
    hcb.open(DataFile.INOUT);
    hcb.deleteTimeLineKeywords();
    hcb.close();
  }

  /** Deletes all Non-TimeLine keywords in the DataFile.
   *  @param hcb Datafile in which to perform the removal of the Non-TimeLine keywords
   *  @since NeXtMidas 3.9.4
   */
  private void ntcdel (DataFile hcb) {
    hcb.open(DataFile.INOUT);
    hcb.deleteNonTimeLineKeywords();
    hcb.close();
  }
}
