package nxm.sys.test;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.lib.*;

/** Used for creating test TimeLines.  For testing, a DataFile would have
 *  its TimeLine replaced with one of these.
 *
 *  CODE IS STILL UNDER DEVELOPMENT
 *
 *  @author  Jeanette McCall
 *  @since   NeXtMidas 3.7.2
*/
@InternalUseOnly("Since 3.7.2 creation")
public class TestTimelineCreator {

  /**
   * This class has static methods and is not intended to be instantiated
   */
  private TestTimelineCreator () {
  }

  /**
   * Creates a TimeLine with entries for each element offset specified, or if
   * only one element offset is specified then at that offset interval
   * Main Purpose: Create test files with a specific timeline entry to elements
   *
   * @param alloc            User-set TimeLineLength (TLL),
   * @param delta            normal time delta between elements (in sec/byte)
   * @param bpe              bytes per element
   * @param startTime        Start Time as a Time
   * @param adjustment       amount to shift the Time each timeline entry
   * @param numElements      number of elements
   * @param elemOffsets      element offsets that will have a time code entry
   *                         or interval between entries
   * @return                 created TimeLine
   */
  public static TimeLine timelineByElement (int alloc, double delta, int bpe, Time startTime,
      double adjustment, int numElements, int ... elemOffsets) {
    //System.out.println("TestTimelineCreator.timelineByElement alloc:"+alloc+" delta:"+delta+" bpe:"+bpe+
    //    " startTime:"+startTime+" adjustment:"+adjustment+" numElements:"+numElements+" elemOffsets:"+elemOffsets);
    double tolerance = Math.abs(adjustment)/2;  // ensure adjustment is out of tolerance range
    Time currentTime = new Time(startTime);
    TimeLine timeline = new TimeLine(alloc, delta, tolerance);
    int[] offsets = {};

    if (elemOffsets.length == 1) {
      // create offsets array based ion offset interval
      int entryInterval = elemOffsets[0];
      offsets = createSetOffsets(entryInterval, numElements);
    }
    else if (elemOffsets.length > 1) {
      // elements contains the element offsets
      offsets = elemOffsets;
    }
    addTimeLineEntries(timeline, currentTime, 0, offsets, delta, bpe, adjustment);

    return timeline;
  }

  /**
   * Create a TimeLine from a Table where the keys are the offsets and the
   * values are times in Standard String form
   *
   * @param delta         normal time delta between elements (in sec/byte)
   * @param bpe           bytes per element
   * @param elemTimeTable table in form {"offset_in_elements"="Time_As_String",...}
   * @return              timeline created from table
   */
  public static TimeLine timelineFromTable (double delta, int bpe, Table elemTimeTable) {
    TimeLine timeline = new TimeLine(TimeLine.GROW_AS_NEEDED, delta, 0);
    for (String offset : elemTimeTable) {
      int off = Integer.parseInt(offset);
      Time timelineEntry = new Time(elemTimeTable.getString(offset));
      timeline.setTimeAt(off, bpe, timelineEntry);
    }
    return timeline;
  }

  /**
   * Create a TimeLine from an offset and a Time array
   *
   * @param alloc         User-set TimeLineLength (TLL), -1 = grow as needed
   * @param delta         normal time delta between elements (in sec/byte)
   * @param bpe           bytes per element
   * @param offsets       offsets for timeline
   * @param times         corresponding times for timeline
   * @return              timeline created from arrays
   */
  public static TimeLine timelineFromArrays (int alloc, double delta, int bpe, double[] offsets, Time[] times) {
    if (offsets.length != times.length) throw new IllegalArgumentException("must have equal # of offsets & times");
    TimeLine timeline = new TimeLine(alloc, delta, 0);
    for (int i = 0; i < offsets.length; i++) {
      timeline.setTimeAt(offsets[i], bpe, times[i]);
    }
    return timeline;
  }

  /**
   * Creates a bad TimeLine where not all the offsets are in increasing order
   *
   * @param delta            normal time delta between elements (in sec/byte)
   * @param bpe              bytes per element
   * @param startTime        Start Time
   * @param numElements      number of elements
   * @param entryInterval    interval (in elements) between timeline entries
   * @return                 timeline with offsets out of order
   *                         (UNTIL insertion of missed entry is supported)
   */
  public static TimeLine timelineBadOffset (double delta, int bpe,
      Time startTime, int numElements, int entryInterval) {
    TimeLine timeline = new TimeLine(TimeLine.GROW_AS_NEEDED,delta,TimeLine.DEF_TOLERANCE);
    Time currentTime = new Time(startTime);
    int maxOffset = Math.min(numElements, TimeLine.DEF_ALLOC * entryInterval);
    // create good set of offsets
    int[] offsets = createSetOffsets(entryInterval, maxOffset);
    if (offsets.length >= 2) {
      // swap offsets to create TimeLine with offsets out of order
      int lastIdx = offsets.length -1;
      int lastOffset = offsets[lastIdx];
      int nextLastOffset = offsets[lastIdx-1];
      offsets[lastIdx] = nextLastOffset;
      offsets[lastIdx-1] = lastOffset;
    }
    else {
      throw new MidasException(
          "Not enough elements in file to create Bad Offset Timeline based on entry interval");
    }

    // make adjustment just slightly bigger than the tolerance
    double adjustment = TimeLine.DEF_TOLERANCE * (1.1);

    addTimeLineEntries(timeline, currentTime, 0, offsets, delta, bpe, adjustment);

    return timeline;
  }

  /**
   * Creates a invalid TimeLine where the next to last entry goes back in time
   *
   * @param delta           normal time delta between elements (in sec/byte)
   * @param bpe             bytes per element
   * @param startTime       Start Time as a Time
   * @param numElements     number of elements
   * @param entryInterval   interval (in elements) between timeline entries
   * @return                invalid timeline with entry back in time
   */
  public static TimeLine timelineBadTime (double delta, int bpe, Time startTime, int numElements, int entryInterval) {
    TimeLine timeline = new TimeLine(TimeLine.GROW_AS_NEEDED,delta,TimeLine.DEF_TOLERANCE);
    Time currentTime = new Time(startTime);
    int maxOffset = Math.min(numElements, TimeLine.DEF_ALLOC * entryInterval);
    // create good set of offsets
    int[] offsets = createSetOffsets(entryInterval, maxOffset);
    if (offsets.length < 2) {
      throw new MidasException(
          "Not enough elements in file to create Bad Time Timeline based on entry interval");
    }

    // make adjustment just slightly bigger than the tolerance
    double adjustment = TimeLine.DEF_TOLERANCE * (1.1);

    // fill in all but the last two entries with valid values
    int[] subOffsets = new int[offsets.length-2];
    System.arraycopy(offsets, 0, subOffsets, 0, offsets.length-2);
    addTimeLineEntries(timeline, currentTime, 0, subOffsets, delta, bpe, adjustment);

    // make bad back in time TimeLine entry
    Time badTime = Time.add(currentTime, -adjustment);
    int idxLastOffset = offsets.length - 1; // index of last offset
    timeline.setTimeAt(offsets[idxLastOffset - 1], bpe, badTime, TimeLine.NotifyInvalidEntry.WARNING);
    // make last entry valid and based on previous valid entry
    double deltaSec = (offsets[idxLastOffset] - offsets[idxLastOffset-3]) * delta * bpe;
    currentTime.addSec(deltaSec + adjustment);
    timeline.setTimeAt(offsets[idxLastOffset], bpe, currentTime, TimeLine.NotifyInvalidEntry.WARNING);

    return timeline;
  }

  /**
   * Creates a TimeLine where the next to last entry is greater than or
   * equal to the last time code entry, but is back it time with respect to
   * the time delta between elements.  Timelines of this type are normally
   * invalid.
   *
   * @param delta           normal time delta between elements (in sec/byte)
   * @param bpe             bytes per element
   * @param startTime       Start Time as a Time
   * @param numElements     number of elements
   * @param entryInterval   interval (in elements) between timeline entries
   * @return                timeline containing entry that is back in time
   *                        taking into account dt
   */
  public static TimeLine timelineSuspectTime (double delta, int bpe,
      Time startTime, int numElements, int entryInterval) {
    if (delta*entryInterval <= 1e7) {
      Shell.warning("Cannot creat suspect timeline unless delta*entryInterval > 1e7");
    }
    TimeLine timeline = new TimeLine(TimeLine.GROW_AS_NEEDED,delta,TimeLine.DEF_TOLERANCE);
    Time currentTime = new Time(startTime);
    int maxOffset = Math.min(numElements, TimeLine.DEF_ALLOC * entryInterval);
    // create good set of offsets
    int[] offsets = createSetOffsets(entryInterval, maxOffset);
    if (offsets.length < 2) {
      throw new MidasException(
          "Not enough elements in file to create Bad Time Timeline based on entry interval");
    }

    // make adjustment just slightly bigger than the tolerance
    double adjustment = TimeLine.DEF_TOLERANCE * (1.1);

    // fill in all but the last two entries with valid values
    int[] subOffsets = new int[offsets.length-2];
    System.arraycopy(offsets, 0, subOffsets, 0, offsets.length-2);
    addTimeLineEntries(timeline, currentTime, 0, subOffsets, delta, bpe,
        adjustment);

    // make suspicious back in time with respect it time delta
    //Time badTime = Time.add(currentTime, -adjustment);
    int idxLastOffset = offsets.length - 1; // index of last offset
    timeline.setTimeAt(offsets[idxLastOffset - 1], bpe, currentTime,
        TimeLine.NotifyInvalidEntry.WARNING);
    // make last entry valid and based on previous valid entry
    double deltaSec = (offsets[idxLastOffset] -
        offsets[idxLastOffset-3]) * delta * bpe;
    currentTime.addSec(deltaSec + adjustment);
    timeline.setTimeAt(offsets[idxLastOffset], bpe, currentTime,
        TimeLine.NotifyInvalidEntry.WARNING);

    return timeline;
  }

  /**
   * Creates a TimeLine of fixed size, where the timeline entries are at the
   * specified entry interval
   *
   * NOTE: This version of timelineFixedSize uses the default tolerance value
   *
   * @param delta           normal time delta between elements (in sec/byte)
   * @param bpe             bytes per element
   * @param startTime       Start Time as a Time
   * @param numElements     number of elements
   * @param entryInterval   entry interval for timeline entries
   * @param size            size of Timeline (number of entries)
   * @return                timeline of specified size
   */
  public static TimeLine timelineFixedSize (double delta, int bpe, Time startTime,
      int numElements, int entryInterval, int size) {
    return timelineFixedSize(delta,bpe,startTime,numElements,entryInterval,size,TimeLine.DEF_TOLERANCE);
  }

  /**
   * Creates a TimeLine of fixed size, where the timeline entries are at the
   * specified entry interval
   * Purpose: To be able to control whether the test file has timeline entries
   * that all fall within the circular buffer range and where modifications
   * should be allowed, or whether the test file has certain out-of-range
   * timeline entries that can not be modified
   *
   * Note: This method has not been tested
   *
   * @param delta           normal time delta between elements (in sec/byte)
   * @param bpe             bytes per element
   * @param startTime       Start Time as a Time
   * @param numElements     number of elements
   * @param entryInterval   entry interval for timeline entries
   * @param size            size of Timeline (number of entries)
   * @param tolr            tolerance for determining time hacks
   * @return                timeline of specified size
   */
  public static TimeLine timelineFixedSize (double delta, int bpe, Time startTime,
      int numElements, int entryInterval, int size, double tolr) {
    TimeLine timeline = new TimeLine(TimeLine.GROW_AS_NEEDED,delta,tolr);
    Time currentTime = new Time(startTime);
    int maxOffset = size * entryInterval;
    if (maxOffset > numElements) {
      throw new MidasException("Note enough elements to have "+size+" entries at every other element");
    }

    // create good set of offsets
    int[] offsets = createSetOffsets(entryInterval, maxOffset);

    // make adjustment just slightly bigger than the tolerance
    double adjustment = tolr * (1.1);

    addTimeLineEntries(timeline, currentTime, 0, offsets, delta, bpe, adjustment);

    return timeline;
  }

  /**
   * Creates an array of a given size that can be populated using Data's
   * toArray (Object array, int offset) method.
   * This allows macros to call the timelineByElement method
   * @param size  size of the array
   * @return      array of specified size (initialized to all 0s)
   */
  public static int[] getIntArray (int size) {
    return new int[size];
  }

  // === Helper methods for TimeLine creation =================================

  /**
   * Create an array of offsets to be used as TimeLine offsets at even intervals
   * starting with offset 0
   *
   * @param entryInterval number of elements between TimeLine offsets
   * @param maxOffset     maximum offset (in elements) allowed
   * @return              array of offsets (in elements)
   */
  static int[] createSetOffsets (int entryInterval, int maxOffset) {
    int numOffsets = (int)Math.ceil((double)maxOffset/entryInterval);
    int[] offsets = new int[numOffsets];

    int idx = 0;
    for (int thisOffset = 0; thisOffset < maxOffset;
        thisOffset += entryInterval, idx++) {
      offsets[idx] = thisOffset;
    }
    return offsets;
  }

  /**
   * Add new entries to the TimeLine at the specified offsets.
   * Entries are based on Time of last offset, the time distance
   * to the new offset and the adjustment
   * @param timeline       TimeLine to be supplemented      - INOUT
   * @param lastOffsetTime Time associated with last offset - INOUT
   * @param lastOffset     last offset (in elements)
   * @param offsets        new offset entries to be added
   * @param delta          normal time delta between elements (in sec/byte)
   * @param bpe            bytes per element
   * @param adjustment     amount to skew time at each TimeLine entry
   */
  static void addTimeLineEntries (TimeLine timeline, Time lastOffsetTime,
      int lastOffset, int[] offsets, double delta, int bpe, double adjustment) {
    for (int offset : offsets) {
      double deltaSec = (offset - lastOffset) * delta * bpe;
      if (deltaSec > 0) {
        lastOffsetTime.addSec(deltaSec + adjustment);
      }
      timeline.setTimeAt(offset, bpe, lastOffsetTime, TimeLine.NotifyInvalidEntry.WARNING);
      lastOffset = offset; // adjust what was the last offset
    }
  }

}
