package nxm.sys.intr;

import java.util.ArrayList;

import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.Keywords;
import nxm.sys.lib.TextFile;
import nxm.sys.lib.Time;
import nxm.sys.lib.TimeLine;

/** Used for testing timeline issues. Outputs a file with a time line.
    <pre>
      IN=      - Input file (required)
      OUT=     - 5000 Output file containing (byte offset, time, entry) (optional)
      CSVOUT=  - CSV Output file containing (byte offset, time, entry) (optional)
      REPORT=  - Text Report File (optional)
      RESULT=  - name of result for pass/fail boolean (default is TL_VALIDATOR_RESULT)
    </pre>
    @author  Jeanette McCall
    @since   NeXtMidas 3.9.4
*/
public class Timelineisvalid extends Intrinsic {
  private DataFile inFile;            // Input file/pipe
  private DataFile outFile;           // 5000 Output file/pipe
  private TextFile csvFile;           // CSV Output file/pipe
  private TextFile reportFile;        // Report
  private String   resultsName;       // name for boolean result
  private boolean  making5000;        // Are we creating a 5000 file containing timeline stats
  private boolean  makingCSV;         // Are we creating a CSV file containing timeline stats
  private boolean  makingReport;      // Are we making a TextFile report?
  private boolean  validTimeline;     // Is this timeline valid so far
  private Data     outData;           // Output data table
  private String   rframe = "ECR";    // Reference frame
  private double   tdelta;            // File time: delta
  private double   bpe;               // bytes per element
  private double   tolr;              // TimeLine tolr
  private double   size;              // Size of file
  private long     offset;            // current offset
  private int      correctFirstOffset; // 0 for uSec TIMELINE, 1 for TC high precision
  private ArrayList<Double> offsets;
  private ArrayList<Time> times;
  private ArrayList<String> timeStrings;
  private static final String BASE_SAMPLE_NAME = "TCSAMPLE_";
  private static final String BASE_TC_WHOLE    = "TC_WHOLE_";
  private static final String BASE_TC_FRAC     = "TC_FRAC_";
  private static final String DEFAULT_RES_NAME = "TL_VALIDATOR_RESULT";
  private static final int    DECIMAL_PLACES   = 12;  // for STD time strings
  private static final char timeStrMode = '4';     // Data mode for TIMESTR field in 5000 file
  private static final char expTimeStrMode = '4';  // Data mode for EXP_TIMESTR field in 5000 file
  private static final char nameMode = '4';        // Data mode for NAME field in 5000 file
  private static final char noteMode = '6';        // Data mode for NOTE field in 5000 file
  private static final char textType = 'A';        // Data type for all string fields on 5000 file

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

  public int open() {
    offsets = new ArrayList<>();
    times = new ArrayList<>();
    timeStrings = new ArrayList<>();
    validTimeline  = true; // assume good as default
    inFile     = MA.getDataFile("IN");
    inFile.open();
    Time startTime = inFile.getTimeAtStart();
    tdelta = inFile.getDelta();
    bpe = inFile.getBPE();

    int offNum = 1;
    Keywords kw = inFile.getKeywordsObject();
    Object offObj = kw.get(BASE_SAMPLE_NAME+offNum);
    if (offObj != null ) {
      tolr = TimeLine.QUARTER_NANOSECOND_TOLERANCE;
      correctFirstOffset = 1;
      while (offObj != null) {
        String wholeKey = BASE_TC_WHOLE+offNum;
        Object wholeObj = kw.get(wholeKey);
        String fracKey  = BASE_TC_FRAC+offNum;
        Object fracObj = kw.get(fracKey);
        //System.out.println("wholeKey:"+wholeKey+" wholeObj"+wholeObj+"   fracKey:"+fracKey+" fracObj"+fracObj);
        double offset = Double.valueOf(offObj.toString());
        double whole  = Double.valueOf(wholeObj.toString());
        double frac   = Double.valueOf(fracObj.toString());
        Time   offsetTime = new Time(whole,frac);
        offsets.add(offset);
        times.add(offsetTime);
        timeStrings.add(offsetTime.toString("STD",DECIMAL_PLACES));
        offNum++;
        offObj = inFile.getKeyWord(BASE_SAMPLE_NAME+offNum);
      }
    }
    else {
      Data tld = (Data)kw.get("TIMELINE");
      correctFirstOffset = 0;
      if (tld != null ) {
        int j=0;
        tdelta = tld.getD(j++);
        tolr   = tld.getD(j++);
        int size = (tld.size*tld.spa-2)/2;
        for (int i=0; i<size; i++) {
          double byteOff = tld.getD(j++);
          double elemOff = byteOff/bpe;
          offsets.add(elemOff);
          double dtime = tld.getD(j++);
          Time offsetTime = new Time(dtime);
          times.add(offsetTime);
          timeStrings.add(offsetTime.toString("STD",DECIMAL_PLACES));
        }
      }
    }

    making5000 = MA.find("OUT");
    if (making5000) {
      int size = offNum - 1;
      outFile = MA.getDataFile("OUT");
      outFile.setComponents("POS/VD/6/5,ENTRY/SL/0/0,ELEM/SD/0/0,TIME/SD/0/0,TIMESTR/"+timeStrMode+"A/0/0,EXP_TIME/SD/0/0,EXP_TIMESTR/"+
        expTimeStrMode+"A/0/0,ACT_TDELTA/SD/0/0,FILE_TDELTA/SD/0/0,TOLR/SD/0/0,NAME/"+nameMode+"A/0/0,NOTE/"+noteMode+"A/0/0");
      outFile.setSize(size);
      outFile.open(BaseFile.OUTPUT);
      outFile.setTime(startTime);
      outFile.setDelta(tdelta);
      outFile.setReferenceFrame(rframe);
      outData = outFile.getDataBuffer(1);
    }

    makingCSV = MA.find("CSVOUT");
    if (makingCSV) {
      String csvName = MA.getCS("CSVOUT");
      if (csvName.indexOf('.')==-1) csvName += ".csv";
      csvFile = new TextFile(M, csvName);
      csvFile.open(BaseFile.OUTPUT);
      csvFile.writeln("ENTRY,ELEM,TIME,TIMESTR,EXP_TIME,EXP_TIMESTR,ACT_TDELTA,FILE_TDELTA,TOLR,NAME,NOTE");
    }

    makingReport = MA.find("REPORT");
    if (makingReport) {
      reportFile = MA.getTextFile("REPORT");
      reportFile.open(BaseFile.OUTPUT);
      reportFile.writeln("Timeline Keywords Report for "+inFile.fn);

      if (!offsets.isEmpty() && (offsets.get(0) > correctFirstOffset)) {
        validTimeline = false;
        reportFile.writeln("");
        if (correctFirstOffset == 1) {
          reportFile.writeln("BAD INITIAL OFFSET: TCSAMPLE_1 should always be 1, but was "+offsets.get(0));
        }
        else {
          reportFile.writeln("BAD INITIAL OFFSET: TIMELINE(2) should always be 0, but was "+offsets.get(0));
        }
        reportFile.writeln("This indicates a wrapped timeline. Time hacks were lost and only the last ones remain.");
        reportFile.writeln("This gives the file a 'start time' of 0, even though 'abscissa start' indicates a start time of " +
            Time.toString(inFile.getStart(),"STD",DECIMAL_PLACES));
        // **** see of Auto correction would have helped ****
        double effectiveDelta = calcEffectiveDelta(0);
        // CoreIO auto-correct does not help if already gone past first TC offset time with file tdelta
        String tdeltaMsg = "    file header tdelta:"+tdelta+", effective tdelta to first TC entry:"+effectiveDelta;
        if (inFile.isAutoCorrectEnabled()) {
          reportFile.writeln("Auto-correction is currently on, so the first element will be assigned the abscissa start value");
        }
        else {
          reportFile.writeln("Auto-correction is currently off, so all the elements up to the first time hack will be assigned a time of 0");
          reportFile.writeln("Turn off CoreIO.IOOptionsDisableTimeLineAutoCorrectNoOffset0 to enable auto-correct");
        }
        reportFile.writeln(tdeltaMsg);
      }
    }

    // get name of primitive boolean pass/fail result
    resultsName = MA.getS("RESULT",DEFAULT_RES_NAME);
    if (resultsName.trim().isEmpty()) resultsName = DEFAULT_RES_NAME;

    return NORMAL;
  }

  public int process() {
    int    status = NORMAL;

    double lastOffset = 0;
    Time   lastTime   = new Time (0);
    int timeStrLen = Data.getBPA(""+timeStrMode+textType);
    int expTimeStrLen = Data.getBPA(""+expTimeStrMode+textType);
    int nameLen = Data.getBPA(""+nameMode+textType);
    int noteLen = Data.getBPA(""+noteMode+textType);

    for (int i = 0; i < offsets.size(); i++) {
      double thisOffset = offsets.get(i);
      Time   thisTime   = times.get(i);

      boolean badOffset = thisOffset < lastOffset;
      boolean badTime   = thisTime.compareTo(lastTime)   < 0;
      boolean badEntry  = badOffset || badTime;

      int entry = i+1;                                                  // ENTRY - Z (Beginning of packet)
      double elem = thisOffset;                                         // ELEM (offset) - Y
      double time = thisTime.getSec();                                  // TIME (Current time in seconds) - X
      String timeStr = timeStrings.get(i);                              // TIMESTR (Current time in STD format)
      Time expectedTime = calcExpectedTime(i);                          // EXP_TIME (Expected time in seconds)
      String expTimeStr = expectedTime.toString(DECIMAL_PLACES);        // EXP_TIMESTR (Expected time in STD format)
      double effectiveDelta = calcEffectiveDelta(i);                    // ACT_TDELTA (Effective tdelta since last hack)
      String name = timeStrings.get(i);                                 // NAME
      String note = (!Double.valueOf(effectiveDelta).equals(Double.NaN)) ? "TC based tdelta:"+effectiveDelta :
        "Header tdelta:"+tdelta;                                        // NOTE

      validTimeline &= !badEntry;

      if (makingReport && badEntry) {
        reportFile.writeln("");
        reportFile.writeln("INVALID TC ENTRY "+(i+2)+" : Byte offsets and sample times must be >= previous entry");
        reportFile.writeln("    offset["+(i+1)+"]:"+lastOffset+" time["+i+"]"+lastTime);
        reportFile.writeln("    offset["+(i+2)+"]:"+thisOffset+" time["+(i+1)+"]"+thisTime);
        if (badOffset) reportFile.writeln("    INVALID OFFSET");
        if (badTime)   reportFile.writeln("    INVALID TIME");
      }

      if (making5000) {
        // Columns: POS,ENTRY,ELEM,TIME,TIMESTR,EXP_TIME,EXP_TIMESTR,ACT_TDELTA,FILE_TDELTA,TOLR,NAME,NOTE
        outData.packD(0, entry);                                        // ENTRY (Z-POS)
        outData.packD(8, elem);                                         // ELEM (Y-POS)
        outData.packD(16, time);                                        // TIME (X-POS)
        outData.packL(24, entry);                                       // ENTRY
        outData.packD(28, elem);                                        // ELEM
        outData.packD(36, time);                                        // TIME
        outData.packS(44, timeStrLen, timeStr);                         // TIMESTR
        outData.packD(76, expectedTime.getSec());                       // EXP_TIME
        outData.packS(84, expTimeStrLen, expTimeStr);                   // EXP_TIMESTR
        outData.packD(116, effectiveDelta);                             // ACT_TDELTA
        outData.packD(124, tdelta);                                     // FILE_TDELTA
        outData.packD(132, tolr);                                       // TOLR
        outData.packS(140, nameLen, name);                              // NAME
        outData.packS(172, noteLen, note);                              // NOTE

        outFile.write(outData);
      }

      if (makingCSV) {
        // Columns: ENTRY,ELEM,TIME,TIMESTR,EXP_TIME,EXP_TIMESTR,ACT_TDELTA,FILE_TDELTA,TOLR,NAME,NOTE
        csvFile.write(""+(i+1)+",");                                    // ENTRY
        csvFile.write(""+thisOffset+",");                               // ELEM
        csvFile.write(""+thisTime.getSec()+",");                        // TIME
        csvFile.write(""+timeStrings.get(i)+",");                       // TIMESTR
        csvFile.write(""+expectedTime.getSec()+",");                    // EXP_TIME
        csvFile.write(""+expectedTime.toString(DECIMAL_PLACES)+",");    // EXP_TIMESTR
        csvFile.write(""+effectiveDelta+",");                           // ACT_TDELTA
        csvFile.write(""+tdelta+",");                                   // File_TDELTA
        csvFile.write(""+tolr+",");                                     // TOLR
        csvFile.write(""+name+",");                                     // NAME
        csvFile.writeln("\""+note+"\"");                                // NOTE
      }
      lastOffset = thisOffset;
      offset++;
    }

    return (offset < size)? status: FINISH;
  }

  private Time calcExpectedTime (int index) {
    Time expectedTime = times.get(index);
    if (index == 0) {
      if (offsets.get(0) > correctFirstOffset) {
        double expectedElapsedTime = (offsets.get(index) - correctFirstOffset) * tdelta;
        expectedTime = Time.add(inFile.getStart(), expectedElapsedTime);
      }
    }
    else {
      double expectedElapsedTime = (offsets.get(index) - offsets.get(index-1)) * tdelta;
      expectedTime = Time.add(times.get(index-1), expectedElapsedTime);
    }
    return expectedTime;
  }

  private double calcEffectiveDelta (int index) {
    double effectiveDelta = tdelta;
    if (index == 0) {
      if (offsets.get(0) > correctFirstOffset) {
        effectiveDelta = (times.get(index).getSec() - inFile.getStart()) / (offsets.get(0) - correctFirstOffset); // 0 or 1-based offsets
      }
    }
    else {
      effectiveDelta = times.get(index).diff(times.get(index - 1)) /
          (offsets.get(index) - offsets.get(index - 1));
    }
    return effectiveDelta;
  }

  public int close() {
    if (inFile != null) inFile.close();
    if (outFile != null) outFile.close();
    if (csvFile != null) csvFile.close();
    if (reportFile != null) {
      if (validTimeline) {
        reportFile.writeln("PASSED TIMELINE VERIFICATION CHECK");
      }
      reportFile.close();
    }
    MR.putx(resultsName, validTimeline);
    return NORMAL;
  }

  /** Returns the csv column names
      @return csv column names
   */
  public static String getCSVColumnNames () {
    return "COLUMNNAMES=\"L:ENTRY,D:ELEM,D:TIME,TIMESTR,D:EXP_TIME,EXP_TIMESTR,D:ACT_TDELTA,D:FILE_TDELTA,D:TOLR,NAME,NOTE\"";
  }
}

