package nxm.dsp.test;

import java.util.Arrays;

import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.Primitive;

/** A class to perform the moving average filter (recursive implementation)
 *  @author Jeanette McCall
 *  @since NeXtMidas 3.9.1
 */
@ProvisionalUseOnly("This was created for educational purposes and has been minimally tested")
public class movingaverage extends Primitive {
  private DataFile inFile;              // The input file
  private DataFile outFile;             // The output file
  private Data     dataBuffer;          // The data buffer holding input data
  private Data     outBuffer;          // The data buffer holding output data
  private int      length;              // length of moving average
  private float[]  previous;            // length worth of previous data
  private float    savedOutput;         // either first value or previous from last process
  private int      previousToFill;
  private boolean  debug;

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

  @Override
  public int open() {
    inFile = MA.getDataFile("IN", "1000", "S#", 0); // only allow type 1000 files
    inFile.open();

    outFile = MA.getDataFile("OUT", inFile, 0);
    outFile.open();

    length = MA.getL("LENGTH",101);
    previous = new float[length];
    previousToFill = length-1; // leaving first value as 0

    int xfer = MA.getL("/TL", 1024);
    dataBuffer = inFile.getDataBuffer(xfer,Data.DOUBLE);
    outBuffer = outFile.getDataBuffer(xfer,Data.DOUBLE);

    debug = MA.find("/DEBUG");

    return NORMAL;
  }

  @Override
  public int process() {
    int status  = NORMAL;
    int numRead = inFile.read(dataBuffer);

    if (numRead < 0) {
      status = FINISH; // End of file
    } else if (numRead == 0) {
      status = NOOP;   // No data available, wait for pipe.
    } else {
      int numOutput = performMovingAverage(numRead);
      if (numOutput > 0) outFile.write(outBuffer,numOutput);
    }

    return status;
  }

  /** perform data moving average */
  private int performMovingAverage (int numRead) {
    float[] fbuf = dataBuffer.castF(true);
    float[] outBuf = outBuffer.castF(false);
    int numOutput = numRead;

    int numFilling = Math.min(numRead, previousToFill);
    if (previousToFill > 0) {
      // start up condition - if do not have enough data to populate previous buffer, then no output
      numOutput -= previousToFill;
      if (!fillInitialPreviousBuffer(fbuf, numFilling) || (numOutput==0)) return 0;
    }

    // calculate first output point based on last process cycle's output point
    outBuf[0] = savedOutput + (fbuf[numFilling] - previous[0])/length;
    if (debug) System.out.println("first output:"+outBuf[0]+"= savedOutput:"+savedOutput+" + (fbuf["+numFilling+"] - previous[0]) / length:"+length);

    // Section while still removing saved previous points
    int numPreviousToUse = Math.min(length, numOutput); // can not use up more than the number of points we are outputting
    for (int i = 1; i < numPreviousToUse; i++) {
      outBuf[i] = outBuf[i-1] + (fbuf[i+numFilling] - previous[i])/length;
    }

    // section while completely in new data
    for (int i=length; i<numOutput; i++) {
      outBuf[i] = outBuf[i-1] + (fbuf[i+numFilling] - fbuf[i-length+numFilling])/length;
    }

    // ** get ready for the next process cycler **
    // save off the last output because it is needed to calculate the first output of the next process cycle
    savedOutput = outBuf[numOutput-1];
    // save the last length worth of results to previous
    int numPreviousUnused = length - numPreviousToUse;
    if (numPreviousUnused > 0) { // if we did not use all point previously saved, grab them first
      System.arraycopy(previous, numPreviousToUse, previous, 0, numPreviousUnused);
    }
    System.arraycopy(fbuf, numRead-(length-numPreviousUnused), previous, numPreviousUnused, numPreviousToUse);

    // ready to put values in dataBuffer
    outBuffer.uncast(outBuf, true);
    return numOutput;
  }

  /** One time setup code to place enough points in the "previous" buffer to calculate the first output pt
   *  This keeps the code more consistent with follow-on process cycles
   *
   *  @param fbuf       input data
   *  @param numFilling number of points to place in this buffer
   *  @return true if this buffer has now been filled/initialized
   */
  private boolean fillInitialPreviousBuffer(float[] fbuf, int numFilling) {
    boolean filled = false;
    int startFillIndex = length - previousToFill;
    int fbufIdx=0;
    for (int i = startFillIndex; i < numFilling + startFillIndex; i++) {
      previous[i] = fbuf[fbufIdx++];
    }
    startFillIndex += numFilling;
    previousToFill -= numFilling;
    if (previousToFill == 0) {
      //populate saved last output value
      savedOutput = 0;
      for (int i=1; i < length; i++) {
        // this first savedOutput is artificial and does not need to add in previous[0],
        // as it is always 0 on initial filling to trick out this
        savedOutput += previous[i];
      }
      savedOutput /= length;
      filled = true;
      if (debug) System.out.println("previous buffer initialized - saved output:"+savedOutput+" previous:"+Arrays.toString(previous));
    }
    return filled;
  }

  @Override
  public int close() {
    inFile.close();
    outFile.close();

    return NORMAL;
  }
}
