/**
* @file LLVirtualTrackball.cpp
* @author Andrey Lihatskiy
* @brief Implementation for LLVirtualTrackball
*
* $LicenseInfo:firstyear=2001&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2018, 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$
*/

// A control for positioning the sun and the moon in the celestial sphere.

#include "linden_common.h"
#include "llvirtualtrackball.h"
#include "llstring.h"
#include "llrect.h"
#include "lluictrlfactory.h"
#include "llrender.h"

// Globals
static LLDefaultChildRegistry::Register<LLVirtualTrackball> register_virtual_trackball("sun_moon_trackball");

const LLVector3 VectorZero(1.0f, 0.0f, 0.0f);

LLVirtualTrackball::Params::Params()
  : border("border"),
    image_moon_back("image_moon_back"),
    image_moon_front("image_moon_front"),
    image_sphere("image_sphere"),
    image_sun_back("image_sun_back"),
    image_sun_front("image_sun_front"),
    btn_rotate_top("button_rotate_top"),
    btn_rotate_bottom("button_rotate_bottom"),
    btn_rotate_left("button_rotate_left"),
    btn_rotate_right("button_rotate_right"),
    thumb_mode("thumb_mode"),
    lbl_N("labelN"),
    lbl_S("labelS"),
    lbl_W("labelW"),
    lbl_E("labelE"),
    increment_angle_mouse("increment_angle_mouse", 0.5f),
    increment_angle_btn("increment_angle_btn", 3.0f)
{
}

LLVirtualTrackball::LLVirtualTrackball(const LLVirtualTrackball::Params& p)
  : LLUICtrl(p),
    mImgMoonBack(p.image_moon_back),
    mImgMoonFront(p.image_moon_front),
    mImgSunBack(p.image_sun_back),
    mImgSunFront(p.image_sun_front),
    mImgSphere(p.image_sphere),
    mThumbMode(p.thumb_mode() == "moon" ? ThumbMode::MOON : ThumbMode::SUN),
    mIncrementMouse(DEG_TO_RAD * p.increment_angle_mouse()),
    mIncrementBtn(DEG_TO_RAD * p.increment_angle_btn())
{
    LLRect border_rect = getLocalRect();
    S32 centerX = border_rect.getCenterX();
    S32 centerY = border_rect.getCenterY();
    U32 btn_size = 32; // width & height
    U32 axis_offset_lt = 16; // offset from the axis for left/top sides
    U32 axis_offset_rb = btn_size - axis_offset_lt; //  and for right/bottom

    LLViewBorder::Params border = p.border;
    border.rect(border_rect);
    mBorder = LLUICtrlFactory::create<LLViewBorder>(border);
    addChild(mBorder);


    LLButton::Params btn_rt = p.btn_rotate_top;
    btn_rt.rect(LLRect(centerX - axis_offset_lt, border_rect.mTop, centerX + axis_offset_rb, border_rect.mTop - btn_size));
    btn_rt.click_callback.function(boost::bind(&LLVirtualTrackball::onRotateTopClick, this));
    btn_rt.mouse_held_callback.function(boost::bind(&LLVirtualTrackball::onRotateTopClick, this));
    btn_rt.mouseenter_callback.function(boost::bind(&LLVirtualTrackball::onRotateTopMouseEnter, this));
    mBtnRotateTop = LLUICtrlFactory::create<LLButton>(btn_rt);
    addChild(mBtnRotateTop);

    LLTextBox::Params lbl_N = p.lbl_N;
    LLRect rect_N = btn_rt.rect;
    //rect_N.translate(btn_rt.rect().getWidth(), 0);
    lbl_N.rect = rect_N;
    lbl_N.initial_value(lbl_N.label());
    mLabelN = LLUICtrlFactory::create<LLTextBox>(lbl_N);
    addChild(mLabelN);


    LLButton::Params btn_rr = p.btn_rotate_right;
    btn_rr.rect(LLRect(border_rect.mRight - btn_size, centerY + axis_offset_lt, border_rect.mRight, centerY - axis_offset_rb));
    btn_rr.click_callback.function(boost::bind(&LLVirtualTrackball::onRotateRightClick, this));
    btn_rr.mouse_held_callback.function(boost::bind(&LLVirtualTrackball::onRotateRightClick, this));
    btn_rr.mouseenter_callback.function(boost::bind(&LLVirtualTrackball::onRotateRightMouseEnter, this));
    mBtnRotateRight = LLUICtrlFactory::create<LLButton>(btn_rr);
    addChild(mBtnRotateRight);

    LLTextBox::Params lbl_E = p.lbl_E;
    LLRect rect_E = btn_rr.rect;
    //rect_E.translate(0, -1 * btn_rr.rect().getHeight());
    lbl_E.rect = rect_E;
    lbl_E.initial_value(lbl_E.label());
    mLabelE = LLUICtrlFactory::create<LLTextBox>(lbl_E);
    addChild(mLabelE);


    LLButton::Params btn_rb = p.btn_rotate_bottom;
    btn_rb.rect(LLRect(centerX - axis_offset_lt, border_rect.mBottom + btn_size, centerX + axis_offset_rb, border_rect.mBottom));
    btn_rb.click_callback.function(boost::bind(&LLVirtualTrackball::onRotateBottomClick, this));
    btn_rb.mouse_held_callback.function(boost::bind(&LLVirtualTrackball::onRotateBottomClick, this));
    btn_rb.mouseenter_callback.function(boost::bind(&LLVirtualTrackball::onRotateBottomMouseEnter, this));
    mBtnRotateBottom = LLUICtrlFactory::create<LLButton>(btn_rb);
    addChild(mBtnRotateBottom);

    LLTextBox::Params lbl_S = p.lbl_S;
    LLRect rect_S = btn_rb.rect;
    //rect_S.translate(btn_rb.rect().getWidth(), 0);
    lbl_S.rect = rect_S;
    lbl_S.initial_value(lbl_S.label());
    mLabelS = LLUICtrlFactory::create<LLTextBox>(lbl_S);
    addChild(mLabelS);


    LLButton::Params btn_rl = p.btn_rotate_left;
    btn_rl.rect(LLRect(border_rect.mLeft, centerY + axis_offset_lt, border_rect.mLeft + btn_size, centerY - axis_offset_rb));
    btn_rl.click_callback.function(boost::bind(&LLVirtualTrackball::onRotateLeftClick, this));
    btn_rl.mouse_held_callback.function(boost::bind(&LLVirtualTrackball::onRotateLeftClick, this));
    btn_rl.mouseenter_callback.function(boost::bind(&LLVirtualTrackball::onRotateLeftMouseEnter, this));
    mBtnRotateLeft = LLUICtrlFactory::create<LLButton>(btn_rl);
    addChild(mBtnRotateLeft);

    LLTextBox::Params lbl_W = p.lbl_W;
    LLRect rect_W = btn_rl.rect;
    //rect_W.translate(0, -1* btn_rl.rect().getHeight());
    lbl_W.rect = rect_W;
    lbl_W.initial_value(lbl_W.label());
    mLabelW = LLUICtrlFactory::create<LLTextBox>(lbl_W);
    addChild(mLabelW);


    LLPanel::Params touch_area;
    touch_area.rect = LLRect(centerX - mImgSphere->getWidth() / 2,
                             centerY + mImgSphere->getHeight() / 2,
                             centerX + mImgSphere->getWidth() / 2,
                             centerY - mImgSphere->getHeight() / 2);
    mTouchArea = LLUICtrlFactory::create<LLPanel>(touch_area);
    addChild(mTouchArea);
}

LLVirtualTrackball::~LLVirtualTrackball()
{
}

bool LLVirtualTrackball::postBuild()
{
    return true;
}


void LLVirtualTrackball::drawThumb(S32 x, S32 y, ThumbMode mode, bool upperHemi)
{
    LLUIImage* thumb;
    if (mode == ThumbMode::SUN)
    {
        if (upperHemi)
        {
            thumb = mImgSunFront;
        }
        else
        {
            thumb = mImgSunBack;
        }
    }
    else
    {
        if (upperHemi)
        {
            thumb = mImgMoonFront;
        }
        else
        {
            thumb = mImgMoonBack;
        }
    }
    thumb->draw(LLRect(x - thumb->getWidth() / 2,
                       y + thumb->getHeight() / 2,
                       x + thumb->getWidth() / 2,
                       y - thumb->getHeight() / 2));
}

bool LLVirtualTrackball::pointInTouchCircle(S32 x, S32 y) const
{
    S32 centerX = mTouchArea->getRect().getCenterX();
    S32 centerY = mTouchArea->getRect().getCenterY();

    bool in_circle = pow(x - centerX, 2) + pow(y - centerY, 2) <= pow(mTouchArea->getRect().getWidth() / 2, 2);
    return in_circle;
}

void LLVirtualTrackball::draw()
{
    LLVector3 draw_point = VectorZero * mValue;

    S32 halfwidth = mTouchArea->getRect().getWidth() / 2;
    S32 halfheight = mTouchArea->getRect().getHeight() / 2;
    draw_point.mV[VX] = (draw_point.mV[VX] + 1.0f) * halfwidth + mTouchArea->getRect().mLeft;
    draw_point.mV[VY] = (draw_point.mV[VY] + 1.0f) * halfheight + mTouchArea->getRect().mBottom;
    bool upper_hemisphere = (draw_point.mV[VZ] >= 0.f);

    mImgSphere->draw(mTouchArea->getRect(), upper_hemisphere ? UI_VERTEX_COLOR : UI_VERTEX_COLOR % 0.5f);
    drawThumb((S32)draw_point.mV[VX], (S32)draw_point.mV[VY], mThumbMode, upper_hemisphere);


    if (LLView::sDebugRects)
    {
        gGL.color4fv(LLColor4::red.mV);
        gl_circle_2d((F32)mTouchArea->getRect().getCenterX(), (F32)mTouchArea->getRect().getCenterY(), (F32)mImgSphere->getWidth() / 2.f, 60, false);
        gl_circle_2d(draw_point.mV[VX], draw_point.mV[VY], (F32)mImgSunFront->getWidth() / 2.f, 12, false);
    }

    // hide the direction labels when disabled
    bool enabled = isInEnabledChain();
    mLabelN->setVisible(enabled);
    mLabelE->setVisible(enabled);
    mLabelS->setVisible(enabled);
    mLabelW->setVisible(enabled);

    LLView::draw();
}

void LLVirtualTrackball::onRotateTopClick()
{
    if (getEnabled())
    {
        LLQuaternion delta;
        delta.setAngleAxis(mIncrementBtn, 1, 0, 0);
        mValue *= delta;
        setValueAndCommit(mValue);

        make_ui_sound("UISndClick");
    }
}

void LLVirtualTrackball::onRotateBottomClick()
{
    if (getEnabled())
    {
        LLQuaternion delta;
        delta.setAngleAxis(mIncrementBtn, -1, 0, 0);
        mValue *= delta;
        setValueAndCommit(mValue);

        make_ui_sound("UISndClick");
    }
}

void LLVirtualTrackball::onRotateLeftClick()
{
    if (getEnabled())
    {
        LLQuaternion delta;
        delta.setAngleAxis(mIncrementBtn, 0, 1, 0);
        mValue *= delta;
        setValueAndCommit(mValue);

        make_ui_sound("UISndClick");
    }
}

void LLVirtualTrackball::onRotateRightClick()
{
    if (getEnabled())
    {
        LLQuaternion delta;
        delta.setAngleAxis(mIncrementBtn, 0, -1, 0);
        mValue *= delta;
        setValueAndCommit(mValue);

        make_ui_sound("UISndClick");
    }
}

void LLVirtualTrackball::onRotateTopMouseEnter()
{
    mBtnRotateTop->setHighlight(true);
}

void LLVirtualTrackball::onRotateBottomMouseEnter()
{
    mBtnRotateBottom->setHighlight(true);
}

void LLVirtualTrackball::onRotateLeftMouseEnter()
{
    mBtnRotateLeft->setHighlight(true);
}

void LLVirtualTrackball::onRotateRightMouseEnter()
{
    mBtnRotateRight->setHighlight(true);
}

void LLVirtualTrackball::setValue(const LLSD& value)
{
    if (value.isArray() && value.size() == 4)
    {
        mValue.setValue(value);
    }
}

void LLVirtualTrackball::setRotation(const LLQuaternion &value)
{
    mValue = value;
}

void LLVirtualTrackball::setValue(F32 x, F32 y, F32 z, F32 w)
{
    mValue.set(x, y, z, w);
}

void LLVirtualTrackball::setValueAndCommit(const LLQuaternion &value)
{
    mValue = value;
    onCommit();
}

LLSD LLVirtualTrackball::getValue() const
{
    return mValue.getValue();
}

LLQuaternion LLVirtualTrackball::getRotation() const
{
    return mValue;
}

// static
void LLVirtualTrackball::getAzimuthAndElevation(const LLQuaternion &quat, F32 &azimuth, F32 &elevation)
{
    // LLQuaternion has own function to get azimuth, but it doesn't appear to return correct values (meant for 2d?)
    LLVector3 point = VectorZero * quat;

    if (!is_approx_zero(point.mV[VX]) || !is_approx_zero(point.mV[VY]))
    {
        azimuth = atan2f(point.mV[VX], point.mV[VY]);
    }
    else
    {
        azimuth = 0;
    }

    azimuth -= F_PI_BY_TWO;

    if (azimuth < 0)
    {
        azimuth += F_PI * 2;
    }

    // while vector is '1', F32 is not sufficiently precise and we can get
    // values like 1.0000012 which will result in -90deg angle instead of 90deg
    F32 z = llclamp(point.mV[VZ], -1.f, 1.f);
    elevation = asin(z); // because VectorZero's length is '1'
}

// static
void LLVirtualTrackball::getAzimuthAndElevationDeg(const LLQuaternion &quat, F32 &azimuth, F32 &elevation)
{
    getAzimuthAndElevation(quat, azimuth, elevation);
    azimuth *= RAD_TO_DEG;
    elevation *= RAD_TO_DEG;
}

bool LLVirtualTrackball::handleHover(S32 x, S32 y, MASK mask)
{
    if (hasMouseCapture())
    {
        if (mDragMode == DRAG_SCROLL)
        { // trackball (move to roll) mode
            LLQuaternion delta;

            F32 rotX = (F32)(x - mPrevX);
            F32 rotY = (F32)(y - mPrevY);

            if (abs(rotX) > 1)
            {
                F32 direction = (rotX < 0) ? -1.f : 1.f;
                delta.setAngleAxis(mIncrementMouse * abs(rotX), 0.f, direction, 0.f);  // changing X - rotate around Y axis
                mValue *= delta;
            }

            if (abs(rotY) > 1)
            {
                F32 direction = (rotY < 0) ? 1.f : -1.f; // reverse for Y (value increases from bottom to top)
                delta.setAngleAxis(mIncrementMouse * abs(rotY), direction, 0.f, 0.f);  // changing Y - rotate around X axis
                mValue *= delta;
            }
        }
        else
        { // set on click mode
            if (!pointInTouchCircle(x, y))
            {
                return true; // don't drag outside the circle
            }

            F32 radius = (F32)mTouchArea->getRect().getWidth() / 2.f;
            F32 xx = (F32)(x - mTouchArea->getRect().getCenterX());
            F32 yy = (F32)(y - mTouchArea->getRect().getCenterY());
            F32 dist = (F32)(sqrt(pow(xx, 2) + pow(yy, 2)));

            F32 azimuth = llclamp(acosf(xx / dist), 0.0f, F_PI);
            F32 altitude = llclamp(acosf(dist / radius), 0.0f, F_PI_BY_TWO);

            if (yy < 0)
            {
                azimuth = F_TWO_PI - azimuth;
            }

            LLVector3 draw_point = VectorZero * mValue;
            if (draw_point.mV[VZ] >= 0.f)
            {
                if (is_approx_zero(altitude)) // don't change the hemisphere
                {
                    altitude = F_APPROXIMATELY_ZERO;
                }
                altitude *= -1;
            }

            mValue.setAngleAxis(altitude, 0, 1, 0);
            LLQuaternion az_quat;
            az_quat.setAngleAxis(azimuth, 0, 0, 1);
            mValue *= az_quat;
        }

        // we are doing a lot of F32 mathematical operations with loss of precision,
        // re-normalize to compensate
        mValue.normalize();

        mPrevX = x;
        mPrevY = y;
        onCommit();
    }
    return true;
}

bool LLVirtualTrackball::handleMouseUp(S32 x, S32 y, MASK mask)
{
    if (hasMouseCapture())
    {
        mPrevX = 0;
        mPrevY = 0;
        gFocusMgr.setMouseCapture(NULL);
        make_ui_sound("UISndClickRelease");
    }
    return LLView::handleMouseUp(x, y, mask);
}

bool LLVirtualTrackball::handleMouseDown(S32 x, S32 y, MASK mask)
{
    if (pointInTouchCircle(x, y))
    {
        mPrevX = x;
        mPrevY = y;
        gFocusMgr.setMouseCapture(this);
        mDragMode = (mask == MASK_CONTROL) ? DRAG_SCROLL : DRAG_SET;
        make_ui_sound("UISndClick");
    }
    return LLView::handleMouseDown(x, y, mask);
}

bool LLVirtualTrackball::handleRightMouseDown(S32 x, S32 y, MASK mask)
{
    if (pointInTouchCircle(x, y))
    {

        //make_ui_sound("UISndClick");
    }
    return LLView::handleRightMouseDown(x, y, mask);
}

bool LLVirtualTrackball::handleKeyHere(KEY key, MASK mask)
{
    bool handled = false;
    switch (key)
    {
    case KEY_DOWN:
        onRotateTopClick();
        handled = true;
        break;
    case KEY_LEFT:
        onRotateRightClick();
        handled = true;
        break;
    case KEY_UP:
        onRotateBottomClick();
        handled = true;
        break;
    case KEY_RIGHT:
        onRotateLeftClick();
        handled = true;
        break;
    default:
        break;
    }
    return handled;
}