//=====================================================================
// File:    TraceView.java
// Class:   TraceView
// Package: AFLPgui
//
// Author:  James J. Benham
// Date:    August 12, 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 AFLPgui;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Label;
import java.awt.Panel;
import java.awt.PrintGraphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import AFLPcore.Cutoff;
import AFLPcore.CutoffFunction;
import AFLPcore.DataList;
import AFLPcore.Lane;
import AFLPcore.LinearCutoff;
import AFLPcore.ProgOptions;

/**
 * Displays the trace of a lane. The lane can be set using the 
 * <code>init</code> method. The trace's size can be adjusted and
 * the name of the lane will be displayed on the trace. The 
 * <code>paint</code> method is capable of drawing to a page for the
 * printer, but this class should be sized appropriately first. When
 * drawing to the screen, the trace display is double-buffered.
 *
 * @author James J. Benham
 * @version 1.0.0
 * @date August 12, 1998
 */

public class TraceView extends Panel implements ActionListener
{
  // Info Bar component parameters
  private static int LABEL_H_INSET = 5;
  private static int LABEL_V_INSET = 3;
  private static int LABEL_WIDTH   = 600;
  private static int LABEL_HEIGHT  = 18;

  // Error message location
  private static int LABEL_HORZ = 20;
  private static int LABEL_VERT = 20;

  // Button bar constants
  private static int HORZ_SPACE = 5;

  // Files for images
  private static String REL_PATH      = "";
  private static String INVERT_FILE   = "invert.gif";
  private static String PREVIOUS_FILE = "l_arrow.gif";
  private static String NEXT_FILE = "r_arrow.gif";

  // Double buffering stuff
  private Dimension offDimension;
  private Image offImage;
  private Graphics offGraphics;

  // Bar stuff
  private ButtonBar buttonBar;
  private Bar infoBar;
  private Label infoLabel;

  // Button Bar components
  private ImgButton previousButton;
  private ImgButton nextButton;

  private Frame topWindow;

  private DataList lanes;
  private Lane lane;

  private int h_inset;
  private int v_inset;
  private int width;
  private int height;

  private double maxIntensity;

  private double hScale;
  private double vScale;

  private double minSize;
  private double maxSize;

  /** 
   * Create a new TraceView with the specified parameters.
   *
   * @param parentWindow  used as an owner for various dialog boxes.
   */
  public TraceView(Frame parentWindow)
  {
    topWindow = parentWindow;

    lane = null;

    h_inset = 10;
    v_inset = 60;
    width = 500;
    height = 240;

    setLayout(null);
    setBounds(0, 0, h_inset + width, v_inset + height);

    createButtonBar();
    createInfoBar();
  }

  /**
   * Sets the trace to the specified lane and sets the scales.
   *
   * @param ln   the lane whose trace should be displayed
   */
  public void init(Lane ln, DataList lanes)
  {
    this.lanes = lanes;
    lane = ln;

    if(lane != null) 
      {
	//==============find the scales===================
	  minSize = ln.getMinSize();
	  maxSize = ln.getMaxSize();
	  
	  maxIntensity = ln.getMaxHeight(minSize, maxSize);
	  
	  hScale = (maxSize - minSize)/(width - 2); // -2 for left border
	  vScale = (height - 1)/maxIntensity;
      }
  }

  /**
   * Gives the width of the trace.
   * 
   * @return the width, excluding borders
   */
  public int getWidth()
  {
    return width;
  }

  /**
   * Sets the width of the trace, including borders, to the specified
   * value.
   *
   * @param width   the new width for this object.
   */
  public void setWidth(int width)
  {
    this.width = width - h_inset;
    setBounds(0, 0, width, v_inset + height);
  }

  /**
   * Gives the height of the trace, excluding borders.
   *
   * @return the height of the trace area.
   */
  public int getHeight()
  {
    return height;
  }

  /**
   * Sets the height of the trace, including borders to the specified
   * value.
   *
   * @param height  the new height for this
   */
  public void setHeight(int height)
  {
    this.height = height - v_inset;
    setBounds(0, 0, h_inset + width, height);
  }

  /**
   * Draws the trace to the specified graphics. If the graphics is
   * a <code>PrintGraphics</code> then specail care is taken, such
   * as disabeling double-buffering.
   */
  public void paint(Graphics g)
  {
    //==============find the scales===================
    // printing could change it.
    hScale = (maxSize - minSize)/(width - 2); // -2 for left border
    vScale = (height - 1)/maxIntensity;

    if(g instanceof PrintGraphics)
      {
     // we don't need to doulbe buffer the printer :)
     offGraphics = g;
      }
    else
      {
     //=======================Off screen buffer setup==========
     // make sure we have the offscreen buffer and that it is the right
     // size.
     Dimension d = getSize();
     if ( (offGraphics == null)
          || (d.width != offDimension.width)
      || (d.height != offDimension.height) )
       {
         offDimension = d;
         offImage = createImage(d.width, d.height);
         offGraphics = offImage.getGraphics();
       }
      }
     
    // make sure we have a lane. If not, we don't have anything to display.
    // Draw to the graphics screen and then bail. don't use the double
    // buffer.
    if(lane == null)
      {
	g.clearRect(0, 0, h_inset + width, v_inset + height);
	g.drawString("No Lanes! Lane trace not available.",
		     LABEL_HORZ, LABEL_VERT);
	return; // bail!
      }

    // ============== Normal drawing ===================

    offGraphics.clearRect(0, 0, h_inset + width, v_inset + height);

    // draw the axis
    int lowY = v_inset + height - 1;
    offGraphics.drawLine(h_inset, v_inset, h_inset, lowY);
    offGraphics.drawLine(h_inset, lowY, h_inset + width, lowY);

    // draw the header
    offGraphics.setColor(Color.black);
    offGraphics.drawString("" + lane.getLaneNumber() + "  " + lane.getName(),
                  h_inset, v_inset - 1);

    //================Draw the trace=====================
    // find the color and set it
    switch(lane.getColor())
      {
      case Lane.RED:
     offGraphics.setColor(Color.red);
     break;
      case Lane.GREEN:
     offGraphics.setColor(Color.green);
     break;
      case Lane.BLUE:
     offGraphics.setColor(Color.blue);
     break;
      case Lane.YELLOW:
     offGraphics.setColor(Color.yellow);
     break;
      }
   
    // draw the actual trace
    double size = minSize;
    int previousIntensity = lowY - (int)(vScale*lane.getHeightAt(minSize));
    int intensity;

    for(int i=1; i < (width - 1); i++)
      {
     // find the height, -1 to place it above the border.
     intensity = lowY - (int) (vScale * lane.getHeightAt(size));
     offGraphics.drawLine(h_inset + i, previousIntensity, 
                    h_inset + i + 1, intensity);

     previousIntensity = intensity;
     size += hScale;
      }

    //================Draw the cutoff functions============
    
    offGraphics.setColor(Color.lightGray);

    DataList cutoffs = lane.getCutoffs();

    if(!cutoffs.isEmpty())
      {
     int startPoint;
     int endPoint;

     Cutoff ct1 = (Cutoff) cutoffs.dataAt(0);
     Cutoff ct2;

     for(int i=1; i < (cutoffs.size() - 1); i++)
       {
         ct2 = (Cutoff) cutoffs.dataAt(i);
         
         endPoint = h_inset + (int)(hScale*ct2.getStartPos()) + 1;

         size = ct1.getStartPos();
         startPoint = h_inset + (int)(hScale*size) + 1;

         drawCutoff(offGraphics, ct1, size, startPoint, endPoint, lowY);

         // set up the next cutoff pair.
         ct1 = ct2;     
       }

     // draw the final one.
     endPoint = h_inset + (int)(hScale*maxSize);
     size = ct1.getStartPos();
     startPoint = h_inset + (int)(hScale*size) + 1;
     
     drawCutoff(offGraphics, ct1, size, startPoint, endPoint, lowY);
      }

    if(!(g instanceof PrintGraphics))
      g.drawImage(offImage, 0, 0, this);
  }

  /**
   * Draws a cutoff using the specified parameters. If one of the levels
   * is a line, a line is simply drawn. Otherwise, the method will
   * play connect the dots at each size to produce the cutoff function.
   *
   * @param g          the graphics object to draw on
   * @param ct         the cutoff to draw
   * @param size       the starting size of the cutoff
   * @param startPoint the first horizontal position, in pixels
   * @param endPoint   the value to stop drawing at, in pixels
   * @param lowY       the y-value of the baseline.
   */
  private void drawCutoff(Graphics g, Cutoff ct, double size,
                 int startPoint, int endPoint, int lowY)
  {
    int intensity;
    int previousIntensity;

    int numLevels = ct.getNumLevels();
    for(int i=0; i < numLevels; i++)
      {
     if(ct.getCutoffFunction(i) instanceof LinearCutoff)
       {
         intensity =  lowY - (int) (vScale * ct.getCutoff(size, i));
         g.drawLine(startPoint, intensity, endPoint, intensity);
       }
     else
       {
         previousIntensity = (lowY - (int)(vScale*ct.getCutoff(size, i)));
         for(int j=startPoint; j < (endPoint); j++)       
           {
          // find the height,
          intensity = lowY - (int) (vScale * ct.getCutoff(size, i));
          g.drawLine(h_inset + j, previousIntensity, 
                  h_inset + j + 1, intensity);
          
          previousIntensity = intensity;
          size += hScale;
           }
       }
      }
  }

  /**
   * Called by JVM to refresh screen, but it simply calls paint.
   */
  public void update(Graphics g)
  {
    paint(g);
  }

  /**
   * Gives the ButtonBar associated with the trace.
   *
   * @return the button bar.
   */
  public ButtonBar getButtonBar()
  {
    return buttonBar;
  }

  /**
   * Lays out the components on the button bar, uses the constants declared
   * in this class to control the layout.
   */
  private void createButtonBar()
  {
    buttonBar = new ButtonBar();
    buttonBar.setBounds(0, 0, 640, 32);

    // Let the parent window handle the action event for the orignal
    // button bar components
    buttonBar.sendActionEventsTo( (ActionListener) topWindow);

    //===========Create the previous button================
    previousButton = new ImgButton(ButtonBar.retrieveImage(ProgOptions.homePath
                                                           + REL_PATH + 
							   PREVIOUS_FILE));
    buttonBar.add(previousButton);
    int startX = buttonBar.getFreeHorzPos();
    previousButton.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET,
			     ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT);
    startX += ButtonBar.BUTTON_WIDTH + HORZ_SPACE;
    previousButton.addActionListener(this);

    //===========Create the next button====================
    nextButton = new ImgButton(ButtonBar.retrieveImage(ProgOptions.homePath +
						       REL_PATH + 
						       NEXT_FILE));
    buttonBar.add(nextButton);
    nextButton.setBounds(startX, ButtonBar.VERT_INSET,
			 ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT);
    startX += ButtonBar.BUTTON_WIDTH;
    nextButton.addActionListener(this);
  }

  /**
   * Returns a bar that displays information about the trace. This object
   * should be displayed somewhere by the container of this object.
   *
   * @return the bar as described above
   */
  public Bar getInfoBar()
  {
    return infoBar;
  }

  /**
   * Lays out the components for the info bar. Layout is controlled with
   * constants in this class.
   */
  private void createInfoBar()
  {
    // create the label
    infoLabel = new Label("Graph info...");
    // create the bar, no bottom border.
    infoBar = new Bar(true, false);
    infoBar.setLayout(null);
    infoBar.add(infoLabel);
    infoLabel.setBounds(LABEL_H_INSET, LABEL_V_INSET,
			LABEL_WIDTH, LABEL_HEIGHT);
    infoBar.setBounds(0, 0, 600, FragmentMap.BAR_HEIGHT);
  }

  /**
   * Update the display so that it matches the data.
   */
  public void refresh()
  {
    repaint();
  }

  /**
   * Handles the buttons in the button bar.
   */
  public void actionPerformed(ActionEvent e)
  {
    if(e.getSource() == previousButton)
      {
	// find the location of the lane in the list
	int location = -1;
	for(int i=0; i < lanes.size(); i++)
	  if(lane == (Lane) lanes.dataAt(i))
	    {
	      location = i;
	      break;
	    }

	// now move to the previous one in the list
	if(location <= 0)
	  infoLabel.setText("Already on the first lane.");
	else
	  {
	    lane = (Lane) lanes.dataAt(location - 1);
	    infoLabel.setText("Showing lane number " + (location));
	    init(lane, lanes);
	    refresh();
	  }
      }
    else if(e.getSource() == nextButton)
      {
	// find the location of the lane in the list
	int location = -1;
	for(int i=0; i < lanes.size(); i++)
	  if(lane == (Lane) lanes.dataAt(i))
	    {
	      location = i;
	      break;
	    }

	// now move to the previous one in the list
	if(location == (lanes.size() - 1))
	  infoLabel.setText("Already on the last lane.");
	else
	  {
	    lane = (Lane) lanes.dataAt(location + 1);
	    infoLabel.setText("Showing lane number " + (location + 2));
	    init(lane, lanes);
	    refresh();
	  }
      }
  }
}
