package tim.prune.data;


/**
 * Represents a range of altitudes, taking units into account.
 * Values assumed to be >= 0.
 */
public class AltitudeRange
{
	/** Range of altitudes in metres */
	private final DoubleRange _range = new DoubleRange();
	/** Tolerance value in metres */
	private final int _wiggleLimit;
	/** Flag for whether previous value exists or not */
	private boolean _gotPreviousValue;
	/** Previous metric value */
	private double _previousValue;
	/** Total climb in metres */
	private int _climb;
	/** Total descent in metres */
	private int _descent;
	/** Flags for whether minimum or maximum has been found */
	private boolean _gotPreviousMinimum, _gotPreviousMaximum;
	/** Value of previous minimum or maximum, if any */
	private double _previousExtreme;


	/**
	 * Constructor
	 * @param toleranceMetres tolerance (wiggle limit) in metres
	 */
	public AltitudeRange(int toleranceMetres)
	{
		_wiggleLimit = toleranceMetres;
		_range.clear();
		_climb = _descent = 0;
		_gotPreviousValue = false;
		_previousValue = 0;
		_gotPreviousMinimum = _gotPreviousMaximum = false;
		_previousExtreme = 0;
	}


	/**
	 * Add a value to the range
	 * @param inAltitude value to add, only positive values considered
	 */
	public void addValue(Altitude inAltitude)
	{
		if (inAltitude != null && inAltitude.isValid())
		{
			int altValue = (int) inAltitude.getMetricValue();
			_range.addValue(altValue);
			// Compare with previous value if any
			if (_gotPreviousValue)
			{
				if (altValue != _previousValue)
				{
					// Got an altitude value which is different from the previous one
					final boolean locallyUp = (altValue > _previousValue);
					final boolean overallUp = _gotPreviousMinimum && _previousValue > _previousExtreme;
					final boolean overallDn = _gotPreviousMaximum && _previousValue < _previousExtreme;
					final boolean moreThanWiggle = Math.abs(altValue - _previousValue) > _wiggleLimit;
					// Do we know whether we're going up or down yet?
					if (!_gotPreviousMinimum && !_gotPreviousMaximum)
					{
						// we don't know whether we're going up or down yet - check limit
						if (moreThanWiggle)
						{
							if (locallyUp) {_gotPreviousMinimum = true;}
							else {_gotPreviousMaximum = true;}
							_previousExtreme = _previousValue;
							_previousValue = altValue;
						}
					}
					else if (overallUp)
					{
						if (locallyUp) {
							// we're still going up - do nothing
							_previousValue = altValue;
						}
						else if (moreThanWiggle)
						{
							// we're going up but have dropped over a maximum
							// Add the climb from _previousExtreme up to _previousValue
							_climb += (_previousValue - _previousExtreme);
							_previousExtreme = _previousValue;
							_gotPreviousMinimum = false; _gotPreviousMaximum = true;
							_previousValue = altValue;
						}
					}
					else if (overallDn)
					{
						if (locallyUp) {
							if (moreThanWiggle)
							{
								// we're going down but have climbed up from a minimum
								// Add the descent from _previousExtreme down to _previousValue
								_descent += (_previousExtreme - _previousValue);
								_previousExtreme = _previousValue;
								_gotPreviousMinimum = true; _gotPreviousMaximum = false;
								_previousValue = altValue;
							}
						}
						else {
							// we're still going down - do nothing
							_previousValue = altValue;
						}
					}
					// Note: Behaviour when WIGGLE_LIMIT == 0 should be same as before, all differences cumulated
				}
			}
			else
			{
				// we haven't got a previous value at all, so it's the start of a new segment
				_previousValue = altValue;
				_gotPreviousValue = true;
			}
		}
	}

	/**
	 * Reset the climb/descent calculations starting from the given value
	 * @param inAltitude altitude value
	 */
	public void ignoreValue(Altitude inAltitude)
	{
		// Process the previous value, if any, to update climb/descent as that's the end of the previous segment
		if (_gotPreviousValue && _gotPreviousMinimum && _previousValue > _previousExtreme) {
			_climb += (_previousValue - _previousExtreme);
		}
		else if (_gotPreviousValue && _gotPreviousMaximum && _previousValue < _previousExtreme) {
			_descent += (_previousExtreme - _previousValue);
		}
		// Eliminate the counting values to start the new segment
		_gotPreviousMinimum = _gotPreviousMaximum = false;
		_gotPreviousValue = false;
		// Now process this value if there is one
		if (inAltitude != null && inAltitude.isValid())
		{
			final int altValue = (int) inAltitude.getMetricValue();
			_range.addValue(altValue);
			_previousValue = altValue;
			_gotPreviousValue = true;
		}
	}

	/**
	 * @return true if altitude range found
	 */
	public boolean hasRange() {
		return _range.hasData();
	}

	/**
	 * @param inUnit altitude units to use
	 * @return minimum value
	 */
	public int getMinimum(Unit inUnit)
	{
		return (int) (_range.getMinimum() * inUnit.getMultFactorFromStd());
	}

	/**
	 * @param inUnit altitude units to use
	 * @return maximum value
	 */
	public int getMaximum(Unit inUnit)
	{
		return (int) (_range.getMaximum() * inUnit.getMultFactorFromStd());
	}

	/**
	 * @param inUnit altitude units to use
	 * @return total climb
	 */
	public int getClimb(Unit inUnit)
	{
		// May need to add climb from last segment
		double lastSegmentClimb = 0.0;
		if (_gotPreviousValue && _gotPreviousMinimum && _previousValue > _previousExtreme) {
			lastSegmentClimb = _previousValue - _previousExtreme;
		}
		return (int) ((_climb + lastSegmentClimb) * inUnit.getMultFactorFromStd());
	}

	/**
	 * @param inUnit altitude units to use
	 * @return total descent
	 */
	public int getDescent(Unit inUnit)
	{
		// May need to add descent from last segment
		double lastSegmentDescent = 0.0;
		if (_gotPreviousValue && _gotPreviousMaximum && _previousValue < _previousExtreme) {
			lastSegmentDescent = _previousExtreme - _previousValue;
		}
		return (int) ((_descent + lastSegmentDescent) * inUnit.getMultFactorFromStd());
	}

	/**
	 * @return overall height gain in metres
	 */
	public double getMetricHeightDiff() {
		return getClimb(UnitSetLibrary.UNITS_METRES) - getDescent(UnitSetLibrary.UNITS_METRES);
	}
}
