/**
    predict4java: An SDP4 / SGP4 library for satellite orbit predictions

    Copyright (C)  2004-2010  David A. B. Johnson, G4DPZ.

    This class is a Java port of one of the core elements of
    the Predict program, Copyright John A. Magliacane,
    KD2BD 1991-2003: http://www.qsl.net/kd2bd/predict.html

    Dr. T.S. Kelso is the author of the SGP4/SDP4 orbital models,
    originally written in Fortran and Pascal, and released into the
    public domain through his website (http://www.celestrak.com/). 
    Neoklis Kyriazis, 5B4AZ, later re-wrote Dr. Kelso's code in C,
    and released it under the GNU GPL in 2002.
    PREDICT's core is based on 5B4AZ's code translation efforts.

    Author: David A. B. Johnson, G4DPZ <dave@g4dpz.me.uk>

    Comments, questions and bugreports should be submitted via
    http://sourceforge.net/projects/websat/
    More details can be found at the project home page:

    http://websat.sourceforge.net

    This program 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 2 of the License, or
    (at your option) any later version.

    This program 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 this program; if not, visit http://www.fsf.org/
 */
package predict4java;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Class which provides Pass Prediction.
 * 
 * @author David A. B. Johnson, g4dpz
 * 
 */
public class PassPredictor {

	private static final String UTC = "UTC";
	private static final String SOUTH = "south";
	private static final String NORTH = "north";
	private static final double SPEED_OF_LIGHT = 2.99792458E8;
	private static final double TWOPI = Math.PI * 2.0;

	private static final String DEADSPOT_NONE = "none";

	/** The time at which we do all the calculations. */
	static final TimeZone TZ = TimeZone.getTimeZone(UTC);

	private static Log log = LogFactory.getLog(PassPredictor.class);

	private final TLE tle;
	private final GroundStationPosition qth;
	private final Satellite sat;

	private int iterationCount;

	/**
	 * Constructor.
	 * 
	 * @param tle
	 *            the Three Line Elements
	 * @param qth
	 *            the ground station position
	 * @throws IllegalArgumentException
	 *             bad argument passed in
	 * @throws SatNotFoundException
	 */
	public PassPredictor(final TLE theTLE, final GroundStationPosition theQTH)
			throws IllegalArgumentException, SatNotFoundException {

		if (null == theTLE) {
			throw new IllegalArgumentException("TLE has not been set");
		}

		if (null == theQTH) {
			throw new IllegalArgumentException("QTH has not been set");
		}

		this.tle = theTLE;
		this.qth = theQTH;

		sat = SatelliteFactory.createSatellite(tle);

		if (null == sat) {
			throw new SatNotFoundException("Satellite has not been created");
		} else if (!sat.willBeSeen(qth)) {
			throw new SatNotFoundException(
					"Satellite will never appear above the horizon");
		}

	}

	/**
	 * Gets the downlink frequency corrected for doppler.
	 * 
	 * @param freq
	 *            the original frequency in Hz
	 * @return the doppler corrected frequency in Hz
	 * @throws InvalidTleException
	 *             bad TLE passed in
	 * @throws SatNotFoundException
	 */
	public Long getDownlinkFreq(final Long freq, final Date date)
			throws SatNotFoundException {
		// get the current position
		final Calendar cal = Calendar.getInstance(TZ);
		cal.clear();
		cal.setTimeInMillis(date.getTime());
		final SatPos satPos = getSatPos(cal.getTime());
		final double rangeRate = satPos.getRangeRate();
		return (long) ((double) freq * (SPEED_OF_LIGHT - rangeRate * 1000.0) / SPEED_OF_LIGHT);
	}

	public SatPos getSatPos(final Date time) throws SatNotFoundException {
		this.iterationCount++;
		return sat.getPosition(qth, time);
	}

	public Long getUplinkFreq(final Long freq, final Date date)
			throws SatNotFoundException {
		final Calendar cal = Calendar.getInstance(TZ);
		cal.clear();
		cal.setTimeInMillis(date.getTime());
		final SatPos satPos = getSatPos(cal.getTime());
		final double rangeRate = satPos.getRangeRate();
		return (long) ((double) freq * (SPEED_OF_LIGHT + rangeRate * 1000.0) / SPEED_OF_LIGHT);
	}

	public SatPassTime nextSatPass(final Date date) throws SatNotFoundException {
		return nextSatPass(date, false);
	}

	public SatPassTime nextSatPass(final Date date, final boolean windBack)
			throws SatNotFoundException {

		int aosAzimuth = 0;
		int losAzimuth = 0;
		double maxElevation = 0;
		double elevation = 0;

		String polePassed = DEADSPOT_NONE;

		// get the current position
		final Calendar cal = Calendar.getInstance(TZ);
		cal.clear();
		cal.setTimeInMillis(date.getTime());

		// wind back time 1/4 of an orbit
		if (windBack) {
			double meanMotion = tle.getMeanmo();
			cal.add(Calendar.MINUTE, (int) (-24.0 * 60.0 / meanMotion / 4.0));
		}

		SatPos satPos = getSatPos(cal.getTime());
		SatPos prevPos = satPos;

		// test for the elevation being above the horizon
		if (satPos.getElevation() > 0.0) {

			// move time forward in 30 second intervals until the sat goes below
			// the horizon
			do {
				satPos = getPosition(cal, 60);
			} while (satPos.getElevation() > 0.0);

			// move time forward 3/4 orbit
			cal.add(Calendar.MINUTE, threeQuarterOrbitMinutes());
		}

		Date tca = null;

		// now find the next time it comes above the horizon
		do {
			satPos = getPosition(cal, 60);
			final Date now = cal.getTime();
			elevation = satPos.getElevation();
			if (elevation > maxElevation) {
				maxElevation = elevation;
				tca = now;
			}
		} while (satPos.getElevation() < 0.0);

		// refine it to 5 seconds
		cal.add(Calendar.SECOND, -60);
		do {
			satPos = getPosition(cal, 5);
			final Date now = cal.getTime();
			elevation = satPos.getElevation();
			if (elevation > maxElevation) {
				maxElevation = elevation;
				tca = now;
			}
			prevPos = satPos;
		} while (satPos.getElevation() < 0.0);

		final Date startDate = satPos.getTime();

		aosAzimuth = (int) ((satPos.getAzimuth() / (2.0 * Math.PI)) * 360.0);

		// now find when it goes below
		do {
			satPos = getPosition(cal, 30);
			final Date now = cal.getTime();
			final String currPolePassed = getPolePassed(prevPos, satPos);
			if (!currPolePassed.equals(DEADSPOT_NONE)) {
				polePassed = currPolePassed;
			}
			log.debug("Current pole passed: " + polePassed);
			elevation = satPos.getElevation();
			if (elevation > maxElevation) {
				maxElevation = elevation;
				tca = now;
			}
			prevPos = satPos;
		} while (satPos.getElevation() > 0.0);

		// refine it to 5 seconds
		cal.add(Calendar.SECOND, -30);
		do {
			satPos = getPosition(cal, 5);
			final Date now = cal.getTime();
			elevation = satPos.getElevation();
			if (elevation > maxElevation) {
				maxElevation = elevation;
				tca = now;
			}
		} while (satPos.getElevation() > 0.0);

		final Date endDate = satPos.getTime();
		losAzimuth = (int) ((satPos.getAzimuth() / (2.0 * Math.PI)) * 360.0);

		return new SatPassTime(startDate, endDate, tca, polePassed, aosAzimuth,
				losAzimuth, (maxElevation / (2.0 * Math.PI)) * 360.0);

	}

	/**
	 * @param cal
	 * @param offSet
	 * @return
	 * @throws InvalidTleException
	 * @throws SatNotFoundException
	 */
	private SatPos getPosition(final Calendar cal, final int offSet)
			throws SatNotFoundException {
		SatPos satPos;
		cal.add(Calendar.SECOND, offSet);
		satPos = getSatPos(cal.getTime());
		return satPos;
	}

	/**
	 * Gets a list of SatPassTime
	 * 
	 * @param start
	 *            Date
	 * 
	 *            newTLE = true; validateData();
	 * @param end
	 *            Date
	 * @param firstAosLimit
	 *            in hours
	 * @return List<SatPassTime>
	 * @throws SatNotFoundException
	 * @throws InvalidTleException
	 */
	public List<SatPassTime> getPasses(final Date start, final int hoursAhead,
			final boolean windBack) throws SatNotFoundException {

		this.iterationCount = 0;

		boolean windBackTime = windBack;

		final List<SatPassTime> passes = new ArrayList<SatPassTime>();

		Date trackStartDate = start;
		final Date trackEndDate = new Date(start.getTime()
				+ (hoursAhead * 60L * 60L * 1000L));

		Date lastAOS;

		int count = 0;

		do {
			if (count > 0) {
				windBackTime = false;
			}
			final SatPassTime pass = nextSatPass(trackStartDate, windBackTime);
			lastAOS = pass.getStartTime();
			passes.add(pass);
			trackStartDate = new Date(pass.getEndTime().getTime()
					+ (threeQuarterOrbitMinutes() * 60L * 1000L));
			count++;
		} while (lastAOS.compareTo(trackEndDate) < 0);

		return passes;
	}

	/**
	 * Returns the iterationCount. @VisibleForTesting
	 * 
	 * @return the iterationCount
	 */
	final int getIterationCount() {
		return iterationCount;
	}

	/**
	 * @return time in mS for 3/4 of an orbit
	 */
	private int threeQuarterOrbitMinutes() {
		return (int) (24.0 * 60.0 / tle.getMeanmo() * 0.75);
	}

	private String getPolePassed(final SatPos prevPos, final SatPos satPos) {
		String polePassed = DEADSPOT_NONE;

		final double az1 = prevPos.getAzimuth() / TWOPI * 360.0;
		final double az2 = satPos.getAzimuth() / TWOPI * 360.0;

		if (az1 > az2) {
			// we may be moving from 350 or greateer thru north
			if (az1 > 350 && az2 < 10) {
				polePassed = NORTH;
			} else {
				// we may be moving from 190 or greateer thru south
				if (az1 > 180 && az2 < 180) {
					polePassed = SOUTH;
				}
			}
		} else {
			// we may be moving from 10 or less through north
			if (az1 < 10 && az2 > 350) {
				polePassed = NORTH;
			} else {
				// we may be moving from 170 or more through south
				if (az1 < 180 && az2 > 180) {
					polePassed = SOUTH;
				}
			}
		}

		return polePassed;
	}

	/**
	 * Calculates positions of satellite for a given point in time, time range
	 * and step increment.
	 * 
	 * @param referenceDate
	 * @param incrementSeconds
	 * @param minutesBefore
	 * @param minutesAfter
	 * @return list of SatPos
	 * @throws SatNotFoundException
	 * @throws InvalidTleException
	 */
	public List<SatPos> getPositions(final Date referenceDate,
			final int incrementSeconds, final int minutesBefore,
			final int minutesAfter) throws SatNotFoundException {

		Date trackDate = new Date(referenceDate.getTime()
				- (minutesBefore * 60L * 1000L));
		final Date endDateDate = new Date(referenceDate.getTime()
				+ (minutesAfter * 60L * 1000L));

		final List<SatPos> positions = new ArrayList<SatPos>();

		while (trackDate.before(endDateDate)) {

			positions.add(getSatPos(trackDate));

			trackDate = new Date(trackDate.getTime()
					+ (incrementSeconds * 1000));
		}

		return positions;
	}
}
