package nxm.sys.libg;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.lib.Args;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.DataFileHeader;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.Keywords;
import nxm.sys.lib.Midas;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;
import nxm.sys.prim.prescaler;

/** This class is intended for use ONLY by plot.
 *  The was an internal class and was separated out in 4.1.4
 *  to simplify the plot class.
 *  <pre>
 *  basic flow:
 *  initPrescale is called from plot.open()
 *  - SCALETABLE (plot Keyword Only Parameter) is used to initalize scaleTable
 *  - If inFile is a single file:
 *    - file qualifiers for PRESCALE information to form prescaleSetupTable
 *    - set to prescale anyhow, if PLOT/ALL of static 1000 file
 *  open (also called from plot.open()) if need to do active prescale
 *  - Full prescale setup and, if needed, launching of the PRESCALER command
 *  - See plot.exp for preference order if prescale specified multiple ways
 *  - Returns true if plot.process() will need to wait on the scale table to
 *    be populated (from launched PRESCALE command or other external source)
 *  </pre>
 */
@InternalUseOnly
public class PrescaleSupport {

  // plot related fields
  /** file use for actual plot data - when prescaling, especially in a pipe, this may be a copy of the FILE= file */
  private String inFile;
  private Midas  M;
  private MPlot  MP;
  private Args   MA;

  /** current debug mode */
  private boolean debug = false;
  private boolean needScaleTable;  // since 3.9.1 for NXM-3078
  /** holds the custom scale values defined by either arguments passed or from prescaler
  @since NeXtMidas 3.9.1 */
  private Table scaleTable;

  // fields specific to PrescaleSetup
  private static final String DEFAULT_OUTCOPY = "plot_internal_prescale_temp_out.tmp{AUX=RAM}";
  /** key for keyword name in table is the same as the switch name without the slash */
  private final String KEY_FOR_KEYWORD_NAME = prescaler.KEYWORD_SWITCH.substring(1);
  /** prescale based on external entity setting SCALETABLE */
  private boolean externalPrescale;
  /** file to read data from after internal prescale - critical to have this in a pipe */
  private String inFileAfterPrescale;
  /** switch command specifying external header keyword holding scale values */
  private String prescalerKeywordSwitch = "";
  /** used with /PRESCALE=KW to check for scale KW in external header keywords */
  private Keywords extHeaderKeywords;
  /** Even in a pipe, we may need to wait to open the output file when doing internal prescaling in /PRESCALE=KW mode */
  private boolean needWaitToOpenOutInFile;
  /** holds precaler primitive instance */
  private prescaler plotPrescaler = null;
  /** holds precaler random percent of file processed during prescale */
  private int percent = 100;
  /** file specified to hold copy of input file */
  private String prescalarOut = null;
  /** external prescaling was selected */
  private boolean prescalerExternal;
  /** use prescale keyword in file if available */
  private boolean preferKeyword;
  /** user specified name for keyword holding scale values from prescaler */
  private String prescaleKeywordName = "";
  /** need to launch PRESCALE command */
  private boolean runPrescaleCommand;
  /** the type of prescaling selected (defaults to INTERNAL) */
  private String  prescaleType;
  private String scaletableResultName;
  private Table quals = null;   // since 3.9.2 to allow quals to be acted on when opening of file is delayed due to prescaling
  private Table prescaleSetupTable = null;
  /** Is this a plot of a 1000 file done with the /ALL switch */
  private boolean prescale1000All = false;
  /** The default maximum number of elements for which a PLOT/ALL will do a FULL PRESCALE */
  private static final int DEFAULT_MAX_SIZE_ALL_PRESCALE = 256000;
  /** The maximum number of elements for which a PLOT/ALL will do a FULL PRESCALE.
   *  Larger files get a 10% random prescale */
  private int maxSizeFullPrescale = DEFAULT_MAX_SIZE_ALL_PRESCALE;
  /** Basefile to prescale */
  BaseFile bf = null;
  /** DataFileHeader matching inFile (if DataFile)*/
  DataFileHeader dfHeader = null;
  /** Input File is single file, not pipe separated list */
  boolean singleInputFile;

  /** Default constructor */
  public PrescaleSupport () {
  }

  /** Initialize PrescaleSetup<br>
   *  - The parameter SCALETABLE is used to initialize the scaleTable<br>
   *  - If PRESCALE directly specified or PLOT/ALL of static 1000 file
   *    initialize accordingly and return true
   *
   *  @param M       Midas instance
   *  @param MP      MPlot associated with plot
   *  @param MA      Args
   *  @param inFile  Input file as String
   *  @return true if prescaling mode
   */
  public boolean initPrescale (Midas M, MPlot MP, Args MA, String inFile) {
   // Save off key plot information into fields
    this.M  = M;
    this.MP = MP;
    this.MA = MA;
    this.inFile = inFile;
    this.singleInputFile = (inFile != null) && !inFile.contains("|");

    // check if prescale settings are present
    if(MA.find("SCALETABLE")) setScaleTable(MA.getTable("SCALETABLE"));
    // prescale directly specified if PRESCALE is specified in file qualifier or command line
    boolean qualifierBasedPrescale = false;
    if (singleInputFile) {
      this.bf = BaseFile.getInstanceFor(M, inFile);
      this.prescaleSetupTable = getPrescaleTableFromQuals();
      qualifierBasedPrescale = ((prescaleSetupTable!=null) &&
          !prescaleSetupTable.isEmpty());
    }
    boolean prescaleDirectlySpecified = qualifierBasedPrescale || MA.find("/PRESCALE");
    // PLOT/ALL on a static 1000 file can also trigger prescale
    if (!prescaleDirectlySpecified && singleInputFile && (bf instanceof DataFile)
        && !inFile.startsWith("_")) {
      this.prescale1000All = is1000PlotAll();
      if (prescale1000All) percent = -maxSizeFullPrescale; // will do full PRESCALE for smaller files & random for larger ones
    }
    return prescaleDirectlySpecified || prescale1000All;
  }

  /** Handles the PRESCALE related functionality that needs to occur during plot.open()<br>
   *  <pre>
   *  1) checking the prescale mode
   *  2) grabbing scale information that is already available
   *     a) from SCALETABLE if /PRESCALE=EXT outside pipe
   *     b) from external header keyword if /PRESCALE=KW
   *  3) determining if the prescaler primitive needs to be run for internal prescaling, or
   *     if the scenario dictates falling back to other scaling means
   *  4) Setting fields that determine if the plot.process() needs to handle the opening of
   *     the file used for suppling the data and/or waiting on the SCALETABLE to be set
   *  </pre>
   *  @return true if plot.process() needs to wait on a scale table to be set
   */
  public boolean open() {

    // We may already have gotten the BaseFile instance in initPrescale
    if (bf == null) bf = BaseFile.getInstanceFor(M,inFile);

    // default these as if running internal prescaling (the default)
    // these will get updated if we are not
    needScaleTable = true;      // need this unless already provided in keyword, cmd line, or quals
    runPrescaleCommand = true;  // need this if running internal prescaling

    // Get command line PRESCALE related information if table not set up
    // from qualifiers during initPrescale
    if (prescaleSetupTable == null || prescaleSetupTable.isEmpty()) {
      commandLineStatusUpdate();
    }
    else {
      tableBasedStatusUpdate(prescaleSetupTable);
    }

    boolean waitingOnPrescale = false;
    if (prescalerExternal) {
      waitingOnPrescale = externalPrescalingSetup();
    }
    else if (!singleInputFile) {
      // We DO NOT support multiple input files with prescaling, unless prescaling is external
      // abort prescaling
      M.warning("Multiple input files not allowed on any prescaling type except EXTERNAL, defaulting to regular plot scaling");
      runPrescaleCommand = false;
      needScaleTable = false;
      return false; // we are aborting prescaling - not waiting on it
    }
    else {
      boolean validFileType = processFileHeaderAndKeywords(preferKeyword);
      if (!validFileType) {
        // abort prescaling
        runPrescaleCommand = false;
        needScaleTable = false;
        return false; // we are aborting prescaling - not waiting on it
      }

      if(!preferKeyword && !(prescaleType.startsWith("int") || prescaleType.startsWith("rand") || prescaleType.equals(""))) {
        M.warning("Invalid prescale option " + prescaleType+ ", defaulting the prescaler command to be run internally");
      }

      if(runPrescaleCommand) {
        waitingOnPrescale = true;
        runPrescale();
      }
    }
    if (debug) M.info(toString()+"\n\n");
    return waitingOnPrescale;
  }

  /** Get the prescaler table from the file qualifiers (part of initPrescale)<br>
   *  This method requires the inFile to be a single input file and not
   *  a pipe separated list of files.
   *  @return PRESCALE setup Table or null
   *  @since NeXtMidas 3.9.2 for NXM-3078
   */
  private Table getPrescaleTableFromQuals () {
    Table prescaleSetupTable = null;

    Object prescaleValue = bf.getQualifier("PRESCALE");
    if (prescaleValue != null) {
      if (prescaleValue.toString().contains("{")) {
        prescaleSetupTable = new Table(prescaleValue.toString());
      }
      else if (prescaleValue instanceof String) {
        // just a single prescale=value, but putting in a prescaleSetupTable
        // make for more uniform processing
        prescaleSetupTable = new Table("{TYPE=" + prescaleValue + "}");
      }
    }
    return prescaleSetupTable;
  }

  /** Is this a PLOT/ALL of a type 1000 file (part of initPrescale)
   *  And no other scaling other than auto-scaling has been specified
   *  This requires bf be a BaseFile that is a DataFile
   *  @return true if PLOT/ALL of 1000 file
   */
  private boolean is1000PlotAll () {
    if (!MA.getState("/ALL") || (MP.scale>MPlot.S_AUTO)) return false; // this is not a PLOT/ALL
    if (dfHeader==null) dfHeader = new DataFileHeader((DataFile)bf); // should need to initialize
    return  (dfHeader.isAbleToParseHeader()) && (dfHeader.getType() == 1000);
  }

  /** Get any command line prescale information and set the appropriate
   *  prescale preferences.  (part of open)<br>
   *  Note: This does NOT grab information from a files qualifiers
   */
  private void commandLineStatusUpdate () {
    // Grab the prescale TYPE or PRESCALE table (which can contain all the information)
    prescaleType =  MA.find("/PRESCALE") ? MA.getS("/PRESCALE","INTERNAL") : "";

    // Now grab values out of PRESCALE table if that is provided
    if (prescaleType.startsWith("{")) {
      Table prescaleTable = new Table(prescaleType);
      tableBasedStatusUpdate(prescaleTable); // note this handles setting type based fields as well as the others
    }
    else {
      // Now get lower precedence values
      // directly on command line is lower precedence than PRESCALE={table}
      prescalarOut = MA.getRaw("OUTCOPY");
      prescaleKeywordName = MA.getS(prescaler.KEYWORD_SWITCH,prescaleKeywordName);
      setTypeRelatedFields(prescaleType);
    }
  }

  /** Use PRESCALE table to update status  (part of open))<br>
   *  Values here get higher precedence than those appearing
   *  separately on the command line, so this need to be called
   *  after command line values are parsed
   *  <pre>
   *  possible keys:
   *  TYPE=                                      (mandatory)
   *    INTERNAL
   *    RANDOM - same as INTERNAL, but with RANDOM of 10% with max cap
   *    EXTRENAL - SCALETABLE provided in here or as external result
   *  RANDOM=percent                             (optional)
   *  SCALETABLE=actual table or result name     (optional)
   *  OUTCOPY=name of file to hold copy of data  (optional)
   *          mainly used when incoming data is piped data
   *  </pre>
   *  @param prescaleSetupTable table holding prescale setup information
   *  @since NeXtMidas 3.9.2 for NXM-3078
   */
  private void tableBasedStatusUpdate (Table prescaleSetupTable) {
    // use table values to set/override your basic prescale information
    prescaleType = prescaleSetupTable.getS("TYPE", "INTERNAL");
    prescaleType = prescaleType.toLowerCase();
    if (prescaleSetupTable.containsKey("SCALETABLE")) {
      Table tempTable = prescaleSetupTable.getTable("SCALETABLE");
      if (tempTable == null) {
        scaletableResultName = prescaleSetupTable.getS("SCALETABLE");
        scaleTable = M.getResults().getTable(scaletableResultName, scaleTable);
      }
      else {
        scaleTable = tempTable;
      }
    }

    // TODO probably need to turn off if result does not exist !!!!!!!!!!!!!!!!!!!!
    needScaleTable = ((scaleTable == null) || (scaleTable.isEmpty()));
    prescalarOut = prescaleSetupTable.getString("OUTCOPY");
    // it is important to set type related fields before setting
    // percent from RANDOM, as a direct RANDOM percent specification
    // should override the default 10 percent from TYPE=RANDOM
    setTypeRelatedFields(prescaleType);
    percent = prescaleSetupTable.getL("RANDOM", percent);
    prescaleKeywordName = prescaleSetupTable.getS(KEY_FOR_KEYWORD_NAME,prescaleKeywordName);
  }

  /** External Prescaling has been selected  (part of open)<br>
   *  1) If we are not in a pipe section, then the SCALETABLE should already be available,
   *  as commands each run to completion. If it is not, THE PRESCALING WILL BE TURNED OFF<br>
   *  2) If we are in a pipe section, we will wait forever until a SCALETABLE becomes available
   *  TODO: should there be an eventual time limit????
   *
   *  @return true if we are waiting on the SCALETABLE to be ready (do not have it and still
   *               doing prescaling)
   */
  private boolean externalPrescalingSetup () {
    boolean waitingOnPrescale = true;
    externalPrescale = true;
    runPrescaleCommand=false;
    inFileAfterPrescale = inFile; // with external prescaling there is only 1 file plot needs to be concerned with

    if(M.isPiped()) {
      // plot will handle the opening of the file and checking the scale table
    }
    else if (scaleTable != null && !scaleTable.isEmpty()) {
      // Don't need to try and get the scale table if we already have it
      waitingOnPrescale = false;
      externalPrescale = false;
    }
    else {
      // If we are not in a pipe and we don't already have a scale table, we messed up.
      M.warning("Not running prescaler=ext due to no scale table and not in a pipe/pipe section");
      waitingOnPrescale = false;
      needScaleTable = false;
    }
    return waitingOnPrescale;
  }

  /** Process the scaleTable by setting MP.scale if necessary and setting the
   *  layer min and max values if necessary
   *  @since 3.9.1
   */
  @InternalUseOnly
  public void processScaleTable () {
    if(scaleTable!=null) {
      if (scaleTable.containsKey("Y1")||scaleTable.containsKey("YMIN")||scaleTable.containsKey("Z1")||scaleTable.containsKey("ZMIN")) MP.scale &= ~MPlot.S_MIN;
      if (scaleTable.containsKey("Y2")||scaleTable.containsKey("YMAX")||scaleTable.containsKey("Z2")||scaleTable.containsKey("ZMAX")) MP.scale &= ~MPlot.S_MAX;
      setLayerMinMaxScale();
    }
  }

  /** Will iterate the layers that MPlot hold. Then apply the scale table values to that layer.
   *  @since NeXtMidas 3.9.1
   */
  private void setLayerMinMaxScale () {
    if(scaleTable == null || scaleTable.isEmpty()) return;
    needScaleTable=false;
    KeyObject.setKeys(MP,scaleTable,MA,KeyObject.SOFT);
    KeyObject.setKeys(MP.getBaseLayer(),scaleTable,MA,KeyObject.SOFT);
  }

  /** Internal prescaling launches the prescaler primitive in the background during
   *  the open() stage, via forming the command string and then using Shell to
   *  execute the command. (last step in open)
   */
  private void runPrescale () {
    //check corrected in 3.9.2, as long as it is an actual file we can be in a pipe section
    if ((prescalarOut == null) && inFile.startsWith("_")) prescalarOut = DEFAULT_OUTCOPY;
    String prescalerCmd = "prescaler IN="+inFile;
    if (prescalarOut!=null) {
      inFileAfterPrescale = prescalarOut;
      prescalerCmd += " OUT="+prescalarOut;
      needWaitToOpenOutInFile = true; // have separate out/in file
    }
    else {
      inFileAfterPrescale = inFile;
    }

    // in 3.9.2, get percent a different way and only add to command if in range
    if (percent <= 20) {
      prescalerCmd += " /RANDOM="+percent;
    }
    if(!prescalerKeywordSwitch.equals("")) prescalerCmd += " " + prescalerKeywordSwitch;
    plotPrescaler = (prescaler)Shell.execBackgroundCommand(M,prescalerCmd);
    scaleTable = plotPrescaler.table;
    // plot handles the opening of the file (previously handled here if pipe and not need wait)
  }

  /** Handles the loading of the scale table either from a Result value or
   *  directly from the prescaler primitive field
   *  @return true if loaded
   */
  public boolean tryLoadScaleTable() {
    boolean loaded = false;
    // scaleTable will automatically be pointing to prescale.table unless we are running as external process
    if (externalPrescale) {
      if (scaletableResultName == null) {
        scaleTable = MA.getTable("SCALETABLE");
      }
      else {
        // look up result directly
        scaleTable = M.getResults().getTable(scaletableResultName, scaleTable);
      }
    }
    if ((scaleTable!=null) && !(scaleTable.isEmpty())) {
      loaded = true;
    }
    return loaded;
  }

  /** Set field directly related to TYPE specification
   *  @param type type of prescaling being done
   */
  private void setTypeRelatedFields (String type) {
    prescaleType = prescaleType.toLowerCase();
    if (prescaleType.equals("kw")|| prescaleType.startsWith("key")) {
      preferKeyword = true;
    }
    else if (prescaleType.startsWith("ext")) {
      prescalerExternal = true;
    }
    else if (prescaleType.startsWith("rand")) {
      percent = -10; // negative percent means enforce the max cap on the percent
    }
  }

  /** Process this file's header (and keywords if selected)
   *
   *  @param needKeywords file's keywords should be extracted
   *  @return true if this file is compatible with the PRESCALE type selected
   */
  private boolean processFileHeaderAndKeywords (boolean needKeywords) {
    if (!(bf instanceof DataFile)) {
      // not even a DataFile
      M.warning("PLOT PRESCALE option requires the file be a DataFile, defaulting to regular scaling :"+bf);
      return false;
    }
    else {
      boolean validFile = false;
      boolean is1000or2000 = false;
      DataFile df = (DataFile)bf;
      if (dfHeader==null) dfHeader = new DataFileHeader(df);
      if(dfHeader.isAbleToParseHeader()) { // this is set if we were successful
        int dfType = dfHeader.getType();
        // we can always default to internal prescaling if file is 1000 or 2000 (always valid)
        is1000or2000 = validFile = (dfType >= 1000) &&  (dfType < 3000);
        if (!is1000or2000 && !needKeywords) {
          M.warning("PLOT INTERNAL PRESCALE option requires the file a 1000 or 2000 file, defaulting to regular scaling :"+df);
          return false;
        }
        scaleTable = new Table(); // create table that can be set from keywords
        if (needKeywords) {
          extHeaderKeywords = dfHeader.getExtendedHeaderKeywords();
          if (scaleTableFromKeywordFromFile()) {
            // we are scaling based on keywords!
            runPrescaleCommand = false;
            validFile = true; // this is valid file even if not 1000 or 2000, since it has the keyword
          }
          else {
            // We are switching to internal prescaling and will open the out/in file
            // later when the precale is complete and all the data has made it into this
            // out/in file - THIS WILL REQUIRE the FILE be 1000 or 2000
            if (is1000or2000) {
              needWaitToOpenOutInFile = true;
            }
            else {
              M.warning("No precale keyword was found in file and file is not 1000 or 2000, defaulting to regular scaling :"+df);
              return false;
            }
          }
        }
      }
      return validFile;
    }
  }

  /** Get if we need to wait to open the file used for prescaler
   *  output and plot input
   *
   *  @return true if we need to wait
   */
  public boolean getNeedWaitToOpenOutInFile () {
    return needWaitToOpenOutInFile;
  }

  /** The inFile plot will actually use. This may not be the original,
   *  especially if the original was a pipe
   *
   *  @return the inFile plot will plot
   */
  public String getInFileAfterPrescale() {
    return inFileAfterPrescale;
  }

  /** Get the file qualifiers table
   *
   *  @return the file qualifiers table
   */
  public Table getFileQualifiers() {
    return quals;
  }

  /** Alter the default on the largest size static 1000 file that
   *  will get a FULL PRESCALE (larger files get random sampling)
   *  @param maxSize largest size in elements
   */
  public void setMaxSizeFullPrescale (int maxSize) {
    this.maxSizeFullPrescale = maxSize;
  }

  /** Function that creates the setupTable from the keyword in the data file. If unsuccessful
   *  it will create the keyword switch=value for {@link #runPrescale(String)}
   *  @param df The file to retrieve the setupTable
   *  @return false if the setupTable was not created from the keyword
   */
  private boolean scaleTableFromKeywordFromFile () {
    if(prescaleKeywordName.equals("")) prescaleKeywordName = prescaler.KEYWORD_DEFAULT;
    prescalerKeywordSwitch = prescaler.KEYWORD_SWITCH  +"="+prescaleKeywordName;
    Object tempScale = extHeaderKeywords.get(prescaleKeywordName);
    if(tempScale != null) {
      scaleTable = new Table(tempScale.toString());
      return true;
    }
    return false;
  }

  @Override
  public String toString () {
    String preScaleState = "PRESCALE type="+prescaleType.toUpperCase()+", inFile:"+inFile+", plot data coming from:"
                            +inFileAfterPrescale +", waiting to open in/out file:"+needWaitToOpenOutInFile;
    if (prescalerExternal) {
      preScaleState += "\n      Selected external prescaling ... are conditions met to proceed:"+externalPrescale+" scaleTable="+scaleTable;
    }
    if (preferKeyword) {
      int startOfKeyword = prescalerKeywordSwitch.indexOf('=') + 1;
      preScaleState += (runPrescaleCommand) ? "\n      Unsuccessful in " : "\n      Successful in ";
      preScaleState += "basing scaling on keyword:"+prescalerKeywordSwitch.substring(startOfKeyword)+
                        "\nextheader keywords:\n"+extHeaderKeywords;
    }
    if (runPrescaleCommand) {
      if (!preferKeyword) preScaleState += "\n";
      preScaleState += "      Launching prescale cmd, processing "+percent+" percent of file, in/out file:"
                       +prescalarOut+ ", with prescaler instance:\n        "+plotPrescaler;
    }

    return preScaleState;
  }

  /** Set file qulaifiers
   *  @param qualifiers file qualifiers
   */
  public void setQualifiers (Table qualifiers) {
    this.quals = qualifiers;
  }

  /** Set the scale Table
   *  @param scaleTable table containing the scaling values
   */
  public void setScaleTable (Table scaleTable) {
    this.scaleTable = scaleTable;
  }

  /** Get if scale table is needed
   *  @return true if the scale table is needed
   */
  public boolean needsScaleTable () {
    return this.needScaleTable;
  }

  /** set debug mode
   *  @param debug true for debug on
   */
  public void setDebug (boolean debug) {
    this.debug = debug;
  }
}

