/**
 * @file llviewermediafocus.cpp
 * @brief Governs focus on Media prims
 *
 * $LicenseInfo:firstyear=2003&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#include "llviewerprecompiledheaders.h"

#include "llviewermediafocus.h"

//LLViewerMediaFocus
#include "llviewerobjectlist.h"
#include "llpanelprimmediacontrols.h"
#include "llpluginclassmedia.h"
#include "llagent.h"
#include "llagentcamera.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"
#include "llhelp.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.
    gFocusMgr.removeKeyboardFocusWithoutCallback(this);
}

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;
        mPrevFocusedImplID = LLUUID::null;
        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.
            LL_WARNS() << "Can't find media entry for focused face" << LL_ENDL;
        }

        media_impl->focus(true);
        gFocusMgr.setKeyboardFocus(this);
        LLViewerMediaImpl* impl = getFocusedMediaImpl();
        if (impl)
        {
            LLEditMenuHandler::gEditMenuHandler = impl;
        }

        // 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 && !static_cast<bool>(parcel->getMediaPreventCameraZoom()))
            {
                // Zoom in on this face
                mMediaControls.get()->resetZoomLevel(false);
                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(hasFocus())
        {
            gFocusMgr.setKeyboardFocus(NULL);
        }

        LLViewerMediaImpl* impl = getFocusedMediaImpl();
        if (LLEditMenuHandler::gEditMenuHandler == impl)
        {
            LLEditMenuHandler::gEditMenuHandler = NULL;
        }


        mFocusedImplID = LLUUID::null;
        if (objectp.notNull())
        {
            // Still record the focused object...it may mean we need to load media data.
            // This will aid us in determining this object is "important enough"
            mFocusedObjectID = objectp->getID();
            mFocusedObjectFace = face;
        }
        else {
            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
LLVector3d LLViewerMediaFocus::setCameraZoom(LLViewerObject* object, LLVector3 normal, F32 padding_factor, bool zoom_in_only)
{
    LLVector3d camera_pos;
    if (object)
    {
        gAgentCamera.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();

        LL_DEBUGS() << "normal = " << normal << ", aspect_ratio = " << aspect_ratio << ", camera_aspect = " << camera_aspect << LL_ENDL;

        // 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.5f * padding_factor / tanf(angle_of_view * 0.5f );

            LL_DEBUGS() << "using width (" << width << "), angle_of_view = " << angle_of_view << ", distance = " << distance << LL_ENDL;
        }
        else
        {
            angle_of_view = llmax(0.1f, LLViewerCamera::getInstance()->getView());
            distance = height * 0.5f * padding_factor / tanf(angle_of_view * 0.5f );

            LL_DEBUGS() << "using height (" << height << "), angle_of_view = " << angle_of_view << ", distance = " << distance << LL_ENDL;
        }

        distance += depth * 0.5f;

        // Finally animate the camera to this new position and focal point
        LLVector3d 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(gAgentCamera.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;
        }

        // If we are not allowing zooming out and the old camera position is closer to
        // the center then the new intended camera position, don't move camera and return
        if (zoom_in_only &&
            (dist_vec_squared(gAgentCamera.getCameraPositionGlobal(), target_pos) < dist_vec_squared(camera_pos, target_pos)))
        {
            return camera_pos;
        }

        gAgentCamera.setCameraPosAndFocusGlobal(camera_pos, target_pos, object->getID() );

    }
    else
    {
        // If we have no object, focus back on the avatar.
        gAgentCamera.setFocusOnAvatar(true, ANIMATE);
    }
    return camera_pos;
}
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_ESCAPE == key)
        {
            // Reset camera zoom in this case.
            if(mFocusedImplID.notNull())
            {
                if(mMediaControls.get())
                {
                    mMediaControls.get()->resetZoomLevel(true);
                }
            }

            clearFocus();
        }
    }

    return true;
}

bool LLViewerMediaFocus::handleKeyUp(KEY key, MASK mask, bool called_from_parent)
{
    LLViewerMediaImpl* media_impl = getFocusedMediaImpl();
    if (media_impl)
    {
        media_impl->handleKeyUpHere(key, mask);
    }
    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(const LLVector2& texture_coords, S32 clicks_x, S32 clicks_y)
{
    bool retval = false;
    LLViewerMediaImpl* media_impl = getFocusedMediaImpl();
    if (media_impl && media_impl->hasMedia())
    {
        media_impl->scrollWheel(texture_coords, clicks_x, clicks_y, gKeyboard->currentMask(true));
        retval = true;
    }
    return retval;
}

bool LLViewerMediaFocus::handleScrollWheel(S32 x, S32 y, S32 clicks_x, S32 clicks_y)
{
    bool retval = false;
    LLViewerMediaImpl* media_impl = getFocusedMediaImpl();
    if(media_impl && media_impl->hasMedia())
    {
        media_impl->scrollWheel(x, y, clicks_x, clicks_y, gKeyboard->currentMask(true));
        retval = true;
    }
    return retval;
}

void LLViewerMediaFocus::update()
{
    if(mFocusedImplID.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.
                mPrevFocusedImplID = mFocusedImplID;
                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;

    if(!media_impl || !viewer_object)
    {
        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;

    LL_DEBUGS() << "bounding box local size = " << bbox_max << ", local_normal = " << local_normal << LL_ENDL;

    // 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();

    // Using temporary variables for these makes the logic a bit more readable.
    bool XgtY = (local_normal.mV[VX] > local_normal.mV[VY]);
    bool XgtZ = (local_normal.mV[VX] > local_normal.mV[VZ]);
    bool YgtZ = (local_normal.mV[VY] > local_normal.mV[VZ]);

    if(XgtY && XgtZ)
    {
        LL_DEBUGS() << "x component of normal is longest, using y and z" << LL_ENDL;
        comp1.mV[VY] = bbox_max.mV[VY];
        comp2.mV[VZ] = bbox_max.mV[VZ];
        *depth = bbox_max.mV[VX];
    }
    else if(!XgtY && YgtZ)
    {
        LL_DEBUGS() << "y component of normal is longest, using x and z" << LL_ENDL;
        comp1.mV[VX] = bbox_max.mV[VX];
        comp2.mV[VZ] = bbox_max.mV[VZ];
        *depth = bbox_max.mV[VY];
    }
    else
    {
        LL_DEBUGS() << "z component of normal is longest, using x and y" << LL_ENDL;
        comp1.mV[VX] = bbox_max.mV[VX];
        comp2.mV[VY] = bbox_max.mV[VY];
        *depth = bbox_max.mV[VZ];
    }

    // 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();

        LL_DEBUGS() << "comp1 = " << comp1 << ", height = " << *height << LL_ENDL;
        LL_DEBUGS() << "comp2 = " << comp2 << ", width = " << *width << LL_ENDL;
    }
    else
    {
        *height = comp2.length();
        *width = comp1.length();

        LL_DEBUGS() << "comp2 = " << comp2 << ", height = " << *height << LL_ENDL;
        LL_DEBUGS() << "comp1 = " << comp1 << ", width = " << *width << LL_ENDL;
    }

    LL_DEBUGS() << "returning " << (*width / *height) << LL_ENDL;

    // 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::getInstance()->getMediaImplFromTextureID(mFocusedImplID);
}

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

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

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

void LLViewerMediaFocus::focusZoomOnMedia(LLUUID media_id)
{
    LLViewerMediaImpl* impl = LLViewerMedia::getInstance()->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.
//              LL_INFOS() << "approximate face normal invalid, using camera direction." << LL_ENDL;
                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();
            }
        }
    }
}

void LLViewerMediaFocus::unZoom()
{
    if(mMediaControls.get())
    {
        mMediaControls.get()->resetZoomLevel();
    }
}

bool LLViewerMediaFocus::isZoomed() const
{
    return (mMediaControls.get() && mMediaControls.get()->getZoomLevel() != LLPanelPrimMediaControls::ZOOM_NONE);
}

bool LLViewerMediaFocus::isZoomedOnMedia(LLUUID media_id)
{
    if (isZoomed())
    {
        return (mFocusedImplID == media_id) || (mPrevFocusedImplID == media_id);
    }
    return false;
}

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

    return LLUUID::null;
}

bool LLViewerMediaFocus::wantsKeyUpKeyDown() const
{
    return true;
}

bool LLViewerMediaFocus::wantsReturnKey() const
{
    return true;
}