/* console.cpp
   Copyright (C) 2005 Free Software Foundation, Inc.

This file is part of Mysaifu JVM

Mysaifu JVM is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.

Mysaifu JVM is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.
*/

#include "StdAfx.h"
#include "console.h"
#include "resource.h"

/**
 * コンソールウインドウのハンドル
 */
static HWND g_hwndConsole;

/**
 * HINSTANCE
 */
static HINSTANCE g_hInstance;

/**
 * 出力バッファの最大行数
 */
#define	MAX_OUTPUT_LINE_COUNT	100

/**
 * リダイレクト先
 */
static HANDLE g_hLogFile;

/**
 * 入力バッファのイベントオブジェクト
 */
static HANDLE g_hInputEvent;

/**
 * コンソールが非表示になったことを示すイベントオブジェクト
 */
static HANDLE g_hClosedEvent;

/**
 * 入力コントロールのウインドウプロシージャ
 */
WNDPROC g_wndprocInput;

/**
 * ダイアログプロシージャ
 */
LRESULT CALLBACK ConsoleDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);

/**
 * ダイアログプロシージャ
 */
LRESULT CALLBACK InputWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

/**
 * ダイアログのレイアウトを行う
 */
static void layoutConsoleDialog(HWND hDlg);

/**
 * stdioが存在するか?
 */
BOOL g_use_stdio;

/**
 * コンソールウインドウを作成する
 */
void console_init(HINSTANCE hInstance, HWND hwndParent, bool use_stdio) {
	g_hInstance = hInstance;

	// コンソールダイアログを作る
	g_hwndConsole = CreateDialog(hInstance,
								 MAKEINTRESOURCE(IDD_CONSOLE),
								 hwndParent,
								 (DLGPROC) ConsoleDialogProc);
	
	// 通知用イベントを初期化する
	g_hInputEvent = CreateEvent(NULL,
								TRUE,	// マニュアルリセット
								FALSE,	// 初期状態
								NULL);

	g_hClosedEvent = CreateEvent(NULL,
								 FALSE,	// オートリセット
								 TRUE,	// 初期状態
								 NULL);
#if 0
	// GetStdiuPathW()では確認できない(?)
	HMODULE hModule = GetModuleHandle(_T("coredll"));
	if (hModule) {
		typedef BOOL(*GET_STUDIO_PATH_W)(DWORD, PWSTR, LPDWORD);
		WCHAR buf[MAX_PATH];

		GET_STUDIO_PATH_W get_stdio_path_w = (GET_STUDIO_PATH_W) GetProcAddress(hModule, _T("GetStdioPathW"));
		if (get_stdio_path_w) {
			DWORD len = MAX_PATH;
			int result = get_stdio_path_w(0, buf, &len);
			if (result && len > 0) {
				g_use_stdio = true;
			}
		}
	}
#endif
	g_use_stdio = use_stdio;

}


/**
 * コンソールウインドウの表示／非表示を切り替える
 */
void console_set_visible(bool visible) {
	if (visible) {
		ResetEvent(g_hClosedEvent);
		ShowWindow(g_hwndConsole, SW_SHOW);
		UpdateWindow(g_hwndConsole);
		SetForegroundWindow(g_hwndConsole);
	} else {
		SetEvent(g_hClosedEvent);
		ShowWindow(g_hwndConsole, SW_HIDE);
	}
}

/**
 * コンソールが表示状態の場合、非表示になるまで待機する
 */
void console_wait_for_close() {
	if (IsWindowVisible(g_hwndConsole)) {
		console_write(_T("JVM exit"));
		SetForegroundWindow(g_hwndConsole);
		WaitForSingleObject(g_hClosedEvent, INFINITE);
	}
}

/**
 * コンソールに１文字出力する
 */
void console_write(_TCHAR c) {
	_TCHAR text[2] = { c, _T('\0') };
	console_write(text);
}

/**
 * コンソールに文字列を出力する
 */
void console_write(const _TCHAR* text) {
	if (g_use_stdio) {
		_fputts(text, stdout);
	}
	HWND hwndOutput = GetDlgItem(g_hwndConsole, IDC_CONSOLE_OUTPUT);
	
	// 一定行数を超えた場合、先頭の行を削除する
	// スクロール位置がいったん先頭に戻ってしまうため、なるべく呼び出す回数を減らす		
	int linecount = SendMessage(hwndOutput, EM_GETLINECOUNT, 0, 0);
	if (linecount >= MAX_OUTPUT_LINE_COUNT * 2) {
		// 最大行数の２倍を超えた場合にのみ処理を行う
		int lineindex = SendMessage(hwndOutput, EM_LINEINDEX, MAX_OUTPUT_LINE_COUNT, 0);
		if (lineindex != -1) {
			SendMessage(hwndOutput, EM_SETSEL, (WPARAM) 0, (LPARAM) lineindex);
			SendMessage(hwndOutput, EM_REPLACESEL, (WPARAM) FALSE, (LPARAM) _T(""));
		}
	}

	int len = _tcslen(text);
	const _TCHAR* pout = text;
	const _TCHAR* work = pout;
	_TCHAR* tmp = NULL;
	const size_t tmp_size = sizeof(_TCHAR) * (len + 2 + 1);	// 文字列 + \r\n + \0
	while ((work = _tcschr(work, _T('\n'))) != NULL) {
		if (work == text || (work != text && *(work - 1) != _T('\r'))) {
			// '\n' が単独で出現した場合
			if (! tmp) {
				tmp = (_TCHAR*) calloc(tmp_size, 1);
				if (! tmp) {
					// メモリ不足の場合はスルーする
					continue;
				}
			} else {
				memset(tmp, 0, tmp_size);
			}
			_tcsncpy(tmp, pout, (work - pout));
			_tcscat(tmp, _T("\r\n"));
			// 部分的に出力する
			int pos = GetWindowTextLength(hwndOutput);
			SendMessage(hwndOutput, EM_SETSEL, (WPARAM) pos, (WPARAM) pos);
			SendMessage(hwndOutput, EM_REPLACESEL, (WPARAM) FALSE, (LPARAM) tmp);

			// 出力開始位置をずらす
			pout = ++work;
		} else {
			work++;
		}
	}
	free(tmp);

	// 末尾に文字列を追加する
	int pos = GetWindowTextLength(hwndOutput);
	SendMessage(hwndOutput, EM_SETSEL, (WPARAM) pos, (WPARAM) pos);
	SendMessage(hwndOutput, EM_REPLACESEL, (WPARAM) FALSE, (LPARAM) pout);

	// コンソールログファイルに出力する
	if (g_hLogFile) {
		DWORD count;
		WriteFile(g_hLogFile,
				  text,
				  _tcslen(text) * sizeof(_TCHAR),
				  &count,
				  NULL);
	}
}

/**
 * コンソールから文字列を読み込む
 */
int console_read(_TCHAR* buff, int len) {
	if (! len) {
		return 0;
	}
	// 必ず２バイトの倍数であること
	assert(! (len & 0x01));

	int copylen = 0;
	if (g_use_stdio && GetForegroundWindow() != g_hwndConsole) {
		// 標準入力から読み込む
		_TCHAR* result = _fgetts(buff, len, stdin);
		if (! result) {
			copylen = -1;
		} else {
			copylen = _tcslen(buff);
		}
	} else {
		if (GetForegroundWindow() != g_hwndConsole) {
			SetForegroundWindow(g_hwndConsole);
		}

		// コンソールから読み込む
		// 読み込み可能になるまで待機する
		WaitForSingleObject(g_hInputEvent, INFINITE);

		// 読み取り専用にする
		HWND hwndInput = GetDlgItem(g_hwndConsole, IDC_CONSOLE_INPUT);
		SendMessage(hwndInput, EM_SETREADONLY, (WPARAM) TRUE, 0);

		// コピーする長さを決定する
		int textlen = GetWindowTextLength(hwndInput) + 2;	// 改行分追加
		copylen = (len < textlen) ? len : textlen;
		
		// テキストを取得する
		GetWindowText(hwndInput, buff, copylen);
		
		// 改行を追加する
		if (textlen == copylen) {
			_tcscat(buff, _T("\r\n"));
		}

		// テキストを選択状態にして、読み込んだ範囲の文字列をクリアする
		SendMessage(hwndInput,
					EM_SETSEL,
					(WPARAM) 0,				// 先頭
					(LPARAM) copylen		// 末尾
					);
		SendMessage(hwndInput,
					EM_REPLACESEL,
					(WPARAM) FALSE,			// UNDO不要
					(LPARAM) _T("")			// 空文字列
					);
		
		if (! GetWindowTextLength(hwndInput)) {
			// もう読み込む文字がない場合は、編集可能に戻す
			SendMessage(hwndInput, EM_SETREADONLY, (WPARAM) FALSE, 0);
			ResetEvent(g_hInputEvent);
		}

		// エコーバックする（不要？）
		_TCHAR echoback_buff[128 + 1];
		int remains = copylen;
		while (remains > 0) {
			int echolen = remains < 128 ? remains : 128;
			_tcsncpy(echoback_buff, buff, echolen);
			echoback_buff[echolen] = _T('\0');
			console_write(echoback_buff);
			remains -= echolen;
			buff += echolen;
		}
	}

	return copylen;	
}

/**
 * ログファイルを設定する
 */
void console_set_logfile(HANDLE out) {
	g_hLogFile = out;
}

/**
 * コンソールダイアログプロシージャ
 */
LRESULT CALLBACK ConsoleDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) {
	static SHACTIVATEINFO sai;

	switch (message) {
		case WM_INITDIALOG:
			{
				sai.cbSize = sizeof(SHACTIVATEINFO);

				// タイトルを設定する
				SetWindowText(hDlg, _T("Mysaifu JVM console"));

				// ダイアログボックスを最大化する
				SHINITDLGINFO shidi = {0};
				shidi.dwMask = SHIDIM_FLAGS;
//				shidi.dwFlags = SHIDIF_SIZEDLGFULLSCREEN | SHIDIF_DONEBUTTON | SHIDIF_EMPTYMENU;
				shidi.dwFlags = SHIDIF_SIZEDLGFULLSCREEN | SHIDIF_DONEBUTTON;
				shidi.hDlg = hDlg;
				SHInitDialog(&shidi);

				layoutConsoleDialog(hDlg);
				
				// テキストボックスをサブクラス化する
				HWND hwndInput = GetDlgItem(hDlg, IDC_CONSOLE_INPUT);
				g_wndprocInput = (WNDPROC) GetWindowLong(hwndInput, GWL_WNDPROC);
				SetWindowLong(hwndInput, GWL_WNDPROC, (LONG) InputWndProc);
			
			}
			return TRUE;

		case WM_ACTIVATE:
			SHHandleWMActivate(hDlg, wParam, lParam, &sai, FALSE);
			return TRUE;

		case WM_SETTINGCHANGE:
			SHHandleWMSettingChange(hDlg, wParam, lParam, &sai);
     		return TRUE;

		case WM_SIZE:
			{
				// 再レイアウトする
				layoutConsoleDialog(hDlg);
			}
			return TRUE;

		case WM_COMMAND:
			switch (LOWORD(wParam)) {
			case IDOK:
				console_set_visible(false);
			}
			return TRUE;
	}
	return FALSE;
}

/**
 * ダイアログのレイアウトを行う
 */
static void layoutConsoleDialog(HWND hDlg) {
	// 入出力テキストボックスのサイズを、ウインドウサイズに合わせる
	HWND hwndOutput = GetDlgItem(hDlg, IDC_CONSOLE_OUTPUT);
	HWND hwndInput = GetDlgItem(hDlg, IDC_CONSOLE_INPUT);

	RECT rect;
	GetWindowRect(hDlg, &rect);
	
	RECT rectInput;
	GetWindowRect(hwndInput, &rectInput);

	// 出力用テキストボックスを中央に配置する
	int outputWidth = rect.right - rect.left;
	int outputHeight = (rect.bottom - rect.top) - (rectInput.bottom - rectInput.top);
	MoveWindow(hwndOutput,
				0,
				0,
				outputWidth,
				outputHeight,
				TRUE);

	// 入力用テキストボックスを一番下に配置する
	int inputY = outputHeight;
	MoveWindow(hwndInput,
			   0,
			   inputY,
			   outputWidth,
			   (rectInput.bottom - rectInput.top),
			   TRUE);
}

/**
 * 入力エディットコントロール
 */
LRESULT CALLBACK InputWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
	switch (message) {
	case WM_CHAR:
		if (wParam == VK_RETURN) {
			// リターンキーが押下された時点でイベントを通知する
			SetEvent(g_hInputEvent);
		}
		break;
	}
	return CallWindowProc(g_wndprocInput, hWnd, message, wParam, lParam);
}