//=====================================================================
// File:    GraphView.java
// Class:   GraphView
// Package: AFLPgui
//
// Author:  James J. Benham
// Date:    January 6, 1999
// 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.Choice;
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 java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import AFLPcore.Bin;
import AFLPcore.Cutoff;
import AFLPcore.CutoffFunction;
import AFLPcore.DataList;
import AFLPcore.FeatureList;
import AFLPcore.Lane;
import AFLPcore.Option;
import AFLPcore.ProgOptions;
import AFLPcore.ScoringFailure;
import AFLPcore.ScoreFunction;

/**
 * Displays a graph of a bin. This class manages the size of the graph
 * and draws the axis and axis labels. It also provides a slider
 * to manipulate the cutoffs in the bin. The actually drawing of the
 * graph is done by objects of the type <code>Graph</code>. Normally,
 * the display is double-buffered, but this is disabled if the class is
 * drawing to a printer page. When it first displays, it will adjust the
 * cutoffs so that only one will appear in the bin, (the cutoff with
 * the largest starting position). The size of the display and the 
 * graph can be manipulated by external classes.
 *
 * @see Graph
 * @see CutoffSlider
 *
 * @author James J. Benham
 * @version 1.1.0
 * @date January, 1999
 */

public class GraphView extends Panel
       implements ActionListener, 
		  ItemListener, 
		  MouseListener, 
		  MouseMotionListener
{
  // 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;

  // Graph layout stuff
  private static int SLIDER_WIDTH = 22;
  private static int H_SPACE = 50;
  private static int TICK_L_WIDTH = 30;
  private static int TICK_WIDTH   =  3;
  private static int BOTTOM_BORDER = 15;

  // Button bar constants
  private static int HORZ_SPACE = 5;
  private static int GRAPH_CHOICE_WIDTH = 70;

  // Files for images
  private static String REL_PATH      = "";
  private static String PREVIOUS_FILE = "l_arrow.gif";
  private static String NEXT_FILE = "r_arrow.gif";
  private static String ADDCT_FILE = "plus.gif";
  private static String CTDIALOG_FILE = "cutoff.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
  Choice graphChoice;
  ImgButton previousButton;
  ImgButton nextButton;
  ImgButton addCutoffButton;
  ImgButton cutoffDialogButton;

  CutoffDialog ctDialog;

  private CutoffSlider slider;
  private Bin currentBin;

  private int height;
  private int width;
  private int h_inset;
  private int v_inset;

  private int graph_width;

  private double maxSize;
  private double minSize;
  private double scale;
  private double tickInc;
  private double maxIntensity;

  private DataList lanes;
  private DataList bins;
  private Graph graph[];
  private int graphIndex;

  private Frame topWindow;

  /**
   * Create a new GraphView with the specified parameters.
   *
   * @param lanes     the lanes to include in the graph.
   * @param bins      a list of bins in the gel.
   * @param parentWindow  an owner for dialog boxes.
   */
  public GraphView(DataList lanes, DataList bins, Frame parentWindow)
  {
    this.lanes = lanes;
    this.bins = bins;
    topWindow = parentWindow;

    height = 320;
    width = 500;
    h_inset = 10;
    v_inset = 10;

    graph_width = 400;

    currentBin = null;

    graph = new Graph[2];
    graph[0] = new BarGraph();
    graph[1] = new ScatterGraph();
    graphIndex = 0;

    graphChoice = new Choice();
    graphChoice.add("Bar");
    graphChoice.add("Scatter");
    graphChoice.select(graphIndex);

    // add the slider control
    slider = new CutoffSlider();
    slider.addMouseListener(this);
    slider.addMouseMotionListener(this);
    add(slider);
    slider.setBounds(0, v_inset, SLIDER_WIDTH, 
		     height - v_inset - BOTTOM_BORDER);

    setLayout(null);
    setBounds(0, 0, width, height);

    ctDialog = new CutoffDialog(topWindow, "Cutoffs...", true);

    createButtonBar();
    createInfoBar();
  }

  /**
   * Initialize the graph view. This will do things like check to see
   * that only a single cutoff applies to the specified bin in the
   * lanes. It will also set the scale for the graph and score any bins
   * that need scoring.
   *
   * @param bin   the bin to show a graph for
   * @param lanes the lanes to include in the graph
   * @param bins  a list of bins in the gel, used so the graph could switch
   *              to another bin.
   */
  public void init(Bin bin, DataList lanes, DataList bins)
  {
    // make sure the size is still set.
    setBounds(0, 0, width, height);

    // update the lists since the gel may have changed
    this.lanes = lanes;
    this.bins = bins;
    currentBin = bin;

    if(bin != null)
      {
	maxSize = bin.getLocation() + bin.getRange();
	minSize = bin.getLocation() - bin.getRange();
      }
    else
      {
	maxSize = -1;
	minSize = 0;
      }

    //=================== find the max intensity=========
    maxIntensity = 0;
    double tempIntensity;
    for(int i=0; i < lanes.size(); i++)
      {
	tempIntensity = ((Lane) lanes.dataAt(i)).getMaxHeight(minSize, 
							      maxSize);
	if(tempIntensity > maxIntensity)
	  maxIntensity = tempIntensity;
      }

    //=================== find the scale================
    scale = (height - v_inset - BOTTOM_BORDER - 1)/maxIntensity;

    //=================== set the tick increment========
    if(maxIntensity >= 2000)
      tickInc = 1000;
    else if(maxIntensity >= 1000)
      tickInc = 500;
    else
      tickInc = 100;

    slider.setScale(scale);
    slider.setSize(minSize);

    slider.refresh();

    // fix anything that has multiple cutoffs in the bin by taking the
    // largest one and applying it to the bin.
    Cutoff ct;
    Lane ln;
    for(int i=0; i < lanes.size(); i++)
      {
	ln = (Lane) lanes.dataAt(i);
	ct = ln.cutoffUnder(maxSize);

	// Look out for errors, like MaxSize = -1, if no bin, so
	// ct will be null. In that case, don't worryabout this stuff
	if( (ct != null) && (ct.getStartPos() > minSize))
	  {
	    // make this cutoff cover the whole bin.
	    // first delete anything between it and the edge of the bin
	    double pos;
	    Cutoff temp = ln.cutoffUnder(ct.getStartPos() - 0.00000001);
	    if(temp != null)
	      pos = temp.getStartPos();
	    else
	      pos = -1;

	    while(pos > minSize)
	      {
		ln.getCutoffs().removeElement(temp);
		// find the next one, subtract a little number to avoid
		// equailty
		temp = ln.cutoffUnder(pos - 0.00000001);
		if(temp != null)
		  pos = temp.getStartPos();
		else
		  pos = -1;
	      }

	    //Now adjust the cutoff.
	    ct.setStartPos(minSize);
	  }
      }

    // set the slider stuff
    // if we don't have any lanes, then we won't be displaying anything
    // anyway, so don't worry about that case.
    if(lanes.size() > 0)
      {
	slider.setCutoff( ((Lane)lanes.dataAt(0)).cutoffUnder(maxSize));
	slider.setLanes(lanes);
      }

    //=====score the bin initially================
    // see if it is neccessary first, but check to see that it exists first
    // It is possible that no bin is selected,
    if(currentBin != null)
      {
	if(!currentBin.isScored())
	  try{
	    currentBin.score(lanes);
	  } catch(ScoringFailure error) {
	    handleScoreError(error);
	  }
	
	// set the info label
	infoLabel.setText(currentBin.getScoreInfo()[0]);
      }
  }

  /**
   * Gives the height of the graph display area, including borders.
   *
   * @return the height
   */
  public int getHeight()
  {
    return height;
  }

  /**
   * Sets the height of the graph display area, including borders, to
   * the specified value.
   *
   * @param height  the new height for the view
   */
  public void setHeight(int height)
  {
    this.height = height;
    setBounds(0, 0, width, height);
  }

  /**
   * Gives the width of the graph display area, including borders.
   *
   * @return the width
   */
  public int getWidth()
  {
    return width;
  }

  /**
   * Sets the width of the graph display area, including borders, to
   * the specified value.
   *
   * @param width the new width
   */
  public void setWidth(int width)
  {
    this.width = width;
    setBounds(0, 0, width, height);
  }

  /**
   * Gets the width of the graph, which does not include borders. This
   * is the value that will be passed to the graph drawing object.
   *
   * @return the width
   *
   * @see Graph
   */
  public int getGraphWidth()
  {
    return graph_width;
  }

  /**
   * Sets the width of the actual graph, which does not include the border.
   * This is the value that will be passed to the graph drawing object.
   *
   * @param width   the new width for the graph
   *
   * @see Graph
   */
  public void setGraphWidth(int width)
  {
    graph_width = width;
  }

  /**
   * Gives the bin that was used to produce the graph.
   *
   * @return  the bin currently displayed as a graph
   */
  public Bin getBin()
  {
    return currentBin;
  }

  /**
   * Draws the graph. It will actually draw the axis and the tick
   * marks, as well as the cutoffs, but the actual graph is
   * drawn by the <code>Graph</code> object selected. The display is
   * normally double buffered, but double-buffering is disabled if
   * the method is drawing to a printed page.
   */
  public void paint(Graphics g)
  {
    //=======================Determine the size===============
    int temp = graph[graphIndex].getPreferredWidth();
    int fullWidth = width - h_inset - H_SPACE;
    if(temp == -1)
      graph_width = fullWidth;
    else if(temp <= fullWidth)
      graph_width = temp;
    else
      {
	// decide if we should resize or not? 
	graph_width = fullWidth;
      }

    if( !(g instanceof PrintGraphics))
      {
	//=======================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();
	  }
      }
    else
      {
	// don't need to double buffer the printer
	offGraphics = g;
      }

    //=================== find the scale================
    // can change when we go to the printer of come back from it
    scale = (height - v_inset - BOTTOM_BORDER - 1)/maxIntensity;

    // make sure we have a bin. If not, we don't have anything to display.
    // Draw to the graphics screen and then bail. don't use the double
    // buffer.
    if(currentBin == null)
      {
	slider.setVisible(false);
	g.clearRect(0, 0, width, height);
	g.drawString("No Bin Selected. Graph unavailable.",
		     LABEL_HORZ, LABEL_VERT);
	return; // bail!
      }
    
    // ============== Normal drawing ===================
    
    // clear the screen
    offGraphics.clearRect(0, 0, width, height);

    slider.setVisible(true);

    // set some of the stuff
    int x = h_inset + H_SPACE;
    int h = height - BOTTOM_BORDER;

    // Draw the axis
    offGraphics.setColor(Color.black);
    offGraphics.drawLine(x, v_inset, x, h);
    offGraphics.drawLine(x, h, x + graph_width, h);

    //================== Draw Tick marks =====================
    int y;
    int tickLoc = (int) tickInc;
    double maxHeight = (h-v_inset - 1)/scale;
    while(tickLoc < maxHeight)
      {
	// Find the location for the tick
	y = h - (int) (scale*tickLoc);
	offGraphics.drawLine(x - TICK_WIDTH, y, x, y);
	offGraphics.drawString("" + tickLoc, x - TICK_L_WIDTH, y + 5);
	tickLoc += (int) tickInc;
      }

    // ==================Draw the graph=========================
    //  Call whichever graphing method is selected.
    graph[graphIndex].drawGraph(offGraphics, x, v_inset, graph_width,
				h - v_inset, BOTTOM_BORDER, scale, minSize,
				maxSize, lanes);

    // ================= Draw the Cutoffs =======================
    // use the first lane for the whole graph, and extend it
    // straight across the graph.
    offGraphics.setColor(Color.lightGray);

    Cutoff ct = ((Lane)lanes.dataAt(0)).cutoffUnder(maxSize);
    // see if we only have one cutoff.
    if(ct.getStartPos() > minSize)
      System.err.println("Cutoff does not span bin!");

    int numLevels = ct.getNumLevels();
    int intensity;
    for(int i=0; i < numLevels; i++)
      {
	// Just draw lines, since the graph doesn't really have a 
	// horizontal scale for the different places in the bin.
	intensity =  h - 1 - (int) (scale * ct.getCutoff(minSize, i));
	offGraphics.drawLine(x + 1, intensity, x + graph_width, intensity);
      }

    if(!(g instanceof PrintGraphics))
      g.drawImage(offImage, 0, 0, this);
  }

  /**
   * Changes the graph to the one selected in the choice box.
   */
  public void itemStateChanged(ItemEvent e)
  {
    if(e.getSource() == graphChoice)
      {
	graphIndex = graphChoice.getSelectedIndex();
	refresh();
      }
  }

  /**
   * Handles the buttons in the button bar.
   */
  public void actionPerformed(ActionEvent e)
  {
    if(e.getSource() == previousButton)
      {
	// find the location of this bin in the list
	int location = bins.find(currentBin.getSearchKey()).location;

	// now move to the previous one in the list
	if(location <= 0)
	  infoLabel.setText("Already on first bin.");
	else
	  {
	    currentBin = (Bin) bins.dataAt(location - 1);
	    init(currentBin, lanes, bins);
	    infoLabel.setText("Bin number " + (location));
	    refresh();
	  }
      }
    else if(e.getSource() == nextButton)
      {
	// find the location of this bin in the list
	int location = bins.find(currentBin.getSearchKey()).location;

	// now move to the previous one in the list
	if(location == (bins.size() - 1))
	  infoLabel.setText("Already on last bin.");
	else
	  {
	    currentBin = (Bin) bins.dataAt(location + 1);
	    init(currentBin, lanes, bins);
	    infoLabel.setText("Bin number " + (location + 2));
	    refresh();
	  }
      }
    else if(e.getSource() == addCutoffButton)
      {
	// Make a new function, set at half height
	CutoffFunction newFunction;
	newFunction = (CutoffFunction) FeatureList.getCutoffMgr().getDefault();
	newFunction = (CutoffFunction) newFunction.clone();
	Option opts[] = newFunction.getOptions();
	opts[0].setValue(maxIntensity/2);
	newFunction.setOptions(opts);

	slider.addCutoffFunction(newFunction);

	// rescore the bin
	try{
	  currentBin.score(lanes);
	} catch(ScoringFailure error) {
	  handleScoreError(error);
	}

	refresh();
      }
    else if(e.getSource() == cutoffDialogButton)
      {
	ctDialog.init(((Lane) lanes.dataAt(0)).cutoffUnder(maxSize),
		      lanes, minSize);
	ctDialog.setVisible(true);
	slider.setCutoff(((Lane) lanes.dataAt(0)).cutoffUnder(maxSize));
	slider.refresh();
	refresh();
      }
  } 

  /**
   * Called when the mouse is released. It will rescore a bin if the
   * mouse is released on the slider.
   */
  public void mouseReleased(MouseEvent e)
  {
    if(e.getSource() == slider)
      {
	try{
	  currentBin.score(lanes);
	} catch(ScoringFailure error) {
	  handleScoreError(error);
	}

	refresh();
	infoLabel.setText(currentBin.getScoreInfo()[0]);
      }
  }

  /**
   * Called when the mouse is dragged. It will refresh the display if
   * the event originated with the cutoff slider.
   */
  public void mouseDragged(MouseEvent e)
  {
    if(e.getSource() == slider)
      refresh();
  }

  /**
   * Called to update the screen by Java. It simply calls paint.
   */
  public void update(Graphics g)
  {
    paint(g);
  }

  /**
   * Gives the ButtonBar associated with the graph.
   *
   * @return the button bar.
   */
  public ButtonBar getButtonBar()
  {
    return buttonBar;
  }

  /**
   * Shows an options dialog if an error occurs when scoring a bin.
   * The most likely reason is that the number of cutoffs do not match
   * the number expected by the scoring function. The options are used
   * to adjust the scoring function.
   *
   * @param error   the orignal failure
   */
  protected void handleScoreError(ScoringFailure error)
    {
      ErrorDialog errorDialog = new ErrorDialog(topWindow);
      errorDialog.showError(error);

      // try setting the number of levels for the scoring method
      ScoreFunction scoreFn = currentBin.getScoreMethod();
      Option opts[] = scoreFn.getOptions();

      // find out the number of levels we have, try using the first lane.
      Cutoff ct = new Cutoff(minSize, 2);
      if(!lanes.isEmpty())
	ct = ((Lane) lanes.dataAt(0)).cutoffUnder(maxSize);
      
      // see if the first option is a number
      if(opts[0].getType() != Option.NUMBER)
	throw new IllegalArgumentException("Error in scoring method " +
					   scoreFn.getName() + ". First " +
					   "option should be number of levels"
					   + " expected.");

      opts[0].setValue((double) ct.getNumLevels() + 1);
      
      scoreFn.setOptions(opts);
      
      // show the options again.
      OptionDialog optD = new OptionDialog(scoreFn.getOptions(),
					   topWindow, 
					   scoreFn.getName() + " Parameters");
      optD.setVisible(true);
      if(!optD.isCanceled())
	{
	  opts = optD.getOptions();
	  scoreFn.setOptions(opts);
	}
    }

  /**
   * 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 graph choice=================
    // already created in constructor, initiallized with graph types
    buttonBar.add(graphChoice);
    graphChoice.addItemListener(this);
    int startX = buttonBar.getFreeHorzPos();
    graphChoice.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET,
			  GRAPH_CHOICE_WIDTH, ButtonBar.BUTTON_HEIGHT);
    startX += GRAPH_CHOICE_WIDTH + HORZ_SPACE;

    //===========Create the previous button================
    previousButton = new ImgButton(ButtonBar.retrieveImage(ProgOptions.homePath
                                                           + REL_PATH + 
							   PREVIOUS_FILE));
    buttonBar.add(previousButton);
    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);

    //===========Create the addCutoff button====================
    String imgFileLoc = ProgOptions.homePath + REL_PATH + ADDCT_FILE;
    addCutoffButton = new ImgButton(ButtonBar.retrieveImage(imgFileLoc));
    buttonBar.add(addCutoffButton);
    addCutoffButton.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET,
			      ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT);
    startX += ButtonBar.BUTTON_WIDTH + HORZ_SPACE;
    addCutoffButton.addActionListener(this);

    //===========Create the cutoff dialog button===============
    cutoffDialogButton = 
      new ImgButton(ButtonBar.retrieveImage(ProgOptions.homePath +
					    REL_PATH + CTDIALOG_FILE));
    buttonBar.add(cutoffDialogButton);
    cutoffDialogButton.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET,
				 ButtonBar.BUTTON_WIDTH, 
				 ButtonBar.BUTTON_HEIGHT);
    startX += ButtonBar.BUTTON_WIDTH + HORZ_SPACE;
    cutoffDialogButton.addActionListener(this);
  }

  /**
   * Returns a bar that displays information about the graph. 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);
  }

  /**
   * Updates the display so that it matches the data.
   */
  public void refresh()
  {
    repaint();
  }
  
  //=================Unused methods to satisfy intefaces=================
  /**Unused*/ public void mouseClicked(MouseEvent e) {}
  /**Unused*/ public void mousePressed(MouseEvent e) {}
  /**Unused*/ public void mouseMoved(MouseEvent e) {}
  /**Unused*/ public void mouseEntered(MouseEvent e){}
  /**Unused*/ public void mouseExited(MouseEvent e){}
}
