/*
 * Copyright 2001-2006, Haiku, Inc.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Marc Flerackers (mflerackers@androme.be)
 *		Stefano Ceccherini (burton666@libero.it)
 */


#include <Application.h>
#include <Looper.h>
#include <MenuItem.h>
#include <PopUpMenu.h>
#include <Window.h>

#include <new>

#include <binary_compatibility/Interface.h>


struct popup_menu_data {
	BPopUpMenu* object;
	BWindow* window;
	BMenuItem* selected;

	BPoint where;
	BRect rect;

	bool async;
	bool autoInvoke;
	bool startOpened;
	bool useRect;

	sem_id lock;
};


BPopUpMenu::BPopUpMenu(const char* name, bool radioMode, bool labelFromMarked,
	menu_layout layout)
	:
	BMenu(name, layout),
	fUseWhere(false),
	fAutoDestruct(false),
	fTrackThread(-1)
{
	if (radioMode)
		SetRadioMode(true);

	if (labelFromMarked)
		SetLabelFromMarked(true);
}


BPopUpMenu::BPopUpMenu(BMessage* archive)
	:
	BMenu(archive),
	fUseWhere(false),
	fAutoDestruct(false),
	fTrackThread(-1)
{
}


BPopUpMenu::~BPopUpMenu()
{
	if (fTrackThread >= 0) {
		status_t status;
		while (wait_for_thread(fTrackThread, &status) == B_INTERRUPTED)
			;
	}
}


status_t
BPopUpMenu::Archive(BMessage* data, bool deep) const
{
	return BMenu::Archive(data, deep);
}


BArchivable*
BPopUpMenu::Instantiate(BMessage* data)
{
	if (validate_instantiation(data, "BPopUpMenu"))
		return new BPopUpMenu(data);

	return NULL;
}


BMenuItem*
BPopUpMenu::Go(BPoint where, bool deliversMessage, bool openAnyway, bool async)
{
	return _Go(where, deliversMessage, openAnyway, NULL, async);
}


BMenuItem*
BPopUpMenu::Go(BPoint where, bool deliversMessage, bool openAnyway,
	BRect clickToOpen, bool async)
{
	return _Go(where, deliversMessage, openAnyway, &clickToOpen, async);
}


void
BPopUpMenu::MessageReceived(BMessage* message)
{
	BMenu::MessageReceived(message);
}


void
BPopUpMenu::MouseDown(BPoint point)
{
	BView::MouseDown(point);
}


void
BPopUpMenu::MouseUp(BPoint point)
{
	BView::MouseUp(point);
}


void
BPopUpMenu::MouseMoved(BPoint point, uint32 code, const BMessage* message)
{
	BView::MouseMoved(point, code, message);
}


void
BPopUpMenu::AttachedToWindow()
{
	BMenu::AttachedToWindow();
}


void
BPopUpMenu::DetachedFromWindow()
{
	BMenu::DetachedFromWindow();
}


void
BPopUpMenu::FrameMoved(BPoint newPosition)
{
	BMenu::FrameMoved(newPosition);
}


void
BPopUpMenu::FrameResized(float newWidth, float newHeight)
{
	BMenu::FrameResized(newWidth, newHeight);
}


BHandler*
BPopUpMenu::ResolveSpecifier(BMessage* message, int32 index,
	BMessage* specifier, int32 form, const char* property)
{
	return BMenu::ResolveSpecifier(message, index, specifier, form, property);
}


status_t
BPopUpMenu::GetSupportedSuites(BMessage* data)
{
	return BMenu::GetSupportedSuites(data);
}


status_t
BPopUpMenu::Perform(perform_code code, void* _data)
{
	switch (code) {
		case PERFORM_CODE_MIN_SIZE:
			((perform_data_min_size*)_data)->return_value
				= BPopUpMenu::MinSize();
			return B_OK;
		case PERFORM_CODE_MAX_SIZE:
			((perform_data_max_size*)_data)->return_value
				= BPopUpMenu::MaxSize();
			return B_OK;
		case PERFORM_CODE_PREFERRED_SIZE:
			((perform_data_preferred_size*)_data)->return_value
				= BPopUpMenu::PreferredSize();
			return B_OK;
		case PERFORM_CODE_LAYOUT_ALIGNMENT:
			((perform_data_layout_alignment*)_data)->return_value
				= BPopUpMenu::LayoutAlignment();
			return B_OK;
		case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
			((perform_data_has_height_for_width*)_data)->return_value
				= BPopUpMenu::HasHeightForWidth();
			return B_OK;
		case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
		{
			perform_data_get_height_for_width* data
				= (perform_data_get_height_for_width*)_data;
			BPopUpMenu::GetHeightForWidth(data->width, &data->min, &data->max,
				&data->preferred);
			return B_OK;
		}
		case PERFORM_CODE_SET_LAYOUT:
		{
			perform_data_set_layout* data = (perform_data_set_layout*)_data;
			BPopUpMenu::SetLayout(data->layout);
			return B_OK;
		}
		case PERFORM_CODE_LAYOUT_INVALIDATED:
		{
			perform_data_layout_invalidated* data
				= (perform_data_layout_invalidated*)_data;
			BPopUpMenu::LayoutInvalidated(data->descendants);
			return B_OK;
		}
		case PERFORM_CODE_DO_LAYOUT:
		{
			BPopUpMenu::DoLayout();
			return B_OK;
		}
	}

	return BMenu::Perform(code, _data);
}


void
BPopUpMenu::ResizeToPreferred()
{
	BMenu::ResizeToPreferred();
}


void
BPopUpMenu::GetPreferredSize(float* _width, float* _height)
{
	BMenu::GetPreferredSize(_width, _height);
}


void
BPopUpMenu::MakeFocus(bool state)
{
	BMenu::MakeFocus(state);
}


void
BPopUpMenu::AllAttached()
{
	BMenu::AllAttached();
}


void
BPopUpMenu::AllDetached()
{
	BMenu::AllDetached();
}


void
BPopUpMenu::SetAsyncAutoDestruct(bool on)
{
	fAutoDestruct = on;
}


bool
BPopUpMenu::AsyncAutoDestruct() const
{
	return fAutoDestruct;
}


BPoint
BPopUpMenu::ScreenLocation()
{
	// This case is when the BPopUpMenu is standalone
	if (fUseWhere)
		return fWhere;

	// This case is when the BPopUpMenu is inside a BMenuField
	BMenuItem* superItem = Superitem();
	BMenu* superMenu = Supermenu();
	BMenuItem* selectedItem = FindItem(superItem->Label());
	BPoint point = superItem->Frame().LeftTop();

	superMenu->ConvertToScreen(&point);

	if (selectedItem != NULL) {
		while (selectedItem->Menu() != this
			&& selectedItem->Menu()->Superitem() != NULL) {
			selectedItem = selectedItem->Menu()->Superitem();
		}
		point.y -= selectedItem->Frame().top;
	}

	return point;
}


//	#pragma mark - private methods


void BPopUpMenu::_ReservedPopUpMenu1() {}
void BPopUpMenu::_ReservedPopUpMenu2() {}
void BPopUpMenu::_ReservedPopUpMenu3() {}


BPopUpMenu&
BPopUpMenu::operator=(const BPopUpMenu& other)
{
	return *this;
}


BMenuItem*
BPopUpMenu::_Go(BPoint where, bool autoInvoke, bool startOpened,
	BRect* _specialRect, bool async)
{
	if (fTrackThread >= B_OK) {
		// we already have an active menu, wait for it to go away before
		// spawning another
		status_t unused;
		while (wait_for_thread(fTrackThread, &unused) == B_INTERRUPTED)
			;
	}

	popup_menu_data* data = new (std::nothrow) popup_menu_data;
	if (data == NULL)
		return NULL;

	sem_id sem = create_sem(0, "window close lock");
	if (sem < B_OK) {
		delete data;
		return NULL;
	}

	// Get a pointer to the window from which Go() was called
	BWindow* window
		= dynamic_cast<BWindow*>(BLooper::LooperForThread(find_thread(NULL)));
	data->window = window;

	// Asynchronous menu: we set the BWindow menu's semaphore
	// and let BWindow block when needed
	if (async && window != NULL)
		_set_menu_sem_(window, sem);

	data->object = this;
	data->autoInvoke = autoInvoke;
	data->useRect = _specialRect != NULL;
	if (_specialRect != NULL)
		data->rect = *_specialRect;
	data->async = async;
	data->where = where;
	data->startOpened = startOpened;
	data->selected = NULL;
	data->lock = sem;

	// Spawn the tracking thread
	fTrackThread = spawn_thread(_thread_entry, "popup", B_DISPLAY_PRIORITY,
		data);
	if (fTrackThread < B_OK) {
		// Something went wrong. Cleanup and return NULL
		delete_sem(sem);
		if (async && window != NULL)
			_set_menu_sem_(window, B_BAD_SEM_ID);
		delete data;
		return NULL;
	}

	resume_thread(fTrackThread);

	if (!async)
		return _WaitMenu(data);

	return 0;
}


/* static */
int32
BPopUpMenu::_thread_entry(void* menuData)
{
	popup_menu_data* data = static_cast<popup_menu_data*>(menuData);
	BPopUpMenu* menu = data->object;
	BRect* rect = NULL;

	if (data->useRect)
		rect = &data->rect;

	data->selected = menu->_StartTrack(data->where, data->autoInvoke,
		data->startOpened, rect);

	// Reset the window menu semaphore
	if (data->async && data->window)
		_set_menu_sem_(data->window, B_BAD_SEM_ID);

	delete_sem(data->lock);

	// Commit suicide if needed
	if (data->async && menu->fAutoDestruct) {
		menu->fTrackThread = -1;
		delete menu;
	}

	if (data->async)
		delete data;

	return 0;
}


BMenuItem*
BPopUpMenu::_StartTrack(BPoint where, bool autoInvoke, bool startOpened,
	BRect* _specialRect)
{
	// I know, this doesn't look senseful, but don't be fooled,
	// fUseWhere is used in ScreenLocation(), which is a virtual
	// called by BMenu::Track()
	fWhere = where;
	fUseWhere = true;

	// Determine when mouse-down-up will be taken as a 'press',
	// rather than a 'click'
	bigtime_t clickMaxTime = 0;
	get_click_speed(&clickMaxTime);
	clickMaxTime += system_time();

	// Show the menu's window
	Show();
	snooze(50000);
	BMenuItem* result = Track(startOpened, _specialRect);

	// If it was a click, keep the menu open and tracking
	if (system_time() <= clickMaxTime)
		result = Track(true, _specialRect);
	if (result != NULL && autoInvoke)
		result->Invoke();

	fUseWhere = false;

	Hide();
	be_app->ShowCursor();

	return result;
}


BMenuItem*
BPopUpMenu::_WaitMenu(void* _data)
{
	popup_menu_data* data = static_cast<popup_menu_data*>(_data);
	BWindow* window = data->window;
	sem_id sem = data->lock;
	if (window != NULL) {
		while (acquire_sem_etc(sem, 1, B_TIMEOUT, 50000) != B_BAD_SEM_ID)
			window->UpdateIfNeeded();
	}

 	status_t unused;
	while (wait_for_thread(fTrackThread, &unused) == B_INTERRUPTED)
		;

	fTrackThread = -1;

	BMenuItem* selected = data->selected;
		// data->selected is filled by the tracking thread

	delete data;

	return selected;
}
