import com.xinapse.dynamic.SelectableDynamicModel;
import com.xinapse.dynamic.DynamicModel;
import com.xinapse.dynamic.DynamicResult;
import com.xinapse.dynamic.AbstractDynamicFrame;
import com.xinapse.dynamic.AutoCorrelationEstimate;
import com.xinapse.image.ReadableImage;
import com.xinapse.util.ReportGenerator;
import com.xinapse.util.MonitorWorker;
import com.xinapse.util.CancelledException;
import com.lowagie.text.DocumentException;

import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.CommandLine;

/**
   Simple example plugin for Dynamic Analysis tool to calculate the linear trend in the
   time-series (slope and optionally the intercept).
*/
public class LinearTrend extends SelectableDynamicModel {

  /**
     Command line option for computing the intercept (not just the slope).
     If you are to run the Dynamic Analysis tool from the command-line you can specify
     model-specific options like this one.
  */
  public static final Option INTERCEPT_OPTION = OptionBuilder.hasArg(false)
    .withDescription("Compute the intercept of the linear trend in time-series.")
    .withLongOpt("intercept")
    .create("i");

  /** The time between samples, in seconds. */
  private final float dt;

  /** Whether the option to compute the intercept has been selected, either from the command-line,
      or in the GUI. */
  private final boolean computeIntercept;

  /**
     You must provide a public no-arguments constructor.
  */
  public LinearTrend() {
    super();
    dt = 0;
    computeIntercept = false;
  }

  /**
     This constructor is called when this SelectableDynamicModel is selected in the GUI.

     @param dt the time between samples, in seconds.
     @param computeIntercept whether the option to compute the intercept was selected.
  */
  private LinearTrend(float dt, boolean computeIntercept) {
    this.dt = dt;
    this.computeIntercept = computeIntercept;
  }

  /**
     This constructor is called from the command-line Dynamic programme. The arguments may,
     or may not, be useful for constructing the model.

     @param commandLine the command-line that contains any selected Options.
     @param nTimePoints the number of time points set on the command-line.
     @param dt the time between images set on the command-line.
     @param nSteadyStates the number of pre-steady-state time points set on the command-line.
     @param nCols the number of columns of pixels in the input image(s).
     @param nRows the number of rows of pixels in the input image(s).
     @param nSlices the number of separate physical slice locations in the input image(s).
  */
  public LinearTrend(CommandLine commandLine, int nTimePoints, float dt,
                     int nSteadyStates, int nCols, int nRows, int nSlices) {
    this.dt = dt;
    if (commandLine.hasOption(INTERCEPT_OPTION.getOpt())) {
      this.computeIntercept = true;
    }
    else {
      this.computeIntercept = false;
    }
  }

  /**
     Returns the model name as it is to appear in the tab for selecting the model.

     @return the model name.
  */
  @Override public String getModelName() {
    return("Linear trend");
  }

  /**
     Returns a short description of the model. This may appear in tool tips etc.

     @return a short description of the model.
  */
  @Override public String getModelDescription() {
    return("computes the slope and intercept");
  }


  /**
     Returns the time between images.

     @return the time between images, in seconds.
  */
  @Override public float getDt() {
    return(dt);
  }

  /**
     Returns an array of the names the parameters that are computed.

     @return an array of the names the parameters that are computed. The length of the
     array returned must match the number of variables computed.
  */
  @Override public String[] getVarNames() {
    if (computeIntercept) {
      // Both the slope and intercept are computed, if that option was selected.
      return(new String[] {"slope", "intercept"});
    }
    else {
      // Just the slope is computed.
      return(new String[] {"slope"});
    }
  }

  /**
     Return the units of any variables that are computed (such as "mm/s", or "metres"). If any
     variable does not have units an empty String should be returned for that variable.

     @return the units of any variables that are computed. The length of the array
     returned must match the number of variables computed.
  */
  @Override public String[] getVarUnits() {
    if (computeIntercept) {
      // Both the slope and intercept are computed, if that option was selected.
      return(new String[] {"", ""});
    }
    else {
      // Just the slope is computed.
      return(new String[] {""});
    }
  }

  /**
     LinearTrend does not calculate statistical parametric maps and therefore no Bonferroni
     correction is needed and this model does not need to know the number of tests performed.

     @param N the number of statistical tests - ignored.
  */
  @Override public void setBonferroniN(float N) {
  }

  /**
     LinearTrend does not calculate statistical parametric maps and therefore no Bonferroni
     correction is needed.

     @return false.
  */
  @Override public boolean getDoBonferroni() {
    return(false);
  }

  /**
     Returns a DynamicModel.SpecifierPanel that can be used to specify any options for the
     model.

     @param parentFrame the AbstractDynamicFrame into which the DynamicModel.SpecifierPanel
     will be embedded.
     @param preferencesNodeName the name of the user Preferences node that may be used to
     retrieve any user preferences about the setup of the DynamicModel.SpecifierPanel.
  */
  @Override public DynamicModel.SpecifierPanel getSpecifierPanel(AbstractDynamicFrame parentFrame,
                                                                 String preferencesNodeName) {
    return(new LinearTrendPanel(parentFrame));
  }

  /**
     Calculate the result for one pixel for this model.

     If needed for your model fitting, you can access the AIF (plasma concentration values
     in the feeding artery) as the instance variable Cpa.

     @param S the time-series of signal intensity values for this pixel.
     @param col the pixel column number - unused.
     @param row the pixel row number - unused.
     @param slice the pixel slice number - unused.
     @param autoCorrelationEstimate will be null since this model does not use
     auto-correlation estimates.

     @throws CancelledException if the operation is cancelled by the user.
  */
  @Override public DynamicResult
    fit(float[] S, int col, int row, int slice, AutoCorrelationEstimate autoCorrelationEstimate,
        MonitorWorker worker) throws CancelledException {

    // Standard least-squares fit of a straight line to the data.
    double Sx = 0, Sy = 0, Sxx = 0, Sxy = 0;
    double t = 0;
    int n = S.length;
    for (int i = 0; i < n; i++) {
      Sx += t;
      Sy += S[i];
      Sxx += t * t;
      Sxy += t * S[i];
      t += dt;
    }
    double del = (n * Sxx) - (Sx * Sx);
    float slope = (float) (((n * Sxy) - (Sx * Sy)) / del);
    float intercept = (float) (((Sxx * Sy) - (Sx * Sxy)) / del);

    float[] yFit = new float[n];
    float rmsError = 0;
    t = 0;
    for (int i = 0; i < n; i++) {
      yFit[i] = (float) ((slope * t) + intercept);
      rmsError += (yFit[i] - S[i]) * (yFit[i] - S[i]);
      t += dt;
    }
    rmsError = (float) Math.sqrt(rmsError);

    if (computeIntercept) {
      return(new LinearTrendResult(slope, intercept, rmsError, yFit));
    }
    else {
      return(new LinearTrendResult(slope, rmsError, yFit));
    }
  }

  /**
     This model does not compute the root-mean-square difference between the model and the data.

     @return false.
  */
  @Override public boolean computesRMSDiff() {
    return(false);
  }

  @Override public boolean getCorrectAutoCorrelation() {
    return(false);
  }

  public static String getOptionName() {
    return("lineartrend");
  }

  public static Option[] getModelOptions() {
    return(new Option[] {INTERCEPT_OPTION});
  }


  /**
     A result for the fitting using LinearTrend.
  */
  public class LinearTrendResult extends DynamicResult {

    /**
       Create a new LinearTrendResult - with both slope and intercept having been computed.

       @param slope the slope of the trend line calculated.
       @param intercept the intercept of the trend line calculated.
       @param rmsError the root-mean-square difference between the model fit and the data.
       @param yFit the data that will be shown along with data to indicate the model fit.
       This data will be shown in reports.
    */
    LinearTrendResult(float slope, float intercept, float rmsError, float[] yFit) {
      super(new LinearTrend(), new float[] {slope, intercept}, rmsError, yFit);
    }

    /**
       Create a new LinearTrendResult - with only the slope having been computed.

       @param slope the slope of the trend line calculated.
       @param rmsError the root-mean-square difference between the model fit and the data.
       @param yFit the data that will be shown along with data to indicate the model fit.
       This data will be shown in reports.
    */
    LinearTrendResult(float slope, float rmsError, float[] yFit) {
      super(new LinearTrend(), new float[] {slope}, rmsError, yFit);
    }

    /**
       Returns the title of this model to be used in dialogs and reports.

       @return a title.
    */
    @Override public String getResultTitle() {
      if (computeIntercept) {
        return("Linear Trend - Slope and Intercept");
      }
      else {
        return("Linear Trend - Slope");
      }
    }
  }

  /**
     A DynamicModel.SpecifierPanel to specify the LinearTrend model in a GUI.
  */
  public static class LinearTrendPanel extends DynamicModel.SpecifierPanel {

    /** A JCheckBox that will appear in this Component to select whether the intercept is to
        be computed. */
    private final javax.swing.JCheckBox interceptCheckBox =
      new javax.swing.JCheckBox("Compute intercept");

    /**
       Create a LinearTrendPanel for embedding in the parentFrame.

       @param parentFrame the AbstractDynamicFrame into which this LinearTrendPanel will be
       embedded.
    */
    LinearTrendPanel(AbstractDynamicFrame parentFrame) {
      super(parentFrame);
      this.add(interceptCheckBox);
      interceptCheckBox.setToolTipText("Select if you want the intercept to be outputted");
    }

    /**
       Returns the LinearTrend currently selected. You can use your GUI components to
       specify any variable settings in the model.

       @param nTimePoints the number of time points in the time-series.
       @param nSlices the number of physical image slices in the dataset to be analyzed.
       @param dy the time between samples.
       @param image an example of the type of image to be analysed.
    */
    @Override public LinearTrend getModel(int nTimePoints, int nSlices, float dt,
                                          ReadableImage image) {
      return(new LinearTrend(dt, interceptCheckBox.isSelected()));
    }
  }
}
