// (C)2007 Attila Kovacs <attila@submm.caltech.edu>

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

import nom.tam.fits.*;
import nom.tam.util.*;

// TODO add BinaryTableIO interface (with projections...)

public class SphericalCoordinates extends CoordinatePair {
	protected double cosLat = 1.0;
	protected double sinLat = 0.0;
	
	public CoordinateSystem coordinateSystem;
	public CoordinateSystem localCoordinateSystem;

	public SphericalCoordinates() {	
		super();
		defaults();
	}

	public SphericalCoordinates(double X, double Y) { this(); setCanonical(X,Y); }
	
	public SphericalCoordinates(String text) { this(); parse(text); }
	
	public void defaults() {	
		double[] angularUnits = { Unit.deg, Unit.arcmin, Unit.arcsec, Unit.mas };
		AngleFormat af = new AngleFormat(3);
		
		coordinateSystem = new CoordinateSystem("Spherical Coordinates");
		coordinateSystem.add(new CoordinateAxis("phi"));
		coordinateSystem.add(new CoordinateAxis("theta"));
		
		for(CoordinateAxis axis : coordinateSystem) {
			axis.setFormat(af);
			axis.setTickUnits(angularUnits);
		}

		localCoordinateSystem = new CoordinateSystem(coordinateSystem);
		localCoordinateSystem.get(0).setLabel("d phi"); 
		localCoordinateSystem.get(1).setLabel("d theta");
	}
	
	public boolean equals(Object o) {
		if(o.getClass().equals(getClass())) return false;
		SphericalCoordinates coords = (SphericalCoordinates) o;
		if(!equalAngles(coords.x, x)) return false;
		if(!equalAngles(coords.y, y)) return false;
		return true;		
	}
	
	public void copy(CoordinatePair coords) {
		setLongitude(coords.x);
		setLatitude(coords.y);
	}

	public void zero() { super.zero(); cosLat = 1.0; sinLat = 0.0; }

	public void NaN() { super.NaN(); cosLat = Double.NaN; sinLat = Double.NaN; }

	public void set(double lon, double lat) { setLongitude(lon); setLatitude(lat); }
	
	public void setCanonical(double lon, double lat) { setCanonicalLongitude(lon); setCanonicalLatitude(lat); }
	
	public final double longitude() { return x; }
	
	public final double latitude() { return y; }
	
	// Like long on lat except returns the actual directly formattable
	// coordinates for this system...
	public final double canonicalLongitude() { return coordinateSystem.get(0).isReverse() ? coordinateSystem.get(0).reverseFrom-longitude() : longitude(); }
	
	public final double canonicalLatitude() { return coordinateSystem.get(1).isReverse() ? coordinateSystem.get(1).reverseFrom-latitude() : latitude(); }
	
	public final void setLongitude(double value) { x = Math.IEEEremainder(value, 2.0*Math.PI); }
	
	public final void setLatitude(double value) { 
		y = Math.IEEEremainder(value, Math.PI);
		cosLat = Math.cos(y);
		sinLat = Math.sin(y);
	}

	public final void setCanonicalLongitude(double value) {
		setLongitude(coordinateSystem.get(0).isReverse() ? coordinateSystem.get(0).reverseFrom-value : value);
	}
	
	public final void setCanonicalLatitude(double value) {
		setLatitude(coordinateSystem.get(1).isReverse() ? coordinateSystem.get(1).reverseFrom-value : value);
	}
	
	public double getCosLat() { return cosLat; }
	
	public double getSinLat() { return sinLat; }

	
	public void project(SphericalProjection projection, CoordinatePair toOffset) {
		projection.project(this, toOffset);
	}
	
	public void setProjected(SphericalProjection projection, CoordinatePair fromOffset) {
		projection.deproject(fromOffset, this);
	}
		
	public CoordinatePair getProjected(SphericalProjection projection) { return projection.getProjected(this); }
	
	
	public void addOffset(Vector2D offset) {
		x += offset.x / cosLat;
		setLatitude(y + offset.y);
	}
	
	public Vector2D getOffsetFrom(SphericalCoordinates reference) {
		Vector2D offset = new Vector2D();
		getOffsetFrom(reference, offset);
		return offset;
	}
	
	public void getOffsetFrom(SphericalCoordinates reference, Vector2D toOffset) {
		toOffset.x = (x - reference.x) * reference.cosLat;
		toOffset.y = y - reference.y;
	}	
		
	public void standardize() {
		x = Math.IEEEremainder(x, 2.0*Math.PI);
		y = Math.IEEEremainder(y, Math.PI);
	}
	
	public String[] getFitsAxisNames(SphericalProjection projection) {
		String[] name = new String[2];
		name[0] = coordinateSystem.get(0).wcsName + projection.getFitsID();
		name[1] = coordinateSystem.get(1).wcsName + projection.getFitsID();	
		return name;
	}
	
	public String toString() {
		return coordinateSystem.get(0).format(x) + " " + coordinateSystem.get(1).format(y);
	}
	
	public String toString(NumberFormat nf) {
		return nf.format(canonicalLongitude()) + " " + nf.format(canonicalLatitude());		
	}
	
	// Just use decimalformat. else one should call e.g. format(new HourAngleFormat(df))
	public String toString(DecimalFormat df) {		
		return df.format(canonicalLongitude()) + " " + df.format(canonicalLatitude());
	}

	public void parse(String coords) throws NumberFormatException, IllegalArgumentException {
		StringTokenizer tokens = new StringTokenizer(coords, ", \t\n");
		
		try {
			setCanonicalLongitude(coordinateSystem.get(0).format.parse(tokens.nextToken()).doubleValue());
			setCanonicalLatitude(coordinateSystem.get(1).format.parse(tokens.nextToken()).doubleValue());
		} 
		catch(ParseException e) { throw new NumberFormatException(e.getMessage()); }
	}
	
	// Safe asin and acos for when rounding errors make values fall outside of -1:1 range.
	protected static double asin(double value) {
		if(value < -1.0) value = -1.0;
		else if(value > 1.0) value = 1.0;
		return Math.asin(value);
	}
	
	protected static double acos(double value) {
		if(value < -1.0) value = -1.0;
		else if(value > 1.0) value = 1.0;
		return Math.acos(value);
	}

	public double distanceTo(SphericalCoordinates point) {
		double sindTheta = Math.sin(point.y - y);
		double sindPhi = Math.sin(point.x - x);
		return 2.0 * asin(Math.sqrt(sindTheta * sindTheta + cosLat * point.cosLat * sindPhi * sindPhi));
	}

	public void edit(Cursor cursor) throws HeaderCardException { edit(cursor, ""); }
	
	public void edit(Cursor cursor, String alt) throws HeaderCardException {
		cursor.add(new HeaderCard("CRVAL1" + alt, canonicalLongitude() / Unit.deg, "The reference longitude coordinate (deg)."));
		cursor.add(new HeaderCard("CRVAL2" + alt, canonicalLatitude() / Unit.deg, "The reference latitude coordinate (deg)."));
	}

	
	public void parse(Header header) { parse(header, ""); }
		
	public void parse(Header header, String alt) {
		setCanonicalLongitude(header.getDoubleValue("CRVAL1" + alt, 0.0) * Unit.deg);
		setCanonicalLatitude(header.getDoubleValue("CRVAL2" + alt, 0.0) * Unit.deg);
	}
	
	
	public static boolean equalAngles(double a1, double a2) {
		return Math.abs(a1-a2) < angularAccuracy;
	}
	
	public final static double angularAccuracy = 1e-12;
	public final static double fullTurn = 2.0 * Math.PI;
	public final static double halfTurn = Math.PI;
	public final static double quarterTurn = 0.5 * Math.PI;
	
	public static void transform(SphericalCoordinates from, SphericalCoordinates newPole, double phi0, SphericalCoordinates to) {		
		double dL = from.x - newPole.x;
		double cosdL = Math.cos(dL);	
		to.setLatitude(asin(newPole.sinLat * from.sinLat + newPole.cosLat * from.cosLat * cosdL));
		double acosdL = -from.cosLat * Math.sin(dL);
		double asindL = -from.sinLat * newPole.cosLat + from.cosLat * newPole.sinLat * cosdL;
		to.setLongitude(quarterTurn + Math.atan2(asindL, acosdL) - phi0);	
	}
	
	public static void inverseTransform(SphericalCoordinates from, SphericalCoordinates pole, double phi0, SphericalCoordinates to) {		
		double dL = from.x + phi0;
		double cosdL = Math.cos(dL);
		
		to.setLatitude(asin(pole.sinLat * from.sinLat + pole.cosLat * from.cosLat * cosdL));
		double acosdL = -from.cosLat * Math.sin(dL);
		double asindL = -from.sinLat * pole.cosLat + from.cosLat * pole.sinLat * cosdL;
		
		to.setLongitude(pole.x + quarterTurn + Math.atan2(asindL, acosdL));	
	}
	


}
