package nxm.sys.prim;

import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.Table;

/** Computes the statistics on a data file.

    @author Jay Hallam
*/
public class statistics extends Primitive {

  protected DataFile hin;                 // Input data file
  protected int      base;                // Indicates if output OMIN, OMAX is one-based (Added 2.1.0)
  protected int      frameSize;           // Indicates the original frame size           (Added 2.1.0)
  protected Table    table = new Table(); // Table with output values.
  protected double   drsum,disum;         // Real/Imag sum
  protected double   dstdDev;             // Standard Deviation
  protected double   dmax,dmin;           // Max/Min Values
  protected double   real_min,real_max,imag_min,imag_max; // For complex
  protected double   dmaxOff,dminOff;     // Offset of Max/Min 0..n-1
  protected double   dnelem;              // Nelements in file
  protected boolean  complex;             // Is the data complex? (includes FORM=CF, but not FORM=2F)
  protected boolean  paged;               // Compute output on a transfer by transfer basis
  protected int      spa;                 // The number of scalars-per-atom
  protected Data     data;                // The data buffer to use.
  protected int      ngot;                // amount of data read in

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

  @Override
  public int open () {
    // Get input file
    hin = MA.getDataFile("IN","1000,2000","S#,C#",0);
    hin.open();

    // set up the data buffer and force type 1000 data handling
    frameSize = hin.getFrameSize();
    if (hin.typeClass==2) hin.setFS(0);
    xfer    = MA.getL("/TL", Math.max(1,(int)(bufSize/hin.dbpe)) );
    complex = (hin.getFormatMode() == COMPLEX);
    spa     = hin.getSPA();
    data    = hin.getDataBuffer(xfer);
    base    = MA.getState("/ONEBASE")? 1 : 0;
    verbose = MA.getState("/VERBOSE", false); // outputResults() will enable if no args given.
    paged   = MA.getState("/PIECEWISE");

    resetAccumulators();

    return NORMAL;
  }

  @Override
  public int process () {

    ngot = hin.read(data);
    if (ngot == 0) return NOOP;
    if (ngot < 0) return FINISH;

    double[] dbuf = data.castD(true);
    computeStatistics (dbuf, ngot, spa, complex);
    data.uncast(dbuf,false);

    if (paged) {
      outputResults();
      resetAccumulators();
    }
    return NORMAL;
  }

  @Override
  public int close () {
    if (hin.isOpen && !paged) outputResults();
    hin.close();
    return NORMAL;
  }

  /** Compute the statistics for a given set of data.
      @param dbuf    The data read in (cast to double).
      @param ngot    The number of atoms read in.
      @param spa     The number of scalars per atom (1=Scalar, 2=Complex, ...).
      @param complex Is the data complex? (Complex includes FORM=CF, but not FORM=2F.)
   */
  protected void computeStatistics (double[] dbuf, int ngot, int spa, boolean complex) {
    if (complex) {
      for (int i=0; i<ngot*spa; ) {
        double rval = dbuf[i++];
        double ival = dbuf[i++];
        drsum += rval;
        disum += ival;
        double dabs = rval*rval + ival*ival;
        dstdDev += dabs;
        // leave dmin/dmax as squared until end for speed
        if (dabs > dmax) { dmax = dabs; dmaxOff = dnelem; }
        if (dabs < dmin) { dmin = dabs; dminOff = dnelem; }

        // Only for complex
        if (rval < real_min) real_min = rval;
        if (rval > real_max) real_max = rval;
        if (ival < imag_min) imag_min = ival;
        if (ival > imag_max) imag_max = ival;

        dnelem = dnelem + 1;
      }
    } else {
      for (int i=0; i<ngot*spa; ) {
        double dval = dbuf[i++];
        drsum += dval;
        dstdDev += dval*dval;
        if (dval > dmax) { dmax = dval; dmaxOff = dnelem; }
        if (dval < dmin) { dmin = dval; dminOff = dnelem; }
        dnelem = dnelem + 1;
      }
    }
  }

  /** Outputs the results. This is called from process() for pipes and close() for files. */
  protected void outputResults() {

    // finish computation
    double drmean = drsum / dnelem;
    double dimean = disum / dnelem;
    dstdDev = dstdDev/dnelem - drmean*drmean - dimean*dimean;
    dstdDev = Math.sqrt( Math.max(0.0,dstdDev) );

    // revert complex squared back to magnitude
    if (complex) dmin = Math.sqrt(dmin);
    if (complex) dmax = Math.sqrt(dmax);

    double dminAbs = hin.getXStart();
    double dmaxAbs = hin.getXStart();
    if (frameSize > 1) {
      // Type 2000 files need their ABSC values computed based on column index
      double colMinIndex = dminOff - Math.floor(dminOff/frameSize) * frameSize;
      double colMaxIndex = dmaxOff - Math.floor(dmaxOff/frameSize) * frameSize;
      dminAbs += colMinIndex*hin.getXDelta();
      dmaxAbs += colMaxIndex*hin.getXDelta();
    } else {
      dminAbs += dminOff*hin.getXDelta();
      dmaxAbs += dmaxOff*hin.getXDelta();
    }

    // now safe to correct for one/zero based reporting
    dminOff += base;
    dmaxOff += base;

    Data Dsum = new Data(complex?"CD":"SD",1);
    Dsum.setD(0,drsum); if (complex) Dsum.setD(1,disum);

    Data Dmean = new Data(complex?"CD":"SD",1);
    Dmean.setD(0,drmean); if (complex) Dmean.setD(1,dimean);

    // set values in table
    table.put("MEAN",Dmean);
    table.put("SDEV",dstdDev);
    table.put("ELEM",dnelem);
    table.put("MIN",dmin);
    table.put("MAX",dmax);
    table.put("AMIN",dminAbs);
    table.put("AMAX",dmaxAbs);
    table.put("OMIN",dminOff);
    table.put("OMAX",dmaxOff);
    table.put("SUM",Dsum);
    if (complex) {
      table.put("REALMIN", real_min);
      table.put("REALMAX", real_max);
      table.put("IMAGMIN", imag_min);
      table.put("IMAGMAX", imag_max);
    }

    // loop through assignment arguments
    boolean hasOutputResult = outputToResTable();

    // text output
    if (verbose || !hasOutputResult) {
      String s = "";
      s += "Number of elements   : "+dnelem+"\n";
      s += "Mean Value           : "+Dmean+"\n";
      if (dnelem<=1024) // report sum only for short files
      s += "Sum Value            : "+Dsum+"\n";
      s += "Standard deviation   : "+dstdDev+"\n";
      s += "Max/Absc/Offset      : "+dmax+" / "+dmaxAbs+" / "+dmaxOff+"\n";
      s += "Min/Absc/Offset      : "+dmin+" / "+dminAbs+" / "+dminOff;
      M.println (s);  // since 3.7.1 use M.println instead of terminal.writeln to allow logging of interactive commands NXM-2813
    }
  }

  /** Copies the values in table to the results table as appropriate. */
  protected boolean outputToResTable() {
    // loop through assignment arguments
    boolean hasOutputResult = false;
    for (int n=2; n<=MA.numberOf(); n++) {
      String key = fixKey(MA.getKey(n));
      String label = MA.getU(n);
      if ((key.equals("IMEAN") || key.equals("ISUM")) && label !=null && label.length() > 0) {
        // IMEAN deprecated since NeXtMidas 2.3.0, ISUM has been deprecated since
        // it was added, but we continue to support them for compatibility with
        // legacy macros and with X-Midas.
        M.deprecate("Key "+key+" deprecated, use "+key.substring(1)+".i instead.");
      }
      if (label.length()<=0) continue;
      hasOutputResult = true;
      if (key.equals("TABLE"))          { MR.put(label,table); }
      else if (!table.containsKey(key)) { M.warning("Key="+key+" not supported"); }
      else if (label.equals("TERMINAL")){ M.println("Key="+key+" Value="+table.get(key)); }  // since 3.7.1 use M.println instead of terminal.writeln to allow logging of interactive commands NXM-2813
      else                              { MR.put(label,table.get(key)); }
    }
    return hasOutputResult;
  }

  /** Resets the accumulators. */
  protected void resetAccumulators() {
    drsum=0; disum=0;
    dstdDev=0;
    dmax=DMIN; dmin=DMAX;
    dmaxOff=-1; dminOff=-1;
    dnelem=0;
    if (complex) {
      real_min=DMAX;
      real_max=DMIN;
      imag_min=DMAX;
      imag_max=DMIN;
    }
  }

  /** This returns the table holding any of the statistics. The return value is a pointer to the
      actual table used inside this class. Any changes to the statistics tracked by this class
      will be reflected in this table. <br>
      <br>
      Since changes are not written until the end of a data set this method is no more useful than
      the <code>TABLE=</code> parameter for large files. It is, however, useful when working with
      pipes since it allows a macro (or another primitive) to monitor the output of this primitive.
      @return the table holding the actual statistics used inside this class
   */
  public Table getTable() {
    return table;
  }

  /**
    Some option trees have extended this class to provide additional statistical information
    relevant to their option tree. In most cases this has been done to compute statistical
    information that is application-specific (hence does not belong in the SYS option tree).
    The following is a template for how these classes typically look.
    <pre>
    public class mystats extends statistics {
      // ****define custom variables here****

      protected void computeStatistics(double[] dbuf, int ngot, int spa, boolean complex) {
        super.computeStatistics(dbuf,ngot,spa,complex);
        // ****do custom stuff here****
      }

      protected void outputResults()  {
        // ****do custom stuff here****
        // Note: If you are doing computations that require the use of some of the
        //       standard statistics (e.g. dstdDev), you will want to add "default"
        //       values to the table before the call to super.outputResults() (this
        //       prevents "Key=??? not supported" warnings) and then compute things
        //       after the call to super. Note that when you do this you will need
        //       to call outputToResTable() at the end of your method.
        super.outputResults(); // This writes out values to results table
      }

      protected void resetAccumulators() {
        super.resetAccumulators();
        // ****do custom stuff here****
      }
    }
    </pre>
  */
  public void howToExtend() {}


  /** Converts legacy X-Midas key names to the corresponding NeXtMidas
   *  counterpart. If a normal NeXtMidas key is given, it is returned as-is.
   *  @since NeXtMidas 3.3.0
   */
  private String fixKey (String key) {
    String fix;

    // The keys being checked below are in the same order as they appear in
    // the X-Midas explain file for FSTATISTICS in X-Midas 4.8.2.
         if (key.equals("MEAN" )) return key;  // same
    else if (key.equals("STDV" )) fix = "SDEV";
    else if (key.equals("MAXV" )) fix = "MAX";
    else if (key.equals("MINV" )) fix = "MIN";
    else if (key.equals("MAXI" )) fix = "OMAX";
    else if (key.equals("MINI" )) fix = "OMIN";
    else if (key.equals("MAXA" )) fix = "AMAX";
    else if (key.equals("MINA" )) fix = "AMIN";
    else if (key.equals("ELEM" )) return key; // same
    else if (key.equals("IMEAN")) return key; // same (deprecated in NXM)
    else if (key.equals("SUM"  )) return key; // same
    else if (key.equals("ISUM" )) return key; // same (deprecated in NXM)
    else                          return key; // assume a normal NeXtMidas key

    M.deprecate("Use of "+key+"= is for legacy X-Midas compatibility only, "
              + "please use "+fix+"=");

    if (key.equals("MAXI") || key.equals("MINI")) {
      // After some discussion it was decided that it was best to use 0-based
      // output since users will often take this value and then turn around
      // and use it for indexing into the file (which will be 0-based in
      // NeXtMidas). However, we provide a warning since this is a clear
      // difference from X-Midas.
      M.warning("The results for "+key+"= will be zero-based rather than "
              + "one-based.");
    }

    return fix;
  }
}