package tim.prune.data;

/**
 * Class to do basic calculations of range statistics such as distances, durations,
 * and altitude ranges, and to hold the results of the calculations.
 */
public class RangeStats
{
	private int _numPoints   = 0;
	private int _numSegments = 0;
	private boolean _foundTrackPoint = false;
	protected final AltitudeRange _totalAltitudeRange;
	protected final AltitudeRange _movingAltitudeRange;
	private Timestamp _earliestTimestamp = null, _latestTimestamp = null, _movingTimestamp = null;
	private long _movingMilliseconds = 0L;
	private boolean _timesIncomplete = false;
	private boolean _timesOutOfSequence = false;
	protected double _totalDistanceRads = 0.0, _movingDistanceRads = 0.0;
	protected DataPoint _prevPoint = null;


	/** Constructor */
	public RangeStats(int inAltitudeTolerance)
	{
		_totalAltitudeRange = new AltitudeRange(inAltitudeTolerance);
		_movingAltitudeRange = new AltitudeRange(inAltitudeTolerance);
	}

	/**
	 * Constructor giving Track
	 * @param inTrack track object to calculate with
	 */
	public RangeStats(Track inTrack, int inStartIndex, int inEndIndex, int inAltitudeTolerance)
	{
		this(inAltitudeTolerance);
		populateFromTrack(inTrack, inStartIndex, inEndIndex);
	}

	/**
	 * Add the specified points from the given track to the calculations
	 * @param inTrack track object
	 * @param inStartIndex start index (inclusive)
	 * @param inEndIndex end index (inclusive)
	 */
	protected void populateFromTrack(Track inTrack, int inStartIndex, int inEndIndex)
	{
		for (int i=inStartIndex; i<=inEndIndex; i++) {
			addPoint(inTrack.getPoint(i));
		}
	}

	/**
	 * @param inPoint point to add to the calculations
	 */
	public void addPoint(DataPoint inPoint)
	{
		if (inPoint == null) {
			return;
		}
		_numPoints++;
		// ignore all waypoints
		if (inPoint.isWaypoint()) {
			return;
		}
		if (inPoint.getSegmentStart() || !_foundTrackPoint) {
			_numSegments++;
		}
		_foundTrackPoint = true;
		// Get the distance to the previous track point
		if (_prevPoint != null)
		{
			double rads = DataPoint.calculateRadiansBetween(_prevPoint, inPoint);
			_totalDistanceRads += rads;
			if (!inPoint.getSegmentStart()) {
				_movingDistanceRads += rads;
			}
		}

		// timestamps
		if (inPoint.getSegmentStart())
		{
			// reset movingTimestamp for moving time at the start
			// of each segment
			_movingTimestamp = null;
		}
		if (inPoint.hasTimestamp())
		{
			Timestamp currTstamp = inPoint.getTimestamp();
			if (_earliestTimestamp == null || currTstamp.isBefore(_earliestTimestamp)) {
				_earliestTimestamp = currTstamp;
			}
			if (_latestTimestamp == null || currTstamp.isAfter(_latestTimestamp)) {
				_latestTimestamp = currTstamp;
			}

			// Work out duration without segment gaps
			if (_movingTimestamp != null)
			{
				long millisLater = currTstamp.getMillisecondsSince(_movingTimestamp);
				if (millisLater < 0) {
					_timesOutOfSequence = true;
				}
				else {
					_movingMilliseconds += millisLater;
				}
			}
			_movingTimestamp = currTstamp;
		}
		else {
			_timesIncomplete = true;
		}

		// altitudes
		if (inPoint.hasAltitude())
		{
			Altitude altitude = inPoint.getAltitude();
			_totalAltitudeRange.addValue(altitude);
			if (inPoint.getSegmentStart()) {
				_movingAltitudeRange.ignoreValue(altitude);
			}
			else
			{
				_movingAltitudeRange.addValue(altitude);
			}
		}

		// allow child classes to do additional calculations
		doFurtherCalculations(inPoint);

		_prevPoint = inPoint;
	}

	/**
	 * Hook for subclasses to do what they want in addition
	 * @param inPoint incoming point
	 */
	protected void doFurtherCalculations(DataPoint inPoint)
	{
	}


	/** @return number of points in range */
	public int getNumPoints() {
		return _numPoints;
	}

	/** @return number of segments in range */
	public int getNumSegments() {
		return _numSegments;
	}

	/** @return altitude range of range including segment gaps */
	public AltitudeRange getTotalAltitudeRange() {
		return _totalAltitudeRange;
	}

	/** @return altitude range of range just within segments */
	public AltitudeRange getMovingAltitudeRange() {
		return _movingAltitudeRange;
	}

	/** @return the earliest timestamp found */
	public Timestamp getEarliestTimestamp() {
		return _earliestTimestamp;
	}

	/** @return the latest timestamp found */
	public Timestamp getLatestTimestamp() {
		return _latestTimestamp;
	}

	/** @return total number of seconds in the range */
	public long getTotalDurationInSeconds()
	{
		if (_earliestTimestamp != null && _latestTimestamp != null) {
			return _latestTimestamp.getSecondsSince(_earliestTimestamp);
		}
		return 0L;
	}

	/** @return number of seconds within the segments of the range */
	public long getMovingDurationInSeconds() {
		return _movingMilliseconds / 1000;
	}

	/** @return true if any timestamps are missing */
	public boolean getTimestampsIncomplete() {
		return _timesIncomplete;
	}

	/** @return true if any timestamps are out of sequence */
	public boolean getTimestampsOutOfSequence() {
		return _timesOutOfSequence;
	}

	/** @return total distance in the given distance units (km or mi) */
	public double getTotalDistance(Unit inUnit) {
		return Distance.convertRadiansToDistance(_totalDistanceRads, inUnit);
	}

	/** @return moving distance in the given distance units (km or mi) */
	public double getMovingDistance(Unit inUnit) {
		return Distance.convertRadiansToDistance(_movingDistanceRads, inUnit);
	}

	/** @return moving distance in km */
	public double getMovingDistanceKilometres() {
		return Distance.convertRadiansToDistance(_movingDistanceRads, UnitSetLibrary.UNITS_KILOMETRES);
	}

	/**
	 * @return the total vertical speed (including segment gaps) in metric units
	 */
	public double getTotalVerticalSpeed()
	{
		long time = getTotalDurationInSeconds();
		if (time > 0 && _totalAltitudeRange.hasRange()) {
			return _totalAltitudeRange.getMetricHeightDiff() / time;
		}
		return 0.0;
	}

	/**
	 * @return the moving vertical speed (ignoring segment gaps) in metric units
	 */
	public double getMovingVerticalSpeed()
	{
		long time = getMovingDurationInSeconds();
		if (time > 0 && _movingAltitudeRange.hasRange()) {
			return _movingAltitudeRange.getMetricHeightDiff() / time;
		}
		return 0.0;
	}
}
