/* 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 "common/system.h"
#include "common/config-manager.h"
#include "common/enc-internal.h"
#include "common/macresman.h"
#include "common/str.h"

#include "graphics/cursorman.h"
#include "graphics/maccursor.h"
#include "graphics/macgamma.h"
#include "graphics/paletteman.h"
#include "graphics/fonts/macfont.h"
#include "graphics/macgui/macwindowmanager.h"

#include "image/cicn.h"
#include "image/pict.h"

#include "scumm/macgui/macgui_impl.h"
#include "scumm/scumm.h"
#include "scumm/scumm_v4.h"

namespace Scumm {

static void activateMenuCallback(void *ref) {
	MacGuiImpl *gui = (MacGuiImpl *)ref;

	gui->onMenuOpen();
}

// ===========================================================================
// Base class for Macintosh game user interface. Handles menus, fonts, image
// loading, etc.
// ===========================================================================

MacGuiImpl::MacGuiImpl(ScummEngine *vm, const Common::Path &resourceFile) : _vm(vm), _system(_vm->_system), _surface(_vm->_macScreen), _resourceFile(resourceFile) {
	_fonts.clear();
	_strsStrings.clear();

	// kMacRomanConversionTable is a conversion table from Mac Roman
	// 128-255 to unicode. What we need, however, is a mapping from
	// unicode 160-255 to Mac Roman.

	for (int i = 0; i < ARRAYSIZE(_unicodeToMacRoman); i++)
		_unicodeToMacRoman[i] = 0;

	for (int i = 0; i < ARRAYSIZE(Common::kMacRomanConversionTable); i++) {
		int unicode = Common::kMacRomanConversionTable[i];

		if (unicode >= 160 && unicode <= 255)
			_unicodeToMacRoman[unicode - 160] = 128 + i;
	}
}

MacGuiImpl::~MacGuiImpl() {
	delete _bannerWindow;
	delete _windowManager;
}

uint32 MacGuiImpl::getBlack() const {
	return _windowManager->_colorBlack;
}

uint32 MacGuiImpl::getWhite() const {
	return _windowManager->_colorWhite;
}

int MacGuiImpl::toMacRoman(int unicode) const {
	if (unicode >= 32 && unicode <= 127)
		return unicode;

	if (unicode < 160 || unicode > 255)
		return 0;

	int macRoman = _unicodeToMacRoman[unicode - 160];

	// These characters are defined in Mac Roman, but apparently not
	// present in older fonts like Chicago?

	if (macRoman >= 0xD9 && macRoman != 0xF0)
		macRoman = 0;

	return macRoman;
}

void MacGuiImpl::setPaletteDirty() {
	_paletteDirty = true;
}

void MacGuiImpl::updatePalette() {
	if (_paletteDirty && !_suspendPaletteUpdates) {
		_paletteDirty = false;
		_windowManager->passPalette(_vm->_currentPalette, getNumColors());
	}
}

bool MacGuiImpl::handleEvent(Common::Event event) {
	// The situation we're trying to avoid here is the user opening e.g.
	// the save dialog using keyboard shortcuts while the game is paused.

	if (_vm->_messageBannerActive)
		return false;

	return _windowManager->processEvent(event);
}

MacGuiImpl::DelayStatus MacGuiImpl::delay(uint32 ms) {
	uint32 to;

	to = _system->getMillis() + ms;

	while (ms == 0 || _system->getMillis() < to) {
		Common::Event event;

		while (_system->getEventManager()->pollEvent(event)) {
			switch (event.type) {
			case Common::EVENT_QUIT:
				return kDelayAborted;

			case Common::EVENT_LBUTTONDOWN:
				return kDelayInterrupted;

			default:
				break;
			}
		}

		uint32 delta = to - _system->getMillis();

		if (delta > 0) {
			_system->delayMillis(MIN<uint32>(delta, 10));
			_system->updateScreen();
		}
	}

	return kDelayDone;
}

// --------------------------------------------------------------------------
// Menu handling
//
// In the original, the menu was activated by pressing the "Command key".
// This does not seem very friendly to touch devices, so we instead use the
// "mouse over" method in the Mac Window Manager class.
//
// Later games added clicking to open the menu, and even an option to keep the
// menu bar always visible.
//
// TODO: Ideally we should perhaps implement some of these behaviors, but I'm
//       not sure which one can sensibly coexist.
// --------------------------------------------------------------------------

void MacGuiImpl::menuCallback(int id, Common::String &name, void *data) {
	MacGuiImpl *gui = (MacGuiImpl *)data;

	// This menu item (e.g. a menu separator) has no action, so it can be
	// immediately ignored.
	if (id == 0)
		return;

	Graphics::MacMenu *menu = gui->_windowManager->getMenu();
	bool forceMenuClosed = false;

	// If the menu is opened through a shortcut key, force it to activate
	// to avoid screen corruption. In that case, we also force the menu to
	// close afterwards, or the game will stay paused. Which is
	// particularly bad during a restart.

	if (!menu->_active) {
		gui->_windowManager->activateMenu();
		forceMenuClosed = true;
	}

	// This is how we keep the menu bar visible.

	menu->closeMenu();
	menu->setActive(true);
	menu->setVisible(true);
	gui->updateWindowManager();

	gui->handleMenu(id, name);

	if (forceMenuClosed)
		menu->closeMenu();
}

bool MacGuiImpl::initialize() {
	if (!readStrings()) {
		// FIXME: THIS IS A TEMPORARY WORKAROUND
		// The Mac GUI initialization must never fail for Loom or Last Crusade,
		// because it's used for more than just the Mac menus and dialogs. So
		// for those games we just silently disable the original GUI, and let
		// the game start anyway.
		if (_vm->_game.id == GID_LOOM || _vm->_game.id == GID_INDY3)
			_vm->_useOriginalGUI = false;
		else
			return false;
	}

	uint32 menuMode = Graphics::kWMModeNoDesktop | Graphics::kWMModeAutohideMenu |
		Graphics::kWMModalMenuMode | Graphics::kWMModeNoCursorOverride | Graphics::kWMModeForceMacFonts;

	// Allow a more modern UX: the menu doesn't close if the mouse accidentally goes outside the menu area
	if (_vm->enhancementEnabled(kEnhUIUX))
		menuMode |= Graphics::kWMModeWin95 | Graphics::kWMModeForceMacFontsInWin95 | Graphics::kWMModeForceMacBorder;

	_windowManager = new Graphics::MacWindowManager(menuMode);
	_windowManager->setEngine(_vm);
	_windowManager->setScreen(640, _vm->_useMacScreenCorrectHeight ? 480 : 400);
	_windowManager->setEngineActivateMenuCallback(this, activateMenuCallback);

	if (_vm->isUsingOriginalGUI()) {
		_windowManager->setMenuHotzone(Common::Rect(640, 23));
		_windowManager->setMenuDelay(250);

		Common::MacResManager resource;
		Graphics::MacMenu *menu = _windowManager->addMenu();

		resource.open(_resourceFile);

		// Add the Apple menu

		const Graphics::MacMenuData menuSubItems[] = {
			{ 0, nullptr, 0, 0, false }
		};

		menu->setCommandsCallback(menuCallback, this);

		// Newer games define their menus through an MBAR resource

		Common::SeekableReadStream *mbar = resource.getResource(MKTAG('M', 'B', 'A', 'R'), 128);

		if (mbar) {
			uint16 numMenus = mbar->readUint16BE();
			for (uint i = 0; i < numMenus; i++) {
				uint16 menuId = mbar->readUint16BE();
				addMenu(menu, menuId);
			}
			delete mbar;
		} else {
			Common::String aboutMenuDef = _strsStrings[kMSIAboutGameName].c_str();
			int maxMenu = -1;
			switch (_vm->_game.id) {
			case GID_INDY3:
			case GID_LOOM:
				maxMenu = 130;
				break;
			case GID_MONKEY:
				maxMenu = 131;
				break;
			default:
				maxMenu = 132;
			}

			if (_vm->_game.id == GID_LOOM) {
				aboutMenuDef += ";";

				if (!_vm->enhancementEnabled(kEnhUIUX))
					aboutMenuDef += "(";

				aboutMenuDef += "Drafts Inventory";
			}

			menu->addStaticMenus(menuSubItems);
			menu->createSubMenuFromString(0, aboutMenuDef.c_str(), 0);

			for (int i = 129; i <= maxMenu; i++) {
				addMenu(menu, i);
			}

			resource.close();
		}

		// Assign sensible IDs to the menu items

		int numberOfMenus = menu->numberOfMenus();

		for (int i = 0; i < numberOfMenus; i++) {
			Graphics::MacMenuItem *item = menu->getMenuItem(i);
			int numberOfMenuItems = menu->numberOfMenuItems(item);
			int id = 100 * (i + 1);
			for (int j = 0; j < numberOfMenuItems; j++) {
				Graphics::MacMenuItem *subItem = menu->getSubMenuItem(item, j);
				Common::String str = menu->getName(subItem);

				if (!str.empty()) {
					menu->setAction(subItem, id++);
				}

				if (str.contains("^3")) {
					Common::replace(str, "^3", name());
					menu->setName(subItem, str);
				}
			}
		}
	}

	// Register custom fonts. The font family just happens to match the
	// printed name of the game.

	const Common::String fontFamily = name();

	const Common::Array<Graphics::MacFontFamily *> &fontFamilies = _windowManager->_fontMan->getFontFamilies();

	_windowManager->_fontMan->loadFonts(_resourceFile);

	for (uint i = 0; i < fontFamilies.size(); i++) {
		if (fontFamilies[i]->getName() == fontFamily) {
			_gameFontId = fontFamilies[i]->getFontFamilyId();
			break;
		}
	}

	return true;
}

void MacGuiImpl::addMenu(Graphics::MacMenu *menu, int menuId) {
	Common::MacResManager resource;

	resource.open(_resourceFile);

	Common::SeekableReadStream *res = resource.getResource(MKTAG('M', 'E', 'N', 'U'), menuId);

	if (!res) {
		resource.close();
		return;
	}

	Common::StringArray *menuDef = Graphics::MacMenu::readMenuFromResource(res);
	Common::String name = menuDef->operator[](0);
	Common::String string = menuDef->operator[](1);
	int id = menu->addMenuItem(nullptr, name);

	// The CD version of Fate of Atlantis has a menu item for toggling graphics
	// smoothing. We retroactively add that to the remaining V5 games, but not
	// to Loom and Last Crusade.

	if (_vm->enhancementEnabled(kEnhUIUX)) {
		if ((_vm->_game.id == GID_MONKEY || _vm->_game.id == GID_MONKEY2) && id == 3) {
			string += ";(-;Smooth Graphics";
		}

		// Floppy version
		if (_vm->_game.id == GID_INDY4 && !string.contains("Smooth Graphics") && id == 3) {
			string += ";(-;Smooth Graphics";
		}
	}

	menu->createSubMenuFromString(id, string.c_str(), 0);

	delete menuDef;
	delete res;

	resource.close();
}

void MacGuiImpl::updateMenus() {
	// We can't use the name of the menus here, because there are
	// non-English versions. Let's hope the menu positions are always the
	// same, at least!

	Graphics::MacMenu *menu = _windowManager->getMenu();
	Graphics::MacMenuItem *gameMenu = menu->getMenuItem(1);
	Graphics::MacMenuItem *loadMenu = menu->getSubMenuItem(gameMenu, 0);
	Graphics::MacMenuItem *saveMenu = menu->getSubMenuItem(gameMenu, 1);

	loadMenu->enabled = _vm->canLoadGameStateCurrently();
	saveMenu->enabled = _vm->canSaveGameStateCurrently();
}

bool MacGuiImpl::handleMenu(int id, Common::String &name) {
	int saveSlotToHandle = -1;
	Common::String savegameName;

	switch (id) {
	case 100:	// About
		runAboutDialog();
		return true;

	case 200:	// Open
		if (runOpenDialog(saveSlotToHandle)) {
			if (saveSlotToHandle > -1) {
				_vm->loadGameState(saveSlotToHandle);
				if (_vm->_game.id == GID_INDY3)
					((ScummEngine_v4 *)_vm)->updateIQPoints();
			}
		}

		return true;

	case 201:	// Save
		_vm->beginTextInput();
		if (runSaveDialog(saveSlotToHandle, savegameName)) {
			if (saveSlotToHandle > -1) {
				_vm->saveGameState(saveSlotToHandle, savegameName);
			}
		}
		_vm->endTextInput();
		return true;

	case 202:	// Restart
		if (runRestartDialog())
			_vm->restart();
		return true;

	case 203:	// Pause
		if (!_vm->_messageBannerActive) {
			_windowManager->getMenu()->closeMenu();

			if (_vm->_game.version == 3)
				_vm->mac_showOldStyleBannerAndPause(_vm->getGUIString(gsPause), -1);
			else
				_vm->showBannerAndPause(0, -1, _vm->getGUIString(gsPause));
		}
		return true;

	// In the original, the Edit menu is active during save dialogs, though
	// only Cut, Copy and Paste.

	case 300:	// Undo
	case 301:	// Cut
	case 302:	// Copy
	case 303:	// Paste
	case 304:	// Clear
		return true;
	}

	return false;
}

void MacGuiImpl::updateWindowManager() {
	Graphics::MacMenu *menu = _windowManager->getMenu();

	if (!menu)
		return;

	updateMenus();

	if (!_windowManager->isMenuActive() && _menuIsActive)
		onMenuClose();

	if (menu->isVisible())
		updatePalette();

	_windowManager->draw();
}

void MacGuiImpl::onMenuOpen() {
	// We want the arrow cursor for menus. Note that the menu triggers even
	// when the mouse is invisible, which may or may not be a bug. But the
	// original did allow you to open the menu with Alt even when the
	// cursor was visible, so for now it's a feature.

	if (!_menuIsActive) {
		_menuIsActive = true;
		_cursorWasVisible = CursorMan.showMouse(true);
		_windowManager->pushCursor(Graphics::MacGUIConstants::kMacCursorArrow);
	}
}

void MacGuiImpl::onMenuClose() {
	if (_menuIsActive) {
		_menuIsActive = false;
		if (_windowManager->getCursorType() == Graphics::MacGUIConstants::kMacCursorArrow)
			_windowManager->popCursor();
		CursorMan.showMouse(_cursorWasVisible);
	}
}

// ---------------------------------------------------------------------------
// Font handling
// ---------------------------------------------------------------------------

const Graphics::Font *MacGuiImpl::getFont(FontId fontId) {
	const Graphics::Font *font = _fonts.getValOrDefault((int)fontId);

	if (font)
		return font;

	int id;
	int size;
	int slant;

	switch (fontId) {
	case kSystemFont:
		id = Graphics::kMacFontSystem;
		size = 12;
		slant = Graphics::kMacFontRegular;
		break;

	default:
		getFontParams(fontId, id, size, slant);
		break;
	}


	font = _windowManager->_fontMan->getFont(Graphics::MacFont(id, size, slant));
	_fonts[(int)fontId] = font;

	return font;
}

bool MacGuiImpl::getFontParams(FontId fontId, int &id, int &size, int &slant) const {
	switch (fontId) {
	case kAboutFontHeaderOutside:
		id = _gameFontId;
		size = 12;
		slant = Graphics::kMacFontItalic | Graphics::kMacFontBold | Graphics::kMacFontOutline;
		return true;

	case kAboutFontHeaderInside:
		id = _gameFontId;
		size = 12;
		slant = Graphics::kMacFontItalic | Graphics::kMacFontBold | Graphics::kMacFontExtend;
		return true;

	case kAboutFontRegular:
		id = Graphics::kMacFontApplication;
		size = 9;
		slant = Graphics::kMacFontRegular;
		return true;

	case kAboutFontBold:
		id = _gameFontId;
		size = 9;
		slant = Graphics::kMacFontRegular;
		return true;

	case kAboutFontExtraBold:
		id = _gameFontId;
		size = 9;
		slant = Graphics::kMacFontBold;
		return true;

	default:
		return false;
	}
}

Graphics::Surface *MacGuiImpl::createRemappedSurface(const Graphics::Surface *surface, const byte *palette, uint colorCount) {
	Graphics::Surface *s = new Graphics::Surface();
	s->create(surface->w, surface->h, Graphics::PixelFormat::createFormatCLUT8());

	byte paletteMap[256];
	memset(paletteMap, 0, sizeof(paletteMap));

	const byte monoPalette[] = {
		0xFF, 0xFF, 0xFF,
		0x00, 0x00, 0x00
	};

	if (colorCount == 0) {
		colorCount = 2;
		palette = monoPalette;
	}

	for (uint i = 0; i < colorCount; i++) {
		int r = palette[3 * i];
		int g = palette[3 * i + 1];
		int b = palette[3 * i + 2];

		uint32 c;

		c = _windowManager->findBestColor(r, g, b);
		paletteMap[i] = c;
	}

	// Colors outside the palette are not remapped.

	for (uint i = colorCount; i < 256; i++)
		paletteMap[i] = i;

	if (palette) {
		for (int y = 0; y < s->h; y++) {
			for (int x = 0; x < s->w; x++) {
				uint color = surface->getPixel(x, y);
				if (color > colorCount)
					color = getBlack();
				else
					color = paletteMap[color];

				s->setPixel(x, y, color);
			}
		}
	} else {
		s->copyFrom(*surface);
	}

	return s;
}

bool MacGuiImpl::setupResourceCursor(int id, int &width, int &height, int &hotspotX, int &hotspotY, int &animate) {
	Common::MacResManager resource;
	Graphics::MacCursor macCursor;
	bool success = false;

	resource.open(_resourceFile);

	Common::SeekableReadStream *curs = resource.getResource(MKTAG('C', 'U', 'R', 'S'), id);

	if (curs && macCursor.readFromStream(*curs)) {
		width = macCursor.getWidth();
		height = macCursor.getHeight();
		hotspotX = macCursor.getHotspotX();
		hotspotY = macCursor.getHotspotY();
		animate = 0;

		_windowManager->replaceCursor(Graphics::MacGUIConstants::kMacCursorCustom, &macCursor);
		success = true;
	}

	delete curs;
	resource.close();
	return success;
}

// ---------------------------------------------------------------------------
// Icon loader
// ---------------------------------------------------------------------------

bool MacGuiImpl::loadIcon(int id, Graphics::Surface **icon, Graphics::Surface **mask) {
	bool result = false;
	Common::MacResManager resource;

	resource.open(_resourceFile);

	Common::SeekableReadStream *res = resource.getResource(MKTAG('c', 'i', 'c', 'n'), id);

	Image::CicnDecoder iconDecoder;

	*icon = nullptr;
	*mask = nullptr;

	if (res && iconDecoder.loadStream(*res)) {
		result = true;
		const Graphics::Surface *s1 = iconDecoder.getSurface();
		const Graphics::Surface *s2 = iconDecoder.getMask();
		const Graphics::Palette &palette = iconDecoder.getPalette();

		*icon = createRemappedSurface(s1, palette.data(), palette.size());
		*mask = new Graphics::Surface();
		(*mask)->copyFrom(*s2);
	}

	delete res;
	resource.close();

	return result;
}

// ---------------------------------------------------------------------------
// PICT loader
// ---------------------------------------------------------------------------

Graphics::Surface *MacGuiImpl::loadPict(int id) {
	Common::MacResManager resource;

	resource.open(_resourceFile);

	Common::SeekableReadStream *res = resource.getResource(MKTAG('P', 'I', 'C', 'T'), id);

	Image::PICTDecoder pictDecoder;
	Graphics::Surface *s = nullptr;

	if (res && pictDecoder.loadStream(*res)) {
		const Graphics::Surface *surface = pictDecoder.getSurface();
		const Graphics::Palette &palette = pictDecoder.getPalette();

		s = createRemappedSurface(surface, palette.data(), palette.size());
	}

	delete res;
	resource.close();

	return s;
}

// ---------------------------------------------------------------------------
// Window handling
// ---------------------------------------------------------------------------

// In the older Mac games, the GUI and game graphics are made to coexist, so
// the GUI should look good regardless of what's on screen. In the newer ones
// there is no such guarantee, so the screen is blacked out whenever the GUI
// is shown.
//
// We hard-code the upper part of the palette to have all the colors used by
// the Mac Window manager and window borders. This is so that drawing the
// window will not mess up what's already on screen. The lower part is used for
// any graphical elements (pictures and icons) in the window. Anything in
// between is set to black.
//
// The Mac window manager gets the palette before gamma correction, the backend
// gets it afterwards.

void MacGuiImpl::setMacGuiColors(Graphics::Palette &palette) {
	// Colors used by the Mac Window Manager
	palette.set(255, 0x00, 0x00, 0x00); // Black
	palette.set(254, 0xFF, 0xFF, 0xFF); // White
	palette.set(253, 0x80, 0x80, 0x80); // Gray80
	palette.set(252, 0x88, 0x88, 0x88); // Gray88
	palette.set(251, 0xEE, 0xEE, 0xEE); // GrayEE
	palette.set(250, 0x00, 0xFF, 0x00); // Green
	palette.set(249, 0x00, 0xCF, 0x00); // Green2

	// Colors used by Mac dialog window borders
	palette.set(248, 0xCC, 0xCC, 0xFF);
	palette.set(247, 0xBB, 0xBB, 0xBB);
	palette.set(246, 0x66, 0x66, 0x99);

	for (int i = 0; i < 246; i++)
		palette.set(i, 0x00, 0x00, 0x00);
}

MacGuiImpl::MacDialogWindow *MacGuiImpl::createWindow(Common::Rect bounds, MacDialogWindowStyle windowStyle, MacDialogMenuStyle menuStyle) {
	if (!_vm->_isModernMacVersion) {
		updatePalette();
		_macBlack = _windowManager->_colorBlack;
		_macWhite = _windowManager->_colorWhite;
	}

	if (bounds.left < 0 || bounds.top < 0 || bounds.right >= 640 || bounds.bottom >= 400) {
		// This happens with the Last Crusade file dialogs.
		bounds.moveTo((640 - bounds.width()) / 2, 27);
	}

	// Adjust the dialog to the actual screen size. This is slightly
	// wasteful since we've already adjusted the dialog coordinates for
	// 640x400 pixels, but that may not be a bad thing if we want to keep
	// support for that resolution later.

	bounds.translate(0, 2 * _vm->_macScreenDrawOffset);

	return new MacDialogWindow(this, _system, _surface, bounds, windowStyle, menuStyle);
}

MacGuiImpl::MacDialogWindow *MacGuiImpl::createDialog(int dialogId) {
	Common::Rect bounds;

	// Default dialog sizes for dialogs without a DITL resource.
	if (!_vm->_isModernMacVersion) {
		bounds.top = 0;
		bounds.left = 0;
		bounds.bottom = 86;
		bounds.right = 340;

		bounds.translate(86, 88);
	} else {
		bounds.top = 0;
		bounds.left = 0;
		bounds.bottom = 113;
		bounds.right = 267;

		bounds.translate(187, 94);
	}

	return createDialog(dialogId, bounds);
}

MacGuiImpl::MacDialogWindow *MacGuiImpl::createDialog(int dialogId, Common::Rect bounds) {
	uint32 black = getBlack();

	Common::MacResManager resource;
	Common::SeekableReadStream *res;

	Common::String gameFileResStr = _strsStrings[kMSIGameFile].c_str();

	resource.open(_resourceFile);

	res = resource.getResource(MKTAG('D', 'L', 'O', 'G'), dialogId);
	if (res) {
		bounds.top = res->readUint16BE();
		bounds.left = res->readUint16BE();
		bounds.bottom = res->readUint16BE();
		bounds.right = res->readUint16BE();

		// Grow the window to include the outer bounds
		bounds.grow(8);

		// Compensate for the original not drawing the game at the very top of
		// the screen.
		bounds.translate(0, -40);
	}

	delete res;

	_macWhite = _windowManager->_colorWhite;
	_macBlack = _windowManager->_colorBlack;

	if (_vm->_isModernMacVersion) {
		res = resource.getResource(MKTAG('D', 'I', 'T', 'L'), dialogId);
		if (!res)
			return nullptr;

		saveScreen();

		// Collect the palettes from all the icons and pictures in the
		// dialog, and combine them into a single palette that they
		// will all be remapped to use. This is probably not what the
		// original did, as the colors become slightly different. Maybe
		// the original just picked one of the palettes, and then used
		// the closest available colors for the rest?
		//
		// That might explain why some of them have seemingly larger
		// palettes than necessary, but why not use the exact colors
		// when we can?

		Common::HashMap<uint32, byte> paletteMap;
		int numWindowColors = 0;

		// Additional colors for hard-coded elements
		paletteMap[0xCDCDCD] = numWindowColors++;

		int numItems = res->readUint16BE() + 1;

		for (int i = 0; i < numItems; i++) {
			res->skip(12);
			int type = res->readByte();
			int len = res->readByte();

			Image::PICTDecoder pictDecoder;
			Image::CicnDecoder iconDecoder;

			Common::SeekableReadStream *imageRes = nullptr;
			Image::ImageDecoder *decoder = nullptr;

			switch (type & 0x7F) {
			case 32:
				imageRes = resource.getResource(MKTAG('c', 'i', 'c', 'n'), res->readUint16BE());
				decoder = &iconDecoder;
				break;

			case 64:
				imageRes = resource.getResource(MKTAG('P', 'I', 'C', 'T'), res->readUint16BE());
				decoder = &pictDecoder;
				break;

			default:
				res->skip(len);
				break;
			}

			if (imageRes && decoder->loadStream(*imageRes)) {
				const Graphics::Palette &palette = decoder->getPalette();
				for (uint j = 0; j < palette.size(); j++) {
					byte r, g, b;
					palette.get(j, r, g, b);
					uint32 color = (r << 16) | (g << 8) | b;
					if (!paletteMap.contains(color))
						paletteMap[color] = numWindowColors++;
				}
			}

			if (len & 1)
				res->skip(1);

			delete imageRes;
		}

		delete res;

		Graphics::Palette palette(256);
		setMacGuiColors(palette);

		for (auto &k : paletteMap) {
			int r = (k._key >> 16) & 0xFF;
			int g = (k._key >> 8) & 0xFF;
			int b = k._key & 0xFF;
			palette.set(k._value, r, g, b);
		}

		_windowManager->passPalette(palette.data(), 256);

		if (_vm->_useGammaCorrection) {
			for (int i = 0; i < 256; i++) {
				byte r, g, b;

				palette.get(i, r, g, b);
				r = Graphics::macGammaCorrectionLookUp[r];
				g = Graphics::macGammaCorrectionLookUp[g];
				b = Graphics::macGammaCorrectionLookUp[b];
				palette.set(i, r, g, b);
			}
		}

		_system->getPaletteManager()->setPalette(palette);
	}

	res = resource.getResource(MKTAG('D', 'I', 'T', 'L'), dialogId);
	if (!res)
		return nullptr;

	MacDialogWindow *window = createWindow(bounds);

	if (res) {
		int numItems = res->readUint16BE() + 1;

		for (int i = 0; i < numItems; i++) {
			res->skip(4);	// Placeholder for handle or procedure pointer

			Common::Rect r;

			r.top = res->readUint16BE();
			r.left = res->readUint16BE();
			r.bottom = res->readUint16BE();
			r.right = res->readUint16BE();

			// Move to appropriate position on inner surface
			r.translate(2, 2);

			int type = res->readByte();
			int len = res->readByte();

			Common::String str;
			bool enabled = ((type & 0x80) == 0);

			switch (type & 0x7F) {
			case 0:
				// User item
//				window->innerSurface()->frameRect(r, black);
				res->skip(len);
				break;
			case 4:
				// Button
				res->seek(-1, SEEK_CUR);
				str = res->readPascalString();
				window->addButton(r, str, enabled);
				break;

			case 5:
				// Checkbox
				res->seek(-1, SEEK_CUR);
				str = res->readPascalString();
				window->addCheckbox(r, str, enabled);
				break;

			case 7:
				// Control
				window->addControl(r, res->readUint16BE());
				break;

			case 8:
				// Static text
				res->seek(-1, SEEK_CUR);
				str = res->readPascalString();
				window->addStaticText(r, str, enabled);
				break;

			case 16:
			{
				// Editable text

				// Adjust for pixel accuracy...
				r.left -= 1;

				MacGuiImpl::MacEditText *editText = window->addEditText(r, gameFileResStr, enabled);
				editText->selectAll();

				window->innerSurface()->frameRect(Common::Rect(r.left - 2, r.top - 3, r.right + 3, r.bottom + 3), black);
				res->skip(len);
				break;
			}
			case 32:
				// Icon
				window->addIcon(r.left, r.top, res->readUint16BE(), enabled);
				break;

			case 64:
				// Picture
				window->addPicture(r, res->readUint16BE(), enabled);
				break;

			default:
				warning("MacGuiImpl::createDialog(): Unknown item type %d", type);
				res->skip(len);
				break;
			}

			if (len & 1)
				res->skip(1);
		}
	}

	delete res;
	resource.close();

	return window;
}

// ---------------------------------------------------------------------------
// Standard dialogs
// ---------------------------------------------------------------------------

// A standard file picker dialog doesn't really make sense in ScummVM, so we
// make something that just looks similar to one.

bool MacGuiImpl::runOpenDialog(int &saveSlotToHandle) {
	Common::Rect bounds(88, 28, 448, 210);

	MacDialogWindow *window = createWindow(bounds);

	MacButton *buttonOpen = window->addButton(Common::Rect(254, 137, 334, 157), "Open", true);
	MacButton *buttonCancel = window->addButton(Common::Rect(254, 106, 334, 126), "Cancel", true);
	window->addButton(Common::Rect(254, 62, 334, 82), "Desktop", false);
	window->addButton(Common::Rect(254, 34, 334, 54), "Eject", false);

	window->drawDottedHLine(253, 93, 334);

	bool availSlots[100];
	int slotIds[100];
	Common::StringArray savegameNames;
	prepareSaveLoad(savegameNames, availSlots, slotIds, ARRAYSIZE(availSlots));

	drawFakePathList(window, Common::Rect(14, 8, 232, 27), name().c_str());
	drawFakeDriveLabel(window, Common::Rect(232, 10, 344, 28), "ScummVM");

	MacListBox *listBox = window->addListBox(Common::Rect(14, 31, 232, 161), savegameNames, true);

	window->setDefaultWidget(buttonOpen);

	while (!_vm->shouldQuit()) {
		MacDialogEvent event;

		while (window->runDialog(event)) {
			switch (event.type) {
			case kDialogClick:
				if (event.widget == buttonOpen || event.widget == listBox) {
					saveSlotToHandle =
						listBox->getValue() < ARRAYSIZE(slotIds) ?
						slotIds[listBox->getValue()] : -1;
					delete window;
					return true;
				} else if (event.widget == buttonCancel) {
					delete window;
					return false;
				}

				break;

			default:
				break;
			}
		}

		window->delayAndUpdate();
	}

	// When quitting, do not load the saved game
	delete window;
	return false;
}

bool MacGuiImpl::runSaveDialog(int &saveSlotToHandle, Common::String &saveName) {
	uint32 black = getBlack();

	Common::Rect bounds(110, 27, 470, 231);

	MacDialogWindow *window = createWindow(bounds);

	MacButton *buttonSave = window->addButton(Common::Rect(254, 163, 334, 183), "Save", true);
	MacButton *buttonCancel = window->addButton(Common::Rect(254, 132, 334, 152), "Cancel", true);
	window->addButton(Common::Rect(254, 90, 334, 110), "New", false);
	window->addButton(Common::Rect(254, 62, 334, 82), "Desktop", false);
	window->addButton(Common::Rect(254, 34, 334, 54), "Eject", false);

	bool busySlots[100];
	int slotIds[100];
	Common::StringArray savegameNames;
	prepareSaveLoad(savegameNames, busySlots, slotIds, ARRAYSIZE(busySlots));

	Common::String saveGameFileAsResStr = _strsStrings[kMSISaveGameFileAs].c_str();
	Common::String gameFileResStr = _strsStrings[kMSIGameFile].c_str();

	int firstAvailableSlot = -1;
	for (int i = 1; i < ARRAYSIZE(busySlots); i++) { // Skip the autosave slot
		if (!busySlots[i]) {
			firstAvailableSlot = i;
			break;
		}
	}

	drawFakePathList(window, Common::Rect(14, 8, 232, 27), name().c_str());
	drawFakeDriveLabel(window, Common::Rect(232, 10, 344, 28), "ScummVM");

	window->addListBox(Common::Rect(14, 31, 232, 129), savegameNames, true, true);

	MacGuiImpl::MacEditText *editText = window->addEditText(Common::Rect(16, 159, 229, 175), gameFileResStr, true);

	Graphics::Surface *s = window->innerSurface();
	const Graphics::Font *font = getFont(kSystemFont);

	s->frameRect(Common::Rect(14, 156, 232, 178), black);

	window->drawDottedHLine(253, 121, 334);

	font->drawString(s, saveGameFileAsResStr, 14, 138, 218, black, Graphics::kTextAlignLeft, 4);

	window->setDefaultWidget(buttonSave);
	editText->selectAll();

	while (!_vm->shouldQuit()) {
		MacDialogEvent event;

		while (window->runDialog(event)) {
			switch (event.type) {
			case kDialogClick:
				if (event.widget == buttonSave) {
					saveName = editText->getText();
					saveSlotToHandle = firstAvailableSlot;
					delete window;
					return true;
				} else if (event.widget == buttonCancel) {
					delete window;
					return false;
				}

				break;

			case kDialogKeyDown:
				if (event.widget == editText) {
					// Disable "Save" button when text is empty
					buttonSave->setEnabled(!editText->getText().empty());
				}

				break;

			default:
				break;
			}
		}

		window->delayAndUpdate();
	}

	// When quitting, do not save the game
	delete window;
	return false;
}

void MacGuiImpl::prepareSaveLoad(Common::StringArray &savegameNames, bool *availSlots, int *slotIds, int size) {
	int saveCounter = 0;

	for (int i = 0; i < size; i++) {
		slotIds[i] = -1;
	}

	Common::String name;
	_vm->listSavegames(availSlots, size);

	for (int i = 0; i < size; i++) {
		if (availSlots[i]) {
			// Save the slot ids for slots which actually contain savegames...
			slotIds[saveCounter] = i;
			saveCounter++;
			if (_vm->getSavegameName(i, name)) {
				Common::String temp = Common::U32String(name, _vm->getDialogCodePage()).encode(Common::kMacRoman);
				savegameNames.push_back(temp);
			} else {
				// The original printed "WARNING... old savegame", but we do support old savegames :-)
				savegameNames.push_back(Common::String::format("%s", "WARNING: wrong save version"));
			}
		}
	}
}

bool MacGuiImpl::runOkCancelDialog(Common::String text) {
	// Widgets:
	//
	// 0 - Okay button
	// 1 - Cancel button
	// 2 - "^0" text

	MacDialogWindow *window = createDialog(502);

	MacButton *buttonOk = (MacButton *)window->getWidget(kWidgetButton, 0);
	MacButton *buttonCancel = (MacButton *)window->getWidget(kWidgetButton, 1);

	window->setDefaultWidget(buttonOk);
	window->addSubstitution(text);

	while (!_vm->shouldQuit()) {
		MacDialogEvent event;

		while (window->runDialog(event)) {
			switch (event.type) {
			case kDialogClick:
				if (event.widget == buttonOk) {
					delete window;
					return true;
				} else if (event.widget == buttonCancel) {
					delete window;
					return false;
				}

			default:
				break;
			}
		}

		window->delayAndUpdate();
	}

	// Quitting is the same as clicking Ok
	delete window;
	return true;
}

void MacGuiImpl::drawFakePathList(MacDialogWindow *window, Common::Rect r, const char *text) {
	uint32 black = getBlack();
	const Graphics::Font *font = getFont(kSystemFont);

	// Draw the text...
	int x0 = r.left + 23;
	int x1 = r.right - 23;

	font->drawString(window->innerSurface(), text, x0 + 1, r.top + 2, x1 - x0, black, Graphics::kTextAlignCenter, 0, true);

	int width = font->getStringWidth(text);

	if (width < x1 - x0) {
		int middle = (x0 + x1) / 2;
		x0 = middle - width / 2;
		x1 = middle + width / 2;
	}

	Common::Rect iconRect(16, 16);

	const uint16 folderIcon[16] = {
		0x0000,	0x0000,	0x0000,	0x0000,	0x1E00,	0x21FC,	0x2002,	0xFFE2,
		0x8012,	0x4012,	0x400A,	0x200A,	0x2006,	0x1FFE,	0x0000,	0x0000
	};

	const uint16 arrowDownIcon[16] = {
		0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x3FF8, 0x1FF0, 0x0FE0,
		0x07C0, 0x0380, 0x0100, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
	};

	// Draw the icon...
	iconRect.moveTo(x0 - 19, r.top + 1);
	drawBitmap(window->innerSurface(), iconRect, folderIcon, black);

	// Draw the arrow...
	iconRect.moveTo(x1 + 4, r.top + 1);
	drawBitmap(window->innerSurface(), iconRect, arrowDownIcon, black);

	// Draw the black frame...
	window->innerSurface()->frameRect(Common::Rect(x0 - 23, r.top, x1 + 23, r.bottom - 1), black);

	// Draw the shadows...
	window->innerSurface()->hLine(x0 - 20, r.bottom - 1, x1 + 23, black);
	window->innerSurface()->vLine(x1 + 23, r.top + 3, r.bottom - 1, black);
}

void MacGuiImpl::drawFakeDriveLabel(MacDialogWindow *window, Common::Rect r, const char *text) {
	uint32 black = getBlack();
	const Graphics::Font *font = getFont(kSystemFont);

	font->drawString(window->innerSurface(), text, r.left, r.top, r.width(), black, Graphics::kTextAlignCenter, 9, true);

	const uint16 hardDriveIcon[16] = {
		0x0000,	0x0000,	0x0000,	0x0000,	0x0000,	0x7FFE,	0x8001,	0x8001,
		0xA001,	0x8001,	0x7FFE,	0x0000,	0x0000,	0x0000,	0x0000, 0x0000
	};

	Common::Rect iconRect(16, 16);
	int width = font->getStringWidth(text);

	if (width < r.width())
		iconRect.moveTo((r.left + r.right - width - 20) / 2, r.top);
	else
		iconRect.moveTo(r.left, r.top);

	drawBitmap(window->innerSurface(), iconRect, hardDriveIcon, black);
}

bool MacGuiImpl::runQuitDialog() {
	return runOkCancelDialog(_strsStrings[kMSIAreYouSureYouWantToQuit].c_str());
}

bool MacGuiImpl::runRestartDialog() {
	return runOkCancelDialog(_strsStrings[kMSIAreYouSureYouWantToRestart].c_str());
}

void MacGuiImpl::drawBanner(char *message) {
	if (_bannerWindow)
		undrawBanner();

	_bannerWindow = createWindow(
		Common::Rect(70, 189, 570, 211),
		kWindowStyleRounded, kMenuStyleNone);
	const Graphics::Font *font = getFont(_vm->_game.id == GID_INDY3 ? kIndy3FontMedium : kLoomFontMedium);

	Graphics::Surface *s = _bannerWindow->innerSurface();
	font->drawString(s, (char *)message, 0, 0, s->w, getBlack(), Graphics::kTextAlignCenter);

	_bannerWindow->show();
}

void MacGuiImpl::undrawBanner() {
	if (_bannerWindow) {
		delete _bannerWindow;
		_bannerWindow = nullptr;
	}
}

void MacGuiImpl::drawBitmap(Graphics::Surface *s, Common::Rect r, const uint16 *bitmap, uint32 color) const {
	assert(r.width() <= 16);

	for (int y = 0; y < r.height(); y++) {
		uint16 bit = 0x8000;
		for (int x = 0; x < r.width(); x++) {
			if (bitmap[y] & bit)
				s->setPixel(r.left + x, r.top + y, color);
			bit >>= 1;
		}
	}
}

} // End of namespace Scumm
