/* Copyright (C) 1994-2004 Revolution Software Ltd
 *
 * 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * $Header$
 */

#include "common/stdafx.h"
#include "common/rect.h"
#include "common/config-manager.h"
#include "sword2/sword2.h"
#include "sword2/controls.h"
#include "sword2/defs.h"
#include "sword2/logic.h"
#include "sword2/resman.h"
#include "sword2/driver/d_draw.h"
#include "sword2/driver/d_sound.h"

#define	MAX_STRING_LEN		64	// 20 was too low; better to be safe ;)
#define CHARACTER_OVERLAP	 2	// overlap characters by 3 pixels

// our fonts start on SPACE character (32)
#define SIZE_OF_CHAR_SET	(256 - 32)

#define MAX_WIDGETS 25

namespace Sword2 {

class Widget;

/**
 * Base class for all dialogs.
 */

class Dialog {
private:
	int _numWidgets;
	Widget *_widgets[MAX_WIDGETS];
	bool _finish;
	int _result;

public:
	Gui *_gui;

	Dialog(Gui *gui);
	virtual ~Dialog();

	void registerWidget(Widget *widget);

	virtual void paint();
	virtual void setResult(int result);

	int run();

	virtual void onAction(Widget *widget, int result = 0) {}
};

/**
 * Base class for all widgets.
 */

class Widget {
protected:
	Dialog *_parent;

	SpriteInfo *_sprites;

	struct WidgetSurface {
		byte *_surface;
		bool _original;
	};
 
	WidgetSurface *_surfaces;
	int _numStates;
	int _state;

	Common::Rect _hitRect;

public:
	Widget(Dialog *parent, int states);

	virtual ~Widget();

	void createSurfaceImage(int state, uint32 res, int x, int y, uint32 pc);
	void linkSurfaceImage(Widget *from, int state, int x, int y);

	void createSurfaceImages(uint32 res, int x, int y);
	void linkSurfaceImages(Widget *from, int x, int y);

	void setHitRect(int x, int y, int width, int height);
	bool isHit(int16 x, int16 y);

	void setState(int state);
	int getState();

	virtual void paint(Common::Rect *clipRect = NULL);

	virtual void onMouseEnter() {}
	virtual void onMouseExit() {}
	virtual void onMouseMove(int x, int y) {}
	virtual void onMouseDown(int x, int y) {}
	virtual void onMouseUp(int x, int y) {}
	virtual void onWheelUp(int x, int y) {}
	virtual void onWheelDown(int x, int y) {}
	virtual void onKey(KeyboardEvent *ke) {}
	virtual void onTick() {}

	virtual void releaseMouse(int x, int y) {}
};

/**
 * This class is used to draw text in dialogs, buttons, etc.
 */

class FontRendererGui {
private:
	Gui *_gui;
	
	struct Glyph {
		byte *_data;
		int _width;
		int _height;
	};

	Glyph _glyph[SIZE_OF_CHAR_SET];

	int _fontId;

public:
	enum {
		kAlignLeft,
		kAlignRight,
		kAlignCenter
	};

	FontRendererGui(Gui *gui, int fontId);
	~FontRendererGui();

	void fetchText(uint32 textId, byte *buf);

	int getCharWidth(byte c);
	int getCharHeight(byte c);

	int getTextWidth(byte *text);
	int getTextWidth(uint32 textId);

	void drawText(byte *text, int x, int y, int alignment = kAlignLeft);
	void drawText(uint32 textId, int x, int y, int alignment = kAlignLeft);
};

FontRendererGui::FontRendererGui(Gui *gui, int fontId)
	: _gui(gui), _fontId(fontId) {
	byte *font = _gui->_vm->_resman->openResource(fontId);
	FrameHeader *head;
	SpriteInfo sprite;

	sprite.type = RDSPR_NOCOMPRESSION | RDSPR_TRANS;

	for (int i = 0; i < SIZE_OF_CHAR_SET; i++) {
		head = (FrameHeader *) _gui->_vm->fetchFrameHeader(font, i);
		sprite.data = (byte *) (head + 1);
		sprite.w = head->width;
		sprite.h = head->height;
		_gui->_vm->_graphics->createSurface(&sprite, &_glyph[i]._data);
		_glyph[i]._width = head->width;
		_glyph[i]._height = head->height;
	}

	_gui->_vm->_resman->closeResource(fontId);
}

FontRendererGui::~FontRendererGui() {
	for (int i = 0; i < SIZE_OF_CHAR_SET; i++)
		_gui->_vm->_graphics->deleteSurface(_glyph[i]._data);
}

void FontRendererGui::fetchText(uint32 textId, byte *buf) {
	byte *data = _gui->_vm->fetchTextLine(_gui->_vm->_resman->openResource(textId / SIZE), textId & 0xffff);
	int i;

	for (i = 0; data[i + 2]; i++) {
		if (buf)
			buf[i] = data[i + 2];
	}
			
	buf[i] = 0;
	_gui->_vm->_resman->closeResource(textId / SIZE);
}

int FontRendererGui::getCharWidth(byte c) {
	if (c < 32)
		return 0;
	return _glyph[c - 32]._width;
}

int FontRendererGui::getCharHeight(byte c) {
	if (c < 32)
		return 0;
	return _glyph[c - 32]._height;
}

int FontRendererGui::getTextWidth(byte *text) {
	int textWidth = 0;

	for (int i = 0; text[i]; i++)
		if (text[i] >= ' ')
			textWidth += (getCharWidth(text[i]) - CHARACTER_OVERLAP);
	return textWidth;
}

int FontRendererGui::getTextWidth(uint32 textId) {
	byte text[MAX_STRING_LEN];

	fetchText(textId, text);
	return getTextWidth(text);
}

void FontRendererGui::drawText(byte *text, int x, int y, int alignment) {
	SpriteInfo sprite;
	int i;

	if (alignment != kAlignLeft) {
		int textWidth = getTextWidth(text);

		switch (alignment) {
		case kAlignRight:
			x -= textWidth;
			break;
		case kAlignCenter:
			x -= (textWidth / 2);
			break;
		}
	}

	sprite.x = x;
	sprite.y = y;

	for (i = 0; text[i]; i++) {
		if (text[i] >= ' ') {
			sprite.w = getCharWidth(text[i]);
			sprite.h = getCharHeight(text[i]);

			_gui->_vm->_graphics->drawSurface(&sprite, _glyph[text[i] - 32]._data);

			sprite.x += (getCharWidth(text[i]) - CHARACTER_OVERLAP);
		}
	}
}

void FontRendererGui::drawText(uint32 textId, int x, int y, int alignment) {
	byte text[MAX_STRING_LEN];

	fetchText(textId, text);
	drawText(text, x, y, alignment);
}

//
// Dialog class functions
//

Dialog::Dialog(Gui *gui)
	: _numWidgets(0), _finish(false), _result(0), _gui(gui) {
	_gui->_vm->setFullPalette(CONTROL_PANEL_PALETTE);
	_gui->_vm->_graphics->clearScene();

	// HACK: Since the dialogs don't do normal scene updates we need to
	// trigger a full redraw manually.

	_gui->_vm->_graphics->setNeedFullRedraw();
	_gui->_vm->_graphics->updateDisplay();
}

Dialog::~Dialog() {
	for (int i = 0; i < _numWidgets; i++)
		delete _widgets[i];
}

void Dialog::registerWidget(Widget *widget) {
	if (_numWidgets < MAX_WIDGETS)
		_widgets[_numWidgets++] = widget;
}

void Dialog::paint() {
	_gui->_vm->_graphics->clearScene();
	for (int i = 0; i < _numWidgets; i++)
		_widgets[i]->paint();
}

void Dialog::setResult(int result) {
	_result = result;
	_finish = true;
}

int Dialog::run() {
	uint32 oldFilter = _gui->_vm->setEventFilter(0);

	int i;

	paint();

	int16 oldMouseX = -1;
	int16 oldMouseY = -1;

	while (!_finish) {
		// So that the menu icons will reach their full size
		_gui->_vm->_graphics->processMenu();
		_gui->_vm->_graphics->updateDisplay(false);

		int16 newMouseX = _gui->_vm->_mouseX;
		int16 newMouseY = _gui->_vm->_mouseY + 40;

		MouseEvent *me = _gui->_vm->mouseEvent();
		KeyboardEvent *ke = _gui->_vm->keyboardEvent();

		if (ke) {
			if (ke->keycode == 27)
				setResult(0);
			else if (ke->keycode == '\n' || ke->keycode == '\r')
				setResult(1);
		}

		int oldHit = -1;
		int newHit = -1;

		// Find out which widget the mouse was over the last time, and
		// which it is currently over. This assumes the widgets do not
		// overlap.

		for (i = 0; i < _numWidgets; i++) {
			if (_widgets[i]->isHit(oldMouseX, oldMouseY))
				oldHit = i;
			if (_widgets[i]->isHit(newMouseX, newMouseY))
				newHit = i;
		}

		// Was the mouse inside a widget the last time?

		if (oldHit >= 0) {
			if (newHit != oldHit)
				_widgets[oldHit]->onMouseExit();
		}

		// Is the mouse currently in a widget?

		if (newHit >= 0) {
			if (newHit != oldHit)
				_widgets[newHit]->onMouseEnter();

			if (me) {
				switch (me->buttons) {
				case RD_LEFTBUTTONDOWN:
					_widgets[newHit]->onMouseDown(newMouseX, newMouseY);
					break;
				case RD_LEFTBUTTONUP:
					_widgets[newHit]->onMouseUp(newMouseX, newMouseY);
					break;
				case RD_WHEELUP:
					_widgets[newHit]->onWheelUp(newMouseX, newMouseY);
					break;
				case RD_WHEELDOWN:
					_widgets[newHit]->onWheelDown(newMouseX, newMouseY);
					break;
				}
			}
		}

		// Some events are passed to the widgets regardless of where
		// the mouse cursor is.

		for (i = 0; i < _numWidgets; i++) {
			if (me && me->buttons == RD_LEFTBUTTONUP) {
				// So that slider widgets will know when the
				// user releases the mouse button, even if the
				// cursor is outside of the slider's hit area.
				_widgets[i]->releaseMouse(newMouseX, newMouseY);
			}

			// This is to make it easier to drag the slider widget

			if (newMouseX != oldMouseX || newMouseY != oldMouseY)
				_widgets[i]->onMouseMove(newMouseX, newMouseY);

			if (ke)
				_widgets[i]->onKey(ke);

			_widgets[i]->onTick();
		}

		oldMouseX = newMouseX;
		oldMouseY = newMouseY;

		_gui->_vm->_system->delay_msecs(20);

		if (_gui->_vm->_quit)
			setResult(0);
	}

	_gui->_vm->setEventFilter(oldFilter);
	return _result;
}

//
// Widget functions
//

Widget::Widget(Dialog *parent, int states)
	: _parent(parent), _numStates(states), _state(0) {
	_sprites = (SpriteInfo *) calloc(states, sizeof(SpriteInfo));
	_surfaces = (WidgetSurface *) calloc(states, sizeof(WidgetSurface));

	_hitRect.left = _hitRect.right = _hitRect.top = _hitRect.bottom = -1;
}

Widget::~Widget() {
	for (int i = 0; i < _numStates; i++) {
		if (_surfaces[i]._original)
			_parent->_gui->_vm->_graphics->deleteSurface(_surfaces[i]._surface);
	}
	free(_sprites);
	free(_surfaces);
}

void Widget::createSurfaceImage(int state, uint32 res, int x, int y, uint32 pc) {
	byte *file, *colTablePtr = NULL;
	AnimHeader *anim_head;
	FrameHeader *frame_head;
	CdtEntry *cdt_entry;
	uint32 spriteType = RDSPR_TRANS;

	// open anim resource file, point to base
	file = _parent->_gui->_vm->_resman->openResource(res);

	anim_head = _parent->_gui->_vm->fetchAnimHeader(file);
	cdt_entry = _parent->_gui->_vm->fetchCdtEntry(file, pc);
	frame_head = _parent->_gui->_vm->fetchFrameHeader(file, pc);

	// If the frame is flipped. (Only really applicable to frames using
	// offsets.)

	if (cdt_entry->frameType & FRAME_FLIPPED)
		spriteType |= RDSPR_FLIP;

	// Which compression was used?

	switch (anim_head->runTimeComp) {
	case NONE:
		spriteType |= RDSPR_NOCOMPRESSION;
		break;
	case RLE256:
		spriteType |= RDSPR_RLE256;
		break;
	case RLE16:
		spriteType |= RDSPR_RLE256;
		// Points to just after last cdt_entry, i.e. start of colour
		// table
		colTablePtr = (byte *) (anim_head + 1) +
			anim_head->noAnimFrames * sizeof(CdtEntry);
		break;
	}

	_sprites[state].x = x;
	_sprites[state].y = y;
	_sprites[state].w = frame_head->width;
	_sprites[state].h = frame_head->height;
	_sprites[state].scale = 0;
	_sprites[state].type = spriteType;
	_sprites[state].blend = anim_head->blend;

	// Points to just after frame header, ie. start of sprite data
	_sprites[state].data = (byte *) (frame_head + 1);

	_parent->_gui->_vm->_graphics->createSurface(&_sprites[state], &_surfaces[state]._surface);
	_surfaces[state]._original = true;

	// Release the anim resource
	_parent->_gui->_vm->_resman->closeResource(res);
}

void Widget::linkSurfaceImage(Widget *from, int state, int x, int y) {
	_sprites[state].x = x;
	_sprites[state].y = y;
	_sprites[state].w = from->_sprites[state].w;
	_sprites[state].h = from->_sprites[state].h;
	_sprites[state].scale = from->_sprites[state].scale;
	_sprites[state].type = from->_sprites[state].type;
	_sprites[state].blend = from->_sprites[state].blend;

	_surfaces[state]._surface = from->_surfaces[state]._surface;
	_surfaces[state]._original = false;
}

void Widget::createSurfaceImages(uint32 res, int x, int y) {
	for (int i = 0; i < _numStates; i++)
		createSurfaceImage(i, res, x, y, i);
}

void Widget::linkSurfaceImages(Widget *from, int x, int y) {
	for (int i = 0; i < from->_numStates; i++)
		linkSurfaceImage(from, i, x, y);
}

void Widget::setHitRect(int x, int y, int width, int height) {
	_hitRect.left = x;
	_hitRect.right = x + width;
	_hitRect.top = y;
	_hitRect.bottom = y + height;
}

bool Widget::isHit(int16 x, int16 y) {
	return _hitRect.left >= 0 && _hitRect.contains(x, y);
}

void Widget::setState(int state) {
	if (state != _state) {
		_state = state;
		paint();
	}
}

int Widget::getState() {
	return _state;
}

void Widget::paint(Common::Rect *clipRect) {
	_parent->_gui->_vm->_graphics->drawSurface(&_sprites[_state], _surfaces[_state]._surface, clipRect);
}

/**
 * Standard button class.
 */

class Button : public Widget {
public:
	Button(Dialog *parent, int x, int y, int w, int h)
		: Widget(parent, 2) {
		setHitRect(x, y, w, h);
	}

	virtual void onMouseExit() {
		setState(0);
	}

	virtual void onMouseDown(int x, int y) {
		setState(1);
	}

	virtual void onMouseUp(int x, int y) {
		if (getState() != 0) {
			setState(0);
			_parent->onAction(this);
		}
	}
};

/**
 * Scroll buttons are used to scroll the savegame list. The difference between
 * this and a normal button is that we want this to repeat.
 */

class ScrollButton : public Widget {
private:
	uint32 _holdCounter;

public:
	ScrollButton(Dialog *parent, int x, int y, int w, int h)
		: Widget(parent, 2), _holdCounter(0) {
		setHitRect(x, y, w, h);
	}

	virtual void onMouseExit() {
		setState(0);
	}

	virtual void onMouseDown(int x, int y) {
		setState(1);
		_parent->onAction(this);
		_holdCounter = 0;
	}

	virtual void onMouseUp(int x, int y) {
		setState(0);
	}

	virtual void onTick() {
		if (getState() != 0) {
			_holdCounter++;
			if (_holdCounter > 16 && (_holdCounter % 4) == 0)
				_parent->onAction(this);
		}
	}
};

/**
 * A switch is a button that changes state when clicked, and keeps that state
 * until clicked again.
 */

class Switch : public Widget {
private:
	bool _holding, _value;
	int _upState, _downState;

public:
	Switch(Dialog *parent, int x, int y, int w, int h)
		: Widget(parent, 2), _holding(false), _value(false),
		  _upState(0), _downState(1) {
		setHitRect(x, y, w, h);
	}

	// The sound mute switches have 0 as their "down" state and 1 as
	// their "up" state, so this function is needed to get consistent
	// behaviour.

	void reverseStates() {
		_upState = 1;
		_downState = 0;
	}

	void setValue(bool value) {
		_value = value;
		if (_value)
			setState(_downState);
		else
			setState(_upState);
	}

	bool getValue() {
		return _value;
	}

	virtual void onMouseExit() {
		if (_holding && !_value)
			setState(_upState);
		_holding = false;
	}

	virtual void onMouseDown(int x, int y) {
		_holding = true;
		setState(_downState);
	}

	virtual void onMouseUp(int x, int y) {
		if (_holding) {
			_holding = false;
			_value = !_value;
			if (_value)
				setState(_downState);
			else
				setState(_upState);
			_parent->onAction(this, getState());
		}
	}
};

/**
 * A slider is used to specify a value within a pre-defined range.
 */

class Slider : public Widget {
private:
	Widget *_background;
	bool _dragging;
	int _value, _targetValue;
	int _maxValue;
	int _dragOffset;

	int posFromValue(int value) {
		return _hitRect.left + (value * (_hitRect.width() - 38)) / _maxValue;
	}

	int valueFromPos(int x) {
		return (int) ((double) (_maxValue * (x - _hitRect.left)) / (double) (_hitRect.width() - 38) + 0.5);
	}

public:
	Slider(Dialog *parent, Widget *background, int max,
		int x, int y, int w, int h, Widget *base = NULL)
		: Widget(parent, 1), _background(background),
		  _dragging(false), _value(0), _targetValue(0),
		  _maxValue(max) {
		setHitRect(x, y, w, h);

		if (base)
			linkSurfaceImages(base, x, y);
		else
			createSurfaceImages(3406, x, y);
	}

	virtual void paint(Common::Rect *clipRect = NULL) {
		// This will redraw a bit more than is strictly necessary,
		// but I doubt that will make any noticeable difference.

		_background->paint(&_hitRect);
		Widget::paint(clipRect);
	}

	void setValue(int value) {
		_value = value;
		_targetValue = value;
		_sprites[0].x = posFromValue(_value);
		paint();
	}

	int getValue() {
		return _value;
	}

	virtual void onMouseMove(int x, int y) {
		if (_dragging) {
			int newX = x - _dragOffset;
			int newValue;

			if (newX < _hitRect.left)
				newX = _hitRect.left;
			else if (newX + 38 > _hitRect.right)
				newX = _hitRect.right - 38;

			_sprites[0].x = newX;

			newValue = valueFromPos(newX);
			if (newValue != _value) {
				_value = newValue;
				_targetValue = newValue;
				_parent->onAction(this, newValue);
			}

			paint();
		}
	}

	virtual void onMouseDown(int x, int y) {
		if (x >= _sprites[0].x && x < _sprites[0].x + 38) {
			_dragging = true;
			_dragOffset = x - _sprites[0].x;
		} else if (x < _sprites[0].x) {
			if (_targetValue > 0)
				_targetValue--;
		} else {
			if (_targetValue < _maxValue)
				_targetValue++;
		}
	}

	virtual void releaseMouse(int x, int y) {
		if (_dragging)
			_dragging = false;
	}

	virtual void onTick() {
		if (!_dragging) {
			int target = posFromValue(_targetValue);

			if (target != _sprites[0].x) {
				if (target < _sprites[0].x) {
					_sprites[0].x -= 4;
					if (_sprites[0].x < target)
						_sprites[0].x = target;
				} else if (target > _sprites[0].x) {
					_sprites[0].x += 4;
					if (_sprites[0].x > target)
						_sprites[0].x = target;
				}

				int newValue = valueFromPos(_sprites[0].x);
				if (newValue != _value) {
					_value = newValue;
					_parent->onAction(this, newValue);
				}

				paint();
			}
		}
	}
};

/**
 * A "mini" dialog is usually a yes/no question, but also used for the
 * restart/restore dialog at the beginning of the game.
 */

class MiniDialog : public Dialog {
private:
	uint32 _headerTextId;
	uint32 _okTextId;
	uint32 _cancelTextId;
	FontRendererGui *_fr;
	Widget *_panel;
	Button *_okButton;
	Button *_cancelButton;

public:
	MiniDialog(Gui *gui, uint32 headerTextId, uint32 okTextId = 149618688, uint32 cancelTextId = 149618689)
		: Dialog(gui), _headerTextId(headerTextId), _okTextId(okTextId), _cancelTextId(cancelTextId) {
		_fr = new FontRendererGui(_gui, _gui->_vm->_controlsFontId);

		_panel = new Widget(this, 1);
		_panel->createSurfaceImages(1996, 203, 104);

		_okButton = new Button(this, 243, 214, 24, 24);
		_okButton->createSurfaceImages(2002, 243, 214);

		_cancelButton = new Button(this, 243, 276, 24, 24);
		_cancelButton->linkSurfaceImages(_okButton, 243, 276);

		registerWidget(_panel);
		registerWidget(_okButton);
		registerWidget(_cancelButton);
	}

	~MiniDialog() {
		delete _fr;
	}

	virtual void paint() {
		Dialog::paint();

		if (_headerTextId)
			_fr->drawText(_headerTextId, 310, 134, FontRendererGui::kAlignCenter);
		_fr->drawText(_okTextId, 270, 214);
		_fr->drawText(_cancelTextId, 270, 276);
	}

	virtual void onAction(Widget *widget, int result = 0) {
		if (widget == _okButton)
			setResult(1);
		else if (widget == _cancelButton)
			setResult(0);
	}
};

/**
 * The game settings dialog.
 */

class OptionsDialog : public Dialog {
private:
	FontRendererGui *_fr;
	Widget *_panel;
	Switch *_objectLabelsSwitch;
	Switch *_subtitlesSwitch;
	Switch *_reverseStereoSwitch;
	Switch *_musicSwitch;
	Switch *_speechSwitch;
	Switch *_fxSwitch;
	Slider *_musicSlider;
	Slider *_speechSlider;
	Slider *_fxSlider;
	Slider *_gfxSlider;
	Widget *_gfxPreview;
	Button *_okButton;
	Button *_cancelButton;

public:
	OptionsDialog(Gui *gui) : Dialog(gui) {
		_fr = new FontRendererGui(gui, gui->_vm->_controlsFontId);

		_panel = new Widget(this, 1);
		_panel->createSurfaceImages(3405, 0, 40);

		_objectLabelsSwitch = new Switch(this, 304, 100, 53, 32);
		_objectLabelsSwitch->createSurfaceImages(3687, 304, 100);

		_subtitlesSwitch = new Switch(this, 510, 100, 53, 32);
		_subtitlesSwitch->linkSurfaceImages(_objectLabelsSwitch, 510, 100);

		_reverseStereoSwitch = new Switch(this, 304, 293, 53, 32);
		_reverseStereoSwitch->linkSurfaceImages(_objectLabelsSwitch, 304, 293);

		_musicSwitch = new Switch(this, 516, 157, 40, 32);
		_musicSwitch->createSurfaceImages(3315, 516, 157);
		_musicSwitch->reverseStates();

		_speechSwitch = new Switch(this, 516, 205, 40, 32);
		_speechSwitch->linkSurfaceImages(_musicSwitch, 516, 205);
		_speechSwitch->reverseStates();

		_fxSwitch = new Switch(this, 516, 250, 40, 32);
		_fxSwitch->linkSurfaceImages(_musicSwitch, 516, 250);
		_fxSwitch->reverseStates();

		_musicSlider = new Slider(this, _panel, 16, 309, 161, 170, 27);
		_speechSlider = new Slider(this, _panel, 14, 309, 208, 170, 27, _musicSlider);
		_fxSlider = new Slider(this, _panel, 14, 309, 254, 170, 27, _musicSlider);
		_gfxSlider = new Slider(this, _panel, 3, 309, 341, 170, 27, _musicSlider);

		_gfxPreview = new Widget(this, 4);
		_gfxPreview->createSurfaceImages(256, 495, 310);

		_okButton = new Button(this, 203, 382, 53, 32);
		_okButton->createSurfaceImages(901, 203, 382);

		_cancelButton = new Button(this, 395, 382, 53, 32);
		_cancelButton->linkSurfaceImages(_okButton, 395, 382);

		registerWidget(_panel);
		registerWidget(_objectLabelsSwitch);
		registerWidget(_subtitlesSwitch);
		registerWidget(_reverseStereoSwitch);
		registerWidget(_musicSwitch);
		registerWidget(_speechSwitch);
		registerWidget(_fxSwitch);
		registerWidget(_musicSlider);
		registerWidget(_speechSlider);
		registerWidget(_fxSlider);
		registerWidget(_gfxSlider);
		registerWidget(_gfxPreview);
		registerWidget(_okButton);
		registerWidget(_cancelButton);

		_gui->readOptionSettings();

		_objectLabelsSwitch->setValue(_gui->_pointerTextSelected);
		_subtitlesSwitch->setValue(_gui->_subtitles);
		_reverseStereoSwitch->setValue(_gui->_stereoReversed);
		_musicSwitch->setValue(!_gui->_vm->_sound->isMusicMute());
		_speechSwitch->setValue(!_gui->_vm->_sound->isSpeechMute());
		_fxSwitch->setValue(!_gui->_vm->_sound->isFxMute());
		_musicSlider->setValue(_gui->_vm->_sound->getMusicVolume());
		_speechSlider->setValue(_gui->_vm->_sound->getSpeechVolume());
		_fxSlider->setValue(_gui->_vm->_sound->getFxVolume());
		_gfxSlider->setValue(_gui->_vm->_graphics->getRenderLevel());
		_gfxPreview->setState(_gui->_vm->_graphics->getRenderLevel());
	}

	~OptionsDialog() {
		delete _fr;
	}

	virtual void paint() {
		Dialog::paint();

		int maxWidth = 0;
		int width;

		uint32 alignTextIds[] = {
			149618700,	// object labels
			149618702,	// music volume
			149618703,	// speech volume
			149618704,	// fx volume
			149618705,	// graphics quality
			149618709,	// reverse stereo
		};

		for (int i = 0; i < ARRAYSIZE(alignTextIds); i++) {
			width = _fr->getTextWidth(alignTextIds[i]);
			if (width > maxWidth)
				maxWidth = width;
		}

		// Options
		_fr->drawText(149618698, 321, 55, FontRendererGui::kAlignCenter);
		// Subtitles
		_fr->drawText(149618699, 500, 103, FontRendererGui::kAlignRight);
		// Object labels
		_fr->drawText(149618700, 299 - maxWidth, 103);
		// Music volume
		_fr->drawText(149618702, 299 - maxWidth, 161);
		// Speech volume
		_fr->drawText(149618703, 299 - maxWidth, 208);
		// FX volume
		_fr->drawText(149618704, 299 - maxWidth, 254);
		// Reverse stereo
		_fr->drawText(149618709, 299 - maxWidth, 296);
		// Graphics quality
		_fr->drawText(149618705, 299 - maxWidth, 341);
		// Ok
		_fr->drawText(149618688, 193, 382, FontRendererGui::kAlignRight);
		// Cancel
		_fr->drawText(149618689, 385, 382, FontRendererGui::kAlignRight);
	}

	virtual void onAction(Widget *widget, int result = 0) {
		// Since there is music playing while the dialog is displayed
		// we need to update music volume immediately.

		if (widget == _musicSwitch) {
			_gui->_vm->_sound->muteMusic(result != 0);
		} else if (widget == _musicSlider) {
			_gui->_vm->_sound->setMusicVolume(result);
			_gui->_vm->_sound->muteMusic(result == 0);
			_musicSwitch->setValue(result != 0);
		} else if (widget == _speechSlider) {
			_speechSwitch->setValue(result != 0);
		} else if (widget == _fxSlider) {
			_fxSwitch->setValue(result != 0);
		} else if (widget == _gfxSlider) {
			_gfxPreview->setState(result);
			_gui->updateGraphicsLevel(result);
		} else if (widget == _okButton) {
			_gui->_subtitles = _subtitlesSwitch->getValue();
			_gui->_pointerTextSelected = _objectLabelsSwitch->getValue();
			_gui->_stereoReversed = _reverseStereoSwitch->getValue();

			// Apply the changes
			_gui->_vm->_sound->muteMusic(!_musicSwitch->getValue());
			_gui->_vm->_sound->muteSpeech(!_speechSwitch->getValue());
			_gui->_vm->_sound->muteFx(!_fxSwitch->getValue());
			_gui->_vm->_sound->setMusicVolume(_musicSlider->getValue());
			_gui->_vm->_sound->setSpeechVolume(_speechSlider->getValue());
			_gui->_vm->_sound->setFxVolume(_fxSlider->getValue());
			_gui->_vm->_sound->buildPanTable(_gui->_stereoReversed);

			_gui->updateGraphicsLevel(_gfxSlider->getValue());

			_gui->writeOptionSettings();
			setResult(1);
		} else if (widget == _cancelButton) {
			// Revert the changes
			_gui->readOptionSettings();
			setResult(0);
		}
	}
};

// FIXME: Move these into some class

enum {
	kSaveDialog,
	kLoadDialog
};

// Slot button actions. Note that keyboard input generates positive actions

enum {
	kSelectSlot = -1,
	kDeselectSlot = -2,
	kWheelDown = -3,
	kWheelUp = -4,
	kStartEditing = -5,
	kCursorTick = -6
};

class Slot : public Widget {
private:
	int _mode;
	FontRendererGui *_fr;
	byte _text[SAVE_DESCRIPTION_LEN];
	bool _clickable;
	bool _editable;

public:
	Slot(Dialog *parent, int x, int y, int w, int h)
		: Widget(parent, 2), _clickable(false), _editable(false) {
		setHitRect(x, y, w, h);
		_text[0] = 0;
	}

	void setMode(int mode) {
		_mode = mode;
	}

	void setClickable(bool clickable) {
		_clickable = clickable;
	}

	void setEditable(bool editable) {
		_editable = editable;
	}

	bool isEditable() {
		return _editable;
	}

	void setText(FontRendererGui *fr, int slot, byte *text) {
		_fr = fr;
		if (text)
			sprintf((char *) _text, "%d.  %s", slot, text);
		else
			sprintf((char *) _text, "%d.  ", slot);
	}

	byte *getText() {
		return &_text[0];
	}

	virtual void paint(Common::Rect *clipRect = NULL) {
		Widget::paint();

		// HACK: The main dialog is responsible for drawing the text
		// when in editing mode.

		if (!_editable)
			_fr->drawText(_text, _sprites[0].x + 16, _sprites[0].y + 4 + 2 * getState());
	}

	virtual void onMouseDown(int x, int y) {
		if (_clickable) {
			if (getState() == 0) {
				setState(1);
				_parent->onAction(this, kSelectSlot);
				if (_mode == kSaveDialog)
					_parent->onAction(this, kStartEditing);
			} else if (_mode == kLoadDialog) {
				setState(0);
				_parent->onAction(this, kDeselectSlot);
			}
		}
	}

	virtual void onWheelUp(int x, int y) {
		_parent->onAction(this, kWheelUp);
	}

	virtual void onWheelDown(int x, int y) {
		_parent->onAction(this, kWheelDown);
	}

	virtual void onKey(KeyboardEvent *ke) {
		if (_editable) {
			if (ke->keycode == 8)
				_parent->onAction(this, 8);
			else if (ke->ascii >= ' ' && ke->ascii <= 255) {
				// Accept the character if the font renderer
				// has what looks like a valid glyph for it.
				if (_fr->getCharWidth(ke->ascii))
					_parent->onAction(this, ke->ascii);
			}
		}
	}

	virtual void onTick() {
		if (_editable)
			_parent->onAction(this, kCursorTick);
	}

	void setY(int y) {
		for (int i = 0; i < _numStates; i++)
			_sprites[i].y = y;
		setHitRect(_hitRect.left, y, _hitRect.width(), _hitRect.height());
	}

	int getY() {
		return _sprites[0].y;
	}
};

class SaveLoadDialog : public Dialog {
private:
	int _mode, _selectedSlot;
	byte _editBuffer[SAVE_DESCRIPTION_LEN];
	int _editPos, _firstPos;
	int _cursorTick;

	FontRendererGui *_fr1;
	FontRendererGui *_fr2;
	Widget *_panel;
	Slot *_slotButton[8];
	ScrollButton *_zupButton;
	ScrollButton *_upButton;
	ScrollButton *_downButton;
	ScrollButton *_zdownButton;
	Button *_okButton;
	Button *_cancelButton;

public:
	SaveLoadDialog(Gui *gui, int mode)
		: Dialog(gui), _mode(mode), _selectedSlot(-1) {
		int i;

		// FIXME: The "control font" and the "red font" are currently
		// always the same font, so one should be eliminated.

		_fr1 = new FontRendererGui(_gui, _gui->_vm->_controlsFontId);
		_fr2 = new FontRendererGui(_gui, _gui->_vm->_redFontId);

		_panel = new Widget(this, 1);
		_panel->createSurfaceImages(2016, 0, 40);

		for (i = 0; i < 4; i++) {
			_slotButton[i] = new Slot(this, 114, 0, 384, 36);
			_slotButton[i]->createSurfaceImages(2006 + i, 114, 0);
			_slotButton[i]->setMode(mode);
			_slotButton[i + 4] = new Slot(this, 114, 0, 384, 36);
			_slotButton[i + 4]->linkSurfaceImages(_slotButton[i], 114, 0);
			_slotButton[i + 4]->setMode(mode);
		}

		updateSlots();

		_zupButton = new ScrollButton(this, 516, 65, 17, 17);
		_zupButton->createSurfaceImages(1982, 516, 65);

		_upButton = new ScrollButton(this, 516, 85, 17, 17);
		_upButton->createSurfaceImages(2067, 516, 85);

		_downButton = new ScrollButton(this, 516, 329, 17, 17);
		_downButton->createSurfaceImages(1986, 516, 329);

		_zdownButton = new ScrollButton(this, 516, 350, 17, 17);
		_zdownButton->createSurfaceImages(1988, 516, 350);

		_okButton = new Button(this, 130, 377, 24, 24);
		_okButton->createSurfaceImages(2002, 130, 377);

		_cancelButton = new Button(this, 350, 377, 24, 24);
		_cancelButton->linkSurfaceImages(_okButton, 350, 377);

		registerWidget(_panel);

		for (i = 0; i < 8; i++)
			registerWidget(_slotButton[i]);

		registerWidget(_zupButton);
		registerWidget(_upButton);
		registerWidget(_downButton);
		registerWidget(_zdownButton);
		registerWidget(_okButton);
		registerWidget(_cancelButton);
	}

	~SaveLoadDialog() {
		delete _fr1;
		delete _fr2;
	}

	// There aren't really a hundred different button objects of course,
	// there are only eight. Re-arrange them to simulate scrolling.

	void updateSlots() {
		for (int i = 0; i < 8; i++) {
			Slot *slot = _slotButton[(_gui->_baseSlot + i) % 8];
			FontRendererGui *fr;
			byte description[SAVE_DESCRIPTION_LEN];

			slot->setY(72 + i * 36);

			if (_gui->_baseSlot + i == _selectedSlot) {
				slot->setEditable(_mode == kSaveDialog);
				slot->setState(1);
				fr = _fr2;
			} else {
				slot->setEditable(false);
				slot->setState(0);
				fr = _fr1;
			}

			if (_gui->_vm->getSaveDescription(_gui->_baseSlot + i, description) == SR_OK) {
				slot->setText(fr, _gui->_baseSlot + i, description);
				slot->setClickable(true);
			} else {
				slot->setText(fr, _gui->_baseSlot + i, NULL);
				slot->setClickable(_mode == kSaveDialog);
			}

			if (slot->isEditable())
				drawEditBuffer(slot);
			else
				slot->paint();
		}
	}

	virtual void onAction(Widget *widget, int result = 0) {
		if (widget == _zupButton) {
			if (_gui->_baseSlot > 0) {
				if (_gui->_baseSlot >= 8)
					_gui->_baseSlot -= 8;
				else
					_gui->_baseSlot = 0;
				updateSlots();
			}
		} else if (widget == _upButton) {
			if (_gui->_baseSlot > 0) {
				_gui->_baseSlot--;
				updateSlots();
			}
		} else if (widget == _downButton) {
			if (_gui->_baseSlot < 92) {
				_gui->_baseSlot++;
				updateSlots();
			}
		} else if (widget == _zdownButton) {
			if (_gui->_baseSlot < 92) {
				if (_gui->_baseSlot <= 84)
					_gui->_baseSlot += 8;
				else
					_gui->_baseSlot = 92;
				updateSlots();
			}
		} else if (widget == _okButton) {
			setResult(1);
		} else if (widget == _cancelButton) {
			setResult(0);
		} else {
			Slot *slot = (Slot *) widget;
			int textWidth;
			byte tmp;
			int i;
			int j;

			switch (result) {
			case kWheelUp:
				onAction(_upButton);
				break;
			case kWheelDown:
				onAction(_downButton);
				break;
			case kSelectSlot:
			case kDeselectSlot:
				if (result == kSelectSlot)
					_selectedSlot = _gui->_baseSlot + (slot->getY() - 72) / 35;
				else if (result == kDeselectSlot)
					_selectedSlot = -1;

				for (i = 0; i < 8; i++)
					if (widget == _slotButton[i])
						break;

				for (j = 0; j < 8; j++) {
					if (j != i) {
						_slotButton[j]->setEditable(false);
						_slotButton[j]->setState(0);
					}
				}
				break;
			case kStartEditing:
				if (_selectedSlot >= 10)
					_firstPos = 5;
				else
					_firstPos = 4;

				strcpy((char *) _editBuffer, (char *) slot->getText());
				_editPos = strlen((char *) _editBuffer);
				_cursorTick = 0;
				_editBuffer[_editPos] = '_';
				_editBuffer[_editPos + 1] = 0;
				slot->setEditable(true);
				drawEditBuffer(slot);
				break;
			case kCursorTick:
				_cursorTick++;
				if (_cursorTick == 7) {
					_editBuffer[_editPos] = ' ';
					drawEditBuffer(slot);
				} else if (_cursorTick == 14) {
					_cursorTick = 0;
					_editBuffer[_editPos] = '_';
					drawEditBuffer(slot);
				}
				break;
			case 8:
				if (_editPos > _firstPos) {
					_editBuffer[_editPos - 1] = _editBuffer[_editPos];
					_editBuffer[_editPos--] = 0;
					drawEditBuffer(slot);
				}
				break;
			default:
				tmp = _editBuffer[_editPos];
				_editBuffer[_editPos] = 0;
				textWidth = _fr2->getTextWidth(_editBuffer);
				_editBuffer[_editPos] = tmp;

				if (textWidth < 340 && _editPos < SAVE_DESCRIPTION_LEN - 2) {
					_editBuffer[_editPos + 1] = _editBuffer[_editPos];
					_editBuffer[_editPos + 2] = 0;
					_editBuffer[_editPos++] = result;
					drawEditBuffer(slot);
				}
				break;
			}
		}
	}

	void drawEditBuffer(Slot *slot) {
		if (_selectedSlot == -1)
			return;

		// This will redraw a bit more than is strictly necessary,
		// but I doubt that will make any noticeable difference.

		slot->paint();
		_fr2->drawText(_editBuffer, 130, 78 + (_selectedSlot - _gui->_baseSlot) * 36);
	}

	virtual void paint() {
		Dialog::paint();

		if (_mode == kLoadDialog) {
			// Restore
			_fr1->drawText(149618690, 165, 377);
		} else {
			// Save
			_fr1->drawText(149618691, 165, 377);
		}
		// Cancel
		_fr1->drawText(149618689, 382, 377);
	}

	virtual void setResult(int result) {
		// Cancel

		if (result == 0) {
			Dialog::setResult(result);
			return;
		}

		// Save / Restore

		if (_selectedSlot == -1)
			return;

		if (_mode == kSaveDialog) {
			if (_editPos <= _firstPos)
				return;

			_editBuffer[_editPos] = 0;

			uint32 rv = _gui->_vm->saveGame(_selectedSlot, (byte *) &_editBuffer[_firstPos]);

			if (rv != SR_OK) {
				uint32 textId;

				switch (rv) {
				case SR_ERR_FILEOPEN:
					textId = 213516674;
					break;
				default:	// SR_ERR_WRITEFAIL
					textId = 213516676;
					break;
				}

				_gui->_vm->displayMsg(_gui->_vm->fetchTextLine(_gui->_vm->_resman->openResource(textId / SIZE), textId & 0xffff) + 2, 0);
				result = 0;
			}
		} else {
			uint32 rv = _gui->_vm->restoreGame(_selectedSlot);

			if (rv != SR_OK) {
				uint32 textId;

				switch (rv) {
				case SR_ERR_FILEOPEN:
					textId = 213516670;
					break;
				case SR_ERR_INCOMPATIBLE:
					textId = 213516671;
					break;
				default:	// SR_ERR_READFAIL
					textId = 213516673;
					break;
				}

				_gui->_vm->displayMsg(_gui->_vm->fetchTextLine(_gui->_vm->_resman->openResource(textId / SIZE), textId & 0xffff) + 2, 0);
				result = 0;
			} else {
				// Prime system with a game cycle

				// Reset the graphic 'BuildUnit' list before a
				// new logic list (see fnRegisterFrame)
				_gui->_vm->resetRenderLists();

				// Reset the mouse hot-spot list (see
				// fnRegisterMouse and fnRegisterFrame)
				_gui->_vm->resetMouseList();

				if (_gui->_vm->_logic->processSession())
					error("restore 1st cycle failed??");
			}
		}

		Dialog::setResult(result);
	}
};

Gui::Gui(Sword2Engine *vm) : _vm(vm), _baseSlot(0) {
	int i;

	for (i = 0; i < ARRAYSIZE(_musicVolume); i++) {
		_musicVolume[i] = (i * 255) / (ARRAYSIZE(_musicVolume) - 1);
		if ((i * 255) % (ARRAYSIZE(_musicVolume) - 1))
			_musicVolume[i]++;
	}

	for (i = 0; i < ARRAYSIZE(_soundVolume); i++) {
		_soundVolume[i] = (i * 255) / (ARRAYSIZE(_soundVolume) - 1);
		if ((i * 255) % (ARRAYSIZE(_soundVolume) - 1))
			_soundVolume[i]++;
	}

	ConfMan.registerDefault("music_volume", _musicVolume[12]);
	ConfMan.registerDefault("speech_volume", _soundVolume[10]);
	ConfMan.registerDefault("sfx_volume", _soundVolume[10]);
	ConfMan.registerDefault("music_mute", false);
	ConfMan.registerDefault("speech_mute", false);
	ConfMan.registerDefault("sfx_mute", false);
	ConfMan.registerDefault("gfx_details", 2);
	ConfMan.registerDefault("subtitles", false);
	ConfMan.registerDefault("object_labels", true);
	ConfMan.registerDefault("reverse_stereo", false);
}

void Gui::readOptionSettings(void) {
	_subtitles = ConfMan.getBool("subtitles");
	_pointerTextSelected = ConfMan.getBool("object_labels");
	_stereoReversed = ConfMan.getBool("reverse_stereo");

	updateGraphicsLevel((uint8) ConfMan.getInt("gfx_details"));

	_vm->_sound->setMusicVolume((16 * ConfMan.getInt("music_volume")) / 255);
	_vm->_sound->setSpeechVolume((14 * ConfMan.getInt("speech_volume")) / 255);
	_vm->_sound->setFxVolume((14 * ConfMan.getInt("sfx_volume")) / 255);
	_vm->_sound->muteMusic(ConfMan.getBool("music_mute"));
	_vm->_sound->muteSpeech(ConfMan.getBool("speech_mute"));
	_vm->_sound->muteFx(ConfMan.getBool("sfx_mute"));
	_vm->_sound->buildPanTable(_stereoReversed);
}

void Gui::writeOptionSettings(void) {
	ConfMan.set("music_volume", _musicVolume[_vm->_sound->getMusicVolume()]);
	ConfMan.set("speech_volume", _soundVolume[_vm->_sound->getSpeechVolume()]);
	ConfMan.set("sfx_volume", _soundVolume[_vm->_sound->getFxVolume()]);
	ConfMan.set("music_mute", _vm->_sound->isMusicMute());
	ConfMan.set("speech_mute", _vm->_sound->isSpeechMute());
	ConfMan.set("sfx_mute", _vm->_sound->isFxMute());
	ConfMan.set("gfx_details", _vm->_graphics->getRenderLevel());
	ConfMan.set("subtitles", _subtitles);
	ConfMan.set("object_labels", _pointerTextSelected);
	ConfMan.set("reverse_stereo", _stereoReversed);

	ConfMan.flushToDisk();
}

uint32 Gui::restoreControl(void) {
	// returns 0 for no restore
	//         1 for restored ok

	SaveLoadDialog loadDialog(this, kLoadDialog);
	return loadDialog.run();
}

void Gui::saveControl(void) {
	SaveLoadDialog saveDialog(this, kSaveDialog);
	_vm->_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, true);
	saveDialog.run();
	_vm->_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false);
}

bool Gui::startControl(void) {
	while (1) {
		MiniDialog startDialog(this, 0, 149618693, 149618690);

		if (startDialog.run())
			return true;

		if (_vm->_quit)
			return false;

		if (restoreControl())
			return false;

		if (_vm->_quit)
			return false;
	}

	return true;
}

void Gui::quitControl(void) {
	MiniDialog quitDialog(this, 149618692);

	if (quitDialog.run())
		_vm->closeGame();
}

void Gui::restartControl(void) {
	uint32 temp_demo_flag;

	MiniDialog restartDialog(this, 149618693);

	if (!restartDialog.run())
		return;

	_vm->_graphics->closeMenuImmediately();

	// Restart the game. To do this, we must...

	// Stop music instantly!
	_vm->killMusic();

	// In case we were dead - well we're not anymore!
	Logic::_scriptVars[DEAD] = 0;

	// Restart the game. Clear all memory and reset the globals
	temp_demo_flag = Logic::_scriptVars[DEMO];

	// Remove all resources from memory, including player object and
	// global variables
	_vm->_resman->removeAll();

	// Reopen global variables resource & send address to interpreter -
	// it won't be moving
	_vm->_logic->resetScriptVars();

	Logic::_scriptVars[DEMO] = temp_demo_flag;

	// Free all the route memory blocks from previous game
	_vm->_logic->_router->freeAllRouteMem();

	// Call the same function that first started us up
	_vm->startGame();

	// Prime system with a game cycle

	// Reset the graphic 'BuildUnit' list before a new logic list
	// (see fnRegisterFrame)
	_vm->resetRenderLists();

	// Reset the mouse hot-spot list (see fnRegisterMouse and
	// fnRegisterFrame)
	_vm->resetMouseList();

	_vm->_graphics->closeMenuImmediately();

	// FOR THE DEMO - FORCE THE SCROLLING TO BE RESET!
	// - this is taken from fnInitBackground
	// switch on scrolling (2 means first time on screen)
	_vm->_thisScreen.scroll_flag = 2;

	if (_vm->_logic->processSession())
		error("restart 1st cycle failed??");

	// So palette not restored immediately after control panel - we want
	// to fade up instead!
	_vm->_thisScreen.new_palette = 99;
}

void Gui::optionControl(void) {
	OptionsDialog optionsDialog(this);

	optionsDialog.run();
	return;
}

void Gui::updateGraphicsLevel(int newLevel) {
	if (newLevel < 0)
		newLevel = 0;
	else if (newLevel > 3)
		newLevel = 3;

	_vm->_graphics->setRenderLevel(newLevel);

	// update our global variable - which needs to be checked when dimming
	// the palette in PauseGame() in sword2.cpp (since palette-matching
	// cannot be done with dimmed palette so we turn down one notch while
	// dimmed, if at top level)

	_currentGraphicsLevel = newLevel;
}

} // End of namespace Sword2