package nxm.ucl.intr;

import static java.lang.Math.floor;
import static nxm.sys.lib.StateVector.computeMeasurements;

import nxm.sys.inc.Units;
import nxm.sys.lib.Convert;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.StateVector;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;

/** This is intended as a drop-in-replacement for the X-Midas DUAL5000 command
    that can be used by users migrating to NeXtMidas; all of the underlying
    computations are done by {@link StateVector#computeMeasurements}.
    @author  J. Czechowski IV
    @since   NeXtMidas 3.3.0
 */
public class Dual5000 extends Intrinsic {

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

  @Override
  public int process () {
    // SORT SOME THINGS OUT BASED ON THE INPUT =================================
    // In X-Midas the deltas were nominally taken from XD in the file as-is. This
    // creates scenarios where wackiness ensues since XD is usually an estimate
    // from SVINTERP and there can be situations where two files are used with
    // similar, but not quite identical, XD values. This presents situations
    // (such as one shown in %TEST_DUAL5000) where the number of points in the
    // output file is less than one should expect. With this approach the output
    // values are closer to what an X-Midas user would expect, despite doing a
    // better job of making sure that start and end times are the intersection
    // of the two files.
    DataFile    in1    = MA.getDataFile("IN1", "5000", "NH|VD", DataFile.INPUT); in1.open();
    DataFile    in2    = MA.getDataFile("IN2", "5000", "NH|VD", DataFile.INPUT); in2.open();
    StateVector sv1    = new StateVector(this, in1.getName());
    StateVector sv2    = new StateVector(this, in2.getName());
    Time        start1 = sv1.getStartTime();
    Time        start2 = sv2.getStartTime();
    Time        end1   = sv1.getEndTime();
    Time        end2   = sv2.getEndTime();
    double      delta1 = end1.diff(start1) / (in1.getSize()-1);
    double      delta2 = end2.diff(start2) / (in2.getSize()-1);
    boolean     ltc    = MA.getState("/LTC");
    double      ltcTol = (!ltc)? -2 : MA.getD("/LTCTOL", -1);
    DataFile    out;
    Time        start;
    double      delta;
    int         size;

    // Although it is tempting to use Position rather than StateVector in the
    // case where only a single point is given, this is not valid. The reason
    // is that X-Midas does not assume that a single-entry file is necessarially
    // a fixed point (i.e. it can have VEL=). Although this is a fairly silly
    // use case, it is presumably done for the benefit of users who found they
    // could using DUAL5000 to compare multiple unrelated configurations in a
    // single call (i.e. compare A->B,A->C,A->D) rather than making multiple
    // calls.
    if ((in1.getSize() == 1) && (in2.getSize() == 1)) {
      // This case is not in X-Midas, since X-Midas makes the implicit assumption
      // (as suggested in the explain file) that no more than 1 of the inputs
      // may be a single point.
      out   = MA.getDataFile("OUT", in1, DataFile.OUTPUT);
      start = start1;
      delta = in1.getDelta();      // delta1=Inf in this case
      size  = (int)in1.getSize();
    }
    else if (in1.getSize() == 1) {
      out   = MA.getDataFile("OUT", in2, DataFile.OUTPUT);
      start = start2;
      delta = delta2;
      size  = (int)in2.getSize();
    }
    else if (in2.getSize() == 1) {
      out   = MA.getDataFile("OUT", in1, DataFile.OUTPUT);
      start = start1;
      delta = delta1;
      size  = (int)in1.getSize();
    }
    else {
      Time end;
      out   = MA.getDataFile("OUT", in1, DataFile.OUTPUT);
      start = (start1.compareTo(start2) > 0)? start1 : start2;
      end   = (end1.compareTo(end2)     < 0)? end1   : end2;
      delta = (delta1 + delta2) / 2;

      // Since DUAL5000 often follows a call to CONVERT's SVINTERP we expect
      // relatively large rounding errors since SVINTERP sets delta to match
      // the *average* delta. Accordingly we use the following algorithm to
      // match what X-Midas's DUAL5000 is doing via its call to M$ALIGN (which
      // permits a 1e-3 rounding-error tolerance):
      //   size = (end-start) / delta
      //   size = floor(size + 0.999)      // round up, except if error <=1e-3
      //   size = size + 1                 // convert from num intervals to num points
      size = (int)floor((end.diff(start) / delta) + 1.999);
    }

    double rf  = -1;
    Object val = out.getKeyWord("RF");
    if (val != null) {
      rf = Convert.o2d(val);
    }

    // Although X-Midas allows the user to override RF, it does not permit the
    // start/delta to be overridden and presumes that both files will have the
    // same delta.
    rf    = MA.getD("/RF",       rf);
    delta = MA.getD("/DELTA",    delta);
    size  = MA.getL("/SIZE",     size);
    start = MA.getTime("/START", start);

         if (out.getTimeCode() == 0) {                          out.setXStart(start.getSec()); }
    else if (out.getXStart()   == 0) { out.setTime(start); }
    else                             { out.setTime(new Time()); out.setXStart(start.getSec()); }

    out.setXDelta(delta);
    out.setXUnits(Units.TIME);

    in1.close();
    in2.close();

    // CONFIGURE THE OUTPUT ====================================================
    boolean needRF = false;
    out.setComponents("");
    for (int i = 4; i <= MA.getNArgs(); i++) {
      String key = MA.getS(i);
           if (key.equals("R"   )) out.addComp("R",    "SD", Units.UNDEFINED, Units.DISTANCE_M);
      else if (key.equals("RR"  )) out.addComp("RR",   "SD", Units.UNDEFINED, Units.SPEED_M_S);
      else if (key.equals("DELT")) out.addComp("DELT", "SD", Units.UNDEFINED, Units.DELAY_S);
      else if (key.equals("DELF")) out.addComp("DELF", "SD", Units.UNDEFINED, Units.DOPPLER_HZ);
      else if (key.equals("SPRF")) out.addComp("SPRF", "SD", Units.UNDEFINED, Units.DIMENSIONLESS);
      else if (key.equals("AZ"  )) out.addComp("AZ",   "SD", Units.UNDEFINED, Units.ANGLE_DEG);
      else if (key.equals("EL"  )) out.addComp("EL",   "SD", Units.UNDEFINED, Units.ANGLE_DEG);
      else                         throw new MidasException("Unsupported function: "+key);

      if (key.equals("DELF")) needRF = true;
    }

    if (out.getNumberComponents() == 0) {
      // Unlike X-Midas (which would output an empty file in this case), if no
      // output components are specified, we include all of them, however we
      // do not guarentee order.
      out.addComp("R",    "SD", Units.UNDEFINED, Units.DISTANCE_M);
      out.addComp("RR",   "SD", Units.UNDEFINED, Units.SPEED_M_S);
      out.addComp("DELT", "SD", Units.UNDEFINED, Units.DELAY_S);
      out.addComp("DELF", "SD", Units.UNDEFINED, Units.DOPPLER_HZ);
      out.addComp("SPRF", "SD", Units.UNDEFINED, Units.DIMENSIONLESS);
      out.addComp("AZ",   "SD", Units.UNDEFINED, Units.ANGLE_DEG);
      out.addComp("EL",   "SD", Units.UNDEFINED, Units.ANGLE_DEG);
      needRF = true;
    }

    if (rf < 0) {
      // X-Midas always issues the following and then uses the 1 MHz default
      // whenever rf<0. However it seems silly to warn users about a missing
      // value when it clearly isn't required.
      if (needRF) {
        M.warning("No RF specified for doppler calc - using 1 MHz");
        rf = 1e6;
      }
      else {
        // Force computations that use RF to come out to 0.0 if somehow they are
        // present. The X-Midas version seemingly permits this via setting RF to
        // 0.0 (due to the "rf<0" check not matching "rf==0"), so we just
        // continue this on in the rf<0 case. [This really shouldn't do anything,
        // but it is here to make sure that any future additions to the
        // computeMeasurements(..) don't inadvertantly include a seemingly-valid
        // value when rf is bogus.]
        rf = 0.0;
      }
    }

    // WRITE OUT THE OUTPUT ====================================================
    Table tbl = new Table();

    out.open();
    out.setTime(start);
    for (int i = 0; i < size; i++) {
      Time time = (new Time(start)).addSec(i * delta);
      computeMeasurements(sv1, sv2, rf, tbl, true, time, ltcTol);
      out.setTimeAt(time);
      out.writeDataTable(tbl);
    }
    out.close();

    return FINISH;
  }
}
