/*******************************************************************************
 * 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
 ******************************************************************************/
// Copyright (c) 2009,2010 Attila Kovacs

package crush;

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

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

import crush.sourcemodel.*;
import util.*;

public abstract class Scan<InstrumentType extends Instrument<?>, IntegrationType extends Integration<InstrumentType, ?>>
extends Vector<IntegrationType> implements Comparable<Scan<InstrumentType, IntegrationType>>, TableEntries {
	/**
	 * 
	 */
	private static final long serialVersionUID = 8967822331907667222L;
	public InstrumentType instrument;
	
	public int serialNo = -1;
	
	public double MJD = Double.NaN, LST = Double.NaN;
	public String sourceName;
	public String timeStamp;
	public String descriptor;
	public String observer, project, creator;
	
	public EquatorialCoordinates equatorial, apparent;
	public HorizontalCoordinates horizontal;
	public GeodeticCoordinates site;
	public Precession fromApparent, toApparent;
	public Vector2D pointingCorrection;
	
	public Range longitudeRange, latitudeRange;
	public boolean isTracking = false;
	
	public SourceModel<?,?> sourceModel;
	public double weight = 1.0;
	
	public int sourcePoints = 0;
	public boolean hasSiblings = false;
	
	public GaussianSource pointing;
	
	// Creates a scan with an initialized copy of the instrument
	@SuppressWarnings("unchecked")
	public Scan(InstrumentType instrument) { 
		this.instrument = (InstrumentType) instrument.copy();
	}
	
	public int compareTo(Scan<InstrumentType, IntegrationType> other) {
		if(serialNo == other.serialNo) return 0;
		return serialNo < other.serialNo ? -1 : 1;
	}
	
	public void validate() {	
		System.err.println(" Processing scan data:");
		
		if(Double.isNaN(MJD)) MJD = 0.5 * (getFirstIntegration().getFirstFrame().MJD + getLastIntegration().getLastFrame().MJD);
		if(Double.isNaN(LST)) LST = 0.5 * (getFirstIntegration().getFirstFrame().LST + getLastIntegration().getLastFrame().LST);
		
		//System.err.println("### MJD: " + Util.f3.format(MJD) + " --> Epoch is J" + Util.f2.format(JulianEpoch.getYearForMJD(MJD)));
		//System.err.println("### LST: " + Util.f3.format(LST/Unit.hour));
		
		if(equatorial == null) calcEquatorial();

		// Use J2000 coordinates
		if(!equatorial.epoch.equals(CoordinateEpoch.J2000)) precess(CoordinateEpoch.J2000);
		System.err.println("   Equatorial: " + equatorial.toString());
		
		// Calculate apparent and approximate horizontal coordinates.... 
		if(apparent == null) calcApparent();
		if(horizontal == null && site != null) calcHorizontal();
		
		if(horizontal != null) System.err.println("   Horizontal: " + horizontal.toString());
	
		for(Integration<InstrumentType, ?> integration : this) integration.validate();	
		
		if(hasOption("pointing")) setPointing(option("pointing"));
	}
	
	public void setPointing(Configurator option) {		
		String value = option.getValue().toLowerCase();
		if(value.equals("auto") || value.equals("suggest")) return;
		
		Vector2D correction = option.getVector2D(); 
		System.err.println("   Adjusting pointing by " + correction + " " + instrument.getDefaultSizeName() + ".");
		correction.scale(instrument.getDefaultSizeUnit());
		pointingAt(correction);		
	}
	
	public void pointingAt(Vector2D correction) {
		for(Integration<?,?> integration : this) integration.pointingAt(correction);
		if(pointingCorrection == null) pointingCorrection = correction;
		else pointingCorrection.add(correction);
	}
	
	public void applyPointing() {
		Vector2D differential = instrument.getNativePointingIncrement(this, pointing);
		pointingAt(differential);
		
		Vector2D arcsecs = (Vector2D) differential.clone();
		arcsecs.scale(1.0 / Unit.arcsec);
		//System.err.println("### pointing at " + arcsecs);
		
		// Reset the source coordinates to the pointing center
		if(pointing.coords instanceof HorizontalCoordinates) pointing.coords = (SphericalCoordinates) horizontal.clone();
		else if(pointing.coords instanceof EquatorialCoordinates) pointing.coords = (SphericalCoordinates) equatorial.clone();
		else ((CelestialCoordinates) pointing.coords).fromEquatorial(equatorial);
		
		if(pointingCorrection == null) pointingCorrection = differential;
		else pointingCorrection.add(differential);
	}
	
	
	public void precess(CoordinateEpoch epoch) {
		Precession toEpoch = new Precession(equatorial.epoch, epoch);
		toEpoch.precess(equatorial);
		for(Integration<?,?> integration : this) for(Frame frame : integration) 
			if(frame != null) if(frame.equatorial != null) toEpoch.precess(frame.equatorial);	
		calcPrecessions(epoch);
	}
	
	public void calcPrecessions(CoordinateEpoch epoch) {
		JulianEpoch apparentEpoch = JulianEpoch.forMJD(MJD);
		fromApparent = new Precession(apparentEpoch, epoch);
		toApparent = new Precession(epoch, apparentEpoch);
	}
		
	public void calcEquatorial() {
		equatorial = horizontal.toEquatorial(site, LST);
		equatorial.epoch = new JulianEpoch();
		equatorial.epoch.setMJD(MJD);
		if(fromApparent == null) calcPrecessions(CoordinateEpoch.J2000);
		fromApparent.precess(equatorial);		
	}
	
	public void calcApparent() {
		apparent = (EquatorialCoordinates) equatorial.clone();
		if(toApparent == null) calcPrecessions(equatorial.epoch);
		toApparent.precess(apparent);
	}
	
	public void calcHorizontal() {
		if(apparent == null) calcApparent();
		horizontal = apparent.toHorizontal(site, LST);
	}
	
	public IntegrationType getFirstIntegration() {
		return get(0); 
	}

	public IntegrationType getLastIntegration() {
		return get(size()-1);
	}
	
	public boolean hasOption(String key) {
		return instrument.hasOption(key);
	}
	
	public Configurator option(String key) {
		return instrument.option(key);
	}

	// Read should validate the instrument before instantiating integrations for reading...
	public abstract void readHeader(String descriptor) throws Exception;
	
	// Read should validate the instrument before instantiating integrations for reading...
	public abstract void read(String descriptor) throws Exception;
	
	// The integration should carry a copy of the instrument s.t. the integration can freely modify it...
	// The constructor of Integration thus copies the Scan instrument for private use...
	public abstract IntegrationType getIntegrationInstance();
	
	public String getDataPath() {
		return hasOption("datapath") ? Util.getSystemPath(option("datapath").getValue()) + File.separator : "";
	}
	
	@SuppressWarnings("unchecked")
	public void mergeSubscans() {	
		if(size() < 2) return;

		// TODO What if different sampling intervals...
		System.err.println(" Merging integrations...");
		
		Integration<InstrumentType, Frame> single = (Integration<InstrumentType, Frame>) get(0).clone();

		for(int i=0; i<size(); i++) {
			IntegrationType integration = get(i);
			integration.reindex();
			
			int from = integration.getFirstFrame().index;
			int to = integration.getLastFrame().index;
		
			if(i>0) {
				double mjdGap = integration.get(from).MJD - get(i-1).getLastFrame().MJD;
				int padding = (int) Math.round(mjdGap * Unit.day / integration.instrument.samplingInterval);
				if(padding < 0) padding = 0;
				for(int t=0; t<padding; t++) single.add(null);
			}

			for(int t=from; t<=to; t++) single.add(integration.get(t));		
		}
	
		clear();
		add((IntegrationType) single);
	}
	
	public double getPA() {
		HorizontalFrame first = (HorizontalFrame) get(0).getFirstFrame();
		HorizontalFrame last = (HorizontalFrame) get(size()-1).getLastFrame();
		return 0.5 * (Math.atan2(first.sinPA, first.cosPA) + Math.atan2(last.sinPA, last.cosPA));
	}
	
	public BasicHDU getSummaryHDU(Configurator global) throws FitsException, IOException {
		Object[][] table = new Object[size()][];
		boolean details = hasOption("write.scandata.details");
		
		LinkedHashMap<String, Object> first = null;

		for(int i=0; i<table.length; i++) {
			IntegrationType integration = get(i);
			LinkedHashMap<String, Object> data = new LinkedHashMap<String, Object>();
			if(i == 0) first = data;
			integration.getFitsData(data);
			if(details) integration.addDetails(data);
			Object[] row = new Object[data.size()];
			int k=0;
			for(Object entry : data.values()) row[k++] = entry; 
			table[i] = row;
		}

		BasicHDU hdu = Fits.makeHDU(table);
		Header header = hdu.getHeader();
		editImageHeader(header);

		Cursor cursor = header.iterator();

		int k=1;
		for(String name : first.keySet()) {
			cursor.setKey("TFORM" + k);
			cursor.add(new HeaderCard("TTYPE" + (k++), name, "The column name"));
		}

		instrument.startupOptions.difference(global).editHeader(cursor);	
		
		return hdu;
	}

		
	public void editImageHeader(Header header) throws FitsException, HeaderCardException, IOException {	
		Locale.setDefault(Locale.US);
		
		header.addValue("EXTNAME", "Scan-" + getID(), "Scan data");
		
		header.addValue("INSTRUME", instrument.name, "The instrument name");
		
		if(serialNo > 0) header.addValue("SCANNO", serialNo, "Serial number for the scan");
		if(descriptor != null) header.addValue("SCANSPEC", descriptor, "Specifier by which the scan was invoked.");
		if(observer != null) header.addValue("OBSERVER", observer, "Name(s) of the observer(s).");
		if(project != null) header.addValue("PROJECT", project, "Description of the project");
		if(creator != null) header.addValue("CREATOR", creator, "Software that wrote the scan data.");
		if(timeStamp != null) header.addValue("DATE-OBS", timeStamp, "Start of observation.");
		
		// The Source Descriptors
		header.addValue("OBJECT", sourceName, "Object catalog name");
		header.addValue("RADESYS", (equatorial.epoch instanceof JulianEpoch ? "FK5" : "FK4"), "World coordinate system id.");
		header.addValue("RA", Util.hf2.format(equatorial.RA()), "Human Readable Right Ascention.");
		header.addValue("DEC", Util.af1.format(equatorial.DEC()), "Human Readable Declination.");
		header.addValue("EQUINOX", equatorial.epoch.getYear(), "Precession epoch.");	
	
		if(!Double.isNaN(MJD)) header.addValue("MJD", MJD, "Modified Julian Day.");
		if(!Double.isNaN(LST)) header.addValue("LST", LST / Unit.hour, "Local Sidereal Time (hours).");
		
		if(this instanceof GroundBased) {
			header.addValue("AZ", horizontal.AZ()/Unit.deg, "Azymuth (deg).");
			header.addValue("EL", horizontal.EL()/Unit.deg, "Elevation (deg).");
			header.addValue("PA", getPA()/Unit.deg, "Direction of zenith w.r.t. North (deg).");
		}
		
		if(site != null) {
			header.addValue("SITELON", Util.af1.format(site.longitude()), "Geographic longitude of the observing site (deg).");
			header.addValue("SITELAT", Util.af1.format(site.latitude()), "Geographic latitude of the observing site (deg).");
		}
		
		header.addValue("WEIGHT", weight, "Relative source weight of the scan.");
		
		header.addValue("TRACKIN", isTracking, "Was the telescope tracking during the observation?");
	}
	
	public void setSourceModel(SourceModel<?,?> model) {
		sourceModel = model;
	}
	
	public double getObservingTime() {
		double t = 0.0;
		for(IntegrationType integration : this) t += integration.getFrameCount(~0) * instrument.integrationTime;
		return t;
	}
	
	
	// separators: ' ' \t - =
	// entries:	name(format)
	//      format: e.g. d3, f1, e3, t:1 (time with colons, 1 decimal), ad2 (angle with symbols, 2 decimals)
	public void writeLog() throws IOException {
		String fileName = option("log").getValue();
		if(fileName.length() == 0) fileName = CRUSH.workPath + File.separator + instrument.name + ".log";
		PrintWriter out = new PrintWriter(new FileOutputStream(fileName, true));
		
		String format = hasOption("log.format") ? option("log.format").getValue() : "date(YYYY-MM-DD)\tserial\tobject\tobsmins(f1)\tAZ(f1) EL(f1)\tRA(f1) DEC(f1)";

		// TODO construct a header for new files...
		
		out.println(TableFormatter.format(this, format));
		out.close();
	}
	

	public void writeObsLog() throws IOException {
		String fileName = option("obslog").getValue();
		if(fileName.length() == 0) fileName = CRUSH.workPath + File.separator + instrument.name + ".obs.log";
		PrintWriter out = new PrintWriter(new FileOutputStream(fileName, true));
		
		String format = hasOption("obslog.format") ? option("obslog.format").getValue() : "MJD(d5)\tserial\tobject\tobsmins(f1)\tAZ(f1) EL(f1)\tRA(f1) DEC(f1)";

		// TODO construct a header for new files...
		
		out.println(TableFormatter.format(this, format));
		out.close();
	}
	
	public String getFormattedEntry(String name, String formatSpec) {
		NumberFormat f = TableFormatter.getNumberFormat(formatSpec);
		
		if(horizontal == null) horizontal = equatorial.toHorizontal(site, LST);
		
		if(name.equals("object")) return sourceName;
		else if(name.equals("id")) return getID();
		else if(name.equals("serial")) return Integer.toString(serialNo);
		else if(name.equals("MJD")) return defaultFormat(MJD, f);
		else if(name.equals("AZ")) return defaultFormat(horizontal.AZ(), f);
		else if(name.equals("EL")) return defaultFormat(horizontal.EL(), f);
		else if(name.equals("RA")) return defaultFormat(equatorial.RA() / Unit.timeAngle, f);
		else if(name.equals("DEC")) return defaultFormat(equatorial.DEC(), f);
		else if(name.equals("epoch")) return equatorial.epoch.toString();
		else if(name.equals("epochY")) return defaultFormat(equatorial.epoch.getYear(), f);
		else if(name.equals("LST")) return defaultFormat(LST, f);
		else if(name.equals("date")) {
			AstroTime time = new AstroTime();
			time.setMJD(MJD);
			return new SimpleDateFormat(formatSpec).format(time.getDate());
		}
		else if(name.equals("obstime")) return defaultFormat(getObservingTime() / Unit.sec, f);
		else if(name.equals("obsmins")) return defaultFormat(getObservingTime() / Unit.min, f);
		else if(name.equals("obshours")) return defaultFormat(getObservingTime() / Unit.hour, f);
		else if(name.equals("project")) return project;
		else if(name.equals("observer")) return observer; 
		else if(name.equals("descriptor")) return descriptor; 
		else if(name.equals("integrations")) return Integer.toString(size());
		return "N/A";
	}
	
	private String defaultFormat(double value, NumberFormat f) {
		return f == null ? Double.toString(value) : f.format(value);
	}
	
	public int getSourceGeneration() {
		int max = 0;
		for(Integration<?,?> integration : this) max = Math.max(integration.sourceGeneration, max);
		return max;		
	}
	
	public void writeProducts() {
		for(Integration<?,?> integration : this) integration.writeProducts();
		printPointing();
	}
	
	public void printPointing() {
		if(pointing != null) {
			System.out.println();
			System.out.println(" Pointing Results for Scan " + getID() + ":");
			System.out.println();
			instrument.printPointing(this);
		}
		else if(hasOption("pointing")) {
			Configurator pointingOption = option("pointing");
			if(pointingOption.equals("suggest") || pointingOption.equals("auto")) {
				System.out.println(" WARNING! Cannot suggest pointing for scan " + getID() + ": S/N is below critical.");
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	public ArrayList<Scan<InstrumentType, IntegrationType>> split() {
		System.err.println(" Splitting subscans into separate scans.");
		ArrayList<Scan<InstrumentType, IntegrationType>> scans = new ArrayList<Scan<InstrumentType, IntegrationType>>();
		for(IntegrationType integration : this) {
			Scan<InstrumentType, IntegrationType> scan = (Scan<InstrumentType, IntegrationType>) clone();
			if(size() > 1) scan.hasSiblings = true;
			scan.clear();
			scan.instrument = integration.instrument;
			integration.scan = scan;
			scan.add(integration);
			scans.add(scan);
		}
		return scans;
	}
	
	public String getID() { return Integer.toString(serialNo); }
	
	public final static byte CASSEGRAIN = 0;
	public final static byte LEFT_NASMYTH = 1;
	public final static byte RIGHT_NASMYTH = 2;
	
}
