/*  
 *  src/org/metaqtl/graph/MetaGraph.java
 * 
 *  $Author: veyrieras $, $Date: 2006-10-17 15:41:54 $, $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.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

public class MetaGraph {
	
	private double height;
	
	private double width;
	
	private double sx;
	
	private double sy;
	
	public MetaGraph.DataLayer[]  dataLayers=null;
	
	public MetaGraph.CrossLayer[] crossLayers=null;
	
	public MetaGraph.LegendLayer legendLayer=null;	
	/**
	 * 
	 * @param graph
	 * @param x
	 * @param y
	 * @param sx
	 * @param sy
	 */
	public void draw(Graphics2D graph, double x, double y) {
		
		AffineTransform t         = graph.getTransform();
		AffineTransform translate = new AffineTransform();
		
		translate.translate(x,y);
		
		graph.transform(translate);
		
		if (dataLayers != null) {
			
			for(int i=0;i<dataLayers.length;i++) {
				
				dataLayers[i].draw(graph);		
			
			}
		}
		
		if (crossLayers != null) {
			
			for(int i=0;i<crossLayers.length;i++) {
				
				crossLayers[i].draw(graph);		
				
			}
			
		}
		
		if (legendLayer != null) {
			
			legendLayer.draw(graph);
			
		}
						
		graph.setTransform(t);
	}
	
	private void initColor() {
		MetaGraphPar.initPalettes();
		int np = getQtlPartNames().size();
		if (MetaGraphPar.QTL_PALETTE==null || MetaGraphPar.QTL_PALETTE.length<np) {
			MetaGraphPar.initQTLPalette(np);		
		}
	}
	
	private ArrayList getQtlPartNames() {
		for(int i=0;i<dataLayers.length;i++) {
			ArrayList p =dataLayers[i].getQtlPartNames();
			if (p!=null && p.size()>0) return p;
		}
		return new ArrayList();
	}
	/**
	 * 
	 * @param graph
	 * @param maps
	 */
	public void init(Graphics2D graph, ChromGraph[] maps) {
		optimizeGraph(graph);
		initDataLayers(maps.length);
		boolean hasProba = false;
		for(int i=0;i<maps.length;i++) {
			dataLayers[i].attach(maps[i]);
			dataLayers[i].build(graph);
			hasProba |= dataLayers[i].hasProba();
		}
		organizeDataLayers();
		crossDataLayers(graph);
		initColor();
		if (MetaGraphPar.WITH_LEGEND) {
			height+=MetaGraphPar.LAYER_VSPACE;
			legendLayer(graph, 0, height, hasProba);
		}
	}	
	/**
	 * 
	 * @return
	 */
	public double getHeight() {
		return height;
	}
	/**
	 * 
	 * @return
	 */
	public double getWidth() {
		return width;
	}
	/**
	 * 
	 * @param nlayer
	 */
	private void initDataLayers(int nlayer) {
		this.dataLayers   = new MetaGraph.DataLayer[nlayer];
		for(int i=0;i<nlayer;i++)
			this.dataLayers[i] = new MetaGraph.DataLayer(0,0);
	}
	/**
	 * 
	 *
	 */
	private void organizeDataLayers() {
		double y_min=0.0;
		height=width=0.0;
		// First align and look if there are some layers with
		// negative minimal ordonate.
		for(int i=0;i<dataLayers.length;i++) {
			dataLayers[i].align(dataLayers[0], MetaGraphPar.CHROM_ALIGN_MODE);
			if (dataLayers[i].getYMin() < y_min)
				y_min = dataLayers[i].getYMin();
		}
		for(int i=0;i<dataLayers.length;i++) {
			height = Math.max(height, Math.abs(dataLayers[i].getYMin())+dataLayers[i].getHeight());
			dataLayers[i].position(width, -y_min);	
			width += dataLayers[i].getWidth();
			if (i<dataLayers.length-1) width += MetaGraphPar.LAYER_VSPACE;
		}
	}
	/**
	 * 
	 *
	 */
	private void crossDataLayers(Graphics2D graph) {
		crossLayers = new CrossLayer[dataLayers.length-1];
		for(int i=0;i<dataLayers.length-1;i++) {
			crossLayers[i] = new CrossLayer(0,0);
			crossLayers[i].attach(getCrossData(dataLayers[i], dataLayers[i+1]));
			crossLayers[i].build(graph);
		}
	}
	
	private void legendLayer(Graphics2D graph, double xx, double yy, boolean withProba) {
		LegendData legendData  = new LegendData();
		legendData.mapScale    = MetaGraphPar.CHROM_DISTANCE_SCALE;
		ArrayList t = getQtlPartNames();
		legendData.partNames = new String[t.size()];
		for(int i=0;i<t.size();i++) legendData.partNames[i] = (String) t.get(i);
		legendData.partColors = MetaGraphPar.QTL_PALETTE;
		if (withProba) {
			legendData.gColors = MetaGraphPar.PROBA_PALETTE;
			legendData.gmin    = 0;
			legendData.gmax    = 1;
		}
		legendLayer = new LegendLayer(xx, yy);
		legendLayer.attach(legendData);
		legendLayer.build(graph);
		// Update graph dimension
		height += legendLayer.getHeight();
		width   = Math.max(width, legendLayer.getWidth());
	}
	/**
	 * 
	 * @param layer1
	 * @param layer2
	 * @return
	 */
	private CrossData getCrossData(DataLayer layer1, DataLayer layer2) {
		CrossData cross = new CrossData();
		if (MetaGraphPar.WITH_COMMON_MARKER)
			cross.cMrkPaths = layer1.getCommonMarkerPaths(layer2);
		return cross;
	}
	/**
	 * 
	 * @param graph
	 */
	private void optimizeGraph(Graphics2D graph) {
		graph.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		graph.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
		graph.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		graph.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);		
	}	
	
	
	private class CrossData {
		ChromLayer.CMrkTickPath[] cMrkPaths;
		
	}
	
	private class CrossLayer extends Layer {
		
		private CrossData crossData;
		
		public CrossLayer(double x, double y) {
			super(x,y);
		}

		/* (non-Javadoc)
		 * @see org.thalia.metaqtl.graph.Layer#attach(java.lang.Object)
		 */
		public void attach(Object object) {
			if (object instanceof CrossData) {
				crossData = (CrossData) object;				
			}
		}

		/* (non-Javadoc)
		 * @see org.thalia.metaqtl.graph.Layer#build(java.awt.Graphics2D)
		 */
		public void build(Graphics2D graph) {
			// nothing to do
		}

		/* (non-Javadoc)
		 * @see org.thalia.metaqtl.graph.Layer#draw(java.awt.Graphics2D)
		 */
		public void draw(Graphics2D graph) {
			if (crossData != null) {
				Color           c     = graph.getColor();
				AffineTransform t     = graph.getTransform();
				AffineTransform trans = new AffineTransform();
				trans.translate(x,y);
				graph.transform(trans);
				ChromLayer.CMrkTickPath[] paths = crossData.cMrkPaths;
				if (paths != null) {
					for(int i=0;i<paths.length;i++) {
						//float[] dash = new float[] {1, 2}; 
						graph.setStroke(new BasicStroke((float) MetaGraphPar.COMMON_STROKE_WIDTH));//, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 1, dash, 1));
						switch(paths[i].status) {
							case ChromLayer.SINGLE_COMMON :
								graph.setColor(MetaGraphPar.SINGLE_COMMON_COLOR);
								break;
							case ChromLayer.POS_COMMON :
								graph.setColor(MetaGraphPar.POS_COMMON_COLOR);
								break;
							case ChromLayer.NEG_COMMON :
								graph.setColor(MetaGraphPar.NEG_COMMON_COLOR);
								break;
						}
						graph.draw(paths[i].path);
					}
				}
				graph.setTransform(t);
				graph.setColor(c);
			}
		}

		/* (non-Javadoc)
		 * @see org.thalia.metaqtl.graph.Layer#getYMin()
		 */
		public double getYMin() {
			// TODO Auto-generated method stub
			return 0;
		}

		/* (non-Javadoc)
		 * @see org.thalia.metaqtl.graph.Layer#getYMax()
		 */
		public double getYMax() {
			// TODO Auto-generated method stub
			return 0;
		}
	}						  
	
	private class DataLayer extends Layer {
		
		public ChromLayer chromLayer;
		
		public QtlPartitionLayer qtlPartLayer;
		
		// Only for meta-analysis result display 
		public QtlPartitionLayer qtlPartLayer2;
		
		public DataLayer(double x, double y) {
			super(x, y);
		}
		/**
		 * @return
		 */
		public boolean hasProba() {
			return chromLayer.hasProba();
		}
		/**
		 * @param right_label
		 */
		public void setMrkLabelSide(int side) {
			chromLayer.setLabelSide(side);
		}

		public void attach(Object object) {
			if (object instanceof ChromGraph) {
				ChromGraph chromGraph = (ChromGraph) object; 
				chromLayer = new ChromLayer(x, y);
				chromLayer.attach(chromGraph.chromosome);
				qtlPartLayer = new QtlPartitionLayer(x, y);
				qtlPartLayer.attach(chromGraph.qtlPart);
			}
		}
		/**
		 * @return
		 */
		public double getHeight() {
			return Math.max(chromLayer.getHeight(), qtlPartLayer.getHeight());
		}
		/**
		 * 
		 * @return
		 */
		public double getWidth() {
			return chromLayer.getWidth()+MetaGraphPar.LAYER_VSPACE+qtlPartLayer.getWidth();
		}
		/**
		 * 
		 * @return
		 */
		public double getYMin() {
			return Math.min(chromLayer.getYMin(), qtlPartLayer.getYMin());
		}
		/**
		 * 
		 * @return
		 */
		public double getYMax() {
			return Math.min(chromLayer.getYMax(), qtlPartLayer.getYMax());
		}
		/**
		 * 
		 * @param x
		 * @param y
		 */
		public void position(double x, double y) {
			chromLayer.setX(chromLayer.getX()+x);
			chromLayer.setY(chromLayer.getY()+y);
			qtlPartLayer.setX(x+qtlPartLayer.getX()+MetaGraphPar.LAYER_VSPACE+chromLayer.getWidth());
			qtlPartLayer.setY(qtlPartLayer.getY()+y);
		}
		
		public void align(DataLayer ref, int mode) {
			ChromLayer.alignChromLayers(ref.chromLayer, chromLayer, mode);
			qtlPartLayer.setY(chromLayer.getY());
		}
		
		/**
		 * 
		 * @param graph
		 * @param chromGraph
		 * @param idx
		 */
		public void build(Graphics2D graph)
		{
			buildChromLayer(graph);
			buildQtlPartLayer(graph);
		}
		/**
		 * 
		 * @param graph
		 */
		public void draw(Graphics2D graph) {
			chromLayer.draw(graph);
			qtlPartLayer.draw(graph);
		}
		/**
		 * @return
		 */
		public ArrayList getQtlPartNames() {
			return qtlPartLayer.getQtlPartNames();
		}
		/**
		 * 
		 * @param layer1
		 * @param layer2
		 */
		public ChromLayer.CMrkTickPoint[] getCommonMarkerPoints(DataLayer layer2) {
			if (chromLayer != null)
				return chromLayer.getCommonMarkerPoints(layer2.chromLayer);
			return null;
		}
		
		public ChromLayer.CMrkTickPath[] getCommonMarkerPaths(DataLayer layer2) {
			if (chromLayer != null)
				return chromLayer.getCommonMarkerPaths(layer2.chromLayer);
			return null;
		}
		/**
		 * 
		 * @param graph
		 * @param chrom
		 * @param idx
		 */
		private void buildChromLayer(Graphics2D graph) {
			if (chromLayer != null) {
				chromLayer.build(graph);
			}
		}
		/**
		 * 
		 * @param graph
		 * @param qtlPart
		 * @param idx
		 */
		private void buildQtlPartLayer(Graphics2D graph) {
			if (qtlPartLayer != null) {
				if (chromLayer != null)
					qtlPartLayer.setChromAxe(chromLayer.getChromAxe());
				qtlPartLayer.build(graph);
			}
		}		
		
		
	}
	
	private class LegendData {
		// The scale of the map in cM
		public double mapScale=0;
		// The partition names
		public String[] partNames;
		// The partition colors
		public Color[] partColors;
		// The gradient
		public double gmin;
		public double gmax;
		public int    gnc;
		public Color[] gColors;
	}
	/**
	 * 
	 *
	 */
	private class LegendLayer extends Layer {
		private LegendData legendData;
		private boolean withScale=false;
		private boolean withPart=false;
		private boolean withGrad=false;
		private double x_scale;
		private double y_scale;
		private double w_scale;
		private double h_scale;
		private double x_grad;
		private double y_grad;
		private double h_grad;
		private double w_grad;
		private double h_part;
		private double w_part;
		private double[] x_parts;
		private double[] y_parts;		
		private TextLayout scaleTxt;
		private TextLayout gradMinTxt;
		private TextLayout gradMaxTxt;
		private TextLayout[] partTxts;
		/**
		 * @param x
		 * @param y
		 */
		public LegendLayer(double x, double y) {
			super(x, y);
		}

		/* (non-Javadoc)
		 * @see org.thalia.metaqtl.graph.Layer#attach(java.lang.Object)
		 */
		public void attach(Object object) {
			if (object instanceof LegendData) {
				legendData = (LegendData) object;
			}
		}

		/* (non-Javadoc)
		 * @see org.thalia.metaqtl.graph.Layer#build(java.awt.Graphics2D)
		 */
		public void build(Graphics2D graph) {
			// First comes the scale if defined
			if (legendData.mapScale>0) {
				withScale   = true;
				int unit    = MetaGraphPar.LEGEND_SCALE_UNIT;
				String text = new String(unit+" cM");
				x_scale     = 0;
				y_scale     = 0;
				w_scale     = unit*legendData.mapScale;
				h_scale     = MetaGraphPar.CHROM_TICK_WIDTH_1;
				scaleTxt    = new TextLayout(text, MetaGraphPar.LEGEND_FONT, graph.getFontRenderContext());
				height      = Math.max(h_scale, scaleTxt.getBounds().getHeight());
				width       = w_scale*1.1 + scaleTxt.getBounds().getWidth();
			}
			if (legendData.partNames!=null) {
				height += MetaGraphPar.LEGEND_HSPACE;
				withPart = true;
				h_part   = MetaGraphPar.LEGEND_PART_HEIGHT;
				w_part   = MetaGraphPar.LEGEND_PART_WIDTH;	
				partTxts = new TextLayout[legendData.partNames.length];
				x_parts  = new double[partTxts.length];
				y_parts  = new double[partTxts.length];
				for(int i=0;i<partTxts.length;i++) {
					y_parts[i]  = height;
					x_parts[i]  = 0;
					partTxts[i] = new TextLayout(legendData.partNames[i], MetaGraphPar.LEGEND_FONT, graph.getFontRenderContext());
					height     += Math.max(partTxts[i].getBounds().getHeight(), h_part);
					height     += MetaGraphPar.LEGEND_HSPACE;
					width       = Math.max(width, w_part*1.1 + partTxts[i].getBounds().getWidth());
				}
			}
			if (legendData.gColors != null) {
				height += MetaGraphPar.LEGEND_HSPACE;
				withGrad = true;
				y_grad   = height;
				x_grad   = 0;
				h_grad   = MetaGraphPar.LEGEND_GRAD_HEIGHT;
				w_grad   = MetaGraphPar.LEGEND_GRAD_WIDTH;
				gradMinTxt  = new TextLayout(Double.toString(legendData.gmin), MetaGraphPar.LEGEND_FONT, graph.getFontRenderContext());
				gradMaxTxt  = new TextLayout(Double.toString(legendData.gmax), MetaGraphPar.LEGEND_FONT, graph.getFontRenderContext());
				width    = Math.max(width, gradMinTxt.getBounds().getWidth()+w_grad*1.2+gradMaxTxt.getBounds().getWidth());
				height  += Math.max(h_grad, Math.max(gradMinTxt.getBounds().getHeight(),gradMaxTxt.getBounds().getHeight()));
			}
			// Rescale...
			if (withScale) {
				x_scale+=width*MetaGraphPar.LEGEND_BOX_CEX/2;
				y_scale+=height*MetaGraphPar.LEGEND_BOX_CEX/2;
			}
			if (withPart) {
				for(int i=0;i<x_parts.length;i++) {
					x_parts[i]+=width*MetaGraphPar.LEGEND_BOX_CEX/2;
					y_parts[i]+=height*MetaGraphPar.LEGEND_BOX_CEX/2;
				}
			}
			if (withGrad) {
				x_grad+=width*MetaGraphPar.LEGEND_BOX_CEX/2;
				y_grad+=height*MetaGraphPar.LEGEND_BOX_CEX/2;
			}
			width  *= (1+MetaGraphPar.LEGEND_BOX_CEX);
			height *= (1+MetaGraphPar.LEGEND_BOX_CEX);			
		}

		/* (non-Javadoc)
		 * @see org.thalia.metaqtl.graph.Layer#draw(java.awt.Graphics2D)
		 */
		public void draw(Graphics2D graph) {
			if (legendData != null) {
				Color           c     = graph.getColor();
				AffineTransform t     = graph.getTransform();
				AffineTransform trans = new AffineTransform();
				trans.translate(x,y);
				graph.transform(trans);
				graph.setStroke(new BasicStroke());
				if (withScale) {
					graph.setColor(Color.BLACK);
					Line2D.Double line = new Line2D.Double(x_scale, y_scale+h_scale/2, x_scale+w_scale, y_scale+h_scale/2);
					graph.draw(line);
					line.setLine(x_scale,y_scale,x_scale,y_scale+h_scale);
					graph.draw(line);
					line.setLine(x_scale+w_scale,y_scale,x_scale+w_scale,y_scale+h_scale);
					graph.draw(line);
					scaleTxt.draw(graph, (float)(x_scale+w_scale*1.1), (float)(y_scale+scaleTxt.getBounds().getHeight()));
					graph.setColor(Color.BLACK);
				}
				if (withPart) {
					for(int i=0;i<x_parts.length;i++) {
						Rectangle2D.Double rect = new Rectangle2D.Double(x_parts[i], y_parts[i], w_part, h_part);
						graph.setColor(legendData.partColors[i]);
						graph.fill(rect);
						graph.setColor(Color.BLACK);
						partTxts[i].draw(graph, (float)(x_parts[i]+w_part*1.1), (float)(y_parts[i]+h_part/2+partTxts[i].getBounds().getHeight()/2));
					}
				}
				if (withGrad) {
					double xx,ww;
					gradMinTxt.draw(graph, (float)x_grad, (float)(y_grad+h_grad));
					xx=x_grad+gradMinTxt.getBounds().getWidth()+w_grad*0.1;
					ww=w_grad/(double)legendData.gColors.length;
					for(int i=0;i<legendData.gColors.length;i++) {
						Rectangle2D.Double unit = new Rectangle2D.Double(xx, y_grad, ww, h_grad);
						graph.setColor(legendData.gColors[i]);
						graph.fill(unit);
						xx+=ww;
					}
					graph.setColor(Color.BLACK);
					xx=x_grad+gradMinTxt.getBounds().getWidth()+w_grad*0.1;
					Rectangle2D.Double stroke = new Rectangle2D.Double(xx, y_grad, w_grad, h_grad);
					graph.draw(stroke);
					for(int i=0;i<legendData.gColors.length;i+=2) {
						Line2D.Double sep = new Line2D.Double(xx, y_grad, xx, y_grad+h_grad);
						graph.draw(sep);
						xx+=2*ww;
					}					
					xx+=w_grad*0.1;
					gradMaxTxt.draw(graph, (float)xx, (float)(y_grad+h_grad));
					graph.setColor(Color.BLACK);
				}
				// Finally draw a box around the legend area
				Rectangle2D.Double box = new Rectangle2D.Double(0, 0, width, height);
				graph.draw(box);
				graph.setTransform(t);
				graph.setColor(c);
			}
		}

		/* (non-Javadoc)
		 * @see org.thalia.metaqtl.graph.Layer#getYMin()
		 */
		public double getYMin() {
			return y;
		}

		/* (non-Javadoc)
		 * @see org.thalia.metaqtl.graph.Layer#getYMax()
		 */
		public double getYMax() {
			return y+height;
		}
		
	}
	
	
}