/**
 * @file lllocationinputctrl.cpp
 * @brief Combobox-like location input control
 *
 * $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 "llviewerprecompiledheaders.h"

// file includes
#include "lllocationinputctrl.h"

// common includes
#include "llbutton.h"
#include "llfloaterreg.h"
#include "llfocusmgr.h"
#include "llkeyboard.h"
#include "llstring.h"
#include "lluictrlfactory.h"
#include "v2math.h"

// newview includes
#include "llagent.h"
#include "llfloaterland.h"
#include "llinventorymodel.h"
#include "lllandmarklist.h"
#include "lllocationhistory.h"
#include "llpanelplaces.h"
#include "llsidetray.h"
#include "llviewerinventory.h"
#include "llviewerparcelmgr.h"

//============================================================================
/*
 * "ADD LANDMARK" BUTTON UPDATING LOGIC
 * 
 * If the current parcel has been landmarked, we should draw
 * a special image on the button.
 * 
 * To avoid determining the appropriate image on every draw() we do that
 * only in the following cases:
 * 1) Navbar is shown for the first time after login.
 * 2) Agent moves to another parcel.
 * 3) A landmark is created or removed.
 * 
 * The first case is handled by the handleLoginComplete() method.
 * 
 * The second case is handled by setting the "agent parcel changed" callback
 * on LLViewerParcelMgr.
 * 
 * The third case is the most complex one. We have two inventory observers for that:
 * one is designated to handle adding landmarks, the other handles removal.
 * Let's see how the former works.
 * 
 * When we get notified about landmark addition, the landmark position is unknown yet. What we can
 * do at that point is initiate loading the landmark data by LLLandmarkList and set the
 * "loading finished" callback on it. Finally, when the callback is triggered,
 * we can determine whether the landmark refers to a point within the current parcel
 * and choose the appropriate image for the "Add landmark" button.
 */

// Returns true if the given inventory item is a landmark pointing to the current parcel.
// Used to filter inventory items.
class LLIsAgentParcelLandmark : public LLInventoryCollectFunctor
{
public:
	/*virtual*/ bool operator()(LLInventoryCategory* cat, LLInventoryItem* item)
	{
		if (!item || item->getType() != LLAssetType::AT_LANDMARK)
			return false;

		LLLandmark* landmark = gLandmarkList.getAsset(item->getAssetUUID());
		if (!landmark) // the landmark not been loaded yet
			return false;

		LLVector3d landmark_global_pos;
		if (!landmark->getGlobalPos(landmark_global_pos))
			return false;

		return LLViewerParcelMgr::getInstance()->inAgentParcel(landmark_global_pos);
	}
};

/**
 * Initiates loading the landmarks that have been just added.
 *
 * Once the loading is complete we'll be notified
 * with the callback we set for LLLandmarkList.
 */
class LLAddLandmarkObserver : public LLInventoryAddedObserver
{
public:
	LLAddLandmarkObserver(LLLocationInputCtrl* input) : mInput(input) {}

private:
	/*virtual*/ void done()
	{
		std::vector<LLUUID>::const_iterator it = mAdded.begin(), end = mAdded.end();
		for(; it != end; ++it)
		{
			LLInventoryItem* item = gInventory.getItem(*it);
			if (!item || item->getType() != LLAssetType::AT_LANDMARK)
				continue;

			// Start loading the landmark.
			LLLandmark* lm = gLandmarkList.getAsset(
					item->getAssetUUID(),
					boost::bind(&LLLocationInputCtrl::onLandmarkLoaded, mInput, _1));
			if (lm)
			{
				// Already loaded? Great, handle it immediately (the callback won't be called).
				mInput->onLandmarkLoaded(lm);
			}
		}

		mAdded.clear();
	}

	LLLocationInputCtrl* mInput;
};

/**
 * Updates the "Add landmark" button once a landmark gets removed.
 */
class LLRemoveLandmarkObserver : public LLInventoryObserver
{
public:
	LLRemoveLandmarkObserver(LLLocationInputCtrl* input) : mInput(input) {}

private:
	/*virtual*/ void changed(U32 mask)
	{
		if (mask & (~(LLInventoryObserver::LABEL|LLInventoryObserver::INTERNAL|LLInventoryObserver::ADD)))
		{
			mInput->updateAddLandmarkButton();
		}
	}

	LLLocationInputCtrl* mInput;
};

//============================================================================


static LLDefaultWidgetRegistry::Register<LLLocationInputCtrl> r("location_input");

LLLocationInputCtrl::Params::Params()
:	add_landmark_image_enabled("add_landmark_image_enabled"),
	add_landmark_image_disabled("add_landmark_image_disabled"),
	add_landmark_button("add_landmark_button"),
	add_landmark_hpad("add_landmark_hpad", 0),
	info_button("info_button"),
	background("background")
{
}

LLLocationInputCtrl::LLLocationInputCtrl(const LLLocationInputCtrl::Params& p)
:	LLComboBox(p),
	mAddLandmarkHPad(p.add_landmark_hpad),
	mInfoBtn(NULL),
	mAddLandmarkBtn(NULL)
{
	// Background image.
	LLButton::Params bg_params = p.background;
	mBackground = LLUICtrlFactory::create<LLButton>(bg_params);
	addChildInBack(mBackground);

	// "Place information" button.
	LLButton::Params info_params = p.info_button;
	mInfoBtn = LLUICtrlFactory::create<LLButton>(info_params);
	mInfoBtn->setClickedCallback(boost::bind(&LLLocationInputCtrl::onInfoButtonClicked, this));
	addChild(mInfoBtn);
	
	// "Add landmark" button.
	LLButton::Params al_params = p.add_landmark_button;
	if (p.add_landmark_image_enabled())
	{
		al_params.image_unselected = p.add_landmark_image_enabled;
		al_params.image_selected = p.add_landmark_image_enabled;
	}
	if (p.add_landmark_image_disabled())
	{
		al_params.image_disabled = p.add_landmark_image_disabled;
		al_params.image_disabled_selected = p.add_landmark_image_disabled;
	}
	al_params.click_callback.function(boost::bind(&LLLocationInputCtrl::onAddLandmarkButtonClicked, this));
	mAddLandmarkBtn = LLUICtrlFactory::create<LLButton>(al_params);
	enableAddLandmarkButton(true);
	addChild(mAddLandmarkBtn);
	
	setFocusReceivedCallback(boost::bind(&LLLocationInputCtrl::onFocusReceived, this));
	setFocusLostCallback(boost::bind(&LLLocationInputCtrl::onFocusLost, this));
	setPrearrangeCallback(boost::bind(&LLLocationInputCtrl::onLocationPrearrange, this, _2));

	updateWidgetlayout();

	// - Make the "Add landmark" button updated when either current parcel gets changed
	//   or a landmark gets created or removed from the inventory.
	// - Update the location string on parcel change.
	LLViewerParcelMgr::getInstance()->setAgentParcelChangedCallback(
		boost::bind(&LLLocationInputCtrl::onAgentParcelChange, this));

	LLLocationHistory::getInstance()->setLoadedCallback(
			boost::bind(&LLLocationInputCtrl::onLocationHistoryLoaded, this));

	mRemoveLandmarkObserver	= new LLRemoveLandmarkObserver(this);
	mAddLandmarkObserver	= new LLAddLandmarkObserver(this);
	gInventory.addObserver(mRemoveLandmarkObserver);
	gInventory.addObserver(mAddLandmarkObserver);
}

LLLocationInputCtrl::~LLLocationInputCtrl()
{
	gInventory.removeObserver(mRemoveLandmarkObserver);
	gInventory.removeObserver(mAddLandmarkObserver);
	delete mRemoveLandmarkObserver;
	delete mAddLandmarkObserver;
}

void LLLocationInputCtrl::setEnabled(BOOL enabled)
{
	LLComboBox::setEnabled(enabled);
	mAddLandmarkBtn->setEnabled(enabled);
}

void LLLocationInputCtrl::hideList()
{
	LLComboBox::hideList();
	if (mTextEntry && hasFocus())
		focusTextEntry();
}

BOOL LLLocationInputCtrl::handleToolTip(S32 x, S32 y, std::string& msg, LLRect* sticky_rect_screen)
{
	// Let the buttons show their tooltips.
	if (LLUICtrl::handleToolTip(x, y, msg, sticky_rect_screen) && !msg.empty())
	{
		return TRUE;
	}

	// Cursor is above the text entry.
	msg = LLUI::sShowXUINames ? getShowNamesToolTip() : gAgent.getSLURL();
	if (mTextEntry && sticky_rect_screen)
	{
		*sticky_rect_screen = mTextEntry->calcScreenRect();
	}

	return TRUE;
}

BOOL LLLocationInputCtrl::handleKeyHere(KEY key, MASK mask)
{
	BOOL result = LLComboBox::handleKeyHere(key, mask);

	if (key == KEY_DOWN && hasFocus() && mList->getItemCount() != 0)
	{
		showList();
	}

	return result;
}

void LLLocationInputCtrl::onTextEntry(LLLineEditor* line_editor)
{
	KEY key = gKeyboard->currentKey();

	if (line_editor->getText().empty())
	{
		prearrangeList(); // resets filter
		hideList();
	}
	// Typing? (moving cursor should not affect showing the list)
	else if (key != KEY_LEFT && key != KEY_RIGHT && key != KEY_HOME && key != KEY_END)
	{
		prearrangeList(line_editor->getText());
		if (mList->getItemCount() != 0)
		{
			showList();
			focusTextEntry();
		}
		else
		{
			// Hide the list if it's empty.
			hideList();
		}
	}
	
	LLComboBox::onTextEntry(line_editor);
}

/**
 * Useful if we want to just set the text entry value, no matter what the list contains.
 *
 * This is faster than setTextEntry().
 */
void LLLocationInputCtrl::setText(const LLStringExplicit& text)
{
	if (mTextEntry)
	{
		mTextEntry->setText(text);
		mHasAutocompletedText = FALSE;
	}
}

void LLLocationInputCtrl::setFocus(BOOL b)
{
	LLComboBox::setFocus(b);

	if (mTextEntry && b && !mList->getVisible())
		mTextEntry->setFocus(TRUE);
}

void LLLocationInputCtrl::handleLoginComplete()
{
	// An agent parcel update hasn't occurred yet, so we have to
	// manually set location and the appropriate "Add landmark" icon.
	refresh();
}

//== private methods =========================================================

void LLLocationInputCtrl::onFocusReceived()
{
	prearrangeList();
	setText(gAgent.getSLURL());
	if (mTextEntry)
		mTextEntry->endSelection(); // we don't want handleMouseUp() to "finish" the selection
}

void LLLocationInputCtrl::onFocusLost()
{
	refreshLocation();
}

void LLLocationInputCtrl::onInfoButtonClicked()
{
	LLSD key;
	key["type"] = LLPanelPlaces::AGENT;

	LLSideTray::getInstance()->showPanel("panel_places", key);
}

void LLLocationInputCtrl::onAddLandmarkButtonClicked()
{
	LLFloaterReg::showInstance("add_landmark");
}

void LLLocationInputCtrl::onAgentParcelChange()
{
	refresh();
}

void LLLocationInputCtrl::onLandmarkLoaded(LLLandmark* lm)
{
	(void) lm;
	updateAddLandmarkButton();
}

void LLLocationInputCtrl::onLocationHistoryLoaded()
{
	rebuildLocationHistory();
}

void LLLocationInputCtrl::onLocationPrearrange(const LLSD& data)
{
	std::string filter = data.asString();
	rebuildLocationHistory(filter);
	mList->mouseOverHighlightNthItem(-1); // Clear highlight on the last selected item.
}

void LLLocationInputCtrl::refresh()
{
	refreshLocation();			// update location string
	updateAddLandmarkButton();	// indicate whether current parcel has been landmarked 
}

void LLLocationInputCtrl::refreshLocation()
{
	// Is one of our children focused?
	if (LLUICtrl::hasFocus() || mButton->hasFocus() || mList->hasFocus() ||
		(mTextEntry && mTextEntry->hasFocus()) || (mAddLandmarkBtn->hasFocus()))

	{
		llwarns << "Location input should not be refreshed when having focus" << llendl;
		return;
	}

	// Update location field.
	std::string location_name;

	if (!gAgent.buildLocationString(location_name, LLAgent::LOCATION_FORMAT_NORMAL))
		location_name = "Unknown";

	setText(location_name);
}

void LLLocationInputCtrl::rebuildLocationHistory(std::string filter)
{
	LLLocationHistory::location_list_t filtered_items;
	const LLLocationHistory::location_list_t* itemsp = NULL;
	LLLocationHistory* lh = LLLocationHistory::getInstance();
	
	if (filter.empty())
	{
		itemsp = &lh->getItems();
	}
	else
	{
		lh->getMatchingItems(filter, filtered_items);
		itemsp = &filtered_items;
	}
	
	removeall();
	for (LLLocationHistory::location_list_t::const_reverse_iterator it = itemsp->rbegin(); it != itemsp->rend(); it++)
	{
		add(*it);
	}
}

void LLLocationInputCtrl::focusTextEntry()
{
	// We can't use "mTextEntry->setFocus(TRUE)" instead because
	// if the "select_on_focus" parameter is true it places the cursor
	// at the beginning (after selecting text), thus screwing up updateSelection().
	if (mTextEntry)
		gFocusMgr.setKeyboardFocus(mTextEntry);
}

void LLLocationInputCtrl::enableAddLandmarkButton(bool val)
{
	// Enable/disable the button.
	mAddLandmarkBtn->setEnabled(val);
}

// Change the "Add landmark" button image
// depending on whether current parcel has been landmarked.
void LLLocationInputCtrl::updateAddLandmarkButton()
{
	bool cur_parcel_landmarked = false;

	// Determine whether there are landmarks pointing to the current parcel.
	LLInventoryModel::cat_array_t cats;
	LLInventoryModel::item_array_t items;
	LLIsAgentParcelLandmark is_current_parcel_landmark;
	gInventory.collectDescendentsIf(gAgent.getInventoryRootID(),
		cats,
		items,
		LLInventoryModel::EXCLUDE_TRASH,
		is_current_parcel_landmark);
	cur_parcel_landmarked = !items.empty();

	enableAddLandmarkButton(!cur_parcel_landmarked);
}

void LLLocationInputCtrl::updateWidgetlayout()
{
	const LLRect&	rect			= getLocalRect();
	const LLRect&	hist_btn_rect	= mButton->getRect();
	LLRect			info_btn_rect	= mButton->getRect();

	// info button
	info_btn_rect.setOriginAndSize(
		0, (rect.getHeight() - info_btn_rect.getHeight()) / 2,
		info_btn_rect.getWidth(), info_btn_rect.getHeight());
	mInfoBtn->setRect(info_btn_rect);

	// background
	mBackground->setRect(LLRect(info_btn_rect.getWidth(), rect.mTop,
		rect.mRight - hist_btn_rect.getWidth(), rect.mBottom));

	// history button
	mButton->setRightHPad(0);

	// "Add Landmark" button
	{
		LLRect al_btn_rect = mAddLandmarkBtn->getRect();
		al_btn_rect.translate(
			hist_btn_rect.mLeft - mAddLandmarkHPad - al_btn_rect.getWidth(),
			(rect.getHeight() - al_btn_rect.getHeight()) / 2);
		mAddLandmarkBtn->setRect(al_btn_rect);
	}

	// text entry
	if (mTextEntry)
	{
		LLRect text_entry_rect(rect);
		text_entry_rect.mLeft = info_btn_rect.getWidth();
		text_entry_rect.mRight = mAddLandmarkBtn->getRect().mLeft;
		text_entry_rect.stretch(0, -1); // make space for border
		mTextEntry->setRect(text_entry_rect);
	}
}