/*
3DHOP - 3D Heritage Online Presenter
Copyright (c) 2014-2023, Visual Computing Lab, ISTI - CNR
All rights reserved.

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 3 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, see <http://www.gnu.org/licenses/>.
*/

/**
 * Constructs a PanTiltTrackball object.
 * @class Interactor which implements a pan-tilt trackball controller with bounds.
 */
function PanTiltTrackball() {
}

PanTiltTrackball.prototype = {

	setup : function (options,myPresenter) {
		options = options || {};
		var opt = sglGetDefaultObject({
			startCenter   : [ 0.0, 0.0, 0.0 ],
			startPanX     : 0.0,
			startPanY     : 0.0,
			startAngleX   : 0.0,
			startAngleY   : 0.0,
			startDistance : 2.0,
			minMaxDist    : [0.2, 4.0],
			minMaxPanX    : [-0.7, 0.7],
			minMaxPanY    : [-0.7, 0.7],
			minMaxAngleX  : [-70.0, 70.0],
			minMaxAngleY  : [-70.0, 70.0],
			pathStates      : [ ],	// path points array
			animationLocked : false,// if true disable trackball interactions during animation
			animationTime   : null	// when single position navigation, each to point navigation is # seconds (if null, automatically computed)
		}, options);

		this._action = SGL_TRACKBALL_NO_ACTION;
		this._new_action = true;
		this._matrix = SglMat4.identity();

		this.myPresenter = myPresenter;// parent presenter

		// path
		this._pathStates = opt.pathStates;
		this._animationLocked = opt.animationLocked;
		this._pathPosNum = 0;

		// trackball center
		this._center = opt.startCenter;

		// starting/default parameters
		this._startPanX = opt.startPanX;					//pan X
		this._startPanY = opt.startPanY;					//pan Y
		this._startAngleX = sglDegToRad(opt.startAngleX);	//angle X
		this._startAngleY = sglDegToRad(opt.startAngleY);	//angle Y
		this._startDistance = opt.startDistance;			//distance

		// current parameters
		this._panX = this._startPanX;
		this._panY = this._startPanY;
		this._angleX = this._startAngleX;
		this._angleY = this._startAngleY;
		this._distance = this._startDistance;

		// target paramenters
		this._targetPanX = this._startPanX;
		this._targetPanY = this._startPanY;
		this._targetAngleX = this._startAngleX;
		this._targetAngleY = this._startAngleY;
		this._targetDistance = this._startDistance;

		//animation data
		this._isAnimating = false;
		this._speedPanX = 0.0;
		this._speedPanY = 0.0;
		this._speedAngleX = 0.0;
		this._speedAngleY = 0.0;
		this._speedDistance = 0.0;
		this._isAutoWalking = false;
		this._animationTime = opt.animationTime;

		//limits
		this._minMaxDist  = opt.minMaxDist;
		this._minMaxPanX   = opt.minMaxPanX;
		this._minMaxPanY   = opt.minMaxPanY;
		this._minMaxAngleX = opt.minMaxAngleX;
		this._minMaxAngleX[0] = sglDegToRad(this._minMaxAngleX[0]);
		this._minMaxAngleX[1] = sglDegToRad(this._minMaxAngleX[1]);
		this._minMaxAngleY = opt.minMaxAngleY;
		this._minMaxAngleY[0] = sglDegToRad(this._minMaxAngleY[0]);
		this._minMaxAngleY[1] = sglDegToRad(this._minMaxAngleY[1]);

		this._start = [0.0, 0.0];
		this.reset();
	},

	_clamp : function(value, low, high) {
		if(value < low) return low;
		if(value > high) return high;
		return value;
	},

	_computeMatrix: function() {
		var m = SglMat4.identity();

		// centering
		m = SglMat4.mul(m, SglMat4.translation([-this._center[0], -this._center[1], -this._center[2]]));
		// zoom
		m = SglMat4.mul(m, SglMat4.translation([0.0, 0.0, -this._distance]));
		// tilt
		m = SglMat4.mul(m, SglMat4.rotationAngleAxis(this._angleY, [1.0, 0.0, 0.0]));
		m = SglMat4.mul(m, SglMat4.rotationAngleAxis(this._angleX, [0.0, -1.0, 0.0]));
		// pan
		m = SglMat4.mul(m, SglMat4.translation([this._panX, this._panY, 0.0]));

		this._matrix = m;

		if(typeof onTrackballUpdate != "undefined")
			onTrackballUpdate(this.getState());
	},

	getState : function () {
		return [this._panX, this._panY, sglRadToDeg(this._angleX), sglRadToDeg(this._angleY), this._distance];
	},

	setState : function (newstate) {
		// stop animation
		this._isAnimating = this._isAutoWalking = false;

		this._panX     = newstate[0];
		this._panY     = newstate[1];
		this._angleX   = sglDegToRad(newstate[2]);
		this._angleY   = sglDegToRad(newstate[3]);
		this._distance = newstate[4];

		//check limits
		this._panX = this._clamp(this._panX, this._minMaxPanX[0], this._minMaxPanX[1]);
		this._panY = this._clamp(this._panY, this._minMaxPanY[0], this._minMaxPanY[1]);
		this._angleX = this._clamp(this._angleX, this._minMaxAngleX[0], this._minMaxAngleX[1]);
		this._angleY = this._clamp(this._angleY, this._minMaxAngleY[0], this._minMaxAngleY[1]);
		this._distance = this._clamp(this._distance, this._minMaxDist[0], this._minMaxDist[1]);

		this._computeMatrix();
	},

	animateToState : function (newstate, newtime) {
		// stop animation
		this._isAnimating = false;

		if(newstate)
		{
			// stop autoWalking
			this._isAutoWalking = false;

			// setting targets
			this._targetPanX     = newstate[0];
			this._targetPanY     = newstate[1];
			this._targetAngleX   = sglDegToRad(newstate[2]);
			this._targetAngleY   = sglDegToRad(newstate[3]);
			this._targetDistance = newstate[4];

			//check limits
			this._targetPanX = this._clamp(this._targetPanX, this._minMaxPanX[0], this._minMaxPanX[1]);
			this._targetPanY = this._clamp(this._targetPanY, this._minMaxPanY[0], this._minMaxPanY[1]);
			this._targetAngleX = this._clamp(this._targetAngleX, this._minMaxAngleX[0], this._minMaxAngleX[1]);
			this._targetAngleY = this._clamp(this._targetAngleY, this._minMaxAngleY[0], this._minMaxAngleY[1]);
			this._targetDistance = this._clamp(this._targetDistance, this._minMaxDist[0], this._minMaxDist[1]);

			// setting base velocities
			this._speedPanX = 2.0;
			this._speedPanY = 2.0;
			this._speedAngleX = Math.PI;
			this._speedAngleY = Math.PI;
			this._speedDistance = 2.0;

			// find max animation time to set a time limit and then synchronize all movements
			var timePanX = Math.abs((this._targetPanX - this._panX) / this._speedPanX);
			var timePanY = Math.abs((this._targetPanY - this._panY) / this._speedPanY);
			var timeAngleX = Math.abs((this._targetAngleX - this._angleX) / this._speedAngleX);
			var timeAngleY = Math.abs((this._targetAngleY - this._angleY) / this._speedAngleY);
			var timeDistance = Math.abs((this._targetDistance - this._distance) / this._speedDistance);

			var maxtime = Math.max( timePanX, Math.max( timePanY, Math.max( timeAngleX, Math.max( timeAngleY, timeDistance ) ) ));
			var animationtime = this._clamp(maxtime, 0.5, 2.0);

			if(newtime) animationtime = newtime;
			else if (this._animationTime) animationtime = this._animationTime;

			this._speedPanX     *= timePanX / animationtime;
			this._speedPanY     *= timePanY / animationtime;
			this._speedAngleX   *= timeAngleX / animationtime;
			this._speedAngleY   *= timeAngleY / animationtime;
			this._speedDistance *= timeDistance / animationtime;
		}
		else
		{
			if(this._pathPosNum == this._pathStates.length){
				this._isAutoWalking = false;
				this._pathPosNum = 0;
			}
			else {
				var state = this._pathStates[this._pathPosNum][0];
				var time = this._animationTime;
				if(!Array.isArray(state)) state = this._pathStates[this._pathPosNum];
				else if (this._pathStates[this._pathPosNum][1]) time = this._pathStates[this._pathPosNum][1];
				if(!this._isAutoWalking) this.animateToState(state, time);
				this._isAutoWalking = true;
			}
		}

		// start animation
		this._isAnimating = true;
	},

	recenter : function (newpoint) {
		// stop animation
		this._isAnimating = this._isAutoWalking = false;

		var newpanX = -(newpoint[0]-this.myPresenter.sceneCenter[0]) * this.myPresenter.sceneRadiusInv;
		var newpanY = -(newpoint[1]-this.myPresenter.sceneCenter[1]) * this.myPresenter.sceneRadiusInv;

		this.animateToState([newpanX, newpanY, sglRadToDeg(this._angleX), sglRadToDeg(this._angleY), (this._distance * 0.6)]);
	},

	tick : function (dt) {
		if(!this._isAnimating) return false;

		var deltaPanX     = this._speedPanX * dt;
		var deltaPanY     = this._speedPanY * dt;
		var deltaAngleX   = this._speedAngleX * dt;
		var deltaAngleY   = this._speedAngleY * dt;
		var deltaDistance = this._speedDistance * dt;

		var diffPanX = this._targetPanX - this._panX;
		var diffPanY = this._targetPanY - this._panY;
		var diffAngleX = this._targetAngleX - this._angleX;
		var diffAngleY = this._targetAngleY - this._angleY;
		var diffDistance = this._targetDistance - this._distance;

		if (diffPanX > deltaPanX)
			this._panX += deltaPanX;
		else if (diffPanX < -deltaPanX)
			this._panX -= deltaPanX;
		else
			this._panX = this._targetPanX;

		if (diffPanY > deltaPanY)
			this._panY += deltaPanY;
		else if (diffPanY < -deltaPanY)
			this._panY -= deltaPanY;
		else
			this._panY = this._targetPanY;

		if (diffAngleX > deltaAngleX)
			this._angleX += deltaAngleX;
		else if (diffAngleX < -deltaAngleX)
			this._angleX -= deltaAngleX;
		else
			this._angleX = this._targetAngleX;

		if (diffAngleY > deltaAngleY)
			this._angleY += deltaAngleY;
		else if (diffAngleY < -deltaAngleY)
			this._angleY -= deltaAngleY;
		else
			this._angleY = this._targetAngleY;

		if (diffDistance > deltaDistance)
			this._distance += deltaDistance;
		else if (diffDistance < -deltaDistance)
			this._distance -= deltaDistance;
		else
			this._distance = this._targetDistance;

		if(this._panX == this._targetPanX)
			if(this._panY == this._targetPanY)
				if(this._angleX == this._targetAngleX)
					if(this._angleY == this._targetAngleY)
						if(this._distance == this._targetDistance){
								this._isAnimating = false;
								if(typeof onTrackballArrived != "undefined")
									onTrackballArrived(this.getState());
								if(this._isAutoWalking) { this._pathPosNum++; this._isAutoWalking = false; this.animateToState(); }
							}

		this._computeMatrix();
		return true;
	},

	set action(a) { if(this._action != a) { this._new_action = true; this._action = a; } },

	get action()  { return this._action; },

	get matrix() { this._computeMatrix(); return this._matrix; },

	get distance() { return this._distance; },

	reset : function () {
		this._matrix = SglMat4.identity();
		this._action = SGL_TRACKBALL_NO_ACTION;
		this._new_action = true;

		this._panX = this._startPanX;
		this._panY = this._startPanY;
		this._angleX = this._startAngleX;
		this._angleY = this._startAngleY;
		this._distance = this._startDistance;

		this._pathPosNum = 0;

		this._isAutoWalking = false;
		this._isAnimating = false;

		this._computeMatrix();
	},

	track : function(m, x, y, z) {
		if(this._animationLocked && this._isAnimating) this._action = SGL_TRACKBALL_NO_ACTION;
		if(this._new_action) {
			this._start[0] = x;
			this._start[1] = y;
			this._new_action = false;
		}

		var dx = this._start[0] - x;
		var dy = this._start[1] - y;
		this._start[0] = x;
		this._start[1] = y;

		switch (this._action) {
			case SGL_TRACKBALL_ROTATE:
				this._isAnimating = this._isAutoWalking = false; //stopping animation
				this.rotate(m, dx, dy);
			break;

			case SGL_TRACKBALL_PAN:
				this._isAnimating = this._isAutoWalking = false; //stopping animation
				this.pan(m, dx, dy);
			break;

			case SGL_TRACKBALL_SCALE:
				this._isAnimating = this._isAutoWalking = false; //stopping animation
				this.scale(m, z);
			break;

			default:
			break;
		}
		return this._computeMatrix();
	},

	rotate: function(m, dx, dy) {
		this._angleX += dx;
		this._angleY += dy;
		this._angleX = this._clamp(this._angleX, this._minMaxAngleX[0], this._minMaxAngleX[1]);
		this._angleY = this._clamp(this._angleY, this._minMaxAngleY[0], this._minMaxAngleY[1]);
	},

	pan: function(m, dx, dy) {
		var panSpeed = Math.max(Math.min(1.5, this._distance),0.05);
		this._panX -= dx/2.0 * panSpeed; 
		this._panY -= dy/2.0 * panSpeed;
		this._panX = this._clamp(this._panX, this._minMaxPanX[0], this._minMaxPanX[1]);
		this._panY = this._clamp(this._panY, this._minMaxPanY[0], this._minMaxPanY[1]);
	},

	scale : function(m, s) {
		this._distance *= s;
		this._distance = this._clamp(this._distance, this._minMaxDist[0], this._minMaxDist[1]);
	}
};
/***********************************************************************/
