/*	
 *	dstdpipe.c
 *
 *	dstdpipe - P/ECE標準入出力中継パイプ
 *	Copyright (C) 2002-2014 Naoyuki Sawa
 *
 *	* Thu Mar 13 01:25:35 JST 2014 Naoyuki Sawa
 *	- 1st リリース。
 *	* Fri Mar 14 04:56:05 JST 2014 Naoyuki Sawa
 *	- 毎回、標準出力をフラッシュするように、修正しました。
 *	  stdout_routine()内のコメントを参照してください。
 */
#define STRICT
#include <windows.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <process.h>

#include <pieceif.h>
#pragma comment(lib,"pieceif.lib")

#define VERSION		"20140314"

/****************************************************************************
 *	変数
 ****************************************************************************/

volatile int terminate;	/* 終了要求 */
char devno;	/* P/ECEデバイス番号 */
USBCOMS ucs;	/* USBCOM状態取得用 */

/****************************************************************************
 *	usage
 ****************************************************************************/

void usage() {
	fprintf(stderr, "dstdpipe - P/ECE標準入出力中継パイプ (%s)\n", VERSION);
	fprintf(stderr, "Copyright (C) 2014 Naoyuki Sawa\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "USAGE: dstdpipe [devno]\n");
	fprintf(stderr, "  devno  P/ECEデバイス番号\n");
	fprintf(stderr, "         省略時は、#0(一台目のP/ECE)に接続します。\n");
	exit(EXIT_FAILURE);
}

/****************************************************************************
 *	disconnect
 ****************************************************************************/

void disconnect() {
	ismUCClose();	/* USBCOM切断(P/ECE独自のプロトコルレイヤでの切断) */
	ismExit();	/* バルクPIPE切断(OSレベルのUSB通信レイヤでの切断) */
}

/****************************************************************************
 *	sigint
 ****************************************************************************/

void sigint(int sig) {
	terminate = 1; /* 終了要求 */
}

/****************************************************************************
 *	stdin_routine
 ****************************************************************************/

HANDLE stdin_event;	/* 標準入力バッファが空であることを示す、手動リセットイベント */
volatile char stdin_str[1024];

void stdin_routine(void* arglist) {
	/* このスレッドはIOバウンドなので、メインスレッドよりも優先度を上げます。 */
	SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);
	for(;;) {
		/* 標準入力バッファが空になるまで待つ。手動リセットイベントなので、WaitForSingleObject()が処理を返した後も、イベントはシグナル状態のままです。 */
		WaitForSingleObject(stdin_event, INFINITE);
		/* 標準入力から、標準入力バッファへ読み込む。標準入力が閉じられたら抜ける */
		if(!fgets((char*)stdin_str, sizeof stdin_str, stdin)) { break; }	/* キャストはvolatileの警告回避のため */
		/* 読み込んだ結果が空でなければ、手動リセットイベントをリセットして、メインスレッドへ、標準入力バッファが空でなくなったことを示す。 */
		if(stdin_str[0]) { ResetEvent(stdin_event); }
	}
	terminate = 1; /* 終了要求 */
}

/****************************************************************************
 *	stdout_routine
 ****************************************************************************/

HANDLE stdout_event;	/* 標準出力バッファが空でないことを示す、手動リセットイベント */
volatile char stdout_str[1024];

void stdout_routine(void* arglist) {
	/* このスレッドはIOバウンドなので、メインスレッドよりも優先度を上げます。 */
	SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);
	for(;;) {
		/* 標準出力バッファが空でなくなるまで待つ。手動リセットイベントなので、WaitForSingleObject()が処理を返した後も、イベントはシグナル状態のままです。 */
		WaitForSingleObject(stdout_event, INFINITE);
		/* 標準出力バッファから、標準出力へ書き出す。標準出力が閉じられたら抜ける */
		if(fputs((char*)stdout_str, stdout) == -1) { break; }	/* キャストはvolatileの警告回避のため */
		/* 毎回、標準出力をフラッシュする。
		 * - 当ツールをコマンドラインから使用した場合は、フラッシュしなくても問題ない。
		 *   標準出力がコンソールに向いているので、そのままでもバッファリングされないから。
		 * - しかし、当ツールを双方向パイプとして使用した場合に、フラッシュしないと問題が生じる。
		 *   ＜例:Tclスクリプト＞
		 *   │set fp [open "|dstdpipe.exe" "r+"]
		 *   │fconfigure $fp -buffering line
		 *   │puts $fp "How do you do?"
		 *   │gets $fp s	#//これが取得できずに待ち続ける問題が生じる。
		 *   │puts "P/ECE said '$s'."
		 *   毎回、標準出力をフラッシュすれば、上記の問題は解消する。
		 * - 尚、当ツールをコマンドラインから使用した場合は、元々大丈夫なので、標準出力をフラッシュする必要は無いが、しても害は無い。
		 *   僅かに性能低下する可能性はあるが、元々、P/ECEとの通信速度がとても遅いので、多少遅くなっても、実質、性能の違いは生じない。
		 */
		fflush(stdout);
		/* 手動リセットイベントをリセットして、メインスレッドへ、標準出力バッファが空になったことを示す。 */
		ResetEvent(stdout_event);
	}
	terminate = 1; /* 終了要求 */
}

/****************************************************************************
 *	main
 ****************************************************************************/

int main(int argc, char* argv[]) {
	int retval, exit_code;
	char* p;

	/* 終了コードを失敗としておく。 */
	exit_code = EXIT_FAILURE;

	/*-------------------- 初期化 --------------------*/

	/* コマンドライン解析。 */
	switch(argc) {
	case 1:	/* dstdpipe */
		/** no job **/
		break;
	case 2:	/* dstdpipe devno */
		/* デバイス番号指定 */
		devno = (char)strtol(argv[1], &p, 10);
		if(*p != '\0') { usage(); } /* 数字以外の文字で終端した */
		break;
	default:
		usage();
	}

	/* Ctrl+Cをフックします。 */
	signal(SIGINT, sigint);

	/* P/ECEに接続します。 */
	retval = ismInitEx(devno, PIECE_DEF_WAITN);
	if(retval != 0) {
		fprintf(stderr, "### P/ECEに接続できません。\n");
		goto EXIT;
	}

	/* USBCOMに接続します。 */
	retval = ismUCOpen(&ucs);
	if(retval != 0) {
		fprintf(stderr, "### USBCOMに接続できません。\n");
		goto EXIT;
	}
	if(!ucs.mystat || strcmp(ucs.signature, "stderr") != 0) {
		fprintf(stderr, "### 標準エラー出力に接続できません。\n");
		goto EXIT;
	}

	/* 標準入力スレッドを開始します。 */
	stdin_event = CreateEvent(NULL, TRUE/*手動リセット*/, TRUE/*シグナル状態*/, NULL);	/* 標準入力バッファが空であることを示す、手動リセットイベント。初期状態で標準入力バッファは空であるので、シグナル状態とする。 */
	if(!stdin_event) {
		fprintf(stderr, "### 標準入力バッファが空であることを示す、手動リセットイベントの作成に失敗しました。\n");
		goto EXIT;
	}
	retval = _beginthread(stdin_routine, 0, NULL);
	if(retval == -1) {
		fprintf(stderr, "### 標準入力スレッドの作成に失敗しました。\n");
		goto EXIT;
	}

	/* 標準出力スレッドを開始します。 */
	stdout_event = CreateEvent(NULL, TRUE/*手動リセット*/, FALSE/*非シグナル状態*/, NULL);	/* 標準出力バッファが空でないことを示す、手動リセットイベント。初期状態で標準出力バッファは空であるので、非シグナル状態とする。 */
	if(!stdout_event) {
		fprintf(stderr, "### 標準出力バッファが空でないことを示す、手動リセットイベントの作成に失敗しました。\n");
		goto EXIT;
	}
	retval = _beginthread(stdout_routine, 0, NULL);
	if(retval == -1) {
		fprintf(stderr, "### 標準出力スレッドの作成に失敗しました。\n");
		goto EXIT;
	}

	/*-------------------- メインループ --------------------*/

	/* 終了要求がセットされるまで… */
	while(!terminate) {
		Sleep(0);	/* 気休め */

		/* USBCOMの状態を取得します。 */
		retval = ismUCGetStat(&ucs); /* ismUCGetStat()はシグネチャ部分は読み込みません!!要注意 */
		if(retval != 0) {
			fprintf(stderr, "### USBCOMの状態取得に失敗しました。\n");
			goto EXIT;
		}

		/* P/ECE側は、単にpceUSBCOMStop()を呼び出しで切断する*のではなく*、
		 * 空文字シグネチャでpceUSBCOMSetup()を行い、すぐにpceUSBCOMStop()を呼んで切断してください。
		 * なぜかというと、PCとの接続が確立していると、pceUSBCOMStop()は切断処理を行わないからです。
		 * そのため、pceUSBCOMSetup()を使ってPCとの接続フラグおよびシグネチャをクリアしておき、
		 * あらためてpceUSBCOMStop()を呼び出して切断を行うのです。(BIOS 1.18のふるまいに依存)
		 */
		//if(!ucs.mystat || strcmp(ucs.signature, "stderr") != 0) {
		if(!ucs.mystat/* || strcmp(ucs.signature, "stderr") != 0*/) { /* ismUCGetStat()はシグネチャ部分を読まないので、検査しても無駄でした */
			fprintf(stderr, "### 標準エラー出力が切断されました。\n");
			goto EXIT;
		}

		/* 標準出力バッファが空ならば… */
		if(WaitForSingleObject(stdout_event, 0) == WAIT_TIMEOUT) {	/* 標準出力バッファが空でないことを示す、手動リセットイベントが、非シグナル状態ならば… */
			/* P/ECEからの送信要求があれば... */
			if(ucs.txstat & USBCOM_STAT_TXWAIT/*TXING等のビットを無視するために、このマスク必須！*/) {
				if((ucs.txlen < 1) || (ucs.txlen > sizeof stdout_str - 1/*nul*/)) {
					fprintf(stderr, "### 送信要求サイズが不正です。(%d)\n", ucs.txlen);
					goto EXIT;
				}
				/* P/ECEからの送信データを受信します。 */
				retval = ismUCRead((char*)stdout_str, ucs.txlen);	/* キャストはvolatileの警告回避のため */
				if(retval != 0) {
					fprintf(stderr, "### データ受信エラーです。\n");
					goto EXIT;
				}
				/* P/ECEからの送信データはnul終端されていないので、nul終端を追加する。 */
				stdout_str[ucs.txlen] = '\0';
				/* 手動リセットイベントをセットして、標準出力バッファが空でなくなったことを示す。 */
				SetEvent(stdout_event);
			}
		}

		/* 標準入力バッファが空でなければ… */
		if(WaitForSingleObject(stdin_event, 0) == WAIT_TIMEOUT) {	/* 標準入力バッファが空であることを示す、手動リセットイベントが、非シグナル状態ならば… */
			/* P/ECEからの受信要求があれば... */
			if(ucs.rxstat & USBCOM_STAT_RXWAIT/*RXING等のビットを無視するために、このマスク必須！*/) {
				if((ucs.rxlen < 1) || (ucs.rxlen > sizeof stdin_str)) {
					fprintf(stderr, "### 受信要求サイズが不正です。(%d)\n", ucs.rxlen);
					goto EXIT;
				}
				/* 文字列をP/ECEへ送信します。 */
				retval = ismUCWrite((char*)stdin_str, ucs.rxlen);	/* キャストはvolatileの警告回避のため */
				if(retval != 0) {
					fprintf(stderr, "### データ送信エラーです。\n");
					goto EXIT;
				}
				/* 手動リセットイベントをセットして、標準入力バッファが空になったことを示す。 */
				SetEvent(stdin_event);
			}
		}
	}

	/*-------------------- クリーンアップ --------------------*/

	/* 終了コードを成功とする。 */
	exit_code = EXIT_SUCCESS;
EXIT:
	/* 確実に切断します。 */
	disconnect();

	return exit_code;
}

