/*******************************************************************************
 * Copyright (c) 2010 Attila Kovacs <attila_kovacs@post.harvard.edu>.
 * All rights reserved. 
 * 
 * This file is part of crush.
 * 
 *     crush 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 3 of the License, or
 *     (at your option) any later version.
 * 
 *     crush 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 crush.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Attila Kovacs <attila_kovacs@post.harvard.edu> - initial API and implementation
 ******************************************************************************/
// Copyright (c) 2010 Attila Kovacs 

package crush.gui;

import crush.*;
import crush.sourcemodel.Region;
import crush.sourcemodel.SkyMap;
import util.*;

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;


//Display with value/coordinate readout
class MapPanel extends JPanel {
	/**
	 * 
	 */
	private static final long serialVersionUID = 8619921669585536452L;
	
	SkyMap map;
	
	InfoPanel infobar;
	ColorScalePanel colorScale;
	Ruler ruler;
	Scale scale;
	AxisLabel fluxLabel;
	Box scaleBox;
	
	Vector<ColorScheme> colorSchemes = new Vector<ColorScheme>();
	ColorScheme colorScheme;
	int useScheme = 0;
	
	double x0, y0; // The panel coordinates corresponding to the 0,0 data index (center of drawn pixel!) 
	double dx, dy; // The pixel sizes;
	int dw, dh;

	double zoom=1.0;
	boolean inverted = false;
	boolean circled = true;
	boolean masked = false;
	boolean polygonDrawMode = false;

	public MapPanel() {
		setMinimumSize(new Dimension(100, 100));
		setSize(200, 100);
		infobar = new InfoPanel(3);
		
		//setBorder(BorderFactory.createLineBorder(Color.black));
		setBackground(Color.LIGHT_GRAY);

		
		colorScale = new ColorScalePanel(20);
		colorScale.colorScheme = (ColorScheme) colorSchemes.get(useScheme);
		ruler = new Ruler();
		scale = ruler.scale;
		fluxLabel = new AxisLabel(map.fitsUnit.name, AxisLabel.RIGHT);

		scaleBox = Box.createHorizontalBox();

		scaleBox.add(colorScale);
		scaleBox.add(ruler);
		scaleBox.add(fluxLabel);
	
		setDefaults();

		addMouseListener(new MouseAdapter() {
			double centerX, centerY;

			public void mouseEntered(MouseEvent evt) {
				setCursor(new java.awt.Cursor(java.awt.Cursor.CROSSHAIR_CURSOR));
			}
			public void mouseExited(MouseEvent evt) {
				setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR));
				infobar.clear();
			}
			public void mousePressed(MouseEvent evt) {			
				if(!polygonDrawMode) if(circled || masked) {
					centerX = getXOffset(evt.getX());
					centerY = getYOffset(evt.getY());			
				}
			}
			public void mouseReleased(MouseEvent evt) {
				if(!polygonDrawMode) if(circled || masked) if(evt.getButton() == MouseEvent.BUTTON1) {
					double radius = Math.pow(centerX - getXOffset(evt.getX()), 2.0);
					radius += Math.pow(centerY - getYOffset(evt.getY()), 2.0);
					radius = Math.sqrt(radius); 
					regions.add(new Region(centerX, centerY, radius));
					repaint();
				}
			}

			public void mouseClicked(MouseEvent evt) {
				if(polygonDrawMode) {
					centerX = getXOffset(evt.getX());
					centerY = getYOffset(evt.getY());	
					
					Polygon polygon = polygons.get(polygons.size() -1);
					
					if(evt.getButton() == MouseEvent.BUTTON1) polygon.add(new Vector2D(map.fracIndexOfdX(centerX), map.fracIndexOfdY(centerY)));
					if(evt.getButton() == MouseEvent.BUTTON3) if(polygons.size() > 0) polygon.remove(polygon.size() - 1); 
					
					repaint();
				}
				
				else if(circled || masked) {
					if(evt.getButton() == MouseEvent.BUTTON2) 
						regions.add(new Region(centerX, centerY, map.getImageFWHM()));
					if(evt.getButton() == MouseEvent.BUTTON3) 
						regions.add(new Region(centerX, centerY, 2.0*map.getImageFWHM()));
					repaint();
				}
			}					    
		});
	
		addMouseMotionListener(new MouseMotionAdapter() {
			public void mouseMoved(MouseEvent evt) {
				int i = (int)Math.round(geti(evt.getX()));
				int j = (int)Math.round(getj(evt.getY()));

				offset.x = getXOffset(evt.getX());
				offset.y = getYOffset(evt.getY());

				map.projection.deproject(offset, coords);
				offset.scale(1.0 / Unit.arcsec);
							
				if(i>=0 && i<map.sizeX && j>=0 && j<map.sizeY) if(map.flag[i][j] == 0) {			
					if(map == signal) {
						String value = "Flux " + format(s2n[i][j]).format(map[i][j]/displayUnit.value) + " +- ";
						value += Util.s2.format(rms[i][j]/displayUnit.value) + " " + displayUnit.name;
						infobar.lines[2] = value;
					}
					else if(map == s2n) infobar.lines[2] = "S/N " + format(s2n[i][j]).format(s2n[i][j]); 
					else if(map == rms) infobar.lines[2] = "RMS " + Util.s3.format(rms[i][j]/displayUnit.value) + " " + displayUnit.name; 
				} 
				else infobar.lines[2] = "";

				if(map.isHorizontal) infobar.lines[1] = "AZO " + Util.f1.format(offset.x) + "  ELO "+ Util.f1.format(offset.y) + "  ";
				else infobar.lines[1] = "RAO " + Util.f1.format(-offset.x) + "  DECO "+ Util.f1.format(offset.y) + "  ";
				
				infobar.lines[0] = "POS " + coords.toString();

				infobar.repaint();
				
			}
		});
		
		ruler.addMouseListener(new MouseAdapter() {
			
			
			
			public void mouseEntered(MouseEvent evt) {
				setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR));
				infobar.lines[0] = "You can drag the zero point to change level";
				infobar.lines[1] = "Drag ruler elsewhere to change dynamic range";
				infobar.lines[2] = "Click ruler to toggle linear/log/sqrt scaling";
			}
			
			public void mouseExited(MouseEvent evt) {
				infobar.clear();
			}
			
			public void mouseClicked(MouseEvent evt) {
				if(evt.getButton() == MouseEvent.BUTTON1) {
					if(scale.logarithmic) scale.sqrt();
					else if(scale.sqrt) scale.linear();
					else scale.log();
					setDefaults();
					repaint(); 
				}
				
			}
			
			public void mousePressed(MouseEvent evt) {		
				fromScale = (Scale) scale.clone();
				fromScaled = ruler.getValue(evt.getY());
				double zeroScaled = scale.getScaled(0.0);
				
				if(Math.abs(fromScaled - zeroScaled) < 0.05) 
					setCursor(new java.awt.Cursor(java.awt.Cursor.MOVE_CURSOR));
				else 
					setCursor(new java.awt.Cursor(java.awt.Cursor.N_RESIZE_CURSOR));
			}
			public void mouseReleased(MouseEvent evt) {
				setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR));
				repaint();
			}
			
		});
		
		ruler.addMouseMotionListener(new MouseMotionAdapter() {
			public void mouseDragged(MouseEvent evt) {
				toScaled = ruler.getValue(evt.getY());

				double zeroScaled = fromScale.getScaled(scale.logarithmic ? 1.0 : 0.0);
				
				scale.min = fromScale.min;
				scale.max = fromScale.max;
				
				if(Math.abs(fromScaled - zeroScaled) < 0.05) {
					offsetScale(fromScaled - toScaled);
				}
				else {
					double stretch = (toScaled - zeroScaled) / (fromScaled - zeroScaled);
					if(stretch < 0.0) {
						fromScaled = 2.0*zeroScaled - fromScaled;
						inverted = !inverted;
						colorScale.invert();
						stretch *= -1;
					}
					expandScale(1.0 / stretch);
				}
					
				ruler.repaint();	
			}
		});
		
		
		
		
		fluxLabel.addMouseListener(new MouseAdapter() {
			public void mouseClicked(MouseEvent evt) {	
				//if(evt.getClickCount() == 1) return;
				
				int index = units.indexOf(displayUnit);
				index++;
				if(index >= units.size()) index = 0;
				Unit oldUnit = displayUnit;
				displayUnit = units.get(index);
				Scale scale = display.scale;
				scale.setScale(scale.min * oldUnit.value/displayUnit.value, scale.max * oldUnit.value/displayUnit.value);
					
				display.fluxLabel.label = map == s2n ? "S/N" : displayUnit.name;
				
				fluxLabel.repaint();
				ruler.repaint();
			}	
			

			public void mouseEntered(MouseEvent evt) {
				infobar.lines[0] = "Click on label to change unit.";
			}
			
			public void mouseExited(MouseEvent evt) {
				infobar.clear();
			}
			
		});
		
		colorScale.addMouseListener(new MouseAdapter() {
			public void mouseClicked(MouseEvent evt) {
				int n = evt.getButton();
				
				if(n == MouseEvent.BUTTON1) {
					useScheme=(useScheme+1)%colorSchemes.size();  
					repaint();
				}
				else if(n == MouseEvent.BUTTON3) {
					inverted = !inverted;
					colorScale.invert();
					repaint();
				}
			}	
			
			public void mouseEntered(MouseEvent evt) {
				infobar.lines[0] = "Click mouse to change color scale.";
				infobar.lines[1] = "Right-click to invert colors.";
			}
			
			public void mouseExited(MouseEvent evt) {
				infobar.clear();
			}
			
		});
		
		infobar.addMouseListener(new MouseAdapter() {
			
			public void mouseEntered(MouseEvent evt) {
				infobar.lines[0] = "Press 'h' for help on action keys.";
				infobar.lines[1] = "Press '?' for image summary.";
			}
			
			public void mouseExited(MouseEvent evt) {
				infobar.clear();
			}
			
		});
		
	}	
	
	

	public DecimalFormat format(double significance) {
		if(Double.isNaN(significance)) return Util.f1;
		if(significance == 0.0) return Util.f2;
		int figures = Math.min(4, (int) Math.log10(Math.abs(significance)) + 2);
		figures = Math.max(1, figures);
		return Util.s[figures];
	}
	
	public void setDefaults() {
		if(map == s2n) {
			if(scale.logarithmic) scale.setScale(1.0, getMax(s2n));
			else scale.setScale(getMin(s2n), getMax(s2n));
		}
		else if(map == rms) scale.setScale(getMin(rms)/displayUnit.value, getMax(rms)/displayUnit.value);
		else {
			if(scale.logarithmic) scale.setScale(getMin(rms)/displayUnit.value, getMax(signal)/displayUnit.value);
			else scale.setScale(getMin(signal)/displayUnit.value, getMax(signal)/displayUnit.value);				
		}
	}	
	
	private double fromScaled = 0.0, toScaled = 0.0;
	private Scale fromScale;
	


	public void expandScale(double factor) {
		if(scale.logarithmic) scale.setScale(Math.pow(scale.min, factor), Math.pow(scale.max, factor));
		else if(scale.sqrt) scale.setScale(factor*factor * scale.min, factor*factor * scale.max);
		else { scale.setScale(factor*scale.min, factor*scale.max); }
	}
	
	public void offsetScale(double relativeToRange) {
		double offset = scale.range * relativeToRange;
		
		if(scale.logarithmic) scale.setScale(scale.min * Math.pow(10.0, offset), scale.max * Math.pow(10.0, offset));
		else if(scale.sqrt) {
			// range ~ Smax * sqrt(|max|) - Smin * sqrt(|min|);
			// zero ~ -Smin * sqrt(|min|)
			// sqrt(|min|) ---> sqrt(|min|) + offset
			double min = Math.sqrt(Math.abs(scale.min)) - offset;
			double max = Math.sqrt(Math.abs(scale.max)) + offset;
			scale.setScale(Math.signum(scale.min) * min * min, Math.signum(scale.max) * max * max);
		}
		else scale.setScale(scale.min + offset, scale.max + offset);
	}
	
	public Dimension getPreferredSize() {
		return new Dimension(imageWidth, imageHeight);
	}

	public void paintComponent(Graphics g) {
		super.paintComponent(g);

		colorScheme = (ColorScheme) colorSchemes.get(useScheme);
		colorScale.colorScheme = colorScheme;

		int imageWidth = (int)Math.ceil(map.sizeX*zoom);
		int imageHeight = (int)Math.ceil(map.sizeY*zoom);

		dx = (double) imageWidth / map.sizeX;
		dy = (double) imageHeight / map.sizeY;
		dw = (int)Math.ceil(dx);
		dh = (int)Math.ceil(dy);
		
		int top = (height - imageHeight)/2;
		int left = (width - imageWidth)/2;
		
		x0 = left + dx/2;
		y0 = height - top - dy/2;

		graphics = g.create(left, top, imageWidth, imageHeight);

		for(int j=0; j<sizeY; j++) for(int i=0; i<sizeX; i++) if(flag[i][j] == 0) drawPoint(i, j); 

		if(masked) for(int i=0; i<regions.size(); i++) maskRegion(regions.get(i));
		if(circled) for(int i=0; i<regions.size(); i++) circleRegion(regions.get(i));
		if(names && (circled || masked)) {
			setFont(font);
			for(int i=0; i<regions.size(); i++) idRegion(regions.get(i));
		}
		for(Polygon polygon : polygons) drawPolygon(polygon, polygon.isClosed);
		

		// Repaint the scales
		scaleBox.repaint();
		colorScale.repaint();
		ruler.repaint();

		display.fluxLabel.label = map == s2n ? "S/N" : displayUnit.name;
		display.fluxLabel.repaint();
	}


	private final void drawPoint(int i, int j) {	 
		double value = scale.getScaled(map == s2n ? map[i][j] : map[i][j]/displayUnit.value);

		if(Double.isNaN(value)) value = 0.0; // needed for log scaling...
		if(inverted) value = 1.0-value;

		graphics.setColor(colorScheme.getColor(value));
		graphics.fillRect((int) Math.floor(i*dx), (int) Math.floor(imageHeight-(j+1)*dy), dw, dh);
	}



	Font font = new Font("Monospaced", Font.PLAIN, 10);
	
	private void maskRegion(Region region) {
		double a = dx * region.radius / delta;
		double b = dy * region.radius / delta;				
		double x = dx * fracIndexOfdX(region.dX);
		double y = imageHeight - dy * fracIndexOfdY(region.dY);
		
		int x0 = (int)Math.round(x - 0.5 * a);
		int y0 = (int)Math.round(y - 0.5 * b);

		graphics.setColor(new Color(1.0F, 1.0F, 1.0F, 0.5F));
		graphics.fillOval(x0, y0, (int)Math.round(a), (int)Math.round(b));
	}
	
	private void idRegion(Region region) {
		double a = dx * region.radius / delta;
		double b = dy * region.radius / delta;				
		double x = dx * fracIndexOfdX(region.dX);
		double y = imageHeight - dy * fracIndexOfdY(region.dY);
		
		setFont(font);
		
		graphics.drawString(region.id, (int)Math.round(x + a + 3), (int)Math.round(y - b - 3));
	}

	private void circleRegion(Region region) {
		graphics.setColor(((ColorScheme)colorSchemes.get(useScheme)).getHighlight());
		
		double a = dx * 2.0 * region.radius / delta;
		double b = dy * 2.0 * region.radius / delta;					
		double x = dx * (0.5 + fracIndexOfdX(region.dX));
		double y = imageHeight - dy * (0.5 + fracIndexOfdY(region.dY));
		
		int x0 = (int)Math.round(x - 0.5 * a);
		int y0 = (int)Math.round(y - 0.5 * b);
		int xc = (int)Math.round(x);
		int yc = (int)Math.round(y);

		graphics.drawOval(x0, y0, (int)Math.round(a), (int)Math.round(b));
		
		graphics.drawLine(xc - 5, yc, xc + 5, yc);
		graphics.drawLine(xc, yc - 5, xc, yc + 5);
	}
	
	private void drawPolygon(Polygon polygon, boolean closed) {
		getGraphics().setColor(((ColorScheme)colorSchemes.get(useScheme)).getHighlight());
		
		int size = closed ? polygon.size() : polygon.size() - 1; 
		
		for(int i=0; i<size; i++) {
			Vector2D from = polygon.get(i);
			Vector2D to = polygon.get((i+1)%polygon.size());
			
			int fromx = (int) Math.round(dx * (0.5 + from.x));
			int fromy = (int) Math.round(imageHeight - dy * (0.5 + from.y));
			int tox = (int) Math.round(dx * (0.5 + to.x));
			int toy = (int) Math.round(imageHeight - dy * (0.5 + to.y));
			
			graphics.drawLine(fromx, fromy, tox, toy);
		}
		
	}

	public final double geti(final int x) { 
		return (x-x0)/dx;
	}

	public final double getj(final int y) { return (y0-y)/dy; } 

	public final double getXOffset(final int x) { return map.dXofIndex(geti(x)); }

	public final double getYOffset(final int y) { return map.dYofIndex(getj(y)); }


