/**
 * @file llscrollbar.cpp
 * @brief Scrollbar UI widget
 *
 * $LicenseInfo:firstyear=2001&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 "linden_common.h"

#include "llscrollbar.h"

#include "llmath.h"
#include "lltimer.h"
#include "v3color.h"

#include "llbutton.h"
#include "llcriticaldamp.h"
#include "llkeyboard.h"
#include "llui.h"
#include "llfocusmgr.h"
#include "llwindow.h"
#include "llcontrol.h"
#include "llrender.h"
#include "lluictrlfactory.h"

static LLDefaultChildRegistry::Register<LLScrollbar> register_scrollbar("scroll_bar");

LLScrollbar::Params::Params()
:   orientation ("orientation", HORIZONTAL),
    doc_size ("doc_size", 0),
    doc_pos ("doc_pos", 0),
    page_size ("page_size", 0),
    step_size ("step_size", 1),
    thumb_image_vertical("thumb_image_vertical"),
    thumb_image_horizontal("thumb_image_horizontal"),
    track_image_vertical("track_image_vertical"),
    track_image_horizontal("track_image_horizontal"),
    track_color("track_color"),
    thumb_color("thumb_color"),
    thickness("thickness"),
    up_button("up_button"),
    down_button("down_button"),
    left_button("left_button"),
    right_button("right_button"),
    bg_visible("bg_visible", false),
    bg_color("bg_color", LLColor4::black)
{}

LLScrollbar::LLScrollbar(const Params & p)
:       LLUICtrl(p),
        mChangeCallback( p.change_callback() ),
        mOrientation( p.orientation ),
        mDocSize( p.doc_size ),
        mDocPos( p.doc_pos ),
        mPageSize( p.page_size ),
        mStepSize( p.step_size ),
        mDocChanged(FALSE),
        mDragStartX( 0 ),
        mDragStartY( 0 ),
        mHoverGlowStrength(0.15f),
        mCurGlowStrength(0.f),
        mTrackColor( p.track_color() ),
        mThumbColor ( p.thumb_color() ),
        mThumbImageV(p.thumb_image_vertical),
        mThumbImageH(p.thumb_image_horizontal),
        mTrackImageV(p.track_image_vertical),
        mTrackImageH(p.track_image_horizontal),
        mThickness(p.thickness.isProvided() ? p.thickness : LLUI::getInstance()->mSettingGroups["config"]->getS32("UIScrollbarSize")),
        mBGVisible(p.bg_visible),
        mBGColor(p.bg_color)
{
    updateThumbRect();

    // Page up and page down buttons
    LLRect line_up_rect;
    LLRect line_down_rect;

    if( VERTICAL == mOrientation )
    {
        line_up_rect.setLeftTopAndSize( 0, getRect().getHeight(), mThickness, mThickness );
        line_down_rect.setOriginAndSize( 0, 0, mThickness, mThickness );
    }
    else // HORIZONTAL
    {
        line_up_rect.setOriginAndSize( 0, 0, mThickness, mThickness );
        line_down_rect.setOriginAndSize( getRect().getWidth() - mThickness, 0, mThickness, mThickness );
    }

    LLButton::Params up_btn(mOrientation == VERTICAL ? p.up_button : p.left_button);
    up_btn.name(std::string("Line Up"));
    up_btn.rect(line_up_rect);
    up_btn.click_callback.function(boost::bind(&LLScrollbar::onLineUpBtnPressed, this, _2));
    up_btn.mouse_held_callback.function(boost::bind(&LLScrollbar::onLineUpBtnPressed, this, _2));
    up_btn.tab_stop(false);
    up_btn.follows.flags = (mOrientation == VERTICAL ? (FOLLOWS_RIGHT | FOLLOWS_TOP) : (FOLLOWS_LEFT | FOLLOWS_BOTTOM));

    addChild(LLUICtrlFactory::create<LLButton>(up_btn));

    LLButton::Params down_btn(mOrientation == VERTICAL ? p.down_button : p.right_button);
    down_btn.name(std::string("Line Down"));
    down_btn.rect(line_down_rect);
    down_btn.follows.flags(FOLLOWS_RIGHT|FOLLOWS_BOTTOM);
    down_btn.click_callback.function(boost::bind(&LLScrollbar::onLineDownBtnPressed, this, _2));
    down_btn.mouse_held_callback.function(boost::bind(&LLScrollbar::onLineDownBtnPressed, this, _2));
    down_btn.tab_stop(false);

    addChild(LLUICtrlFactory::create<LLButton>(down_btn));
}


LLScrollbar::~LLScrollbar()
{
    // Children buttons killed by parent class
}

void LLScrollbar::setDocParams( S32 size, S32 pos )
{
    mDocSize = size;
    setDocPos(pos);
    mDocChanged = TRUE;

    updateThumbRect();
}

// returns true if document position really changed
bool LLScrollbar::setDocPos(S32 pos, BOOL update_thumb)
{
    pos = llclamp(pos, 0, getDocPosMax());
    if (pos != mDocPos)
    {
        mDocPos = pos;
        mDocChanged = TRUE;

        if( mChangeCallback )
        {
            mChangeCallback( mDocPos, this );
        }

        if( update_thumb )
        {
            updateThumbRect();
        }
        return true;
    }
    return false;
}

void LLScrollbar::setDocSize(S32 size)
{
    if (size != mDocSize)
    {
        mDocSize = size;
        setDocPos(mDocPos);
        mDocChanged = TRUE;

        updateThumbRect();
    }
}

void LLScrollbar::setPageSize( S32 page_size )
{
    if (page_size != mPageSize)
    {
        mPageSize = page_size;
        setDocPos(mDocPos);
        mDocChanged = TRUE;

        updateThumbRect();
    }
}

bool LLScrollbar::isAtBeginning() const
{
    return mDocPos == 0;
}

bool LLScrollbar::isAtEnd() const
{
    return mDocPos == getDocPosMax();
}


void LLScrollbar::updateThumbRect()
{
//  llassert( 0 <= mDocSize );
//  llassert( 0 <= mDocPos && mDocPos <= getDocPosMax() );

    const S32 THUMB_MIN_LENGTH = 16;

    S32 window_length = (mOrientation == LLScrollbar::HORIZONTAL) ? getRect().getWidth() : getRect().getHeight();
    S32 thumb_bg_length = llmax(0, window_length - 2 * mThickness);
    S32 visible_lines = llmin( mDocSize, mPageSize );
    S32 thumb_length = mDocSize ? llmin(llmax( visible_lines * thumb_bg_length / mDocSize, THUMB_MIN_LENGTH), thumb_bg_length) : thumb_bg_length;

    S32 variable_lines = mDocSize - visible_lines;

    if( mOrientation == LLScrollbar::VERTICAL )
    {
        S32 thumb_start_max = thumb_bg_length + mThickness;
        S32 thumb_start_min = mThickness + THUMB_MIN_LENGTH;
        S32 thumb_start = variable_lines ? llmin( llmax(thumb_start_max - (mDocPos * (thumb_bg_length - thumb_length)) / variable_lines, thumb_start_min), thumb_start_max ) : thumb_start_max;

        mThumbRect.mLeft =  0;
        mThumbRect.mTop = thumb_start;
        mThumbRect.mRight = mThickness;
        mThumbRect.mBottom = thumb_start - thumb_length;
    }
    else
    {
        // Horizontal
        S32 thumb_start_max = thumb_bg_length + mThickness - thumb_length;
        S32 thumb_start_min = mThickness;
        S32 thumb_start = variable_lines ? llmin(llmax( thumb_start_min + (mDocPos * (thumb_bg_length - thumb_length)) / variable_lines, thumb_start_min), thumb_start_max ) : thumb_start_min;

        mThumbRect.mLeft = thumb_start;
        mThumbRect.mTop = mThickness;
        mThumbRect.mRight = thumb_start + thumb_length;
        mThumbRect.mBottom = 0;
    }
}

BOOL LLScrollbar::handleMouseDown(S32 x, S32 y, MASK mask)
{
    // Check children first
    BOOL handled_by_child = LLView::childrenHandleMouseDown(x, y, mask) != NULL;
    if( !handled_by_child )
    {
        if( mThumbRect.pointInRect(x,y) )
        {
            // Start dragging the thumb
            // No handler needed for focus lost since this clas has no state that depends on it.
            gFocusMgr.setMouseCapture( this );
            mDragStartX = x;
            mDragStartY = y;
            mOrigRect.mTop = mThumbRect.mTop;
            mOrigRect.mBottom = mThumbRect.mBottom;
            mOrigRect.mLeft = mThumbRect.mLeft;
            mOrigRect.mRight = mThumbRect.mRight;
            mLastDelta = 0;
        }
        else
        {
            if(
                ( (LLScrollbar::VERTICAL == mOrientation) && (mThumbRect.mTop < y) ) ||
                ( (LLScrollbar::HORIZONTAL == mOrientation) && (x < mThumbRect.mLeft) )
            )
            {
                // Page up
                pageUp(0);
            }
            else
            if(
                ( (LLScrollbar::VERTICAL == mOrientation) && (y < mThumbRect.mBottom) ) ||
                ( (LLScrollbar::HORIZONTAL == mOrientation) && (mThumbRect.mRight < x) )
            )
            {
                // Page down
                pageDown(0);
            }
        }
    }

    return TRUE;
}


BOOL LLScrollbar::handleHover(S32 x, S32 y, MASK mask)
{
    // Note: we don't bother sending the event to the children (the arrow buttons)
    // because they'll capture the mouse whenever they need hover events.

    BOOL handled = FALSE;
    if( hasMouseCapture() )
    {
        S32 height = getRect().getHeight();
        S32 width = getRect().getWidth();

        if( VERTICAL == mOrientation )
        {
//          S32 old_pos = mThumbRect.mTop;

            S32 delta_pixels = y - mDragStartY;
            if( mOrigRect.mBottom + delta_pixels < mThickness )
            {
                delta_pixels = mThickness - mOrigRect.mBottom - 1;
            }
            else
            if( mOrigRect.mTop + delta_pixels > height - mThickness )
            {
                delta_pixels = height - mThickness - mOrigRect.mTop + 1;
            }

            mThumbRect.mTop = mOrigRect.mTop + delta_pixels;
            mThumbRect.mBottom = mOrigRect.mBottom + delta_pixels;

            S32 thumb_length = mThumbRect.getHeight();
            S32 thumb_track_length = height - 2 * mThickness;


            if( delta_pixels != mLastDelta || mDocChanged)
            {
                // Note: delta_pixels increases as you go up.  mDocPos increases down (line 0 is at the top of the page).
                S32 usable_track_length = thumb_track_length - thumb_length;
                if( 0 < usable_track_length )
                {
                    S32 variable_lines = getDocPosMax();
                    S32 pos = mThumbRect.mTop;
                    F32 ratio = F32(pos - mThickness - thumb_length) / usable_track_length;

                    S32 new_pos = llclamp( S32(variable_lines - ratio * variable_lines + 0.5f), 0, variable_lines );
                    // Note: we do not call updateThumbRect() here.  Instead we let the thumb and the document go slightly
                    // out of sync (less than a line's worth) to make the thumb feel responsive.
                    changeLine( new_pos - mDocPos, FALSE );
                }
            }

            mLastDelta = delta_pixels;

        }
        else
        {
            // Horizontal
//          S32 old_pos = mThumbRect.mLeft;

            S32 delta_pixels = x - mDragStartX;

            if( mOrigRect.mLeft + delta_pixels < mThickness )
            {
                delta_pixels = mThickness - mOrigRect.mLeft - 1;
            }
            else
            if( mOrigRect.mRight + delta_pixels > width - mThickness )
            {
                delta_pixels = width - mThickness - mOrigRect.mRight + 1;
            }

            mThumbRect.mLeft = mOrigRect.mLeft + delta_pixels;
            mThumbRect.mRight = mOrigRect.mRight + delta_pixels;

            S32 thumb_length = mThumbRect.getWidth();
            S32 thumb_track_length = width - 2 * mThickness;

            if( delta_pixels != mLastDelta || mDocChanged)
            {
                // Note: delta_pixels increases as you go up.  mDocPos increases down (line 0 is at the top of the page).
                S32 usable_track_length = thumb_track_length - thumb_length;
                if( 0 < usable_track_length )
                {
                    S32 variable_lines = getDocPosMax();
                    S32 pos = mThumbRect.mLeft;
                    F32 ratio = F32(pos - mThickness) / usable_track_length;

                    S32 new_pos = llclamp( S32(ratio * variable_lines + 0.5f), 0, variable_lines);

                    // Note: we do not call updateThumbRect() here.  Instead we let the thumb and the document go slightly
                    // out of sync (less than a line's worth) to make the thumb feel responsive.
                    changeLine( new_pos - mDocPos, FALSE );
                }
            }

            mLastDelta = delta_pixels;
        }

        getWindow()->setCursor(UI_CURSOR_ARROW);
        LL_DEBUGS("UserInput") << "hover handled by " << getName() << " (active)" << LL_ENDL;
        handled = TRUE;
    }
    else
    {
        handled = childrenHandleHover( x, y, mask ) != NULL;
    }

    // Opaque
    if( !handled )
    {
        getWindow()->setCursor(UI_CURSOR_ARROW);
        LL_DEBUGS("UserInput") << "hover handled by " << getName() << " (inactive)"  << LL_ENDL;
        handled = TRUE;
    }

    mDocChanged = FALSE;
    return handled;
} // end handleHover


BOOL LLScrollbar::handleScrollWheel(S32 x, S32 y, S32 clicks)
{
    BOOL handled = changeLine( clicks * mStepSize, TRUE );
    return handled;
}

BOOL LLScrollbar::handleScrollHWheel(S32 x, S32 y, S32 clicks)
{
    BOOL handled = FALSE;
    if (LLScrollbar::HORIZONTAL == mOrientation)
    {
        handled = changeLine(clicks * mStepSize, TRUE);
    }
    return handled;
}

BOOL LLScrollbar::handleDragAndDrop(S32 x, S32 y, MASK mask, BOOL drop,
                                    EDragAndDropType cargo_type, void *cargo_data, EAcceptance *accept, std::string &tooltip_msg)
{
    // enable this to get drag and drop to control scrollbars
    //if (!drop)
    //{
    //  //TODO: refactor this
    //  S32 variable_lines = getDocPosMax();
    //  S32 pos = (VERTICAL == mOrientation) ? y : x;
    //  S32 thumb_length = (VERTICAL == mOrientation) ? mThumbRect.getHeight() : mThumbRect.getWidth();
    //  S32 thumb_track_length = (VERTICAL == mOrientation) ? (getRect().getHeight() - 2 * SCROLLBAR_SIZE) : (getRect().getWidth() - 2 * SCROLLBAR_SIZE);
    //  S32 usable_track_length = thumb_track_length - thumb_length;
    //  F32 ratio = (VERTICAL == mOrientation) ? F32(pos - SCROLLBAR_SIZE - thumb_length) / usable_track_length
    //      : F32(pos - SCROLLBAR_SIZE) / usable_track_length;
    //  S32 new_pos = (VERTICAL == mOrientation) ? llclamp( S32(variable_lines - ratio * variable_lines + 0.5f), 0, variable_lines )
    //      : llclamp( S32(ratio * variable_lines + 0.5f), 0, variable_lines );
    //  changeLine( new_pos - mDocPos, TRUE );
    //}
    //return TRUE;
    return FALSE;
}

BOOL LLScrollbar::handleMouseUp(S32 x, S32 y, MASK mask)
{
    BOOL handled = FALSE;
    if( hasMouseCapture() )
    {
        gFocusMgr.setMouseCapture( NULL );
        handled = TRUE;
    }
    else
    {
        // Opaque, so don't just check children
        handled = LLView::handleMouseUp( x, y, mask );
    }

    return handled;
}

BOOL LLScrollbar::handleDoubleClick(S32 x, S32 y, MASK mask)
{
    // just treat a double click as a second click
    return handleMouseDown(x, y, mask);
}


void LLScrollbar::reshape(S32 width, S32 height, BOOL called_from_parent)
{
    if (width == getRect().getWidth() && height == getRect().getHeight()) return;
    LLView::reshape( width, height, called_from_parent );
    LLButton* up_button = getChild<LLButton>("Line Up");
    LLButton* down_button = getChild<LLButton>("Line Down");

    if (mOrientation == VERTICAL)
    {
        up_button->reshape(up_button->getRect().getWidth(), llmin(getRect().getHeight() / 2, mThickness));
        down_button->reshape(down_button->getRect().getWidth(), llmin(getRect().getHeight() / 2, mThickness));
        up_button->setOrigin(0, getRect().getHeight() - up_button->getRect().getHeight());
        down_button->setOrigin(0, 0);
    }
    else
    {
        up_button->reshape(llmin(getRect().getWidth() / 2, mThickness), up_button->getRect().getHeight());
        down_button->reshape(llmin(getRect().getWidth() / 2, mThickness), down_button->getRect().getHeight());
        up_button->setOrigin(0, 0);
        down_button->setOrigin(getRect().getWidth() - down_button->getRect().getWidth(), 0);
    }
    updateThumbRect();
}


void LLScrollbar::draw()
{
    if (!getRect().isValid()) return;

    if(mBGVisible)
    {
        gl_rect_2d(getLocalRect(), mBGColor.get(), TRUE);
    }

    S32 local_mouse_x;
    S32 local_mouse_y;
    LLUI::getInstance()->getMousePositionLocal(this, &local_mouse_x, &local_mouse_y);
    BOOL other_captor = gFocusMgr.getMouseCapture() && gFocusMgr.getMouseCapture() != this;
    BOOL hovered = getEnabled() && !other_captor && (hasMouseCapture() || mThumbRect.pointInRect(local_mouse_x, local_mouse_y));
    if (hovered)
    {
        mCurGlowStrength = lerp(mCurGlowStrength, mHoverGlowStrength, LLSmoothInterpolation::getInterpolant(0.05f));
    }
    else
    {
        mCurGlowStrength = lerp(mCurGlowStrength, 0.f, LLSmoothInterpolation::getInterpolant(0.05f));
    }

    // Draw background and thumb.
    if (   ( mOrientation == VERTICAL&&(mThumbImageV.isNull() || mThumbImageH.isNull()) )
        || (mOrientation == HORIZONTAL&&(mTrackImageH.isNull() || mTrackImageV.isNull()) ))
    {
        gl_rect_2d(mOrientation == HORIZONTAL ? mThickness : 0,
        mOrientation == VERTICAL ? getRect().getHeight() - 2 * mThickness : getRect().getHeight(),
        mOrientation == HORIZONTAL ? getRect().getWidth() - 2 * mThickness : getRect().getWidth(),
        mOrientation == VERTICAL ? mThickness : 0, mTrackColor.get(), TRUE);

        gl_rect_2d(mThumbRect, mThumbColor.get(), TRUE);

    }
    else
    {
        // Thumb
        LLRect outline_rect = mThumbRect;
        outline_rect.stretch(2);
        // Background

        if(mOrientation == HORIZONTAL)
        {
            mTrackImageH->drawSolid(mThickness                              //S32 x
                                   , 0                                      //S32 y
                                   , getRect().getWidth() - 2 * mThickness  //S32 width
                                   , getRect().getHeight()                  //S32 height
                                   , mTrackColor.get());                    //const LLColor4& color

            if (gFocusMgr.getKeyboardFocus() == this)
            {
                mTrackImageH->draw(outline_rect, gFocusMgr.getFocusColor());
            }

            mThumbImageH->draw(mThumbRect, mThumbColor.get());
            if (mCurGlowStrength > 0.01f)
            {
                gGL.setSceneBlendType(LLRender::BT_ADD_WITH_ALPHA);
                mThumbImageH->drawSolid(mThumbRect, LLColor4(1.f, 1.f, 1.f, mCurGlowStrength));
                gGL.setSceneBlendType(LLRender::BT_ALPHA);
            }

        }
        else if(mOrientation == VERTICAL)
        {
            mTrackImageV->drawSolid(  0                                     //S32 x
                                   , mThickness                             //S32 y
                                   , getRect().getWidth()                   //S32 width
                                   , getRect().getHeight() - 2 * mThickness //S32 height
                                   , mTrackColor.get());                    //const LLColor4& color
            if (gFocusMgr.getKeyboardFocus() == this)
            {
                mTrackImageV->draw(outline_rect, gFocusMgr.getFocusColor());
            }

            mThumbImageV->draw(mThumbRect, mThumbColor.get());
            if (mCurGlowStrength > 0.01f)
            {
                gGL.setSceneBlendType(LLRender::BT_ADD_WITH_ALPHA);
                mThumbImageV->drawSolid(mThumbRect, LLColor4(1.f, 1.f, 1.f, mCurGlowStrength));
                gGL.setSceneBlendType(LLRender::BT_ALPHA);
            }
        }
    }

    // Draw children
    LLView::draw();
} // end draw


bool LLScrollbar::changeLine( S32 delta, BOOL update_thumb )
{
    return setDocPos(mDocPos + delta, update_thumb);
}

void LLScrollbar::setValue(const LLSD& value)
{
    setDocPos((S32) value.asInteger());
}


BOOL LLScrollbar::handleKeyHere(KEY key, MASK mask)
{
    if (getDocPosMax() == 0 && !getVisible())
    {
        return FALSE;
    }

    BOOL handled = FALSE;

    switch( key )
    {
    case KEY_HOME:
        setDocPos( 0 );
        handled = TRUE;
        break;

    case KEY_END:
        setDocPos( getDocPosMax() );
        handled = TRUE;
        break;

    case KEY_DOWN:
        setDocPos( getDocPos() + mStepSize );
        handled = TRUE;
        break;

    case KEY_UP:
        setDocPos( getDocPos() - mStepSize );
        handled = TRUE;
        break;

    case KEY_PAGE_DOWN:
        pageDown(1);
        break;

    case KEY_PAGE_UP:
        pageUp(1);
        break;
    }

    return handled;
}

void LLScrollbar::pageUp(S32 overlap)
{
    if (mDocSize > mPageSize)
    {
        changeLine( -(mPageSize - overlap), TRUE );
    }
}

void LLScrollbar::pageDown(S32 overlap)
{
    if (mDocSize > mPageSize)
    {
        changeLine( mPageSize - overlap, TRUE );
    }
}

void LLScrollbar::onLineUpBtnPressed( const LLSD& data )
{
    changeLine( -mStepSize, TRUE );
}

void LLScrollbar::onLineDownBtnPressed( const LLSD& data )
{
    changeLine( mStepSize, TRUE );
}

void LLScrollbar::setThickness(S32 thickness)
{
    mThickness = thickness < 0 ? LLUI::getInstance()->mSettingGroups["config"]->getS32("UIScrollbarSize") : thickness;
}