package nxm.sys.prim;

import java.util.ArrayList;

import nxm.sys.inc.DataTypes;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.Table;

/**
  Compares data from two files

  @author Ken Smith (modified JGS's original version)
  @author Neon Ngo (added support for comparing record-based data files)
 */
public class compare extends Primitive {

  private DataFile hi1,hi2;
  private Data data1,data2;
  /** record/column definitions for the input files */
  private Table recordDef1, recordDef2;
  private ArrayList<RecordInfo> numericSubRecs;
  private int numberFieldsPerRecord;
  private int tl=4096;
  private float diffmin,diffmax,max_thr,min_thr;
  private float minScale,maxScale,fullScale;
  private String max_result_name, min_result_name, result_name;
  private boolean noErrorInOpen;

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

  /** Open files and get command line */
  public int open () {
    noErrorInOpen = false;
    hi1 = MA.getDataFile("IN1");
    hi1.open();

    if (MA.isPresent("IN2")) {
      hi2 = MA.getDataFile("IN2");
      hi2.open();
    }

    max_thr = MA.getF("MAX_THR", 1.e-9f);
    min_thr = MA.getF("MIN_THR",-1.e-9f);

    max_result_name = MA.getS("MAX_OUT", null);
    min_result_name = MA.getS("MIN_OUT", null);
    result_name     = MA.getS("RESULT",  null);

    if (hi1.typeClass >= 2) tl=1;
    tl = MA.getL("/TL", tl);

    if (hi1.typeClass <= 2) {
      data1 = hi1.getDataBuffer(tl, FLOAT);   // data buffer of just floats for IN1
    }
    else if (hi1.isRecordBased()) {           // support for record based data for BUG 2374
      data1 = hi1.getDataBuffer(tl);
      recordDef1 = hi1.getRecordDefs();
      numericSubRecs = createRecordInfoFromRecDefTab(recordDef1);
    }
    if (hi2 != null) {
      if (hi2.typeClass <= 2) {
        data2 = hi2.getDataBuffer(tl, FLOAT); // data buffer of just floats for IN2
        if (recordDef1 != null) {
          M.error("Cannot compare record based ["+hi1.getName()+"] to frame based file ["+hi2.getName()+"].");
        }
      } else if (hi2.isRecordBased()) {       // support for record based data for BUG 2374
        data2 = hi2.getDataBuffer(tl);
        recordDef2 = hi2.getRecordDefs();
        if (recordDef1 == null) {
          M.error("Cannot compare frame based file ["+hi1.getName()+"] to record based file ["+hi2.getName()+"].");
        }
        else if (! recordDef1.equals(recordDef2)) {
          M.error("SubRecord definition of both input files must match. IN1="+hi1.getName()+" IN2="+hi2.getName());
        }
      }
    }

    diffmin = maxScale = fullScale = 1.0e30F;
    diffmax = minScale = -1.0e30F;

    final byte in1FormatType = hi1.getFormatType();
    if (in1FormatType != NONHOMOGENEOUS) {
      if (MA.getState("/LEGACYSCALE", false)) { // behavior prior to 2.9.2
        M.deprecate("/LEGACYSCALE is deprecated since NeXtMidas 2.9.2 b/c it did not calculate the proper max/min/full scale for byte and other data types.");
        if (hi1.getFormatType()==INT) {
          maxScale=32767; minScale=-32768; fullScale=65536;
        }
        else if (hi1.getFormatType()==BYTE) {
          maxScale=255; minScale=-256; fullScale=512;
        }
      }
      else { // since 2.9.2
        // max/min/full scale are base on first input file (IN1)
        maxScale = (float) Data.getTypeMax(in1FormatType);
        minScale = (float) Data.getTypeMin(in1FormatType);
        fullScale = maxScale - minScale + 1;
      }
    }

    // if (verbose) M.info("DEBUG: maxScale = "+maxScale+" minScale="+minScale+" fullScale="+fullScale);

    noErrorInOpen = true;
    return NORMAL;
  }

  /** Process the data in the files */
  public int process () {

    final int elementsRead1 = hi1.read(data1);
    if (elementsRead1 == 0) return NOOP;
    if (elementsRead1 < 0)  return FINISH;
    int numAtoms1 = elementsRead1 * hi1.ape; // number of atoms (scalars) to process for IN1
    int numAtoms = numAtoms1;     // number of atoms (scalars) to process (for frame based data)
    int numElems = elementsRead1; // number of elements to process (for record based data)

    if (hi2 != null) {
      int elementsRead2 = hi2.read(data2, elementsRead1);
      if (elementsRead2 == 0) return NOOP;
      if (elementsRead2 < 0)  return FINISH;
      int numAtoms2 = elementsRead2 * hi2.ape;  // number of atoms (scalars) to process for IN2
      numAtoms = Math.min(numAtoms1, numAtoms2);
      numElems = Math.min(elementsRead1, elementsRead2);
    }
    if (recordDef1 != null) {
      processRecordData(numElems);
    }
    else if (hi2 != null) {
      float[] farray1 = data1.castF(true);
      float[] farray2 = data2.castF(true);
      checkDiffOfArrays(farray1, farray2, numAtoms);
      data1.uncast(farray1, false);
      data2.uncast(farray2, false);
    }
    else {
      float[] farray1 = data1.castF(true);
      checkMaxMinOfArray(farray1, numAtoms);
      data1.uncast(farray1, false);
    }

    if (max_result_name != null) MR.put(max_result_name, diffmax);
    if (min_result_name != null) MR.put(min_result_name, diffmin);

    // See if the user wants a test against the thresholds
    if (result_name != null) {
      MR.put(result_name, thresholdResult());
    }
    return NORMAL;
  }

  /** Close files and output results */
  public int close () {
    if (noErrorInOpen) {
      // Output to the screen if the user did not ask for any results
      if (verbose || (max_result_name == null && min_result_name == null)) {
        M.info("Diff MAX = "+diffmax);
      }
      if (verbose || (max_result_name == null && min_result_name == null)) {
        M.info("Diff MIN = "+diffmin);
      }
      if (verbose && (result_name == null)) {
        M.info("Diff within THRESHOLDS = "+thresholdResult());
      }
    }

    hi1.close();
    if (hi2 != null) hi2.close();
    return NORMAL;
  }

  /** check that input file(s) are with max/min thresholds
      @return 1 if diff within thresholds, 0 if not
   */
  private final int thresholdResult () {
    return (diffmax < max_thr) && (diffmin > min_thr) ? 1 : 0;
  }

  /** process record based data files
      @param ngot number of elements to process
      @since NeXtMidas 2.9.2
   */
  private void processRecordData (int ngot) {
    if (recordDef2 != null) {
      for (int ii = 0; ii < ngot; ii++) {
        for (RecordInfo recInfo : numericSubRecs) {
          int offset = recInfo.offset + (ii * data1.bpe);
          Data subdata1 = data1.getAtomAt(offset, recInfo.format);
          Data subdata2 = data2.getAtomAt(offset, recInfo.format);
          if (recInfo.spa == 1) {
            float f1 = subdata1.toF();
            float f2 = subdata2.toF();
            checkDiff(f1, f2);
          }
          else if (recInfo.spa > 1) {
            float[] farray1 = subdata1.castF(true);
            float[] farray2 = subdata2.castF(true);
            checkDiffOfArrays(farray1, farray2, farray1.length);
            subdata1.uncast(farray1, false);
            subdata2.uncast(farray2, false);
          }
        } // end inner for loop over SubRecords
      } // end outter for loop over elements
    }
    else if (recordDef1 != null) {
      for (int ii = 0; ii < ngot; ii++) {
        for (RecordInfo recInfo : numericSubRecs) {
          int offset = recInfo.offset + (ii * data1.bpe);
          Data subdata1 = data1.getAtomAt(offset, recInfo.format);
          if (recInfo.spa == 1) {
            checkMaxMin(subdata1.toF());
          }
          else if (recInfo.spa > 1) {
            float[] farray = subdata1.castF(true);
            checkMaxMinOfArray(farray, farray.length);
            subdata1.uncast(farray, false);
          }
        } // end inner for loop over SubRecords
      } // end outter for loop over elements
    }
  }

  /** Check difference between first and second float and check/update the max/min diffs. */
  private final void checkDiff (float f1, float f2) {
    float diff = f2 - f1;
    diff = checkDiffWithScale(diff);
    checkMaxMin(diff);
  }

  /** Check difference between first and second float array, processing only n items
      and check/update the max/min diffs.
      @param farray1 first array
      @param farray2 second array
      @param n       number of items to process (must be <= to length of input arrays)
   */
  private final void checkDiffOfArrays (float[] farray1, float[] farray2, int n) {
    for (int ii = 0; ii < n; ii++) {
      float diff = farray2[ii] - farray1[ii];
      diff = checkDiffWithScale(diff);
      checkMaxMin(diff);
    }
  }

  /** Check the diff against the max/min scale and diff which may be adjusted by the fullScale.
      The adjustment by fullScale can come into play when comparing files of different format types,
      e.g. if IN1 is SB and IN2 is SF. The max/min/full scale are based on the first input file (IN1).
   */
  private final float checkDiffWithScale (float diff) {
    if (diff > maxScale) diff -= fullScale; // KCS: I'm not sure how the difference can ever exceed fullScale,
    if (diff < minScale) diff += fullScale; // KCS: but I left the checks in anyway
    return diff;
  }

  /** Check float against max/min diffs and update the max/min diffs if larger/smaller, respectively. */
  private final void checkMaxMin (float f) {
    if (f > diffmax) diffmax = f;
    if (f < diffmin) diffmin = f;
  }

  /** Check max/min for n items in float array.
      @param farray array of floats to process
      @param n      number of items to process (must be <= to length of input array)
   */
  private final void checkMaxMinOfArray (float[] farray, int n) {
    for (int ii = 0; ii < n; ii++) {
      float f = farray[ii];
      if (f > diffmax) diffmax = f;
      if (f < diffmin) diffmin = f;
    }
  }

  /** setup SubRecord definition lookup for iterating over record-based data
      @since 2.9.2
   */
  private ArrayList<RecordInfo> createRecordInfoFromRecDefTab (Table recDefTab) {
    final ArrayList<RecordInfo> list = new ArrayList<>();
    numberFieldsPerRecord = 0; // reset
    for (Table.Iterator iter = recDefTab.iterator(); iter.hasNext();) {
      final Object key = iter.next();
      final Object val = iter.getValue();
      if (val instanceof Table) {
        final Table  tbl    = Convert.o2t(val);
        final String format = tbl.getS("FORMAT");
        final char   type   = format.charAt(1);
        if (DataTypes.numericTypeList.indexOf(type) >= 0) { // if have a numeric SubRecord
          final char mode = format.charAt(0);
          final int  spa  = Data.getSPA(mode);
          numberFieldsPerRecord += spa;

          RecordInfo recInfo = new RecordInfo(tbl.getL("OFFSET"), format, spa);
          list.add(recInfo);
        } else if (verbose) {
          M.info("COMPARE: will ignore non-numeric SubRecord Name="+key+" Format="+format);
        }
      }
    } // end for loop

    return list;
  }

  /** Immutable SubRecord definition info class used for iterating over and reading record-based data.
      @since NeXtMidas 2.9.2
   */
  static class RecordInfo {
    /** byte-offset for SubRecord */
    final int    offset;
    /** format string of SubRecord*/
    final String format;
    /** scalars per atom (i.e. SubRecord) */
    final int    spa;

    public RecordInfo (int off, String formatType, int scalarsPerRecord) {
      offset = off;
      format = formatType;
      spa    = scalarsPerRecord;
    }
    public String toString() { return "(" + offset + "," + format + ")"; }
  } // end class RecordInfo

} // end of class
