package nxm.sys.prim;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.Random;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;

/** Uses statistics to get minimum and maximum value and place them in a SCALETABLE for PLOT
 *  The data is effectively NOOPed through so that data does not need to be reread (save time on files
 *  read over https).
 *  PLOT/ALL should use the data and table output from this primitive.
 *  This is intended to run through all the data and place the min/max in either YMIN/YMAX or ZMIN/ZMAX,
 *  depending on the type of file.
 *
 *  @author Jeanette McCall
 *  @since NeXtMidas 3.9.1
 */
public class prescaler extends Primitive {
  /** Input data file  */ protected DataFile hin;
  /** Output data file */ protected DataFile hout;

  /** percent of input file to process (to form scale values */
  protected double   percent;
  /** true if we have an output file specified to noop to */
  protected boolean  makingCopy;
  /** true if the file being processes is type 2000 (rather than type 1000) */
  protected boolean  isType2000;

  /** frame size                                 */ protected int      frameSize;
  /** units associated with file being processed */ protected int      units;
  /** Nelements in file                          */ protected double   dnelem;

  /** save of YDelta if 2000 file, so value can be restored */
  private double ydelta;
  /** save of YStart if 2000 file, so value can be restored */
  private double ystart;

  /** Is the data complex? (includes FORM=CF, but not FORM=2F) */
  protected boolean  complex;

  /** The number of scalars-per-atom */ protected int      spa;
  /** The data buffer to use.        */ protected Data     data;
  /** amount of data read in         */ protected int      ngot;
  /** Max Values                     */ protected double   dmax;
  /** Min Values                     */ protected double   dmin;
  /** Offset of Max 0..n-1           */ protected double   dmaxOff;
  /** Offset of Min 0..n-1           */ protected double   dminOff;

  /** Only read selective portions needed for random based scaling */
  protected boolean   selectiveRead;
  /** read indices used when doing random sampling of data         */
  LinkedList<Integer> selectedReadIndicies;

  /** Table with output values. */
  @InternalUseOnly("Since NeXtMidas 3.9.1")
  public Table table = new Table();
  /** result name for table containing scale values */
  String resultsName;
  /** default name results (Table containing scale values) */
  String DEFAULT_RES_TBL_NAME = "PRESCALER_RESULT_TBL";
  /** default name for extended header keyword to contain scale values */
  public static final String KEYWORD_DEFAULT = "VOLATILE_KW_PLOT_SCALE";
  /** name of switch used to trigger saving of scale values in keyword */
  public static final String KEYWORD_SWITCH = "/SCALEKW";

  /** default percentage to be processed if just "RANDOM" is specified (i.e. 10% with cap) */
  public static final int DEFAULT_RANDOM_PERCENT = -10;
  /** maximum percentage allowed before just processing whole file with normal random */
  static final int MAX_RANDOM_PERCENT = 20;
  /** always do at least first and last in random mode */
  static final int MIN_RANDOM_SAMPLES = 2;
  /** Cap on number of frames/TL for negative or default RANDOM */
  static int maxForCappedRandom = 1000;
  /** use seeded random rather than selectively scattered values */
  static boolean useSeededRandom = true;
  /** maintain consistency using seed */
  static final long RANDOM_SEED = 10125811212113l;

  /** name of extended hearer keyword to contain scale values                      */
  String  prescaleKW = "";
  /** insert an extended header keyword into the BLUE file that gives scale values */
  boolean addPrescaleKeyword;

  /** 100% of file */
  public static final int WHOLE_FILE = 100;
  /** percent deviations */
  static final float[] PERCENT_DEVIATIONS = new float[] {-0.3f,0.3f,-0.1f,0.1f,-0.4f,0.4f,-0.2f,0.2f,0f,
                                                         -0.35f,0.35f,-0.15f,0.15f,-0.25f,0.25f,0f};

  /** debugging is turned off by default */ static boolean debugDefault = false;
  /** switch for debugging               */        boolean debug        = false;

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

  @Override
  public int open () {
    // Get input file
    hin = MA.getDataFile("IN","1000,2000","S#,C#",DataFile.INPUT);
    hin.open();
    if(MA.find(KEYWORD_SWITCH)) {
      addPrescaleKeyword = true;
      prescaleKW =  MA.getS(KEYWORD_SWITCH);
      if(prescaleKW == null || prescaleKW == "") {
        prescaleKW = KEYWORD_DEFAULT;
      }
    }

    units = hin.getUnits();
    // create matching output file
    hout = MA.getDataFile("OUT",hin,0);
    hout.open(DataFile.OPTIONAL);
    makingCopy = hout.isOpen;

    percent = MA.getD("/RANDOM",(double)WHOLE_FILE,(double)DEFAULT_RANDOM_PERCENT); // if random is specified, default to 10% (other wise default to processing whole file)

    xfer    = MA.getL("/TL", Math.max(1,(int)(bufSize/hin.dbpe)) );
    debug = debugDefault || MA.find("/DEBUG");

    // set up the data buffer and force type 1000 data handling
    frameSize = hin.getFrameSize();
    if(hin.typeClass==2) {
      ydelta = hin.getYDelta();
      ystart = hin.getYStart();
    }
    isType2000 = hin.typeClass==2;
    if (isType2000) {
      hin.setFS(0);
      if (makingCopy) hout.setFS(0);
      xfer = frameSize; // TL=1 when type 2000 is equivalent to 1 frame size worth of type 1000
    }

    int numTLofData = (int)(hin.getSize() / xfer); // get number of elements total
    percent = adjustPercent(numTLofData);
    if ((percent < WHOLE_FILE) && (percent > MAX_RANDOM_PERCENT)) {
      M.info("PRESCALER only supports a random percent of "+MAX_RANDOM_PERCENT+"% or less. The percent "
             +percent+"% is not supported. Defaulting to processing the whole file");
      percent = WHOLE_FILE;
    }
    // This will read all the data in the file unless all of the following conditions are true
    // 1) we are basing scaling on random data, 2) we are NOT NOOPing to an output file and the input is NOT Pipe data
    selectiveRead = (percent < WHOLE_FILE) && !makingCopy && !hin.isPipe();
    if (selectiveRead) {
      // which frames/blocks are we going to access
      // 1) get a Set of frames/blocks that we will read (number_read/num_in_file = percent)
      // 2) convert those to index values for read;
      int numValuesInSet = (int)Math.max(percent*numTLofData/100,2);
      if (useSeededRandom) {
        selectedReadIndicies = new LinkedList<>(getNRandomMultiplesOfValue(numValuesInSet,numTLofData - 1, xfer));
      }
      else {
        selectedReadIndicies = getNScatteredMultiplesOfValue(numValuesInSet,numTLofData - 1, xfer);
      }
      if (debug) {
        if (useSeededRandom) {
          System.out.println("Selected Indices based on seeded random:"+selectedReadIndicies);
        }
        else {
          System.out.println("Selected Indices based on scattered:"+selectedReadIndicies);
        }
      }
    }


    complex = (hin.getFormatMode() == COMPLEX);
    spa     = hin.getSPA();
    data    = hin.getDataBuffer(xfer);
    verbose = MA.getState("/VERBOSE", false); // outputResults() will enable if no args given.

    resetAccumulators();

    // Get the name of the RESULT that will be a table holding the scale values
    resultsName = MA.getS("SCALETABLE",DEFAULT_RES_TBL_NAME);
    if (resultsName.trim().isEmpty()) resultsName = DEFAULT_RES_TBL_NAME;

    // this value is not propogated from hin so we need to do it ourselves
    if(makingCopy) hout.setTimeLineLength(-1);

    return NORMAL;
  }

  @Override
  public int process () {
    if (!selectiveRead) {
      ngot = hin.read(data);
    }
    else {
      // index is next value in random index set calculated in open
      if (!selectedReadIndicies.isEmpty()) {
        int index = selectedReadIndicies.removeFirst();
        ngot = hin.read(data, index, xfer);
      }
      else {
        ngot = -1;
      }
    }
    if (ngot == 0) return NOOP;

    else if ((ngot < 0)) {
      if (makingCopy) {
        if(isType2000) {
          hout.setFrameSize(frameSize);
          hout.setUnits(units);
          hout.setYDelta(ydelta);
          hout.setYStart(ystart);
        }
      }

      outputToResTableAndKeywords();

      return FINISH;
    }

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

    // we are storing a copy of the file
    if (makingCopy) {
      hout.write(data,ngot);
    }
    return NORMAL;
  }

  @Override
  public int close () {
    hin.close();
    if (makingCopy) {
      if (!M.isPiped()) {
        // if we are making a copy in a pipe, the file is probably open in plot and should not be closed here
        hout.close();
      }
      else {
        hout.update();
      }
    }
    return NORMAL;
  }

  /** Adjust percent based on capping mode and ensuring RANDOM processes at least 2 xfer lengths of data
   *
   *  @param numTLofData number of transfer lengths of data in the file
   *  @return adjusted percent
   */
  private double adjustPercent (int numTLofData) {
    if (percent < -100) { // file # elements based percent
      percent = (hin.size <= -percent) ? WHOLE_FILE : DEFAULT_RANDOM_PERCENT;
    }
    boolean cappingRandom = (percent < 0);
    double newPercent = Math.abs(percent);
    if (cappingRandom) {
      double maxPercent = Math.min(WHOLE_FILE, (100.0 * maxForCappedRandom) / numTLofData);
      percent = Math.min(newPercent, maxPercent);
    }
    newPercent = Math.max(newPercent, MIN_RANDOM_SAMPLES*100.0/numTLofData); // ensure at least 2 samples in random
    return newPercent;
  }

  /** 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++];
        if (isType2000) {
          // for type 20000, magnitude is plotted and scale needs to be based on magnitude
          double dabs = rval*rval + ival*ival;
          // leave dmin/dmax as squared until end for speed
          if (dabs > dmax) { dmax = dabs; dmaxOff = dnelem; }
          if (dabs < dmin) { dmin = dabs; dminOff = dnelem; }
        }
        else {
          // for type 1000, Real and Imaginary are separate traces
          double valMin = Math.min(rval, ival);
          double valMax = Math.max(rval, ival);
          if (valMax > dmax) { dmax = valMax; dmaxOff = dnelem; }
          if (valMin < dmin) { dmin = valMin; dminOff = dnelem; }
        }
      }
    } else {
      for (int i=0; i<ngot*spa; ) {
        double dval = dbuf[i++];
        if (dval > dmax) { dmax = dval; dmaxOff = dnelem; }
        if (dval < dmin) { dmin = dval; dminOff = dnelem; }
      }
    }
  }

  /** Put the scale values in the appropriate place (result or extended header keyword) */
  protected void outputToResTableAndKeywords () {

    Table keywordTable = new Table();;

    if (!isType2000) {
      keywordTable.put("YMIN",dmin);
      keywordTable.put("YMAX",dmax);
    }
    else {
      // revert complex squared back to magnitude
      if (complex) {
        dmin = Math.sqrt(dmin);
        dmax = Math.sqrt(dmax);
      }
      keywordTable.put("ZMIN",dmin);
      keywordTable.put("ZMAX",dmax);

    }
    if (addPrescaleKeyword) {
      if (makingCopy) {
        hout.keywords.add(prescaleKW, keywordTable);
        hout.update(); // need to update the keywords before close
      }
      else {
        hin.close();
        hin.setFlags(DataFile.INOUT);
        hin.open();
        hin.keywords.put(prescaleKW, keywordTable);
        hin.update(); // need to update the keywords before close
      }
    }
    table.putAll(keywordTable);
    MR.put(resultsName, table);

  }

  /** Resets the accumulators. */
  protected void resetAccumulators() {
    dmax=DMIN; dmin=DMAX;
    dmaxOff=-1; dminOff=-1;
    dnelem=0;
  }

  /** Gets a Set of N random int values that are multiples of the multiplication factor
   *  (first and last always included)
   *
   *  @param n           number of values to place in set
   *  @param maxN        maximum number that can be chosen (to be multiplied by the multiplication factor)
   *  @param multFactor  multiplication factor
   *  @return            a random Set of N numbers
   */
  @InternalUseOnly
  public static HashSet<Integer> getNRandomMultiplesOfValue(int n, int maxN, int multFactor) {
    if (n > (maxN + 1)) {
      Shell.warning("prescaler.getNRandomMultiplesOfValue n:"+n+" can not be greater than maxN:"
                     + maxN + " ... setting n to maxValue");
      n = maxN;
    }
    else if (n < 2) {
      Shell.warning("prescaler.getNRandomMultiplesOfValue n:"+n+" can not be less than 2 - first and last xfer length always included.");
    }

    // create set and add first and last xfer length as samples (support unwrapped phase)
    HashSet<Integer> set = new HashSet<>();
    set.add(0);    // add first
    set.add(maxN * multFactor); // add last

    // Use pre-seeded random for consistency
    Random random = new Random(RANDOM_SEED);

    while(set.size() < n){
      int thisOne = random.nextInt(maxN + 1) * multFactor; // nextInt has exclusive bound
      set.add(thisOne);
    }

    return set;
  }

  /** Gets a list of approximately n scattered int values that are multiples of the multiplication factor
   *  (first and last always included)
   *
   *  @param n           number of indices to be chosen (not strict - gives 1 extra)
   *  @param maxN        maximum number that can be chosen (to be multiplied by the multiplication factor)
   *  @param multFactor  multiplication factor
   *  @return            a List of scattered indices
   */
   @InternalUseOnly
   public static LinkedList<Integer> getNScatteredMultiplesOfValue(int n, int maxN, int multFactor) {
     if (n > (maxN + 1)) {
       Shell.warning("prescaler.getNRandomMultiplesOfValue n:"+n+" can not be greater than maxN:"
                      + maxN + " ... setting n to maxValue");
       n = maxN;
     }
     LinkedList<Integer> scatteredIndices = new LinkedList<>();
     int dataGrpIdx = 0;
     float avgSpacing = maxN / n;
     float[] deviations = getDeviations(avgSpacing/2);
     float touchstoneIndex = 0;
     int deviationIdx = 0;

     while(dataGrpIdx < maxN){
       int thisOne = dataGrpIdx * multFactor; // multiply to take into account xfer or frame size
       scatteredIndices.add(thisOne);
       touchstoneIndex += avgSpacing; // keep touch with indices we are varying about
       dataGrpIdx = (int)(touchstoneIndex + deviations[deviationIdx]); // add deviation so we are not just processing even intervals
       deviationIdx = (deviationIdx + 1) % deviations.length; // keep circulating through deviations
     }

     // also add the last one to handle the unwrapped phase case, even though this technically makes us a little over on percent
     scatteredIndices.add(maxN * multFactor);

     return scatteredIndices;
 }

  private static float[] getDeviations(float maxDeviation) {
    float[] deviations = new float[PERCENT_DEVIATIONS.length];
    for (int i = 0; i < deviations.length; i++) {
      deviations[i] = maxDeviation * PERCENT_DEVIATIONS[i];
    }
    return deviations;
  }

  /** Use Seeded Random Indices rather than Scattered
   *  @param useSeededRandom true if should use seeded random rather than scattered
   */
  public static void setUseSeededRandom (boolean useSeededRandom) {
    prescaler.useSeededRandom = useSeededRandom;
  }

  /** Use Seeded Random Indices rather than Scattered
   *  @param useSeededRandom "TRUE" if should use seeded random rather than scattered
   */
  public static void setUseSeededRandom (String useSeededRandom) {
    prescaler.useSeededRandom = useSeededRandom.toLowerCase().startsWith("t");
  }

  /** Sets the default state for Midas debugging
   *  @param debugOn the default state for Midas debugging
   */
  public static void setDebug (String debugOn) {
    debugDefault = debugOn.toLowerCase().startsWith("t");
  }

  /** Set cap on number of frames/TL for negative or default RANDOM
   *  @param cap the cap on number of frames/TL for negative or default RANDOM
   */
  public static void setMaxForCappedRandom (int cap) {
    maxForCappedRandom = cap;
  }

  /** Get cap on number of frames/TL for negative or default RANDOM
   *  @return the cap on number of frames/TL for negative or default RANDOM
   */
  public static int getMaxForCappedRandom () {
    return maxForCappedRandom;
  }

}
