/*  
 *  src/org/metaqtl/graph/ChromShape.java
 * 
 *  $Author: veyrieras $, $Date: 2006-10-17 15:41:55 $, $Version$
 *
 *  MetaQTL : a Java package for QTL Meta-Analysis
 * 
 *  Copyright (C) 2003-2006  Jean-Baptiste Veyrieras, INRA, France.
 *
 *  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; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  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 * 
 */
package org.metaqtl.graph;

import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;

import org.metaqtl.Chromosome;
import org.metaqtl.Qtl;
import org.metaqtl.bio.util.NumberFormat;


public class ChromShape extends GraphShape {
	public static final int RIGHT_LABEL = 0;
	public static final int LEFT_LABEL  = 1;
	private  boolean	  withMrkLabel;
	private  boolean	  withMrkPos;
	private  boolean 	  withChromName;
	private  boolean 	  withMapName;
	private  double       chrom_height;
	private  double       chrom_width;
	private  double       y_chrom;
	private  double       y_map_label;
	private  double       y_chrom_label;
	private  double 	 max_label_width=0.0;
	private  double 	 max_pos_width=0.0;
	private  double		 tick_width_1=MetaGraphPar.CHROM_TICK_WIDTH_1;
	private  double		 tick_width_2=MetaGraphPar.CHROM_TICK_WIDTH_2;
	private  double		 tick_width_3=MetaGraphPar.CHROM_TICK_WIDTH_3;
	private double[]	 ymin_mqtls;
	private double[]	 ymax_mqtls;
 	private double[]     y_markers;
 	private double[]     y_labels;
 	private double[]	 label_widths;
 	private double[]	 pos_widths;
 	private double[][]   proba;
 	private double[]	 pos_markers;
 	private boolean[]    labelStatus; 
 	private String[] labels;
	private int whichLabelSide=LEFT_LABEL;
	private int whichPosSide=RIGHT_LABEL;
	private ChromAxe chromAxe=null;
	private String chromName;
	private String mapName;
	
	public ChromShape() {
		super();
	}
	/**
	 * @param chrom
	 * @return
	 */
	public void buildShape(Graphics2D graph, Object object) {
		// First init some parameters from the global parameters
		withMapName   = MetaGraphPar.WITH_MAP_NAME;
		withChromName = MetaGraphPar.WITH_CHROM_NAME;
		withMrkLabel  = MetaGraphPar.WITH_MARKER_NAME;
		withMrkPos    = MetaGraphPar.WITH_MARKER_POSITION;
		// Get the chromosome to draw
		Chromosome chrom = (Chromosome) object;
		if (withMapName) {
			this.mapName = chrom.getMapName();
			withMapName = (this.mapName != null && !this.mapName.equals(""));
		}
		if (withChromName) {
			this.chromName = chrom.getName();
			withChromName = (this.chromName != null && !this.chromName.equals(""));
			if (withChromName) {
				TextLayout t   = new TextLayout(this.chromName, MetaGraphPar.CHROM_NAME_FONT, graph.getFontRenderContext());
				y_chrom_label  = t.getBounds().getHeight();
				y_chrom        = t.getBounds().getHeight()+MetaGraphPar.CHROM_NAME_HSPACE;
			}	
		}
		// Build the chromosome axe
		chromAxe          = new ChromAxe(y_chrom, MetaGraphPar.CHROM_DISTANCE_SCALE*chrom.totd, chrom.totd);
		this.chrom_height = chromAxe.getHeight() + MetaGraphPar.CHROM_FLANKING_CEX*chromAxe.getHeight();
		// Then translate the chromAxe
		chromAxe.setYMin(chromAxe.getYMin()+MetaGraphPar.CHROM_FLANKING_CEX*chromAxe.getHeight()/2);
		chromAxe.setYMax(chromAxe.getYMax()+MetaGraphPar.CHROM_FLANKING_CEX*chromAxe.getHeight()/2);
		this.chrom_width  = MetaGraphPar.CHROM_WIDTH;
		this.pos_markers  = new double[chrom.nm];
		this.y_markers    = new double[chrom.nm];
		this.y_labels  	  = new double[chrom.nm];
		this.labels    	  = new String[chrom.nm];
		for(int i=0;i<chrom.nm;i++) {
			this.pos_markers[i]      = chrom.getDistance(i);
			this.y_markers[i]    	 = chromAxe.transformY(this.pos_markers[i]);
			this.y_labels[i] 		 = this.y_markers[i];
			this.labels[i]   		 = chrom.mrkNames[i]; 
		}
		if (withMrkLabel || withMrkPos) {
			// Check the labels
			checkLabelLocation(graph);
			resolveLabelLocation(graph);
		}
		// Now if some proba are defined along the chromosome then get them
		this.proba = chrom.getProba();
		if (this.proba != null) {
			for(int i=0;i<this.proba.length;i++) {
				this.proba[i][0]=chromAxe.transformY(this.proba[i][0]);
				this.proba[i][1]=chromAxe.transformY(this.proba[i][1]);
			}
		}
		Qtl[] mqtls = chrom.mqtls;
		if (mqtls != null) {
			this.ymin_mqtls      = new double[mqtls.length];
			this.ymax_mqtls      = new double[mqtls.length];
			for(int i=0;i<mqtls.length;i++) {
				this.ymin_mqtls[i] = chromAxe.transformY(mqtls[i].position - mqtls[i].sd_position*1.96);
				this.ymax_mqtls[i] = chromAxe.transformY(mqtls[i].position + mqtls[i].sd_position*1.96);
			}
		}		
		initShapeDimension(graph);
	}
	
	private TextLayout getLabelTextLayout(FontRenderContext fctx, int idx) {
		if (labels != null)
			return new TextLayout(labels[idx], MetaGraphPar.MARKER_NAME_FONT, fctx);
		else 
			return null;
	}
	
	/* (non-Javadoc)
	 * @see org.thalia.metaqtl.graph.MetaQtlGraph.GraphShape#draw(java.awt.Graphics2D)
	 */
	public void draw(Graphics2D graph) {
		double xx,xl,xp,yy;
		Line2D.Double line;
		
		xx = getXChrom();		
		yy = y_chrom;
		
		if (proba != null) {
			for(int i=0;i<proba.length;i++) {
				Rectangle2D.Double prect = new Rectangle2D.Double(xx, proba[i][0], chrom_width, proba[i][1]-proba[i][0]);
				graph.setColor(MetaGraphPar.getProbaColor(proba[i][2]));
				graph.fill(prect);
			}	
		}
		
		if (ymin_mqtls != null && ymax_mqtls != null) {
			for(int i=0;i<ymin_mqtls.length;i++) {
				Rectangle2D.Double prect = new Rectangle2D.Double(xx, ymin_mqtls[i], chrom_width, ymax_mqtls[i]-ymin_mqtls[i]);
				graph.setColor(MetaGraphPar.QTL_PALETTE[i]);
				graph.fill(prect);
			}
		}
		
		RoundRectangle2D.Double rect = new RoundRectangle2D.Double(xx, yy, chrom_width, chrom_height, 10, 10);
		graph.setColor(MetaGraphPar.CHROM_STROKE_COLOR);
		graph.setStroke(MetaGraphPar.CHROM_STROKE);
		graph.draw(rect);
		
		if (y_markers != null) {
			graph.setColor(MetaGraphPar.CHROM_TICK_COLOR);
			graph.setStroke(MetaGraphPar.CHROM_TICK_STROKE);
			for(int i=0;i<y_markers.length;i++) {
				// First draw a line at the marker position 
				line = new Line2D.Double(getXChrom() , y_markers[i], getXChrom() + chrom_width, y_markers[i]);
				graph.draw(line);
				if (withMrkLabel) {
					TextLayout labelTxt = getLabelTextLayout(graph.getFontRenderContext(), i);
					// Get the path to this label and draw it
					GeneralPath path = getPath2MrkLabel(i);
					graph.draw(path);
					xl = getLabelX(labelTxt, (whichLabelSide==LEFT_LABEL));
					labelTxt.draw(graph, (float)xl, (float)(y_labels[i]+labelTxt.getBounds().getHeight()/2.0));
				}
				if (withMrkPos) {
					TextLayout posTxt   = getPositionTextLayout(graph.getFontRenderContext(), i);
					// Get the path to this label and draw it
					GeneralPath path = getPath2MrkPos(i);
					graph.draw(path);
					xp = getLabelX(posTxt, (whichPosSide==LEFT_LABEL));
					posTxt.draw(graph, (float)xp, (float)(y_labels[i]+posTxt.getBounds().getHeight()/2.0));
				}				
			}
		}
		if (withMapName) {
			TextLayout txt = new TextLayout(this.mapName, MetaGraphPar.CHROM_NAME_FONT, graph.getFontRenderContext());
//			xx  = getXChrom() + chrom_width/2;
//			xx -= txt.getBounds().getHeight()/2;
//			txt.draw(graph, (float)xx, (float)y_map_label);
			AffineTransform t = graph.getTransform();
			AffineTransform rot   = new AffineTransform();
			rot.rotate(Math.PI/2, xx, y_map_label);
			graph.transform(rot);
			txt.draw(graph, (float)xx, (float)y_map_label);
			// Restore the transformation context
			graph.setTransform(t);			
			
		}
		if (withChromName) {
			TextLayout t = new TextLayout(this.chromName, MetaGraphPar.CHROM_NAME_FONT, graph.getFontRenderContext());
			xx  = getXChrom() + chrom_width/2;
			xx -= t.getBounds().getWidth()/2;
			t.draw(graph, (float)xx, (float)y_chrom_label);
		}
	}
	/**
	 * @param i
	 * @return
	 */
	private double getLabelX(TextLayout label, boolean side) {
		double xx=0;
		if (side) {
			xx  = getXChrom();
			xx -= getTickWidth();
			xx -= label.getBounds().getWidth();
		} else {
			xx  = getXChrom()+chrom_width;
			xx += getTickWidth();
		}
		return xx;
	}
	/**
	 * 
	 * @param i
	 * @return
	 */
	private GeneralPath getPath2MrkPos(int i) {
		return getPath2Label(i, (whichPosSide==LEFT_LABEL));
	}
	
	private GeneralPath getPath2MrkLabel(int i) {
		return getPath2Label(i, (whichLabelSide==LEFT_LABEL));
	}
	/**
	 * @param i
	 * @param whichLabelSide2
	 * @return
	 */
	private GeneralPath getPath2Label(int i, boolean side) {
		double xx=0;
		GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, 4);
		// First horizontal line
		if (side) { // Left
			xx = getXChrom();
			path.moveTo((float)xx, (float)y_markers[i]);
			xx -= tick_width_1;
		} else { // Right
			xx = getXChrom()+chrom_width;
			path.moveTo((float)xx, (float)y_markers[i]);
			xx += tick_width_1;
		}
		path.lineTo((float)(xx), (float)y_markers[i]);
		// Oblique line
		if(side) {
			xx -= tick_width_2;
		} else {
			xx += tick_width_2;
		}
		path.lineTo((float)(xx), (float)y_labels[i]);			
		// Last horizontal line
		if (side) {
			xx -= tick_width_3;
		} else {
			xx += tick_width_3;
		}
		path.lineTo((float)(xx), (float)y_labels[i]);
		return path;
	}
	/**
	 * 
	 * @return
	 */
	public double getXChrom() {
		double shift = getTickWidth();
		// Do we have to draw the labels
		if (withMrkLabel) {
			// Do we have to draw the positions
			if (withMrkPos) {
				if (whichPosSide == LEFT_LABEL)
					return max_pos_width + shift;
				else 
					return max_label_width + shift;
			} else {
				if (whichLabelSide == LEFT_LABEL)
					return max_label_width + shift;
			}
		} else {
			// Do we have to draw the positions
			if (withMrkPos)
				if (whichPosSide == LEFT_LABEL)
					return max_pos_width + shift;
		}
		return 0.0;
	}
	
	
	
	private void initShapeDimension(Graphics2D graph) {
		double y_shift;
		
		height=width=0.0; 
			
		if (withMrkLabel) {
			label_widths=new double[labels.length];
			for(int i=0;i<labels.length;i++) {
				label_widths[i]=getLabelWidth(graph, i);
				if (label_widths[i]>width)
					this.width=label_widths[i];
			}
			this.max_label_width       = this.width;
			width+=getTickWidth();
		}
		if (withMrkPos) {
			pos_widths=new double[pos_markers.length];
			for(int i=0;i<pos_markers.length;i++)
				pos_widths[i]=getPositionWidth(graph, i);
			max_pos_width=getPositionWidth(graph, pos_markers.length-1);
			width+=max_pos_width;
			width+=getTickWidth();
		}
		width         += this.chrom_width;
		height  	   = y_chrom+this.chrom_height;
		if (this.y_labels !=null) {
			if (this.y_labels[0] < 0.0) {
				y_shift = this.getLabelHeight(graph, 0)-this.y_labels[0];
				/*this.y_chrom += y_shift;
				chromAxe.setYMin(chromAxe.getYMin()+y_shift);
				chromAxe.setYMax(chromAxe.getYMax()+y_shift);*/
				height+=y_shift;
				/*for(int i=0;i<y_labels.length;i++)
					this.y_labels[i]+=y_shift;*/
			}
			if (this.y_labels[y_labels.length-1] > this.chrom_height) {
				height += (this.y_labels[y_labels.length-1]-this.chrom_height);
				height += this.getLabelHeight(graph, y_labels.length-1);
			}
		}
		if (withMapName) {
			TextLayout t = new TextLayout(this.mapName, MetaGraphPar.CHROM_NAME_FONT, graph.getFontRenderContext());
			y_map_label  = height + MetaGraphPar.CHROM_NAME_HSPACE;
			height      += t.getBounds().getWidth()+MetaGraphPar.CHROM_NAME_HSPACE;
			width        = Math.max(width, t.getBounds().getHeight());
		}
		
	}
	/**
	 * @return
	 */
	private double getTickWidth() {
		double w  = 1.1*this.tick_width_1;
		w += this.tick_width_2;
		w += this.tick_width_3;
		return w;
	}
	/**
	 * @param i
	 * @return
	 */
	private TextLayout getPositionTextLayout(FontRenderContext fctx, int idx) {
		if (pos_markers != null) {
			String s = NumberFormat.formatDouble(pos_markers[idx]);
			return new TextLayout(s, MetaGraphPar.MARKER_POSITION_FONT, fctx);
		} else 
			return null;
	}
	
	private double getPositionWidth(Graphics2D graph, int idx) {
		TextLayout t = getPositionTextLayout(graph.getFontRenderContext(), idx);
		if (t != null) return t.getBounds().getWidth();
		return 0.0;
	}
	/**
	 * 
	 *
	 */
	private void checkLabelLocation(Graphics2D graph) {
		double y1,y2;
		
		labelStatus = new boolean[labels.length];
		for(int i=0;i<labels.length;i++) labelStatus[i]=true;
		
		for(int i=0;i<labels.length-1;i++) {
			y1  =  y_labels[i];
			y1 += getLabelHeight(graph, i);
			y2  = y_labels[i+1];
			labelStatus[i]&=(y1<=y2);
			labelStatus[i+1]&=(y1<=y2);			
		}
	}
	/**
	 * 
	 * @return
	 */
	private void resolveLabelLocation(Graphics2D graph) {
		int i,j,nl;
		double y_top, y_bottom,space,require,y_box;
		
		// Go along the chromosome, find the conflicts and
		// try to solve them.
		for(i=0;i<labels.length;) {
			nl=1;
			if (!labelStatus[i]) {
				if (i>0) y_top=y_labels[i-1]+getLabelHeight(graph, i-1);
				else y_top=y_labels[i]; 
				// How many label are implied in the conflict ?
				for(j=i+1;j<labels.length;j++,nl++)
					if (labelStatus[j]) break;
				if (i+nl<labels.length)
					y_bottom=y_labels[i+nl];
				else y_bottom=y_labels[i+nl-1]+getLabelHeight(graph, i+nl-1);
				space=y_bottom-y_top;
				// What is the amout of space necessary to solve
				// this conflict ?
				require=0.0; // a slight shift...
				for(j=i;j<i+nl;j++)
					require+=getLabelHeight(graph, j);
				if (space < require) {
					// shift the rigth and left labels in order
					// to obtain as many space as require.
					if (i+nl<labels.length-1) {
						for(j=i-1;j>=0;j--) {
							y_labels[j]-=(require-space)/2.0;
							if (j>0 && y_labels[j-1]+getLabelHeight(graph, j-1) < y_labels[j])
								break;
						}
					} else {
						for(j=i;j<labels.length;j++) 
							y_labels[j]+=(require-space);
					}
					if (i>0) {
						for(j=i+nl;j<labels.length;j++) {
							y_labels[j]+=(require-space)/2.0;
							if (j+1<labels.length && y_labels[j+1] > y_labels[j]+getLabelHeight(graph, j))
								break;
						}
					} else {
						for(j=0;j<nl;j++)
							y_labels[j]-=(require-space);
					}
					// Now re-position the labels
					if (i>0) y_top=y_labels[i-1] + getLabelHeight(graph, i-1);
					else y_top=y_labels[i]; 
					for(j=0;j<nl;j++) {
						y_labels[i+j] = y_top;
						y_top        += getLabelHeight(graph, i+j);
					}
					y_bottom=y_top;
					if (i>0) y_top=y_labels[i-1] + getLabelHeight(graph, i-1);
					else y_top=y_labels[i];
				} else {
					
					
					// Then divide the space in as many boxes as the number
					// of labels to position.
					y_box = space/(double)nl;
					for(j=0;j<nl;j++) {
						y_labels[i+j] = y_top + (y_box-getLabelHeight(graph, i+j))/2.0;
						y_top        += y_box;
					}
				}
			}
			i+=nl;
		}
	}
	
	private double getLabelHeight(Graphics2D graph, int i) {
		TextLayout t = getLabelTextLayout(graph.getFontRenderContext(), i);
		double h;
		if (i < labels.length-1) {
			h=t.getBounds().getHeight()+MetaGraphPar.MAKER_NAME_VSPACE;
		}
		else {
			h=t.getBounds().getHeight();
		}
		t=null;
		return h;
	}
	
	private double getLabelWidth(Graphics2D graph, int i) {
		TextLayout t = getLabelTextLayout(graph.getFontRenderContext(), i);
		double w;
		w=t.getBounds().getWidth();
		t=null;
		return w;
	}
	
	private double getMapLabelHeigth(Graphics2D graph, int i) {
		TextLayout t = getLabelTextLayout(graph.getFontRenderContext(), i);
		double w;
		w=t.getBounds().getWidth();
		t=null;
		return w;
	}
	
	public void setLabelSide(int side) {
		this.whichLabelSide=side;
	}
	/**
	 * @return
	 */
	public double getChromWidth() {
		return this.chrom_width;
	}
	/**
	 * @return
	 */
	public ChromAxe getChromAxe() {
		return this.chromAxe;
	}
	/* (non-Javadoc)
	 * @see org.thalia.metaqtl.graph.GraphShape#getYMin()
	 */
	public double getYMin() {
		if (y_labels!=null) 
			return Math.min(y_chrom, y_labels[0]);
		else return y_chrom;
	}
	/* (non-Javadoc)
	 * @see org.thalia.metaqtl.graph.GraphShape#getYMax()
	 */
	public double getYMax() {
		double y_max = y_chrom+chrom_height;
		if (y_labels!=null) { 
			y_max = Math.max(y_max, y_labels[y_labels.length-1]);
		}
		if (mapName != null) {
			y_max = Math.max(y_max, y_map_label);
		}
		return y_max;
	}
	/**
	 * @param mrkIdx
	 * @return
	 */
	public Point2D getMrkTickPoint2D(int mrkIdx, boolean reverse) {
		double xx=0,yy=0;
		if (reverse) {
			if (withMrkLabel) {
				switch (whichLabelSide) {
					case LEFT_LABEL :
						xx=max_label_width-label_widths[mrkIdx]*1.05;
						yy=y_labels[mrkIdx];
						break;
					case RIGHT_LABEL :
						if (withMrkPos) {
							xx=max_pos_width-pos_widths[mrkIdx]*1.05;
							yy=y_labels[mrkIdx];
						} else {
							xx=getXChrom()*0.95;
							yy=y_markers[mrkIdx];
						}
						break;
				}
			} else if (withMrkPos) {
				if (whichLabelSide == LEFT_LABEL) {
					xx=max_pos_width-pos_widths[mrkIdx]*1.05;
					yy=y_labels[mrkIdx];
				} else {
					xx=getXChrom()*0.95;
					yy=y_markers[mrkIdx];
				}
			} else {
				xx=getXChrom()*0.95;
				yy=y_markers[mrkIdx];
			}
		}
		else {
			if (withMrkLabel) {
				switch (whichLabelSide) {
					case LEFT_LABEL :
						if (withMrkPos) {
							xx=getXChrom()+chrom_width+getTickWidth()+pos_widths[mrkIdx]*1.05;
							yy=y_labels[mrkIdx];
						} else {
							xx=getXChrom()+chrom_width*1.05;
							yy=y_markers[mrkIdx];
						}
						break;
					case RIGHT_LABEL :
						xx=getXChrom()+chrom_width+getTickWidth()+label_widths[mrkIdx]*1.05;
						yy=y_labels[mrkIdx];
						break;
				}
			} else if (withMrkPos) {
				switch (whichLabelSide) {
					case LEFT_LABEL :
						xx=getXChrom()+chrom_width*1.05;
						yy=y_markers[mrkIdx];
						break;
					case RIGHT_LABEL :
						xx=getXChrom()+chrom_width+getTickWidth()+label_widths[mrkIdx]*1.05;
						yy=y_labels[mrkIdx];
						break;
				}
			} else {
				xx=getXChrom()+chrom_width*1.05;
				yy=y_markers[mrkIdx];
			}
		}
		return new Point2D.Double(xx, yy);
	}
	/**
	 * @return
	 */
	public int getLabelSide() {
		return this.whichLabelSide;
	}	
}