#include "CmdList.h"

#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "swis.h"

#include "WimpLib:Array.h"
#include "WimpLib:Exception.h"
#include "WimpLib:CIcon.h"
#include "WimpLib:Desktop.h"
#include "WimpLib:mem.h"
#include "WimpLib:Message.h"
#include "WimpLib:Menu.h"
#include "WimpLib:Sprites.h"
#include "WimpLib:Window.h"
#include "WimpLib:Task.h"
#include "WimpLib:Template.h"
#include "WimpLib:Throwback.h"
#include "WimpLib:UTils.h"
#include "DigitalCD.h"

/*
 * Warning: a big assumption is made that the arrays of CmdList
 *          are initially large enough and won't be resized
 *          as this would mess the direct pointers
 *          to the various objects in these arrays.
 */

#define TOOLBAR_BORDER 4

typedef enum
{ EKeyName_Esc = 0
, EKeyName_Return
, EKeyName_Home
, EKeyName_Space
, EKeyName_Delete
, EKeyName_Print
, EKeyName_Tab
, EKeyName_End
, EKeyName_Left
, EKeyName_Right
, EKeyName_Down
, EKeyName_Up
, EKeyName_Insert
, EKeyName_Max
} EKeyNames;

static const char* KeyNames[EKeyName_Max];

static CmdList* TheCmdList = NULL;
static char* pkeynames = NULL;

static CmdContext ctx =
{ NULL
, NULL
, -1
, HWind_None
, HIcon_None
};

typedef struct CmdDef
{
	CmdID       id;
	char        name[32];
	char        msgid[9];
} CmdDef;

typedef struct KeyDef
{
	int             key;
	const CmdDef*   pcmd;
} KeyDef;

struct Keymap
{
	char            name[32];
	const KeyDef*   pkeys;
	const Keymap*   pparent;
};

typedef enum
{ MenuDef_Flags_Seperator = 0x01
, MenuDef_Flags_End       = 0x80
} MenuDef_Flags;

typedef struct MenuDef MenuDef;
typedef struct MItemDef
{
	unsigned int    flags;
	const CmdDef*   pcmd;
	const CmdDef*   psubcmd;
	const MenuDef*  psubmenu;
} MItemDef;

struct MenuDef
{
	char                name[32];
	char                titleid[16];
	char                itemsid[16];
	const MItemDef*     pitems;
	const Keymap*       pkmap;
	HMenu               menu;
};

typedef enum
{ ToolbarDef_Flags_Seperator = 0x01
, ToolbarDef_Flags_End       = 0x80
} ToolbarDef_Flags;

typedef struct ButtonDef
{
	unsigned int    flags;
	char		    sprite[13];
	const CmdDef*   pselect;
	const CmdDef*   padjust;
} ButtonDef;

struct Toolbar
{
	// Def
	char                name[32];
	const ButtonDef*    buttons;
	// Runtime copies only
	HWind               w;
	unsigned int        count;
	const CmdHandler*   handler;
	void*               handle;
};

// Array of CmdDef, with indexes for dichotomic search
struct CmdList
{
	Array*     cmds;
	Array*     mitems;
	Array*     menus;
	Array*     keys;
	Array*     keymaps;
	Array*     buttons;
	Array*     toolbars;
	CmdDef**   pcmdindexes;
	CmdDef**   pidindexes;
};

static const char* KeyToString(int key)
{
	static char string[32];
	char* p = string;

	if (key < 0)
		return NULL;

	if (key >= 0x180)
	{
		if (key & 0x10)
		{
			*p = '';
			p++;
		}
		if (key & 0x20)
		{
			*p = '^';
			p++;
		}
		key &= ~0x30;

		if (key == 0x180)
			strcpy(p, KeyNames[EKeyName_Print]);
		else if (key <= 0x189)
		{
			p[0] = 'F';
			p[1] = '0' + (key & 0xf);
			p[2] = 0;
		}
		else if (key == 0x18A)
			strcpy(p, KeyNames[EKeyName_Tab]);
		else if (key == 0x18B)
			strcpy(p, KeyNames[EKeyName_End]);
		else if (key == 0x18C)
			strcpy(p, KeyNames[EKeyName_Left]);
		else if (key == 0x18D)
			strcpy(p, KeyNames[EKeyName_Right]);
		else if (key == 0x18E)
			strcpy(p, KeyNames[EKeyName_Down]);
		else if (key == 0x18F)
			strcpy(p, KeyNames[EKeyName_Up]);
		else if (key <= 0x1C9)
			return NULL;
		else if (key <= 0x1CC)
		{
			p[0] = 'F';
			p[1] = '1';
			p[2] = '0' + (key - 0x1CA);
			p[3] = 0;
		}
		else if (key == 0x1CD)
			strcpy(p, KeyNames[EKeyName_Insert]);
		else
			return NULL;

		return string;
	}

	if (key >= 0x100)
	{
		if ((key <= 0x120) || (key == 0x17f))
		{
			*p = '';
			p++;
			key -= 0x100;
		}
		else if ((key < 0x13B) && (key != 0x12D))
			return NULL;
		else if (key < 0x13F)
		{
			*p = '^';
			p++;
			key -= 0x120;
		}
		else if ((key < 0x15B) && (key != 0x14D))
			return NULL;
		else if (key < 0x15F)
		{
			p[0] = '';
			p[1] = '^';
			p += 2;
			key -= 0x140;
		}
        else
        	return NULL;
	}

	if (key == 0x0D)
		strcpy(p, KeyNames[EKeyName_Return]);
	else if (key <= 0x1A)
	{
		*p = '^';
		p++;

		if (key == 0x00)
			strcpy(p, KeyNames[EKeyName_Space]);
		else
		{
			p[0] = key - 1 + 'A';
			p[1] = 0;
		}
	}
	else if (key == 0x1B)
		strcpy(p, KeyNames[EKeyName_Esc]);
	else if (key == 0x1E)
		strcpy(p, KeyNames[EKeyName_Home]);
	else if (key == 0x1F)
	{
		p[0] = '^';
		strcpy(p + 1, KeyNames[EKeyName_Delete]);
	}
	else if (key == 0x20)
	{
		if (p != string)
			strcpy(p, KeyNames[EKeyName_Space]);
		else
			return NULL; // Not a RISC OS menu function key, see KeyNames list in Wimp messages :-(
	}
	else if (key == 0x7f)
		strcpy(p, KeyNames[EKeyName_Delete]);
	else
		return NULL;

	return string;
}

/**
 * Finds the first token in a string (blank seperated)
 * and returns a pointer after the end of the found token.
 * A Nil char is set at the end of the token.
 *
 * @param  pstring  String to parse
 * @param  pptoken  Pointer where address start of token will be stored on exit
 *                  (will contain NULL if no token was found).
 *
 * @returns  Pointer to character following the found token or NULL.
 */
static char* get_token(char* pstring, const char** pptoken)
{
	if (!pstring)
	{
		*pptoken = NULL;
		return NULL;
	}

	while (*pstring)
	{
		if (*pstring > 32)
			break;
		pstring++;
	}

	if (*pstring)
	{
		// token start found
		*pptoken = pstring;

		while (*pstring > 32)
			pstring++;

		// token end, more tokens?
		if (*pstring)
		{
			*pstring = 0;
			return pstring + 1;
		}

		return NULL;
	}

	*pptoken = NULL;
	return NULL;
}

// Compare callback for qsort
static int compare_ids(const void* pa, const void* pb)
{
	const CmdDef* const* pca = pa;
	const CmdDef* const* pcb = pb;

	return (*pca)->id - (*pcb)->id;
}

// Compare callback for qsort
static int compare_names(const void* pa, const void* pb)
{
	const CmdDef* const* pca = pa;
	const CmdDef* const* pcb = pb;

	return strcmp((*pca)->name, (*pcb)->name);
}

/**
 *  Returns a new CmdList.
 *
 * @returns  A new CmdList.
 */
static CmdList* throw_New_CmdList(void)
{
	CmdList* This = throw_mem_calloc(sizeof(*This), 1);

	try
	{
		if (!pkeynames)
		{
			const char* ptoken;
			char* pnext;
			int i = 0;

			pkeynames = throw_mem_allocstring(Msg_Lookup("KeyNames"));

			pnext = get_token(pkeynames, &ptoken);
			while (ptoken && (i < EKeyName_Max))
			{
				KeyNames[i++] = ptoken;

				pnext = get_token(pnext, &ptoken);
			}
		}

		This->cmds = New_Array(sizeof(CmdDef), 512);
		This->mitems = New_Array(sizeof(MItemDef), 512);
		This->menus = New_Array(sizeof(MenuDef), 128);
		This->keys = New_Array(sizeof(KeyDef), 512);
		This->keymaps = New_Array(sizeof(Keymap), 128);
		This->buttons = New_Array(sizeof(ButtonDef), 128);
		This->toolbars = New_Array(sizeof(Toolbar), 128);
	}
	catch
	{
		mem_free(This);
		throw_current();
	}
	catch_end

	return This;
}

/**
 * Deletes an CmdList.
 */
static void Delete_CmdList(CmdList* This)
{
	if (This)
	{
		mem_free(This->pcmdindexes);
		mem_free(This->pidindexes);
		Delete_Array(This->toolbars);
		Delete_Array(This->buttons);
		Delete_Array(This->keymaps);
		Delete_Array(This->keys);
		if (This->menus)
		{
			for (int i = Array_Count(This->menus) - 1; i >= 0; i--)
			{
				MenuDef* menu = (MenuDef*) Array_Get(This->menus, i);
				Delete_Menu(menu->menu, false);
			}
			Delete_Array(This->menus);
		}
		Delete_Array(This->mitems);
		Delete_Array(This->cmds);
		mem_free(This);
	}
	mem_free(pkeynames);
	pkeynames = NULL;
}

/**
 * Returns a unique instance of CmdList.
 *
 * @returns  Unique instance of CmdList.
 */
CmdList* CmdList_Get(void)
{
	if (!TheCmdList)
		TheCmdList = throw_New_CmdList();

	return TheCmdList;
}

/**
 * Releases existing CmdList.
 */
void CmdList_Release(void)
{
	Delete_CmdList(TheCmdList);

	TheCmdList = NULL;
}

static void parse_error(int line, const char* pformat, ...)
{
	va_list arg;

	va_start(arg, pformat);
	Throwback_VError(line, 0, pformat, arg);
	va_end(arg);
}

/**
 * Finds a command by its name.
 *
 * @param  name  Name of command to find.
 *
 * @returns  Command descriptor or NULL.
 */
static const CmdDef* CmdList_FindCmdByName(const CmdList* This, const char* name)
{
	int i = 0;
	int k = Array_Count(This->cmds) - 1;
	int j = (i + k) / 2;
	int val = strcmp(This->pcmdindexes[j]->name, name);

	while (i < j)
	{
		if (!val) return This->pcmdindexes[j];
		else if (val > 0) k = j;
		else i = j;

	    j = (i + k) / 2;
		val = strcmp(This->pcmdindexes[j]->name, name);
	}

	if (!val) return This->pcmdindexes[j];

	val = strcmp(This->pcmdindexes[k]->name, name);
	if (!val) return This->pcmdindexes[k];

	// Not found
	return NULL;
}

/**
 * Finds a command by its id.
 *
 * @param  id  Id of command to find.
 *
 * @returns  Command descriptor or NULL.
 */
static const CmdDef* CmdList_FindCmdById(const CmdList* This, CmdID id)
{
	int i = 0;
	int k = Array_Count(This->cmds) - 1;
	int j = (i + k) / 2;
	int val = This->pcmdindexes[j]->id - id;

	while (i < j)
	{
		if (!val) return This->pcmdindexes[j];
		else if (val > 0) k = j;
		else i = j;

	    j = (i + k) / 2;
		val = This->pcmdindexes[j]->id - id;
	}

	if (!val) return This->pcmdindexes[j];

	val = This->pcmdindexes[k]->id - id;
	if (!val) return This->pcmdindexes[k];

	// Not found
	return NULL;
}

/**
 * Parses the command file "<App$Dir>.Cmds".
 */
static void throw_CmdList_ParseCommands(CmdList* This)
{
	const char* volatile pfilename = NULL;
	FILE* volatile file = NULL;
	char string[256];
	char prefix[10];
	char msgprefix[5];
	CmdDef elem = {0};
	const char* ptoken;
	char* pnext;
	int line = 0;
	bool bError = false;

	try
	{
		pfilename = throw_mem_allocprint("%s.Cmds", Task_GetDir());
		file = fopen(pfilename, "rb");

		if (file == NULL)
			throw_string("ErrDocFRead: %s", pfilename);

		Throwback_Start(pfilename);

		while (true)
		{
			int val;

			line++;

			if (fgets(string, sizeof(string) - 1, file) == NULL)
				break;

			// ignore comments
			pnext = strchr(string, '#');
			if (pnext) *pnext = 0;

			pnext = get_token(string, &ptoken);
			if (ptoken)
			{
				if (isdigit(ptoken[0]))
				{
					// command

					// command index
					val = -1;
					sscanf(ptoken, "%i", &val);
					if (val & ~0xffff)
					{
						parse_error(line, "Invalid command id '%s'", ptoken);
						bError = true;
						continue;
					}
					snprintf(elem.msgid, sizeof(elem.msgid), "%s%04X", msgprefix, val);
					val += (elem.id & ~0xffff);
					if (val < elem.id)
					{
						parse_error(line, "Command ids must be in increasing order");
						bError = true;
						continue;
					}
					elem.id = val;

					// command suffix
					pnext = get_token(pnext, &ptoken);
					if (!ptoken)
					{
						parse_error(line, "Missing command suffix");
						bError = true;
						continue;
					}
					snprintf(&elem.name[0], sizeof(elem.name) - 1, "%s.%s", prefix, ptoken);

					Array_Insert(This->cmds, -1, &elem);
				}
				else
				{
					// new command block

					// name prefix
					if (strlen(ptoken) >= sizeof(prefix))
					{
						parse_error(line, "Command prefix too long");
						bError = true;
						continue;
					}
					strcpy(prefix, ptoken);

					// base index
					pnext = get_token(pnext, &ptoken);
					val = -1;
					if (!ptoken)
					{
						parse_error(line, "Missing base index");
						bError = true;
						continue;
					}

					sscanf(ptoken, "%i", &val);
					if ((val & 0xffff) || (val < elem.id))
					{
						parse_error(line, "Invalid base index '%s'", ptoken);
						bError = true;
						continue;
					}
					elem.id = val;

					// msg prefix
					pnext = get_token(pnext, &ptoken);
					if (!ptoken)
					{
						parse_error(line, "Missing message token prefix", ptoken);
						bError = true;
						continue;
					}

					if (strlen(ptoken) >= sizeof(msgprefix))
					{
						parse_error(line, "Invalid message token prefix '%s'", ptoken);
						bError = true;
						continue;
					}
					strcpy(msgprefix, ptoken);
				}
			}

			pnext = get_token(pnext, &ptoken);
			if (ptoken)
			{
				parse_error(line, "Unexpected '%s'", ptoken);
				bError = true;
				continue;
			}
		}

		Throwback_Stop();

		if (bError) throw_string("ErrParse:%s", pfilename);
	}
	catch
	{
		if (file != NULL) fclose(file);
		if (pfilename != NULL) mem_free(pfilename);
		throw_current();
	}
	catch_end

	fclose(file);
	mem_free(pfilename);

	int count = Array_Count(This->cmds);

	// define indexes
	This->pidindexes = throw_mem_alloc(count*sizeof(*This->pidindexes));
	This->pcmdindexes = throw_mem_alloc(count*sizeof(*This->pcmdindexes));

	for (int i = 0; i < count; i++)
	{
		CmdDef* pcmd = Array_Get(This->cmds, i);
		This->pidindexes[i] = pcmd;
		This->pcmdindexes[i] = pcmd;
	}

	qsort(This->pidindexes, count, sizeof(This->pidindexes[0]), compare_ids);
	qsort(This->pcmdindexes, count, sizeof(This->pcmdindexes[0]), compare_names);
}

//------------------------------------------------------------------------------

/**
 * Finds a keymap by its name.
 *
 * @param  name  Name of keymap to find.
 *
 * @returns  Keymap descriptor.
 */
static const Keymap* CmdList_FindKeymap(const CmdList* This, const char* name)
{
	for (int i = Array_Count(This->keymaps) - 1; i >= 0; i--)
	{
		const Keymap* pdef = Array_Get(This->keymaps, i);

		if (!strcmp(pdef->name, name))
			return pdef;
	}

	return NULL;
}

/**
 * Finds the command corresponding to a key.
 *
 * @param  name  Name of keymap to find.
 *
 * @returns  Command descriptor.
 */
static const CmdDef* Keymap_FindCmd(const Keymap* kmap, int key)
{
	for (const KeyDef* pdef = kmap->pkeys; pdef->key != -1; pdef++)
	{
		if (pdef->key == key)
			return pdef->pcmd;
	}

	if (kmap->pparent)
		return Keymap_FindCmd(kmap->pparent, key);

	// Not found
	return NULL;
}

/**
 * Finds the key corresponding to a command.
 *
 * @param  name  Name of keymap to find.
 *
 * @returns  key.
 */
static int Keymap_FindKey(const Keymap* kmap, const CmdDef* pcmd)
{
	for (const KeyDef* pdef = kmap->pkeys; pdef->key != -1; pdef++)
	{
		if (pdef->pcmd == pcmd)
			return pdef->key;
	}

	if (kmap->pparent)
		return Keymap_FindKey(kmap->pparent, pcmd);

	// Not found
	return -1;
}

/**
 * Parses the keymaps file "<App$Dir>.Keymaps".
 */
static void throw_CmdList_ParseKeymaps(CmdList* This)
{
	const char* volatile pfilename = NULL;
	FILE* volatile file = NULL;
	char string[256];
	KeyDef key = {-1, 0};
	Keymap map;
	const char* ptoken;
	char* pnext;
	int line = 0;
	bool bError = false;

	try
	{
		pfilename = throw_mem_allocprint("%s.Keymaps", Task_GetDir());
		file = fopen(pfilename, "rb");

		if (file == NULL)
			throw_string("ErrDocFRead: %s", pfilename);

		Throwback_Start(pfilename);

		while (true)
		{
			line++;

			if (fgets(string, 255, file) == NULL)
				break;

			// ignore comments
			pnext = strchr(string, '#');
			if (pnext) *pnext = 0;

			pnext = get_token(string, &ptoken);
			if (ptoken)
			{
				if (*ptoken == '=')
				{
					map.pparent = NULL;
					// new keymap
					ptoken++;

					// keymap name
					if (!*ptoken || (strlen(ptoken) >= sizeof(map.name)))
					{
						parse_error(line, "Missing or invalid keymap name");
						bError = true;
						continue;
					}
					strcpy(map.name, ptoken);

					pnext = get_token(pnext, &ptoken);
					if (ptoken)
					{
						map.pparent = CmdList_FindKeymap(This, ptoken);
						if (map.pparent == NULL)
						{
							parse_error(line, "Unknown parent keymap");
							bError = true;
							continue;
						}
					}

					// insert end marker for previous keymap
					key.key = -1;
					key.pcmd = NULL;
					Array_Insert(This->keys, -1, &key);

					const KeyDef* pKey = Array_Get(This->keys, 0);
					map.pkeys = pKey + Array_Count(This->keys);
					Array_Insert(This->keymaps, -1, &map);

					pnext = get_token(pnext, &ptoken);
				}
				else if (isdigit(*ptoken) || !ptoken[1])
				{
					// key
					if (ptoken[1])
						sscanf(ptoken, "%i", &key.key);
					else
						key.key = *ptoken;

					// command name
					pnext = get_token(pnext, &ptoken);
					key.pcmd = CmdList_FindCmdByName(This, ptoken);
					if (key.pcmd == NULL)
					{
						parse_error(line, "Unkown command '%s'", ptoken);
						bError = true;
						continue;
					}

					Array_Insert(This->keys, -1, &key);

					pnext = get_token(pnext, &ptoken);
				}
			}

			if (ptoken)
			{
				parse_error(line, "Unexpected '%s'", ptoken);
				bError = true;
				continue;
			}
		}

		Throwback_Stop();

		// insert end marker for last keymap
		key.key = -1;
		key.pcmd = NULL;
		Array_Insert(This->keys, -1, &key);

		if (bError) throw_string("ErrParse:%s", pfilename);
	}
	catch
	{
		if (file != NULL) fclose(file);
		if (pfilename != NULL) mem_free(pfilename);
		throw_current();
	}
	catch_end

	fclose(file);
	mem_free(pfilename);
}

//------------------------------------------------------------------------------

/**
 * Finds a menu by its name.
 *
 * @param  name  Name of menu to find.
 *
 * @returns  Menu descriptor.
 */
static const MenuDef* CmdList_FindMenu(const CmdList* This, const char* name)
{
	for (int i = Array_Count(This->menus) - 1; i >= 0; i--)
	{
		const MenuDef* pdef = Array_Get(This->menus, i);

		if (!strcmp(pdef->name, name))
			return pdef;
	}

	return NULL;
}

/**
 * Parses the menu file "<App$Dir>.Menus".
 */
static void throw_CmdList_ParseMenus(CmdList* This)
{
	const char* volatile pfilename = NULL;
	FILE* volatile file = NULL;
	char string[256];
	MItemDef item;
	MenuDef menu;
	const char* ptoken;
	char* pnext;
	int line = 0;
	bool bError = false;

	try
	{
		pfilename = throw_mem_allocprint("%s.Menus", Task_GetDir());
		file = fopen(pfilename, "rb");

		if (file == NULL)
			throw_string("ErrDocFRead: %s", pfilename);

		Throwback_Start(pfilename);

		while (true)
		{
			const char* ptitle = NULL;
			line++;

			if (fgets(string, 255, file) == NULL)
				break;

			// ignore comments
			pnext = strchr(string, '#');
			if (pnext) *pnext = 0;

			pnext = get_token(string, &ptoken);
			if (ptoken)
			{
				if (*ptoken == '=')
				{
					// new menu
					ptoken++;

					// menu name
					if (!*ptoken || (strlen(ptoken) >= sizeof(menu.name)))
					{
						parse_error(line, "Missing or invalid menu name");
						bError = true;
						continue;
					}
					strcpy(menu.name, ptoken);
					menu.titleid[0] = 0;
					menu.itemsid[0] = 0;
					menu.menu = NULL;

					// menu keymap
					pnext = get_token(pnext, &ptoken);
					if (!*ptoken)
					{
						parse_error(line, "Missing menu keymap");
						bError = true;
						continue;
					}

					menu.pkmap = CmdList_FindKeymap(This, ptoken);
					if (menu.pkmap == NULL)
					{
						parse_error(line, "Unknown menu keymap");
						bError = true;
						continue;
					}

					// menu title msgid (optional)
					pnext = get_token(pnext, &ptitle);

					if (ptitle)
					{
						if (strlen(ptitle) >= sizeof(menu.titleid))
						{
							parse_error(line, "Missing or invalid menu title message id");
							bError = true;
							continue;
						}
						strcpy(menu.titleid, ptitle);

						// menu items msgid (optional)
						pnext = get_token(pnext, &ptoken);
						if (ptoken)
						{
							if (strlen(ptitle) >= sizeof(menu.titleid))
							{
								parse_error(line, "Missing or invalid menu title message id");
								bError = true;
								continue;
							}
							strcpy(menu.titleid, ptitle);
						}
					}

					// insert end marker for previous menu
					item.flags = MenuDef_Flags_End;
					Array_Insert(This->mitems, -1, &item);

					const MItemDef* pItem = Array_Get(This->mitems, 0);
					menu.pitems = pItem + Array_Count(This->mitems);
					Array_Insert(This->menus, -1, &menu);
					item.flags = 0;
				}
				else if (*ptoken == '-')
				{
					// seperator, nothing should follow
					if (ptoken[1])
					{
						parse_error(line, "Unexpected '%s'", ptoken + 1);
						bError = true;
						continue;
					}

					item.flags |= MenuDef_Flags_Seperator;
				}
				else
				{
					// new menu item
					const char* psub = NULL;
					item.pcmd = NULL;
					item.psubcmd = NULL;
					item.psubmenu = NULL;

					if (*ptoken != '>')
					{
						item.pcmd = CmdList_FindCmdByName(This, ptoken);

						// base command
						if (item.pcmd == NULL)
						{
							parse_error(line, "Unkown menu item command '%s'", ptoken);
							bError = true;
							continue;
						}
						pnext = get_token(pnext, &ptoken);
					}

					// optional submenu
					if (ptoken)
					{
						if (*ptoken != '>')
						{
							parse_error(line, "Expected '>' instead of '%s'", ptoken);
							bError = true;
							continue;
						}

						// sub command
						pnext = get_token(pnext, &psub);
						if (!psub)
						{
							parse_error(line, "Missing submenu item command");
							bError = true;
							continue;
						}

						if (*psub == '=')
						{
							item.psubmenu = CmdList_FindMenu(This, psub + 1);
							if (item.psubmenu == NULL)
							{
								parse_error(line, "Unkown submenu name '%s'", psub + 1);
								bError = true;
								continue;
							}
						}
						else
						{
							item.psubcmd = CmdList_FindCmdByName(This, psub);
							if (item.psubcmd == NULL)
							{
								parse_error(line, "Unkown submenu item command '%s'", psub);
								bError = true;
								continue;
							}
						}
					}

					Array_Insert(This->mitems, -1, &item);
					item.flags = 0;
				}
			}

			pnext = get_token(pnext, &ptoken);
			if (ptoken)
			{
				parse_error(line, "Unexpected '%s'", ptoken);
				bError = true;
				continue;
			}
		}

		Throwback_Stop();

		// insert end marker for previous menu
		item.flags = MenuDef_Flags_End;
		Array_Insert(This->mitems, -1, &item);

		if (bError) throw_string("ErrParse:%s", pfilename);
	}
	catch
	{
		if (file != NULL) fclose(file);
		if (pfilename != NULL) mem_free(pfilename);
		throw_current();
	}
	catch_end

	fclose(file);
	mem_free(pfilename);
}

//------------------------------------------------------------------------------

/**
 * Finds a toolbar by its name.
 *
 * @param  name  Name of toolbar to find.
 *
 * @returns  Toolbar descriptor.
 */
static const Toolbar* CmdList_FindToolbar(const CmdList* This, const char* name)
{
	for (int i = Array_Count(This->toolbars) - 1; i >= 0; i--)
	{
		const Toolbar* pdef = Array_Get(This->toolbars, i);

		if (!strcmp(pdef->name, name))
			return pdef;
	}

	return NULL;
}

/**
 * Parses the toobar file "<App$Dir>.Toolbars".
 */
static void throw_CmdList_ParseToolbars(CmdList* This)
{
	const char* volatile pfilename = NULL;
	FILE* volatile file = NULL;
	char string[256];
	ButtonDef button = {0};
	Toolbar bar;
	const char* ptoken;
	char* pnext;
	int line = 0;
	bool bError = false;

	try
	{
		pfilename = throw_mem_allocprint("%s.Toolbars", Task_GetDir());
		file = fopen(pfilename, "rb");

		if (file == NULL)
			throw_string("ErrDocFRead: %s", pfilename);

		Throwback_Start(pfilename);

		while (true)
		{
			line++;

			if (fgets(string, 255, file) == NULL)
				break;

			// ignore comments
			pnext = strchr(string, '#');
			if (pnext) *pnext = 0;

			pnext = get_token(string, &ptoken);
			if (ptoken)
			{
				if (*ptoken == '=')
				{
					// new toolbar
					ptoken++;

					// toolbar name
					if (!*ptoken || (strlen(ptoken) >= sizeof(bar.name)))
					{
						parse_error(line, "Missing or invalid toolbar name");
						bError = true;
						continue;
					}
					strcpy(bar.name, ptoken);

					// insert end marker for previous menu
					button.flags = ToolbarDef_Flags_End;
					Array_Insert(This->buttons, -1, &button);

					const ButtonDef* pButton = Array_Get(This->buttons, 0);
					bar.buttons = pButton + Array_Count(This->buttons);
					Array_Insert(This->toolbars, -1, &bar);
					button.flags = 0;
				}
				else if (*ptoken == '-')
				{
					// seperator, nothing should follow
					if (ptoken[1])
					{
						parse_error(line, "Unexpected '%s'", ptoken + 1);
						bError = true;
						continue;
					}

					button.flags |= ToolbarDef_Flags_Seperator;
				}
				else
				{
					// new button
					button.pselect = NULL;
					button.padjust = NULL;

					// sprite to use
					if (!ptoken || (strlen(ptoken) >= sizeof(button.sprite)))
					{
						parse_error(line, "Invalid sprite name '%s'", ptoken);
						bError = true;
						continue;
					}
					strcpy(&button.sprite[0], ptoken);

					// base command
					pnext = get_token(pnext, &ptoken);
					button.pselect = CmdList_FindCmdByName(This, ptoken);

					if (button.pselect == NULL)
					{
						parse_error(line, "Unkown command '%s'", ptoken);
						bError = true;
						continue;
					}

					// optional data
					pnext = get_token(pnext, &ptoken);

					// optional submenu
					if (ptoken)
					{
						button.padjust = CmdList_FindCmdByName(This, ptoken);

						if (button.padjust == NULL)
						{
							parse_error(line, "Unkown command '%s'", ptoken);
							bError = true;
							continue;
						}
					}

					Array_Insert(This->buttons, -1, &button);
					button.flags = 0;
				}
			}

			pnext = get_token(pnext, &ptoken);
			if (ptoken)
			{
				parse_error(line, "Unexpected '%s'", ptoken);
				bError = true;
				continue;
			}
		}

		Throwback_Stop();

		// insert end marker for previous menu
		button.flags = ToolbarDef_Flags_End;
		Array_Insert(This->buttons, -1, &button);

		if (bError) throw_string("ErrParse:%s", pfilename);
	}
	catch
	{
		if (file != NULL) fclose(file);
		if (pfilename != NULL) mem_free(pfilename);
		throw_current();
	}
	catch_end

	fclose(file);
	mem_free(pfilename);
}

static void throw_CmdList_BuildMenus(const CmdList* This)
{
	for (int i = Array_Count(This->menus) - 1; i >= 0; i--)
	{
		MenuDef* menu = (MenuDef*) Array_Get(This->menus, i);
		const char* ptitle;
		const MItemDef* pdef;
		int maxsize = 0;

		if (menu->titleid[0])
			ptitle = Msg_Lookup(menu->titleid);
		else
			ptitle = Task_GetName();

		if (menu->itemsid[0])
		{
			menu->menu = throw_New_Menu(ptitle, Msg_Lookup(menu->itemsid));
			continue;
		}

		menu->menu = throw_New_Menu_Empty(ptitle);

		// determine max width of menu (cf OS < 3.5)
		for (pdef = menu->pitems; !(pdef->flags & MenuDef_Flags_End); pdef++)
		{
			int size = 0;
			const CmdDef* pcmd = pdef->pcmd;

			if (!pcmd) pcmd = pdef->psubcmd;

			if (pcmd)
				size = strlen(Msg_Lookup(pcmd->msgid));
			else
				size = strlen(Msg_Lookup(pdef->psubmenu->titleid));

			if (menu->pkmap && pcmd)
			{
				const char* pkeytext = KeyToString(Keymap_FindKey(menu->pkmap, pcmd));
				if (pkeytext) size += strlen(pkeytext) + 1;
			}

			if (maxsize < size) maxsize = size;
		}

		// build menu items
		for (pdef = menu->pitems; !(pdef->flags & MenuDef_Flags_End); pdef++)
		{
			const CmdDef* pcmd = pdef->pcmd;
			char* ptitle;
			int index;

			if (!pcmd) pcmd = pdef->psubcmd;

			if (pcmd)
				ptitle = SPrintf("%s", Msg_Lookup(pcmd->msgid));
			else
				ptitle = SPrintf("%s", Msg_Lookup(pdef->psubmenu->titleid));

			if (menu->pkmap && pcmd)
			{
				const char* pkeytext = KeyToString(Keymap_FindKey(menu->pkmap, pcmd));

				if (pkeytext)
				{
					char* p = ptitle + strlen(ptitle);
					char* pend = ptitle + maxsize - strlen(pkeytext);

					for (;p < pend;)
						*p++ = ' ';

					strcpy(p, pkeytext);
				}
			}

			index = throw_Menu_InsertItem(menu->menu, ptitle, -1);

			if (pdef->flags & MenuDef_Flags_Seperator)
				Menu_ItemSetProperty(menu->menu, index - 1, EMenu_Item_Separator, true);
		}
	}
}

/**
 * Parses application command files.
 */
void throw_CmdList_Parse(CmdList* This)
{
	throw_CmdList_ParseCommands(This);
	throw_CmdList_ParseKeymaps(This);
	throw_CmdList_ParseMenus(This);
	throw_CmdList_ParseToolbars(This);
	throw_CmdList_BuildMenus(This);
}

typedef struct
{
	const MenuDef*    mdef;
	void*             handle;
	const CmdHandler* handler;
} MenuStackItem;

static MenuStackItem MenuStack[10];

const CmdContext* CmdHandler_GetContext(const CmdHandler* handler)
{
	IGNORE(handler);

	return &ctx;
}

void CmdHandler_ExecCommand(const CmdHandler* handler, void* handle, CmdID id)
{
	Event e = {handle, EEvent_Null, NULL};

	ctx.e     = &e;
	ctx.menu = NULL;
	ctx.w    = HWind_None;
	ctx.i    = HIcon_None;

	try
	{
		if (handler->FCheckCommand(handle, id) & ECmdState_Allow)
			handler->FExecCommand(handle, id);
	}
	catch
	{
		App_ReportException();
	}
	catch_end
}

bool CmdHandler_KeyPressed(const CmdHandler* handler, void* handle, const char* kmapname, int key)
{
	const Keymap* kmap = CmdList_FindKeymap(CmdList_Get(), kmapname);

	if (kmap == NULL)
		throw_runtime("Unknown keymap %s", kmapname);

	const CmdDef* pcmd = Keymap_FindCmd(kmap, key);

	if (pcmd)
	{
		CmdHandler_ExecCommand(handler, handle, pcmd->id);
		return true;
	}

	return false;
}

static EListenerAction CmdHandler_MenuHandler(void* handle, const int* hit, const Event* e);

static void CmdHandler_OpenMenuDef(const CmdHandler* handler, void* handle, const MenuDef* mdef)
{
	const int32_t* hit = Menu_GetContext();
	int count = 0;

	while (hit[count] >= 0)
		count++;

	MenuStackItem* pitem = &MenuStack[count];

	pitem->mdef = mdef;
	pitem->handle = handle;
	pitem->handler = handler;

	pitem++;
	pitem->mdef = NULL;
	pitem->handle = NULL;
	pitem->handler = NULL;

	if (count == 0)
		Menu_Open(mdef->menu, CmdHandler_MenuHandler, MenuStack);
	else
		Menu_OpenSubMenu(mdef->menu);
}

void CmdHandler_OpenMenu(const CmdHandler* handler, void* handle, const char* name)
{
	const MenuDef* mdef = CmdList_FindMenu(CmdList_Get(), name);

	if (mdef)
		CmdHandler_OpenMenuDef(handler, handle, CmdList_FindMenu(CmdList_Get(), name));
	else
		throw_runtime("Unknown menu %s", name);
}

static EListenerAction CmdHandler_MenuHandler(void* handle, const int* hit, const Event* e)
{
	const MenuStackItem* pstack = handle;
	int count = 0;
	int i;

	if (e->Type == EEvent_MenuClose)
	{
		// we must empty the stack
		MenuStackItem* pstack = handle;

		pstack->mdef = NULL;
		pstack->handle = NULL;
		pstack->handler = NULL;

		return EListenerAction_ContinueEvent;
	}

	while ((hit[count] >= 0) && (pstack->handler != NULL))
	{
		pstack++;
		count++;
	}

	if (e->Type != EEvent_MenuOpen)
	{
		pstack--;
		count--;
	}

	if ((count < 0) || (pstack->handler == NULL))
		return EListenerAction_StopEvent;

	ctx.e = e;
	ctx.w = HWind_None;
	ctx.i = HIcon_None;
	ctx.menu = pstack->mdef->menu;
	ctx.item = hit[count];
	if (ctx.item != -1)
	{
		for (i = 0; hit[i + count + 1] != -1; i++)
			ctx.hit[i] = hit[i + count + 1];
	}
	else i = 0;
	ctx.hit[i] = -1;

	switch(e->Type)
	{
		case EEvent_Menu:
		{
			const MItemDef* pdef = pstack->mdef->pitems + hit[count];

			if (hit[count+1] == -1)
			{
				if (pdef->pcmd)
					pstack->handler->FExecCommand(pstack->handle, pdef->pcmd->id);
			}
			else
			{
				// actual action is on submenu not handled here
				if (pdef->psubcmd)
					pstack->handler->FExecCommand(pstack->handle, pdef->psubcmd->id);
			}

			return EListenerAction_StopEvent;
		}
		break;
		case EEvent_MenuOpen:
		{
			const MItemDef* pdef = pstack->mdef->pitems;
			int state;

			if (hit[count] == -1)
			{
				for (int i = 0; !(pdef->flags & MenuDef_Flags_End); i++, pdef++)
				{
					const CmdDef* pcmd = pdef->pcmd;
					if (!pcmd) pcmd = pdef->psubcmd;

					ctx.item = i;

					if (pcmd)
					{
						state = pstack->handler->FCheckCommand(pstack->handle, pcmd->id);
						Menu_ItemFade(pstack->mdef->menu, i, (state & ECmdState_Allow) == 0);
						Menu_ItemTick(pstack->mdef->menu, i, (state & ECmdState_Tick) != 0);
					}
					else
					{
						Menu_ItemFade(pstack->mdef->menu, i, false);
						Menu_ItemTick(pstack->mdef->menu, i, false);
					}

					if (pdef->psubcmd)
						state = pstack->handler->FCheckCommand(pstack->handle, pdef->psubcmd->id);
					else if (pdef->psubmenu)
						state = ECmdState_Allow;
					else
						state = 0;

					Menu_ItemSetProperty
						( pstack->mdef->menu
						, i
						, EMenu_Item_SubMenu
						, (state & ECmdState_Allow) != 0
						);
				}
			}
			else
			{
				pdef += hit[count];
				// actual action is on submenu not handled here
				if (pdef->psubcmd)
					pstack->handler->FExecCommand(pstack->handle, pdef->psubcmd->id);
			}

			return EListenerAction_StopEvent;
		}
		break;
		case EEvent_Message:
		case EEvent_MessageWantAck:
		{
			const MItemDef* pdef = pstack->mdef->pitems + hit[count];

			switch(((Msg*) e->pData)->hdr.action)
			{
				case EMsg_MenuWarning:
				{
					if (pdef->psubcmd)
						pstack->handler->FExecCommand(pstack->handle, pdef->psubcmd->id);
					else if (pdef->psubmenu)
						CmdHandler_OpenMenuDef(pstack->handler, pstack->handle, pdef->psubmenu);

					return EListenerAction_StopEvent;
				}
				break;
				case EMsg_HelpRequest:
				{
					if (hit[count+1] == -1)
					{
						const Msg_HelpRequest* pmsg = e->pData;
						char msg1[10];
						char msg2[17];

						if (pdef->pcmd)
						{
							strcpy(msg1, pdef->pcmd->msgid);
							strcat(msg1, "H");
						}

						if (pdef->psubcmd)
						{
							strcpy(msg2, pdef->psubcmd->msgid);
							strcat(msg2, "H");
						}
						else if (pdef->psubmenu)
						{
							strcpy(msg2, pdef->psubmenu->titleid);
							strcat(msg2, "H");
						}

						Task_HelpReply_FromMenu
							( pmsg
							, pdef->pcmd ? msg1 : NULL
							, (pdef->psubcmd || pdef->psubmenu) ? msg2 : NULL
							);
					}
					else
					{
						// request help on submenus
						pstack->handler->FExecCommand(pstack->handle, pdef->psubcmd->id);
					}

					return EListenerAction_StopEvent;
				}
				break;
			}
		}
		break;
	}

	return EListenerAction_ContinueEvent;
}

static EListenerAction CmdHandler_NullHandler(void* handle, const Event* e)
{
	const Toolbar* bar = handle;
	const ButtonDef* pdef;
	int i;

	ctx.e = e;
	ctx.w = bar->w;
	ctx.i = HIcon_None;
	ctx.menu = NULL;

	for (pdef = bar->buttons, i = 0; !(pdef->flags & ToolbarDef_Flags_End); pdef++, i++)
	{
		int state;

		ctx.i = i;

		state = bar->handler->FCheckCommand(bar->handle, pdef->pselect->id);

		if (pdef->padjust)
			state |= bar->handler->FCheckCommand(bar->handle, pdef->padjust->id);

		Icon_SetDimmed(bar->w, i, !(state & ECmdState_Allow));
	}

	return EListenerAction_ContinueEvent;
}

static EListenerAction CmdHandler_EventHandler(void* handle, const Event* e)
{
	const Toolbar* bar = handle;

	switch(e->Type)
	{
		case EEvent_WindowOpen:
		{
			CWindOpen* po = (CWindOpen*) e->pData;
			CWindOpen o = Window_GetOwnerInfo(po->w);
			CRect box = RectToWindow(&o.cvt.box, &o.cvt);

			box.y0 = box.y1 - (po->cvt.box.y1 - po->cvt.box.y0);
			box.x1 = box.x0 - Task_GetModeInfo()->dx;
			box.x0 = box.x1 - (po->cvt.box.x1 - po->cvt.box.x0);

			Window_OpenPane(po, &o, &box, 0);

			return EListenerAction_ContinueEvent;
		}
		break;
		case EEvent_Mouse:
		{
			const Mouse* m = e->pData;
			const ButtonDef* pdef = bar->buttons + m->i;
			const CmdDef* pcmd = NULL;

			if ((m->but & EBut_Menu)
			||  (m->i == HIcon_None))
				return EListenerAction_ContinueEvent;

			if (m->but == EBut_Adjust)
				pcmd = pdef->padjust;

			if (!pcmd) pcmd = pdef->pselect;

			if (pcmd)
			{
				ctx.e = e;
				ctx.w = m->w;
				ctx.i = m->i;
				ctx.menu = NULL;

				CmdHandler_ExecCommand(bar->handler, bar->handle, pcmd->id);
			}

			return EListenerAction_StopEvent;
		}
		break;
		case EEvent_Message:
		case EEvent_MessageWantAck:
		{
			const Msg_HelpRequest* pmsg = e->pData;

			switch(pmsg->hdr.action)
			{
				case EMsg_HelpRequest:
				{
					if (pmsg->m.i >= 0)
					{
						const ButtonDef* pdef = bar->buttons + pmsg->m.i;
						char* str = SPrintf("");
						char msg1[10];

						if (pdef->pselect)
						{
							strcpy(msg1, pdef->pselect->msgid);
							strcat(msg1, "H");
							sprintf(str, "%s %s.", Msg_Lookup("HlpSelect"), Msg_Lookup(msg1));
							if (pdef->padjust)
								strcat(str, "|M");
						}

						if (pdef->padjust)
						{
							strcpy(msg1, pdef->padjust->msgid);
							strcat(msg1, "H");
							sprintf(str + strlen(str), "%s %s.", Msg_Lookup("HlpAdjust"), Msg_Lookup(msg1));
						}

						Task_HelpReply(pmsg, str);

						return EListenerAction_StopEvent;
					}
				}
				break;
			}
		}
		break;
	}

	return EListenerAction_ContinueEvent;
}

EListenerAction Cmd_Help(const Msg_HelpRequest* pmsg, CmdID sel, CmdID adj)
{
	const CmdDef* pselect = CmdList_FindCmdById(CmdList_Get(), sel);
	const CmdDef* padjust = CmdList_FindCmdById(CmdList_Get(), adj);
	char* str = SPrintf("");
	char msg1[10];

	if (!pselect && !padjust)
		return EListenerAction_ContinueEvent;

	if (pselect)
	{
		strcpy(msg1, pselect->msgid);
		strcat(msg1, "H");
		sprintf(str, "%s %s.", Msg_Lookup("HlpSelect"), Msg_Lookup(msg1));
		if (padjust)
			strcat(str, "|M");
	}

	if (padjust)
	{
		strcpy(msg1, padjust->msgid);
		strcat(msg1, "H");
		sprintf(str + strlen(str), "%s %s.", Msg_Lookup("HlpAdjust"), Msg_Lookup(msg1));
	}

	Task_HelpReply(pmsg, str);

	return EListenerAction_StopEvent;
}

Toolbar* throw_New_Toolbar(const CmdHandler* handler, void* handle, const char* name)
{
	const Toolbar* ref = CmdList_FindToolbar(CmdList_Get(), name);
	CTemplate* volatile t = NULL;
	volatile struct
	{
		HWind w;
		CIcon i;
	} icon;

	if (ref == NULL)
		throw_runtime("Unknown toolbar %s", name);

	Toolbar* bar = throw_mem_alloc(sizeof(*bar));
	bar->buttons = ref->buttons;
	bar->w = HWind_None;
	bar->count = 0;
	bar->handler = handler;
	bar->handle = handle;

	memset((void*) &icon, 0, sizeof(icon));

	try
	{
		const ButtonDef* pdef;

		t = throw_Templates_Blank(0, EWind_Pane | EWind_Moveable | EWind_NoRedraw, 0, 0);
		CWind* pinfo = Template_GetWindow(t);
		pinfo->o.o.cvt.box.x0 = 0;
		pinfo->o.o.cvt.box.x1 = 4096;
		pinfo->o.o.cvt.box.y0 = -4096;
		pinfo->o.o.cvt.box.y1 = 0;
		pinfo->work_bg = 1;
		pinfo->extra_flags = EWindX_No3DBorder;

		bar->w = throw_Window_Create(t, NULL);

		icon.w = bar->w;
		icon.i.flags  = 0x1700311B;
		icon.i.box.x0 = TOOLBAR_BORDER;
		icon.i.box.y1 = -TOOLBAR_BORDER;

		for ( pdef = bar->buttons, bar->count = 0
		    ; !(pdef->flags & ToolbarDef_Flags_End)
		    ; pdef++, bar->count++)
		{
			CSpriteHdr* pSprite = Sprites_SelectSprite(Task_GetSpriteArea(), pdef->sprite);
			CSize size = Desktop_GetSpriteSize(pSprite);

			if (pdef->flags & ToolbarDef_Flags_Seperator)
				icon.i.box.y1 -= 2*TOOLBAR_BORDER;

			icon.i.box.x1 = icon.i.box.x0 + size.cx;
			icon.i.box.y0 = icon.i.box.y1 - size.cy;

			icon.i.data.it.buffer = (char*) &nil;
			icon.i.data.it.buf_size = 1;
			icon.i.data.it.validation = throw_mem_allocprint("R5;S%s,%sp;", pdef->sprite, pdef->sprite);

			// Wimp create icon
			throw_os(_swix(0x0400c2, _INR(0,1), 0, &icon));

			icon.i.box.y1 = icon.i.box.y0;
		}

		icon.i.box.x0 = 0;
		icon.i.box.x1 += TOOLBAR_BORDER;
		icon.i.box.y1 = 0;
		icon.i.box.y0 -= TOOLBAR_BORDER;
		Window_SetExtent(bar->w, (const CRect*) &icon.i.box, true);

		throw_Window_RegisterEventHandler(bar->w, CmdHandler_EventHandler, bar, false);
		throw_Task_AddListener(EEvent_Null, CmdHandler_NullHandler, bar, false);
	}
	catch
	{
		Delete_Toolbar(bar);
		throw_current();
	}
	catch_end

	return bar;
}

void Delete_Toolbar(Toolbar* bar)
{
	if (bar)
	{
		Task_RemoveListener(EEvent_Null, CmdHandler_NullHandler, bar);
		if (bar->w != HWind_None)
		{
			CIcon info;

			for (int i = 0; i < bar->count; i++)
			{
				if (Icon_GetInfo(bar->w, i, &info))
				{
					mem_free(info.data.it.validation);
				}
			}

			Window_DeRegisterEventHandler(bar->w, CmdHandler_EventHandler, bar);
			Window_Delete(bar->w);
		}
		mem_free(bar);
	}
}

HWind Toolbar_GetWindow(const Toolbar* bar)
{
	return bar->w;
}
