/** 
 * @file llflatlistview.cpp
 * @brief LLFlatListView base class
 *
 * $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 "llpanel.h"
#include "lltextbox.h"

#include "llflatlistview.h"

static const LLDefaultChildRegistry::Register<LLFlatListView> flat_list_view("flat_list_view");

const LLSD SELECTED_EVENT	= LLSD().insert("selected", true);
const LLSD UNSELECTED_EVENT	= LLSD().insert("selected", false);

static const std::string COMMENT_TEXTBOX = "comment_text";

//forward declaration
bool llsds_are_equal(const LLSD& llsd_1, const LLSD& llsd_2);

LLFlatListView::Params::Params()
:	item_pad("item_pad"),
	allow_select("allow_select"),
	multi_select("multi_select"),
	keep_one_selected("keep_one_selected")
{};

void LLFlatListView::reshape(S32 width, S32 height, BOOL called_from_parent /* = TRUE */)
{
	LLScrollContainer::reshape(width, height, called_from_parent);
	setItemsNoScrollWidth(width);
	rearrangeItems();
}

const LLRect& LLFlatListView::getItemsRect() const
{
	return mItemsPanel->getRect(); 
}

bool LLFlatListView::addItem(LLPanel * item, const LLSD& value /*= LLUUID::null*/, EAddPosition pos /*= ADD_BOTTOM*/)
{
	if (!item) return false;
	if (value.isUndefined()) return false;
	
	//force uniqueness of items, easiest check but unreliable
	if (item->getParent() == mItemsPanel) return false;
	
	item_pair_t* new_pair = new item_pair_t(item, value);
	switch (pos)
	{
	case ADD_TOP:
		mItemPairs.push_front(new_pair);
		//in LLView::draw() children are iterated in backorder
		mItemsPanel->addChildInBack(item);
		break;
	case ADD_BOTTOM:
		mItemPairs.push_back(new_pair);
		mItemsPanel->addChild(item);
		break;
	default:
		break;
	}
	
	//_4 is for MASK
	item->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4));
	item->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4));

	rearrangeItems();
	notifyParentItemsRectChanged();
	return true;
}


bool LLFlatListView::insertItemAfter(LLPanel* after_item, LLPanel* item_to_add, const LLSD& value /*= LLUUID::null*/)
{
	if (!after_item) return false;
	if (!item_to_add) return false;
	if (value.isUndefined()) return false;

	if (mItemPairs.empty()) return false;

	//force uniqueness of items, easiest check but unreliable
	if (item_to_add->getParent() == mItemsPanel) return false;

	item_pair_t* after_pair = getItemPair(after_item);
	if (!after_pair) return false;

	item_pair_t* new_pair = new item_pair_t(item_to_add, value);
	if (after_pair == mItemPairs.back())
	{
		mItemPairs.push_back(new_pair);
		mItemsPanel->addChild(item_to_add);
	}
	else
	{
		pairs_iterator_t it = mItemPairs.begin();
		for (; it != mItemPairs.end(); ++it)
		{
			if (*it == after_pair)
			{
				// insert new elements before the element at position of passed iterator.
				mItemPairs.insert(++it, new_pair);
				mItemsPanel->addChild(item_to_add);
				break;
			}
		}
	}

	//_4 is for MASK
	item_to_add->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4));
	item_to_add->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4));

	rearrangeItems();
	notifyParentItemsRectChanged();
	return true;
}


bool LLFlatListView::removeItem(LLPanel* item)
{
	if (!item) return false;
	if (item->getParent() != mItemsPanel) return false;
	
	item_pair_t* item_pair = getItemPair(item);
	if (!item_pair) return false;

	return removeItemPair(item_pair);
}

bool LLFlatListView::removeItemByValue(const LLSD& value)
{
	if (value.isUndefined()) return false;
	
	item_pair_t* item_pair = getItemPair(value);
	if (!item_pair) return false;

	return removeItemPair(item_pair);
}

bool LLFlatListView::removeItemByUUID(const LLUUID& uuid)
{
	return removeItemByValue(LLSD(uuid));
}

LLPanel* LLFlatListView::getItemByValue(const LLSD& value) const
{
	if (value.isUndefined()) return NULL;

	item_pair_t* pair = getItemPair(value);
	if (pair) return pair->first;
	return NULL;
}

bool LLFlatListView::selectItem(LLPanel* item, bool select /*= true*/)
{
	if (!item) return false;
	if (item->getParent() != mItemsPanel) return false;
	
	item_pair_t* item_pair = getItemPair(item);
	if (!item_pair) return false;

	return selectItemPair(item_pair, select);
}

bool LLFlatListView::selectItemByValue(const LLSD& value, bool select /*= true*/)
{
	if (value.isUndefined()) return false;

	item_pair_t* item_pair = getItemPair(value);
	if (!item_pair) return false;

	return selectItemPair(item_pair, select);
}

bool LLFlatListView::selectItemByUUID(const LLUUID& uuid, bool select /* = true*/)
{
	return selectItemByValue(LLSD(uuid), select);
}


LLSD LLFlatListView::getSelectedValue() const
{
	if (mSelectedItemPairs.empty()) return LLSD();

	item_pair_t* first_selected_pair = mSelectedItemPairs.front();
	return first_selected_pair->second;
}

void LLFlatListView::getSelectedValues(std::vector<LLSD>& selected_values) const
{
	if (mSelectedItemPairs.empty()) return;

	for (pairs_const_iterator_t it = mSelectedItemPairs.begin(); it != mSelectedItemPairs.end(); ++it)
	{
		selected_values.push_back((*it)->second);
	}
}

LLUUID LLFlatListView::getSelectedUUID() const
{
	const LLSD& value = getSelectedValue();
	if (value.isDefined() && value.isUUID())
	{
		return value.asUUID();
	}
	else 
	{
		return LLUUID::null;
	}
}

void LLFlatListView::getSelectedUUIDs(std::vector<LLUUID>& selected_uuids) const
{
	if (mSelectedItemPairs.empty()) return;

	for (pairs_const_iterator_t it = mSelectedItemPairs.begin(); it != mSelectedItemPairs.end(); ++it)
	{
		selected_uuids.push_back((*it)->second.asUUID());
	}
}

LLPanel* LLFlatListView::getSelectedItem() const
{
	if (mSelectedItemPairs.empty()) return NULL;

	return mSelectedItemPairs.front()->first;
}

void LLFlatListView::getSelectedItems(std::vector<LLPanel*>& selected_items) const
{
	if (mSelectedItemPairs.empty()) return;

	for (pairs_const_iterator_t it = mSelectedItemPairs.begin(); it != mSelectedItemPairs.end(); ++it)
	{
		selected_items.push_back((*it)->first);
	}
}

void LLFlatListView::resetSelection(bool no_commit_on_deselection /*= false*/)
{
	if (mSelectedItemPairs.empty()) return;

	for (pairs_iterator_t it= mSelectedItemPairs.begin(); it != mSelectedItemPairs.end(); ++it)
	{
		item_pair_t* pair_to_deselect = *it;
		LLPanel* item = pair_to_deselect->first;
		item->setValue(UNSELECTED_EVENT);
	}

	mSelectedItemPairs.clear();

	if (mCommitOnSelectionChange && !no_commit_on_deselection)
	{
		onCommit();
	}
}

void LLFlatListView::setNoItemsCommentText(const std::string& comment_text)
{
	if (NULL == mNoItemsCommentTextbox)
	{
		LLRect comment_rect = getRect();
		comment_rect.setOriginAndSize(0, 0, comment_rect.getWidth(), comment_rect.getHeight());
		comment_rect.stretch(-getBorderWidth());
		LLTextBox::Params text_p;
		text_p.name(COMMENT_TEXTBOX);
		text_p.border_visible(false);
		text_p.rect(comment_rect);
		text_p.follows.flags(FOLLOWS_ALL);
		mNoItemsCommentTextbox = LLUICtrlFactory::create<LLTextBox>(text_p, this);
	}

	mNoItemsCommentTextbox->setValue(comment_text);
}

void LLFlatListView::clear()
{
	// do not use LLView::deleteAllChildren to avoid removing nonvisible items. drag-n-drop for ex.
	for (pairs_iterator_t it = mItemPairs.begin(); it != mItemPairs.end(); ++it)
	{
		mItemsPanel->removeChild((*it)->first);
		(*it)->first->die();
		delete *it;
	}
	mItemPairs.clear();
	mSelectedItemPairs.clear();

	// also set items panel height to zero. Reshape it to allow reshaping of non-item children
	LLRect rc = mItemsPanel->getRect();
	rc.mBottom = rc.mTop;
	mItemsPanel->reshape(rc.getWidth(), rc.getHeight());
	mItemsPanel->setRect(rc);

	setNoItemsCommentVisible(true);
	notifyParentItemsRectChanged();
}

void LLFlatListView::sort()
{
	if (!mItemComparator)
	{
		llwarns << "No comparator specified for sorting FlatListView items." << llendl;
		return;
	}

	mItemPairs.sort(ComparatorAdaptor(*mItemComparator));
	rearrangeItems();
}

bool LLFlatListView::updateValue(const LLSD& old_value, const LLSD& new_value)
{
	if (old_value.isUndefined() || new_value.isUndefined()) return false;
	if (llsds_are_equal(old_value, new_value)) return false;

	item_pair_t* item_pair = getItemPair(old_value);
	if (!item_pair) return false;

	item_pair->second = new_value;
	return true;
}

//////////////////////////////////////////////////////////////////////////
// PROTECTED STUFF
//////////////////////////////////////////////////////////////////////////


LLFlatListView::LLFlatListView(const LLFlatListView::Params& p)
:	LLScrollContainer(p)
  , mItemComparator(NULL)
  , mItemsPanel(NULL)
  , mItemPad(p.item_pad)
  , mAllowSelection(p.allow_select)
  , mMultipleSelection(p.multi_select)
  , mKeepOneItemSelected(p.keep_one_selected)
  , mCommitOnSelectionChange(false)
  , mPrevNotifyParentRect(LLRect())
  , mNoItemsCommentTextbox(NULL)
{
	mBorderThickness = getBorderWidth();

	LLRect scroll_rect = getRect();
	LLRect items_rect;

	setItemsNoScrollWidth(scroll_rect.getWidth());
	items_rect.setLeftTopAndSize(mBorderThickness, scroll_rect.getHeight() - mBorderThickness, mItemsNoScrollWidth, 0);

	LLPanel::Params pp;
	pp.rect(items_rect);
	mItemsPanel = LLUICtrlFactory::create<LLPanel> (pp);
	addChild(mItemsPanel);

	//we don't need to stretch in vertical direction on reshaping by a parent
	//no bottom following!
	mItemsPanel->setFollows(FOLLOWS_LEFT | FOLLOWS_RIGHT | FOLLOWS_TOP);
};

void LLFlatListView::rearrangeItems()
{
	static LLUICachedControl<S32> scrollbar_size ("UIScrollbarSize", 0);

	setNoItemsCommentVisible(mItemPairs.empty());

	if (mItemPairs.empty()) return;

	//calculating required height - assuming items can be of different height
	//list should accommodate all its items
	S32 height = 0;

	S32 invisible_children_count = 0;
	pairs_iterator_t it = mItemPairs.begin();
	for (; it != mItemPairs.end(); ++it)
	{
		LLPanel* item = (*it)->first;

		// skip invisible child
		if (!item->getVisible())
		{
			++invisible_children_count;
			continue;
		}

		height += item->getRect().getHeight();
	}

	// add paddings between items, excluding invisible ones
	height += mItemPad * (mItemPairs.size() - invisible_children_count - 1);

	LLRect rc = mItemsPanel->getRect();
	S32 width = mItemsNoScrollWidth;

	// update width to avoid horizontal scrollbar
	if (height > getRect().getHeight() - 2 * mBorderThickness)
		width -= scrollbar_size;

	//changes the bottom, end of the list goes down in the scroll container
	rc.setLeftTopAndSize(rc.mLeft, rc.mTop, width, height);
	mItemsPanel->setRect(rc);

	//reshaping items
	S32 item_new_top = height;
	pairs_iterator_t it2, first_it = mItemPairs.begin();
	for (it2 = first_it; it2 != mItemPairs.end(); ++it2)
	{
		LLPanel* item = (*it2)->first;

		// skip invisible child
		if (!item->getVisible())
			continue;

		LLRect rc = item->getRect();
		rc.setLeftTopAndSize(rc.mLeft, item_new_top, width, rc.getHeight());
		item->reshape(rc.getWidth(), rc.getHeight());
		item->setRect(rc);

		// move top for next item in list
		item_new_top -= (rc.getHeight() + mItemPad);
	}
}

void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask)
{
	if (!item_pair) return;
	
	bool select_item = !isSelected(item_pair);

	//*TODO find a better place for that enforcing stuff
	if (mKeepOneItemSelected && numSelected() == 1 && !select_item) return;
	
	if (!(mask & MASK_CONTROL) || !mMultipleSelection) resetSelection();
	selectItemPair(item_pair, select_item);
}

LLFlatListView::item_pair_t* LLFlatListView::getItemPair(LLPanel* item) const
{
	llassert(item);

	for (pairs_const_iterator_t it= mItemPairs.begin(); it != mItemPairs.end(); ++it)
	{
		item_pair_t* item_pair = *it;
		if (item_pair->first == item) return item_pair;
	}
	return NULL;
}

//compares two LLSD's
bool llsds_are_equal(const LLSD& llsd_1, const LLSD& llsd_2)
{
	llassert(llsd_1.isDefined());
	llassert(llsd_2.isDefined());
	
	if (llsd_1.type() != llsd_2.type()) return false;

	if (!llsd_1.isMap())
	{
		if (llsd_1.isUUID()) return llsd_1.asUUID() == llsd_2.asUUID();

		//assumptions that string representaion is enough for other types
		return llsd_1.asString() == llsd_2.asString();
	}

	if (llsd_1.size() != llsd_2.size()) return false;

	LLSD::map_const_iterator llsd_1_it = llsd_1.beginMap();
	LLSD::map_const_iterator llsd_2_it = llsd_2.beginMap();
	for (S32 i = 0; i < llsd_1.size(); ++i)
	{
		if ((*llsd_1_it).first != (*llsd_2_it).first) return false;
		if (!llsds_are_equal((*llsd_1_it).second, (*llsd_2_it).second)) return false;
		++llsd_1_it;
		++llsd_2_it;
	}
	return true;
}

LLFlatListView::item_pair_t* LLFlatListView::getItemPair(const LLSD& value) const
{
	llassert(value.isDefined());
	
	for (pairs_const_iterator_t it= mItemPairs.begin(); it != mItemPairs.end(); ++it)
	{
		item_pair_t* item_pair = *it;
		if (llsds_are_equal(item_pair->second, value)) return item_pair;
	}
	return NULL;
}

bool LLFlatListView::selectItemPair(item_pair_t* item_pair, bool select)
{
	llassert(item_pair);

	if (!mAllowSelection && select) return false;

	if (isSelected(item_pair) == select) return true; //already in specified selection state
	if (select)
	{
		mSelectedItemPairs.push_back(item_pair);
	}
	else
	{
		mSelectedItemPairs.remove(item_pair);
	}

	//a way of notifying panel of selection state changes
	LLPanel* item = item_pair->first;
	item->setValue(select ? SELECTED_EVENT : UNSELECTED_EVENT);

	if (mCommitOnSelectionChange)
	{
		onCommit();
	}

	return true;
}

bool LLFlatListView::isSelected(item_pair_t* item_pair) const
{
	llassert(item_pair);

	pairs_const_iterator_t it_end = mSelectedItemPairs.end();
	return std::find(mSelectedItemPairs.begin(), it_end, item_pair) != it_end;
}

bool LLFlatListView::removeItemPair(item_pair_t* item_pair)
{
	llassert(item_pair);

	bool deleted = false;
	for (pairs_iterator_t it = mItemPairs.begin(); it != mItemPairs.end(); ++it)
	{
		item_pair_t* _item_pair = *it;
		if (_item_pair == item_pair)
		{
			mItemPairs.erase(it);
			deleted = true;
			break;
		}
	}

	if (!deleted) return false;

	for (pairs_iterator_t it = mSelectedItemPairs.begin(); it != mSelectedItemPairs.end(); ++it)
	{
		item_pair_t* selected_item_pair = *it;
		if (selected_item_pair == item_pair)
		{
			it = mSelectedItemPairs.erase(it);
			break;
		}
	}

	mItemsPanel->removeChild(item_pair->first);
	item_pair->first->die();
	delete item_pair;

	rearrangeItems();
	notifyParentItemsRectChanged();

	return true;
}

void LLFlatListView::notifyParentItemsRectChanged()
{
	S32 comment_height = 0;

	// take into account comment text height if exists
	if (mNoItemsCommentTextbox && mNoItemsCommentTextbox->getVisible())
	{
		comment_height = mNoItemsCommentTextbox->getTextPixelHeight();
	}

	LLRect req_rect =  getItemsRect();

	// get maximum of items total height and comment text height
	req_rect.setOriginAndSize(req_rect.mLeft, req_rect.mBottom, req_rect.getWidth(), llmax(req_rect.getHeight(), comment_height));

	// take into account border size.
	req_rect.stretch(getBorderWidth());

	if (req_rect == mPrevNotifyParentRect)
		return;

	mPrevNotifyParentRect = req_rect;

	LLSD params;
	params["action"] = "size_changes";
	params["width"] = req_rect.getWidth();
	params["height"] = req_rect.getHeight();

	getParent()->notifyParent(params);
}

void LLFlatListView::setNoItemsCommentVisible(bool visible) const
{
	if (mNoItemsCommentTextbox)
	{
		if (visible)
		{
			// We have to update child rect here because of issues with rect after reshaping while creating LLTextbox
			// It is possible to have invalid LLRect if Flat List is in LLAccordionTab
			LLRect comment_rect = getLocalRect();
			comment_rect.stretch(-getBorderWidth());
			mNoItemsCommentTextbox->setRect(comment_rect);
		}
		mNoItemsCommentTextbox->setVisible(visible);
	}
}

void LLFlatListView::getItems(std::vector<LLPanel*>& items) const
{
	if (mItemPairs.empty()) return;

	items.clear();
	for (pairs_const_iterator_t it = mItemPairs.begin(); it != mItemPairs.end(); ++it)
	{
		items.push_back((*it)->first);
	}
}

void LLFlatListView::getValues(std::vector<LLSD>& values) const
{
	if (mItemPairs.empty()) return;

	values.clear();
	for (pairs_const_iterator_t it = mItemPairs.begin(); it != mItemPairs.end(); ++it)
	{
		values.push_back((*it)->second);
	}
}

//EOF
