/*	
 *	main.cpp
 *
 *	pcemon - P/ECE monitor
 *	Copyright (C) 2002-2018 Naoyuki Sawa
 *
 *	* Mon Mar 25 07:00:00 JST 2002 Naoyuki Sawa
 *	- 作成開始。
 *	* Wed Jul 10 21:01:00 JST 2002 Naoyuki Sawa
 *	- WM_MOVEでの位置保存で、最上位ビットをゼロ拡張していたのを符号拡張に修正。
 *	  マイナス座標で終了したときに、次回右端に復帰してしまう現象が治りました。
 *	- 画面外への移動禁止オプションを追加。
 *	* Mon Jul 15 05:07:00 JST 2002 Naoyuki Sawa
 *	- WM_MOVEでの位置保存で、ウインドウ位置でなくクライアント領域の位置を保存してしまっていたのを修正。
 *	- スプライトフレームバッファの自動探索機能追加。
 *	- キャプチャ時のポーズ完了待ちウェイトをなくし、キャプチャ負荷を低減。
 *	- モード切替・スケール切替・終了のショートカットキー追加。
 *	* Fri Aug  9 12:30:00 JST 2002 Naoyuki Sawa
 *	- 起動時スプライトモード優先オプションを追加しました。
 *	* Fri Aug 15 18:30:00 JST 2002 Naoyuki Sawa
 *	- 起動時スプライトモード優先の挙動を変更し、自動スプライト検索に名称変更しました。
 *	  ウインドウがアクティブ化する度にスプライト情報を検索し、
 *	  見つかればスプライトモード、見つからなければグラフィックモードに切り替えます。
 *	* Fri Nov 15 12:30:00 JST 2002 Naoyuki Sawa
 *	- 縦画面モードを追加しました。
 *	* Sun Jun 6 20:36:00 JST 2004 Naoyuki Sawa
 *	- 16階調モードを追加しました。
 *	* Sat Jul 14 23:59:59 JST 2018 Naoyuki Sawa
 *	- 非同期キャプチャ時に、仮想VRAMアドレスを取得していなかったバグを修正しました。
 *	- 非同期キャプチャでない場合に、最近の高速なPCだと、P/ECEに負荷が掛かり過ぎる問題を修正しました。
 */
#include "app.h"

/****************************************************************************
 *	定数
 ****************************************************************************/

#define EDGESNAP_DISTANCE	8	/* エッジスナップ距離 */

/****************************************************************************
 *	グローバル変数
 ****************************************************************************/

char ini_path[_MAX_PATH];

HINSTANCE g_hinst;
BITMAPINFO* g_bmi_active;
BITMAPINFO* g_bmi_active16;
BITMAPINFO* g_bmi_inactive;
BITMAPINFO* g_bmi_inactive16;
HWND g_hwnd;
NOTIFYICONDATA g_nid;

int g_interval = INTERVAL_MAX;
int g_always = FALSE;
int g_async = FALSE;
int g_clip = FALSE;
int g_scale = SCALE_MIN;
int g_x_pos = 0;
int g_y_pos = 0;
int g_sprite = 0;
int g_vscreen = 0;
int g_xcolor = 0;

unsigned long fram_addr;

/****************************************************************************
 *	WinMain
 ****************************************************************************/

int PASCAL
WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
	int retval, i;
	char drive[_MAX_DRIVE];
	char dir[_MAX_DIR];
	char fname[_MAX_FNAME];
	char ext[_MAX_EXT];
	BITMAPINFOHEADER* bi;
	RGBQUAD* rgb;
	WNDCLASS wc;
	MSG msg;
	char buf[100];

	/* インスタンスハンドルを保存します。 */
	g_hinst = hInst;

	/* 多重起動チェック。 */
	g_hwnd = FindWindow(NULL, APPTITLE);
	if(g_hwnd != NULL) {
		/* 最小化されていたら、復元します。 */
		if(!IsWindowVisible(g_hwnd)) {
			PostMessage(g_hwnd, WM_NOTIFY_ICON, 0, WM_LBUTTONDBLCLK);
		}
		return 0; /* こちらのインスタンスは終了。 */
	}

	/* INIファイル名を作成します。 */
	GetModuleFileName(NULL, ini_path, sizeof ini_path);
	_splitpath(ini_path, drive, dir, fname, ext);
	_makepath(ini_path, drive, dir, fname, ".ini");

	/* INIファイルを読みます。 */
	g_interval = GetPrivateProfileInt(SEC_DEFAULT, KEY_INTERVAL, g_interval, ini_path);
	if(g_interval < INTERVAL_MIN) g_interval = INTERVAL_MIN;
	if(g_interval > INTERVAL_MAX) g_interval = INTERVAL_MAX;
	g_always = GetPrivateProfileInt(SEC_DEFAULT, KEY_ALWAYS, g_always, ini_path);
	g_always = g_always != 0;
	g_async = GetPrivateProfileInt(SEC_DEFAULT, KEY_ASYNC, g_async, ini_path);
	g_async = g_async != 0;
	g_clip = GetPrivateProfileInt(SEC_DEFAULT, KEY_CLIP, g_clip, ini_path);
	g_clip = g_clip != 0;
	g_scale = GetPrivateProfileInt(SEC_DEFAULT, KEY_SCALE, g_scale, ini_path);
	if(g_scale < SCALE_MIN) g_scale = SCALE_MIN;
	if(g_scale > SCALE_MAX) g_scale = SCALE_MAX;
	g_x_pos = GetPrivateProfileInt(SEC_DEFAULT, KEY_X_POS, g_x_pos, ini_path);
	g_y_pos = GetPrivateProfileInt(SEC_DEFAULT, KEY_Y_POS, g_y_pos, ini_path);
	g_sprite = GetPrivateProfileInt(SEC_DEFAULT, KEY_SPRITE, g_sprite, ini_path);
	g_vscreen = GetPrivateProfileInt(SEC_DEFAULT, KEY_VSCREEN, g_vscreen, ini_path);
	g_xcolor = GetPrivateProfileInt(SEC_DEFAULT, KEY_XCOLOR, g_xcolor, ini_path);

	/* 画面転送用のBITMAPINFO構造体を用意しておきます。 */
	/* アクティブ用(4階調) */
	g_bmi_active = (BITMAPINFO*)calloc(sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 256, 1);
	if(g_bmi_active == NULL) {
		MessageBox(NULL, "calloc() failed.", APPTITLE, MB_ICONSTOP | MB_OK);
		exit(1);
	}
	bi = &g_bmi_active->bmiHeader;
	rgb = &g_bmi_active->bmiColors[0];
	bi->biSize = sizeof(BITMAPINFOHEADER);
	bi->biWidth = DISP_X;                      /* →転送直前に再設定します */
	bi->biHeight = -DISP_Y; /* トップダウン */ /* →転送直前に再設定します */
	bi->biPlanes = 1;
	bi->biBitCount = 8;
	for(i = 0; i < 256; i++) {
		if(i < 4) {
			rgb[i].rgbRed   =
			rgb[i].rgbGreen =
			rgb[i].rgbBlue  = 255 - 85 * i;
		} else {
			rgb[i].rgbRed   = 255;
			rgb[i].rgbGreen =   0;
			rgb[i].rgbBlue  = 255;
		}
	}
	/* アクティブ用(16階調) */
	g_bmi_active16 = (BITMAPINFO*)calloc(sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 256, 1);
	if(g_bmi_active16 == NULL) {
		MessageBox(NULL, "calloc() failed.", APPTITLE, MB_ICONSTOP | MB_OK);
		exit(1);
	}
	bi = &g_bmi_active16->bmiHeader;
	rgb = &g_bmi_active16->bmiColors[0];
	bi->biSize = sizeof(BITMAPINFOHEADER);
	bi->biWidth = DISP_X;                      /* →転送直前に再設定します */
	bi->biHeight = -DISP_Y; /* トップダウン */ /* →転送直前に再設定します */
	bi->biPlanes = 1;
	bi->biBitCount = 8;
	for(i = 0; i < 256; i++) {
		if(i < 16) {
			rgb[i].rgbRed   =
			rgb[i].rgbGreen =
			rgb[i].rgbBlue  = 255 - 17 * i;
		} else {
			rgb[i].rgbRed   = 255;
			rgb[i].rgbGreen =   0;
			rgb[i].rgbBlue  = 255;
		}
	}
	/* 非アクティブ用(4階調) */
	g_bmi_inactive = (BITMAPINFO*)calloc(sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 256, 1);
	if(g_bmi_inactive == NULL) {
		MessageBox(NULL, "calloc() failed.", APPTITLE, MB_ICONSTOP | MB_OK);
		exit(1);
	}
	bi = &g_bmi_inactive->bmiHeader;
	rgb = &g_bmi_inactive->bmiColors[0];
	bi->biSize = sizeof(BITMAPINFOHEADER);
	bi->biWidth = DISP_X;                      /* →転送直前に再設定します */
	bi->biHeight = -DISP_Y; /* トップダウン */ /* →転送直前に再設定します */
	bi->biPlanes = 1;
	bi->biBitCount = 8;
	for(i = 0; i < 256; i++) {
		if(i < 4) {
			rgb[i].rgbRed   =
			rgb[i].rgbGreen =
			rgb[i].rgbBlue  = (int)((255 - 85 * i) * 0.75);
		} else {
			rgb[i].rgbRed   = (int)(255 * 0.75);
			rgb[i].rgbGreen = (int)(  0 * 0.75);
			rgb[i].rgbBlue  = (int)(255 * 0.75);
		}
	}
	/* 非アクティブ用(16階調) */
	g_bmi_inactive16 = (BITMAPINFO*)calloc(sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 256, 1);
	if(g_bmi_inactive16 == NULL) {
		MessageBox(NULL, "calloc() failed.", APPTITLE, MB_ICONSTOP | MB_OK);
		exit(1);
	}
	bi = &g_bmi_inactive16->bmiHeader;
	rgb = &g_bmi_inactive16->bmiColors[0];
	bi->biSize = sizeof(BITMAPINFOHEADER);
	bi->biWidth = DISP_X;                      /* →転送直前に再設定します */
	bi->biHeight = -DISP_Y; /* トップダウン */ /* →転送直前に再設定します */
	bi->biPlanes = 1;
	bi->biBitCount = 8;
	for(i = 0; i < 256; i++) {
		if(i < 16) {
			rgb[i].rgbRed   =
			rgb[i].rgbGreen =
			rgb[i].rgbBlue  = (int)((255 - 17 * i) * 0.75);
		} else {
			rgb[i].rgbRed   = (int)(255 * 0.75);
			rgb[i].rgbGreen = (int)(  0 * 0.75);
			rgb[i].rgbBlue  = (int)(255 * 0.75);
		}
	}

	/* メインウインドウクラスを登録します。 */
	memset(&wc, 0, sizeof wc);
	wc.lpfnWndProc = WndProc;
	wc.hInstance = g_hinst;
	wc.hIcon = LoadIcon(g_hinst, MAKEINTRESOURCE(IDR_MAINFRAME));
	wc.lpszClassName = APPNAME;
	retval = RegisterClass(&wc);
	if(!retval) {
		MessageBox(NULL, "RegisterClass() failed.", APPTITLE, MB_ICONSTOP | MB_OK);
		exit(1);
	}

	/* メインウインドウを作成します。 */
	g_hwnd = CreateWindowEx(
		WS_EX_TOPMOST | WS_EX_TOOLWINDOW/*タスクバーに表示しない*/,
		APPNAME,
		APPTITLE,
		WS_POPUP | WS_BORDER,
		g_x_pos, g_y_pos, 0, 0,
		NULL,
		NULL,
		g_hinst,
		NULL);

	/* タスクトレイへの登録/解除のための構造体を用意しておきます。 */
	memset(&g_nid, 0, sizeof g_nid);
	g_nid.cbSize = sizeof g_nid;
	g_nid.hWnd = g_hwnd;
	g_nid.uID = 1;
	g_nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
	g_nid.uCallbackMessage = WM_NOTIFY_ICON;
	g_nid.hIcon = wc.hIcon;
	strcpy(g_nid.szTip, APPTITLE);

	/* メインウインドウの初期位置を設定します。 */
	move_resize();

	/* メインウインドウを表示します。 */
	switch(nCmdShow) {
	default:
		show(TRUE);
		break;
	case SW_SHOWMINIMIZED:
	case SW_SHOWMINNOACTIVE:
		show(FALSE);
		break;
	}

	/* メッセージループ。 */
	while(GetMessage(&msg, NULL, 0, 0)) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	/* BITMAPINFO構造体を開放します。 */
	free(g_bmi_active);
	free(g_bmi_active16);
	free(g_bmi_inactive);
	free(g_bmi_inactive16);

	/* INIファイルを書きます。 */
	WritePrivateProfileString(SEC_DEFAULT, KEY_INTERVAL, itoa(g_interval, buf, 10), ini_path);
	WritePrivateProfileString(SEC_DEFAULT, KEY_ALWAYS, itoa(g_always, buf, 10), ini_path);
	WritePrivateProfileString(SEC_DEFAULT, KEY_ASYNC, itoa(g_async, buf, 10), ini_path);
	WritePrivateProfileString(SEC_DEFAULT, KEY_CLIP, itoa(g_clip, buf, 10), ini_path);
	WritePrivateProfileString(SEC_DEFAULT, KEY_SCALE, itoa(g_scale, buf, 10), ini_path);
	WritePrivateProfileString(SEC_DEFAULT, KEY_X_POS, itoa(g_x_pos, buf, 10), ini_path);
	WritePrivateProfileString(SEC_DEFAULT, KEY_Y_POS, itoa(g_y_pos, buf, 10), ini_path);
	WritePrivateProfileString(SEC_DEFAULT, KEY_SPRITE, itoa(g_sprite, buf, 10), ini_path);
	WritePrivateProfileString(SEC_DEFAULT, KEY_VSCREEN, itoa(g_vscreen, buf, 10), ini_path);
	WritePrivateProfileString(SEC_DEFAULT, KEY_XCOLOR, itoa(g_xcolor, buf, 10), ini_path);

	return msg.wParam;
}

/****************************************************************************
 *	WndProc
 ****************************************************************************/

LRESULT CALLBACK
WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	RECT rc;
	RECT* pr;

	switch(msg) {
	case WM_DESTROY:
		/* 確実にタスクトレイから登録解除。 */
		Shell_NotifyIcon(NIM_DELETE, &g_nid);
		PostQuitMessage(0);
		return 0;
	case WM_NCHITTEST:
		/* どこでもドラッグしてウインドウ移動。 */
		return HTCAPTION;
	case WM_MOVE:
		/* WM_MOVEのlparamはクライアント領域の左上座標をスクリーン座標に直したものなので、
		 * そのまま保存すると次回起動時にウインドウ位置がずれて復元されてしまいます。
		 * 正しいウインドウ位置を保存するために、lparamは使わず明示的に位置を取得します。
		 */
		GetWindowRect(hwnd, &rc);
		g_x_pos = rc.left;
		g_y_pos = rc.top;
		return 0;
	case WM_MOVING:
		if(g_clip) {
			/* 画面外に移動しない。 */
			pr = (RECT*)lparam;
			SystemParametersInfo(SPI_GETWORKAREA, 0, &rc, 0);
			if(pr->right  > rc.right  ) { pr->left   -= pr->right  - rc.right ; pr->right  = rc.right ; }
			if(pr->bottom > rc.bottom ) { pr->top    -= pr->bottom - rc.bottom; pr->bottom = rc.bottom; }
			if(pr->left   < rc.left   ) { pr->right  -= pr->left   - rc.left  ; pr->left   = rc.left  ; }
			if(pr->top    < rc.top    ) { pr->bottom -= pr->top    - rc.top   ; pr->top    = rc.top   ; }
		}
		return 0;
	case WM_ACTIVATEAPP:
		/* WM_ACTIVATEではなくWM_ACTIVATEAPPを使います。
		 * WM_ACTIVATEでは、このアプリケーション自身のダイアログが閉じた後にもメッセージが来るので、
		 * スプライト検索が発生しすぎます。
		 * WM_ACTIVATEAPPならば、別のアプリケーションから切り替わった時だけ、メッセージが来ます。
		 */
		if(g_sprite) {
			if(wparam) sprite_mode();
		}
		return 0;
	case WM_TIMER:
		InvalidateRect(hwnd, NULL, FALSE);
		return 0;
	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);
		GetClientRect(hwnd, &rc);
		update(hdc, &rc);
		EndPaint(hwnd, &ps);
		return 0;
	case WM_NCRBUTTONDOWN:
		popup_menu();
		return 0;
	case WM_KEYDOWN:
		switch(wparam) {
		case VK_SPACE: /* グラフィック/スプライトトグル */
			if(fram_addr == 0) {
				sprite_mode();
				if(fram_addr == 0) {
					MessageBox(g_hwnd,
						"スプライト情報が見つかりません。",
						APPTITLE,
						MB_ICONEXCLAMATION | MB_OK);
				}
			} else {
				graphic_mode();
			}
			return 0;
		case '1':
		case '2':
		case '4':
			change_scale(wparam - '0');
			return 0;
		case 'V':
			change_vscreen(!g_vscreen);
			return 0;
		case 'X':
			g_xcolor = !g_xcolor;
			return 0;
		case VK_ESCAPE: /* 隠す */
			show(FALSE);
			return 0;
		case VK_F12: /* 終了 */
			SendMessage(hwnd, WM_CLOSE, 0, 0);
			return 0;
		}
		return 0;
	case WM_NOTIFY_ICON:
		switch(lparam) {
		case WM_RBUTTONUP:
			popup_menu();
			return 0;
		case WM_LBUTTONDBLCLK:
			show(TRUE);
			return 0;
		}
		return 0;
	}
	return DefWindowProc(hwnd, msg, wparam, lparam);
}

/****************************************************************************
 *	popup_menu
 ****************************************************************************/

void
popup_menu()
{
	int retval, visible;
	MENUITEMINFO mii;
	HMENU hmenu;
	POINT point;

	memset(&mii, 0, sizeof mii);
	mii.cbSize = sizeof mii;
	mii.fMask = MIIM_ID | MIIM_STATE | MIIM_TYPE;

	/* メニューを作成します。 */
	hmenu = CreatePopupMenu();

	/* 表示/非表示によって、メニューを切り替えます。 */
	visible = IsWindowVisible(g_hwnd);
	if(visible) {
		/* グラフィック/スプライトモード */
		mii.fType = MFT_STRING;
		mii.wID = ID_GRAPHIC;
		mii.dwTypeData = fram_addr == 0 ? mii.dwTypeData = "グラフィック" : "グラフィック\tSpace";
		mii.fState = MFS_ENABLED;
		if(fram_addr == 0) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);
		mii.wID = ID_SPRITE;
		mii.dwTypeData = fram_addr != 0 ?"スプライト(再検索)" : "スプライト\tSpace";
		mii.fState = MFS_ENABLED;
		if(fram_addr != 0) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);
		mii.wID = -1;
		mii.fType = MFT_SEPARATOR;
		InsertMenuItem(hmenu, -1, TRUE, &mii);

		/* スケール */
		mii.fType = MFT_STRING;
		mii.wID = ID_SCALE_1;
		mii.dwTypeData = "1倍\t1";
		mii.fState = MFS_ENABLED;
		if(g_scale == 1) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);
		mii.wID = ID_SCALE_2;
		mii.dwTypeData = "2倍\t2";
		mii.fState = MFS_ENABLED;
		if(g_scale == 2) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);
		mii.wID = ID_SCALE_4;
		mii.dwTypeData = "4倍\t4";
		mii.fState = MFS_ENABLED;
		if(g_scale == 4) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);
		mii.wID = -1;
		mii.fType = MFT_SEPARATOR;
		InsertMenuItem(hmenu, -1, TRUE, &mii);

		/* インターバル */
		mii.fType = MFT_STRING;
		mii.wID = ID_INTERVAL_10;
		mii.dwTypeData = "10ミリ秒";
		mii.fState = MFS_ENABLED;
		if(g_interval == 10) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);
		mii.wID = ID_INTERVAL_100;
		mii.dwTypeData = "100ミリ秒";
		mii.fState = MFS_ENABLED;
		if(g_interval == 100) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);
		mii.wID = ID_INTERVAL_1000;
		mii.dwTypeData = "1000ミリ秒";
		mii.fState = MFS_ENABLED;
		if(g_interval == 1000) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);
		mii.wID = -1;
		mii.fType = MFT_SEPARATOR;
		InsertMenuItem(hmenu, -1, TRUE, &mii);

		/* 縦画面モード。 */
		mii.fType = MFT_STRING;
		mii.wID = ID_APP_VSCREEN;
		mii.dwTypeData = "縦画面モード\tV";
		mii.fState = MFS_ENABLED;
		if(g_vscreen) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);

		/* 16階調モード。 */
		mii.fType = MFT_STRING;
		mii.wID = ID_APP_XCOLOR;
		mii.dwTypeData = "16階調モード\tX";
		mii.fState = MFS_ENABLED;
		if(g_xcolor) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);

		/* 自動スプライト検索。 */
		mii.fType = MFT_STRING;
		mii.wID = ID_APP_SPRITE;
		mii.dwTypeData = "自動スプライト検索";
		mii.fState = MFS_ENABLED;
		if(g_sprite) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);

		/* 常に更新。 */
		mii.fType = MFT_STRING;
		mii.wID = ID_APP_ALWAYS;
		mii.dwTypeData = "非アクティブ時も更新";
		mii.fState = MFS_ENABLED;
		if(g_always) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);

		/* 非同期キャプチャ。 */
		mii.fType = MFT_STRING;
		mii.wID = ID_APP_ASYNC;
		mii.dwTypeData = "非同期キャプチャ";
		mii.fState = MFS_ENABLED;
		if(g_async) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);

		/* 画面外に移動しない。 */
		mii.fType = MFT_STRING;
		mii.wID = ID_APP_CLIP;
		mii.dwTypeData = "画面外に移動しない";
		mii.fState = MFS_ENABLED;
		if(g_clip) mii.fState |= MFS_CHECKED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);

		/* 隠す。 */
		mii.fType = MFT_STRING;
		mii.wID = ID_APP_SHOW;
		mii.dwTypeData = "隠す\tEsc";
		mii.fState = MFS_ENABLED;
		InsertMenuItem(hmenu, -1, TRUE, &mii);
	} else {
		/* 表示。 */
		mii.fType = MFT_STRING;
		mii.wID = ID_APP_SHOW;
		mii.dwTypeData = "表示";
		mii.fState = MFS_ENABLED | MFS_DEFAULT;
		InsertMenuItem(hmenu, -1, TRUE, &mii);
	}
	/* バージョン情報。 */
	mii.fType = MFT_STRING;
	mii.wID = ID_APP_ABOUT;
	mii.dwTypeData = "バージョン情報";
	mii.fState = MFS_ENABLED;
	InsertMenuItem(hmenu, -1, TRUE, &mii);
	mii.wID = -1;
	mii.fType = MFT_SEPARATOR;
	InsertMenuItem(hmenu, -1, TRUE, &mii);

	/* 終了 */
	mii.fType = MFT_STRING;
	mii.wID = ID_APP_EXIT;
	mii.dwTypeData = visible ? "終了\tF12" : "終了" ;
	mii.fState = MFS_ENABLED;
	InsertMenuItem(hmenu, -1, TRUE, &mii);

	/* メニューを表示します。 */
	GetCursorPos(&point);
	retval = TrackPopupMenu(hmenu,
		TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
		point.x, point.y,
		0,
		g_hwnd,
		NULL);

	/* メニューを削除します。 */
	DestroyMenu(hmenu);

	/* メニュー処理。 */
	switch(retval) {
	case ID_APP_EXIT:
		DestroyWindow(g_hwnd);
		return;
	case ID_APP_VSCREEN:
		change_vscreen(!g_vscreen);
		return;
	case ID_APP_XCOLOR:
		g_xcolor = !g_xcolor;
		return;
	case ID_APP_SPRITE:
		g_sprite = !g_sprite;
		return;
	case ID_APP_ALWAYS:
		g_always = !g_always;
		return;
	case ID_APP_ASYNC:
		g_async = !g_async;
		return;
	case ID_APP_CLIP:
		g_clip = !g_clip;
		if(g_clip) move_resize();
		return;
	case ID_APP_SHOW:
		visible = !visible;
		show(visible);
		return;
	case ID_APP_ABOUT:
		MessageBox(g_hwnd,
			APPTITLE " " VERSION "\n" \
			"Copyright (C) 2002-2018 Naoyuki Sawa",
			APPTITLE,
			MB_ICONINFORMATION | MB_OK);
		return;
	case ID_SCALE_1:
	case ID_SCALE_2:
	case ID_SCALE_4:
		switch(retval) {
		case ID_SCALE_1: change_scale(1); break;
		case ID_SCALE_2: change_scale(2); break;
		case ID_SCALE_4: change_scale(4); break;
		}
		return;
	case ID_INTERVAL_10:
	case ID_INTERVAL_100:
	case ID_INTERVAL_1000:
		switch(retval) {
		case ID_INTERVAL_10:   change_interval(10  ); break;
		case ID_INTERVAL_100:  change_interval(100 ); break;
		case ID_INTERVAL_1000: change_interval(1000); break;
		}
		return;
	case ID_GRAPHIC:
		graphic_mode();
		return;
	case ID_SPRITE:
		sprite_mode();
		if(fram_addr == 0) {
			MessageBox(g_hwnd,
				"スプライト情報が見つかりません。",
				APPTITLE,
				MB_ICONEXCLAMATION | MB_OK);
		}
		return;
	}
}

/****************************************************************************
 *	show
 ****************************************************************************/

void
show(int visible)
{
	if(visible) {
		/* 表示。 */
		Shell_NotifyIcon(NIM_DELETE, &g_nid);
		ShowWindow(g_hwnd, SW_SHOW);
		UpdateWindow(g_hwnd);
		SetTimer(g_hwnd, TIMER_ID, g_interval, NULL);
	} else {
		/* 隠す。 */
		KillTimer(g_hwnd, TIMER_ID);
		ShowWindow(g_hwnd, SW_HIDE);
		Shell_NotifyIcon(NIM_ADD, &g_nid);
	}
}

/****************************************************************************
 *	move_resize
 ****************************************************************************/

void
move_resize()
{
	int w, h;
	RECT rc;

	/* サイズ計算。 */
	if(!g_vscreen) { /* 横画面 */
		w = DISP_X * g_scale + GetSystemMetrics(SM_CXBORDER) * 2;
		h = DISP_Y * g_scale + GetSystemMetrics(SM_CYBORDER) * 2;
	} else { /* 縦画面 */
		w = DISP_Y * g_scale + GetSystemMetrics(SM_CXBORDER) * 2;
		h = DISP_X * g_scale + GetSystemMetrics(SM_CYBORDER) * 2;
	}

	/* はみだしチェック。 */
	SystemParametersInfo(SPI_GETWORKAREA, 0, &rc, 0);
	if(g_x_pos + w > rc.right ) g_x_pos = rc.right  - w;
	if(g_y_pos + h > rc.bottom) g_y_pos = rc.bottom - h;
	if(g_x_pos     < rc.left  ) g_x_pos = rc.left     ;
	if(g_y_pos     < rc.top   ) g_y_pos = rc.top      ;

	/* ウインドウ移動・リサイズ。 */
	MoveWindow(g_hwnd, g_x_pos, g_y_pos, w, h, TRUE/*FALSEにすると、デスクトップにゴミが残ります。*/);

	/* すぐに強制再描画。 */
	InvalidateRect(g_hwnd, NULL, FALSE); /* これ忘れないで! */
	UpdateWindow(g_hwnd);
}

/****************************************************************************
 *	update
 ****************************************************************************/

/* vbuff[横画面]->rbuff[縦画面]へ右90°回転。 */
static void
rotbuf(unsigned char vbuff[DISP_X * DISP_Y], unsigned char rbuff[DISP_X * DISP_Y])
{
	unsigned char (*src)[DISP_X] = (unsigned char (*)[DISP_X])&vbuff[0];
	unsigned char (*dst)[DISP_Y] = (unsigned char (*)[DISP_Y])&rbuff[0];
	int x, y;

	for(y = 0; y < DISP_Y; y++) {
		for(x = 0; x < DISP_X; x++) {
			dst[x][(DISP_Y - 1) - y] = src[y][x];
		}
	}
}

void
update(HDC hdc, const RECT* rc)
{
	static unsigned char vbuff[DISP_X * DISP_Y];
	       unsigned char rbuff[DISP_X * DISP_Y];
	BITMAPINFO* bmi;
	int w, h;

	/* アクティブ状態によって、キャプチャの有無とパレット選択。 */
	if(g_always || GetForegroundWindow() == g_hwnd) {
		if(capture(vbuff, sizeof vbuff) != 0) memset(vbuff, -1, sizeof vbuff);
		if(g_xcolor && !fram_addr) { /* 16階調モード。ただしスプライトモード時は常に4階調 */
			bmi = g_bmi_active16;
		} else {
			bmi = g_bmi_active;
		}
	} else {
		if(g_xcolor && !fram_addr) { /* 16階調モード。ただしスプライトモード時は常に4階調 */
			bmi = g_bmi_inactive16;
		} else {
			bmi = g_bmi_inactive;
		}
	}

	/* 縦画面モードによって、転送サイズ選択と回転。 */
	if(!g_vscreen) {
		w = DISP_X;
		h = DISP_Y;
		memcpy(rbuff, vbuff, DISP_X * DISP_Y);
	} else {
		w = DISP_Y;
		h = DISP_X;
		rotbuf(vbuff, rbuff);
	}

	/* 画面転送。 */
	bmi->bmiHeader.biWidth = w;
	bmi->bmiHeader.biHeight = -h; /* トップダウン */
	StretchDIBits(hdc,
		rc->left, rc->top, rc->right - rc->left, rc->bottom - rc->top,
		0, 0, w, h,
		rbuff,
		bmi,
		DIB_RGB_COLORS,
		SRCCOPY);
}

/****************************************************************************
 *	capture
 ****************************************************************************/

int
capture(unsigned char* vbuff, int len)
{
	int retval, stat, x, y, T0, T, dT;
	unsigned long pbuff;
	unsigned char fram[DISP_Y][2][DISP_X / 8];
	unsigned char* p;

	retval = ismInitEx(0, 0);
	if(retval != 0) goto ERR;

	/* * ismLCDCaptureScreen()は、ポーズ完了待ちループ中で20msのウェイトを取っています。
	 *   キャプチャ負荷が高いのは、どうやらこれが原因のようです。
	 * *  ismAppPause(1)発行後、一度目のismLCDGetInfo()ではまだポーズ完了していませんが、
	 *   20msよりもかなり早いタイミングでポーズがかかる場合にも、
	 *   ismLCDCaptureScreen()は無条件で20ms待ってしまいます。
	 *   そのため、P/ECEもポーズ状態のまま20ms弱待たされてしまいます。
	 * * USB通信自体はP/ECEにさほど負荷をかけないはずなので、
	 *   ウェイトを外した自前のループを回すことにしました。
//{{2018/07/14コメント追加:非同期キャプチャでない場合に、最近の高速なPCだと、P/ECEに負荷が掛かり過ぎる問題を修正しました。
	 * * 当ツールを作成した当時(2002年頃)のPCは遅かったので、全力で回しても良かったのですが、
	 *   最近(2018年現在)の速いPCだと、ループが早過ぎてP/ECEへのUSB割り込み負荷が高過ぎます。
	 *   負荷が高過ぎると、P/ECEのポーズ状態への移行も遅れ、キャプチャのレートも遅くなります。
	 *   上記の問題を避けるために、ここに1ミリ秒のウェイトを入れる事にしました。
	 *   ウェイト時間は調整可能ですが、0では効果が無く、1で十分効果が有ったので、1にしました。
	 *   尚、今回の修正は、'Wed Jul 19 18:18:33 JST 2017 Naoyuki Sawa'に、UfeSvrに対して行ったのと、同じ問題の修正となります。
	 *   詳細については、/clip/tool/ufesvr/ufesvr.cの、'Wed Jul 19 18:18:33 JST 2017 Naoyuki Sawa'のコメントを参照して下さい。
//}}2018/07/14コメント追加:非同期キャプチャでない場合に、最近の高速なPCだと、P/ECEに負荷が掛かり過ぎる問題を修正しました。
	 */

	/* 非同期キャプチャでなければポーズをかけます。 */
	if(!g_async) {
		retval = ismAppPause(1);
		if(retval != 0) goto ERR;
		/* アプリケーションがポーズ状態になるまで待ちます。 */
		T0 = GetTickCount();
		for(;;) {
			retval = ismLCDGetInfo(&stat, &pbuff); /* 後で使うので仮想VRAMアドレスも取得 */
			if(retval != 0) goto ERR;
			/* * 以前のカーネルは、
			 *	statのbit0にポーズ要求フラグ
			 *	statのbit1にpceAppProc実行中フラグ
			 *   を返していたので (stat & 3) == 1 になるまで待つ必要がありました。
			 *   (拙作miniISDはその頃に作ったので、そうなっています)
			 * * BIOS1.18現在のカーネルは、
			 *	stat=0:ポーズ要求が発行されていない、または要求が発行されたがまだpceAppProcが実行中
			 *	     1:ポーズ状態に入った
			 *   を返すようになったので、単にstatが0か否かを判断するだけでよくなりました。
			 * * 現在のカーネルでも、以前の判定式 (stat & 3) == 1 は正しく動きますが、
			 *   もはやbit2が0かどうかを調べるのは無意味になったので(常に0だから)、
			 *   今回は新しいカーネルを前提とした判断式を使うことにしました。
			 */
			if(stat) break;
//{{2018/07/14変更:非同期キャプチャでない場合に、最近の高速なPCだと、P/ECEに負荷が掛かり過ぎる問題を修正しました。
//			/* * ここにウェイトを入れてもいいが、いまのところ全速で回すことにします。
//			 *   この関数の先頭あたりのコメント参照。
//			 */
//			T = GetTickCount();
//			dT = T - T0;
//			if(dT >= 1000) { /* 1秒でタイムアウト */
//				retval = ismAppPause(0); /* ポーズ解除 */
//				goto ERR;
//			}
//↓2018/07/14変更:非同期キャプチャでない場合に、最近の高速なPCだと、P/ECEに負荷が掛かり過ぎる問題を修正しました。
			/* * 当ツールを作成した当時(2002年頃)のPCは遅かったので、全力で回しても良かったのですが、
			 *   最近(2018年現在)の速いPCだと、ループが早過ぎてP/ECEへのUSB割り込み負荷が高過ぎます。
			 *   負荷が高過ぎると、P/ECEのポーズ状態への移行も遅れ、キャプチャのレートも遅くなります。
			 *   上記の問題を避けるために、ここに1ミリ秒のウェイトを入れる事にしました。
			 *   ウェイト時間は調整可能ですが、0では効果が無く、1で十分効果が有ったので、1にしました。
			 *   尚、今回の修正は、'Wed Jul 19 18:18:33 JST 2017 Naoyuki Sawa'に、UfeSvrに対して行ったのと、同じ問題の修正となります。
			 *   詳細については、/clip/tool/ufesvr/ufesvr.cの、'Wed Jul 19 18:18:33 JST 2017 Naoyuki Sawa'のコメントを参照して下さい。
			 */
			T = GetTickCount();
			dT = T - T0;
			if(dT >= 1000) { /* 1秒でタイムアウト */
				retval = ismAppPause(0); /* ポーズ解除 */
				goto ERR;
			}
			Sleep(1/*調整可*/); /* P/ECEに負荷が掛かり過ぎないよう、ウェイトを入れる。 */
//}}2018/07/14変更:非同期キャプチャでない場合に、最近の高速なPCだと、P/ECEに負荷が掛かり過ぎる問題を修正しました。
		}
	}

	/* スプライトモード */
	if(fram_addr != 0) {
		/* スプライトライブラリ内のフレームバッファを読み込みます。 */
		retval = ismReadMem((unsigned char*)fram, fram_addr, sizeof fram);
		if(retval != 0) {
			if(!g_async) ismAppPause(0); /* ポーズ解除 */
			goto ERR;
		}

	/* グラフィックモード */
	} else {
//{{2018/07/14追加:非同期キャプチャ時に、仮想VRAMアドレスを取得していなかったバグを修正しました。
		/* 非同期キャプチャ時は、まだ仮想VRAMアドレスを取得していないので、ここで取得する。 */
		if(g_async) {
			retval = ismLCDGetInfo(&stat, &pbuff); /* 後で使うので仮想VRAMアドレスも取得 */
			if(retval != 0) goto ERR;
		}
//}}2018/07/14追加:非同期キャプチャ時に、仮想VRAMアドレスを取得していなかったバグを修正しました。

		/* 仮想VRAMを読み込みます。
		 * * 仮想VRAMアドレスはポーズ完了待ちのためのismLCDGetInfo()でいっしょに取得済み！
		 *   よく考えられているなあ。
		 */
		retval = ismReadMem(vbuff, pbuff, len);
		if(retval != 0) {
			if(!g_async) ismAppPause(0); /* ポーズ解除 */
			goto ERR;
		}
	}

	/* 非同期キャプチャでなければポーズ解除します。 */
	if(!g_async) ismAppPause(0);

	/* スプライトモードなら、フレームバッファを仮想VRAM形式に展開します。
	 * P/ECEのポーズ時間を少しでも短くするため、ポーズ解除後に行います。
	 */
	if(fram_addr != 0) {
		p = vbuff;
		for(y = 0; y < DISP_Y; y++) {
			for(x = 0; x < DISP_X; x++) {
				*p++ = (fram[y][0][x / 8] >> x % 8 & 1) << 1 |
				       (fram[y][1][x / 8] >> x % 8 & 1);
			}
		}
	}

	/* FALL THRU */
ERR:
	ismExit();

	return retval;
}

/****************************************************************************
 *	change_scale
 ****************************************************************************/

void
change_scale(int scale)
{
	g_scale = scale;
	if(IsWindowVisible(g_hwnd)) {
		move_resize();
	}
}

/****************************************************************************
 *	change_interval
 ****************************************************************************/

void
change_interval(int interval)
{
	g_interval = interval;
	if(IsWindowVisible(g_hwnd)) {
		KillTimer(g_hwnd, TIMER_ID);
		SetTimer(g_hwnd, TIMER_ID, g_interval, NULL);
	}
}

/****************************************************************************
 *	change_vscreen
 ****************************************************************************/

void
change_vscreen(int vscreen)
{
	g_vscreen = vscreen;
	if(IsWindowVisible(g_hwnd)) {
		move_resize();
	}
}

/****************************************************************************
 *	graphic_mode
 ****************************************************************************/

void
graphic_mode()
{
	fram_addr = 0;
}

/****************************************************************************
 *	sprite_mode
 ****************************************************************************/

void
sprite_mode()
{
	HCURSOR old_cursor;
	old_cursor = SetCursor(LoadCursor(NULL, IDC_WAIT));
	fram_addr = fram_find();
	SetCursor(old_cursor);
	/* エラーメッセージ表示は呼び出し側で行ってください。 */
}
