/*******************************************************************************
 * 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) 2009 Attila Kovacs 

package crush.sourcemodel;

import util.*;

import java.text.*;
import java.util.*;

public class CircularRegion extends Region {
	Vector2D center, centerRMS;
	DataPoint radius;
	
	public CircularRegion(SkyMap map) { super(map); }

	public CircularRegion(SkyMap map, SphericalCoordinates coords, double r) {
		this(map);
		setCenter(coords);
		radius = new DataPoint();
		radius.value = r;
		radius.setRMS(map.delta / SkyMap.fwhm2size / Util.sigmasInFWHM);
	}
	
	public CircularRegion(SkyMap map, String line) throws ParseException { super(map, line); }
	
	public void setCenter(SphericalCoordinates coords) {
		Vector2D offset = new Vector2D();
		map.projection.project(coords, offset);
		center = new Vector2D();
		map.getIndex(offset, center);		
	}
	
	@Override
	protected void getLimits() { getLimits(1.0); }
	
	protected void getLimits(double beams) {
		getLimits(beams, radius.value);
	}
	
	protected void getLimits(double beams, double FWHM) {
		double delta = beams * FWHM / map.delta;
		
		fromi = Math.max(0, (int)Math.floor(center.x - delta));
		toi = Math.min(map.sizeX()-1, (int)Math.ceil(center.x + delta));
		fromj = Math.max(0, (int)Math.floor(center.y-delta));
		toj = Math.min(map.sizeY()-1, (int) Math.ceil(center.y+delta));
	}

	public double distanceFrom(double i, double j) {
		double temp = i - center.x;
		double r = temp*temp;
		temp = j - center.y;
		r += temp*temp;
		return map.delta * Math.sqrt(r);
	}

	@Override
	public boolean isInside(double i, double j) { return distanceFrom(i, j) <= radius.value; }
	
	public void moveToPeak() throws IllegalStateException {
		getLimits();
		
		if(!map.containsIndex(center.x, center.y)) throw new IllegalStateException("Region falls outside of map.");
		MapIndex index = new MapIndex(center);
		
		double significance = map.getS2N(index.i, index.j);
		
		for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++) if(map.flag[i][j] == 0) 
			if(map.getS2N(i,j) > significance) if(isInside(i, j)) {
				significance = map.getS2N(i,j);
				index.i = i;
				index.j = j;				
			}
		
		if(map.flag[index.i][index.j] != 0) throw new IllegalStateException("No valid peak in search area. ");
		
		center.x = index.i;
		center.y = index.j;
		
		if(isInside(index.i+1, index.j)) if(isInside(index.i-1, index.j)) 
			if(isInside(index.i, index.j+1)) if(isInside(index.i, index.j-1)) finetunePeak();
	}
	
	public DataPoint finetunePeak() {
		int i = (int) Math.round(center.x);
		int j = (int) Math.round(center.y);
		
		double a=0.0,b=0.0,c=0.0,d=0.0;
			
		double y0 = map.getS2N(i,j);
	
		if(i>0 && i<map.sizeX()-1) if((map.flag[i+1][j] | map.flag[i-1][j]) == 0) {
			a = 0.5 * (map.getS2N(i+1,j) + map.getS2N(i-1,j)) - y0;
			c = 0.5 * (map.getS2N(i+1,j) - map.getS2N(i-1,j));
		}
		
		if(j>0 && j<map.sizeY()-1) if((map.flag[i][j+1] | map.flag[i][j-1]) == 0) {
			b = 0.5 * (map.getS2N(i,j+1) + map.getS2N(i,j-1)) - y0;
			d = 0.5 * (map.getS2N(i,j+1) - map.getS2N(i,j-1));
		}
		
		final double di = (a == 0.0) ? 0.0 : -0.5*c/a;
		final double dj = (b == 0.0) ? 0.0 : -0.5*d/b;		
		
		final double significance = y0 + (a*di + c)*di + (b*dj + d)*dj;			
		
		center.x = i + di;
		center.y = j + dj;
		
		y0 = map.getRMS(i,j);
		
		if(i>0 && i<map.sizeX()-1) if((map.flag[i+1][j] | map.flag[i-1][j]) == 0) {
			a = 0.5 * (map.getRMS(i+1,j) + map.getRMS(i-1,j)) - y0;
			c = 0.5 * (map.getRMS(i+1,j) - map.getRMS(i-1,j));
		}
		if(j>0 && j<map.sizeY()-1) if((map.flag[i][j+1] | map.flag[i][j-1]) == 0) {
			b = 0.5 * (map.getRMS(i,j+1) + map.getRMS(i,j-1)) - y0;
			d = 0.5 * (map.getRMS(i,j+1) - map.getRMS(i,j-1));
		}
		
		double dpeak = y0 + (a*di + c)*di + (b*dj + d)*dj;			
		double peak = dpeak * significance;
		
		return new DataPoint(peak, dpeak);
	}

	
	@Override
	public String toString() {
		return toString(FORMAT_CRUSH);
	}
	
	public String toString(int format) {
		String line = null;
		
		switch(format) {
		case FORMAT_CRUSH : line = toCrushString(); break;
		case FORMAT_OFFSET : line = toOffsetString(); break;
		case FORMAT_GREG : line = toGregString(); break;
		case FORMAT_DS9 : line = toDS9String(); break;
		}
		
		String comment = getComment();
		if(comment.length() > 0) line += "\t#" + comment; 
		
		return line;
	}
	
	public SphericalCoordinates getCoordinates() {
		SphericalCoordinates coords = (SphericalCoordinates) map.projection.getReference().clone();
		coords.addOffset(new Vector2D(map.dXofIndex(center.x), map.dYofIndex(center.y)));
		return coords;
	}
	
	public String toCrushString() {
		SphericalCoordinates coords = getCoordinates();
		
		((AngleFormat) coords.coordinateSystem.get(0).format).colons();
		((AngleFormat) coords.coordinateSystem.get(1).format).colons();
		
		return id + "\t" + coords.toString() + "  " + Util.f1.format(radius.value/Unit.arcsec) + " # " + comment;
	}

	public String toGregString() {
		return "ellipse " + Util.f1.format(radius.value/Unit.arcsec) + " /user " +
		Util.f1.format(-map.dXofIndex(center.x) / Unit.arcsec) + " " + Util.f1.format(map.dYofIndex(center.y) / Unit.arcsec);
	}
	
	public String toDS9String() {
		SphericalCoordinates coords = getCoordinates();
	
		((AngleFormat) coords.coordinateSystem.get(0).format).colons();
		((AngleFormat) coords.coordinateSystem.get(1).format).colons();
		
		String line = "circle(" 
			+ coords.coordinateSystem.get(0).format(coords.x) + ","
			+ coords.coordinateSystem.get(1).format(coords.y) + ","
			+ Util.f3.format(radius.value / Unit.arcsec) + "\")";
		
		return line;
	}
	
	public String toOffsetString() {
		Vector2D offset = new Vector2D();
		map.getOffset(center, offset);
		
		SphericalCoordinates reference = map.projection.getReference();
		CoordinateAxis x = reference.localCoordinateSystem.get(0);
		CoordinateAxis y = reference.localCoordinateSystem.get(1);
		
		return x.label + " = " + x.format(offset.x) + "\t" + y.label + " = " + y.format(offset.y);
	}
	
	public String getComment() {
		return "";
	}
	
	@Override
	public void parse(String line) throws ParseException {
		parseCrush(line);
	}
	
	public void parse(String line, int format) {	
		switch(format) {
		case FORMAT_CRUSH : parseCrush(line); break;
		//case FORMAT_OFFSET : parseOffset(line); break;
		case FORMAT_GREG : parseGreg(line); break;
		case FORMAT_DS9 : parseDS9(line); break;
		}
	}
	
	public StringTokenizer parseCrush(String line) {
		SphericalCoordinates coords = getCoordinates();
		
		((AngleFormat) coords.coordinateSystem.get(0).format).colons();
		((AngleFormat) coords.coordinateSystem.get(1).format).colons();
		
		StringTokenizer tokens = new StringTokenizer(line);
		id = tokens.nextToken();
		coords.parse(tokens.nextToken() + " " + tokens.nextToken() + " " + tokens.nextToken());
		setCenter(coords);
		radius.value = Double.parseDouble(tokens.nextToken()) * Unit.arcsec;
		radius.weight = 0.0;
		
		if(line.contains("#")) comment = line.substring(line.indexOf('#') + 2);
	
		return tokens;
	}

	public void parseGreg(String line) {
		StringTokenizer tokens = new StringTokenizer(line);
		if(!tokens.nextToken().equalsIgnoreCase("ellipse"))
			throw new IllegalArgumentException("WARNING! " + getClass().getSimpleName() + " can parse 'ellipse' only.");
		
		radius.value = Double.parseDouble(tokens.nextToken()) * Unit.arcsec;
		radius.weight = 0.0;
		// TODO What if not '/user' coordinates?
		tokens.nextToken(); // Assumed to be '/user';
		
		center.x = -Double.parseDouble(tokens.nextToken()) * Unit.arcsec;
		center.y = Double.parseDouble(tokens.nextToken()) * Unit.arcsec;
	}
	
	public void parseDS9(String line) {	
		SphericalCoordinates coords = getCoordinates();
	
		((AngleFormat) coords.coordinateSystem.get(0).format).colons();
		((AngleFormat) coords.coordinateSystem.get(1).format).colons();
	
		StringTokenizer tokens = new StringTokenizer(line, "(), \t");
		boolean isCircle = tokens.nextToken().equalsIgnoreCase("circle");
	
		coords.parse(tokens.nextToken() + " " + tokens.nextToken() + " (J2000)");
		
		if(isCircle) {
			String R = tokens.nextToken();
			char unit = R.charAt(R.length() - 1);
			radius.value = Double.parseDouble(R.substring(0, R.length()-1));
			if(unit == '\'') radius.value *= Unit.arcmin;
			else if(unit == '"') radius.value *= Unit.arcsec;
			radius.weight = 0.0;
		}
		else radius.value = Double.NaN;
	}
	
	
	public final static int FORMAT_CRUSH = 0;
	public final static int FORMAT_GREG = 1;
	public final static int FORMAT_DS9 = 2;
	public final static int FORMAT_OFFSET = 3;
	public final static int FORMAT_POINTING = 4; // TODO
	public final static int FORMAT_DISPLAY = 5; // TODO
	public final static int FORMAT_LONG = 6; // TODO
}

