/** 
 * @file lljoystickbutton.cpp
 * @brief LLJoystick class implementation
 *
 * $LicenseInfo:firstyear=2001&license=viewergpl$
 * 
 * Copyright (c) 2001-2008, Linden Research, Inc.
 * 
 * Second Life Viewer Source Code
 * The source code in this file ("Source Code") is provided by Linden Lab
 * to you under the terms of the GNU General Public License, version 2.0
 * ("GPL"), unless you have obtained a separate licensing agreement
 * ("Other License"), formally executed by you and Linden Lab.  Terms of
 * the GPL can be found in doc/GPL-license.txt in this distribution, or
 * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
 * 
 * There are special exceptions to the terms and conditions of the GPL as
 * it is applied to this Source Code. View the full text of the exception
 * in the file doc/FLOSS-exception.txt in this software distribution, or
 * online at http://secondlifegrid.net/programs/open_source/licensing/flossexception
 * 
 * By copying, modifying or distributing this software, you acknowledge
 * that you have read and understood your obligations described above,
 * and agree to abide by those obligations.
 * 
 * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
 * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
 * COMPLETENESS OR PERFORMANCE.
 * $/LicenseInfo$
 */

#include "llviewerprecompiledheaders.h"

#include "lljoystickbutton.h"

// Library includes
#include "llcoord.h"
#include "indra_constants.h"
#include "llglimmediate.h"

// Project includes
#include "llui.h"
#include "llagent.h"
#include "llviewerimage.h"
#include "llviewerimagelist.h"
#include "llviewerwindow.h"
#include "llmoveview.h"

#include "llglheaders.h"

static LLRegisterWidget<LLJoystickAgentSlide> r1("joystick_slide");
static LLRegisterWidget<LLJoystickAgentTurn> r2("joystick_turn");


const F32 NUDGE_TIME = 0.25f;		// in seconds
const F32 ORBIT_NUDGE_RATE = 0.05f; // fraction of normal speed

//
// Public Methods
//
LLJoystick::LLJoystick(
	const LLString& name, 
	LLRect rect,
	const LLString &default_image,
	const LLString &selected_image,
	EJoystickQuadrant initial_quadrant )
	:	
	LLButton(name, rect, default_image, selected_image, NULL, NULL),
	mInitialQuadrant(initial_quadrant),
	mInitialOffset(0, 0),
	mLastMouse(0, 0),
	mFirstMouse(0, 0),
	mVertSlopNear(0),
	mVertSlopFar(0),
	mHorizSlopNear(0),
	mHorizSlopFar(0),
	mHeldDown(FALSE),
	mHeldDownTimer()
{
	setHeldDownCallback(&LLJoystick::onHeldDown);
	setCallbackUserData(this);
}


void LLJoystick::updateSlop()
{
	mVertSlopNear = getRect().getHeight();
	mVertSlopFar = getRect().getHeight() * 2;

	mHorizSlopNear = getRect().getWidth();
	mHorizSlopFar = getRect().getWidth() * 2;

	// Compute initial mouse offset based on initial quadrant.
	// Place the mouse evenly between the near and far zones.
	switch (mInitialQuadrant)
	{
	case JQ_ORIGIN:
		mInitialOffset.set(0, 0);
		break;

	case JQ_UP:
		mInitialOffset.mX = 0;
		mInitialOffset.mY = (mVertSlopNear + mVertSlopFar) / 2;
		break;

	case JQ_DOWN:
		mInitialOffset.mX = 0;
		mInitialOffset.mY = - (mVertSlopNear + mVertSlopFar) / 2;
		break;

	case JQ_LEFT:
		mInitialOffset.mX = - (mHorizSlopNear + mHorizSlopFar) / 2;
		mInitialOffset.mY = 0;
		break;

	case JQ_RIGHT:
		mInitialOffset.mX = (mHorizSlopNear + mHorizSlopFar) / 2;
		mInitialOffset.mY = 0;
		break;

	default:
		llerrs << "LLJoystick::LLJoystick() - bad switch case" << llendl;
		break;
	}

	return;
}


BOOL LLJoystick::handleMouseDown(S32 x, S32 y, MASK mask)
{
	//llinfos << "joystick mouse down " << x << ", " << y << llendl;

	mLastMouse.set(x, y);
	mFirstMouse.set(x, y);

	mMouseDownTimer.reset();
	return LLButton::handleMouseDown(x, y, mask);
}


BOOL LLJoystick::handleMouseUp(S32 x, S32 y, MASK mask)
{
	// llinfos << "joystick mouse up " << x << ", " << y << llendl;

	if( hasMouseCapture() )
	{
		mLastMouse.set(x, y);
		mHeldDown = FALSE;
		onMouseUp();
	}

	return LLButton::handleMouseUp(x, y, mask);
}


BOOL LLJoystick::handleHover(S32 x, S32 y, MASK mask)
{
	if( hasMouseCapture() )
	{
		mLastMouse.set(x, y);
	}

	return LLButton::handleHover(x, y, mask);
}

F32 LLJoystick::getElapsedHeldDownTime()
{
	if( mHeldDown )
	{
		return getHeldDownTime();
	}
	else
	{
		return 0.f;
	}
}

// static
void LLJoystick::onHeldDown(void *userdata)
{
	LLJoystick *self = (LLJoystick *)userdata;

	// somebody removed this function without checking the
	// build. Removed 2007-03-26.
	//llassert( gViewerWindow->hasMouseCapture( self ) );

	self->mHeldDown = TRUE;
	self->onHeldDown();
}

EJoystickQuadrant LLJoystick::selectQuadrant(LLXMLNodePtr node)
{
	
	EJoystickQuadrant quadrant = JQ_RIGHT;

	if (node->hasAttribute("quadrant"))
	{
		LLString quadrant_name;
		node->getAttributeString("quadrant", quadrant_name);

		quadrant = quadrantFromName(quadrant_name.c_str());
	}
	return quadrant;
}


LLString LLJoystick::nameFromQuadrant(EJoystickQuadrant	quadrant)
{
	if (quadrant == JQ_ORIGIN)	    return LLString("origin");
	else if (quadrant == JQ_UP)	    return LLString("up");
	else if (quadrant == JQ_DOWN)	return LLString("down");
	else if (quadrant == JQ_LEFT)	return LLString("left");
	else if (quadrant == JQ_RIGHT)	return LLString("right");
	else return LLString();
}


EJoystickQuadrant LLJoystick::quadrantFromName(const LLString& sQuadrant)
{
	EJoystickQuadrant quadrant = JQ_RIGHT;

	if (sQuadrant == "origin")
	{
		quadrant = JQ_ORIGIN;
	}
	else if (sQuadrant == "up")
	{
		quadrant = JQ_UP;
	}
	else if (sQuadrant == "down")
	{
		quadrant = JQ_DOWN;
	}
	else if (sQuadrant == "left")
	{
		quadrant = JQ_LEFT;
	}
	else if (sQuadrant == "right")
	{
		quadrant = JQ_RIGHT;
	}

	return quadrant;
}


LLXMLNodePtr LLJoystick::getXML(bool save_children) const
{
	LLXMLNodePtr node = LLUICtrl::getXML();

	node->createChild("halign", TRUE)->setStringValue(LLFontGL::nameFromHAlign(getHAlign()));
	node->createChild("quadrant", TRUE)->setStringValue(nameFromQuadrant(mInitialQuadrant));

	addImageAttributeToXML(node,getImageUnselectedName(),getImageUnselectedID(),"image_unselected");
	addImageAttributeToXML(node,getImageSelectedName(),getImageSelectedID(),"image_selected");
	
	node->createChild("scale_image", TRUE)->setBoolValue(getScaleImage());

	return node;
}



//-------------------------------------------------------------------------------
// LLJoystickAgentTurn
//-------------------------------------------------------------------------------

void LLJoystickAgentTurn::onHeldDown()
{
	F32 time = getElapsedHeldDownTime();
	updateSlop();

	//llinfos << "move forward/backward (and/or turn)" << llendl;

	S32 dx = mLastMouse.mX - mFirstMouse.mX + mInitialOffset.mX;
	S32 dy = mLastMouse.mY - mFirstMouse.mY + mInitialOffset.mY;

	float m = (float) (dx)/abs(dy);
	
	if (m > 1) {
		m = 1;
	}
	else if (m < -1) {
		m = -1;
	}
	gAgent.moveYaw(-LLFloaterMove::getYawRate(time)*m);
	

	// handle forward/back movement
	if (dy > mVertSlopFar)
	{
		// ...if mouse is forward of run region run forward
		gAgent.moveAt(1);
	}
	else if (dy > mVertSlopNear)
	{
		if( time < NUDGE_TIME )
		{
			gAgent.moveAtNudge(1);
		}
		else
		{
			// ...else if mouse is forward of walk region walk forward
			// JC 9/5/2002 - Always run / move quickly.
			gAgent.moveAt(1);
		}
	}
	else if (dy < -mVertSlopFar)
	{
		// ...else if mouse is behind run region run backward
		gAgent.moveAt(-1);
	}
	else if (dy < -mVertSlopNear)
	{
		if( time < NUDGE_TIME )
		{
			gAgent.moveAtNudge(-1);
		}
		else
		{
			// ...else if mouse is behind walk region walk backward
			// JC 9/5/2002 - Always run / move quickly.
			gAgent.moveAt(-1);
		}
	}
}

LLView* LLJoystickAgentTurn::fromXML(LLXMLNodePtr node, LLView *parent, LLUICtrlFactory *factory)
{
	LLString name("button");
	node->getAttributeString("name", name);

	LLString	image_unselected;
	if (node->hasAttribute("image_unselected")) node->getAttributeString("image_unselected",image_unselected);
	
	LLString	image_selected;
	if (node->hasAttribute("image_selected")) node->getAttributeString("image_selected",image_selected);

	EJoystickQuadrant quad = JQ_ORIGIN;
	if (node->hasAttribute("quadrant")) quad = selectQuadrant(node);
	
	LLJoystickAgentTurn *button = new LLJoystickAgentTurn(name, 
		LLRect(),
		image_unselected,
		image_selected,
		quad);

	if (node->hasAttribute("halign"))
	{
		LLFontGL::HAlign halign = selectFontHAlign(node);
		button->setHAlign(halign);
	}

	if (node->hasAttribute("scale_image"))
	{
		BOOL	needsScale = FALSE;
		node->getAttributeBOOL("scale_image",needsScale);
		button->setScaleImage( needsScale );
	}

	button->initFromXML(node, parent);
	
	return button;
}



//-------------------------------------------------------------------------------
// LLJoystickAgentSlide
//-------------------------------------------------------------------------------

void LLJoystickAgentSlide::onMouseUp()
{
	F32 time = getElapsedHeldDownTime();
	if( time < NUDGE_TIME )
	{
		switch (mInitialQuadrant)
		{
		case JQ_LEFT:
			gAgent.moveLeftNudge(1);
			break;

		case JQ_RIGHT:
			gAgent.moveLeftNudge(-1);
			break;

		default:
			break;
		}
	}
}

void LLJoystickAgentSlide::onHeldDown()
{
	//llinfos << "slide left/right (and/or move forward/backward)" << llendl;

	updateSlop();

	S32 dx = mLastMouse.mX - mFirstMouse.mX + mInitialOffset.mX;
	S32 dy = mLastMouse.mY - mFirstMouse.mY + mInitialOffset.mY;

	// handle left-right sliding
	if (dx > mHorizSlopNear)
	{
		gAgent.moveLeft(-1);
	}
	else if (dx < -mHorizSlopNear)
	{
		gAgent.moveLeft(1);
	}

	// handle forward/back movement
	if (dy > mVertSlopFar)
	{
		// ...if mouse is forward of run region run forward
		gAgent.moveAt(1);
	}
	else if (dy > mVertSlopNear)
	{
		// ...else if mouse is forward of walk region walk forward
		gAgent.moveAtNudge(1);
	}
	else if (dy < -mVertSlopFar)
	{
		// ...else if mouse is behind run region run backward
		gAgent.moveAt(-1);
	}
	else if (dy < -mVertSlopNear)
	{
		// ...else if mouse is behind walk region walk backward
		gAgent.moveAtNudge(-1);
	}
}


// static
LLView* LLJoystickAgentSlide::fromXML(LLXMLNodePtr node, LLView *parent, LLUICtrlFactory *factory)
{
	LLString name("button");
	node->getAttributeString("name", name);

	LLString	image_unselected;
	if (node->hasAttribute("image_unselected")) node->getAttributeString("image_unselected",image_unselected);
	
	LLString	image_selected;
	if (node->hasAttribute("image_selected")) node->getAttributeString("image_selected",image_selected);
	
	
	EJoystickQuadrant quad = JQ_ORIGIN;
	if (node->hasAttribute("quadrant")) quad = selectQuadrant(node);
	
	LLJoystickAgentSlide *button = new LLJoystickAgentSlide(name, 
		LLRect(),
		image_unselected,
		image_selected,
		quad);

	if (node->hasAttribute("halign"))
	{
		LLFontGL::HAlign halign = selectFontHAlign(node);
		button->setHAlign(halign);
	}

	if (node->hasAttribute("scale_image"))
	{
		BOOL	needsScale = FALSE;
		node->getAttributeBOOL("scale_image",needsScale);
		button->setScaleImage( needsScale );
	}
	
	button->initFromXML(node, parent);
	
	return button;
}


//-------------------------------------------------------------------------------
// LLJoystickCameraRotate
//-------------------------------------------------------------------------------

LLJoystickCameraRotate::LLJoystickCameraRotate(const LLString& name, LLRect rect, const LLString &out_img, const LLString &in_img)
	: 
	LLJoystick(name, rect, out_img, in_img, JQ_ORIGIN),
	mInLeft( FALSE ),
	mInTop( FALSE ),
	mInRight( FALSE ),
	mInBottom( FALSE )
{ }


void LLJoystickCameraRotate::updateSlop()
{
	// do the initial offset calculation based on mousedown location

	// small fixed slop region
	mVertSlopNear = 16;
	mVertSlopFar = 32;

	mHorizSlopNear = 16;
	mHorizSlopFar = 32;

	return;
}


BOOL LLJoystickCameraRotate::handleMouseDown(S32 x, S32 y, MASK mask)
{
	updateSlop();

	// Set initial offset based on initial click location
	S32 horiz_center = getRect().getWidth() / 2;
	S32 vert_center = getRect().getHeight() / 2;

	S32 dx = x - horiz_center;
	S32 dy = y - vert_center;

	if (dy > dx && dy > -dx)
	{
		// top
		mInitialOffset.mX = 0;
		mInitialOffset.mY = (mVertSlopNear + mVertSlopFar) / 2;
		mInitialQuadrant = JQ_UP;
	}
	else if (dy > dx && dy <= -dx)
	{
		// left
		mInitialOffset.mX = - (mHorizSlopNear + mHorizSlopFar) / 2;
		mInitialOffset.mY = 0;
		mInitialQuadrant = JQ_LEFT;
	}
	else if (dy <= dx && dy <= -dx)
	{
		// bottom
		mInitialOffset.mX = 0;
		mInitialOffset.mY = - (mVertSlopNear + mVertSlopFar) / 2;
		mInitialQuadrant = JQ_DOWN;
	}
	else
	{
		// right
		mInitialOffset.mX = (mHorizSlopNear + mHorizSlopFar) / 2;
		mInitialOffset.mY = 0;
		mInitialQuadrant = JQ_RIGHT;
	}

	return LLJoystick::handleMouseDown(x, y, mask);
}


void LLJoystickCameraRotate::onHeldDown()
{
	updateSlop();

	S32 dx = mLastMouse.mX - mFirstMouse.mX + mInitialOffset.mX;
	S32 dy = mLastMouse.mY - mFirstMouse.mY + mInitialOffset.mY;

	// left-right rotation
	if (dx > mHorizSlopNear)
	{
		gAgent.unlockView();
		gAgent.setOrbitLeftKey(getOrbitRate());
	}
	else if (dx < -mHorizSlopNear)
	{
		gAgent.unlockView();
		gAgent.setOrbitRightKey(getOrbitRate());
	}

	// over/under rotation
	if (dy > mVertSlopNear)
	{
		gAgent.unlockView();
		gAgent.setOrbitUpKey(getOrbitRate());
	}
	else if (dy < -mVertSlopNear)
	{
		gAgent.unlockView();
		gAgent.setOrbitDownKey(getOrbitRate());
	}
}

F32 LLJoystickCameraRotate::getOrbitRate()
{
	F32 time = getElapsedHeldDownTime();
	if( time < NUDGE_TIME )
	{
		F32 rate = ORBIT_NUDGE_RATE + time * (1 - ORBIT_NUDGE_RATE)/ NUDGE_TIME;
		//llinfos << rate << llendl;
		return rate;
	}
	else
	{
		return 1;
	}
}


// Only used for drawing
void LLJoystickCameraRotate::setToggleState( BOOL left, BOOL top, BOOL right, BOOL bottom )
{
	mInLeft = left;
	mInTop = top;
	mInRight = right;
	mInBottom = bottom;
}

void LLJoystickCameraRotate::draw()
{
	LLGLSUIDefault gls_ui;

	getImageUnselected()->draw( 0, 0 );

	if( mInTop )
	{
		drawRotatedImage( getImageSelected()->getImage(), 0 );
	}

	if( mInRight )
	{
		drawRotatedImage( getImageSelected()->getImage(), 1 );
	}

	if( mInBottom )
	{
		drawRotatedImage( getImageSelected()->getImage(), 2 );
	}

	if( mInLeft )
	{
		drawRotatedImage( getImageSelected()->getImage(), 3 );
	}

	if (sDebugRects)
	{
		drawDebugRect();
	}
}

// Draws image rotated by multiples of 90 degrees
void LLJoystickCameraRotate::drawRotatedImage( const LLImageGL* image, S32 rotations )
{
	S32 width = image->getWidth();
	S32 height = image->getHeight();

	F32 uv[][2] = 
	{
		{ 1.f, 1.f },
		{ 0.f, 1.f },
		{ 0.f, 0.f },
		{ 1.f, 0.f }
	};

	image->bind();

	gGL.color4fv(UI_VERTEX_COLOR.mV);
	
	gGL.begin(GL_QUADS);
	{
		gGL.texCoord2fv( uv[ (rotations + 0) % 4]);
		gGL.vertex2i(width, height );

		gGL.texCoord2fv( uv[ (rotations + 1) % 4]);
		gGL.vertex2i(0, height );

		gGL.texCoord2fv( uv[ (rotations + 2) % 4]);
		gGL.vertex2i(0, 0);

		gGL.texCoord2fv( uv[ (rotations + 3) % 4]);
		gGL.vertex2i(width, 0);
	}
	gGL.end();
}



//-------------------------------------------------------------------------------
// LLJoystickCameraTrack
//-------------------------------------------------------------------------------


void LLJoystickCameraTrack::onHeldDown()
{
	updateSlop();

	S32 dx = mLastMouse.mX - mFirstMouse.mX + mInitialOffset.mX;
	S32 dy = mLastMouse.mY - mFirstMouse.mY + mInitialOffset.mY;

	if (dx > mVertSlopNear)
	{
		gAgent.unlockView();
		gAgent.setPanRightKey(getOrbitRate());
	}
	else if (dx < -mVertSlopNear)
	{
		gAgent.unlockView();
		gAgent.setPanLeftKey(getOrbitRate());
	}

	// over/under rotation
	if (dy > mVertSlopNear)
	{
		gAgent.unlockView();
		gAgent.setPanUpKey(getOrbitRate());
	}
	else if (dy < -mVertSlopNear)
	{
		gAgent.unlockView();
		gAgent.setPanDownKey(getOrbitRate());
	}
}



//-------------------------------------------------------------------------------
// LLJoystickCameraZoom
//-------------------------------------------------------------------------------

LLJoystickCameraZoom::LLJoystickCameraZoom(const LLString& name, LLRect rect, const LLString &out_img, const LLString &plus_in_img, const LLString &minus_in_img)
	: 
	LLJoystick(name, rect, out_img, "", JQ_ORIGIN),
	mInTop( FALSE ),
	mInBottom( FALSE )
{
	mPlusInImage = LLUIImageList::getInstance()->getUIImage(plus_in_img);
	mMinusInImage = LLUIImageList::getInstance()->getUIImage(minus_in_img);
}


BOOL LLJoystickCameraZoom::handleMouseDown(S32 x, S32 y, MASK mask)
{
	BOOL handled = LLJoystick::handleMouseDown(x, y, mask);

	if( handled )
	{
		if (mFirstMouse.mY > getRect().getHeight() / 2)
		{
			mInitialQuadrant = JQ_UP;
		}
		else
		{
			mInitialQuadrant = JQ_DOWN;
		}
	}
	return handled;
}


void LLJoystickCameraZoom::onHeldDown()
{
	updateSlop();

	const F32 FAST_RATE = 2.5f; // two and a half times the normal rate

	S32 dy = mLastMouse.mY - mFirstMouse.mY + mInitialOffset.mY;

	if (dy > mVertSlopFar)
	{
		// Zoom in fast
		gAgent.unlockView();
		gAgent.setOrbitInKey(FAST_RATE);
	}
	else if (dy > mVertSlopNear)
	{
		// Zoom in slow
		gAgent.unlockView();
		gAgent.setOrbitInKey(getOrbitRate());
	}
	else if (dy < -mVertSlopFar)
	{
		// Zoom out fast
		gAgent.unlockView();
		gAgent.setOrbitOutKey(FAST_RATE);
	}
	else if (dy < -mVertSlopNear)
	{
		// Zoom out slow
		gAgent.unlockView();
		gAgent.setOrbitOutKey(getOrbitRate());
	}
}

// Only used for drawing
void LLJoystickCameraZoom::setToggleState( BOOL top, BOOL bottom )
{
	mInTop = top;
	mInBottom = bottom;
}

void LLJoystickCameraZoom::draw()
{
	if( mInTop )
	{
		mPlusInImage->draw(0,0);
	}
	else
	if( mInBottom )
	{
		mMinusInImage->draw(0,0);
	}
	else
	{
		getImageUnselected()->draw( 0, 0 );
	}

	if (sDebugRects)
	{
		drawDebugRect();
	}
}

void LLJoystickCameraZoom::updateSlop()
{
	mVertSlopNear = getRect().getHeight() / 4;
	mVertSlopFar = getRect().getHeight() / 2;

	mHorizSlopNear = getRect().getWidth() / 4;
	mHorizSlopFar = getRect().getWidth() / 2;

	// Compute initial mouse offset based on initial quadrant.
	// Place the mouse evenly between the near and far zones.
	switch (mInitialQuadrant)
	{
	case JQ_ORIGIN:
		mInitialOffset.set(0, 0);
		break;

	case JQ_UP:
		mInitialOffset.mX = 0;
		mInitialOffset.mY = (mVertSlopNear + mVertSlopFar) / 2;
		break;

	case JQ_DOWN:
		mInitialOffset.mX = 0;
		mInitialOffset.mY = - (mVertSlopNear + mVertSlopFar) / 2;
		break;

	case JQ_LEFT:
		mInitialOffset.mX = - (mHorizSlopNear + mHorizSlopFar) / 2;
		mInitialOffset.mY = 0;
		break;

	case JQ_RIGHT:
		mInitialOffset.mX = (mHorizSlopNear + mHorizSlopFar) / 2;
		mInitialOffset.mY = 0;
		break;

	default:
		llerrs << "LLJoystick::LLJoystick() - bad switch case" << llendl;
		break;
	}

	return;
}


F32 LLJoystickCameraZoom::getOrbitRate()
{
	F32 time = getElapsedHeldDownTime();
	if( time < NUDGE_TIME )
	{
		F32 rate = ORBIT_NUDGE_RATE + time * (1 - ORBIT_NUDGE_RATE)/ NUDGE_TIME;
//		llinfos << "rate " << rate << " time " << time << llendl;
		return rate;
	}
	else
	{
		return 1;
	}
}