/*******************************************************************************
 * Copyright (c) 2010 Attila Kovacs <attila_kovacs[AT]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[AT]post.harvard.edu> - initial API and implementation
 ******************************************************************************/
package util;

import nom.tam.fits.*;

public class SphericalGrid implements Cloneable {

	public SphericalProjection projection;
	public Vector2D refIndex = new Vector2D();
	public double delta;

	@Override
	public boolean equals(Object o) {
		if(!(o instanceof SphericalGrid)) return false;
		SphericalGrid grid = (SphericalGrid) o;
		if(!grid.projection.getClass().equals(projection.getClass())) return false;
		if(grid.delta != delta) return false;
		if(grid.dXofIndex(0) != dXofIndex(0)) return false;
		if(grid.dYofIndex(0) != dYofIndex(0)) return false;
		return true;
	}
	
	public boolean equals(Object o, double precision) {
		if(!(o instanceof SphericalGrid)) return false;
		SphericalGrid grid = (SphericalGrid) o;
		if(!grid.projection.getClass().equals(projection.getClass())) return false;
		if(Math.abs(grid.delta / delta - 1.0) > precision) return false;
		if(Math.abs(grid.dXofIndex(0) - dXofIndex(0)) / delta > precision) return false;
		if(Math.abs(grid.dYofIndex(0) - dYofIndex(0)) / delta > precision) return false;
		return true;
	}
	
	@Override
	public int hashCode() {
		return projection.hashCode() ^ (~refIndex.hashCode()) ^ HashCode.get(delta);
	}
	
	
	@Override
	public Object clone() {
		try { return super.clone(); }
		catch(CloneNotSupportedException e) { return null; }
	}
	
	public SphericalGrid copy() {
		SphericalGrid copy = (SphericalGrid) clone();
		copy.projection = projection.copy();
		copy.refIndex = (Vector2D) refIndex.clone();
		return copy;
	}
	
	public boolean isHorizontal() {
		return getReference() instanceof HorizontalCoordinates;
	}
	
	public boolean isEquatorial() {
		return getReference() instanceof EquatorialCoordinates;
	}
	
	public boolean isEcliptic() {
		return getReference() instanceof EclipticCoordinates;
	}
	
	public boolean isGalactic() {
		return getReference() instanceof GalacticCoordinates;
	}
	
	public boolean isSuperGalactic() {
		return getReference() instanceof SuperGalacticCoordinates;
	}
	
	public void addCoordinateInfo(BasicHDU hdu) throws HeaderCardException {
		SphericalCoordinates reference = projection.getReference();
		
		hdu.addValue("CTYPE1", reference.coordinateSystem.get(0).wcsName + "-" + projection.getFitsID(), "Longitude Coordinate Type and Projection");
		hdu.addValue("CRPIX1", refIndex.x + 1, "Reference grid position");
		hdu.addValue("CRVAL1", reference.longitude()/Unit.deg, "Longitude at reference position (deg)");
		hdu.addValue("CDELT1", (reference.isReverseLongitude() ? -1 : 1) * delta/Unit.deg, "Grid spacing (deg)");	
		hdu.addValue("CROTA1", 0.0, "Axis rotation (deg)");
		
		hdu.addValue("CTYPE2", reference.coordinateSystem.get(1).wcsName + "-" + projection.getFitsID(), "Latitude Coordinate Type and Projection");
		hdu.addValue("CRPIX2", refIndex.y + 1, "Reference grid position");
		hdu.addValue("CRVAL2", reference.latitude()/Unit.deg, "Latitude at reference position (deg)");
		hdu.addValue("CDELT2", (reference.isReverseLatitude() ? -1 : 1) * delta/Unit.deg, "Grid spacing (deg)");
		hdu.addValue("CROTA2", 0.0, "Axis rotation (deg)");

		if(reference instanceof EquatorialCoordinates) {
			EquatorialCoordinates equatorial = (EquatorialCoordinates) reference;
			hdu.addValue("EQUINOX", equatorial.epoch.getYear(), "The catalog coordinate epoch.");
			hdu.addValue("RADESYS", equatorial.epoch instanceof JulianEpoch ? "FK5" : "FK4", "The Coordinate System Convention.");
		}	
		
		if(reference instanceof EclipticCoordinates) {
			EclipticCoordinates ecliptic = (EclipticCoordinates) reference;
			hdu.addValue("EQUINOX", ecliptic.epoch.getYear(), "The catalog coordinate epoch.");
			hdu.addValue("RADESYS", ecliptic.epoch instanceof JulianEpoch ? "FK5" : "FK4", "The Coordinate System Convention.");			
		}	
	}
	
	public void getCoordinateInfo(Header header) throws HeaderCardException {
		
		String type = header.getStringValue("CTYPE1");
	
		try { projection = SphericalProjection.forName(type.substring(5, 8)); }
		catch(Exception e) {
			System.err.println("ERROR! Unknown projection " + type.substring(5, 8));
		}
		
		SphericalCoordinates reference = null;
		
		if(type.toUpperCase().startsWith("ALON")) {
			reference = new HorizontalCoordinates();
			reference.parse(header);
		}
		else if(type.toUpperCase().startsWith("RA--")) {
			reference = new EquatorialCoordinates();
			reference.parse(header);
		}	
		else System.err.println("ERROR! Unknown Coordinate Definition " + type);
		
		projection.setReference(reference);
		
		refIndex.x = header.getDoubleValue("CRPIX1") - 1;
		refIndex.y = header.getDoubleValue("CRPIX2") - 1;
		
		delta = header.getDoubleValue("CDELT2") * Unit.deg;
	
		// Adjust the index to the determined pointing
		double di0 = (isHorizontal() ? -1 : 1) * header.getDoubleValue("RAP") * Unit.arcsec / delta;
		double dj0 = header.getDoubleValue("DECP") * Unit.arcsec / delta;
	
		refIndex.x += di0;
		refIndex.y += dj0;
	
		if(di0 != 0.0 || dj0 != 0.0)
			System.out.println("Shifting -> (" + 
					header.getDoubleValue("RAP") * Unit.arcsec / delta +  "," +
					header.getDoubleValue("DECP") * Unit.arcsec / delta + ")" );
		
	}
	
	@Override
	public String toString() {	
		SphericalCoordinates reference = projection.getReference();
		String projectionName = reference.getClass().getSimpleName();
		projectionName = projectionName.substring(0, projectionName.length() - "Coordinates".length());
		
		String info =
			"  " + projectionName + ": " + reference.toString() + "\n" +
			"  Projection: " + projection.getFullName() + " (" + projection.getFitsID() + ")\n" + 
			"  Grid Spacing: " + Util.f2.format(delta / Unit.arcsec) + " arcsec.\n";
		
		return info;
	}
	
	
	public final double dXofIndex(final double i) { return (i-refIndex.x) * delta; }
	   
    public final double dYofIndex(final double j) { return (j-refIndex.y) * delta; }
    
    public final int indexOfdX(final double dX) { return (int) Math.round(fracIndexOfdX(dX)); }
    
    public final int indexOfdY(final double dY) { return (int) Math.round(fracIndexOfdY(dY)); }

    public final double fracIndexOfdX(final double dX) { return refIndex.x + dX/delta; }
    
    public final double fracIndexOfdY(final double dY) { return refIndex.y + dY/delta; }
	
    public final SphericalCoordinates getReference() { return projection.getReference(); }
    
    public final SphericalProjection getProjection() { return projection; }

    public void getIndex(SphericalCoordinates coords, Vector2D index) {
    	projection.project(coords, index);
    	index.x = fracIndexOfdX(index.x);
    	index.y = fracIndexOfdY(index.y);
    }
   
    public void getCoords(Vector2D index, SphericalCoordinates coords) {
    	double i = index.x;
    	double j = index.y; 
    	
    	index.x = dXofIndex(index.x);
    	index.y = dYofIndex(index.y);
    	projection.deproject(index, coords);
    	
    	index.x = i;
    	index.y = j;
    }
    

    
	
}
