//=====================================================================
// File:    SegregatingScore.java
// Class:   SegregatingScore
// Package: AFLPcore
//
// Author:  James J. Benham
// Date:    August 10, 1998
// Contact: james_benham@hmc.edu
//
// Genographer v1.0 - Computer assisted scoring of gels.
// Copyright (C) 1998  Montana State University
// 
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; version 2
// of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//
// The GNU General Public License is distributed in the file GPL
//=====================================================================

package AFLPcore;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

/**
 * This scores each bin by assigning different labels to the differnet
 * cutoff regions. A cutoff region is a region between two cutoffs, or 
 * between the cutoff and the zero level. For example, in a bin with
 * one cutoff, lanes that have a peak above the cutoff might be labeled "A",
 * and those below "B". For a bin with two cutoffs, those below both may
 * be "B", those between the two would be "h", and those above both would
 * be "A". The options can be used to set the number of levels and the
 * labels. If the number of cutoff levels for a lane does not match the
 * number expected by this function, an error will be thrown. Also, those
 * peaks that are above a cutoff are marked, and are then used to 
 * calculate the mean and standard deviation of the values in the bin.
 * It also will count the number of each label that occur in a bin.
 *
 * @see Cutoff
 * @see Lane
 *
 * @author James J. Benham
 * @version 1.0.0
 * @date August 10, 1998
 */

public class SegregatingScore extends ScoreFunction
{
  String[] labels;
  int numLevels;

  /**
   * Creates a new segregating score function. The default number of
   * levels is one.
   */
  public SegregatingScore()
  {
    name = "Segregating";
    descript = "Score a segregating population with labels like A and B.";
    helpFile = "segregating.html";

    // Assume we start with just one cutoff
    numLevels = 1;
    labels = new String[numLevels + 1];
    labels[0] = "A";
    labels[1] = "B";
  }

  /**
   * Scores a lane by seeing if any of the peaks cross the highest cutoff,
   * and then labeling it appropriately. It then sees if anything crosses
   * the next cutoff, and so on.
   *
   * @param cutoff  the cutoff used to determine the labels
   * @param peaks   the peaks in a region to compare to the cutoff
   *
   * @exception ScoringFailure  occurs if the number of levels in the
   *    cutoff do not match the number of levels that the function is
   *    expecting. This number can be changed by setting the options for
   *    this function.
   *
   * @see SegregatingScore#getOptions
   */
  public String score(Cutoff cutoff, DataList peaks)
  {
     // check to see that we have the correct number of cutoffs
    if(cutoff.getNumLevels() != numLevels)
      throw new ScoringFailure("Number of cutoff levels does not match " +
			       "expected value of " + numLevels);

    Peak pk;

    // Go through all of the levels
    for(int i=0; i < numLevels; i++)
      {
	// Look for a peak above the level
	for(int j=0; j < peaks.size(); j++)
	  {
	    pk = (Peak) peaks.dataAt(j);
	    // compare peak height to the cutoff at the peak location
	    if(pk.getHeight() >= cutoff.getCutoff(pk.getLocation(), 
						  numLevels - 1 -i))
	      {
		// we found a peak, so mark it and set the label
		pk.setMarkedState(true);
		return labels[i];
	      }
	  }
      }

    // we didn't find a peak above any of the cutoffs, so return the last
    // label
    return labels[numLevels];
  }

  /**
   * This returns a set of strings that describes the overall scoring of
   * the bin. It includes the mean of all of the peaks with non-zero
   * confidences as well as the standard deviation of those peaks. It
   * tells how many peaks were used to calculate the standard deviation
   * as well as how many peaks were defined ("used/defined"). It also
   * returns a count for each of the different types of labels. For example:
   * 
   * <p><tt>Mean: 98.38 StdDev: 0.06 #A: 20 #B: 14 from 20/20 peaks</tt>
   *
   * @param tags  the label for each lane.
   * @param peaks the peaks that were marked by the score method.
   *
   * @return a string with the information described above.
   */
  public String[] getInfo(String[] tags, DataList peaks)
  {
    String info[] = new String[1];

    int count[] = new int[numLevels + 1];
    for(int i=0; i < count.length; i++)
      count[i] = 0;

    // get the counts for each label
    for(int i=0; i < tags.length; i++)
      {
	for(int j=0; j < (numLevels + 1); j++)
	  if(tags[i].equals(labels[j]))
	    count[j]++;
      }
    
    // find the mean of the peaks
    Peak pk;
    double sum = 0;
    int numUsed = 0;
    for(int i=0; i < peaks.size(); i++)
      {
	pk = (Peak) peaks.dataAt(i);
	if(pk.getConfidence() != 0)
	  {
	    sum += pk.getLocation();
	    numUsed++;
	  }
      }

    double mean = Math.round(sum/numUsed*100)/100.0;

    // find the standard deviation using the formula 
    // sqrt( sum((mean-peak(i))^2)/(n-1) ), where n is the number of elements. 
    sum = 0;
    double squareValue;
    for(int i=0; i < peaks.size(); i++)
      {
	pk = (Peak) peaks.dataAt(i);
	if(pk.getConfidence() != 0)
	  {
	    squareValue = mean - pk.getLocation();
	    sum += squareValue * squareValue;
	  }
      }

    double stdDev = Math.round(Math.sqrt(sum/(numUsed - 1))*100)/100.0;

    // ==================== assemble the string ===================
    info[0] = "Mean: " + mean + " StdDev: " + stdDev;
    // add the counts
    for(int i=0; i < (numLevels + 1); i++)
      info[0] += " #" + labels[i] + ": " + count[i];
    // give the number of data points
    info[0] += " from " + numUsed + "/" + peaks.size() + " peaks";

    return info;
  }

  /**
   * Gives the labels that the scoring function can assign. Only one
   * label will be assigned per lane, but this method will return all of
   * the possiblities.
   */
  public String[] getChoices(int numLevels)
  {
    return labels;
  }

  /**
   * Gives the options for this scoring. The first option determines
   * the number of levels the function should handle, and the other options
   * will contain the string for the label of each section. Note that
   * if the nubmer of levels is increased, it will be difficult to 
   * add more fields without setting the options and then getting them
   * again. The value that the first option should actually contain is
   * the number of labels, which is simply 1 plus the number of levels.
   *
   * @return a list of options.
   */
  public Option[] getOptions()
  {
    options = new Option[numLevels + 2];
    options[0] = new Option("Number of Fields", Option.NUMBER, false, 0);

    for(int i = 1; i <= (numLevels + 1); i++)
      {
	options[i] = new Option("Label " + i, Option.STRING, false, "");
      }
    
    return options;
  }

  /**
   * Sets the option to the specified values. The first option should be
   * the number of labels, which is simply one more than the number of
   * levels. The following options should contain the label for each level.
   * If a level is not set, it will have a label of "_" or "?". "_" occurs
   * if an option is provided but not set. "?" is the result of more labels
   * then options specified.
   *
   * @param opts   the options as described above.
   *
   * @exception IllegalArgumentException  if the number of options does not
   *    match 1 plus the number of labels. This means that changing the
   *    number of levels and the labels will require several calls to this
   *    method.
   */
  public void setOptions(Option[] opts)
  {
    if(opts.length != (numLevels + 2))
      throw new IllegalArgumentException("Invalid options for Segregating " +
					 "Score method. " + (numLevels + 2)
					 + " options expected, but " +
					 opts.length + " were provieded.");

    // get the number of levels
    int temp = (int) opts[0].getNumValue();
    if(temp != -1)
      numLevels = temp - 1;

    labels = new String[numLevels + 1];

    for(int i=0; i <= numLevels; i++)
      {
	if((i+1) < opts.length)
	  {
	    labels[i] = opts[i+1].getStringValue();
	    if(labels[i] == null)
	      labels[i] = "_";
	  }
	else
	  labels[i] = "?";
      }
  }

  /**
   * This switches the labels that will be used to there opposite. If
   * a peak above the cutoff would have been labeled 'A' and one below 'B',
   * it will now be labeled as a 'B.' If there is a third label, so A, H, B,
   * invert will switch it to B, H, A.
   */
  public void invert()
  {
    int length = labels.length;
    String temp[] = new String[length];

    for(int i=0; i < length; i++)
      temp[i] = labels[length - 1 - i];

    labels = temp;
  }

  /**
   * Gives the name of this segregating score function.
   *
   * @return the name, which is "Segregating".
   */
  public String getName()
  {
    return name;
  }

  /**
   * Gives a one sentence description of this score function.
   *
   * @return the description
   */
  public String getDescription()
  {
    return descript;
  }

  /**
   * Gives a file that is the help file for this scoring function
   *
   * @return a plaintext of html file containing help information
   */
  public String getHelpFile()
  {
    return helpFile;
  }

  /**
   * Writes all of the information this class needs to store in order
   * to be recreated. This will be used for things like storing the
   * class in a file. Therefore, the write should output enough information
   * so that <code>read</code> can recreate the essential properties of this
   * class.
   *
   * @param out  the destination to write the data to.
   *
   * @exception IOException  occurs when a problem is encountered when
   *     writing to the stream.
   */
  public void write(DataOutputStream out) throws IOException
  {
    out.writeInt(numLevels);
    out.writeInt(labels.length);

    for(int i=0; i < labels.length; i++)
      out.writeUTF(labels[i]);
  }

  /**
   * Reads in the properties of this class from the specified input stream.
   * The stream data should have been created by <code>write</code>. This
   * will retrieve this classes state from the input stream. All of the
   * current data in this class will be replaced by the data from the 
   * stream.
   *
   * @param in  the input stream with the data for the class.
   *
   * @exception IOException  occurs when a problem is encountered when
   *     writing to the stream.
   */
  public void read(DataInputStream in) throws IOException
  {
    numLevels = in.readInt();
    int length = in.readInt();

    labels = new String[length];
    for(int i=0; i < length; i++)
      labels[i] = in.readUTF();    
  }
}
