/*
** settingsmenu.cpp
**
** This file is part of mkxp.
**
** Copyright (C) 2014 - 2021 Amaryllis Kulla <ancurio@mapleshrine.eu>
**
** mkxp 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.
**
** mkxp 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 mkxp.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "settingsmenu.h"

#include <SDL_video.h>
#include <SDL_ttf.h>
#include <SDL_surface.h>
#include <SDL_keyboard.h>
#include <SDL_rect.h>

#include "keybindings.h"
#include "eventthread.h"
#include "font.h"
#include "input.h"
#include "etc-internal.h"
#include "util.h"

#include <algorithm>
#include <assert.h>

const Vec2i winSize(580, 356);

const uint8_t cBgNorm = 50;
const uint8_t cBgDark = 20;
const uint8_t cLine = 0;
const uint8_t cText = 255;

const uint8_t frameWidth = 4;
const uint8_t fontSize = 15;

static bool pointInRect(const SDL_Rect &r, int x, int y)
{
	return (x >= r.x && x <= r.x+r.w && y >= r.y && y <= r.y+r.h);
}

typedef SettingsMenuPrivate SMP;

#define BTN_STRING(btn) { Input:: btn, #btn }
struct VButton
{
	Input::ButtonCode code;
	const char *str;
} static const vButtons[] =
{
	BTN_STRING(Up),
	BTN_STRING(Down),
	{ Input::L, "-" },
	BTN_STRING(Left),
	BTN_STRING(Right),
	{ Input::R, "-" },
	{ Input::A, "Sprint" },
	{ Input::B, "Cancel" },
	{ Input::C, "Action" },
	{ Input::X, "-" },
	{ Input::Y, "-" },
	{ Input::Z, "-" },
};

static elementsN(vButtons);

/* Human readable string representation */
std::string sourceDescString(const SourceDesc &src)
{
	char buf[128];
	char pos;

	switch (src.type)
	{
	case Invalid:
		return std::string();

	case Key:
	{
		if (src.d.scan == SDL_SCANCODE_LSHIFT)
			return "Shift";

		SDL_Keycode key = SDL_GetKeyFromScancode(src.d.scan);
		const char *str = SDL_GetKeyName(key);

		if (*str == '\0')
			return "Unknown key";
		else
			return str;
	}
	case JButton:
		snprintf(buf, sizeof(buf), "JS %d", src.d.jb);
		return buf;

	case JHat:
		switch(src.d.jh.pos)
		{
		case SDL_HAT_UP:
			pos = 'U';
			break;

		case SDL_HAT_DOWN:
			pos = 'D';
			break;

		case SDL_HAT_LEFT:
			pos = 'L';
			break;

		case SDL_HAT_RIGHT:
			pos = 'R';
			break;

		default:
			pos = '-';
		}
		snprintf(buf, sizeof(buf), "Hat %d:%c",
		         src.d.jh.hat, pos);
		return buf;

	case JAxis:
		snprintf(buf, sizeof(buf), "Axis %d%c",
		         src.d.ja.axis, src.d.ja.dir == Negative ? '-' : '+');
		return buf;
	}

	assert(!"unreachable");
	return "";
}

struct Widget
{
	/* Widgets have a static size and position,
	 * defined at creation */
	Widget(SMP *p, const IntRect &rect);

	/* Public methods take coordinates in global
	 * window coordinates */
	bool hit(int x, int y);
	void draw(SDL_Surface *surf);
	void motion(int x, int y);
	void leave();
	void click(int x, int y, uint8_t button);

protected:
	SMP *p;
	IntRect rect;

	/* Protected abstract methods are called with
	 * widget-local coordinates */
	virtual void drawHandler(SDL_Surface *surf) = 0;
	virtual void motionHandler(int x, int y) = 0;
	virtual void leaveHandler() = 0;
	virtual void clickHandler(int x, int y, uint8_t button) = 0;
};

struct BindingWidget : Widget
{
	VButton vb;
	/* Source slots */
	SourceDesc src[4];
	/* Flag indicating whether a slot source is used
	 * for multiple button targets (red indicator) */
	bool dupFlag[4];

	BindingWidget(int vbIndex, SMP *p, const IntRect &rect)
	    : Widget(p, rect),
	      vb(vButtons[vbIndex]),
	      hoveredCell(-1)
	{}

	void appendBindings(BDescVec &d) const;

protected:
	int hoveredCell;
	void setHoveredCell(int cell);
	/* Get the slot cell index that contains (x,y),
	 * or -1 if none */
	int cellIndex(int x, int y) const;

	void drawHandler(SDL_Surface *surf);
	void motionHandler(int x, int y);
	void leaveHandler();
	void clickHandler(int x, int y, uint8_t button);
};

struct Button : Widget
{
	typedef void (SMP::*Callback)();

	const char *str;
	Callback cb;

	Button(SMP *p, const IntRect &rect,
	       const char *str, Callback cb)
	    : Widget(p, rect),
	      str(str), cb(cb), hovered(false)
	{}

protected:
	bool hovered;

	void setHovered(bool val);

	void drawHandler(SDL_Surface *surf);
	void motionHandler(int, int);
	void leaveHandler();
	void clickHandler(int, int, uint8_t button);
};

struct Label : Widget
{
	const char *str;
	SDL_Color c;

	Label() : Widget(0, IntRect()) {}

	Label(SMP *p, const IntRect &rect,
	      const char *str, uint8_t r, uint8_t g, uint8_t b)
	    : Widget(p, rect),
	      str(str),
	      visible(true)
	{
		c.r = r;
		c.g = g;
		c.b = b;
		c.a = 255;
	}

	void setVisible(bool val);

protected:
	bool visible;

	void drawHandler(SDL_Surface *surf);
	void motionHandler(int, int) {}
	void leaveHandler() {}
	void clickHandler(int, int, uint8_t) {}
};

enum State
{
	Idle,
	AwaitingInput
};

enum Justification
{
	Left,
	Center
};

struct SettingsMenuPrivate
{
	State state;

	/* Necessary to decide which window gets to
	 * process joystick events */
	bool hasFocus;

	/* Tell the outer EventThread to destroy us */
	bool destroyReq;

	/* Offset added for all draw calls */
	Vec2i drawOff;

	SDL_Window *window;
	SDL_Surface *winSurf;
	uint32_t winID;

	TTF_Font *font;
	SDL_PixelFormat *rgb;

	RGSSThreadData &rtData;

	std::vector<BindingWidget> bWidgets;
	std::vector<Button> buttons;
	Label infoLabel;
	Label dupWarnLabel;
	std::vector<Widget*> widgets;
	Widget *hovered;

	SourceDesc *captureDesc;
	const char *captureName;

	SettingsMenuPrivate(RGSSThreadData &rtData)
	    : rtData(rtData)
	{}

	SDL_Surface *createSurface(int w, int h)
	{
		int bpp;
		Uint32 rMask, gMask, bMask, aMask;
		SDL_PixelFormatEnumToMasks(SDL_PIXELFORMAT_ABGR8888,
		                           &bpp, &rMask, &gMask, &bMask, &aMask);

		return SDL_CreateRGBSurface(0, w, h, bpp, rMask, gMask, bMask, 0);
	}

	void fillSurface(SDL_Surface *surf, uint8_t grey)
	{
		SDL_FillRect(surf, 0, SDL_MapRGBA(rgb, grey, grey, grey, 255));
	}

	void fillRect(SDL_Surface *surf,
	              int x, int y, int w, int h,
	              uint8_t r, uint8_t g, uint8_t b)
	{
		SDL_Rect rect = { drawOff.x+x, drawOff.y+y, w, h };
		SDL_FillRect(surf, &rect, SDL_MapRGB(rgb, r, g, b));
	}

	void fillRect(SDL_Surface *surf, uint8_t grey,
	              int x, int y, int w, int h)
	{
		fillRect(surf, x, y, w, h, grey, grey, grey);
	}

	void strokeLineH(SDL_Surface *surf, uint8_t grey,
	                 int x, int y, int length, int width)
	{
		fillRect(surf, grey, x, y-width/2, length, width);
	}

	void strokeLineV(SDL_Surface *surf, uint8_t grey,
	                 int x, int y, int length, int width)
	{
		fillRect(surf, grey, x-width/2, y, width, length);
	}

	void strokeLineH(SDL_Surface *surf, uint8_t r, uint8_t g, uint8_t b,
	                 int x, int y, int length, int width)
	{
		fillRect(surf, r, g, b, x, y-width/2, length, width);
	}

	void strokeLineV(SDL_Surface *surf, uint8_t r, uint8_t g, uint8_t b,
	                 int x, int y, int length, int width)
	{
		fillRect(surf, r, g, b, x-width/2, y, width, length);
	}

	void strokeRect(SDL_Surface *surf, uint8_t grey,
	                int x, int y, int w, int h,
	                int lineW)
	{
		strokeLineH(surf, grey, x, y, w, lineW);
		strokeLineH(surf, grey, x, y+h, w, lineW);

		strokeLineV(surf, grey, x, y, h, lineW);
		strokeLineV(surf, grey, x+w, y, h, lineW);
	}

	void strokeRectInner(SDL_Surface *surf,
	                     int x, int y, int w, int h, int lineW,
	                     uint8_t r, uint8_t g, uint8_t b)
	{
		fillRect(surf, x, y, w, lineW, r, g, b);
		fillRect(surf, x, y+h-lineW, w, lineW, r, g, b);
		fillRect(surf, x, y, lineW, h, r, g, b);
		fillRect(surf, x+w-lineW, y, lineW, h, r, g ,b);
	}

	void strokeRectInner(SDL_Surface *surf, uint8_t grey,
	                     int x, int y, int w, int h,
	                     int lineW)
	{
		strokeRectInner(surf, x, y, w, h, lineW, grey, grey, grey);
	}

	void applyFontStyle(bool bold)
	{
		if (bold)
			TTF_SetFontStyle(font, TTF_STYLE_BOLD);
		else
			TTF_SetFontStyle(font, TTF_STYLE_NORMAL);
	}

	SDL_Surface *createTextSurface(const char *str, bool bold)
	{
		SDL_Color c = { cText, cText, cText, 255 };
		applyFontStyle(bold);

		return TTF_RenderUTF8_Blended(font, str, c);
	}

	SDL_Surface *createTextSurface(const char *str, SDL_Color c,
	                               bool bold)
	{
		applyFontStyle(bold);
		return TTF_RenderUTF8_Blended(font, str, c);
	}

	/* Horizontally centered */
	void blitTextSurf(SDL_Surface *surf, int x, int y,
	                  int alignW, SDL_Surface *txtSurf,
	                  Justification just)
	{
		SDL_Rect dstRect;
		dstRect.x = drawOff.x;
		dstRect.y = drawOff.y + y - txtSurf->h / 2;
		dstRect.w = txtSurf->w;
		dstRect.h = txtSurf->h;

		switch (just)
		{
		case Left:
			dstRect.x += x;
			break;
		case Center:
			dstRect.x += x - (txtSurf->w - alignW) / 2;
			break;
		}

		if (txtSurf->w <= alignW)
		{
			SDL_BlitSurface(txtSurf, 0, surf, &dstRect);
		}
		else
		{
			dstRect.w = alignW;
			dstRect.x = x;
			SDL_BlitScaled(txtSurf, 0, surf, &dstRect);
		}
	}

	void drawText(SDL_Surface *surf, const char *str,
	              int x, int y, int alignW,
	              Justification just, SDL_Color c, bool bold = false)
	{
		SDL_Surface *txt = createTextSurface(str, c, bold);
		blitTextSurf(surf, x, y, alignW, txt, just);
		SDL_FreeSurface(txt);
	}

	void drawText(SDL_Surface *surf, const char *str,
	              int x, int y, int alignW,
	              Justification just, bool bold = false)
	{
		SDL_Color c = { cText, cText, cText, 255 };
		drawText(surf, str, x, y, alignW, just, c, bold);
	}

	void setupBindingData(const BDescVec &d)
	{
		size_t slotI[vButtonsN] = { 0 };

		for (size_t i = 0; i < bWidgets.size(); ++i)
			for (size_t j = 0; j < 4; ++j)
				bWidgets[i].src[j].type = Invalid;

		for (size_t i = 0; i < d.size(); ++i)
		{
			const BindingDesc &desc = d[i];
			const Input::ButtonCode trg = desc.target;

			size_t j;
			for (j = 0; j < vButtonsN; ++j)
				if (bWidgets[j].vb.code == trg)
					break;

			assert(j < vButtonsN);

			size_t &slot = slotI[j];
			BindingWidget &w = bWidgets[j];

			if (slot == 4)
				continue;

			w.src[slot++] = desc.src;
		}
	}

	void updateDuplicateStatus()
	{
		for (size_t i = 0; i < bWidgets.size(); ++i)
			for (size_t j = 0; j < 4; ++j)
				bWidgets[i].dupFlag[j] = false;

		bool haveDup = false;

		for (size_t i = 0; i < bWidgets.size(); ++i)
		{
			for (size_t j = 0; j < 4; ++j)
			{
				const SourceDesc &src = bWidgets[i].src[j];

				if (src.type == Invalid)
					continue;

				for (size_t k = 0; k < bWidgets.size(); ++k)
				{
					if (k == i)
						continue;

					for (size_t l = 0; l < 4; ++l)
					{
						if (bWidgets[k].src[l] != src)
							continue;

						bWidgets[i].dupFlag[j] = true;
						bWidgets[k].dupFlag[l] = true;
						haveDup = true;
					}
				}
			}
		}

		dupWarnLabel.setVisible(haveDup);
	}

	void redraw()
	{
		fillSurface(winSurf, cBgNorm);

		for (size_t i = 0; i < widgets.size(); ++i)
			widgets[i]->draw(winSurf);

		if (state == AwaitingInput)
		{
			char buf[64];
			snprintf(buf, sizeof(buf), "Press key or joystick button for \"%s\"", captureName);

			drawOff = Vec2i();

			SDL_Surface *dark = createSurface(winSize.x, winSize.y);
			fillSurface(dark, 0);
			SDL_SetSurfaceAlphaMod(dark, 128);
			SDL_SetSurfaceBlendMode(dark, SDL_BLENDMODE_BLEND);
			SDL_Surface *txt = createTextSurface(buf, false);

			SDL_BlitSurface(dark, 0, winSurf, 0);

			SDL_Rect fill;
			fill.x = (winSize.x - txt->w - 20) / 2;
			fill.y = (winSize.y - txt->h - 20) / 2;
			fill.w = txt->w + 20;
			fill.h = txt->h + 20;

			fillRect(winSurf, cBgNorm, fill.x, fill.y, fill.w, fill.h);
			strokeRectInner(winSurf, cLine, fill.x, fill.y, fill.w, fill.h, 2);

			fill.x += 10;
			fill.y += 10;
			fill.w = txt->w;
			fill.h = txt->h;

			SDL_BlitSurface(txt, 0, winSurf, &fill);

			SDL_FreeSurface(txt);
			SDL_FreeSurface(dark);
		}

		SDL_UpdateWindowSurface(window);
	}

	Widget *findWidget(int x, int y)
	{
		Widget *w = 0;

		for (size_t i = 0; i < widgets.size(); ++i)
			if (widgets[i]->hit(x, y))
			{
				w = widgets[i];
				break;
			}

		return w;
	}

	void onClick(const SDL_MouseButtonEvent &e)
	{
		if (e.button != SDL_BUTTON_LEFT && e.button != SDL_BUTTON_RIGHT)
			return;

		if (state == AwaitingInput)
		{
			state = Idle;
			redraw();
			return;
		}

		Widget *w = findWidget(e.x, e.y);

		if (w)
			w->click(e.x, e.y, e.button);
	}

	void onMotion(const SDL_MouseMotionEvent &e)
	{
		if (state == AwaitingInput)
			return;

		Widget *w = findWidget(e.x, e.y);

		if (w != hovered)
		{
			if (hovered)
				hovered->leave();
			hovered = w;
		}

		if (hovered)
			hovered->motion(e.x, e.y);
	}

	bool onCaptureInputEvent(const SDL_Event &event)
	{
		assert(captureDesc);
		SourceDesc &desc = *captureDesc;

		switch (event.type)
		{
		case SDL_KEYDOWN:
			desc.type = Key;
			desc.d.scan = event.key.keysym.scancode;

			/* Special case aliases */
			if (desc.d.scan == SDL_SCANCODE_RSHIFT)
				desc.d.scan = SDL_SCANCODE_LSHIFT;

			if (desc.d.scan == SDL_SCANCODE_KP_ENTER)
				desc.d.scan = SDL_SCANCODE_RETURN;

			break;

		case SDL_JOYBUTTONDOWN:
			desc.type = JButton;
			desc.d.jb = event.jbutton.button;
			break;

		case SDL_JOYHATMOTION:
		{
			int v = event.jhat.value;

			/* Only register if single directional input */
			if (v != SDL_HAT_LEFT && v != SDL_HAT_RIGHT &&
			    v != SDL_HAT_UP   && v != SDL_HAT_DOWN)
				return true;

			desc.type = JHat;
			desc.d.jh.hat = event.jhat.hat;
			desc.d.jh.pos = v;
			break;
		}

		case SDL_JOYAXISMOTION:
		{
			int v = event.jaxis.value;

			/* Only register if pushed halfway through */
			if (v > -JAXIS_THRESHOLD && v < JAXIS_THRESHOLD)
				return true;

			desc.type = JAxis;
			desc.d.ja.axis = event.jaxis.axis;
			desc.d.ja.dir = v < 0 ? Negative : Positive;
			break;
		}
		default:
			return false;
		}

		captureDesc = 0;
		state = Idle;
		updateDuplicateStatus();
		redraw();

		return true;
	}

	void onBWidgetCellClicked(SourceDesc &desc, const char *str, uint8_t button)
	{
		if (state != Idle)
			return;

		if (button == SDL_BUTTON_LEFT)
		{
			captureDesc = &desc;
			captureName = str;
			state = AwaitingInput;
		}
		else /* e.button == SDL_BUTTON_RIGHT */
		{
			/* Clear binding */
			desc.type = Invalid;
		}

		updateDuplicateStatus();
		redraw();
	}

	void onResetToDefault()
	{
		setupBindingData(genDefaultBindings(rtData.config));
		updateDuplicateStatus();
		redraw();
	}

	void onAccept()
	{
		BDescVec binds;

		for (size_t i = 0; i < bWidgets.size(); ++i)
			bWidgets[i].appendBindings(binds);

		rtData.bindingUpdateMsg.post(binds);

		/* Store the key bindings to disk as well to prevent config loss */
		storeBindings(binds, rtData.config);

		destroyReq = true;
	}

	void onCancel()
	{
		destroyReq = true;
	}
};

Widget::Widget(SMP *p, const IntRect &rect)
    : p(p), rect(rect)
{}

bool Widget::hit(int x, int y)
{
	return pointInRect(rect, x, y);
}

void Widget::draw(SDL_Surface *surf)
{
	Vec2i prev = p->drawOff;
	p->drawOff = rect.pos();
	drawHandler(surf);
	p->drawOff = prev;
}

void Widget::motion(int x, int y)
{
	motionHandler(x - rect.x, y - rect.y);
}

void Widget::leave()
{
	leaveHandler();
}

void Widget::click(int x, int y, uint8_t button)
{
	clickHandler(x - rect.x, y - rect.y, button);
}

/* Ratio of cell area to total widget width */
#define BW_CELL_R 0.69f

void BindingWidget::drawHandler(SDL_Surface *surf)
{
	const int cellW = (rect.w*BW_CELL_R) / 2;
	const int cellH = rect.h / 2;
	const int cellOffX = (1.0f-BW_CELL_R) * rect.w;

	const int cellOff[] =
	{
	    cellOffX, 1,
	    cellOffX+cellW, 1,
	    cellOffX, cellH,
	    cellOffX+cellW, cellH
	};

	const int lbOff[] =
	{
	    0, cellH / 2,
	    cellW, cellH / 2,
	    0, cellH + cellH / 2,
	    cellW, cellH + cellH / 2
	};

	/* Hovered cell background */
	if (hoveredCell != -1)
		p->fillRect(surf, cBgDark,
		            cellOff[hoveredCell*2], cellOff[hoveredCell*2+1],
		            cellW, cellH);

	/* Frame */
	p->strokeRectInner(surf, cLine, 0, 0, rect.w, rect.h, 2);

	/* Virtual button name */
	p->drawText(surf, vb.str, 1, rect.h/2, cellOffX, Center, true);

	/* Cell frames */
	p->strokeLineV(surf, cLine, cellOffX, 0, rect.h, 2);
	p->strokeLineV(surf, cLine, cellOffX+cellW, 0, rect.h, 2);
	p->strokeLineH(surf, cLine, cellOffX, cellH, cellW*2, 2);

	/* Draw binding labels */
	for (size_t i = 0; i < 4; ++i)
	{
		std::string lb = sourceDescString(src[i]);
		if (lb.empty())
			continue;

		const int x = lbOff[i*2+0];
		const int y = lbOff[i*2+1];
		p->drawText(surf, lb.c_str(), cellOffX+x+1, y, cellW-2, Center);
	}

	for (size_t i = 0; i < 4; ++i)
	{
		if (!dupFlag[i])
			continue;

		p->strokeRectInner(surf, cellOff[i*2]+1, cellOff[i*2+1]+1,
		        cellW-2, cellH-3, 1, 255, 0, 0);
	}
}

void BindingWidget::setHoveredCell(int cell)
{
	if (hoveredCell == cell)
		return;

	hoveredCell = cell;
	p->redraw();
}

void BindingWidget::motionHandler(int x, int y)
{
	setHoveredCell(cellIndex(x, y));
}

void BindingWidget::leaveHandler()
{
	setHoveredCell(-1);
}

void BindingWidget::clickHandler(int x, int y, uint8_t button)
{
	int cell = cellIndex(x, y);

	if (cell == -1)
		return;

	p->onBWidgetCellClicked(src[cell], vb.str, button);
}

int BindingWidget::cellIndex(int x, int y) const
{
	const int cellW = (rect.w*BW_CELL_R) / 2;
	const int cellH = rect.h / 2;
	const int cellOff = (1.0f-BW_CELL_R) * rect.w;

	if (x < cellOff)
		return -1;

	x -= cellOff;

	if (y < cellH)
		if (x < cellW)
			return 0;
		else
			return 1;
	else
		if (x < cellW)
			return 2;
		else
			return 3;

	return -1;
}

void BindingWidget::appendBindings(BDescVec &d) const
{
	for (size_t i = 0; i < 4; ++i)
	{
		if (src[i].type == Invalid)
			continue;

		BindingDesc desc;
		desc.src = src[i];
		desc.target = vb.code;
		d.push_back(desc);
	}
}

void Button::setHovered(bool val)
{
	if (hovered == val)
		return;

	hovered = val;
	p->redraw();
}

void Button::drawHandler(SDL_Surface *surf)
{
	if (hovered)
		p->fillRect(surf, cBgDark, 0, 0, rect.w, rect.h);

	p->strokeRectInner(surf, cLine, 0, 0, rect.w, rect.h, 2);
	p->drawText(surf, str, 0, rect.h/2, rect.w, Center);
}

void Button::motionHandler(int, int)
{
	setHovered(true);
}

void Button::leaveHandler()
{
	setHovered(false);
}

void Button::clickHandler(int, int, uint8_t button)
{
	if (button != SDL_BUTTON_LEFT)
		return;

	(p->*cb)();
}

void Label::setVisible(bool val)
{
	if (visible == val)
		return;

	visible = val;
	p->redraw();
}

void Label::drawHandler(SDL_Surface *surf)
{
	if (visible)
		p->drawText(surf, str, 0, rect.h/2, rect.w, Left, c);
}

SettingsMenu::SettingsMenu(RGSSThreadData &rtData)
{
	p = new SettingsMenuPrivate(rtData);
	p->state = Idle;

	p->hasFocus = false;
	p->destroyReq = false;

	p->window = SDL_CreateWindow("Key bindings",
	                             SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
	                             winSize.x, winSize.y, SDL_WINDOW_INPUT_FOCUS);
	p->winSurf = SDL_GetWindowSurface(p->window);
	p->winID = SDL_GetWindowID(p->window);

	p->font = SharedFontState::openBundled(fontSize);

	p->rgb = p->winSurf->format;

	const size_t layoutW = 4;
	const size_t layoutH = 3;
	assert(layoutW*layoutH == vButtonsN);

	const int bWidgetW = winSize.x / layoutH;
	const int bWidgetH = 64;
	const int bWidgetY = winSize.y - layoutW*bWidgetH - 48;

	for (int y = 0; y < 4; ++y)
		for (int x = 0; x < 3; ++x)
		{
			int i = y*3+x;
			BindingWidget w(i, p, IntRect(x*bWidgetW, bWidgetY+y*bWidgetH,
			                              bWidgetW, bWidgetH));
			p->bWidgets.push_back(w);
		}

	for (size_t i = 0; i< p->bWidgets.size(); ++i)
		p->widgets.push_back(&p->bWidgets[i]);

	BDescVec binds;
	rtData.bindingUpdateMsg.get(binds);
	p->setupBindingData(binds);

	/* Buttons */
	const int buttonH = 32;
	const int buttonY = winSize.y - buttonH - 8;

	IntRect btRects[] =
	{
	    IntRect(16, buttonY, 112, buttonH),
	    IntRect(winSize.x-16-64*2-8, buttonY, 64, buttonH),
	    IntRect(winSize.x-16-64, buttonY, 64, buttonH)
	};

	p->buttons.push_back(Button(p, btRects[0], "Reset defaults", &SMP::onResetToDefault));
	p->buttons.push_back(Button(p, btRects[1], "Cancel", &SMP::onCancel));
	p->buttons.push_back(Button(p, btRects[2], "Accept", &SMP::onAccept));

	for (size_t i = 0; i< p->buttons.size(); ++i)
		p->widgets.push_back(&p->buttons[i]);

	/* Labels */
	const char *info = "Use left click to bind a slot, right click to clear its binding";
	p->infoLabel = Label(p, IntRect(16, 6, winSize.x, 16), info, cText, cText, cText);

	const char *warn = "Warning: Same physical key bound to multiple slots";
	p->dupWarnLabel = Label(p, IntRect(16, 26, winSize.x, 16), warn, 255, 0, 0);

	p->widgets.push_back(&p->infoLabel);
	p->widgets.push_back(&p->dupWarnLabel);

	p->hovered = 0;
	p->captureDesc = 0;
	p->captureName = 0;

	p->updateDuplicateStatus();

	p->redraw();
}

SettingsMenu::~SettingsMenu()
{
	TTF_CloseFont(p->font);
	SDL_DestroyWindow(p->window);

	delete p;
}

bool SettingsMenu::onEvent(const SDL_Event &event)
{
	/* First, check whether this event is for us */
	switch (event.type)
	{
	case SDL_WINDOWEVENT :
	case SDL_MOUSEBUTTONDOWN :
	case SDL_MOUSEBUTTONUP :
	case SDL_MOUSEMOTION :
	case SDL_KEYDOWN :
	case SDL_KEYUP :
		/* We can do this because windowID has the same
		 * struct offset in all these event types */
		if (event.window.windowID != p->winID)
			return false;
		break;

	case SDL_JOYBUTTONDOWN :
	case SDL_JOYBUTTONUP :
	case SDL_JOYHATMOTION :
	case SDL_JOYAXISMOTION :
		if (!p->hasFocus)
			return false;
		break;

	/* Don't try to handle something we don't understand */
	default:
		return false;
	}

	/* Now process it.. */
	switch (event.type)
	{
	/* Ignore these event */
	case SDL_MOUSEBUTTONUP :
	case SDL_KEYUP :
		return true;

	case SDL_WINDOWEVENT :
		switch (event.window.event)
		{
		case SDL_WINDOWEVENT_SHOWN : // SDL is bugged and doesn't give us a first FOCUS_GAINED event
		case SDL_WINDOWEVENT_FOCUS_GAINED :
			p->hasFocus = true;
			break;

		case SDL_WINDOWEVENT_FOCUS_LOST :
			p->hasFocus = false;
			break;

		case SDL_WINDOWEVENT_EXPOSED :
			SDL_UpdateWindowSurface(p->window);
			break;

		case SDL_WINDOWEVENT_LEAVE:
			if (p->hovered)
			{
				p->hovered->leave();
				p->hovered = 0;
			}
			break;

		case SDL_WINDOWEVENT_CLOSE:
			p->onCancel();
		}

		return true;

	case SDL_MOUSEMOTION:
		p->onMotion(event.motion);
		return true;

	case SDL_KEYDOWN:
		if (p->state != AwaitingInput)
		{
			if (event.key.keysym.sym == SDLK_RETURN)
				p->onAccept();
			else if (event.key.keysym.sym == SDLK_ESCAPE)
				p->onCancel();

			return true;
		}

		/* Don't let the user bind keys that trigger
		 * mkxp functions */
		switch(event.key.keysym.scancode)
		{
		case SDL_SCANCODE_F1:
		case SDL_SCANCODE_F2:
		case SDL_SCANCODE_F12:
			return true;
		default:
			break;
		}

	case SDL_JOYBUTTONDOWN:
	case SDL_JOYHATMOTION:
	case SDL_JOYAXISMOTION:
		if (p->state != AwaitingInput)
			return true;
		break;

	case SDL_MOUSEBUTTONDOWN:
		p->onClick(event.button);
		return true;
	default:
		return true;
	}

	if (p->state == AwaitingInput)
		return p->onCaptureInputEvent(event);

	return true;
}

void SettingsMenu::raise()
{
	SDL_RaiseWindow(p->window);
}

bool SettingsMenu::destroyReq() const
{
	return p->destroyReq;
}
