/** 
 * @file lltextbase.cpp
 * @author Martin Reddy
 * @brief The base class of text box/editor, providing Url handling support
 *
 * $LicenseInfo:firstyear=2009&license=viewergpl$
 * 
 * Copyright (c) 2009, 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 "linden_common.h"

#include "lltextbase.h"
#include "llstl.h"
#include "llview.h"
#include "llwindow.h"
#include "llmenugl.h"
#include "lltooltip.h"
#include "lluictrl.h"
#include "llurlaction.h"
#include "llurlregistry.h"

#include <boost/bind.hpp>

// global state for all text fields
LLUIColor LLTextBase::mLinkColor = LLColor4::blue;

bool LLTextBase::compare_segment_end::operator()(const LLTextSegmentPtr& a, const LLTextSegmentPtr& b) const
{
	return a->getEnd() < b->getEnd();
}

//
// LLTextSegment
//

LLTextSegment::~LLTextSegment()
{}

S32	LLTextSegment::getWidth(S32 first_char, S32 num_chars) const { return 0; }
S32	LLTextSegment::getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const { return 0; }
S32	LLTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const { return 0; }
void LLTextSegment::updateLayout(const LLTextBase& editor) {}
F32	LLTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect) { return draw_rect.mLeft; }
S32	LLTextSegment::getMaxHeight() const { return 0; }
bool LLTextSegment::canEdit() const { return false; }
void LLTextSegment::unlinkFromDocument(LLTextBase*) {}
void LLTextSegment::linkToDocument(LLTextBase*) {}
void LLTextSegment::setHasMouseHover(bool hover) {}
const LLColor4& LLTextSegment::getColor() const { return LLColor4::white; }
void LLTextSegment::setColor(const LLColor4 &color) {}
const LLStyleSP LLTextSegment::getStyle() const {static LLStyleSP sp(new LLStyle()); return sp; }
void LLTextSegment::setStyle(const LLStyleSP &style) {}
void LLTextSegment::setToken( LLKeywordToken* token ) {}
LLKeywordToken*	LLTextSegment::getToken() const { return NULL; }
BOOL LLTextSegment::getToolTip( std::string& msg ) const { return FALSE; }
void LLTextSegment::setToolTip( const std::string &msg ) {}
void LLTextSegment::dump() const {}


//
// LLNormalTextSegment
//

LLNormalTextSegment::LLNormalTextSegment( const LLStyleSP& style, S32 start, S32 end, LLTextBase& editor ) 
:	LLTextSegment(start, end),
	mStyle( style ),
	mToken(NULL),
	mHasMouseHover(false),
	mEditor(editor)
{
	mMaxHeight = llceil(mStyle->getFont()->getLineHeight());
}

LLNormalTextSegment::LLNormalTextSegment( const LLColor4& color, S32 start, S32 end, LLTextBase& editor, BOOL is_visible) 
:	LLTextSegment(start, end),
	mToken(NULL),
	mHasMouseHover(false),
	mEditor(editor)
{
	mStyle = new LLStyle(LLStyle::Params().visible(is_visible).color(color));

	mMaxHeight = llceil(mStyle->getFont()->getLineHeight());
}

F32 LLNormalTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect)
{
	if( end - start > 0 )
	{
		if ( mStyle->isImage() && (start >= 0) && (end <= mEnd - mStart))
		{
			LLUIImagePtr image = mStyle->getImage();
			S32 style_image_height = image->getHeight();
			S32 style_image_width = image->getWidth();
			image->draw(draw_rect.mLeft, draw_rect.mTop-style_image_height, 
				style_image_width, style_image_height);
		}

		return drawClippedSegment( getStart() + start, getStart() + end, selection_start, selection_end, draw_rect.mLeft, draw_rect.mBottom);
	}
	return draw_rect.mLeft;
}

// Draws a single text segment, reversing the color for selection if needed.
F32 LLNormalTextSegment::drawClippedSegment(S32 seg_start, S32 seg_end, S32 selection_start, S32 selection_end, F32 x, F32 y)
{
	const LLWString &text = mEditor.getWText();

	F32 right_x = x;
	if (!mStyle->isVisible())
	{
		return right_x;
	}

	const LLFontGL* font = mStyle->getFont();

	LLColor4 color = mStyle->getColor();

	font = mStyle->getFont();

  	if( selection_start > seg_start )
	{
		// Draw normally
		S32 start = seg_start;
		S32 end = llmin( selection_start, seg_end );
		S32 length =  end - start;
		font->render(text, start, x, y, color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, length, S32_MAX, &right_x, mEditor.allowsEmbeddedItems());
	}
	x = right_x;
	
	if( (selection_start < seg_end) && (selection_end > seg_start) )
	{
		// Draw reversed
		S32 start = llmax( selection_start, seg_start );
		S32 end = llmin( selection_end, seg_end );
		S32 length = end - start;

		font->render(text, start, x, y,
					 LLColor4( 1.f - color.mV[0], 1.f - color.mV[1], 1.f - color.mV[2], 1.f ),
					 LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, length, S32_MAX, &right_x, mEditor.allowsEmbeddedItems());
	}
	x = right_x;
	if( selection_end < seg_end )
	{
		// Draw normally
		S32 start = llmax( selection_end, seg_start );
		S32 end = seg_end;
		S32 length = end - start;
		font->render(text, start, x, y, color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, length, S32_MAX, &right_x, mEditor.allowsEmbeddedItems());
	}
	return right_x;
}

S32	LLNormalTextSegment::getMaxHeight() const	
{ 
	return mMaxHeight; 
}

BOOL LLNormalTextSegment::getToolTip(std::string& msg) const
{
	// do we have a tooltip for a loaded keyword (for script editor)?
	if (mToken && !mToken->getToolTip().empty())
	{
		const LLWString& wmsg = mToken->getToolTip();
		msg = wstring_to_utf8str(wmsg);
		return TRUE;
	}
	// or do we have an explicitly set tooltip (e.g., for Urls)
	if (! mTooltip.empty())
	{
		msg = mTooltip;
		return TRUE;
	}
	return FALSE;
}

void LLNormalTextSegment::setToolTip(const std::string& tooltip)
{
	// we cannot replace a keyword tooltip that's loaded from a file
	if (mToken)
	{
		llwarns << "LLTextSegment::setToolTip: cannot replace keyword tooltip." << llendl;
		return;
	}
	mTooltip = tooltip;
}

S32	LLNormalTextSegment::getWidth(S32 first_char, S32 num_chars) const
{
	LLWString text = mEditor.getWText();
	return mStyle->getFont()->getWidth(text.c_str(), mStart + first_char, num_chars);
}

S32	LLNormalTextSegment::getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const
{
	LLWString text = mEditor.getWText();
	return mStyle->getFont()->charFromPixelOffset(text.c_str(), mStart + start_offset,
											   (F32)segment_local_x_coord,
											   F32_MAX,
											   num_chars,
											   round);
}

S32	LLNormalTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const
{
	LLWString text = mEditor.getWText();
	S32 num_chars = mStyle->getFont()->maxDrawableChars(text.c_str() + segment_offset + mStart, 
												(F32)num_pixels,
												max_chars, 
												mEditor.getWordWrap());

	if (num_chars == 0 
		&& line_offset == 0 
		&& max_chars > 0)
	{
		// If at the beginning of a line, and a single character won't fit, draw it anyway
		num_chars = 1;
	}
	if (mStart + segment_offset + num_chars == mEditor.getLength())
	{
		// include terminating NULL
		num_chars++;
	}
	return num_chars;
}

void LLNormalTextSegment::dump() const
{
	llinfos << "Segment [" << 
//			mColor.mV[VX] << ", " <<
//			mColor.mV[VY] << ", " <<
//			mColor.mV[VZ] << "]\t[" <<
		mStart << ", " <<
		getEnd() << "]" <<
		llendl;
}

//////////////////////////////////////////////////////////////////////////
//
// LLTextBase
//

LLTextBase::LLTextBase(const LLUICtrl::Params &p) :
	mHoverSegment(NULL),
	mDefaultFont(p.font),
	mParseHTML(TRUE),
	mPopupMenu(NULL)
{
}

LLTextBase::~LLTextBase()
{
	clearSegments();
}

void LLTextBase::clearSegments()
{
	setHoverSegment(NULL);
	mSegments.clear();
}

void LLTextBase::setHoverSegment(LLTextSegmentPtr segment)
{
	if (mHoverSegment)
	{
		mHoverSegment->setHasMouseHover(false);
	}
	if (segment)
	{
		segment->setHasMouseHover(true);
	}
	mHoverSegment = segment;
}

void LLTextBase::getSegmentAndOffset( S32 startpos, segment_set_t::const_iterator* seg_iter, S32* offsetp ) const
{
	*seg_iter = getSegIterContaining(startpos);
	if (*seg_iter == mSegments.end())
	{
		*offsetp = 0;
	}
	else
	{
		*offsetp = startpos - (**seg_iter)->getStart();
	}
}

void LLTextBase::getSegmentAndOffset( S32 startpos, segment_set_t::iterator* seg_iter, S32* offsetp )
{
	*seg_iter = getSegIterContaining(startpos);
	if (*seg_iter == mSegments.end())
	{
		*offsetp = 0;
	}
	else
	{
		*offsetp = startpos - (**seg_iter)->getStart();
	}
}

LLTextBase::segment_set_t::iterator LLTextBase::getSegIterContaining(S32 index)
{
	segment_set_t::iterator it = mSegments.upper_bound(new LLIndexSegment(index));
	return it;
}

LLTextBase::segment_set_t::const_iterator LLTextBase::getSegIterContaining(S32 index) const
{
	LLTextBase::segment_set_t::const_iterator it =  mSegments.upper_bound(new LLIndexSegment(index));
	return it;
}

// Finds the text segment (if any) at the give local screen position
LLTextSegmentPtr LLTextBase::getSegmentAtLocalPos( S32 x, S32 y )
{
	// Find the cursor position at the requested local screen position
	S32 offset = getDocIndexFromLocalCoord( x, y, FALSE );
	segment_set_t::iterator seg_iter = getSegIterContaining(offset);
	if (seg_iter != mSegments.end())
	{
		return *seg_iter;
	}
	else
	{
		return LLTextSegmentPtr();
	}
}

BOOL LLTextBase::handleHoverOverUrl(S32 x, S32 y)
{
	setHoverSegment(NULL);

	// Check to see if we're over an HTML-style link
	LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y );
	if (cur_segment)
	{
		setHoverSegment(cur_segment);

		LLStyleSP style =  cur_segment->getStyle();
		if (style && style->isLink())
		{
			return TRUE;
		}
	}

	return FALSE;
}

BOOL LLTextBase::handleMouseUpOverUrl(S32 x, S32 y)
{
	if (mParseHTML && mHoverSegment)
	{
		LLStyleSP style = mHoverSegment->getStyle();
		if (style && style->isLink())
		{
			LLUrlAction::clickAction(style->getLinkHREF());
			return TRUE;
		}
	}

	return FALSE;
}

BOOL LLTextBase::handleRightMouseDownOverUrl(LLView *view, S32 x, S32 y)
{
	// pop up a context menu for any Url under the cursor
	const LLTextSegment* cur_segment = getSegmentAtLocalPos(x, y);
	if (cur_segment && cur_segment->getStyle() && cur_segment->getStyle()->isLink())
	{
		delete mPopupMenu;
		mPopupMenu = createUrlContextMenu(cur_segment->getStyle()->getLinkHREF());
		if (mPopupMenu)
		{
			mPopupMenu->show(x, y);
			LLMenuGL::showPopup(view, mPopupMenu, x, y);
			return TRUE;
		}
	}

	return FALSE;
}

BOOL LLTextBase::handleToolTipForUrl(LLView *view, S32 x, S32 y, std::string& msg, LLRect& sticky_rect_screen)
{
	std::string tooltip_msg;
	const LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y );
	if (cur_segment && cur_segment->getToolTip( tooltip_msg ) && view)
	{
		// Use a slop area around the cursor
		const S32 SLOP = 8;
		// Convert rect local to screen coordinates
		view->localPointToScreen(x - SLOP, y - SLOP, &(sticky_rect_screen.mLeft),
								 &(sticky_rect_screen.mBottom));
		sticky_rect_screen.mRight = sticky_rect_screen.mLeft + 2 * SLOP;
		sticky_rect_screen.mTop = sticky_rect_screen.mBottom + 2 * SLOP;

		LLToolTipMgr::instance().show(LLToolTipParams()
			.message(tooltip_msg)
			.sticky_rect(sticky_rect_screen));
	}
	return TRUE;
}

LLContextMenu *LLTextBase::createUrlContextMenu(const std::string &in_url)
{
	// work out the XUI menu file to use for this url
	LLUrlMatch match;
	std::string url = in_url;
	if (! LLUrlRegistry::instance().findUrl(url, match))
	{
		return NULL;
	}
	
	std::string xui_file = match.getMenuName();
	if (xui_file.empty())
	{
		return NULL;
	}

	// set up the callbacks for all of the potential menu items, N.B. we
	// don't use const ref strings in callbacks in case url goes out of scope
	LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar;
	registrar.add("Url.Open", boost::bind(&LLUrlAction::openURL, url));
	registrar.add("Url.OpenInternal", boost::bind(&LLUrlAction::openURLInternal, url));
	registrar.add("Url.OpenExternal", boost::bind(&LLUrlAction::openURLExternal, url));
	registrar.add("Url.Execute", boost::bind(&LLUrlAction::executeSLURL, url));
	registrar.add("Url.Teleport", boost::bind(&LLUrlAction::teleportToLocation, url));
	registrar.add("Url.CopyLabel", boost::bind(&LLUrlAction::copyLabelToClipboard, url));
	registrar.add("Url.CopyUrl", boost::bind(&LLUrlAction::copyURLToClipboard, url));

	// create and return the context menu from the XUI file
	return LLUICtrlFactory::getInstance()->createFromFile<LLContextMenu>(xui_file, LLMenuGL::sMenuContainer,
																		 LLMenuHolderGL::child_registry_t::instance());	
}
