package nxm.sys.lib;

import java.util.Arrays;
import java.util.logging.Level;

import nxm.sys.inc.InternalUseOnly;

/** PROTOTYPE class designed to support Tune Adjustments
 *  via D:SBT_# and D:JTIME_# entries in Type 2000 files
 *  The XSTART is normally given in RF in those cases,
 *  but to support the tune adjustments the raster display
 *  will need to display IF and just support accurate RF READOUTs
 *
 *  Prototype is starting with assuming NORMAL SNAP_SPECTRAL_SENSE
 *
 *  <pre>
 *  Required Keywords to support this mode:
 *  FS            digital sampling rate (Hz)
 *  VRF           the frequency to which the collector is tuned (Hz)
 *  FNOM          the intermediate frequency (IF) corresponding to the
 *                VRF to which the collector is tuned (Hz)
 *  SBT or SBT_#  the tuned center frequency of the signal of interest,
 *                translated backup to Radio Frequency (RF). SBT reflects
 *                the sub-band tuning of the collected signal. (Hz)
 *
 *  Required if SBT_# is specified:
 *  JTIME_#       required for each SBT_#
 *                (may possibly just give a warning and discard the SBT
 *                if this is missing)
 *
 *  Optional:
 *  LO_CORRECTION the correction factor applied to the local oscillator
 *                frequency due to various tuning errors. For most collectors,
 *                there is no correction, so this will default to 1.00000
 *                if not provided.
 *
 *  Base Equations:
 *    VRF_actual = VRF * LO_CORRECTION
 *    SBT = VRF_actual - FNOM + FTIF   where FTIF = tuned center frequency in IF
 *
 *  Working Equations:
 *    FTIF = SBT_1 - ((VRF_actual*LO_CORRECTION) - FNOM)
 *    RF(current_time_and_IF) = IF(current) + SBT(current_time) - FTIF
 *
 *  XSTART is adjusted to IF, even if specified in RF:
 *    1) XSTART set to 0 for real data
 *    2) XSTART set to -FS/2 for complex IQ data
 *    3) whether the IQ data was real or complex is
 *       determined by the original xstart
 *
 *  Signal Tuning Parameter example for real data:
 *
 *                    ..
 *                   .  .
 *       ............    .......................
 *      .                                       ...
 *     .             SBT         VRF               .
 *    |-------------|----|--------------------------|---- RF (MHz)
 *  1250            1251.2       1253              1255
 *
 *                   FTIF        FNOM
 *    |-------------|----|--------------------------|---- IF (MHz)
 *    0              1.20         3.0               5
 *                                                =FS/2
 *  </pre>
 *
 *  @author Jeanette McCall
 *
 */
@InternalUseOnly
public class TuneAdjust {

  /** SBT keyword values */
  private double[] sbts;
  /** JTIME keyword values */
  private double[] jtimes;
  /** FTIF */
  private double   ftif;
  /** IF XSTART */
  private double xstartIF;
  /** Tune Adjustments available */
  private volatile boolean tuneAdjustAvailable = false;

  /** VRF actual */
  private double vrf;
  /** FNOM - intermediate frequency (IF) corresponding to VRF */
  private double fnom;
  /** sampling frequency actual */
  private double fs;

  /** no correction */
  private static double LO_CORRECTION_DEFAULT = 1.00000;

  /** Construct from SBT, JTIME, VRF, and FNOM keywords
   *
   *  @param kwds    Keywords object
   *  @param xstart  file xstart, which may need to be shifted from RF to IF
   *  @param ystart  time start
   */
  public TuneAdjust (Keywords kwds, double xstart, double ystart) {
    // check for mandatory keywords
    Double dNull = null;
    Double fsValue   = kwds.getD("FS",dNull);
    Double vrfValue  = kwds.getD("VRF",dNull);
    Double fnomValue = kwds.getD("FNOM",dNull);
    Double sbtFirstValue = kwds.getD("SBT_1",dNull);
    Double sbtOnlyValue  = kwds.getD("SBT",dNull);
    boolean haveSBTSeries = sbtFirstValue != null;
    boolean haveSBTSingle = sbtOnlyValue != null;

    boolean haveNeeded = checkIfHaveNeededKeywords(fsValue,vrfValue,
        fnomValue, haveSBTSeries || haveSBTSingle);

    if (!haveNeeded) return; // do not even bother setting anything

    if (!haveSBTSeries) {
      jtimes = new double[1];
      sbts   = new double[1];
      jtimes[0] = ystart;
      sbts[0]   = sbtOnlyValue;
    }
    else {
      int numEntries = getNumSBTEntries(kwds);
      jtimes = new double[numEntries];
      sbts   = new double[numEntries];
      int popEntries = populateJtimesSbts(kwds, sbtFirstValue);
      if (popEntries < numEntries) resizeDueToMissingEntries(popEntries); // handle bad keyword sets
    }

    // set fixed values (that do not vary over time or IF)
    double loCorrection = kwds.getD("LO_CORRECTION",LO_CORRECTION_DEFAULT);
    vrf = vrfValue * loCorrection;
    fnom = fnomValue;
    double rfOffset = vrf - fnom;
    ftif = sbts[0] - rfOffset;

    // auto shift xstart to IF if it is clearly RF
    // complex I/Q data should have a xstart of 0
    // real I/Q data should have an xstart of -FS/2
    // we are now dealing with 2000 PSD data, so the
    // way to tell if the I/Q data was real or complex
    // is by checking the xstart against the sampling rate
    fs = fsValue;
    double halfRealRange = fs/4;
    if (xstart <= halfRealRange) {
      // for now assume the file has are giving IF xstart,
      // though more detailed checks could be done
      xstartIF = xstart;
    }
    else {
      // xstart clearly out of the IF range, assume RF xstart
      // and form IF xstart
      //   IQ was complex if x range is FS in width
      //   IQ was real if x range is FS/2 in width
      boolean complexIQ = xstart < rfOffset - halfRealRange;
      xstartIF = (complexIQ) ? -fs/2 : 0;
    }

    tuneAdjustAvailable = true;
  }

  /** Get the appropriate RRF by grabbing the SBT right at or before the
   *  current time
   *  RF(current_time_and_IF) = IF(current) + SBT(current_time) - FTIF
   *
   *  @param   currentTime currrent time
   *  @param   freqIF      IF frequency
   *  @return  RF based on SBT and IF
   */
  public Double getRF (double currentTime, double freqIF) {
    // search for applicable SBT
    int srchIdx = Arrays.binarySearch(jtimes, currentTime);
    int iSBT = (srchIdx >= 0) ? srchIdx : -(srchIdx + 1) - 1; // exact index : insertionpoint - 1
    // calculate RF
    if (iSBT < 0) return null;
    double currSBT = sbts[iSBT];
    double currRF = freqIF + currSBT - ftif;
    return currRF;
  }

  /** Get IF based XStart
   *  <pre>
   *  real data         0
   *  complex data  -FS/2
   *  </pre>
   *  @return IF based XStart
   */
  public double getXStartIF () {
    return xstartIF;
  }

  /** Is Tune Adjustment Available?
   *
   *  @return true if the required Keywords were available to set this up
   */
  public boolean isTuneAdjustAvailable () {
    return tuneAdjustAvailable;
  }

  /** Populate the jtimes and sbts arrays using the SBT_# and JTIME_#
   *  keywords. If a JTIME entry is missing, WARN and do not add that
   *  value in. The first 'missing SBT' ends the search.
   *
   *  @param kwds      keywords
   *  @param firstSBT  first SBT value
   *  @return number of entries populated
   */
  private int populateJtimesSbts (Keywords kwds, Double firstSBT) {
    int i = 0; // array index
    int k = 1; // keyword number
    Double currentSBT = firstSBT;
    Double dNull = null;
    do {
      sbts[i] = currentSBT;
      Double currentJTime = kwds.getD("JTIME_"+k,dNull);
      if (currentJTime!=null) {
        jtimes[i] = currentJTime;
        i++; // good pair ... ready for next pair
      }
      else {
        LogUtilities.log(TuneAdjust.class, Level.WARNING,
            "Tune Adjustments: Missing JTIME_"+i+
            " entry - skipping this tune adjustment");
      }
      k++;
      currentSBT = kwds.getD("SBT_"+k,dNull);
    } while (currentSBT!=null);

    return i; // number entries populated
  }

  /** Handle case with missing SBT or missing JTIMES entries
   *
   *  @param popEntries number of entries populated in arrays
   */
  private void resizeDueToMissingEntries (int popEntries) {
    sbts = Arrays.copyOf(sbts, popEntries);
    jtimes = Arrays.copyOf(jtimes, popEntries);
    LogUtilities.log(TuneAdjust.class, Level.WARNING,"Tune Adjustments: only "+popEntries+
        " in effect");
  }

  private boolean checkIfHaveNeededKeywords (Double fsValue, Double vrfValue,
       Double fnomValue, boolean haveSBT) {
    if ((fsValue==null) || (vrfValue==null) || (fnomValue==null) || !haveSBT) {
      StringBuilder missingWarning = new StringBuilder(
          "Ignoring Tune Adjustments, as the following required keywords are missing:");
      if (fsValue==null)   missingWarning.append(" FS,");
      if (vrfValue==null)  missingWarning.append(" VRF,");
      if (fnomValue==null) missingWarning.append(" FNOM,");
      if (fnomValue==null) missingWarning.append(" SBT,");
      missingWarning.deleteCharAt(missingWarning.length());
      LogUtilities.log(TuneAdjust.class, Level.WARNING,missingWarning.toString());
      return false;
    }
    return true;
  }

  /** Get number of SBT entries (tune adjustments) in Keywords
  *
  *  @param kwds Keywords
  *  @return number of SBT entries
  */
  private static int getNumSBTEntries (Keywords kwds) {
    String origScope = kwds.getScope();
    String[] sbtKeys = kwds.getKeys("EXT", "SBT_");
    kwds.setScope(origScope);
    return sbtKeys.length;
  }

  public String toString() {
    if (!tuneAdjustAvailable) {
      return "TuneAdjust not avilable - could not load required keywords";
    }
    else {
      String toString = "TuneAdjust IF XStart:"+xstartIF+" FTIF:"+ftif+
          " SBTs:"+Arrays.toString(sbts)+" JTIMEs:"+Arrays.toString(jtimes);
      return toString;
    }
  }

}
