/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "lure/menu.h"
#include "lure/luredefs.h"
#include "lure/decode.h"
#include "lure/surface.h"
#include "lure/res_struct.h"
#include "lure/res.h"
#include "lure/strings.h"
#include "lure/room.h"
#include "lure/events.h"
#include "lure/lure.h"

namespace Lure {

MenuRecord::MenuRecord(const MenuRecordBounds *bounds, int numParams, ...) {
	// Store list of pointers to strings
	va_list params;

	_numEntries = numParams;
	_entries = (const char **) malloc(sizeof(const char *) * _numEntries);

	va_start(params, numParams);
	for (int index = 0; index < _numEntries; ++index)
		_entries[index] = va_arg(params, const char *);
	va_end(params);

	// Store position data
	_hsxstart = bounds->left; _hsxend = bounds->right;
	_xstart = bounds->contentsX << 3;
	_width = (bounds->contentsWidth + 3) << 3;
}

MenuRecord::~MenuRecord() {
	free(_entries);
	_entries = nullptr;
}

const char *MenuRecord::getEntry(uint8 index) {
	if (index >= _numEntries) error("Invalid menuitem index specified: %d", index);
	return _entries[index];
}

/*--------------------------------------------------------------------------*/

static Menu *int_menu = nullptr;

const MenuRecordLanguage menuList[] = {
	{Common::EN_ANY, {{40, 87, 3, 7}, {127, 179, 13, 12}, {224, 281, 27, 10}}},
	{Common::IT_ITA, {{40, 98, 4, 6}, {120, 195, 14, 11}, {208, 281, 24, 13}}},
	{Common::FR_FRA, {{40, 90, 3, 7}, {120, 195, 13, 11}, {232, 273, 23, 13}}},
	{Common::DE_DEU, {{44, 95, 1, 11}, {135, 178, 8, 23}, {232, 273, 22, 15}}},
	{Common::ES_ESP, {{40, 90, 3, 8}, {120, 195, 11, 13}, {208, 281, 17, 18}}},
	{Common::RU_RUS, {{40, 87, 3, 7}, {127, 179, 13, 12}, {224, 281, 27, 10}}},
	{Common::UNK_LANG, {{0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}}
};

Menu::Menu() {
	int_menu = this;
	StringList &sl = Resources::getReference().stringList();
	Common::Language language = LureEngine::getReference().getLanguage();

	MemoryBlock *data = Disk::getReference().getEntry(MENU_RESOURCE_ID);
	PictureDecoder decoder;
	_menu = decoder.decode(data, SCREEN_SIZE);
	delete data;

	const MenuRecordLanguage *rec = &menuList[0];
	while ((rec->language != Common::UNK_LANG) && (rec->language != language))
		++rec;
	if (rec->language == Common::UNK_LANG)
		error("Unknown language encountered in top line handler");

	_menus[0] = new MenuRecord(&rec->menus[0], 1, sl.getString(S_CREDITS));
	_menus[1] = new MenuRecord(&rec->menus[1], 3,
		sl.getString(S_RESTART_GAME), sl.getString(S_SAVE_GAME), sl.getString(S_RESTORE_GAME));
	_menus[2] = new MenuRecord(&rec->menus[2], 3,
		sl.getString(S_QUIT), sl.getString(S_SLOW_TEXT), sl.getString(S_SOUND_ON));

	_selectedMenu = nullptr;
}

Menu::~Menu() {
	for (int ctr=0; ctr<NUM_MENUS; ++ctr) delete _menus[ctr];
	delete _menu;
}

Menu &Menu::getReference() {
	return *int_menu;
}

uint8 Menu::execute() {
	OSystem &system = *g_system;
	LureEngine &engine = LureEngine::getReference();
	Mouse &mouse = Mouse::getReference();
	Events &events = Events::getReference();
	Screen &screen = Screen::getReference();

	mouse.setCursorNum(CURSOR_ARROW);
	system.copyRectToScreen(_menu->data(), FULL_SCREEN_WIDTH, 0, 0,
		FULL_SCREEN_WIDTH, MENUBAR_Y_SIZE);

	_selectedMenu = nullptr;
	_surfaceMenu = nullptr;
	_selectedIndex = 0;

	while (mouse.lButton() || mouse.rButton() || g_system->hasFeature(OSystem::kFeatureTouchscreen)) {
		while (events.pollEvent()) {
			if (engine.shouldQuit()) return MENUITEM_NONE;

			if (mouse.y() < MENUBAR_Y_SIZE) {
				MenuRecord *p = getMenuAt(mouse.x());

				if (_selectedMenu != p) {
					// If necessary, remove prior menu
					if (_selectedMenu) {
						toggleHighlight(_selectedMenu);
						screen.updateArea(0, 0, FULL_SCREEN_WIDTH, _surfaceMenu->height() + 8);
						delete _surfaceMenu;
						_surfaceMenu = nullptr;
						_selectedIndex = 0;
					}

					_selectedMenu = p;

					// If a new menu is selected, show it
					if (_selectedMenu) {
						toggleHighlight(_selectedMenu);
						_surfaceMenu = Surface::newDialog(
							_selectedMenu->width(), _selectedMenu->numEntries(),
							_selectedMenu->entries(), false, DEFAULT_TEXT_COLOR, false);
						_surfaceMenu->copyToScreen(_selectedMenu->xstart(), MENUBAR_Y_SIZE);
					}

					system.copyRectToScreen(_menu->data(), FULL_SCREEN_WIDTH, 0, 0,
						FULL_SCREEN_WIDTH, MENUBAR_Y_SIZE);
				}
			}

			// Check for changing selected index
			uint8 index = getIndexAt(mouse.x(), mouse.y());
			if (index != _selectedIndex) {
				if (_selectedIndex != 0) toggleHighlightItem(_selectedIndex);
				_selectedIndex = index;
				if (_selectedIndex != 0) toggleHighlightItem(_selectedIndex);
			}
		}

		if (g_system->hasFeature(OSystem::kFeatureTouchscreen)) {
			// Close menu only when a sub menu is shown and
			// the user has either clicked on a selected index
			// or no index (outside the sub menu == cancelled)
			if (mouse.lButton() &&
				_surfaceMenu != nullptr)  {
				break;
			}
		}

		system.updateScreen();
		system.delayMillis(10);
	}

	delete _surfaceMenu;

	// Deselect the currently selected menu header
	if (_selectedMenu)
		toggleHighlight(_selectedMenu);

	// Restore the previous screen
	screen.update();

	if ((_selectedMenu == nullptr) || (_selectedIndex == 0)) return MENUITEM_NONE;
	else if (_selectedMenu == _menus[0])
		return MENUITEM_CREDITS;
	else if (_selectedMenu == _menus[1]) {
		switch (_selectedIndex) {
		case 1:
			return MENUITEM_RESTART_GAME;
		case 2:
			return MENUITEM_SAVE_GAME;
		case 3:
			return MENUITEM_RESTORE_GAME;
		default:
			break;
		}
	} else {
		switch (_selectedIndex) {
		case 1:
			return MENUITEM_QUIT;
		case 2:
			return MENUITEM_TEXT_SPEED;
		case 3:
			return MENUITEM_SOUND;
		default:
			break;
		}
	}
	return MENUITEM_NONE;
}

MenuRecord *Menu::getMenuAt(int x) {
	for (int ctr = 0; ctr < NUM_MENUS; ++ctr)
		if ((x >= _menus[ctr]->hsxstart()) && (x <= _menus[ctr]->hsxend()))
			return _menus[ctr];

	return nullptr;
}

uint8 Menu::getIndexAt(uint16 x, uint16 y) {
	if (!_selectedMenu) return 0;

	int ys = MENUBAR_Y_SIZE + Surface::textY();
	int ye = MENUBAR_Y_SIZE + (_surfaceMenu->height() - Surface::textY());
	if ((y < ys) || (y > ye)) return 0;

	uint16 yRelative = y - ys;
	uint8 index = (uint8) (yRelative / 8) + 1;
	if (index > _selectedMenu->numEntries()) index = _selectedMenu->numEntries();
	return index;
}

#define MENUBAR_SELECTED_COLOR 0xf7

void Menu::toggleHighlight(MenuRecord *menuRec) {
	const byte colorList[4] = {4, 2, 0, 0xf7};
	const byte *colors = LureEngine::getReference().isEGA() ? &colorList[0] : &colorList[2];
	byte *addr = _menu->data();

	for (uint16 y=0; y<MENUBAR_Y_SIZE; ++y) {
		for (uint16 x=menuRec->hsxstart(); x<=menuRec->hsxend(); ++x) {
			if (addr[x] == colors[0]) addr[x] = colors[1];
			else if (addr[x] == colors[1]) addr[x] = colors[0];
		}
		addr += FULL_SCREEN_WIDTH;
	}
}

void Menu::toggleHighlightItem(uint8 index) {
	const byte colorList[4] = {EGA_DIALOG_TEXT_COLOR, EGA_DIALOG_WHITE_COLOR,
		VGA_DIALOG_TEXT_COLOR, VGA_DIALOG_WHITE_COLOR};
	const byte *colors = LureEngine::getReference().isEGA() ? &colorList[0] : &colorList[2];
	byte *p = _surfaceMenu->data().data() + (Surface::textY() +
		((index - 1) * FONT_HEIGHT)) * _surfaceMenu->width() + Surface::textX();
	int numBytes =_surfaceMenu->width() - Surface::textX() * 2;

	for (int y = 0; y < FONT_HEIGHT; ++y, p += _surfaceMenu->width()) {
		byte *pTemp = p;

		for (int x = 0; x < numBytes; ++x, ++pTemp) {
			if (*pTemp == colors[0]) *pTemp = colors[1];
			else if (*pTemp == colors[1]) *pTemp = colors[0];
		}
	}

	_surfaceMenu->copyToScreen(_selectedMenu->xstart(), MENUBAR_Y_SIZE);
}

/*--------------------------------------------------------------------------*/

uint16 PopupMenu::ShowInventory() {
	Resources &rsc = Resources::getReference();
	StringData &strings = StringData::getReference();

	uint16 numItems = rsc.numInventoryItems();
	uint16 itemCtr = 0;
	char **itemNames = (char **) Memory::alloc(sizeof(char *) * numItems);
	uint16 *idList = (uint16 *) Memory::alloc(sizeof(uint16) * numItems);

	HotspotDataList::iterator i;
	for (i = rsc.hotspotData().begin(); i != rsc.hotspotData().end(); ++i) {
		HotspotData const &hotspot = **i;
		if (hotspot.roomNumber == PLAYER_ID) {
			idList[itemCtr] = hotspot.hotspotId;
			char *hotspotName = itemNames[itemCtr++] = (char *) malloc(MAX_HOTSPOT_NAME_SIZE);
			strings.getString(hotspot.nameId, hotspotName);
		}
	}

	uint16 result = Show(numItems, const_cast<const char **>(itemNames));
	if (result != 0xffff) result = idList[result];

	for (itemCtr = 0; itemCtr < numItems; ++itemCtr)
		free(itemNames[itemCtr]);

	Memory::dealloc(itemNames);
	Memory::dealloc(idList);
	return result;
}

#define MAX_NUM_DISPLAY_ITEMS 20

uint16 PopupMenu::ShowItems(Action contextAction, uint16 roomNumber) {
	Resources &res = Resources::getReference();
	ValueTableData &fields = res.fieldList();
	RoomDataList &rooms = res.roomData();
	HotspotDataList &hotspots = res.hotspotData();
	StringData &strings = StringData::getReference();
	Room &room = Room::getReference();
	Screen &screen = Screen::getReference();
	Mouse &mouse = Mouse::getReference();
	RoomDataList::iterator ir;
	HotspotDataList::iterator ih;
	uint16 entryIds[MAX_NUM_DISPLAY_ITEMS];
	uint16 nameIds[MAX_NUM_DISPLAY_ITEMS];
	char *entryNames[MAX_NUM_DISPLAY_ITEMS];
	int numItems = 0;
	int itemCtr;
	uint32 contextBitflag = 1 << (contextAction - 1);

	// Loop for rooms
	for (ir = rooms.begin(); ir != rooms.end(); ++ir) {
		RoomData const &roomData = **ir;
		// Pre-condition checks for whether to skip room
		if ((roomData.hdrFlags != 15) && ((roomData.hdrFlags & fields.hdrFlagMask()) == 0))
			continue;
		if (((roomData.flags & HOTSPOTFLAG_MENU_EXCLUSION) != 0) || ((roomData.flags & HOTSPOTFLAG_FOUND) == 0))
			continue;
		if ((roomData.actions & contextBitflag) == 0)
			continue;

		// Add room to list of entries to display
		if (numItems == MAX_NUM_DISPLAY_ITEMS) error("Out of space in ask list");
		entryIds[numItems] = roomData.roomNumber;
		nameIds[numItems] = roomData.roomNumber;
		entryNames[numItems] = (char *) Memory::alloc(MAX_HOTSPOT_NAME_SIZE);
		strings.getString(roomData.roomNumber, entryNames[numItems]);
		++numItems;
	}

	// Loop for hotspots
	for (ih = hotspots.begin(); ih != hotspots.end(); ++ih) {
		HotspotData const &hotspot = **ih;

		if ((hotspot.headerFlags != 15) &&
			((hotspot.headerFlags & fields.hdrFlagMask()) == 0))
			continue;

		if (((hotspot.flags & HOTSPOTFLAG_MENU_EXCLUSION) != 0) || ((hotspot.flags & HOTSPOTFLAG_FOUND) == 0))
			// Skip the current hotspot
			continue;

		// If the hotspot is room specific, skip if the character will not be in the specified room
		if (((hotspot.flags & HOTSPOTFLAG_ROOM_SPECIFIC) != 0) &&
			(hotspot.roomNumber != roomNumber))
			continue;

		// If hotspot does not allow action, then skip it
		if ((hotspot.actions & contextBitflag) == 0)
			continue;

		// If a special hotspot Id, then skip displaying
		if ((hotspot.nameId == 0x17A) || (hotspot.nameId == 0x147))
			continue;

		// Check if the hotspot's name is already used in an already set item
		itemCtr = 0;
		while ((itemCtr < numItems) && (nameIds[itemCtr] != hotspot.nameId))
			++itemCtr;
		if (itemCtr != numItems)
			// Item's name is already present - skip hotspot
			continue;

		// Add hotspot to list of entries to display
		if (numItems == MAX_NUM_DISPLAY_ITEMS) error("Out of space in ask list");
		entryIds[numItems] = hotspot.hotspotId;
		nameIds[numItems] = hotspot.nameId;
		entryNames[numItems] = (char *) Memory::alloc(MAX_HOTSPOT_NAME_SIZE);
		strings.getString(hotspot.nameId, entryNames[numItems]);
		++numItems;
	}

	if (numItems == 0) {
		// No items, so add a 'nothing' to the statusLine
		if (LureEngine::getReference().getLanguage() == Common::RU_RUS)
			Common::strcat_s(room.statusLine(), MAX_DESC_SIZE, "(ybxtuj ytn)");
		else
			Common::strcat_s(room.statusLine(), MAX_DESC_SIZE, "(nothing)");
	}

	room.update();
	screen.update();
	mouse.waitForRelease();

	if (numItems == 0)
		// Return flag for no items to ask for
		return 0xfffe;

	// Display items
	uint16 result = Show(numItems, const_cast<const char **>(entryNames));
	if (result != 0xffff) result = entryIds[result];

	// Deallocate display strings
	for (itemCtr = 0; itemCtr < numItems; ++itemCtr)
		Memory::dealloc(entryNames[itemCtr]);

	return result;
}

static int entryCompare(const char **p1, const char **p2) {
	return strcmp(*p1, *p2);
}

typedef int (*CompareMethod)(const void*, const void*);

Action PopupMenu::Show(uint32 actionMask) {
	StringList &stringList = Resources::getReference().stringList();
	int numEntries = 0;
	uint32 v = actionMask;
	int index;
	int currentAction;
	uint16 resultIndex;
	Action resultAction;

	for (index = 1; index <= EXAMINE; ++index, v >>= 1) {
		if (v & 1) ++numEntries;
	}

	const char **strList = (const char **) Memory::alloc(sizeof(char *) * numEntries);

	int strIndex = 0;
	for (currentAction = 0; currentAction < (int)EXAMINE; ++currentAction) {
		if ((actionMask & (1 << currentAction)) != 0) {
			strList[strIndex] = stringList.getString(currentAction);
			++strIndex;
		}
	}

	// Sort the list
	qsort(strList, numEntries, sizeof(const char *), (CompareMethod) entryCompare);

	// Show the entries
	resultIndex = Show(numEntries, strList);

	resultAction = NONE;
	if (resultIndex != 0xffff) {
		// Scan through the list of actions to find the selected entry
		for (currentAction = 0; currentAction < (int)EXAMINE; ++currentAction) {
			if (strList[resultIndex] == stringList.getString(currentAction)) {
				resultAction = (Action) (currentAction + 1);
				break;
			}
		}
	}

	Memory::dealloc(strList);
	return resultAction;
}

Action PopupMenu::Show(int numEntries, Action *actions) {
	StringList &stringList = Resources::getReference().stringList();
	const char **strList = (const char **) Memory::alloc(sizeof(char *) * numEntries);
	Action *actionPtr = actions;
	for (int index = 0; index < numEntries; ++index)
		strList[index] = stringList.getString(*actionPtr++);
	uint16 result = Show(numEntries, strList);

	Memory::dealloc(strList);
	if (result == 0xffff) return NONE;
	else return actions[result];
}

uint16 PopupMenu::Show(int numEntries, const char *actions[]) {
	if (numEntries == 0) return 0xffff;
	LureEngine &engine = LureEngine::getReference();
	Events &e = Events::getReference();
	Mouse &mouse = Mouse::getReference();
	OSystem &system = *g_system;
	Screen &screen = Screen::getReference();
	bool isEGA = LureEngine::getReference().isEGA();
	byte textColor = isEGA ? EGA_DIALOG_TEXT_COLOR : VGA_DIALOG_TEXT_COLOR;
	byte whiteColor = isEGA ? EGA_DIALOG_WHITE_COLOR : VGA_DIALOG_WHITE_COLOR;


	const uint16 yMiddle = FULL_SCREEN_HEIGHT / 2;

	uint16 numLines = 0, oldX = 0, oldY = 0;
	bool clickable_menu = g_system->hasFeature(OSystem::kFeatureTouchscreen);
	if (clickable_menu) {
		// The whole menu is shown and the items are click-selectable
		mouse.pushCursorNum(CURSOR_ARROW);
		numLines = numEntries;
	} else {
		oldX = mouse.x();
		oldY = mouse.y();
		mouse.cursorOff();
		mouse.setPosition(FULL_SCREEN_WIDTH / 2, yMiddle);

		// Round up number of lines in dialog to next odd number
		numLines = (numEntries / 2) * 2 + 1;
		if (numLines > 5) numLines = 5;
	}

	// Figure out the character width
	uint16 numCols = 0;
	for (int ctr = 0; ctr < numEntries; ++ctr) {
		int len = strlen(actions[ctr]);
		if (len > numCols)
			numCols = len;
	}

	// Create the dialog surface
	Common::Point size;
	Surface::getDialogBounds(size, numCols, numLines, false);
	Surface *s = new Surface(size.x, size.y);
	s->createDialog();

	int selectedIndex = 0;
	bool refreshFlag = true;

	Common::Rect r;
	if (clickable_menu) {
		r.left = Surface::textX();
		r.right = s->width() - Surface::textX() + 1;
		r.top = Surface::textY();
		r.bottom = s->height() - Surface::textY() + 1;
	}

	bool bailOut = false;

	while (!bailOut) {
		if (refreshFlag) {
			// Set up the contents of the menu
			s->refreshDialog();

			for (int index = 0; index < numLines; ++index) {
				int actionIndex = clickable_menu ? index : selectedIndex - (numLines / 2) + index;
				if ((actionIndex >= 0) && (actionIndex < numEntries)) {
					byte color = textColor;
					if (index == (clickable_menu ? selectedIndex : numLines / 2))
						color = whiteColor;
					s->writeString(Surface::textX(), Surface::textY() + index * FONT_HEIGHT,
						actions[actionIndex], true,
						color, false);
				}
			}

			s->copyToScreen(0, yMiddle-(s->height() / 2));
			system.updateScreen();
			refreshFlag = false;
		}

		while (e.pollEvent()) {
			if (engine.shouldQuit()) {
				selectedIndex = 0xffff;
				bailOut = true;
				break;
			} else if (e.type() == Common::EVENT_WHEELUP) {
				// Scroll upwards
				if (selectedIndex > 0) {
					--selectedIndex;
					refreshFlag = true;
				}
			} else if (e.type() == Common::EVENT_WHEELDOWN) {
				// Scroll downwards
				if (selectedIndex < numEntries - 1) {
					++selectedIndex;
					refreshFlag = true;
				}
			} else if (e.type() == Common::EVENT_CUSTOM_ENGINE_ACTION_START) {
				uint16 keycode = e.event().customType;

				if ((keycode == kActionIndexPrevious) && (selectedIndex > 0)) {
					--selectedIndex;
					refreshFlag = true;
				} else if ((keycode == kActionIndexNext) &&
						(selectedIndex < numEntries-1)) {
					++selectedIndex;
					refreshFlag = true;
				} else if (keycode == kActionIndexSelect) {
					bailOut = true;
					break;
				} else if (keycode == kActionEscape) {
					selectedIndex = 0xffff;
					bailOut = true;
					break;
				}
			} else if (clickable_menu && (e.type() == Common::EVENT_LBUTTONDOWN || e.type() == Common::EVENT_MOUSEMOVE)) {
				int16 x = mouse.x();
				int16 y = mouse.y() - yMiddle + (s->height() / 2);
				refreshFlag = true;

				if (r.contains(x, y)) {
					selectedIndex = (y - r.top) / FONT_HEIGHT;
					if (e.type() == Common::EVENT_LBUTTONDOWN) {
						bailOut = true;
						break;
					}
				}
			} else if (!clickable_menu && ((e.type() == Common::EVENT_LBUTTONDOWN) ||
					(e.type() == Common::EVENT_MBUTTONDOWN))) {
				//mouse.waitForRelease();
				bailOut = true;
				break;
			} else if (e.type() == Common::EVENT_RBUTTONDOWN) {
				mouse.waitForRelease();
				selectedIndex = 0xffff;
				bailOut = true;
				break;
			}
		}

		if (!bailOut) {
			if (!clickable_menu) {
				// Warping the mouse to "neutral" even if the top/bottom menu
				// entry has been reached has both pros and cons. It makes the
				// menu behave a bit more sensibly, but it also makes it harder
				// to move the mouse pointer out of the ScummVM window.

				if (mouse.y() < yMiddle - POPMENU_CHANGE_SENSITIVITY) {
					if (selectedIndex > 0) {
						--selectedIndex;
						refreshFlag = true;
					}
					mouse.setPosition(FULL_SCREEN_WIDTH / 2, yMiddle);
				} else if (mouse.y() > yMiddle + POPMENU_CHANGE_SENSITIVITY) {
					if (selectedIndex < numEntries - 1) {
						++selectedIndex;
						refreshFlag = true;
					}
					mouse.setPosition(FULL_SCREEN_WIDTH / 2, yMiddle);
				}
			}

			system.delayMillis(20);
		}
	}

	// bailOut
	delete s;

	if (clickable_menu) {
		mouse.popCursor();
	} else {
		mouse.setPosition(oldX, oldY);
		mouse.cursorOn();
	}
	screen.update();
	return selectedIndex;
}

} // End of namespace Lure
