/* 
This file is part of OpenSatNav.

    OpenSatNav 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.

    OpenSatNav 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 OpenSatNav.  If not, see <http://www.gnu.org/licenses/>.
*/
// Created by plusminus on 21:37:08 - 27.09.2008
package org.andnav.osm.views.controller;

import org.andnav.osm.util.BoundingBoxE6;
import org.andnav.osm.util.GeoPoint;
import org.andnav.osm.views.OpenStreetMapView;
import org.andnav.osm.views.util.MyMath;
import org.andnav.osm.views.util.constants.MathConstants;

/**
 * 
 * @author Nicolas Gramlich
 *
 */
public class OpenStreetMapViewController {
	// ===========================================================
	// Constants
	// ===========================================================
	
	public static final int ANIMATION_SMOOTHNESS_LOW = 4;
	public static final int ANIMATION_SMOOTHNESS_DEFAULT = 10;
	public static final int ANIMATION_SMOOTHNESS_HIGH = 20;		

	public static final int ANIMATION_DURATION_SHORT = 500;
	public static final int ANIMATION_DURATION_DEFAULT = 1000;
	public static final int ANIMATION_DURATION_LONG = 2000;

	// ===========================================================
	// Fields
	// ===========================================================
	
	final OpenStreetMapView mOsmv;
	private AbstractAnimationRunner mCurrentAnimationRunner;

	// ===========================================================
	// Constructors
	// ===========================================================
	
	public OpenStreetMapViewController(final OpenStreetMapView osmv) {
		this.mOsmv = osmv;
	}

	// ===========================================================
	// Getter & Setter
	// ===========================================================

	// ===========================================================
	// Methods from SuperClass/Interfaces
	// ===========================================================

	// ===========================================================
	// Methods
	// ===========================================================
	
	public void zoomToSpan(BoundingBoxE6 bb) {
		zoomToSpan(bb.getLatitudeSpanE6(), bb.getLongitudeSpanE6());
	}
	
	public void zoomToSpan(final int reqLatSpan, final int reqLonSpan) {
		if(reqLatSpan <= 0 || reqLonSpan <= 0 )
			return;
		
		final BoundingBoxE6 bb = this.mOsmv.getVisibleBoundingBoxE6();
		final int curZoomLevel = this.mOsmv.getZoomLevel();
		
		final int curLatSpan = bb.getLatitudeSpanE6();
		final int curLonSpan = bb.getLongitudeSpanE6();
		
		final float diffNeededLat = (float)reqLatSpan / curLatSpan; // i.e. 600/500 = 1,2
		final float diffNeededLon = (float)reqLonSpan / curLonSpan; // i.e. 300/400 = 0,75
		
		final float diffNeeded = Math.max(diffNeededLat, diffNeededLon); // i.e. 1,2
		
		if(diffNeeded > 1){ // Zoom Out
			this.mOsmv.setZoomLevel(curZoomLevel - MyMath.getNextSquareNumberAbove(diffNeeded));
		}else if(diffNeeded < 0.5){ // Can Zoom in
			this.mOsmv.setZoomLevel(curZoomLevel + MyMath.getNextSquareNumberAbove(1 / diffNeeded) - 1);
		}
	}
	
	/**
	 * Animates the underlying {@link OpenStreetMapView} that it centers the passed {@link GeoPoint} in the end.
	 * Uses: {@link OpenStreetMapViewController.ANIMATION_SMOOTHNESS_DEFAULT} and {@link OpenStreetMapViewController.ANIMATION_DURATION_DEFAULT}.
	 * @param gp
	 */
	public void animateTo(final GeoPoint gp, final AnimationType aAnimationType){ 
		animateTo(gp.getLatitudeE6(), gp.getLongitudeE6(), aAnimationType, ANIMATION_DURATION_DEFAULT, ANIMATION_SMOOTHNESS_DEFAULT);
	}
	
	/**
	 * Animates the underlying {@link OpenStreetMapView} that it centers the passed {@link GeoPoint} in the end.
	 * @param gp GeoPoint to be centered in the end.
	 * @param aSmoothness steps made during animation. I.e.: {@link OpenStreetMapViewController.ANIMATION_SMOOTHNESS_LOW}, {@link OpenStreetMapViewController.ANIMATION_SMOOTHNESS_DEFAULT}, {@link OpenStreetMapViewController.ANIMATION_SMOOTHNESS_HIGH}
	 * @param aDuration in Milliseconds. I.e.: {@link OpenStreetMapViewController.ANIMATION_DURATION_SHORT}, {@link OpenStreetMapViewController.ANIMATION_DURATION_DEFAULT}, {@link OpenStreetMapViewController.ANIMATION_DURATION_LONG}
	 */
	public void animateTo(final GeoPoint gp, final AnimationType aAnimationType, final int aSmoothness, final int aDuration){
		animateTo(gp.getLatitudeE6(), gp.getLongitudeE6(), aAnimationType, aSmoothness, aDuration);
	}
	
	/**
	 * Animates the underlying {@link OpenStreetMapView} that it centers the passed coordinates in the end.
	 * Uses: {@link OpenStreetMapViewController.ANIMATION_SMOOTHNESS_DEFAULT} and {@link OpenStreetMapViewController.ANIMATION_DURATION_DEFAULT}.
	 * @param aLatitudeE6
	 * @param aLongitudeE6
	 */
	public void animateTo(final int aLatitudeE6, final int aLongitudeE6, final AnimationType aAnimationType){
		animateTo(aLatitudeE6, aLongitudeE6, aAnimationType, ANIMATION_SMOOTHNESS_DEFAULT, ANIMATION_DURATION_DEFAULT);
	}
	
	/**
	 * Animates the underlying {@link OpenStreetMapView} that it centers the passed coordinates in the end.
	 * @param aLatitudeE6
	 * @param aLongitudeE6
	 * @param aSmoothness steps made during animation. I.e.: {@link OpenStreetMapViewController.ANIMATION_SMOOTHNESS_LOW}, {@link OpenStreetMapViewController.ANIMATION_SMOOTHNESS_DEFAULT}, {@link OpenStreetMapViewController.ANIMATION_SMOOTHNESS_HIGH}
	 * @param aDuration in Milliseconds. I.e.: {@link OpenStreetMapViewController.ANIMATION_DURATION_SHORT}, {@link OpenStreetMapViewController.ANIMATION_DURATION_DEFAULT}, {@link OpenStreetMapViewController.ANIMATION_DURATION_LONG}
	 */
	public void animateTo(final int aLatitudeE6, final int aLongitudeE6, final AnimationType aAnimationType, final int aSmoothness, final int aDuration){
		this.stopAnimation(false); 
		
		switch(aAnimationType){
			case LINEAR:
				this.mCurrentAnimationRunner = new LinearAnimationRunner(aLatitudeE6, aLongitudeE6, aSmoothness, aDuration);
				break;
			case EXPONENTIALDECELERATING:
				this.mCurrentAnimationRunner = new ExponentialDeceleratingAnimationRunner(aLatitudeE6, aLongitudeE6, aSmoothness, aDuration);
				break;
			case QUARTERCOSINUSALDECELERATING:
				this.mCurrentAnimationRunner = new QuarterCosinusalDeceleratingAnimationRunner(aLatitudeE6, aLongitudeE6, aSmoothness, aDuration);
				break;
			case HALFCOSINUSALDECELERATING:
				this.mCurrentAnimationRunner = new HalfCosinusalDeceleratingAnimationRunner(aLatitudeE6, aLongitudeE6, aSmoothness, aDuration);
				break;
			case MIDDLEPEAKSPEED:
				this.mCurrentAnimationRunner = new MiddlePeakSpeedAnimationRunner(aLatitudeE6, aLongitudeE6, aSmoothness, aDuration);
				break;
		}
		
		this.mCurrentAnimationRunner.start();
	}
	
	/**
	 * Stops a running animation.
	 * @param jumpToTarget
	 */
	public void stopAnimation(final boolean jumpToTarget) {
		final AbstractAnimationRunner currentAnimationRunner = this.mCurrentAnimationRunner;
		
		if(currentAnimationRunner != null && !currentAnimationRunner.isDone()){
			currentAnimationRunner.interrupt();
			if(jumpToTarget)
				this.mOsmv.setMapCenter(currentAnimationRunner.mTargetLatitudeE6, currentAnimationRunner.mTargetLongitudeE6);
		}
	}

	// ===========================================================
	// Inner and Anonymous Classes
	// ===========================================================
	
	/**
	 * Choose on of the Styles of approacing the target Coordinates.
	 * <ul>
	 * <li><code>LINEAR</code>
	 * 	<ul>
	 * 		<li>Uses ses linear interpolation </li>
	 * 		<li>Values produced: 10%, 20%, 30%, 40%, 50%, ...</li>
	 * 		<li>Style: Always average speed.</li>
	 * 	</ul>
	 * </li>
	 * <li><code>EXPONENTIALDECELERATING</code>
	 * 	<ul>
	 * 		<li>Uses a exponential interpolation/li>
	 * 		<li>Values produced: 50%, 75%, 87.5%, 93.5%, ...</li>
	 * 		<li>Style: Starts very fast, really slow in the end.</li>
	 * 	</ul>
	 * </li>
	 * <li><code>QUARTERCOSINUSALDECELERATING</code>
	 * 	<ul>
	 * 		<li>Uses the first quarter of the cos curve (from zero to PI/2) for interpolation. </li>
	 * 		<li>Values produced: See cos curve :)</li>
	 * 		<li>Style: Average speed, slows out medium.</li>
	 * 	</ul>
	 * </li>
	 * <li><code>HALFCOSINUSALDECELERATING</code>
	 * 	<ul>
	 * 		<li>Uses the first half of the cos curve (from zero to PI) for interpolation</li>
	 * 		<li>Values produced: See cos curve :)</li>
	 * 		<li>Style: Average speed, slows out smoothly.</li>
	 * 	</ul>
	 * </li>
	 * <li><code>MIDDLEPEAKSPEED</code>
	 * 	<ul>
	 * 		<li>Uses the values of cos around the 0 (from -PI/2 to +PI/2) for interpolation</li>
	 * 		<li>Values produced: See cos curve :)</li>
	 * 		<li>Style: Starts medium, speeds high in middle, slows out medium.</li>
	 * 	</ul>
	 * </li>
	 * </ul>
	 */
	public static enum AnimationType {
		/**
		 * <ul>
		 * <li><code>LINEAR</code>
		 * 	<ul>
		 * 		<li>Uses ses linear interpolation </li>
		 * 		<li>Values produced: 10%, 20%, 30%, 40%, 50%, ...</li>
		 * 		<li>Style: Always average speed.</li>
		 * 	</ul>
		 * </li>
		 * </ul>
		 */
		LINEAR, 
		/**
		 * <ul>
		 * <li><code>EXPONENTIALDECELERATING</code>
		 * 	<ul>
		 * 		<li>Uses a exponential interpolation/li>
		 * 		<li>Values produced: 50%, 75%, 87.5%, 93.5%, ...</li>
		 * 		<li>Style: Starts very fast, really slow in the end.</li>
		 * 	</ul>
		 * </li>
		 * </ul>
		 */
		EXPONENTIALDECELERATING, 
		/**
		 * <ul>
		 * <li><code>QUARTERCOSINUSALDECELERATING</code>
		 * 	<ul>
		 * 		<li>Uses the first quarter of the cos curve (from zero to PI/2) for interpolation. </li>
		 * 		<li>Values produced: See cos curve :)</li>
		 * 		<li>Style: Average speed, slows out medium.</li>
		 * 	</ul>
		 * </li>
		 * </ul> 
		 */
		QUARTERCOSINUSALDECELERATING, 
		 /** <ul>
		 * <li><code>HALFCOSINUSALDECELERATING</code>
		 * 	<ul>
		 * 		<li>Uses the first half of the cos curve (from zero to PI) for interpolation</li>
		 * 		<li>Values produced: See cos curve :)</li>
		 * 		<li>Style: Average speed, slows out smoothly.</li>
		 * 	</ul>
		 * </li>
		 * </ul>
		 */
		HALFCOSINUSALDECELERATING, 
		/** <ul>
		 * <li><code>MIDDLEPEAKSPEED</code>
		 * 	<ul>
		 * 		<li>Uses the values of cos around the 0 (from -PI/2 to +PI/2) for interpolation</li>
		 * 		<li>Values produced: See cos curve :)</li>
		 * 		<li>Style: Starts medium, speeds high in middle, slows out medium.</li>
		 * 	</ul>
		 * </li>
		 * </ul>
		 */
		MIDDLEPEAKSPEED;
	}
	
	private abstract class AbstractAnimationRunner extends Thread{
		
		// ===========================================================
		// Fields
		// ===========================================================

		protected final int mSmoothness, mDuration;
		protected final int mTargetLatitudeE6, mTargetLongitudeE6;
		protected boolean mDone = false;
		
		protected final int mStepDuration;

		protected final int mPanTotalLatitudeE6, mPanTotalLongitudeE6;
		
		// ===========================================================
		// Constructors
		// ===========================================================
		
		public AbstractAnimationRunner(OpenStreetMapViewController openStreetMapViewController, final int aTargetLatitudeE6, final int aTargetLongitudeE6){
			this(aTargetLatitudeE6, aTargetLongitudeE6, OpenStreetMapViewController.ANIMATION_SMOOTHNESS_DEFAULT, OpenStreetMapViewController.ANIMATION_DURATION_DEFAULT);
		}
		
		public AbstractAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6, final int aSmoothness, final int aDuration){
			this.mTargetLatitudeE6 = aTargetLatitudeE6;
			this.mTargetLongitudeE6 = aTargetLongitudeE6;
			this.mSmoothness = aSmoothness;
			this.mDuration = aDuration;
			
			this.mStepDuration = aDuration / aSmoothness;
			
			
			/* Get the current mapview-center. */ 
			final OpenStreetMapView mapview = OpenStreetMapViewController.this.mOsmv;
			int mapCenterLatE6 = mapview.getMapCenterLatitudeE6();
			int mapCenterLonE6 = mapview.getMapCenterLongitudeE6();
			
			this.mPanTotalLatitudeE6 = (mapCenterLatE6 - aTargetLatitudeE6);
			this.mPanTotalLongitudeE6 = (mapCenterLonE6 - aTargetLongitudeE6);
		}
		
		public void run(){
			onRunAnimation();
			this.mDone = true;
		}
		
		public boolean isDone(){
			return this.mDone;
		}
		
		public abstract void onRunAnimation();
	}
	
	private class LinearAnimationRunner extends AbstractAnimationRunner{
		
		// ===========================================================
		// Fields
		// ===========================================================

		protected final int mPanPerStepLatitudeE6, mPanPerStepLongitudeE6;
		
		// ===========================================================
		// Constructors
		// ===========================================================
		
		public LinearAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6){
			this(aTargetLatitudeE6, aTargetLongitudeE6, ANIMATION_SMOOTHNESS_DEFAULT, ANIMATION_DURATION_DEFAULT);
		}
		
		public LinearAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6, final int aSmoothness, final int aDuration){
			super(aTargetLatitudeE6, aTargetLongitudeE6, aSmoothness, aDuration);
			
			/* Get the current mapview-center. */ 
			final OpenStreetMapView mapview = OpenStreetMapViewController.this.mOsmv;
			int mapCenterLatE6 = mapview.getMapCenterLatitudeE6();
			int mapCenterLonE6 = mapview.getMapCenterLongitudeE6();
			
			this.mPanPerStepLatitudeE6 = (mapCenterLatE6 - aTargetLatitudeE6) / aSmoothness;
			this.mPanPerStepLongitudeE6 = (mapCenterLonE6 - aTargetLongitudeE6) / aSmoothness;
			
			this.setName("LinearAnimationRunner");
		}
		
		// ===========================================================
		// Methods from SuperClass/Interfaces
		// ===========================================================

		@Override
		public void onRunAnimation(){		
			final OpenStreetMapView mapview = OpenStreetMapViewController.this.mOsmv;
			final int panPerStepLatitudeE6 = this.mPanPerStepLatitudeE6;
			final int panPerStepLongitudeE6 = this.mPanPerStepLongitudeE6;
			final int stepDuration = this.mStepDuration;
			try {
				int newMapCenterLatE6;
				int newMapCenterLonE6;
				
				for(int i = this.mSmoothness; i > 0 ; i--){
					
					newMapCenterLatE6 = mapview.getMapCenterLatitudeE6() - panPerStepLatitudeE6;
					newMapCenterLonE6 = mapview.getMapCenterLongitudeE6() - panPerStepLongitudeE6;
					mapview.setMapCenter(newMapCenterLatE6, newMapCenterLonE6);
					
					Thread.sleep(stepDuration);
				}
			} catch (Exception e) {
				this.interrupt();
			}
		}
	}
	
	private class ExponentialDeceleratingAnimationRunner extends AbstractAnimationRunner{
		
		// ===========================================================
		// Fields
		// ===========================================================
		
		// ===========================================================
		// Constructors
		// ===========================================================
		
		public ExponentialDeceleratingAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6){
			this(aTargetLatitudeE6, aTargetLongitudeE6, ANIMATION_SMOOTHNESS_DEFAULT, ANIMATION_DURATION_DEFAULT);
		}
		
		public ExponentialDeceleratingAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6, final int aSmoothness, final int aDuration){
			super(aTargetLatitudeE6, aTargetLongitudeE6, aSmoothness, aDuration);
			
			this.setName("ExponentialDeceleratingAnimationRunner");
		}
		
		// ===========================================================
		// Methods from SuperClass/Interfaces
		// ===========================================================

		@Override
		public void onRunAnimation(){		
			final OpenStreetMapView mapview = OpenStreetMapViewController.this.mOsmv;
			final int stepDuration = this.mStepDuration;
			try {
				int newMapCenterLatE6;
				int newMapCenterLonE6;
				
				for(int i = 0; i < this.mSmoothness; i++){
					
					final double delta = Math.pow(0.5, i + 1);
					final int deltaLatitudeE6 = (int)(this.mPanTotalLatitudeE6 * delta);
					final int detlaLongitudeE6 = (int)(this.mPanTotalLongitudeE6 * delta);
					
					newMapCenterLatE6 = mapview.getMapCenterLatitudeE6() - deltaLatitudeE6;
					newMapCenterLonE6 = mapview.getMapCenterLongitudeE6() - detlaLongitudeE6;
					mapview.setMapCenter(newMapCenterLatE6, newMapCenterLonE6);
					
					Thread.sleep(stepDuration);
				}
				mapview.setMapCenter(super.mTargetLatitudeE6, super.mTargetLongitudeE6);
			} catch (Exception e) {
				this.interrupt();
			}
		}
	}
	
	private class CosinusalBasedAnimationRunner extends AbstractAnimationRunner implements MathConstants{
		// ===========================================================
		// Fields
		// ===========================================================
		
		protected final float mStepIncrement, mAmountStretch;
		protected final float mYOffset, mRange, mStart;
		
		// ===========================================================
		// Constructors
		// ===========================================================
		
		public CosinusalBasedAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6, final float aStart, final float aRange, final float aYOffset){
			this(aTargetLatitudeE6, aTargetLongitudeE6, ANIMATION_SMOOTHNESS_DEFAULT, ANIMATION_DURATION_DEFAULT, aStart, aRange, aYOffset);
		}
		
		public CosinusalBasedAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6, final int aSmoothness, final int aDuration, final float aStart, final float aRange, final float aYOffset){
			super(aTargetLatitudeE6, aTargetLongitudeE6, aSmoothness, aDuration);
			this.mYOffset = aYOffset;
			this.mRange = aRange;
			this.mStart = aStart;
			
			this.mStepIncrement =  aRange / aSmoothness;
			
			/* We need to normalize the amount in the end, so wee need the the: sum^(-1) . */
			float amountSum = 0;
			for(int i = 0; i < aSmoothness; i++)
				amountSum += aYOffset + Math.cos(this.mStepIncrement * i + aStart);
			
			this.mAmountStretch = 1 / amountSum;
			
			this.setName("QuarterCosinusalDeceleratingAnimationRunner");
		}
		
		// ===========================================================
		// Methods from SuperClass/Interfaces
		// ===========================================================

		@Override
		public void onRunAnimation(){		
			final OpenStreetMapView mapview = OpenStreetMapViewController.this.mOsmv;
			final int stepDuration = this.mStepDuration;
			final float amountStretch = this.mAmountStretch;
			try {
				int newMapCenterLatE6;
				int newMapCenterLonE6;
				
				for(int i = 0; i < this.mSmoothness; i++){
					
					final double delta = (this.mYOffset + Math.cos(this.mStepIncrement * i + this.mStart)) * amountStretch;
					final int deltaLatitudeE6 = (int)(this.mPanTotalLatitudeE6 * delta);
					final int deltaLongitudeE6 = (int)(this.mPanTotalLongitudeE6 * delta);
					
					newMapCenterLatE6 = mapview.getMapCenterLatitudeE6() - deltaLatitudeE6;
					newMapCenterLonE6 = mapview.getMapCenterLongitudeE6() - deltaLongitudeE6;
					mapview.setMapCenter(newMapCenterLatE6, newMapCenterLonE6);
					
					Thread.sleep(stepDuration);
				}
				mapview.setMapCenter(super.mTargetLatitudeE6, super.mTargetLongitudeE6);
			} catch (Exception e) {
				this.interrupt();
			}
		}
	}
	
	protected class QuarterCosinusalDeceleratingAnimationRunner extends CosinusalBasedAnimationRunner implements MathConstants{
		// ===========================================================
		// Constructors
		// ===========================================================
		
		protected QuarterCosinusalDeceleratingAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6){
			this(aTargetLatitudeE6, aTargetLongitudeE6, ANIMATION_SMOOTHNESS_DEFAULT, ANIMATION_DURATION_DEFAULT);
		}
		
		protected QuarterCosinusalDeceleratingAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6, final int aSmoothness, final int aDuration){
			super(aTargetLatitudeE6, aTargetLongitudeE6, aSmoothness, aDuration, 0, PI_2, 0);
		}
	}
	
	protected class HalfCosinusalDeceleratingAnimationRunner extends CosinusalBasedAnimationRunner implements MathConstants{
		// ===========================================================
		// Constructors
		// ===========================================================
		
		protected HalfCosinusalDeceleratingAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6){
			this(aTargetLatitudeE6, aTargetLongitudeE6, ANIMATION_SMOOTHNESS_DEFAULT, ANIMATION_DURATION_DEFAULT);
		}
		
		protected HalfCosinusalDeceleratingAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6, final int aSmoothness, final int aDuration){
			super(aTargetLatitudeE6, aTargetLongitudeE6, aSmoothness, aDuration, 0, PI, 1);
		}
	}
	
	protected class MiddlePeakSpeedAnimationRunner extends CosinusalBasedAnimationRunner implements MathConstants{
		// ===========================================================
		// Constructors
		// ===========================================================
		
		protected MiddlePeakSpeedAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6){
			this(aTargetLatitudeE6, aTargetLongitudeE6, ANIMATION_SMOOTHNESS_DEFAULT, ANIMATION_DURATION_DEFAULT);
		}
		
		protected MiddlePeakSpeedAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6, final int aSmoothness, final int aDuration){
			super(aTargetLatitudeE6, aTargetLongitudeE6, aSmoothness, aDuration, -PI_2, PI, 0);
		}
	}
}
