package nxm.sys.test.junit.lib;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.regex.Matcher;

import nxm.sys.lib.Midas;
import nxm.sys.lib.Time;
import nxm.sys.lib.TimeLine;
import nxm.sys.lib.TimeLine.NotifyInvalidEntry;
import nxm.sys.test.TestTimelineCreator;
import nxm.sys.test.junit.NMTestCase;

/** This will test the TimeLine class.
    @author  Jeanette McCall
    @since 3.7.3
*/
public class TimeLineTest extends NMTestCase {
  /** public constructor to allow running this test case directly with JUnit.
      @param methodName The name of the method being called.
   */
  public TimeLineTest (String methodName) { this(methodName, null); }

  /** Creates a new instance of the class.
      @param methodName The name of the method being called.
      @param midas      The midas context to use when running the tests.
   */
  public TimeLineTest (String methodName, Midas midas) {
    super(methodName, midas);
  }

  /**
   * Tests EDITING timelines using TimeLine.setAt
   */
  public void testSetAt () {
    //*******************************************************************************************************
    // Testing with normal timeline where first offset is for element 0 and the timeline is NOT full
    //*******************************************************************************************************
    normalTimeLineSetAt();

    //*******************************************************************************************************
    // Atypical timeline where offsets do not start with 1st element, but timeline NOT full
    //*******************************************************************************************************
    notFirstElementTimeLineSetAt();

    //*******************************************************************************************************
    // Wrapped timeline where timeline has overflowed and wrapped
    //*******************************************************************************************************
    wrappedTimeLineSetAt();
  }

  private static Time[] createTimelineTimes (double[] offsets, Time starttime, int numValid, double delta, double shift) {
    if ((offsets) == null || (offsets.length == 0)) return null;
    if ((numValid < 0) || (numValid > offsets.length)) throw new IllegalArgumentException("numValid needs to be in range of array size");
    Time[] times = new Time[offsets.length];
    for (int i = 0; i < numValid; i++) {
      times[i] = Time.add(starttime, (delta+ shift)*offsets[i] );
    }
//    System.out.println("offsets:"+Arrays.toString(offsets));
//    System.out.println("created times:"+Arrays.toString(times));
    return times;
  }

  /**
   * Tests editing typical timelines that have an entry for element 0
   */
  private void normalTimeLineSetAt () {
    // TimeLine - offsets start with 1st element and not full
    double[] offsetsElem0   = {0,2,4,6,8,10,0,0,0};
    int      numValidElem0   = 6;
    Time     starttimeElem0 = new Time("2017:07:17:10:30:00");
    double   deltaElem0     = 0.1;
    double   shiftElem0     = 0.01;
    Time[] timesElem0 = createTimelineTimes(offsetsElem0, starttimeElem0, numValidElem0, deltaElem0, shiftElem0);

    // Overwrite of initial entry (offset 0) not allowed
    TimeLine timeline0a = TestTimelineCreator.timelineFromArrays(offsetsElem0.length, deltaElem0, 1, offsetsElem0, timesElem0);
    double overwriteOffset0Bad = 0;
    Time     overwriteTime0Bad = new Time("2017:07:17:10:29:00");
    testInvalidSetAt (timeline0a, overwriteOffset0Bad, overwriteTime0Bad,
        "Overwriting offsets[0] not allowed when timeline not wrapped");

    // Valid Insertion of offset 1 (between first and second)
    TimeLine timeline0b = TestTimelineCreator.timelineFromArrays(offsetsElem0.length, deltaElem0, 1, offsetsElem0, timesElem0);
    double insertOffset1Good = 1;
    Time     insertTime1Good = new Time("2017:07:17:10:30:00.11");
    double[] expectedOffsets = {0.0, 1.0, 2.0, 4.0, 6.0, 8.0, 10.0, 0.0, 0.0};
    testInsertSetAt(timeline0b, insertOffset1Good, insertTime1Good,
        "Valid insertion of entry at offset 1", 1, expectedOffsets);

    // Invalid Insertion of offset 3 - time too low
    TimeLine timeline0c = TestTimelineCreator.timelineFromArrays(offsetsElem0.length, deltaElem0, 1, offsetsElem0, timesElem0);
    double insertOffset3Low = 3;
    Time     insertTime3Low = new Time("2017:07:17:10:30:00.2");
    testInvalidSetAt (timeline0c, insertOffset3Low, insertTime3Low, "Insertion at 3 too low");

    // Invalid Insertion at 5 - time too high
    TimeLine timeline0d = TestTimelineCreator.timelineFromArrays(offsetsElem0.length, deltaElem0, 1, offsetsElem0, timesElem0);
    double insertOffset5High = 5;
    Time     insertTime5High = new Time("2017:07:17:10:30:00.2");
    testInvalidSetAt (timeline0d, insertOffset5High, insertTime5High, "Insertion at offset 5 too high");

    // Overwrite for offset 6
    TimeLine timeline0e = TestTimelineCreator.timelineFromArrays(offsetsElem0.length, deltaElem0, 1, offsetsElem0, timesElem0);
    double overwriteOffset6Good = 6;
    Time     overwriteTime6Good = new Time("2017:07:17:10:30:00.7");
    testOverwriteSetAt(timeline0e, overwriteOffset6Good, overwriteTime6Good,
        "Valid overwrite of offset 6",3);

    // Invalid Low Overwrite of offset 6
    TimeLine timeline0f = TestTimelineCreator.timelineFromArrays(offsetsElem0.length, deltaElem0, 1, offsetsElem0, timesElem0);
    double insertOffset6Low = 6;
    Time     insertTime6Low = new Time("2017:07:17:10:30:00.43");
    testInvalidSetAt (timeline0f, insertOffset6Low, insertTime6Low, "Overwrite at offset 6 too low");

    // Invalid High Overwrite of offset 6
    TimeLine timeline0g = TestTimelineCreator.timelineFromArrays(offsetsElem0.length, deltaElem0, 1, offsetsElem0, timesElem0);
    double insertOffset6High = 6;
    Time     insertTime6High = new Time("2017:07:17:10:30:00.88");
    testInvalidSetAt (timeline0g, insertOffset6High, insertTime6High, "Overwrite at offset 6 too high");

    //*******************************************************************************************************
    // Full TimeLine - offsets start with 1st element
  //*******************************************************************************************************
    TimeLine timeline0full1a = TestTimelineCreator.timelineFromArrays(numValidElem0, deltaElem0, 1, offsetsElem0, timesElem0);
    // Overwrite of offset 8 with FULL Array
    double overwriteOffset8Good = 8;
    Time     overwriteTime8Good = new Time("2017:07:17:10:30:00.7");
    testOverwriteSetAt(timeline0full1a, overwriteOffset8Good, overwriteTime8Good, "Overwrite 8 allowed with full array",4);

    // Try insert offset 9 with FULL Array
    TimeLine timeline0full1b = TestTimelineCreator.timelineFromArrays(numValidElem0, deltaElem0, 1, offsetsElem0, timesElem0);
    double insertOffset9Full = 9;
    Time     insertTime9Full = new Time("2017:07:17:10:30:00.9");
    testInvalidSetAt(timeline0full1b, insertOffset9Full, insertTime9Full, "Insert a 9 when timeline is full");
  }

  /**
   * Tests editing timelines where there is not an entry for element 0
   * (and the timeline is not wrapped)
   */
  private void notFirstElementTimeLineSetAt () {
    // TimeLine - offsets do not start with 1st element
    double[] offsetsElem10   = {10,12,14,16,18,20,0,0,0};
    int      numValidElem10   = 6;
    Time     starttimeElem10 = new Time("2017:07:17:11:00:00");
    double   deltaElem10     = 0.1;
    double   shiftElem10     = 0.005;
    Time[] timesElem10 = createTimelineTimes(offsetsElem10, starttimeElem10, numValidElem10, deltaElem10, shiftElem10);

    // Invalid Overwrite of offset 10 (first element)
    TimeLine timeline10a = TestTimelineCreator.timelineFromArrays(offsetsElem10.length, deltaElem10, 1, offsetsElem10, timesElem10);
    double insertOffset10Bad = 10;
    Time     insertTime10Bad = new Time("2017:07:17:11:00:01.2");
    testInvalidSetAt(timeline10a, insertOffset10Bad, insertTime10Bad, "Invalid overwrite of first timeline element");

    // Invalid Insertion of offset 5 (before first timeline element)
    TimeLine timeline10b = TestTimelineCreator.timelineFromArrays(offsetsElem10.length, deltaElem10, 1, offsetsElem10, timesElem10);
    double insertOffset5Bad = 5;
    Time     insertTime5Bad = new Time("2017:07:17:11:00:00.52");
    testInvalidSetAt(timeline10b, insertOffset5Bad, insertTime5Bad, "Invalid insertion before timeline range");

    // Valid Insertion of offset 15 (in middle)
    TimeLine timeline10c = TestTimelineCreator.timelineFromArrays(offsetsElem10.length, deltaElem10, 1, offsetsElem10, timesElem10);
    double insertOffset11Good = 11;
    Time     insertTime11Good = new Time("2017:07:17:11:00:01.11");
    double[] expectedOffsets = {10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 0.0, 0.0};
    testInsertSetAt(timeline10c,
        insertOffset11Good, insertTime11Good, "Valid insertion of entry of offset 11", 1, expectedOffsets);
  }

  /**
   * Tests editing wrapped timelines
   */
  private void wrappedTimeLineSetAt () {
    // TimeLine for wrapped timeline- offsets start with 1st element
    double[] offsetsElemWrap   = {0,2,4,6,8,10,12,14,16, 18, 20};
    int      numValidElemWrap1  = 10;
    int      numValidElemWrap3  = 8;
    Time     starttimeElemWrap = new Time("2017:07:17:10:00:00");
    double   deltaElemWrap     = 0.1;
    double   shiftElemWrap     = 0.01;

    //*******************************************************************************************************
    // created "wrapped" offsets and times arrays - 1 element beyond wrap
    //********************************************************************************************************
    Time[] timesElemWrap = createTimelineTimes(offsetsElemWrap, starttimeElemWrap, offsetsElemWrap.length, deltaElemWrap, shiftElemWrap);

    // Insertion at 3 (between oldest and next to oldest element)
    TimeLine timelineWrap1a = TestTimelineCreator.timelineFromArrays(numValidElemWrap1, deltaElemWrap, 1, offsetsElemWrap, timesElemWrap);
    double insertOffset3Good = 3;
    Time     insertTime3Good = new Time("2017:07:17::10:00:00.330");
    double[] expectedOffsets3 = {18.0, 20.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0};
    testInsertSetAt(timelineWrap1a, insertOffset3Good, insertTime3Good,
        "Valid insertion of entry of offset 3 on wrapped timeline", 2, expectedOffsets3);

    // Insertion at 5 (in middle of pre-wrap elements)
    TimeLine timelineWrap1b = TestTimelineCreator.timelineFromArrays(numValidElemWrap1, deltaElemWrap, 1, offsetsElemWrap, timesElemWrap);
    double insertOffset5Good = 5;
    Time     insertTime5Good = new Time("2017:07:17::10:00:00.550");
    double[] expectedOffsets5 = {18.0, 20.0, 4.0, 5.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0};
    testInsertSetAt(timelineWrap1b, insertOffset5Good, insertTime5Good,
        "Valid insertion of entry of offset 5 on wrapped timeline", 3, expectedOffsets5);

    // Invalid Overwrite of offset 5 (oldest element)
    TimeLine timelineWrap1c = TestTimelineCreator.timelineFromArrays(numValidElemWrap1, deltaElemWrap, 1, offsetsElemWrap, timesElemWrap);
    double overwriteOffset2Bad = 2;
    Time     overwriteTime2Bad = new Time("2017:07:17::10:00:00.230");
    testInvalidSetAt(timelineWrap1c, overwriteOffset2Bad, overwriteTime2Bad,
        "Invalid overwrite of oldest entry in wrapped timeline");

    // Insertion at 19 (wrap boundary)
    TimeLine timelineWrap1d = TestTimelineCreator.timelineFromArrays(numValidElemWrap1, deltaElemWrap, 1, offsetsElemWrap, timesElemWrap);
    double insertOffset19Good = 19;
    Time     insertTime19Good = new Time("2017:07:17::10:00:02.0");
    double[] expectedOffsets19 = {19.0, 20.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0};
    testInsertSetAt(timelineWrap1d, insertOffset19Good, insertTime19Good,
        "Valid insertion of entry of offset 19 at wrap boundary on wrapped timeline", 0, expectedOffsets19);


    //*******************************************************************************************************
    // created "wrapped" offsets and times arrays - 3 elements beyond wrap
    //********************************************************************************************************
    // Insertion of offset 19 - (i.e. after wrap)
    TimeLine timelineWrap3a = TestTimelineCreator.timelineFromArrays(numValidElemWrap3, deltaElemWrap, 1, offsetsElemWrap, timesElemWrap);
    double insertOffset19GoodW3 = 19;
    Time     insertTime19GoodW3 = new Time("2017:07:17::10:00:02");
    double[] expectedOffsets19W3 = {16.0, 18.0, 19.0, 20.0, 8.0, 10.0, 12.0, 14.0};
    testInsertSetAt(timelineWrap3a,
        insertOffset19GoodW3, insertTime19GoodW3, "Valid insertion of entry of offset 19 in overwritten section on wrapped timeline",
        2, expectedOffsets19W3);
  }

  /**
   * Used to test invalid inserts/overwrites when editing timeline
   * @param timeline  timeline to edit
   * @param offset    element offset to attempt to insert
   * @param time      offset's Time
   * @param msg       base assert message
   */
  private void testInvalidSetAt (TimeLine timeline, double offset, Time time, String msg) {
    timeline.setEditingEnabled(true);
    Time[] origTimes = timeline.getTimes();
    double[] origOffsets = timeline.getOffsets();
    boolean wasInserted = timeline.setTimeAt(offset, 1,time, NotifyInvalidEntry.NONE);
    //System.out.println(msg+" offsets:"+Arrays.toString(offsetsToMod)+" was inserted:"+wasInserted);
    //System.out.println(msg+" times:"+Arrays.toString(timesToMod));
    assertFalse(msg+" - not inserted/overwritten",wasInserted);
    boolean timeLineUnchanged = Arrays.equals(origOffsets, timeline.getOffsets())
        && Arrays.equals(origTimes, timeline.getTimes());
    assertTrue(msg + " - timeline unchanged",timeLineUnchanged);
  }

  /**
   * Used to test timeline editing where existing offset's time value is overwritten
   * @param timeline  timeline to edit
   * @param offset    element offset to attempt to insert
   * @param time      offset's Time
   * @param msg       base assert message
   * @param idxOffsetCheck index in offsets array that should have overwritten time
   */
  private void testOverwriteSetAt (TimeLine timeline, double offset, Time time, String msg, int idxOffsetCheck) {
    timeline.setEditingEnabled(true);
    double[] origOffsets = timeline.getOffsets();
    boolean wasOverwritten = timeline.setTimeAt(offset, 1,time, NotifyInvalidEntry.WARNING);
//    System.out.println(msg+" offsets:"+Arrays.toString(timeline.getOffsets())+" was overwritten:"+wasOverwritten);
//    System.out.println(msg+" times:"+Arrays.toString(timeline.getTimes()));
    assertTrue(msg+" - timeline entry overwritten",wasOverwritten);
    boolean timeLineExpected = Arrays.equals(origOffsets, timeline.getOffsets()) && time.equals(timeline.getTimes()[idxOffsetCheck], 0.00001);
    assertTrue(msg+" - timeline correct",timeLineExpected);
  }

  /**
   * Used to test timeline editing where missed offset,time pair is inserted
   * @param timeline  timeline to edit
   * @param offset    element offset to attempt to insert
   * @param time      offset's Time
   * @param msg       base assert message
   * @param idxOffsetCheck index in offsets array that should have overwritten time
   * @param expOffsets expected offsets (includes inserted)
   */
  private void testInsertSetAt (TimeLine timeline, double offset, Time time,
      String msg, int idxOffsetCheck, double[] expOffsets) {
    timeline.setEditingEnabled(true);
    int origSize = timeline.getSize();
    int expSize;
    String sizeMsg;
    // Unless the timeline length is maxed out, timeline size should grow by 1
    if (timelineCanGrow(timeline)) {
      expSize = origSize + 1;
      sizeMsg = " - timeline size grows by 1";
    }
    else {
      expSize = origSize;
      sizeMsg = " - wrapped timelines overwrite and stay same size";
    }
    boolean wasInserted = timeline.setTimeAt(offset, 1,time, NotifyInvalidEntry.WARNING);
//    System.out.println(msg+" offsets:"+Arrays.toString(timeline.getOffsets())+" was inserted:"+wasInserted);
//    System.out.println(msg+" times:"+Arrays.toString(timeline.getTimes()));
    assertTrue(msg+" - timeline entry inserted",wasInserted);
    assertEquals(msg+sizeMsg,expSize,timeline.getSize());
    boolean timeLineExpected = Arrays.equals(expOffsets, timeline.getOffsets())
        && time.equals(timeline.getTimes()[idxOffsetCheck], 0.00001);
    assertTrue(msg+" - timeline correct",timeLineExpected);
  }

  /** Tests the deleting of non-timeline keywords
   *  @since NeXtMidas 3.9.4
   */
  public void testNTCDelete() {
    String [] tcKeywords = {"TCSAMPLE_12345","TC_FRAC_1234567","TC_WHOLE_12345","TIMELINE","TIME_DELTA","TIME_EPOCH",
                            "TIME_OFFSET","TIME_TICKS","TIMETAG.OFFSET","TIMETAG.TIME","TIMETAG.TIME.UNITS"};
    String [] ntcKeywords = {"TCSAMPLE_","TC_SAMPLE_123","TCWHOLE_123","TCFRAC_123","TC_WHOLE_","TIMELINE_","TIMEDELTA",
                             "TIME_DELTA_","TIMEEPOCH","TIMEOFFSET","TIMETICKS","TIME_TICKS_123",
                             "TIMETAG.OFFSET.","TIMETAG.TIME.","TIMETAG.TIMEUNITS"};
    ArrayList<String> filteredKeywords = new ArrayList<>();

    for (String key : tcKeywords) {
      Matcher matchTC = TimeLine.TIMELINE_KEYWORD_PATTERN.matcher(key);
      if (!(matchTC.matches())) {
        filteredKeywords.add(key);
      }
    }
    assertTrue(filteredKeywords.size() == 0);

    filteredKeywords.clear();
    for (String key : ntcKeywords) {
      Matcher matchTC = TimeLine.TIMELINE_KEYWORD_PATTERN.matcher(key);
      if (!(matchTC.matches())) {
        filteredKeywords.add(key);
      }
    }
    assertTrue(filteredKeywords.size() == ntcKeywords.length);

    filteredKeywords.clear();
  }

  /**
   * Checks if timeline can contain more elements (bigger 'size')
   * @param timeline timeline to check
   * @return boolean true if timeline can hole more elements (larger 'size')
   */
  private static boolean timelineCanGrow (TimeLine timeline) {
    int timelineAlloc = timeline.getAlloc();
    int maxLength = (timelineAlloc == TimeLine.GROW_AS_NEEDED) ?
        TimeLine.DEF_MAX_ALLOC : timelineAlloc;
    return timeline.getSize() < maxLength;
  }
}
