/*
  Name:             imsample
  Copyright:        Copyright (C) 2003-2017 SIL International.
  Documentation:
  Description:
  Create Date:      24 Oct 2012

  Modified Date:    24 Oct 2012
  Authors:          mcdurdin
  Related Files:
  Dependencies:

  Bugs:
  Todo:
  Notes:
  History:          24 Oct 2012 - mcdurdin - I3481 - V9.0 - Eliminate unsafe calls in C++
*/

#define STRICT

#include <windows.h>
#include <stdio.h>
#include "imlib.h"

HWND hwnd;
HWND hwndChild;
HINSTANCE hinst;
UINT wm_keymanim_close = 0, wm_keymanim_contextchanged;

BOOL FIMWindowUse = FALSE;
BOOL FIMWindowAlwaysVisible = FALSE;
BOOL FIMWindowActive = FALSE;

struct rule
{
	int contextlen;
	WCHAR *context;
	WCHAR key;
	int outputlen;
	BOOL drawvalid;
	WCHAR *outputs[10]; // max ten outputs
};

struct group
{
	char name[32];
	int nrules;
	rule *rules;
};

struct keyboard
{
	char name[128];

	LOGFONT font;
	HFONT hFont;

	int maxoutputs;
	int ngroups;
	int gridx, gridy;
	group *groups;
};

rule *currule = NULL;
keyboard *keyboards = NULL, *curkbd = NULL;
int nkeyboards;

//BOOL FWindow, FWindowAlways, FInWindow = FALSE;

LRESULT CALLBACK IMSampleChildWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK IMSampleWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
extern "C" BOOL __declspec(dllexport) WINAPI KeymanIMReloadConfig(PSTR keyboardname);
void WriteRegSetting(PSTR text, int value);
int  ReadRegSetting(PSTR text);
bool RegKeyExits(PSTR text);
void UnloadRules(PSTR KeyboardName);
BOOL LoadRules(PSTR KeyboardName);
void DeleteKeyboard(PSTR keyboardname);
void CreateKeyboard(PSTR keyboardname);
void CreateRegistryKeys();

/************************************************************************************************************
 * DLL entry functions
 ************************************************************************************************************/

BOOL WINAPI DllEntryPoint(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
	return TRUE;
}

extern "C" BOOL __declspec(dllexport) WINAPI KeymanIMDestroy(PSTR keyboardname)
{
	UnregisterClass("KM_IMSample", hinst);
	DeleteKeyboard(keyboardname);
	DestroyWindow(hwnd);
	UnloadRules(keyboardname);
	currule = NULL;
	curkbd = NULL;
	FIMWindowActive = FALSE;
	return TRUE;
}

extern "C" BOOL __declspec(dllexport) WINAPI KeymanIMInit(PSTR keyboardname)
{
	WNDCLASS wc;

	wm_keymanim_close = RegisterWindowMessage("WM_KEYMANIM_CLOSE");
	wm_keymanim_contextchanged = RegisterWindowMessage("WM_KEYMANIM_CONTEXTCHANGED");

	hinst = GetModuleHandle(NULL);

	if(!GetClassInfo(hinst, "KM_IMSample", &wc))
	{
		wc.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS;// | CS_SAVEBITS;
		wc.lpfnWndProc = (WNDPROC) IMSampleWndProc;
		wc.cbClsExtra = 0;
		wc.cbWndExtra = 0;
		wc.hInstance = hinst;
		wc.hIcon = NULL;
		wc.hCursor = LoadCursor(NULL, IDC_ARROW);
		wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
		wc.lpszMenuName = NULL;
		wc.lpszClassName = "KM_IMSample";
		if(!RegisterClass(&wc)) return FALSE;

		wc.lpfnWndProc = (WNDPROC) IMSampleChildWndProc;
		wc.lpszClassName = "KM_IMSampleChild";
		if(!RegisterClass(&wc)) return FALSE;
	}

	POINT pt;
	pt.x = ReadRegSetting("imsample x");
	pt.y = ReadRegSetting("imsample y");

	hwnd = CreateWindowEx(WS_EX_TOOLWINDOW | WS_EX_TOPMOST, "KM_IMSample", "Keyman Sample IM",
		WS_POPUP | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME,
		pt.x, pt.y, 200, 120, 0, NULL, hinst, NULL);

	if(!hwnd) return FALSE;

	return KeymanIMReloadConfig(keyboardname);
}

extern "C" BOOL __declspec(dllexport) WINAPI KeymanIMReloadConfig(PSTR keyboardname)
{
	currule = NULL;
	curkbd = NULL;
	FIMWindowActive = FALSE;

  CreateRegistryKeys();
	FIMWindowUse = ReadRegSetting("ShowIMWindow");
	FIMWindowAlwaysVisible = ReadRegSetting("ShowIMWindowAlways");

	if(!PrepIM()) return FALSE;

  CreateKeyboard(keyboardname);
	LoadRules(keyboardname);

	if(!FIMWindowUse || !FIMWindowAlwaysVisible) (*KMHideIM)();

	return TRUE;
}

/************************************************************************************************************
 * IM Events
 ************************************************************************************************************/

extern "C" BOOL __declspec(dllexport) WINAPI KeymanIMActivate(PSTR keyboardname)
{
	currule = NULL;
	curkbd = NULL;
	FIMWindowActive = FALSE;
	if(!PrepIM()) return FALSE;
	if(FIMWindowUse && FIMWindowAlwaysVisible) (*KMDisplayIM)(hwnd, TRUE);

	int kbi;

	char kbdname[260];

	if(!(*KMGetActiveKeyboard)(kbdname, 256)) return FALSE;

	for(kbi = 0; kbi < nkeyboards; kbi++)
		if(!_stricmp(keyboards[kbi].name, kbdname)) break;
	if(kbi == nkeyboards) return FALSE;
	curkbd = &keyboards[kbi];

	return TRUE;
}

extern "C" BOOL __declspec(dllexport) WINAPI KeymanIMDeactivate(PSTR keyboardname)
{
	currule = NULL;
	curkbd = NULL;
	FIMWindowActive = FALSE;
	if(!PrepIM()) return FALSE;
	(*KMHideIM)();
	return TRUE;
}

/************************************************************************************************************
 * Window management functions
 ************************************************************************************************************/

void FinishEdit()
{
	FIMWindowActive = FALSE;
	currule = NULL;
	PostMessage(hwndChild, WM_USER+200, 0, 0);
	PostMessage(hwnd, wm_keymanim_close, 1, !FIMWindowAlwaysVisible);
	if(FIMWindowAlwaysVisible) (*KMDisplayIM)(hwnd, TRUE);
}


LRESULT CALLBACK IMSampleWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	RECT rect;
	POINTS pt;
	LRESULT lResult;
	PAINTSTRUCT ps;
	HDC hDC;

	// Handle standard IM functions -- such as window activation, etc.
	if(IMDefWindowProc(hwnd, msg, wParam, lParam, &lResult)) return lResult;

	if(msg == wm_keymanim_contextchanged)
		PostMessage(hwndChild, WM_USER+200, 0, 0);

	switch(msg)
	{
	case WM_CREATE:
		pt.x = ReadRegSetting("imsample x");
		pt.y = ReadRegSetting("imsample y");

		if(pt.x > 0 && pt.y > 0)
			SetWindowPos(hwnd, 0, pt.x, pt.y, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER);

		GetClientRect(hwnd, &rect);
		hwndChild = CreateWindowEx(WS_EX_CLIENTEDGE,
			"KM_IMSampleChild", "", WS_BORDER | WS_CHILD | /*WS_HSCROLL |*/ WS_VSCROLL | WS_VISIBLE,
			2, 2, rect.right - 2, rect.bottom - 2, hwnd, 0, hinst, NULL);
		PostMessage(hwndChild, WM_USER+200, 0, 0);

		return 0;

	case WM_SHOWWINDOW:
		if(wParam) PostMessage(hwndChild, WM_USER+200, 0, 0);
		break;

	case WM_DESTROY:
		GetWindowRect(hwnd, &rect);
		WriteRegSetting("imsample x", rect.left);
		WriteRegSetting("imsample y", rect.top);

		return 0;

	case WM_KEYDOWN:
		if(!FIMWindowActive || !currule) return 0;
		if(wParam == VK_ESCAPE || wParam == VK_BACK)
			FinishEdit();
		return 0;

	case WM_CHAR:
		if(!FIMWindowActive || !currule) return 0;

		if(wParam >= '1' && wParam <= '9')
		{
			wParam -= '1';
			if(!currule->outputs[wParam]) return 0;
			(*KMSetOutput)(currule->outputs[wParam], currule->contextlen);
			FinishEdit();
		}
		return 0;

	case WM_SIZE:
		MoveWindow(hwndChild, 2, 2, LOWORD(lParam) - 2, HIWORD(lParam) - 2, TRUE);
		return 0;

	case WM_PAINT:
		hDC = BeginPaint(hwnd, &ps);
		GetClientRect(hwnd, &rect);
		FillRect(hDC, &rect, GetSysColorBrush(COLOR_BTNFACE));
		EndPaint(hwnd, &ps);

		return 0;
	}

	return DefWindowProc(hwnd, msg, wParam, lParam);
}

struct
{
	int cellcount, visiblecolcount, visiblerowcount, xi, yi;
} wz;

void CalcScrollSize()
{
	RECT rect;

	if(!curkbd) { wz.cellcount = 0; return; }
  if (!curkbd->groups) {
    wz.cellcount = 0;
    return;
  }
	GetClientRect(hwndChild, &rect);

	if(FIMWindowActive && currule)
		for(wz.cellcount = 0; wz.cellcount < 10; wz.cellcount++) { if(!currule->outputs[wz.cellcount]) break; }
	else
	{
		WCHAR buf[48];
		if(!(*KMGetContext)(buf, 48)) buf[0] = 0;
		int buflen = (int)wcslen(buf);

		wz.cellcount = 0;
                int i;

		for(i = 0; i < curkbd->groups[0].nrules; i++)
		{
			rule *r = &curkbd->groups[0].rules[i];
			r->drawvalid = r->contextlen == 0 ||
				(buflen >= r->contextlen && !wcscmp(buf+(buflen-r->contextlen), r->context));
			if(r->drawvalid) wz.cellcount++;
		}
	}

	wz.visiblecolcount = (curkbd->gridx != 0 ? rect.right / curkbd->gridx : rect.right);
  wz.visiblerowcount = (curkbd->gridy != 0 ? rect.bottom / curkbd->gridy : rect.bottom);

	wz.xi = (wz.visiblecolcount !=0 ) ? (wz.cellcount-1) % wz.visiblecolcount : 2;
	wz.yi = (wz.visiblecolcount !=0 ) ? (wz.cellcount-1) / wz.visiblecolcount : 2;

	SCROLLINFO si;
	si.cbSize = sizeof(SCROLLINFO);
	si.fMask = SIF_ALL | SIF_DISABLENOSCROLL;
	si.nMin = 0;
	si.nMax = max(0, wz.yi - wz.visiblerowcount + 1);
	si.nPage = wz.visiblerowcount-1;
	si.nPos = 0;
	SetScrollInfo(hwndChild, SB_VERT, &si, FALSE);
}


LRESULT CALLBACK IMSampleChildWndProc(HWND hwndChild, UINT msg, WPARAM wParam, LPARAM lParam)
{
	static int cx, cy;
	RECT rect;
	POINTS pt;
	PAINTSTRUCT ps;
	HDC hDC;
	int n, nPos;
	LRESULT lResult;

	switch(msg)
	{
	case WM_NCHITTEST:
		lResult = DefWindowProc(hwndChild, msg, wParam, lParam);
		if(lResult != HTCLIENT && lResult != HTVSCROLL && lResult != HTHSCROLL) return HTNOWHERE;

		return lResult;

	case WM_MOUSEACTIVATE:
		return MA_NOACTIVATE;

	case WM_SIZE:
	case WM_USER+200:
		CalcScrollSize();
		InvalidateRect(hwndChild, NULL, TRUE);

		return 0;

	case WM_LBUTTONUP:
		if(!PrepIM()) return DefWindowProc(hwndChild, msg, wParam, lParam);
		if(!curkbd) return 0;
    if (!curkbd->groups) {
      return 0;
    }
		GetClientRect(hwndChild, &rect);

		pt = MAKEPOINTS(lParam);

		pt.x /= cx;
		pt.y /= cy;

		n = pt.y*(rect.right/cx) + pt.x;

		if(!FIMWindowActive || !currule)
		{       int i, j;
			for(i = 0, j = 0; i < curkbd->groups[0].nrules; i++)
				if(curkbd->groups[0].rules[i].drawvalid)
				{
					if(j == n) break;
					j++;
				}

			if(i == curkbd->groups[0].nrules) return 0;

			currule = &curkbd->groups[0].rules[i];
			FIMWindowActive = TRUE;
			PostMessage(hwndChild, WM_USER+200, 0, 0);
			(*KMDisplayIM)(hwnd, FALSE);
		}
		else
		{
			if(n > 9) return 0;
			if(!currule->outputs[n]) return 0;
			(*KMSetOutput)(currule->outputs[n], currule->contextlen);

			FinishEdit();
		}
		return 0;

	case WM_VSCROLL:
		nPos = HIWORD(wParam);
		if(nPos == 0 && LOWORD(wParam) != SB_THUMBTRACK && LOWORD(wParam) != SB_THUMBPOSITION)
			nPos = GetScrollPos(hwndChild, SB_VERT);
		switch(LOWORD(wParam))
		{
			case SB_BOTTOM:			nPos = wz.yi - wz.visiblerowcount + 1; break;
			case SB_ENDSCROLL:		break;
			case SB_LINEDOWN:		nPos++; break;
			case SB_LINEUP:			nPos--; break;
			case SB_PAGEDOWN:		nPos += wz.visiblerowcount - 1; break;
			case SB_PAGEUP:			nPos -= wz.visiblerowcount - 1; break;
			case SB_THUMBPOSITION:	break;
			case SB_THUMBTRACK:		break;
			case SB_TOP:				nPos = 0; break;
		}
		if(nPos < 0) nPos = 0;
		if(nPos > wz.yi - wz.visiblerowcount + 1) nPos = wz.yi - wz.visiblerowcount + 1;

		SetScrollPos(hwndChild, SB_VERT, nPos, TRUE);
		InvalidateRect(hwndChild, NULL, TRUE);
		UpdateWindow(hwndChild);

		return 0;
	case WM_PAINT:
    if (!curkbd || !curkbd->hFont || !curkbd->groups || !PrepIM()) {
      return DefWindowProc(hwndChild, msg, wParam, lParam);
    }

		int vpos = GetScrollPos(hwndChild, SB_VERT) * curkbd->gridy;

		hDC = BeginPaint(hwndChild, &ps);
		GetClientRect(hwndChild, &rect);
		if(curkbd)
		{
			HFONT hOldFont = (HFONT) SelectObject(hDC, curkbd->hFont);

			cx = curkbd->gridx;
			cy = curkbd->gridy;
                        int i;
			for(i = 0; i < wz.visiblecolcount; i++)
			{
				MoveToEx(hDC, (i+1)*cx, -vpos + 0, NULL);
				if(i <= wz.xi || wz.yi > 0) LineTo(hDC, (i+1)*cx, -vpos + (i <= wz.xi ? cy * (wz.yi+1) : cy * wz.yi)+1);
			}
			for(i = 0; i <= wz.yi; i++)
			{
				MoveToEx(hDC, 0, -vpos + (i+1)*cy, NULL);
				LineTo(hDC, (i < wz.yi ? cx * wz.visiblecolcount : cx * (wz.xi+1))+1, -vpos + (i+1)*cy);
			}

			if(FIMWindowActive && currule)
			{
                                int i, x, y;
				for(i = 0, x = 0, y = 0; i < 10; i++)
				{
					if(!currule->outputs[i]) break;
					char buf[2]; buf[0] = i+'1'; buf[1] = 0;
					SelectObject(hDC, GetStockObject(SYSTEM_FONT));
					TextOut(hDC, x * cx + 1, -vpos + y * cy+1, buf, 1);
					SelectObject(hDC, curkbd->hFont);

					SIZE sz2;

					GetTextExtentPoint32W(hDC, currule->outputs[i], (int)wcslen(currule->outputs[i]), &sz2);

					TextOutW(hDC, x * cx + (cx-sz2.cx)/2, -vpos + (y+1) * cy - sz2.cy - 2,
						currule->outputs[i], (int)wcslen(currule->outputs[i]));

					if(++x >= wz.visiblecolcount) { x = 0; y++; }
				}
			}
			else
			{
				SelectObject(hDC, curkbd->hFont);
                                int i,x,y;
				for(i = 0, x = 0, y = 0; i < curkbd->groups[0].nrules; i++)
				{
					rule *r = &curkbd->groups[0].rules[i];
					if(!r->drawvalid) continue;

					SIZE sz2;

					GetTextExtentPoint32W(hDC, &r->key, 1, &sz2);

					TextOutW(hDC, x * cx + (cx-sz2.cx)/2, -vpos + (y+1) * cy - sz2.cy - 2, &r->key, 1);

					if(++x >= wz.visiblecolcount) { x = 0; y++; }
				}
			}
			SelectObject(hDC, hOldFont);
		}
		else
			Rectangle(hDC, -1, -1, rect.right + 1, rect.bottom + 1);

		/*char str[128];
		wsprintf(str, "%d %d %d %d", wz.xi, wz.yi, wz.visiblecolcount, wz.visiblerowcount);
		TextOut(hDC, 0, 0, str, strlen(str));*/

		EndPaint(hwndChild, &ps);
		return 0;
	}

	return DefWindowProc(hwndChild, msg, wParam, lParam);
}



BOOL FindRule(group *g, rule **rp, WCHAR KeyChar)
{
	if (!g || !rp) {
    return FALSE;
  }
  WCHAR buf[48];
	if(!(*KMGetContext)(buf, 48)) return FALSE;
	rule *r = g->rules;
	int buflen = (int)wcslen(buf);
  int i;
	for(i = 0; i < g->nrules; i++)
	{
		if(r[i].key == KeyChar && buflen >= r[i].contextlen && !wcscmp(buf+(buflen-r[i].contextlen), r[i].context))
		{
			*rp = &r[i];
			return TRUE;
		}
	}
	return FALSE;
}

BOOL PostKey(WCHAR KeyChar, WORD KeyStroke)
{
	if(KeyChar != 0)
	{
		WCHAR buf[2];
		buf[0] = KeyChar;
		buf[1] = 0;
		(*KMSetOutput)(buf, 0);
	}
	else
	{
		if(KeyStroke == VK_BACK)
			(*KMQueueAction)(QIT_BACK, 0);
		else
			(*KMQueueAction)(QIT_VKEYDOWN, KeyStroke);
	}
	return TRUE;
}

extern "C" BOOL __declspec(dllexport) WINAPI DFWindow(HWND hwndFocus, WORD KeyStroke, WCHAR KeyChar, DWORD shiftFlags)
{
	if(!curkbd) return FALSE;
  if (!curkbd->groups) {
    return FALSE;
  }

	if(!FindRule(&curkbd->groups[0], &currule, KeyChar))
	{
		if(FIMWindowAlwaysVisible)
			PostMessage(hwndChild, WM_USER+200, 0, 0);
		//UpdateWindow(hwndChild);
		return PostKey(KeyChar, KeyStroke);
	}

	FIMWindowActive = TRUE;
	PostMessage(hwndChild, WM_USER+200, 0, 0);
	(*KMDisplayIM)(hwnd, FALSE);
	return TRUE;
}

extern "C" BOOL __declspec(dllexport) WINAPI DFText(HWND hwndFocus, WORD KeyStroke, WCHAR KeyChar, DWORD shiftFlags)
{
	WCHAR buf[32];
	if (!curkbd || !curkbd->groups) {
    return FALSE;
  }
	if(!currule)
	{
		if(!FindRule(&curkbd->groups[0], &currule, KeyChar))
		{
			PostKey(KeyChar, KeyStroke);
			return TRUE;
		}
		else
		{
			(*KMSetOutput)(L"[", 0);
			for(int j = 0; currule->outputs[j]; j++)
			{
				if(j>0) (*KMSetOutput)(L" ", 0);
				buf[0] = j+'1'; buf[1] = 0;
				(*KMSetOutput)(buf, 0);
				(*KMSetOutput)(currule->outputs[j], 0);
			}
			(*KMSetOutput)(L"]", 0);
		}
	}
	else if(KeyChar >= '1' && KeyChar <= '9')
	{
		KeyChar -= '1';
		if(currule->outputs[KeyChar])
		{
			(*KMSetOutput)(currule->outputs[KeyChar], currule->outputlen);
			currule = NULL;
		}
	}
	else if(KeyStroke == VK_BACK || KeyStroke == VK_ESCAPE)
	{
		(*KMSetOutput)(L"", currule->outputlen);
		currule = NULL;
	}
	return TRUE;
}

extern "C" BOOL __declspec(dllexport) WINAPI DF(HWND hwndFocus, WORD KeyStroke, WCHAR KeyChar, DWORD shiftFlags)
{
	if(!PrepIM()) return FALSE;

	if(FIMWindowUse)
		return DFWindow(hwndFocus, KeyStroke, KeyChar, shiftFlags);
	else
		return DFText(hwndFocus, KeyStroke, KeyChar, shiftFlags);
}


/************************************************************************************************************
 * IM Configuration
 ************************************************************************************************************/

extern "C" BOOL __declspec(dllexport) WINAPI KeymanIMConfigure(PSTR keyboardname, HWND hwndParent)
{
	int n = MessageBox(hwndParent, "Do you want to have a menu window?",
		"IMSample", MB_YESNOCANCEL);

	switch(n)
	{
	case IDYES:
		n = MessageBox(hwndParent, "Do you want to have the menu window always open?",
			"IMSample", MB_YESNOCANCEL);

		switch(n)
		{
		case IDYES:
			WriteRegSetting("ShowIMWindowAlways", 1);
			break;

		case IDNO:
			WriteRegSetting("ShowIMWindowAlways", 0);
			break;

		default:
			return TRUE;
		}

		WriteRegSetting("ShowIMWindow", 1);
		break;

	case IDNO:
		WriteRegSetting("ShowIMWindow", 0);
		break;
	}

	return TRUE;
}

void WriteRegSetting(PSTR text, int value)
{
	HKEY hkey;
  if (RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\Keyman\\Keyman Engine\\Active Keyboards\\imsample",
		0, KEY_ALL_ACCESS, &hkey) == ERROR_SUCCESS)
	{
		RegSetValueEx(hkey, text, 0, REG_DWORD, (PBYTE) &value, 4);
		RegCloseKey(hkey);
	}
}

int ReadRegSetting(PSTR text)
{
	HKEY hkey;
	if(RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\Keyman\\Keyman Engine\\Active Keyboards\\imsample",
		0, KEY_ALL_ACCESS, &hkey) == ERROR_SUCCESS)
	{
		unsigned long value, sz = 4, tp = REG_DWORD;
		RegQueryValueEx(hkey, text, 0, &tp, (PBYTE) &value, &sz);
		RegCloseKey(hkey);
		return value;
	}
	return 0;
}

bool RegKeyExits(PSTR text)
{
	HKEY hkey;
	bool result = false;

  if(RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\Keyman\\Keyman Engine\\Active Keyboards\\imsample",
		0, KEY_ALL_ACCESS, &hkey) == ERROR_SUCCESS)
	{
		unsigned long value, sz = 4, tp = REG_DWORD;

    if (RegQueryValueEx(hkey, text, 0, &tp, (PBYTE) &value, &sz) == ERROR_SUCCESS)
    {
      result = true;
    }

    RegCloseKey(hkey);
	}
	return result;
}

char *strtokquoted(char *str, char *token, char *quotes)
{ //slow-n-ugly but quick to write ;-)
	static char *buf = NULL;
	if(!str)
		if(!buf) return NULL;
		else str = buf;

	char *q = str;
	if(!*q) return q;

	while(strchr(token, *q)) q++;

	if(strchr(quotes, *q))	//quoted substring
	{
		char *r = strchr(q+1, *q);
		if(!r) return NULL; // unterminated quotes
		r++;
		if(!strchr(token, *r)) return NULL; // close of quoted string does not culminate in token
		if(*r != 0)
		{
			*r++ = 0;
			while(*r != 0 && strchr(token, *r)) r++;
		}
		if(*r == 0) buf = NULL; else buf = r;
		return q;
	}
	else
	{
		char *r = q;
		while(!strchr(token, *r) && *r != 0) r++;
		if(*r != 0)
		{
			*r++ = 0;
			while(*r != 0 && strchr(token, *r)) r++;
		}
		if(*r == 0) buf = NULL; else buf = r;
		return q;
	}
}


PWSTR newempty()
{
	PWSTR buf = new WCHAR[1];
	buf[0] = 0;
	return buf;
}

PWSTR extstr(char *p)
{
	if(*p == '"' || *p == '\'')
	{
		p++;
		char *q = strchr(p, *(p-1));
		if(!q) return newempty();
		if(q == p) return newempty();
		*q = 0;
		int n = MultiByteToWideChar(CP_ACP, 0, p, (int)strlen(p), NULL, 0);
		PWCHAR buf = new WCHAR[n+1];
		MultiByteToWideChar(CP_ACP, 0, p, (int)strlen(p), buf, n);
		buf[n] = 0;
		return buf;
	}
	else
	{
		WCHAR buf[64];
		int i = 0, n;
		while(*p)
		{
			switch(*p)
			{
			case ' ':
				p++; break;
			case 'U':
			case 'u':
				p++;
				if(*p != '+') return newempty();

			case 'x':
			case 'X':
				p++;

				n = strtol(p, &p, 16);
				if(!p) return newempty();
				buf[i++] = n;
				break;

			case 'd':
			case 'D':
				p++;

				n = strtol(p, &p, 10);
				if(!p) return newempty();
				buf[i++] = n;
				break;

			default:
				return newempty();
			}
		}
		buf[i] = 0;
		PWSTR p = new WCHAR[i+1];
		wcscpy_s(p, i+1, buf);  // I3481
		return p;
	}
}

void UnloadRules(PSTR KeyboardName)
{
	int nkbi;
	for(nkbi = 0; nkbi < nkeyboards; nkbi++)
		if(!_stricmp(KeyboardName, keyboards[nkbi].name)) break;

	if(nkbi == nkeyboards) return;

	keyboard *kbi = &keyboards[nkbi];

	if(kbi->hFont) DeleteObject(kbi->hFont);
	kbi->hFont = NULL;

  if (kbi->groups == NULL) {
          return;
  }

  int i;
	for(i = 0; i < kbi->ngroups; i++)
	{
		for(int j = 0; j < kbi->groups[i].nrules; j++)
		{
			delete kbi->groups[i].rules[j].context;
			for(int k = 0; k < 10; k++) if(kbi->groups[i].rules[j].outputs[k]) delete kbi->groups[i].rules[j].outputs[k];
		}
		if(kbi->groups[i].nrules > 0) delete kbi->groups[i].rules;
	}
	if(kbi->ngroups > 0) delete kbi->groups;
	kbi->ngroups = 0;
	kbi->groups = NULL;
}

BOOL LoadRules(PSTR KeyboardName)
{
	BOOL FGroups = FALSE, FRules = FALSE, FConfig = FALSE;
	char str[512];
	int nkbi, lfpixelsy;

	UnloadRules(KeyboardName); // Free existing rule base

	for(nkbi = 0; nkbi < nkeyboards; nkbi++)
		if(!_stricmp(KeyboardName, keyboards[nkbi].name)) break;

	if(nkbi == nkeyboards) return FALSE;

	keyboard *kbi = &keyboards[nkbi];

	kbi->maxoutputs = 0;
	kbi->ngroups = 0;

	HDC hDC = GetDC(GetDesktopWindow());
	SetMapMode(hDC, MM_TEXT);
	lfpixelsy = GetDeviceCaps(hDC, LOGPIXELSY);
	ReleaseDC(GetDesktopWindow(), hDC);

	kbi->font.lfHeight         = -MulDiv(10, lfpixelsy, 72);
	kbi->font.lfWidth          = 0;
	kbi->font.lfEscapement     = 0;
	kbi->font.lfOrientation    = 0;
	kbi->font.lfWeight         = FW_NORMAL;
	kbi->font.lfItalic         = FALSE;
	kbi->font.lfUnderline      = FALSE;
	kbi->font.lfStrikeOut      = FALSE;
	kbi->font.lfCharSet        = DEFAULT_CHARSET;
	kbi->font.lfOutPrecision   = OUT_DEFAULT_PRECIS;
	kbi->font.lfClipPrecision  = CLIP_DEFAULT_PRECIS;
	kbi->font.lfQuality        = DEFAULT_QUALITY;
	kbi->font.lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE;
  strcpy_s(kbi->font.lfFaceName, _countof(kbi->font.lfFaceName), "Arial");  // I3481

	char drive[_MAX_DRIVE], dir[_MAX_DIR], buf[260];

	if(!(*KMGetKeyboardPath)(KeyboardName, buf, 260)) return FALSE;
	_splitpath_s(buf, drive, _MAX_DRIVE, dir, _MAX_DIR, NULL, 0, NULL, 0);  // I3481
	_makepath_s(buf, _countof(buf), drive, dir, "imsample", ".txt");  // I3481

  FILE *fp = NULL;
	if(fopen_s(&fp, buf, "rt") != 0) return FALSE;  // I3481
	if(!fp) return FALSE;

	while(fgets(str, 512, fp))
	{
		if(strchr(str, '\n')) *strchr(str, '\n') = 0;

		if(str[0] == '#' || str[0] == 0) continue;

		if(str[0] == '[')
		{
			FConfig = FALSE; FGroups = FALSE; FRules = FALSE;
			if(!_stricmp(str, "[config]"))      FConfig = TRUE;
			else if(!_stricmp(str, "[groups]")) FGroups = TRUE;
			else if(!_stricmp(str, "[rules]"))  FRules = TRUE;
		}
		else if(FConfig)
		{
      char *tokcontext = NULL;
			char *p = strtok_s(str, "= ", &tokcontext); if(!p) continue;  // I3481
			char *q = strtok_s(NULL, "\n", &tokcontext); if(!q) continue;  // I3481
			if(!_stricmp(p, "font"))
			{
				strncpy_s(kbi->font.lfFaceName, _countof(kbi->font.lfFaceName), q, LF_FACESIZE-1);
				kbi->font.lfFaceName[LF_FACESIZE-1] = 0;
			}
			else if(!_stricmp(p, "size"))
			{
				int n = atoi(q); if(n == 0) n = 10;
				kbi->font.lfHeight = -MulDiv(n, lfpixelsy, 72);
			}
			else if(!_stricmp(p, "gridx"))
				kbi->gridx = atoi(q);
			else if(!_stricmp(p, "gridy"))
				kbi->gridy = atoi(q);
		}
		else if(FGroups)
		{
			group *g2;
			g2 = new group[kbi->ngroups+1];
			if(kbi->ngroups > 0)
			{
				memcpy(g2, kbi->groups, sizeof(group)*kbi->ngroups);
				delete kbi->groups;
			}
			kbi->groups = g2;

			kbi->groups[kbi->ngroups].rules = NULL;
			kbi->groups[kbi->ngroups].nrules = 0;
			strncpy_s(kbi->groups[kbi->ngroups].name, _countof(kbi->groups[kbi->ngroups].name), str, 31);  // I3481
			kbi->groups[kbi->ngroups++].name[31] = 0;
		}
		else if(FRules)
		{
			int i;
			PSTR pgroup = strtokquoted(str, ", \t",    "\"\'"); if(!pgroup) continue; // silently fail on that line [for now]
			PSTR pcontext = strtokquoted(NULL, ", \t", "\"\'"); if(!pcontext) continue;
			PSTR pkey = strtokquoted(NULL, ", \t",     "\"\'"); if(!pkey) continue;
			PSTR poutput = strtokquoted(NULL, ", \t",  "\"\'"); if(!poutput) continue; // must be at least one output

			for(i = 0; i < kbi->ngroups; i++)
				if(!_stricmp(kbi->groups[i].name, pgroup)) break;

			if(i == kbi->ngroups) continue;

			if((kbi->groups[i].nrules % 10) == 0)
			{
				rule *r2 = new rule[kbi->groups[i].nrules+10];
				if(kbi->groups[i].nrules > 0)
				{
					memcpy(r2, kbi->groups[i].rules, sizeof(rule)*kbi->groups[i].nrules);
					delete kbi->groups[i].rules;
				}
				kbi->groups[i].rules = r2;
			}

			rule *r2 = &kbi->groups[i].rules[kbi->groups[i].nrules++];

			r2->context = extstr(pcontext);
			r2->contextlen = (int)wcslen(r2->context);
			PWSTR pp = extstr(pkey);
			r2->key = pp[0];
			delete pp;

			i = 0; r2->outputlen = 1;
			while(poutput && i < 10)
			{
				r2->outputs[i++] = extstr(poutput);
				r2->outputlen += (int)wcslen(r2->outputs[i-1]) + 2;
				poutput = strtokquoted(NULL, ", ", "\"\'");
			}
			if(i > kbi->maxoutputs) kbi->maxoutputs = i;
			for(; i < 10; i++) r2->outputs[i] = NULL;
		}
		// fail silently on unrecognised lines
	}

	fclose(fp);
	kbi->hFont = CreateFontIndirect(&kbi->font);

	return TRUE;
}

void DeleteKeyboard(PSTR keyboardname)
{
	int nkbi;

	for(nkbi = 0; nkbi < nkeyboards; nkbi++)
		if(!_stricmp(keyboardname, keyboards[nkbi].name)) break;

	if(nkbi == nkeyboards) return;

	if(nkbi < nkeyboards)
		memmove(&keyboards[nkbi], &keyboards[nkbi+1], sizeof(keyboard)*(nkeyboards-nkbi-1));
	nkeyboards--;
	if(nkeyboards == 0)
	{
		delete keyboards;
		keyboards = NULL;
	}
}

void CreateKeyboard(PSTR keyboardname)
{
	int nkbi;

	for(nkbi = 0; nkbi < nkeyboards; nkbi++)
		if(!_stricmp(keyboardname, keyboards[nkbi].name)) break;

	if(nkbi == nkeyboards)
	{
		keyboard *kb2 = new keyboard[nkeyboards + 1];
		if(nkeyboards > 0)
		{
			memcpy(kb2, keyboards, sizeof(keyboard) * nkeyboards);
			delete keyboards;
		}
		keyboards = kb2;
		strncpy_s(keyboards[nkeyboards].name, _countof(keyboards[nkeyboards].name), keyboardname, 127);  // I3481
		keyboards[nkeyboards].name[127] = 0;
    keyboards[nkeyboards].groups    = NULL;
    keyboards[nkeyboards].hFont     = NULL;
		nkeyboards++;
	}
}

// Write default values if keys are not found
void CreateRegistryKeys()
{
      if(!RegKeyExits("imsample x"))
      {
        WriteRegSetting("imsample x", 50);
      }
      if(!RegKeyExits("imsample y"))
      {
        WriteRegSetting("imsample y", 50);
      }
      if(!RegKeyExits("ShowIMWindow"))
      {
        WriteRegSetting("ShowIMWindow", 1);
      }
      if(!RegKeyExits("ShowIMWindowAlways"))
      {
        WriteRegSetting("ShowIMWindowAlways", 1);
      }
}
