package nxm.dsp.prim;

import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.DataOp;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;

/** Picks multiple peaks from file/pipe.

    <h2>Algorithm (hysteresis = 0)</h2>
    When the hysteresis is 0, a simple iterative algorithm is used. The algorithm is as follows:
    <pre>
      while (not done) {
        while (upward slope) i++;
        logPeak(i);
        while (downward slope) i++;
      }
    </pre>
    In the case of Type 1000 files where any initial downward slope within a transfer length is
    ignored (except the first transfer length of the file). This way the results of the alrogithm
    are independent of the transfer length used.

    <h2>Algorithm (hysteresis &gt; 0)</h2>
    When the hysteresis is non-zero a more involved algorithm is necessary in order to exclude
    "peak" values that fall within the hysteresis. The algorithm has linear growth based on
    the number of peaks being looked for. For each additional peak added, the frame of data will
    need to be iterated over one additional time. Thus finding N peaks on a frame of length FS will
    require N*FS operations (compare this to the algorithm when hysteresis = 0 which requires 1*FS
    operations).
    <pre>
      STAGE 1 - FIND HIGHEST PEAK
          |
       20 +
          |               . &lt;----------- Highest Peak at i = 16
          |              . .  .
          |             .   .. .     .
          |            .        .   . .
       10 +          ..          . .   .
          |   .     .             .     .
          |  . .   .                     .    ..
          | .   . .                       .  .
          |.     .                         ..
          +----+----+----+----+----+----+----+--
          0    5    10   15   20   15   30   35


      STAGE 2 - IF ENOUGH PEAKS HAVE BEEN FOUND, STOP NOTHING
                MORE TO DO, OTHERWISE CONTINUE TO STAGE 3

      STAGE 3 - EXCLUDE ANYTHING WITHIN THE HYSTERESIS (x) AND
                ANYTHING ON A DOWNWARDS SLOPE FROM FROM POINTS
                WITHIN THE HYSTERESIS (s)

          |
       20 +
          |               x                      ---
          |              x x  x                   | Hysteresis
          |             x   xx x     .            v
          | - - - - - -x- - - - x - .-.- - - - - ---
       10 +          .s          s .   .
          |   .     .             s     .
          |  . .   .                     .    ..
          | .   . .                       .  .
          |.     .                         ..
          +----+----+----+----+----+----+----+--
          0    5    10   15   20   15   30   35

          Note: The definition of the downward slope uses a strict
                val[thisPoint] &lt; val[lastPoint] basis.


      STAGE 4 - (a) SPLIT THE FRAME INTO TWO PIECES ON EITHER
                SIDE OF THE PEAK LOCATION, (b) DISCARD THE
                VALUES EXCLUDED IN STAGE 3

          |                       |  .
          |                       | . .
       10 +          .         10 +.   .
          |   .     .             |     .
          |  . .   .              |      .    ..
          | .   . .               |       .  .
          |.     .                |        ..
          +----+----+-            ++----+----+--
          0    5    10             15   30   35


      STAGE 5 - REPEAT THE PROCESS USING EACH OF THE TWO
                "MINI FRAMES" UNTIL ALL PEAKS FOUND.

          Note: Within this class, stages 4a and stage 5 take
                place before stages 3 and 4b. This is done for
                reasons of performance and code-simplicity.
    </pre>

    @author  Ken Smith (initial version)
    @author  J. Czechowski IV (revised version)
    @since NeXtMidas 3.3.1
 */
public class peakpick extends Primitive {
  private static final boolean DEBUG = false;

  private DataFile inFile;       // Input file/pipe
  private DataFile outFileAbs;   // Output file/pipe (peak ABSCISSA locations)
  private DataFile outFileVal;   // Output file/pipe (peak values)
  private Data     inBuf;        // Data buffer used with hi
  private Data     outBufAbs;    // Data buffer used with ho_abs
  private Data     outBufVal;    // Data buffer used with ho_val
  private String   labelTbl;     // Results label for output table
  private String   labelAbs;     // Results label for peak ABSCISSA location (DEPRECATED)
  private String   labelVal;     // Results label for peak value             (DEPRECATED)
  private int      outType;      // ???
  private boolean  is2d;         // Is the input a Type 2000 file?
  private float    padValue;     // Pad value to use if fewer than maxPeaks are found.
  private float    hysteresis;   // Hysteresis to use when maxPeaks > 1
  private long     index;        // Index of current element in the file (0..N-1)
  private double   start;        // Abscissa start
  private double   delta;        // Abscissa delta
  private int      maxPeaks;     // Max number of peaks to find
  private long[]   peakInd;      // Sorted array of peak indicies   (length = maxPeaks)
  private float[]  peakAbs;      // Sorted array of peak abscissas  (length = maxPeaks)
  private float[]  peakVal;      // Sorted array of peak values     (length = maxPeaks)
  private int      peakCount;    // Number of valid entries in peakInd, peakAbs, and peakVal
  private float    findLast;     // Last value in findPeaksX(..)
  private float    findMaxVal;   // Last max value in findPeaksX(..)
  private double   findMaxAbs;   // Last abs value in findPeaksX(..)
  private long     findMaxInd;   // Last ind value in findPeaksX(..)
  private long     findLogged;   // Last ind logged in findPeaksX(..)

  //support for complex data added in 3.3.1 (Bug 2660)
  private boolean    complex;      // whether or not the input data is complex
  private int        cxmode;       // mode to use with complex input
  private final int  M_MAG   = 1;  // MAG mode for complex data
  private final int  M_PHASE = 2;  // PHASE mdoe for complex data
  private final int  M_REAL  = 3;  // REAL mode for complex data
  private final int  M_IMAG  = 4;  // IMAG mode for complex data
  private final int  M_10LOG = 5;  // 10LOG mode for complex data
  private final int  M_20LOG = 6;  // 20LOG mode for complex data
  private final static String modeList = "Mag,Phase,Real,Imag,10Log,20Log"; // Supported complex modes

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

  @Override
  public int open() {
    maxPeaks = MA.getL("MAX_PEAKS");
    inFile   = MA.getDataFile("IN", "1000,2000", "S#,C#", 0); // only allow 1000 or 2000 files of scalar or complex formatMode
    inFile.open();

    int defXfer = 0; // default transfer length
    int typeClass = inFile.getType() / 1000;
    if (typeClass==1) {
      defXfer = 8192;
      is2d    = false;
      outType = 1000;
    }
    else if (typeClass==2) {
      defXfer = 1;
      is2d    = true;
      outType = (maxPeaks==1)? 1000 : 2000;
    }
    xfer       = MA.getL("/TL", defXfer);

    padValue = MA.getF("PAD");  // support for PAD added in 3.3.1 to match X-Midas
    padValue = MA.getF("PAD_VALUE", padValue);  // PAD_VALUE has precedence

    // support for complex data added in 3.3.1 (Bug 2660)
    complex    = (inFile.getFormatMode()=='C') ? true : false;
    if (complex) {
      String arg = MA.getS("CXMODE", "MAG");  // default to MAGNITUDE
      cxmode = Parser.find(modeList,arg,M_MAG,0,Parser.E_WARNING);
    }

    hysteresis = MA.getF("/HYSTER", 0.0f);
    if (!is2d && (hysteresis > 0.0)) {
      // Type 1000 implementation applies hysteresis on pre-set transfer lengths of 8192. It does not
      // properly handle cases where the hysteresis may need to be applied across transfer length
      // boundaries.
      M.deprecate("PEAKPICK: The algorithm used for Type 1000 files with a hysteresis greater than "
                + "0.0 is not accurate and has been deprecated since NeXtMidas 2.5.0.");
      xfer = 8192;
    }
    inBuf      = inFile.getDataBuffer(xfer, Data.FLOAT);
    start      = inFile.getXStart();
    delta      = inFile.getXDelta();
    peakInd    = new long[maxPeaks];
    peakAbs    = new float[maxPeaks];
    peakVal    = new float[maxPeaks];
    peakCount  = 0;
    index      = 0L;
    labelTbl   = MA.getU("TABLE", null);

    // Open our output files, if appropriate
    if (!is2d && (maxPeaks==1)) {
      // Special Case: Output only to result parameters. This does not match X-Midas usage and
      // confuses users since OUT_VAL and OUT_ABS may be used as results names in some cases and
      // file names in other cases. Note that getS(..) below matches old behavior.
      M.deprecate("PEAKPICK: When reading a Type 1000 file with MAX_PEAKS=1, OUT_VAL and OUT_ABS "
                + "are treated as results names. This behavior has been deprecated since NeXtMidas "
                + "2.5.0, see explain file for details.");
      if (MA.find("OUT_VAL")) labelVal = MA.getS("OUT_VAL", null);
      if (MA.find("OUT_ABS")) labelAbs = MA.getS("OUT_ABS", null);
    }
    else {
      outFileAbs = MA.getDataFile("OUT_ABS", inFile, 0);
      outFileAbs.setDataSize(0.0);
      outFileAbs.setSubSize(maxPeaks);
//    outFileAbs.setType(outType); // prior to 3.1.0
      outFileAbs.setDefaultQualifier("TYPE", outType); // BEGIN: since 3.1.0 (allows user to override output type) to test BUG 2518
      Object userSpecifiedType = outFileAbs.getQualifier("TYPE");
      if (Convert.o2l(userSpecifiedType)/1000 != outType/1000) { // warn user if user specified type could be incompatible
        M.warning("specified type["+userSpecifiedType+"] for OUT_ABS= should probably be of type "+outType);
      }                                                 // END: since 3.1.0
      outFileAbs.setFormat("SF");
      outFileAbs.open(BaseFile.OUTPUT|BaseFile.OPTIONAL);
      outBufAbs = new Data("SF", maxPeaks);

      outFileVal = MA.getDataFile("OUT_VAL", inFile, 0);
      outFileVal.setDataSize(0.0);
      outFileVal.setType(outType);
      outFileVal.setFormat("SF");
      outFileVal.setSubSize(maxPeaks);
      outFileVal.open(BaseFile.OUTPUT|BaseFile.OPTIONAL);
      outBufVal = new Data("SF", maxPeaks);
    }
    return NORMAL;
  }

  @Override
  public int process() {
    int status = NORMAL;

    int ngot  = inFile.read(inBuf);
    if (ngot == 0) return NOOP;
    if (ngot <  0) return FINISH;

    int nelem = (is2d)?  inFile.getSubSize() * ngot : ngot;

    // complex data handling added in 3.3.1 for Bug 2660
    if (complex) {
      convertComplexDataBuffer(inBuf, nelem, cxmode);
    }

    float[] fbuf   = inBuf.castF(true);

    if (!is2d) {
      int     offset = 0;

      synchronized (this) {
        if (hysteresis == 0.0) {
          findPeaksX(fbuf, offset, offset+nelem, start, index);           // Excl hysteresis computations
        } else {
          findPeaksD(fbuf, offset+offset, nelem, start, index);           // DEPRECATED
          doOutput();
        }
      }
      start = start + (nelem * delta);
      index = index + nelem;
      inBuf.uncast(fbuf,false);
    }
    else {
      int fs     = inFile.getSubSize();
      int offset = 0;

      for (int frame = 0; frame < ngot; frame++) {
        synchronized (this) {
          offset    = frame * fs;
          peakCount = 0;
          if (hysteresis == 0.0) {
            findPeaksX(fbuf, offset, offset+fs, start, 0L);            // Excl hysteresis computations
          } else {
            findPeaksH(fbuf, offset, offset+fs, start, 0L, maxPeaks);  // With hysteresis computations
          }
          doOutput();
        }
      }

    }

    inBuf.uncast(fbuf,false);

    return status;
  }

  @Override
  public int close() {
    if (findLogged != -1  ) addFindPeak();       // Log last peak for Type 1000 file
    if (inFile     != null) inFile.close();
    if (outFileAbs != null) outFileAbs.close();
    if (outFileVal != null) outFileVal.close();
    if (labelAbs   != null) MR.put(labelAbs, getPeakAbscissa());
    if (labelVal   != null) MR.put(labelVal, getPeakValue());
    if (labelTbl   != null) MR.put(labelTbl, getPeaksTable());

    return NORMAL;
  }

  /** Convert a complex data array to a scalar one.
      @param buf The buffer to convert
      @param nelems The number of elements
      @param cxmode The type of data conversion (def=MAG)
      @since NeXtmidas 3.3.1
   */
  private void convertComplexDataBuffer (Data buf, int nelems, int cxmode) {
    switch (cxmode) {
      case  M_MAG   : DataOp.mag     (buf, buf, nelems, DataOp.CS); break;
      case  M_PHASE : DataOp.phase   (buf, buf, nelems, DataOp.CS); break;
      case  M_REAL  : DataOp.real    (buf, buf, nelems, DataOp.CS); break;
      case  M_IMAG  : DataOp.imag    (buf, buf, nelems, DataOp.CS); break;
      case  M_10LOG : DataOp.dblogb  (buf, buf, nelems, DataOp.CS); break;
      case  M_20LOG : DataOp.dblogb2 (buf, buf, nelems, DataOp.CS); break;
      default       : DataOp.mag     (buf, buf, nelems, DataOp.CS); break;
    }
  }

  /** Output to files and results. */
  private void doOutput() {
    int numOut = (outType == 2000)? 1 : maxPeaks;

    // Clear out slots for peaks not found
    for (int i = peakCount; i < maxPeaks; i++) {
      peakInd[i] = -1L;
      peakAbs[i] = padValue;
      peakVal[i] = padValue;
    }

    if ((outFileAbs != null) && outFileAbs.isOpen()) {
      outBufAbs.fromArray(peakAbs);
      outFileAbs.write(outBufAbs, numOut);
    }
    if ((outFileVal != null) && outFileVal.isOpen()) {
      outBufVal.fromArray(peakVal);
      outFileVal.write(outBufVal, numOut);
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // PUBLIC METHODS
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Gets a table containing all peak values. The table will have entries <code>PEAK_0</code> through
      <code>PEAK_N</code> (where N = {@link #getPeakCount()}-1). The choice of 0-based counting matches
      the 0-based indexing used for the output files.
      <pre>
        PEAK_0={INDEX=150,ABSCISSA=300,VALUE=500}   (highest peak)
        PEAK_1={INDEX=100,ABSCISSA=200,VALUE=450}
        PEAK_2={INDEX=200,ABSCISSA=400,VALUE=200}
      </pre>
      @return A table describing the peaks found.
   */
  public Table getPeaksTable () {
    Table tbl = new Table();

    for (int i = 0; i < peakCount; i++) {
      Table t = new Table();
      tbl.put("PEAK_"+i,t);
      t.put("INDEX",    peakInd[i]);
      t.put("ABSCISSA", peakAbs[i]);
      t.put("VALUE",    peakVal[i]);
    }
    return tbl;
  }

  /** Gets the index of the i'th peak. Note that peaks are sorted such that <code>i=0</code>
      is the highest peak and <code>i={@link #getPeakCount()}-1</code> is the lowest peak recorded.
      @param  i The peak number.
      @return The index of the peak (0..N-1). This is the frame index for a Type 2000 input file
              or the file index for a Type 1000 input. Returns -1 if no such peak was found.
      @since NeXtMidas 2.5.0
   */
  public synchronized long getPeakIndex (int i) {
    return (i < peakCount)? peakInd[i] : -1L;
  }

  /** Gets the abscissa value of the i'th peak. Note that peaks are sorted such that <code>i=0</code>
      is the highest peak and <code>i={@link #getPeakCount()}-1</code> is the lowest peak recorded.
      @param  i The peak number.
      @return The abscissa value of the peak (0..N-1). Returns NaN if no such peak was found.
      @since NeXtMidas 2.5.0
   */
  public synchronized double getPeakAbscissa (int i) {
    return (i < peakCount)? peakAbs[i] : Double.NaN;
  }

  /** Gets the numeric value of the i'th peak. Note that peaks are sorted such that <code>i=0</code>
      is the highest peak and <code>i={@link #getPeakCount()}-1</code> is the lowest peak recorded.
      @param  i The peak number.
      @return The numeric value of the peak (0..N-1). Returns NaN if no such peak was found.
      @since NeXtMidas 2.5.0
   */
  public synchronized double getPeakValue (int i) {
    return (i < peakCount)? peakVal[i] : Double.NaN;
  }

  /** Gets the index of the highest peak. This is the same as <code>getPeakIndex(0)</code>.
      @return The index of the highest highest peak or -1 if no such peak was found.
      @since NeXtMidas 2.5.0
   */
  public final long getPeakIndex () {
    return getPeakIndex(0);
  }

  /** Gets the abscissa value of the highest peak. This is the same as <code>getPeakAbscissa(0)</code>.
      @return The abscissa value of the highest peak or NaN if no such peak was found.
      @since NeXtMidas 2.5.0
   */
  public final double getPeakAbscissa () {
    return getPeakAbscissa(0);
  }

  /** Gets the numeric value of the highest peak. This is the same as <code>getPeakValue(0)</code>.
      @return The numeric value of the highest peak or NaN if no such peak was found.
      @since NeXtMidas 2.5.0
   */
  public final double getPeakValue () {
    return getPeakValue(0);
  }

  /** The number of max peaks to search for.
      @return The number of max peaks to search for (1..N).
      @since NeXtMidas 2.5.0
   */
  public int getMaxPeaks () {
    return maxPeaks;
  }

  /** The number of peaks found. This is usually the same as {@link #getMaxPeaks()} but may be lower
      in the case where the frame/file size or hysteresis dictate a lower number of peaks available.
      @return The number of peaks found.
      @since NeXtMidas 2.5.0
   */
  public int getPeakCount () {
    return peakCount;
  }

  /** Output an indented debug message. */
  private void debug (String msg) {
    System.out.println("PEAKPICK: "+msg);
  }

  /** Output an indented debug message. */
  private void debug (int peaks, String msg) {
    String pad = StringUtil.padLeft("",(maxPeaks-peaks)*2);
    System.out.println("PEAKPICK: "+pad+msg);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // METHODS FOR FINDING PEAKS AND ADDING THEM TO THE LIST OF PEAKS.
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Finds the highest peaks in the buffer, used when hysteresis computations are excluded (X).
      @param buf        The input buffer.
      @param offset     The offset into the input buffer to start reading at.
      @param end        The index of the last value in the buffer to read (exclusive). (Number of
                        elements read is equal to end-offset.)
      @param fileOffset The file offset corresponding to the value at buf[0].
   */
  private void findPeaksX (float[] buf, int offset, int end, double start, long fileOffset) {
    if (DEBUG) debug("findPeaksX offset="+offset+" end="+end+" start="+start+" fileOffset="+fileOffset);

    int i = offset;

    if (is2d) {
      findMaxInd = -1;
      findLogged = -1;
    } else if (fileOffset != 0) {
      while ((i+1 < end) && (buf[i+1] < findLast)) {   // Continue downward slope started on last xfer
        i++;
        findLast = buf[i];
      }
    }

    while (i < end) {
      while ((i+1 < end) && (buf[i+1] > findLast)) {   // Continue upward slope
        i++;
        findLast = buf[i];
      }
      findMaxVal = buf[i];
      findMaxAbs = start + (i * delta);
      findMaxInd = fileOffset + i;
      addFindPeak();
      while ((i+1 < end) && (buf[i+1] < findLast)) {   // Continue downward slope
        i++;
        findLast = buf[i];
      }
      i++;
    }
  }

  /** Finds the highest peaks in the Type 1000 file buffer with a hysteresis, deprecated (D).
      Note that this does not properly account for the hysteresis to the left of a peak value
      (only on the right), it is also transfer-length dependent since the counters are reset
      at the beginning of each call.
      @deprecated Since NeXtMidas 2.5.0
   */
  @Deprecated private void findPeaksD (float[] buf, int offset, int end, double abs, long fileOffset) {
    if (DEBUG) debug("findPeaksD offset="+offset+" end="+end+" abs="+abs+" fileOffset="+fileOffset);
    float   maxVal = buf[offset];
    double  maxAbs = abs;
    int     maxInd = 0;
    boolean logged = false;

    for (int i = offset; i < end; i++) {
      float val = buf[i];

      if (val > maxVal) {
        // We're going up a peak, log our position
        maxVal = val;
        maxAbs = abs;
        maxInd = i;
        logged = false;

      } else if (!logged && (maxVal - val > hysteresis)) {
        // We've gone far enough down to declare a hit, any future peak is separate
        addPeak(fileOffset+maxInd, (float)maxAbs, maxVal);
        maxVal = val;
        maxAbs = abs;
        maxInd = i;
        logged = true;

      } else if (logged) {
        // We're just looking for the next peak, make sure our peak
        // logger doesn't hang up at an old high value.
        maxVal = val;
      }
      abs += delta;
    }
  }

  /** Finds the highest peak in the buffer, used when there is a hysteresis (H).
      @param buf        The input buffer.
      @param offset     The offset into the input buffer to start reading at.
      @param end        The index of the last value in the buffer to read (exclusive). (Number of
                        elements read is equal to end-offset.)
      @param fileOffset The file offset corresponding to the value at buf[0].
      @param peaks      The number of peaks to find.
   */
  private void findPeaksH (float[] buf, int offset, int end, double start, long fileOffset, int peaks) {
    if (DEBUG) debug(peaks, " findPeaksH offset="+offset+" end="+end+" start="+start
                           +" fileOffset="+fileOffset+" peaks="+peaks);

    double abscissa = start;
    float  val      = Float.NEGATIVE_INFINITY;
    double abs      = 0.0;
    int    ind      = -1;

    for (int i = offset; i < end; i++) {
      float v = buf[i];

      if (v > val) {
        val = v;
        abs = abscissa;
        ind = i;
      }
      abscissa += delta;
    }
    if (ind != -1) {
      boolean added = addPeak(fileOffset+ind, (float)abs, val);

      if ((peaks > 1) && added) {
        float min = val - hysteresis;
        // If the peak is left of middle, check the right first. If it is right of middle, check the
        // left first. The assumption is that the side with more data to look at is more likely to
        // have additional peaks. Since the recursion is short-circuiting based on the side not
        // having a high enough peak, this will tend to reduce the number of calls we need to make.
        if ((end-index) < (index-offset)) {
          findPeaksL(buf, offset, end, start, fileOffset, peaks-1, ind, min);
          findPeaksR(buf, offset, end, start, fileOffset, peaks-1, ind, min);
        } else {
          findPeaksR(buf, offset, end, start, fileOffset, peaks-1, ind, min);
          findPeaksL(buf, offset, end, start, fileOffset, peaks-1, ind, min);
        }
      }
    }
  }

  /** Finds the highest peak in the buffer that is to the left (L) of the last peak.
      @param buf        The input buffer.
      @param offset     The offset into the input buffer to start reading at.
      @param end        The index of the last value in the buffer to read (exclusive).
      @param fileOffset The file offset corresponding to the value at buf[0].
      @param peaks      The number of peaks to find.
      @param ind        The index of the last peak.
      @param min        The minimum value after applying the hysteresis (min = val - hysteresis).
   */
  private void findPeaksL (float[] buf, int offset, int end, double start, long fileOffset, int peaks, int ind, float min) {
    if (DEBUG) debug(peaks, "findPeaksL ind="+ind+" min="+min);

    end = ind-1;
    while ((end >= offset) && ((buf[end] > min) || (buf[end] < buf[end+1]))) {
      end--;
    }
    findPeaksH(buf, offset, end, start, fileOffset, peaks);
  }

  /** Finds the highest peak in the buffer that is to the right (R) of the last peak.
      @param buf        The input buffer.
      @param offset     The offset into the input buffer to start reading at.
      @param end        The index of the last value in the buffer to read (exclusive).
      @param fileOffset The file offset corresponding to the value at buf[0].
      @param peaks      The number of peaks to find.
      @param ind        The index of the last peak.
      @param min        The minimum value after applying the hysteresis (min = val - hysteresis).
   */
  private void findPeaksR (float[] buf, int offset, int end, double start, long fileOffset, int peaks, int ind, float min) {
    if (DEBUG) debug(peaks, "findPeaksR ind="+ind+" min="+min);

    start += (ind+1 - offset) * delta;
    offset = ind+1;
    while ((offset < end) && ((buf[offset] > min) || (buf[offset] < buf[offset-1]))) {
      offset++;
      start += delta;
    }
    findPeaksH(buf, offset, end, start, fileOffset, peaks);
  }

  /** Adds the most recent peak found with findPeaksX(..). */
  private void addFindPeak () {
    addPeak(findMaxInd, (float)findMaxAbs, findMaxVal);
    findLogged = findMaxInd;
    findMaxInd = -1;
  }

  /** Adds a potential peak to the ordered set of peaks.
      @param ind  The index of the peak.
      @param abs  The abscissa value of the peak.
      @param val  The value of the peak.
      @return true if it was added, false if it was too small to include.
   */
  private boolean addPeak (long ind, float abs, float val) {
    boolean added = true;
    int index = 0;
    for (int i = peakCount-1; (i >= 0) && (index == 0); i--) {
      if (peakVal[i] > val) index = i+1;
    }

    if (index == peakCount) {                   // add to end if space available
      if (index < maxPeaks) {
        peakCount++;
      } else {
        added = false; // too small to fit
      }
    } else if (peakCount < maxPeaks) {          // add entry (shift existing entries)
      int len = peakCount - index;
      System.arraycopy(peakInd, index, peakInd, index+1, len);
      System.arraycopy(peakAbs, index, peakAbs, index+1, len);
      System.arraycopy(peakVal, index, peakVal, index+1, len);
      peakCount++;
    } else {                                    // add entry (shift existing entries, drop smallest)
      int len = maxPeaks - index - 1;
      System.arraycopy(peakInd, index, peakInd, index+1, len);
      System.arraycopy(peakAbs, index, peakAbs, index+1, len);
      System.arraycopy(peakVal, index, peakVal, index+1, len);
    }

    if (added) {
      peakInd[index] = ind;
      peakAbs[index] = abs;
      peakVal[index] = val;
    }
    return added;
  }
}
