
/**
 * LifeTimeFitDialog.java
 * This dialog is used to fit data to single or multi exponential fits
 *
 * Created: October 19, 2004
 *
 * @author Nathan Stevens
 * @version 0.35 7/7/06
 */
 
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
import java.awt.datatransfer.*;
import javax.swing.event.*;
import java.util.*;

import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.axis.*;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.event.ChartChangeEvent;
import org.jfree.chart.event.ChartChangeListener;
import org.jfree.chart.renderer.*;
import org.jfree.chart.renderer.xy.*;

public class LifeTimeFitDialog extends JFrame implements ActionListener,
  DataPointSelectedListener {
  
  private SpectraDataSource spectraDataSource = null;
  private SpectraData spectraData = null;
  private String sd_name = null;
  private JLabel datastatusLB = null;
  private JLabel irfstatusLB = null;
  private JLabel locationLB = null;
  private JTextField minTF = null;
  private JTextField maxTF = null;
  private JRadioButton comp1RB = null;
  private JRadioButton comp2RB = null;
  private JRadioButton comp3RB = null;
  private JRadioButton comp4RB = null;
  private JCheckBox convoluteCB = null;
  private JTextField dfTF = null;
  private JTextField shiftTF = null;
  private JTextField offsetTF = null;
  private JTextField t0TF = null;
  private JTextField amp1TF = null;
  private JTextField tau1TF = null;
  private JTextField amp2TF = null;
  private JTextField tau2TF = null;
  private JTextField amp3TF = null;
  private JTextField tau3TF = null;
  private JTextField amp4TF = null;
  private JTextField tau4TF = null;
  private JCheckBox fix_shiftCB = null;
  private JCheckBox fix_offsetCB = null;
  private JTextArea textArea = null;
  private String time_delay = "0.00";
  private float[][] xydata = null;
  private float[][] irfdata = null; // IRF data
  private float[][] nirfdata = null; // normalized IRF data
  private float[] xfitdata = null;
  private float[] yfitdata = null;
  private float[] residual = null;
  private float precision = 100.0f; // This is used to find the index of the array
  private boolean convolute = false;
  private String min_string = null;
  private String amp1_string = null;
  private String amp2_string = null;
  private String amp3_string = null;
  private String amp4_string = null;
  
  // variables that stores the maximum time index, and zero time used
  // when normalizing the IRF
  private int maxindex = 0;
  private int zeroTindex = 0;
  private float offset = 0;
  private float zeroT = 0;
  private float t_add = 0;
  private float scaling_factor;
  
  // set the plot properties
  private Properties plotProperties = null;
  
  public LifeTimeFitDialog(String sd_name, SpectraDataSource sds, Properties prop) {
    this(sd_name, sds);
    this.plotProperties = prop;
  }
  
  public LifeTimeFitDialog(String sd_name, SpectraDataSource sds) {
    super("Lifetime Fit ( " + sd_name + " )");
    this.sd_name = sd_name;
    spectraDataSource = sds;
    
    // hides this dialog
		addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent we) {
				setVisible(false);
		  }
		});
    
    // Set the layout to border layout
    getContentPane().setLayout(new BorderLayout());
    
    // Some gui components
    Insets insets = null;
		JButton button;
		JLabel label;
    TitledBorder title1, title2, title3;
    
    // Add the panel to set the fit parameters
    JPanel panel = new JPanel();
    panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
    
    title1 = BorderFactory.createTitledBorder("Fit Parameters");
    panel.setBorder(title1);
    
    JPanel panel_A = new JPanel();
    panel_A.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    
    button = new JButton("Get Profile Data");
		button.setActionCommand("data");
		button.addActionListener(this);
    insets = new Insets(2, 2, 2, 2);
    button.setMargin(insets);
    panel_A.add(button);
    
    datastatusLB = new JLabel(" not set");
    datastatusLB.setForeground(Color.blue);
    panel_A.add(datastatusLB);
    
    panel.add(panel_A);
    
    JPanel panel_A2 = new JPanel();
    panel_A2.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    
    button = new JButton("Get IRF");
		button.setActionCommand("irfdata");
		button.addActionListener(this);
    insets = new Insets(2, 2, 2, 2);
    button.setMargin(insets);
    panel_A2.add(button);
    
    panel_A2.add(Box.createHorizontalStrut(5));
    
    button = new JButton("Get Stored IRF");
		button.setActionCommand("sirfdata");
		button.addActionListener(this);
    insets = new Insets(2, 2, 2, 2);
    button.setMargin(insets);
    panel_A2.add(button);
    
    panel_A2.add(Box.createHorizontalStrut(10));
    
    irfstatusLB = new JLabel(" not set");
    irfstatusLB.setForeground(Color.blue);
    panel_A2.add(irfstatusLB);
    
    
    panel.add(panel_A2);
    
    // add the panel that holds the division factor aka scaling factor
    JPanel sfpanel = new JPanel();
    //sfpanel.setLayout(new BoxLayout(sfpanel, BoxLayout.X_AXIS));
    sfpanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    
    label = new JLabel("Division Factor : ");
    sfpanel.add(label);
    
    dfTF = new JTextField("5", 3);
    dfTF.setActionCommand("scaledata");
		dfTF.addActionListener(this);
    sfpanel.add(dfTF);
    
    sfpanel.add(Box.createHorizontalStrut(80));
    
    button = new JButton("Scale Data");
		button.setActionCommand("scaledata");
		button.addActionListener(this);
    insets = new Insets(2, 2, 2, 2);
    button.setMargin(insets);
    sfpanel.add(button);
    
    panel.add(sfpanel);
    
    // add the B panel
    JPanel panel_B = new JPanel();
    panel_B.setLayout(new BoxLayout(panel_B, BoxLayout.X_AXIS));
    
    label = new JLabel("Current Location : ");
    panel_B.add(label);
    
    locationLB = new JLabel("0.0000000 ");
    locationLB.setForeground(Color.blue);
    panel_B.add(locationLB);
    
    panel_B.add(Box.createHorizontalGlue());
    
    button = new JButton("Min");
		button.setActionCommand("min");
		button.addActionListener(this);
    insets = new Insets(2, 2, 2, 2);
    button.setMargin(insets);
    panel_B.add(button);
    
    panel_B.add(Box.createHorizontalStrut(7));
    
    button = new JButton("Max");
		button.setActionCommand("max");
		button.addActionListener(this);
    insets = new Insets(2, 2, 2, 2);
    button.setMargin(insets);
    panel_B.add(button);
    
    panel_B.add(Box.createHorizontalStrut(2));
    
    panel.add(panel_B);
    
    JPanel panel_C = new JPanel();
    panel_C.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    
    label = new JLabel("Fitting Range : ");
    panel_C.add(label);
    
    minTF = new JTextField("0", 8);
    panel_C.add(minTF);
    
    label = new JLabel(" to ");
    panel_C.add(label);
    
    maxTF = new JTextField("0", 8);
    panel_C.add(maxTF);
    
    panel.add(panel_C);
    
    // Add the panel that allows the selection of different components
    JPanel panel_D = new JPanel();
    panel_D.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    
    label = new JLabel("No. of Components : ");
    panel_D.add(label);
    
    ButtonGroup group = new ButtonGroup();
    
    comp1RB = new JRadioButton("1");
    comp1RB.setSelected(true);
    group.add(comp1RB);
    panel_D.add(comp1RB);
    
    comp2RB = new JRadioButton("2");
    group.add(comp2RB);
    panel_D.add(comp2RB);
    
    comp3RB = new JRadioButton("3");
    group.add(comp3RB);
    panel_D.add(comp3RB);
    
    comp4RB = new JRadioButton("4");
    group.add(comp4RB);
    panel_D.add(comp4RB);
    
    panel.add(panel_D);
    
    // Add the panels that allows the input of the initial values
    JPanel ivpanel = new JPanel();
    ivpanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    
    label = new JLabel("shift");
    ivpanel.add(label);
    
    ivpanel.add(Box.createHorizontalStrut(15));
    
    shiftTF = new JTextField("0", 9);
    shiftTF.addActionListener(this);
    shiftTF.setActionCommand("shift");
    ivpanel.add(shiftTF);
    
    convoluteCB = new JCheckBox("Do Convolution");
    convoluteCB.addActionListener(this);
    convoluteCB.setActionCommand("convolute");
    
    ivpanel.add(convoluteCB);
    
    panel.add(ivpanel);
    
    ivpanel = new JPanel();
    ivpanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    
    label = new JLabel("offset  ");
    ivpanel.add(label);
    
    offsetTF = new JTextField("0", 9);
    ivpanel.add(offsetTF);
    
    label = new JLabel(" TO     ");
    ivpanel.add(label);
    
    t0TF = new JTextField("100", 9);
    ivpanel.add(t0TF);
    
    panel.add(ivpanel);
    
    ivpanel = new JPanel();
    ivpanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    
    label = new JLabel("Amp 1 ");
    ivpanel.add(label);
    
    amp1TF = new JTextField("1000", 9);
    ivpanel.add(amp1TF);
    
    label = new JLabel(" Tau 1 ");
    ivpanel.add(label);
    
    tau1TF = new JTextField("100", 9);
    ivpanel.add(tau1TF);
    
    panel.add(ivpanel);
    
    ivpanel = new JPanel();
    ivpanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    
    label = new JLabel("Amp 2 ");
    ivpanel.add(label);
    
    amp2TF = new JTextField("1000", 9);
    ivpanel.add(amp2TF);
    
    label = new JLabel(" Tau 2 ");
    ivpanel.add(label);
    
    tau2TF = new JTextField("100", 9);
    ivpanel.add(tau2TF);
    
    panel.add(ivpanel);
    
    ivpanel = new JPanel();
    ivpanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    
    label = new JLabel("Amp 3 ");
    ivpanel.add(label);
    
    amp3TF = new JTextField("1000", 9);
    ivpanel.add(amp3TF);
    
    label = new JLabel(" Tau 3 ");
    ivpanel.add(label);
    
    tau3TF = new JTextField("100", 9);
    ivpanel.add(tau3TF);
    
    panel.add(ivpanel);
    
    ivpanel = new JPanel();
    ivpanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    
    label = new JLabel("Amp 4 ");
    ivpanel.add(label);
    
    amp4TF = new JTextField("1000", 9);
    ivpanel.add(amp4TF);
    
    label = new JLabel(" Tau 4 ");
    ivpanel.add(label);
    
    tau4TF = new JTextField("100", 9);
    ivpanel.add(tau4TF);
    
    panel.add(ivpanel);
    
    // panel to allow fixeing of certain parameters
    ivpanel = new JPanel();
    ivpanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    
    label = new JLabel("Fix : ");
    ivpanel.add(label);
    
    fix_shiftCB = new JCheckBox("shift");
    fix_shiftCB.setSelected(true);
    ivpanel.add(fix_shiftCB);
    
    fix_offsetCB = new JCheckBox("offset");
    fix_offsetCB.setSelected(true);
    ivpanel.add(fix_offsetCB);
    
    panel.add(ivpanel);
    
    getContentPane().add(panel, "North");
    
    // Add the spectra
    panel = new JPanel();
    panel.setLayout(new BorderLayout());
    
    title2 = BorderFactory.createTitledBorder("Results");
    panel.setBorder(title2);
    
    textArea = new JTextArea("");
    textArea.setLineWrap(true);
    textArea.setWrapStyleWord(true);
    textArea.setEditable(false);
		
    JScrollPane areaScrollPane = new JScrollPane(textArea);
    areaScrollPane.setPreferredSize(new Dimension(10, 200));
		
    panel.add(areaScrollPane);
    
    getContentPane().add(panel, "Center");
    
    // Add the south button panel
		JPanel buttonPanel = new JPanel();
		buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
    
    button = new JButton("Fit");
		button.setActionCommand("fit");
		button.addActionListener(this);
    insets = new Insets(2, 2, 2, 2);
    button.setMargin(insets);
    buttonPanel.add(button);
    
    button = new JButton("Copy");
		button.setActionCommand("copy");
		button.addActionListener(this);
    insets = new Insets(2, 2, 2, 2);
    button.setMargin(insets);
    buttonPanel.add(button);
    
    button = new JButton("Copy Data");
		button.setActionCommand("copydata");
		button.addActionListener(this);
    insets = new Insets(2, 2, 2, 2);
    button.setMargin(insets);
    buttonPanel.add(button);
    
    button = new JButton("Save");
		button.setActionCommand("save");
		button.addActionListener(this);
    insets = new Insets(2, 2, 2, 2);
    button.setMargin(insets);
    buttonPanel.add(button);
    
    button = new JButton("Plot");
		button.setActionCommand("plot");
		button.addActionListener(this);
    insets = new Insets(2, 2, 2, 2);
    button.setMargin(insets);
    buttonPanel.add(button);
    
    buttonPanel.add(Box.createHorizontalGlue());
    
    button = new JButton("Close");
		button.setActionCommand("close");
		button.addActionListener(this);
    insets = new Insets(2, 2, 2, 2);
    button.setMargin(insets);
    buttonPanel.add(button);
    
    getContentPane().add(buttonPanel, "South");
  }
  
  // Method to handel action events
  public void actionPerformed(ActionEvent ae) {
    String cmd = ae.getActionCommand();
    
    if(cmd.equals("data")) {
      getData();
		}
    else if(cmd.equals("irfdata")) {
      getIRFData();
    }
    else if(cmd.equals("sirfdata")) {
      getStoredIRFData();
    }
    else if(cmd.equals("scaledata")) {
      scaleData();
		}
    else if(cmd.equals("min")) {
      minTF.setText(time_delay);
		}
    else if(cmd.equals("max")) {
      maxTF.setText(time_delay);
		}
    else if(cmd.equals("shift")) {
      setTimeShift();
		}
    else if(cmd.equals("convolute")) {
      setConvoluteParameters();
    }
    else if(cmd.equals("fit")) {
      fitData();
		}
    else if(cmd.equals("plot")) {
      plotData();
		}
    else if(cmd.equals("copy")) {
      copy();
		}
    else if(cmd.equals("copydata")) {
      copyData();
		}
    else if(cmd.equals("close")) {
      setVisible(false);
		}
    else {
      System.err.println("Action Command not handeled in class LifeTimeFitDialog...");
      System.err.println(cmd);
    }
  }
  
  // Method to set what data point was selected
  public void dataPointSelected(String time_delay) {
    this.time_delay = time_delay;
    locationLB.setText(time_delay + " ");
  }
  
  // Method to set the data to be fittted
  public void setSpectraData(SpectraData sd) {
    spectraData = sd;
    if(spectraData != null) {
      xydata = SpectraDataUtil.copy2DArray(sd.getData());
      
      datastatusLB.setText(" loaded");
      precision = xydata[1][0] - xydata[0][0];
      setFitRange();
    }
    else {
      datastatusLB.setText(" error, no data");
    }
    
    yfitdata = null;
    xfitdata = null;
    residual = null;
  }
  
  // Method to get new data set
  private void getData() {
    setSpectraData(spectraDataSource.getSpectraData());
  }
  
  // Method to get the instrument responce function data
  private void getIRFData() {
    SpectraData irfsd = spectraDataSource.getSpectraData();
    if(irfsd != null) {
      irfdata = irfsd.getData();
      normalizeIRF();
    }
  }
  
  // Method to get the stored instrument responce function
  private void getStoredIRFData() {
    SpectraData irfsd = spectraDataSource.getIRF();
    
    if(irfsd != null) {
      irfdata = irfsd.getData();
      float dt1 = irfdata[1][0] - irfdata[0][0];
      float dt2 = xydata[1][0] - xydata[0][0];
      
      // verify the data is on the the same time scale
      if(Math.abs(dt1 - dt2) <= 1.0f) {
        normalizeIRF();
      }
      else {
        irfdata = null;
        System.err.println("IRF and data are on different time scales, unable to continue...");
      }
    }
    else {
      System.err.println("No stored IRF data found...");
    }
  }
  
  // Method to normalize the instrument responce data and calculate the IRF
  private void normalizeIRF() {
    if(irfdata != null) {
      nirfdata = new float[irfdata.length][2];
      float max_intensity = 0.0f;
      int max_intensity_index = 0;
      
      // first get the max intensity and it's index
      for(int i = 0; i < irfdata.length; i++) {
        float intensity = irfdata[i][1];
        
        if(intensity > max_intensity) {
          max_intensity = intensity;
          max_intensity_index = i;
        }
      }
      
      // caculate the scaling factor
      scaling_factor = xydata[maxindex][1]/max_intensity;
      
      // now calculate the time shift and convert that values into a pixel count
      float time_shift = xydata[zeroTindex][0] - irfdata[max_intensity_index][0];
      float time_precision = irfdata[1][0] - irfdata[0][0];
      int pixel_shift = Math.round(time_shift/time_precision);
      
      // now create the normarlization array including pixel shifts
      int index = 0;
      for(int i = 0; i < nirfdata.length; i++) {
        nirfdata[i][0] = irfdata[i][0];
        index = i - pixel_shift;
        if(index > -1 && index < nirfdata.length) {  // make sure we don't get an exception 
          nirfdata[i][1] = irfdata[index][1]*scaling_factor;
        }
        else {
          nirfdata[i][1] = offset;
        }
      }
      
      /*debug code
      System.out.println("IRF Max intenisty " + max_intensity);
      System.out.println("Data Max intenisty " + xydata[maxindex][1]);
      System.out.println("Scaling factor is " + scaling_factor);
      System.out.println("New IRF Max intenisty " + nirfdata[max_intensity_index][1]);
      System.out.println("Time shift is " + time_shift + " pixels " + pixel_shift);
      */
      
      // update in GUI components now
      irfstatusLB.setText(" loaded");
      shiftTF.setText("" + pixel_shift);
    }
    else {
      System.err.println("No IRF data found.  Unable to normalize...");
    }
  }
  
  // Method to set the time shift value
  private void setTimeShift() {
    try {
      int pixel_shift = Integer.parseInt(shiftTF.getText());
      
      // now create the normarlization array including pixel shifts
      int index = 0;
      for(int i = 0; i < nirfdata.length; i++) {
        nirfdata[i][0] = irfdata[i][0];
        index = i - pixel_shift;
        if(index > -1 && index < nirfdata.length) {  // make sure we don't get an exception 
          nirfdata[i][1] = irfdata[index][1]*scaling_factor;
        }
        else {
          nirfdata[i][1] = offset;
        }
      }
    }
    catch(NumberFormatException nfe) { }
  }
  
  // Method to set the parameters when doing convolution
  private void setConvoluteParameters() {
    convolute = convoluteCB.isSelected();
    
    if(convolute && nirfdata != null) {
      // store the old parameters
      min_string = minTF.getText();
      amp1_string = amp1TF.getText();
      amp2_string = amp2TF.getText();
      amp3_string = amp3TF.getText();
      amp4_string = amp4TF.getText();
      
      // now set some new onces
      minTF.setText("" + nirfdata[0][0]);
      amp1TF.setText("1");
      amp2TF.setText("1");
      amp3TF.setText("1");
      amp4TF.setText("1");
    }
    else if(min_string != null) {
      minTF.setText(min_string);
      amp1TF.setText(amp1_string);
      amp2TF.setText(amp2_string);
      amp3TF.setText(amp3_string);
      amp4TF.setText(amp4_string);
    }
  }
  
  // Method to set the range of data to be fitted
  private void setFitRange() {
    float max = 0f;
    float min = 0f;
    float value = 0;
    float lastvalue = 0;
    
    // just find the maximum intensity and set it to the min time
    for(int i = 0; i < xydata.length; i++) {
      value = xydata[i][1];
      if(value > lastvalue) {
        lastvalue = value;
        min = xydata[i][0];
        maxindex = i;
      }
    }
    
    max = xydata[xydata.length -1][0]; // The max will just be the last item in the data array
    
    // now calculate the and zero time and offset
    offset = xydata[0][1]; // should really be averaging some data points to get this
    
    float halfheight = (xydata[maxindex][1] - offset)/2.0f;
    float prec = halfheight*0.2f;  // set the precision to within 20% of this value
    
    float diff = 0;
    for(int i = 0; i <= maxindex; i++) {
      diff = Math.abs(halfheight - xydata[i][1]);
      if(diff <= prec) {
        zeroT = xydata[i][0];
        zeroTindex = i;
      }
    }
    
    // display the values now
    offsetTF.setText(offset + "");
    t0TF.setText(zeroT + "");
    minTF.setText(min + "");
    maxTF.setText(max + "");
  }
  
  // Method to divide by a particular factor
  private void scaleData() {
    try {
      float division_factor = Float.parseFloat(dfTF.getText());
      
      for(int i = 0; i < xydata.length; i++) {
        xydata[i][1] /= division_factor;
      }
      
      yfitdata = null;
      xfitdata = null;
      residual = null;
      
      setFitRange();
      normalizeIRF();
    }
    catch(NumberFormatException nfe) { }
  }
  
  // Method to fit a particular data
  private void fitData() {
    RegressionFunction expfunc = null;
    
    double[] xdata = null;
    double[] ydata = null;
    double[] sdev = null;
    double[] start =null;
    double[] step = null;
    
    double offset = 0;
    double t0 = 0;
    double tmax = 0;
    double tshift = 0;
    float min = 0;
    float max = 0;
    float value = 0;
    float diff1 = 100000;
    float diff2 = 100000;
    int minIndex = 0;
    int maxIndex = 0;
    int size = 0;
    
    // set any parameters that need to be fixed
    boolean[] fix_params = new boolean[2];
    fix_params[0] = fix_shiftCB.isSelected();
    fix_params[1] = fix_offsetCB.isSelected();
    
    // set the min and max values
    try {
      min = Float.parseFloat(minTF.getText());
      max = Float.parseFloat(maxTF.getText());
      offset = Double.parseDouble(offsetTF.getText());
      t0 = Double.parseDouble(t0TF.getText());
      
      // locate the indices we need
      for(int i = 0; i < xydata.length; i++) {
        diff1 = Math.abs(xydata[i][0] - min);
        diff2 = Math.abs(xydata[i][0] - max);
        
        if(diff1 <= precision) {
          minIndex = i;
        }
        else if(diff2 <= precision) {
          maxIndex = i;
        }
      }
      
      size = (maxIndex - minIndex) + 1;
      xdata = new double[size];
      ydata = new double[size];
      sdev = new double[size];
      
      // Fill the data array now
      int index = minIndex;
      for(int i = 0; i < size; i++) {
        if(!convolute) {
          xdata[i] = xydata[index][0];
        }
        else {
          xdata[i] = xydata[index][0] - xydata[minIndex][0];
        }
        
        ydata[i] = xydata[index][1];
        sdev[i] = 1.0d;
        index++;
      }
      
      // set time zero to minimum time if using convolution
      if(convolute) {
        t0 = xydata[minIndex][0];
        t_add = (float)t0;
        tmax = xdata[size -1];
      }
      else {
        t_add = 0.0f;
      }
      
      // now create our function
      if(comp1RB.isSelected()) {
        int param_count = 2;
        
        if(!fix_params[0]) {
          param_count++;
        }
        
        if(!fix_params[1]) {
          param_count++;
        }
        
        start = new double[param_count];
        step = new double[param_count];
        
        start[0] = Double.parseDouble(amp1TF.getText());
        start[1] = Double.parseDouble(tau1TF.getText());
        
        step[0] = 0.01d;
        step[1] = 0.01d;
        
        if(param_count == 3 && fix_params[0]) {
          start[2] = 0.0d;
          step[2] = 0.1d;
        }
        else if(param_count == 3) {
          start[2] = offset;
          step[2] = 0.1d;
        }
        
        if(param_count == 4) {
          start[2] = 0.0d;
          step[2] = 0.1d;
          start[3] = offset;
          step[3] = 0.1d;
        }
        
        expfunc = new SingleExponent(offset, t0, tmax, nirfdata, convolute, fix_params);
      }
      else if(comp2RB.isSelected()) {
        int param_count = 4;
        
        if(!fix_params[0]) {
          param_count++;
        }
        
        if(!fix_params[1]) {
          param_count++;
        }
        
        start = new double[param_count];
        step = new double[param_count];
        
        start[0] = Double.parseDouble(amp1TF.getText());
        start[1] = Double.parseDouble(tau1TF.getText());
        start[2] = Double.parseDouble(amp2TF.getText());
        start[3] = Double.parseDouble(tau2TF.getText());
        
        step[0] = 0.01d;
        step[1] = 0.01d;
        step[2] = 0.01d;
        step[3] = 0.01d;
        
        if(param_count == 5 && fix_params[0]) {
          start[4] = 0.0d;
          step[4] = 0.1d;
        }
        else if(param_count == 5) {
          start[4] = offset;
          step[4] = 0.1d;
        }
        
        if(param_count == 6) {
          start[4] = 0.0d;
          step[4] = 0.1d;
          start[5] = offset;
          step[5] = 0.1d;
        }
        
        expfunc = new DoubleExponent(offset, t0, tmax, nirfdata, convolute, fix_params);
      }
      else if(comp3RB.isSelected()) {
        int param_count = 6;
        
        if(!fix_params[0]) {
          param_count++;
        }
        
        if(!fix_params[1]) {
          param_count++;
        }
        
        start = new double[param_count];
        step = new double[param_count];
        
        start[0] = Double.parseDouble(amp1TF.getText());
        start[1] = Double.parseDouble(tau1TF.getText());
        start[2] = Double.parseDouble(amp2TF.getText());
        start[3] = Double.parseDouble(tau2TF.getText());
        start[4] = Double.parseDouble(amp3TF.getText());
        start[5] = Double.parseDouble(tau3TF.getText());
        
        step[0] = 0.01d;
        step[1] = 0.01d;
        step[2] = 0.01d;
        step[3] = 0.01d;
        step[4] = 0.01d;
        step[5] = 0.01d;
        
        if(param_count == 7 && fix_params[0]) {
          start[6] = 0.0d;
          step[6] = 0.1d;
        }
        else if(param_count == 7) {
          start[6] = offset;
          step[6] = 0.1d;
        }
        
        if(param_count == 8) {
          start[6] = 0.0d;
          step[6] = 0.1d;
          start[7] = offset;
          step[7] = 0.1d;
        }
        
        expfunc = new TripleExponent(offset, t0, tmax, nirfdata, convolute, fix_params);
      }
      else { // comp4RB must be selected
        int param_count = 8;
        
        if(!fix_params[0]) {
          param_count++;
        }
        
        if(!fix_params[1]) {
          param_count++;
        }
        
        start = new double[param_count];
        step = new double[param_count];
        
        start[0] = Double.parseDouble(amp1TF.getText());
        start[1] = Double.parseDouble(tau1TF.getText());
        start[2] = Double.parseDouble(amp2TF.getText());
        start[3] = Double.parseDouble(tau2TF.getText());
        start[4] = Double.parseDouble(amp3TF.getText());
        start[5] = Double.parseDouble(tau3TF.getText());
        start[6] = Double.parseDouble(amp4TF.getText());
        start[7] = Double.parseDouble(tau4TF.getText());
        
        step[0] = 0.01d;
        step[1] = 0.01d;
        step[2] = 0.01d;
        step[3] = 0.01d;
        step[4] = 0.01d;
        step[5] = 0.01d;
        step[6] = 0.01d;
        step[7] = 0.01d;
        
        if(param_count == 9 && fix_params[0]) {
          start[8] = 0.0d;
          step[8] = 0.1d;
        }
        else if(param_count == 9) {
          start[8] = offset;
          step[8] = 0.1d;
        }
        
        if(param_count == 10) {
          start[8] = 0.0d;
          step[8] = 0.1d;
          start[9] = offset;
          step[9] = 0.1d;
        }
        
        expfunc = new QuadrupleExponent(offset, t0, tmax, nirfdata, convolute, fix_params);
      }
    }
    catch(NumberFormatException nfe) {
      System.err.println("Can't fit data, invalid fit range or incorrect input value...");
      return;
    }
    
    // now start the actual fitting
    Regression regression = new Regression(xdata, ydata, sdev);
    regression.setNmax(15000);
    regression.setTrueFreq(true);  // used to calculate chi-square
    regression.simplex(expfunc, start, step);
    
    // create a data array for ploting results
    xfitdata = new float[size];
    yfitdata = new float[size];
    residual = new float[size];
    
    double[] calcdata = regression.getYcalc();
    double[] resdata = regression.getWeightedResiduals();
    
    for(int i = 0; i < xdata.length; i++) {
      xfitdata[i] = (float)xdata[i];
      yfitdata[i] = (float)calcdata[i];
      residual[i] = (float)resdata[i];
    }
    
    // now display the results
    displayResults(regression);
  }
  
  // method that displays the fit results
  private void displayResults(Regression regression) {
    double[] coeff = regression.getCoeff();
    String interations = "" + regression.getNiter();
    String chisqr = "" + (regression.getReducedChiSquare());
    // now print the values to the
    StringBuffer info = new StringBuffer();
    info.append("Interations : ").append(interations).append("\n");
    info.append("Chi Square : ").append(chisqr).append("\n\n");
    
    if(!fix_shiftCB.isSelected() && !fix_offsetCB.isSelected()) {
      info.append("Shift : ").append(coeff[coeff.length -2]).append("\n");
    }
    else if(!fix_shiftCB.isSelected()) {
       info.append("Shift : ").append(coeff[coeff.length -1]).append("\n");
    }
    
    if(!fix_offsetCB.isSelected()) {
      info.append("Offset : ").append(coeff[coeff.length-1]).append("\n");
    }
    
    if(comp1RB.isSelected()) {
      info.append("amp1 : ").append(coeff[0]).append("\n");
      info.append("tau1 : ").append(coeff[1]).append("\n");
    }
    else if(comp2RB.isSelected()) {
      // caculate the percentages now
      double total = Math.abs(coeff[0]) + Math.abs(coeff[2]);
      double t1p = Math.abs((coeff[0]/total)*100);
      double t2p = Math.abs((coeff[2]/total)*100);
      
      info.append("amp1 : ").append(coeff[0]).append("\n");
      info.append("tau1 : ").append(coeff[1]).append("\n");
      info.append("amp2 : ").append(coeff[2]).append("\n");
      info.append("tau2 : ").append(coeff[3]).append("\n\n");
      info.append("% tau1 : ").append(t1p).append("\n");
      info.append("% tau2 : ").append(t2p);
    }
    else if(comp3RB.isSelected()) {
      // caculate the percentages now
      double total = Math.abs(coeff[0]) + Math.abs(coeff[2]) + 
      Math.abs(coeff[4]);
      
      double t1p = Math.abs((coeff[0]/total)*100);
      double t2p = Math.abs((coeff[2]/total)*100);
      double t3p = Math.abs((coeff[4]/total)*100);
      
      info.append("amp1 : ").append(coeff[0]).append("\n");
      info.append("tau1 : ").append(coeff[1]).append("\n");
      info.append("amp2 : ").append(coeff[2]).append("\n");
      info.append("tau2 : ").append(coeff[3]).append("\n");
      info.append("amp3 : ").append(coeff[4]).append("\n");
      info.append("tau3 : ").append(coeff[5]).append("\n\n");
      info.append("% tau1 : ").append(t1p).append("\n");
      info.append("% tau2 : ").append(t2p).append("\n");
      info.append("% tau3 : ").append(t3p);
    }
    else {
      // caculate the percentages now
      double total = Math.abs(coeff[0]) + Math.abs(coeff[2]) + 
      Math.abs(coeff[4]) + Math.abs(coeff[6]);
      
      double t1p = Math.abs((coeff[0]/total)*100);
      double t2p = Math.abs((coeff[2]/total)*100);
      double t3p = Math.abs((coeff[4]/total)*100);
      double t4p = Math.abs((coeff[6]/total)*100);
      
      info.append("amp1 : ").append(coeff[0]).append("\n");
      info.append("tau1 : ").append(coeff[1]).append("\n");
      info.append("amp2 : ").append(coeff[2]).append("\n");
      info.append("tau2 : ").append(coeff[3]).append("\n");
      info.append("amp3 : ").append(coeff[4]).append("\n");
      info.append("tau3 : ").append(coeff[5]).append("\n");
      info.append("amp4 : ").append(coeff[6]).append("\n");
      info.append("tau4 : ").append(coeff[7]).append("\n\n");
      info.append("% tau1 : ").append(t1p).append("\n");
      info.append("% tau2 : ").append(t2p).append("\n");
      info.append("% tau3 : ").append(t3p).append("\n");
      info.append("% tau4 : ").append(t4p);
    }
    
    String infostring = info.toString();
    textArea.setText(infostring);
    textArea.setCaretPosition(0);
    
    // now give the data source its results
    spectraDataSource.setResults(infostring);
  }
  
  // Method to plot the data
  private void plotData() {
    String title = "Lifetime Fit Plot ( " + sd_name + " )";
    LifeTimePlotDialog plot = new LifeTimePlotDialog(this, title, xydata, xfitdata, yfitdata, 
                                                 residual, nirfdata, t_add, convolute, plotProperties);
    plot.setDataPointSelectedListener(this);
    plot.setVisible(true);
  }
  
  // Method to copy the fitted result to the system clip board
  private void copy() {
    Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
    String s = textArea.getText();
    StringSelection contents = new StringSelection(s);
    cb.setContents(contents, null);
  }
  
  // Method to copy the data. The data will be tab delimited
  private void copyData() {
    StringBuffer buffer = null;
    
    if(xfitdata != null && yfitdata != null) {
      buffer = new StringBuffer();
      
      if(!convolute) {
        buffer.append("Time\tIntensity\tTime\tFitted Intensity\tResidual\n");
      }
      else {
        buffer.append("Time\tIntensity\tTime_IRF\tIntensity_IRF\tTime\tFitted Intensity\tResidual\n");
      }
      
      int fitlimit = xfitdata.length - 1;
      int irflimit = 0;
      
      if(convolute) {
        irflimit = nirfdata.length -1;
      }
      
      
      for(int i = 0; i < xydata.length; i++) {
        buffer.append(xydata[i][0]).append('\t').append(xydata[i][1]);
        
        if(convolute && i < irflimit) {
          buffer.append('\t').append(nirfdata[i][0]).append('\t').append(nirfdata[i][1]);
        }
        
        if(i < fitlimit) {
          if(!convolute) {
            buffer.append('\t').append(xfitdata[i]).append('\t').append(yfitdata[i]).append('\t').
            append(residual[i]).append('\n');
          }
          else {
            buffer.append('\t').append(xfitdata[i] + t_add).append('\t').append(yfitdata[i]).append('\t').
            append(residual[i]).append('\n');
          }
        }
        else {
          buffer.append('\n');
        }
      }
    }
    else if(nirfdata != null) {
      buffer = new StringBuffer();
      
      buffer.append("Time\tIntensity\tTime_IRF\tIntensity_IRF\n");
      
      int irflimit = nirfdata.length -1;
       
      for(int i = 0; i < xydata.length; i++) {
        buffer.append(xydata[i][0]).append('\t').append(xydata[i][1]);
        
        if(i < irflimit) {
          buffer.append('\t').append(nirfdata[i][0]).append('\t').append(nirfdata[i][1]).append('\n');
        }
        else {
          buffer.append('\n');
        }
      }
      
    }
    
    if(buffer != null) {
      Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
      String s = buffer.toString();
      StringSelection contents = new StringSelection(s);
      cb.setContents(contents, null);
    }
  }
}//--end class LifeTimeFitDialog

// class the plots the data and fitted results
class LifeTimePlotDialog extends JDialog implements ChartChangeListener {
  private LifeTimeFitDialog lifetimeFitDialog = null;
  private ChartPanel chartPanel = null;
  private DataPointSelectedListener dsListener = null;
  private boolean log = false; // specifies whether to do log plot
  private boolean antialias = false; // specifies whether to draw the plots using antialias
  private boolean crosshair = false; // specifies whether to draw the crosshairs
  private float line_size = 1.5f; // the line size
  
  public LifeTimePlotDialog(LifeTimeFitDialog lifetimeFitDialog, String title, float[][] xydata, 
  float[] xdata, float[] ydata, float[] residual, float[][] nirfdata, float t_add, boolean convolute, Properties prop) {
    super(lifetimeFitDialog, title, false);
    this.lifetimeFitDialog = lifetimeFitDialog;
    
    // set the plot properties
    if(prop != null) {
      setPlotProperties(prop);
    }
    
    // see whether to get log data
    if(log) {
      xydata = getLogData(xydata);
      if(ydata != null) {
        ydata = getLogData(ydata);
      }
      if(nirfdata != null) {
        nirfdata = getLogData(nirfdata);
      }
    }
    
    getContentPane().setLayout(new BorderLayout());
    
    // add the tab pane here
    JTabbedPane tabbedPane = new JTabbedPane();
    
    // add the plot now
    XYSeriesCollection plotdata = new XYSeriesCollection();
		XYSeries ldata = new XYSeries("Data"); // The lifetime data
    XYSeries ldatafit = new XYSeries("Fitted Data"); // The fitted data
    XYSeries irfdata = new XYSeries("IRF Data"); // The instrument response data
    
    // count the number of series counted
    int series_count = 0;
    
    // add xydata now
    for(int i = 0; i < xydata.length; i++) {
      ldata.add(xydata[i][0], xydata[i][1]);
    }
    plotdata.addSeries(ldata);
    series_count++;
    
    // add the fitted data now if they are not null
    if(xdata != null && ydata != null) {
      for(int i = 0; i < xdata.length; i++) {
        if(!convolute) {
          ldatafit.add(xdata[i], ydata[i]);
        }
        else {
          ldatafit.add(xdata[i] + t_add, ydata[i]);
        }
      }
      plotdata.addSeries(ldatafit);
      series_count++;
    }
    
    // add the instrument responce function plot now if needed
    if(convolute && nirfdata != null) {
      for(int i = 0; i < nirfdata.length; i++) {
        irfdata.add(nirfdata[i][0], nirfdata[i][1]);
      }
      plotdata.addSeries(irfdata);
      series_count++;
    }
    
    JFreeChart chart = ChartFactory.createXYLineChart(null,
    "Time (ps/ns/us)", "Intensity", plotdata,
    PlotOrientation.VERTICAL,
    false,
    true,
    false);
    
    chart.setBackgroundPaint(Color.white);
    chart.addChangeListener(this);
    chart.setAntiAlias(antialias);
    XYPlot plot = (XYPlot) chart.getPlot();
    plot.setOutlinePaint(Color.black);
    NumberAxis axis = (NumberAxis) plot.getRangeAxis();
    axis.setAutoRangeIncludesZero(false);
    axis.setAutoRangeMinimumSize(1.0);
    plot.setDomainCrosshairVisible(crosshair);
    plot.setDomainCrosshairLockedOnData(false);
    //plot.setRangeCrosshairVisible(crosshair);
    //plot.setRangeCrosshairLockedOnData(false);
    
    XYLineAndShapeRenderer r1 = (XYLineAndShapeRenderer)plot.getRenderer();
    BasicStroke stroke = new BasicStroke(line_size);
    
    for(int i = 0; i < series_count; i++) {
      r1.setSeriesStroke(i, stroke);
    }
    
    chartPanel = new ChartPanel(chart);
    //chartPanel.setMaximumDrawHeight(350);
    //chartPanel.setMinimumDrawHeight(350);
    chartPanel.setDomainZoomable(true);
    chartPanel.setRangeZoomable(true);
    
    //getContentPane().add(chartPanel);
    tabbedPane.addTab("Lifetime Profile", null, chartPanel, "The Fit of LifeTime Data...");
    tabbedPane.setSelectedIndex(0);
    
    // add the residual plot now
    if(residual != null) {
      XYSeriesCollection residualdata = new XYSeriesCollection();
      XYSeries rdata = new XYSeries("Residual"); // The lifetime data
      
      for(int i = 0; i < xdata.length; i++) {
        if(!convolute) {
          rdata.add(xdata[i], residual[i]);
        }
        else {
          rdata.add(xdata[i] + t_add, residual[i]);
        }
      }
      
      residualdata.addSeries(rdata);
      
      JFreeChart chart2 = ChartFactory.createXYLineChart(null,
      "Time (ps/ns/us)", "", residualdata,
      PlotOrientation.VERTICAL,
      false,
      true,
      false);
      
      chart2.setBackgroundPaint(Color.white);
      chart2.addChangeListener(this);
      chart2.setAntiAlias(antialias);
      XYPlot plot2 = (XYPlot) chart2.getPlot();
      plot2.setOutlinePaint(Color.black);
      NumberAxis axis2 = (NumberAxis) plot.getRangeAxis();
      axis2.setAutoRangeIncludesZero(false);
      axis2.setAutoRangeMinimumSize(1.0);
      
      XYLineAndShapeRenderer r2 = (XYLineAndShapeRenderer)plot2.getRenderer();
      stroke = new BasicStroke(line_size);
      r2.setSeriesStroke(0, stroke);
      
      ChartPanel chartPanel2 = new ChartPanel(chart2);
      //chartPanel2.setMaximumDrawHeight(50);
      chartPanel2.setDomainZoomable(true);
      chartPanel2.setRangeZoomable(true);
      
      //getContentPane().add(chartPanel2, "Center");
      tabbedPane.addTab("Residual", null, chartPanel2, "The Residual Data...");
    }
    
    getContentPane().add(tabbedPane, "Center");
    
    // set the size
    setSize(new Dimension(500,405));
  }
  
  // Method to set the plot properties
  private void setPlotProperties(Properties prop) {
    String key = "log";
    String value =  prop.getProperty(key);
    if(value != null && value.equals("yes")) {
      log = true;
    }
    else {
      log = false;
    }
    
    key = "antialias";
    value =  prop.getProperty(key);
    if(value != null && value.equals("yes")) {
      antialias = true;
    }
    else {
      antialias = false;
    }
    
    key = "crosshair";
    value =  prop.getProperty(key);
    if(value != null && value.equals("yes")) {
      crosshair = true;
    }
    else {
      crosshair = false;
    }
    
    key = "line_size";
    value =  prop.getProperty(key);
    if(value != null) {
      line_size = Float.parseFloat(value);
    }
  }
  
  // Method to set the wavelength Selected Listener
  public void setDataPointSelectedListener(DataPointSelectedListener dsListener) {
    this.dsListener = dsListener;
  }
  
  // Method to listen to chart change events
  public void chartChanged(ChartChangeEvent event) {
    if(chartPanel == null) { //prevents null pointer exceptions
      return;
    }
    
    JFreeChart chart = chartPanel.getChart();
    XYPlot plot = chart.getXYPlot();
    float xx = (float)plot.getDomainCrosshairValue();
    float yy = (float)plot.getRangeCrosshairValue();
    
    if(dsListener != null) {
      dsListener.dataPointSelected(""+ xx);
    }
  }
  
  // Method to return the log of xydata
  private float[] getLogData(float[] ydata) {
    float[] nydata = new float[ydata.length];
    
    for(int i = 0; i < ydata.length; i++) {
      if(ydata[i] > 1.0f) { // avoid negative numbers
        nydata[i] = (float)Math.log(ydata[i]);
      }
      else {
         nydata[i] = 0.0f;
      }
    }
    
    return nydata;
  }
  
  // Method to return the log of xydata
  private float[][] getLogData(float[][] xydata) {
    float[][] nxydata = new float[xydata.length][2];
    
    for(int i = 0; i < xydata.length; i++) {
      nxydata[i][0] = xydata[i][0];
      
      if(xydata[i][1] > 1.0f) { // avoid negative numbers
        nxydata[i][1] = (float)Math.log(xydata[i][1]);
      }
      else {
         nxydata[i][1] = 0.0f;
      }
    }
    
    return nxydata;
  }
}//--end class LifeTimePlotDialog

// class that implements a a single exponential function
class SingleExponent implements RegressionFunction {
  private double irf1 = 0.0d;  // stores previous value of irf intergral
  private int index = 0;
  private double offset = 0.0d;
  private double tmax = 0.0d;
  private double t0 = 0.0d;
  private float[][] irfdata = null;
  private boolean convolute = false;
  private boolean[] fix_params = null;
  
  public SingleExponent(double offset, double t0, double tmax, 
  float[][] irfdata, boolean convolute, boolean[] fix_params) {
    this.offset = offset;
    this.t0 = t0;
    this.tmax = tmax;
    this.irfdata = irfdata;
    this.convolute = convolute;
    this.fix_params = fix_params;
  }
  
  public double function(double[] p, double[] x) {
    double y = 0.0d;
    
    if(!fix_params[1]) {
      if(!fix_params[0]) {
        offset = p[3];
      }
      else {
        offset = p[2];
      }
    }
    
    if(!convolute) {
      y = offset + p[0]*Math.exp(-(x[0] - t0)/p[1]);
    }
    else {
      double dt =  irfdata[1][0] - irfdata[0][0];
      double E1 = 0.0d;
      double E2 = 0.0d;
      
      if((x[0] + 1) >= (irfdata[0][0] - t0) && index+ 1 < irfdata.length) {
        E1 = irfdata[index][1];
        E2 = irfdata[index + 1][1];
        index++;
      }
      else {
        E1 = 0.0d;
        E2 = 0.0d;
      }
      
      double P = Math.exp((-1*dt)/p[1]);
      double S = p[0]*(dt/2);
      
      irf1 = irf1*P + S*(E2 + E1*P);
      
      y = offset + irf1;
      
      // check to see if to reset the irf value
      if(x[0] + 1 >= tmax) {
        irf1 = 0.0d;
        index = 0;
      }
    }
    
    return y;
  }
}//--end class SingleExponent

// class that implements a a double exponential function
class DoubleExponent implements RegressionFunction {
  private double irf1 = 0.0d;  // stores previous value
  private double irf2 = 0.0d;  // stores previous value
  private int index = 0;
  private double offset = 0.0d;
  private double t0 = 0.0d;
  private double tmax = 0.0d;
  private float[][] irfdata = null;
  private boolean convolute = false;
  private boolean[] fix_params = null;
  
  public DoubleExponent(double offset, double t0, double tmax,
  float[][] irfdata, boolean convolute, boolean[] fix_params) {
    this.offset = offset;
    this.t0 = t0;
    this.tmax = tmax;
    this.irfdata = irfdata;
    this.convolute = convolute;
    this.fix_params = fix_params;
  }
  
  public double function(double[] p, double[] x) {
    double y = 0.0d;
    
    if(!fix_params[1]) {
      if(!fix_params[0]) {
        offset = p[5];
      }
      else {
        offset = p[4];
      }
    }
    
    if(!convolute) {
      y = offset + p[0]*Math.exp(-(x[0] - t0)/p[1]) + 
      p[2]*Math.exp(-(x[0] - t0)/p[3]);
    }
    else {
      double dt =  irfdata[1][0] - irfdata[0][0];
      double E1 = 0.0d;
      double E2 = 0.0d;
      
      if((x[0] + 1) >= (irfdata[0][0] - t0) && index+ 1 < irfdata.length) {
        E1 = irfdata[index][1];
        E2 = irfdata[index + 1][1];
        index++;
      }
      
      double P1 = Math.exp((-1*dt)/p[1]);
      double S1 = p[0]*(dt/2);
      double P2 = Math.exp((-1*dt)/p[3]);
      double S2 = p[2]*(dt/2);
      
      irf1 = irf1*P1 + S1*(E2 + E1*P1);
      irf2 = irf2*P2 + S2*(E2 + E1*P2);
      
      y = offset + irf1 + irf2;
      
      // check to see if we at the end of our time if so rest inde and irf_prev
      if((x[0] + 1) >= tmax) {
        irf1 = 0.0d;
        irf2 = 0.0d;
        index = 0;
      }
    }
    
    return y;
  }
}//--end class DoubleExponent

// class that implements a a triple exponential function
class TripleExponent implements RegressionFunction {
  private double irf1 = 0.0d;  // stores previous value
  private double irf2 = 0.0d;  // stores previous value
  private double irf3 = 0.0d;  // stores previous value
  private int index = 0;
  private double offset = 0.0d;
  private double t0 = 0.0d;
  private double dt = 1.0d;
  private double tmax = 0.0d;
  private float[][] irfdata = null;
  private boolean convolute = false;
  private boolean[] fix_params = null;
  
  public TripleExponent(double offset, double t0, double tmax, 
  float[][] irfdata, boolean convolute, boolean[] fix_params) {
    this.offset = offset;
    this.t0 = t0;
    this.tmax = tmax;
    this.irfdata = irfdata;
    this.convolute = convolute;
    this.fix_params = fix_params;
    
    if(convolute) {
      dt =  irfdata[1][0] - irfdata[0][0];
    }
  }
  
  public double function(double[] p, double[] x) {
    double y = 0.0d;
    
    if(!fix_params[1]) {
      if(!fix_params[0]) {
        offset = p[7];
      }
      else {
        offset = p[6];
      }
    }
    
    if(!convolute) {
      y = offset + p[0]*Math.exp(-(x[0] - t0)/p[1]) + 
      p[2]*Math.exp(-(x[0] - t0)/p[3]) +
      p[4]*Math.exp(-(x[0] - t0)/p[5]);
    }
    else {
      double dt =  irfdata[1][0] - irfdata[0][0];
      double E1 = 0.0d;
      double E2 = 0.0d;
      
      if((x[0] + 1) >= (irfdata[0][0] - t0) && index+ 1 < irfdata.length) {
        E1 = irfdata[index][1];
        E2 = irfdata[index + 1][1];
        index++;
      }
      
      double P1 = Math.exp((-1*dt)/p[1]);
      double S1 = p[0]*(dt/2);
      double P2 = Math.exp((-1*dt)/p[3]);
      double S2 = p[2]*(dt/2);
      double P3 = Math.exp((-1*dt)/p[5]);
      double S3 = p[4]*(dt/2);
      
      irf1 = irf1*P1 + S1*(E2 + E1*P1);
      irf2 = irf2*P2 + S2*(E2 + E1*P2);
      irf3 = irf3*P3 + S3*(E2 + E1*P3);
      
      y = offset + irf1 +irf2 + irf3;
      
      // check to see if we at the end of our time if so rest inde and irf_prev
      if((x[0] + 1) >= tmax) {
        irf1 = 0.0d;
        irf2 = 0.0d;
        irf3 = 0.0d;
        index = 0;
      }
    }
    return y;
  }
}//--end class TripleExponent

// class that implements a a triple exponential function
class QuadrupleExponent implements RegressionFunction {
  private double irf1 = 0.0d;  // stores previous value
  private double irf2 = 0.0d;  // stores previous value
  private double irf3 = 0.0d;  // stores previous value
  private double irf4 = 0.0d;  // stores previous value
  private int index = 0;
  private double offset = 0.0d;
  private double dt = 0.0d;
  private double t0 = 0.0d;
  private double tmax = 0.0d;
  private float[][] irfdata = null;
  private boolean convolute = false;
  private boolean[] fix_params = null;
  
  public QuadrupleExponent(double offset, double t0, double tmax, 
  float[][] irfdata, boolean convolute, boolean[] fix_params) {
    this.offset = offset;
    this.t0 = t0;
    this.tmax = tmax;
    this.irfdata = irfdata;
    this.convolute = convolute;
    this.fix_params = fix_params;
    
    if(convolute) {
      dt =  irfdata[1][0] - irfdata[0][0];
    }
  }
  
  public double function(double[] p, double[] x) {
    double y = 0.0d;
    
    if(!fix_params[1]) {
      if(!fix_params[0]) {
        offset = p[9];
      }
      else {
        offset = p[8];
      }
    }
    
    
    if(!convolute) {
      y = offset + p[0]*Math.exp(-(x[0] - t0)/p[1]) + 
      p[2]*Math.exp(-(x[0] - t0)/p[3]) + 
      p[4]*Math.exp(-(x[0] - t0)/p[5]) +
      p[6]*Math.exp(-(x[0] - t0)/p[7]);
    }
    else {
      double dt =  irfdata[1][0] - irfdata[0][0];
      double E1 = 0.0d;
      double E2 = 0.0d;
      
      if((x[0] + 1) >= (irfdata[0][0] - t0) && index+ 1 < irfdata.length) {
        E1 = irfdata[index][1];
        E2 = irfdata[index + 1][1];
        index++;
      }
      
      double P1 = Math.exp((-1*dt)/p[1]);
      double S1 = p[0]*(dt/2);
      double P2 = Math.exp((-1*dt)/p[3]);
      double S2 = p[2]*(dt/2);
      double P3 = Math.exp((-1*dt)/p[5]);
      double S3 = p[4]*(dt/2);
      double P4 = Math.exp((-1*dt)/p[7]);
      double S4 = p[6]*(dt/2);
      
      irf1 = irf1*P1 + S1*(E2 + E1*P1);
      irf2 = irf2*P2 + S2*(E2 + E1*P2);
      irf3 = irf3*P3 + S3*(E2 + E1*P3);
      irf4 = irf4*P4 + S4*(E2 + E1*P4);
      
      y = offset + irf1 + irf2 + irf3 + irf4;
      
      // check to see if we at the end of our time if so rest inde and irf_prev
      if((x[0] + 1) >= tmax) {
        irf1 = 0.0d;
        irf2 = 0.0d;
        irf3 = 0.0d;
        irf4 = 0.0d;
        index = 0;
      }
    }
    return y;
  }
}//--end class QuadrupleExponent
