/** 
 * @file llviewermediafocus.cpp
 * @brief Governs focus on Media prims
 *
 * $LicenseInfo:firstyear=2003&license=viewergpl$
 * 
 * Copyright (c) 2003-2007, 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://secondlife.com/developers/opensource/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://secondlife.com/developers/opensource/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 "llviewermediafocus.h"

//LLViewerMediaFocus
#include "llviewerobjectlist.h"
#include "llpanelprimmediacontrols.h"
#include "llpluginclassmedia.h"
#include "llagent.h"
#include "lltoolpie.h"
#include "llviewercamera.h"
#include "llviewermedia.h"
#include "llhudview.h"
#include "lluictrlfactory.h"
#include "lldrawable.h"
#include "llparcel.h"
#include "llviewerparcelmgr.h"
#include "llweb.h"
#include "llmediaentry.h"
#include "llkeyboard.h"
#include "lltoolmgr.h"
#include "llvovolume.h"

//
// LLViewerMediaFocus
//

LLViewerMediaFocus::LLViewerMediaFocus()
:	mFocusedObjectFace(0),
	mHoverObjectFace(0)
{
}

LLViewerMediaFocus::~LLViewerMediaFocus()
{
	// The destructor for LLSingletons happens at atexit() time, which is too late to do much.
	// Clean up in cleanupClass() instead.
}

void LLViewerMediaFocus::setFocusFace(LLPointer<LLViewerObject> objectp, S32 face, viewer_media_t media_impl, LLVector3 pick_normal)
{	
	LLParcel *parcel = LLViewerParcelMgr::getInstance()->getAgentParcel();
	
	LLViewerMediaImpl *old_media_impl = getFocusedMediaImpl();
	if(old_media_impl)
	{
		old_media_impl->focus(false);
	}
	
	// Always clear the current selection.  If we're setting focus on a face, we'll reselect the correct object below.
	LLSelectMgr::getInstance()->deselectAll();
	mSelection = NULL;

	if (media_impl.notNull() && objectp.notNull())
	{
		bool face_auto_zoom = false;

		mFocusedImplID = media_impl->getMediaTextureID();
		mFocusedObjectID = objectp->getID();
		mFocusedObjectFace = face;
		mFocusedObjectNormal = pick_normal;
		
		// Set the selection in the selection manager so we can draw the focus ring.
		mSelection = LLSelectMgr::getInstance()->selectObjectOnly(objectp, face);

		// Focusing on a media face clears its disable flag.
		media_impl->setDisabled(false);

		LLTextureEntry* tep = objectp->getTE(face);
		if(tep->hasMedia())
		{
			LLMediaEntry* mep = tep->getMediaData();
			face_auto_zoom = mep->getAutoZoom();
			if(!media_impl->hasMedia())
			{
				std::string url = mep->getCurrentURL().empty() ? mep->getHomeURL() : mep->getCurrentURL();
				media_impl->navigateTo(url, "", true);
			}
		}
		else
		{
			// This should never happen.
			llwarns << "Can't find media entry for focused face" << llendl;
		}

		media_impl->focus(true);
		gFocusMgr.setKeyboardFocus(this);
		
		// We must do this before  processing the media HUD zoom, or it may zoom to the wrong face. 
		update();

		if(mMediaControls.get())
		{
			if(face_auto_zoom && ! parcel->getMediaPreventCameraZoom())
			{
				// Zoom in on this face
				mMediaControls.get()->resetZoomLevel();
				mMediaControls.get()->nextZoomLevel();
			}
			else
			{
				// Reset the controls' zoom level without moving the camera.
				// This fixes the case where clicking focus between two non-autozoom faces doesn't change the zoom-out button back to a zoom-in button.
				mMediaControls.get()->resetZoomLevel(false);
			}
		}
	}
	else
	{
		if(mFocusedImplID.notNull())
		{
			if(mMediaControls.get())
			{
				// Don't reset camera zoom by default, just tell the controls they're no longer controlling zoom.
				mMediaControls.get()->resetZoomLevel(false);
			}
		}

		if(hasFocus())
		{
			gFocusMgr.setKeyboardFocus(NULL);
		}
		
		mFocusedImplID = LLUUID::null;
		mFocusedObjectID = LLUUID::null;
		mFocusedObjectFace = 0;
	}
}

void LLViewerMediaFocus::clearFocus()
{
	setFocusFace(NULL, 0, NULL);
}

void LLViewerMediaFocus::setHoverFace(LLPointer<LLViewerObject> objectp, S32 face, viewer_media_t media_impl, LLVector3 pick_normal)
{
	if (media_impl.notNull())
	{
		mHoverImplID = media_impl->getMediaTextureID();
		mHoverObjectID = objectp->getID();
		mHoverObjectFace = face;
		mHoverObjectNormal = pick_normal;
	}
	else
	{
		mHoverObjectID = LLUUID::null;
		mHoverObjectFace = 0;
		mHoverImplID = LLUUID::null;
	}
}

void LLViewerMediaFocus::clearHover()
{
	setHoverFace(NULL, 0, NULL);
}


bool LLViewerMediaFocus::getFocus()
{
	if (gFocusMgr.getKeyboardFocus() == this)
	{
		return true;
	}
	return false;
}

// This function selects an ideal viewing distance based on the focused object, pick normal, and padding value
void LLViewerMediaFocus::setCameraZoom(LLViewerObject* object, LLVector3 normal, F32 padding_factor)
{
	if (object)
	{
		gAgent.setFocusOnAvatar(FALSE, ANIMATE);

		LLBBox bbox = object->getBoundingBoxAgent();
		LLVector3d center = gAgent.getPosGlobalFromAgent(bbox.getCenterAgent());
		F32 height;
		F32 width;
		F32 depth;
		F32 angle_of_view;
		F32 distance;

		// We need the aspect ratio, and the 3 components of the bbox as height, width, and depth.
		F32 aspect_ratio = getBBoxAspectRatio(bbox, normal, &height, &width, &depth);
		F32 camera_aspect = LLViewerCamera::getInstance()->getAspect();

		// We will normally use the side of the volume aligned with the short side of the screen (i.e. the height for 
		// a screen in a landscape aspect ratio), however there is an edge case where the aspect ratio of the object is 
		// more extreme than the screen.  In this case we invert the logic, using the longer component of both the object
		// and the screen.  
		bool invert = (camera_aspect > 1.0f && aspect_ratio > camera_aspect) ||
			(camera_aspect < 1.0f && aspect_ratio < camera_aspect);

		// To calculate the optimum viewing distance we will need the angle of the shorter side of the view rectangle.
		// In portrait mode this is the width, and in landscape it is the height.
		// We then calculate the distance based on the corresponding side of the object bbox (width for portrait, height for landscape)
		// We will add half the depth of the bounding box, as the distance projection uses the center point of the bbox.
		if(camera_aspect < 1.0f || invert)
		{
			angle_of_view = llmax(0.1f, LLViewerCamera::getInstance()->getView() * LLViewerCamera::getInstance()->getAspect());
			distance = width * 0.5 * padding_factor / tan(angle_of_view * 0.5f );
		}
		else
		{
			angle_of_view = llmax(0.1f, LLViewerCamera::getInstance()->getView());
			distance = height * 0.5 * padding_factor / tan(angle_of_view * 0.5f );
		}

		distance += depth * 0.5;

		// Finally animate the camera to this new position and focal point
		LLVector3d camera_pos, target_pos;
		// The target lookat position is the center of the selection (in global coords)
		target_pos = center;
		// Target look-from (camera) position is "distance" away from the target along the normal 
		LLVector3d pickNormal = LLVector3d(normal);
		pickNormal.normalize();
        camera_pos = target_pos + pickNormal * distance;
        if (pickNormal == LLVector3d::z_axis || pickNormal == LLVector3d::z_axis_neg)
        {
			// If the normal points directly up, the camera will "flip" around.
			// We try to avoid this by adjusting the target camera position a 
			// smidge towards current camera position
			// *NOTE: this solution is not perfect.  All it attempts to solve is the
			// "looking down" problem where the camera flips around when it animates
			// to that position.  You still are not guaranteed to be looking at the
			// media in the correct orientation.  What this solution does is it will
			// put the camera into position keeping as best it can the current 
			// orientation with respect to the face.  In other words, if before zoom
			// the media appears "upside down" from the camera, after zooming it will
			// still be upside down, but at least it will not flip.
            LLVector3d cur_camera_pos = LLVector3d(gAgent.getCameraPositionGlobal());
            LLVector3d delta = (cur_camera_pos - camera_pos);
            F64 len = delta.length();
            delta.normalize();
            // Move 1% of the distance towards original camera location
            camera_pos += 0.01 * len * delta;
        }

		gAgent.setCameraPosAndFocusGlobal(camera_pos, target_pos, object->getID() );
	}
	else
	{
		// If we have no object, focus back on the avatar.
		gAgent.setFocusOnAvatar(TRUE, ANIMATE);
	}
}
void LLViewerMediaFocus::onFocusReceived()
{
	LLViewerMediaImpl* media_impl = getFocusedMediaImpl();
	if(media_impl)
		media_impl->focus(true);

	LLFocusableElement::onFocusReceived();
}

void LLViewerMediaFocus::onFocusLost()
{
	LLViewerMediaImpl* media_impl = getFocusedMediaImpl();
	if(media_impl)
		media_impl->focus(false);

	gViewerWindow->focusClient();
	LLFocusableElement::onFocusLost();
}

BOOL LLViewerMediaFocus::handleKey(KEY key, MASK mask, BOOL called_from_parent)
{
	LLViewerMediaImpl* media_impl = getFocusedMediaImpl();
	if(media_impl)
	{
		media_impl->handleKeyHere(key, mask);

		if (key == KEY_ESCAPE)
		{
			// Reset camera zoom in this case.
			if(mFocusedImplID.notNull())
			{
				if(mMediaControls.get())
				{
					mMediaControls.get()->resetZoomLevel(true);
				}
			}
			
			clearFocus();
		}
	}
	
	return true;
}

BOOL LLViewerMediaFocus::handleUnicodeChar(llwchar uni_char, BOOL called_from_parent)
{
	LLViewerMediaImpl* media_impl = getFocusedMediaImpl();
	if(media_impl)
		media_impl->handleUnicodeCharHere(uni_char);
	return true;
}
BOOL LLViewerMediaFocus::handleScrollWheel(S32 x, S32 y, S32 clicks)
{
	BOOL retval = FALSE;
	LLViewerMediaImpl* media_impl = getFocusedMediaImpl();
	if(media_impl && media_impl->hasMedia())
	{
        // the scrollEvent() API's x and y are not the same as handleScrollWheel's x and y.
        // The latter is the position of the mouse at the time of the event
        // The former is the 'scroll amount' in x and y, respectively.
        // All we have for 'scroll amount' here is 'clicks'.
		// We're also not passed the keyboard modifier mask, but we can get that from gKeyboard.
		media_impl->getMediaPlugin()->scrollEvent(0, clicks, gKeyboard->currentMask(TRUE));
		retval = TRUE;
	}
	return retval;
}

void LLViewerMediaFocus::update()
{
	if(mFocusedImplID.notNull() || mFocusedObjectID.notNull())
	{
		// We have a focused impl/face.
		if(!getFocus())
		{
			// We've lost keyboard focus -- check to see whether the media controls have it
			if(mMediaControls.get() && mMediaControls.get()->hasFocus())
			{
				// the media controls have focus -- don't clear.
			}
			else
			{
				// Someone else has focus -- back off.
				clearFocus();
			}
		}
		else if(LLToolMgr::getInstance()->inBuildMode())
		{
			// Build tools are selected -- clear focus.
			clearFocus();
		}
	}
	
	
	LLViewerMediaImpl *media_impl = getFocusedMediaImpl();
	LLViewerObject *viewer_object = getFocusedObject();
	S32 face = mFocusedObjectFace;
	LLVector3 normal = mFocusedObjectNormal;
	bool focus = true;
	
	if(!media_impl || !viewer_object)
	{
		focus = false;
		media_impl = getHoverMediaImpl();
		viewer_object = getHoverObject();
		face = mHoverObjectFace;
		normal = mHoverObjectNormal;
	}
	
	if(media_impl && viewer_object)
	{
		// We have an object and impl to point at.
		
		// Make sure the media HUD object exists.
		if(! mMediaControls.get())
		{
			LLPanelPrimMediaControls* media_controls = new LLPanelPrimMediaControls();
			mMediaControls = media_controls->getHandle();
			gHUDView->addChild(media_controls);	
		}
		mMediaControls.get()->setMediaFace(viewer_object, face, media_impl, normal);
	}
	else
	{
		// The media HUD is no longer needed.
		if(mMediaControls.get())
		{
			mMediaControls.get()->setMediaFace(NULL, 0, NULL);
		}
	}
}


// This function calculates the aspect ratio and the world aligned components of a selection bounding box.
F32 LLViewerMediaFocus::getBBoxAspectRatio(const LLBBox& bbox, const LLVector3& normal, F32* height, F32* width, F32* depth)
{
	// Convert the selection normal and an up vector to local coordinate space of the bbox
	LLVector3 local_normal = bbox.agentToLocalBasis(normal);
	LLVector3 z_vec = bbox.agentToLocalBasis(LLVector3(0.0f, 0.0f, 1.0f));
	
	LLVector3 comp1(0.f,0.f,0.f);
	LLVector3 comp2(0.f,0.f,0.f);
	LLVector3 bbox_max = bbox.getExtentLocal();
	F32 dot1 = 0.f;
	F32 dot2 = 0.f;

	// The largest component of the localized normal vector is the depth component
	// meaning that the other two are the legs of the rectangle.
	local_normal.abs();
	if(local_normal.mV[VX] > local_normal.mV[VY])
	{
		if(local_normal.mV[VX] > local_normal.mV[VZ])
		{
			// Use the y and z comps
			comp1.mV[VY] = bbox_max.mV[VY];
			comp2.mV[VZ] = bbox_max.mV[VZ];
			*depth = bbox_max.mV[VX];
		}
		else
		{
			// Use the x and y comps
			comp1.mV[VY] = bbox_max.mV[VY];
			comp2.mV[VZ] = bbox_max.mV[VZ];
			*depth = bbox_max.mV[VZ];
		}
	}
	else if(local_normal.mV[VY] > local_normal.mV[VZ])
	{
		// Use the x and z comps
		comp1.mV[VX] = bbox_max.mV[VX];
		comp2.mV[VZ] = bbox_max.mV[VZ];
		*depth = bbox_max.mV[VY];
	}
	else
	{
		// Use the x and y comps
		comp1.mV[VY] = bbox_max.mV[VY];
		comp2.mV[VZ] = bbox_max.mV[VZ];
		*depth = bbox_max.mV[VX];
	}
	
	// The height is the vector closest to vertical in the bbox coordinate space (highest dot product value)
	dot1 = comp1 * z_vec;
	dot2 = comp2 * z_vec;
	if(fabs(dot1) > fabs(dot2))
	{
		*height = comp1.length();
		*width = comp2.length();
	}
	else
	{
		*height = comp2.length();
		*width = comp1.length();
	}

	// Return the aspect ratio.
	return *width / *height;
}

bool LLViewerMediaFocus::isFocusedOnFace(LLPointer<LLViewerObject> objectp, S32 face)
{
	return objectp->getID() == mFocusedObjectID && face == mFocusedObjectFace;
}

bool LLViewerMediaFocus::isHoveringOverFace(LLPointer<LLViewerObject> objectp, S32 face)
{
	return objectp->getID() == mHoverObjectID && face == mHoverObjectFace;
}


LLViewerMediaImpl* LLViewerMediaFocus::getFocusedMediaImpl()
{
	return LLViewerMedia::getMediaImplFromTextureID(mFocusedImplID);
}

LLViewerObject* LLViewerMediaFocus::getFocusedObject()
{
	return gObjectList.findObject(mFocusedObjectID);
}

LLViewerMediaImpl* LLViewerMediaFocus::getHoverMediaImpl()
{
	return LLViewerMedia::getMediaImplFromTextureID(mHoverImplID);
}

LLViewerObject* LLViewerMediaFocus::getHoverObject()
{
	return gObjectList.findObject(mHoverObjectID);
}

void LLViewerMediaFocus::focusZoomOnMedia(LLUUID media_id)
{
	LLViewerMediaImpl* impl = LLViewerMedia::getMediaImplFromTextureID(media_id);
	
	if(impl)
	{	
		// Get the first object from the media impl's object list.  This is completely arbitrary, but should suffice.
		LLVOVolume *obj = impl->getSomeObject();
		if(obj)
		{
			// This media is attached to at least one object.  Figure out which face it's on.
			S32 face = obj->getFaceIndexWithMediaImpl(impl, -1);
			
			// We don't have a proper pick normal here, and finding a face's real normal is... complicated.
			LLVector3 normal = obj->getApproximateFaceNormal(face);
			if(normal.isNull())
			{
				// If that didn't work, use the inverse of the camera "look at" axis, which should keep the camera pointed in the same direction.
//				llinfos << "approximate face normal invalid, using camera direction." << llendl;
				normal = LLViewerCamera::getInstance()->getAtAxis();
				normal *= (F32)-1.0f;
			}
			
			// Attempt to focus/zoom on that face.
			setFocusFace(obj, face, impl, normal);
			
			if(mMediaControls.get())
			{
				mMediaControls.get()->resetZoomLevel();
				mMediaControls.get()->nextZoomLevel();
			}
		}
	}
}

LLUUID LLViewerMediaFocus::getControlsMediaID()
{
	if(getFocusedMediaImpl())
	{
		return mFocusedImplID;
	}
	else if(getHoverMediaImpl())
	{
		return mHoverImplID;
	}
	
	return LLUUID::null;
}