//=====================================================================
// File:    Bin.java
// Class:   Bin
// 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;
import java.util.Hashtable;
import java.util.Enumeration;

/**
 * This class represents a bin, which is simply a region defined in a gel.
 * It also contains a <code>ScoreFunction</code>. This is responsible for
 * interpreting the data inside of the bin. The bin also contains the 
 * label assigned to each lane by the score function.
 *
 * @see ScoreFunction
 *
 * @author James J. Benham
 * @version 1.0.0
 * @date August 10, 1998
 */

public class Bin extends SortableData
{
  private static double DEFAULT_RANGE = 2;

  private double location;
  private double range;
  private String name;

  private String scoreInfo[];

  private Hashtable scoreLookup;
  
  private ScoreFunction scoreMethod;

  private boolean scored;

  /**
   * Creates a new bin with the specified parameters.
   *
   * @param loc    the location, in bp, of the bin on the gel.
   */
  public Bin(double loc)
  {
    this(loc, DEFAULT_RANGE);
  }

  /**
   * Creates a new bin with the specified parameters.
   *
   * @param loc    the location, in bp, of the bin on the gel.
   * @param range  the range of the bin, agan in bp.
   */
  public Bin(double loc, double range)
  {
    this(loc, range, new SegregatingScore() );
  }

  /**
   * Creates a new bin with the specified parameters.
   *
   * @param loc    the location, in bp, of the bin on the gel.
   * @param range  the range of the bin, agan in bp.
   * @param score  the method used to score the bin.
   */
  public Bin(double loc, double range, ScoreFunction score)
  {
    location = loc;
    this.range = range;
    scoreMethod = score;
    name = "";
    scoreLookup = new Hashtable(30);
    scored = false;
  }

  /**
   * Gives the value used to search/sort bins.
   *
   * @return the bins location
   */
  public final double getSearchKey()
  {
    return location;
  }

  /**
   * Gives the method used to score a bin. That is to say, how the bin
   * interprets the presense/absence/other of peaks in a bin. For example,
   * one could call all those with a peak 'A' and those without 'B'. The
   * score method could count and return the number of 'A's and the number
   * of 'B's.
   *
   * @return The method used to score the bin.
   *
   * @see Bin#setScoreMethod
   */
  public ScoreFunction getScoreMethod()
  {
    return scoreMethod;
  }

  /**
   * Sets the scoring method to the one specified.
   *
   * @param scorer  The method to use on this bin.
   *
   * @see Bin#getScoreMethod
   */
  public void setScoreMethod(ScoreFunction scorer)
  {
    scoreMethod = scorer;
    scored = false;
  }

  /**
   * Scores this bin using the scoring method set and the lanes specified.
   *
   * @param lanes the lanes to include in the bin.
   */
  public void score(DataList lanes)
  {
    if(scoreMethod == null)
      throw new MissingParameterError("No score method set!");

    double min = location - range;
    double max = location + range;

    // determine if we should call the peaks again.
    // should also determine if the peaks should be cleared first, to
    // remove old peaks. Removing can also be handled by the peak
    // locating algorithm.
    boolean reLabel = true;
    if(reLabel)
      {
     // Retrieve the default method.
     PeakLocate peakFind = ((PeakLocate) 
                      FeatureList.getPeakMgr().getDefault());
     peakFind.findPeak(lanes, min, max);
      }

    // ===============Now score each one=================
    Lane ln;
    String label;
    for(int i=0; i < lanes.size(); i++)
      {
     ln = (Lane) lanes.dataAt(i);
     label = scoreMethod.score(ln.cutoffUnder(max),
                      ln.getPeaksInRange(min, max));
     scoreLookup.put(ln, label);
      }

    scoreOverall(lanes);

    scored = true;
  }

  /**
   * Sets the overall scoring information for the bin. The manner in
   * which each lane was scored is given to the score method currently
   * set. It also gives a list of all of the peaks of interest to
   * the scoring method. "Of interest" is defined by the score function
   * by marking certain peaks. For example, the score method could count
   * the number of A's and B's as well as provide a mean for the peak
   * location. 
   *
   * @param lanes   the lanes to include in the overall scoring of this bin
   *
   * @see Bin#getScoreInfo
   * @see ScoreFunction
   * @see Peak#isMarked
   */
  public void scoreOverall(DataList lanes)
  {
    double min = location - range;
    double max = location + range;

    //===========Make a list of all the interesting peaks==========
    DataList markedPeaks = new DataList();
    DataList peaks;
    Peak pk;
    for(int i=0; i < lanes.size(); i++)
      {
     peaks = ((Lane) lanes.dataAt(i)).getPeaksInRange(min, max);
     for(int j=0; j < peaks.size(); j++)
       {
         pk = (Peak) peaks.dataAt(j);
         if(pk.isMarked())
           markedPeaks.addData(pk);
       }
      }

    //============= Change all of the scores into an array =========
    String labels[];                   // the names in an array

    // Get the names from the Hashtable
    Enumeration namesEnum = scoreLookup.elements();

    // Create an array of the approriate size
    labels = new String[scoreLookup.size()];

    // Copy the names from the Enumeration into the array
    for(int i=0; i < scoreLookup.size(); i++)
      {
     labels[i] = (String) namesEnum.nextElement();
      }

    scoreInfo = scoreMethod.getInfo(labels, markedPeaks);    
  }

  /**
   * Gives the string representing how the given lane was scored.
   *
   * @param ln  the lane to retrieve the score for
   *
   * @return the score found by the scoring method
   *
   * @see Bin#getScoreMethod
   * @see Bin#setScoreMethod
   */
  public String getScore(Lane ln)
  {
    // do some error checking here?
    return (String) scoreLookup.get(ln);
  }

  /**
   * Changes the scoring for the specified lane. Only the value in the
   * table is changed by this method, so in future, it could be 
   * replaced if the table is changed and it will not affect any of the
   * peaks. Appropriate values can be retrieved from the
   * <code>ScoreFuction</code> set for this bin.
   * 
   * @param ln     the lane to change
   * @param value  the new score for the lane, which should be one of the
   *               choices offered by the current score method.
   *
   * @see Bin#getScoreMethod
   * @see ScoreFunction#getChoices
   */
  public void setScore(Lane ln, String value)
  {
    scoreLookup.put(ln, value);
  }

  /**
   * Gives the overall score info for the bin. The information is
   * determined from from the peaks used to score the bin and the label
   * assigned to each lane in the bin. Each entry in the array should
   * be a separate line. The informatin could be as simple as the
   * number of lanes, or it could be something like the mean location
   * of all the peaks in the bin.
   *
   * @return an array containing information on the bin scoring.
   *
   * @see ScoreFunction
   * @see Bin#scoreOverall
   */
  public String[] getScoreInfo()
  {
    return scoreInfo;
  }

  /**
   * Used to tell whether or not the <code>score</code> method has been
   * called on this bins current configuration. For example, if the bin
   * is moved, it needs to be rescored.
   *
   * @return <code>true</code> if the bin still has a valid scoring.
   */
  public boolean isScored()
  {
    return scored;
  }

  /**
   * Sets the bin so that the current scoring is either accepted as valid
   * or invalidated.
   *
   * @param scored <code>true</code> if the current scoring is to be valid.
   */
  public void setScore(boolean scored)
  {
    this.scored = scored;
  }

  /**
   * Gives the position of this bin in the gel.
   *
   * @return the location specified in bp.
   */
  public double getLocation()
  {
    return location;
  }

  /**
   * Sets the position/location of this bin to the one specified.
   *
   * @param location  the new position for the bin
   */
  public void setLocation(double location)
  {
    this.location = location;
    scored = false;  // invalidate scoring
  }

  /**
   * Moves the bin to a new location based on it's present location.
   *
   * @param adjustment  the distance to move the bin, which may be either
   *    positive or negative.
   */
  public void adjustLocation(double adjustment)
  {
    location = location + adjustment;
    scored = false;  // invalidate scoring
  }

  /**
   * Gives the range of the bin. The range of the bin is the distance from
   * the location to the edge of the bin. Therefore, the bin is 2*range
   * bp wide.
   *
   * @return  the range as described above
   */
  public double getRange()
  {
    return range;
  }

  /**
   * Sets the range of the bin to the specified value. The range of the bin 
   * is the distance from the location to the edge of the bin. Therefore, the 
   * bin is 2*range bp wide.
   *
   * @param range  the new value for the range as described above
   */
  public void setRange(double range)
  {
    this.range = range;
    scored = false;  // invalidate scoring
  }

  /**
   * Changes the range relative to the current value of the range. The range 
   * of the bin is the distance from the location to the edge of the bin. 
   * Therefore, the bin is 2*range bp wide.
   *
   * @param adjustment  the value to increment the range by. This value
   *    can be either positive or negative.
   */
  public void adjustRange(double adjustment)
  {
    range = range + adjustment;
    scored = false;  // invalidate scoring
  }

  /**
   * Sets the name of the bin to the specified value. By default, a bin
   * has a name of "", the empty String.
   *
   * @param name the new name for the bin.
   */
  public void setName(String name)
  {
    this.name = name;
  }

  /**
   * Gives the name of this bin. By default, a bin has a name of "", the
   * empty String. In this case, the location of the bin is a much better
   * identifier.
   *
   * @return the name of the bin.
   */
  public String getName()
  {
    return name;
  }

  /**
   * Produces a new bin identical to this one, except it has not been
   * scored.
   *
   * @return a new bin with the same properties as this one.
   */
  public Object clone()
  {
    Bin temp = new Bin(location, range, scoreMethod);
    temp.setName(name);
    return temp;
  }

  /**
   * 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.writeDouble(location);
    out.writeDouble(range);
    out.writeUTF(name);

    out.writeBoolean(scored);
    out.writeUTF(scoreMethod.getName());
    scoreMethod.write(out);

    // Only write the scoring info out when the bin has been scored, 
    // otherwise we're writing worthless info.
    if(scored)
      {
     // write out the score info
     if(scoreInfo == null)
       out.writeInt(0);
     else
       {
         out.writeInt(scoreInfo.length);
         for(int i=0; i < scoreInfo.length; i++)
           out.writeUTF(scoreInfo[i]);
       }
     
     //get keys from the hashtable by retrieving the values and then
     // maing pairs.
     out.writeInt(scoreLookup.size());
     java.util.Enumeration keys = scoreLookup.keys();
     Object key;
     for(int i=0; i < scoreLookup.size(); i++)
       {
         key = keys.nextElement();
         // writes out the lane index
         out.writeInt(key.hashCode());
         // Now write out the value for that lane
         out.writeUTF( (String) scoreLookup.get(key));
       }
      }
  }

  /**
   * 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.
   *
   * <p><b>Warning:</b> this method cannot read in the scoring values
   * because of the way that they are stored, specifically by lane.
   * However, the lanes are not available to this method, so the information
   * cannot be stored. A class calling this method should make provisions
   * to take care of the rest of the data stream. The method
   * <code>setScore</code> can be used to set the value, it takes a
   * lane value pair and records it. The stream will be left at the
   * start of the values. The first thing will be an integer giving the
   * number of entries stored in the stream. It will be followed by
   * pairs of laneIndexies and values. The laneIndexies are integers and
   * the values are UTF-encoded Strings.
   *
   * @param in  the input stream with the data for the class.
   *
   * @exception IOException  occurs when a problem is encountered when
   *     writing to the stream.
   *
   * @see Bin#setScore
   */
  public void read(DataInputStream in) throws IOException
  {
    location = in.readDouble();
    range = in.readDouble();
    name = in.readUTF();

    scored = in.readBoolean();

    String scoreName = in.readUTF();
    try {
      scoreMethod = (ScoreFunction) FeatureList.getScoreMgr().get(scoreName);
      scoreMethod = (ScoreFunction) scoreMethod.clone();
      scoreMethod.read(in);
    }catch(java.util.NoSuchElementException e) {
      throw new IOException("The scoring method " + scoreName + " could not" +
                   " be found.");
    }

    // if the bin isn't scored, then this stuff wasn't written because it
    // would have been worthless, so obviously we don't need to read it in.
    if(scored)
      {
     int length = in.readInt();
     if(length == 0)
       scoreInfo = null;
     else
       {
         scoreInfo = new String[length];
         for(int i=0; i < length; i++)
           scoreInfo[i] = in.readUTF();
       }
     
     // read in the hashtable pairs.
     // This can't be done here because we need the lane object to act
     // as a key. We don't have the lanes. Just rely on the calling
     // class to take care of it. Yuck! 
     
     // We can make a new hashtable though. We wouldn't want to mix
     // old stuff with new
     scoreLookup = new Hashtable(30);
      }
    else
      {
     // if it's not scored, set stuff
     scoreInfo = null;
     scoreLookup = new Hashtable(30);
      }
  }
}
