package nxm.sys.prim;

import nxm.sys.inc.Units;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.Time;
import nxm.sys.libm.Waveform;

/**
  Creates signal waveforms of various shapes.

  @author Jeff Schoen
*/
public final class waveform extends Primitive {

  private int shape, throttle, lrs=1, cur=0;
  private double size, amp, freq, poff, phase, chirp, mean, var, sdev, a2s, start, delta, tstart;
  private boolean dble, exact;	// double precision and exact phase
  private String form;
  private DataFile hcb;
  private Data data;

  /** The waveform shapes this primitive can generate {@value} */
  public  static final String shapeList =
    "Sine,Cosine,Square,Triangle,SawTooth,Pulse,Constant,Zero,White,LRS,Ramp";
  private static final int SIN=1,COS=2,SQU=3,TRI=4,SAW=5,PUL=6,CON=7,ZER=8,WHI=9,LRS=10,RAM=11;

  /** The throttle mode {@value}.
      FULL - the primitive runs as fast as it can.
      REALTIME - the data is produced at the defined sample rate.
      BLOCK - no data is produced. */
  public  static final String throttleList = "RealTime,Full,Block";
  private static final int REALTIME=1,FULL=2,BLOCK=3;

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

  @Override
  public int open () {

    // Get parameters
    shape = MA.getChoice("SHAPE",shapeList,0);
    form  = MA.getFormat("FORM","S#,C#");
    freq  = MA.getD("FREQ");
    amp   = MA.getD("AMP");
    poff  = MA.getD("PHASE");
    size  = MA.getD("ELEM");
    chirp = MA.getD("CHIRP",0.0);
    mean  = MA.getD("MEAN",0.0);
    var   = MA.getD("VARIANCE",Math.pow(amp/LNTEN,2));
    sdev  = MA.getD("SDEV",Math.sqrt(var));
    start = MA.getD("START");
    delta = MA.getD("DELTA");

    // Open output file
    hcb = MA.getDataFile("OUT","1000","SF",0);
    parseRFargs(); // needs hcb to set keywords for now
    hcb.setFormat(form);
    hcb.setXStart(start);
    hcb.setXDelta(delta);
    hcb.setXUnits(Units.TIME);
    hcb.setSize(size);
    hcb.open(DataFile.OUTPUT);

    // Setup throttling mode
    throttle = MA.getState("/RT")? REALTIME : FULL;
    throttle = MA.getChoice("/THROTTLE",throttleList,throttle); // Override any /RT input
    if (throttle==REALTIME) hcb.setTimeAt(Time.current());

    // Setup processing
    dble  = MA.getState("/DP",(hcb.getFormatType()==DOUBLE));
    exact = MA.getState("/EXACT");
    xfer  = MA.getL("/TL",4096);
    data  = hcb.getDataBuffer(xfer,dble?DOUBLE:FLOAT);
    a2s   = sdev/amp; // set user ratio of sdev/amplitude

    todo(size);

    Waveform.setSeed(MA.getL("/SEED",-1));

    return NORMAL;
  }

  @Override
  public int process () {
    // handle data throttling
    if (throttle>0) {
      if (throttle==REALTIME) {
        if (tstart==0) tstart = Time.current()-hcb.seek()*delta;
        if (Time.current() < tstart+hcb.seek()*delta) return NOOP;
      }
      else tstart=0;
      if (throttle==BLOCK) return NOOP;
    }

    int ndo = todo();
    if (ndo==0) return FINISH;

    double p  = phase+poff;             // offset phase
    double dp = freq*delta;             // delta phase

    if (poff!=0) p -= Math.floor(p);    // modulo 1.0 if necessary
    if (dp<0 && shape!=SIN) dp = -dp;   // no negative freqs except for SINs

    int mode = (exact && (shape==SIN || shape==COS))? -hcb.spa : hcb.spa;

    if (dble) {
      double[] dbuf = data.castD(false);  // get array
      switch (shape) {                    // branch on wave shape
        case SIN: Waveform.sincos(dbuf,amp,p,dp,ndo,mode); break;
        case COS: Waveform.sincos(dbuf,amp,p+.25,dp,ndo,mode); break;
        case SQU: Waveform.square(dbuf,amp,p,dp,ndo,mode); break;
        case TRI: Waveform.triangle(dbuf,amp,p,dp,ndo,mode); break;
        case SAW: Waveform.sawtooth(dbuf,amp,p,dp,ndo,mode); break;
        case PUL: Waveform.pulse(dbuf,amp,p,dp,ndo,mode); break;
        case CON: Waveform.constant(dbuf,amp,ndo,mode); break;
        case ZER: Waveform.constant(dbuf,0.0,ndo,mode); break;
        case WHI: Waveform.whitenoise(dbuf,sdev,ndo,mode); break;
        case LRS: lrs=Waveform.lrs(dbuf,amp,ndo,mode,lrs); break;
        case RAM: cur=Waveform.ramp(dbuf,amp,ndo,mode,cur); break;
      }
      if (mean!=0) for (int i=0; i<ndo*hcb.spa; i++) dbuf[i]+=mean;
      data.uncast(dbuf,true);
    }
    else {
      float fmean=(float)mean;
      float[] fbuf = data.castF(false);
      switch (shape) {                    // branch on wave shape
        case SIN: Waveform.sincos(fbuf,amp,p,dp,ndo,mode); break;
        case COS: Waveform.sincos(fbuf,amp,p+.25,dp,ndo,mode); break;
        case SQU: Waveform.square(fbuf,amp,p,dp,ndo,mode); break;
        case TRI: Waveform.triangle(fbuf,amp,p,dp,ndo,mode); break;
        case SAW: Waveform.sawtooth(fbuf,amp,p,dp,ndo,mode); break;
        case PUL: Waveform.pulse(fbuf,amp,p,dp,ndo,mode); break;
        case CON: Waveform.constant(fbuf,amp,ndo,mode); break;
        case ZER: Waveform.constant(fbuf,0.0,ndo,mode); break;
        case WHI: Waveform.whitenoise(fbuf,sdev,ndo,mode); break;
        case LRS: lrs=Waveform.lrs(fbuf,amp,ndo,mode,lrs); break;
        case RAM: cur=Waveform.ramp(fbuf,amp,ndo,mode,cur); break;
      }
      if (fmean!=0) for (int i=0; i<ndo*hcb.spa; i++) fbuf[i]+=fmean;
      data.uncast(fbuf,true);
    }

    hcb.write(data,ndo);

    phase += dp*ndo;			// increment phase
    phase -= Math.floor(phase);		// modulo 1.0

    if (chirp!=0) freq += chirp*delta*ndo;

    return NORMAL;
  }

  @Override
  public int close () {
    hcb.close();
    return NORMAL;
  }

  @Override
  public int restart () {
    hcb.close();
    hcb = MA.getDataFile("OUT",hcb,0);
    hcb.setFormat (form);
    hcb.setSize (todo);
    hcb.open();
    data = hcb.getDataBuffer(xfer,FLOAT);
    tstart = 0;
    return NORMAL;
  }

  @Override
  public int processReady () {
    if (state!=PROCESS) return 0;
    return hcb.processReady(xfer);
  }

  // set run-time properties
  /** Set waveform shape
   *  @param value the shape
   */
  public void setShape (String value) { shape = Parser.find(shapeList,value,shape,0,Parser.E_WARNING); }
  /** Set waveform mean
   *  @param value the mean
   */
  public void setMean (double value) { mean = value; }
  /** Set waveform standard deviation
   *  @param value the standard deviation
   */
  public void setSDev (double value) { sdev = value; }
  /** Set waveform variance
   *  @param value the variance
   */
  public void setVariance (double value) { sdev = Math.sqrt(value); }
  /** Set waveform amplitude
   *  @param value the amplitude
   */
  public void setAmplitude (double value) { amp = value; sdev = amp*a2s; }
  /** Set waveform frequency
   *  @param value the frequency
   */
  public void setFrequency (double value) { freq = value; }
  /** Set waveform chirp
   *  @param value the chirp
   */
  public void setChirp (double value) { chirp = value; }
  /** Set waveform phase
   *  @param value the phase
   */
  public void setPhase (double value) { phase = value; }
  /** Set waveform throttle
   *  @param value the throttle
   */
  public void setThrottle (String value) { throttle = Parser.find(throttleList,value,throttle,0,Parser.E_WARNING); }
  /** Set waveform format
   *  @param format the format
   */
  public void setFormat (String format) { if (form.equals(format)) return; form = format; setState(RESTART); }

  // get run-time properties
  /** Get waveform shape
   *  @return the shape
   */
  public String getShape () { return Parser.get(shapeList,shape); }
  /** Get waveform mean
   *  @return the mean
   */
  public double getMean() { return mean; }
  /** Get waveform standard deviation
   *  @return the standard deviation
   */
  public double getSDev () { return sdev; }
  /** Get waveform variance
   *  @return the variance
   */
  public double getVariance () { return sdev*sdev; }
  /** Get waveform amplitude
   *  @return the amplitude
   */
  public double getAmplitude () { return amp; }
  /** Get waveform frequency
   *  @return the frequency
   */
  public double getFrequency () { return freq; }
  /** Get waveform chirp
   *  @return the chirp
   */
  public double getChirp () { return chirp; }
  /** Get waveform phase
   *  @return the phase
   */
  public double getPhase () { return phase; }
  /** Get waveform throttle
   *  @return the throttle
   */
  public String getThrottle () { return Parser.get(throttleList,throttle); }
  /** Get waveform format
   *  @return the format
   */
  public String getFormat () { return MA.getS("FORM"); }


  // process optional RF argument overrides
  private void parseRFargs() {
    Double rf  = MA.getD("RF",       null); // Signal   RF
    Double vrf = MA.getD("VRF",      null); // Receiver RF
    Double bw  = MA.getD("BW",       null); // Receiver BW
    Double sr  = MA.getD("SR",       null); // Sample Rate
    Double dur = MA.getD("DURATION", null); // File duration in sec
    if (rf != null) {
      if (vrf == null) throw new MidasException("Can not use RF="+rf+" without VRF=");
      if (bw  == null) throw new MidasException("Can not use RF="+rf+" without BW=");
    }
    boolean cx = (Data.getSPA(form.charAt(0)) == 2);
    if (sr  != null) delta = 1.0 / sr;
    if (bw  != null) delta = (cx)? 1.0 / bw : 0.5 / bw;
    if (rf  != null) freq  = (cx)? rf - vrf : rf - vrf + bw/2;
    if (dur != null) size  = Math.ceil(dur / delta);
    // Insert the applicable keywords. There are alternate keywords that could
    // be used here (including some from Platinum), but VRF is the only one that
    // is consistently seen in NeXtMidas and X-Midas usage that is always in Hz.
    // There are various keywords for reporting BW, but they are inconsistent
    // in usage (and units) and are redundant with XDelta, so we don't bother.
    if (vrf != null) hcb.getKeywordsObject().put("D:VRF", vrf);
  }

}
