/*
 *	app.c
 *
 *	dpavidot4.exe - AVIドット抽出ツール
 *	Copyright (C) 2015-2017 Naoyuki Sawa
 *
 *	* Thu Nov 12 21:37:40 JST 2015 Naoyuki Sawa
 *	- 1st リリース。
 *	  '/clip/tool/dpavidot/Sample/4_dLedSeqC用data出力/LedConv4.bat'と同等の処理を行うツールです。
 *	  上記のツールを置き換える事にした理由は、以下の通りです。
 *	- dpavidot.exeは汎用性のため、Luaスクリプトを呼び出して処理を行います。
 *	  上記のツールの場合、dpavidot.exeとLedConv4.luaとLedConv4.batが必要です。
 *	  複数のファイルが必要で、利用環境へ持っていく時に不便が生じたり、一部のファイルの更新忘れで正しく動作しない危険が有ります。
 *	- dpavidot.exeは汎用性のために上記の仕様としたのですが、実際のところ、ほとんどLedConv4.luaとLedConv4.batとの組み合せで使う事が大半でした。
 *	  具体的には、dLedSeqC.exe用のLEDシーケンサスクリプトの'data'部分の作成用として使用する事がほとんどです。
 *	  そこで、その機能に特化して、単体のexeとして作成する事にしました。
 *	- 今後は、上記の目的のためには、LedConv4.batではなく、dpavidot4.exeを使用して下さい。
 *	  LedConv4.batを使用しても同じ結果となるのますが、今後、LedConv4.luaとLedConv4.batは利用環境のツールフォルダから削除する可能性が有ります。
 *	- AVIに対して別の用途の変換を行う場合など、汎用性が必要なケースでは、これまで通りdpavidot.exeを使用して下さい。
 *	* Sun Feb 21 21:02:01 JST 2016 Naoyuki Sawa
 *	- libclipのライブラリ化の伴い、以下の変更を行いました。
 *	  ・extclip.hとextclip.cを削除しました。extclip.hとextclip.cはlibclipに含まれるようになったからです。
 *	  ・"CLiP Files"を削除しました。CLiPライブラリの各モジュールはlibclipに含まれるようになったからです。
 *	  当ツールの動作には変更有りません。
 *	* Tue May 09 22:14:45 JST 2017 Naoyuki Sawa
 *	- ライブラリのバグ修正に伴う、ツールのリビルドを行いました。
 *	  詳細は、clipmisc.cの'Sun May 07 09:13:01 JST 2017 Naoyuki Sawa'のコメントを参照して下さい。
 *	- 前回更新後〜本日までの間に、他のツール群に対して行ったのと同じビルド設定の変更を、当ツールにも対しても適用しました。
 */
#include "app.h"
#define VERSION		"20170509"	//最終更新日
/****************************************************************************
 *	
 ****************************************************************************/
__declspec(noreturn) static void usage();
static int subr(int argc, char* argv[]);
/****************************************************************************
 *	main
 ****************************************************************************/
int main(int argc, char* argv[]) {
	int exitCode;
#ifdef  GC_H
//	putenv("GC_LOG_FILE=CON");//ログ出力先を指定する。
//	GC_set_find_leak(1);//メモリリーク検出を開始する。
#endif//GC_H
//{{2017/05/09削除:GC_init(),及び,GC_allow_register_threads()の呼び出しを削除しました。GC_init(),及び,GC_allow_register_threads()は、だいぶん前から、必要に応じてライブラリ内のextclip.cのGC_threads_init()で行われるようになっているので、アプリケーション側での明示的な呼び出しはもう不要です。
//#if     (defined(GC_H) && defined(PTHREAD_H))
//	GC_init();//この中でGCスレッドが起動する。メインスレッドをpthread_exit()等で終了するとGCスレッドが残ってプロセスが終了しないので注意せよ。
//	GC_allow_register_threads();
//#endif//(defined(GC_H) && defined(PTHREAD_H))
//}}2017/05/09削除:GC_init(),及び,GC_allow_register_threads()の呼び出しを削除しました。GC_init(),及び,GC_allow_register_threads()は、だいぶん前から、必要に応じてライブラリ内のextclip.cのGC_threads_init()で行われるようになっているので、アプリケーション側での明示的な呼び出しはもう不要です。
#ifdef  __CLIP_SEH_H__
	SEH_init();
#endif//__CLIP_SEH_H__
#ifdef  __CLIP_REG_H__
//	if(!(TBL_RegTbl = LockResource(LoadResource(NULL, FindResource(NULL, "RegTbl", "RegTbl"))))) { DIE(); }
#endif//__CLIP_REG_H__
#ifdef  __CLIP_PRP_H__
//	if(!(pPrpKey = REG_open_key(TBL_RegTbl, RegKey_PrpTbl))) { DIE(); }
//	PrpHdr_init(phApp, pPrpKey, PrpTbl_App);
#endif//__CLIP_PRP_H__
	exitCode = subr(argc, argv);
#ifdef  GC_H
//	CHECK_LEAKS();//メモリリークを検出する。
#endif//GC_H
//	cputs("ok");
//	getch();
	return exitCode;
}
/****************************************************************************
 *	usage
 ****************************************************************************/
__declspec(noreturn) static void usage() {
	fprintf(stderr, "dpavidot4.exe - AVIドット抽出ツール (%s)\n", VERSION);
	fprintf(stderr, "Copyright (C) 2015-2017 Naoyuki Sawa All Rights Reserved.\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "USAGE:\n");
	fprintf(stderr, "    dpavidot4.exe [option ...] movie.avi [movie.avi ...]\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "OPTIONS:\n");
	fprintf(stderr, "  -i spec.txt  ドット定義をファイルから読み込みます。\n");
	fprintf(stderr, "               このオプションを指定しなければ、ドット定義を標準入力から読み込みます。\n");
	fprintf(stderr, "  -o data.txt  出力データをファイルに出力します。\n");
	fprintf(stderr, "               このオプションを指定しなければ、出力データを標準出力に出力します。\n");
	fprintf(stderr, "  -q           進捗を表示しません。\n");
	fprintf(stderr, "  -v           出力データにコメントを付与します。\n");
	fprintf(stderr, "  -h           使い方を表示します。\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "EXAMPLE:\n");
	fprintf(stderr, "  dpavidot4.exe -iLedSpec.txt -oLedData.txt Movie1.avi Movie2.avi Movie3.avi\n");
	fprintf(stderr, "    ドット定義をLedSpec.txtから読み込みます。\n");
	fprintf(stderr, "    Movie1.avi+Movie2.avi+Movie3.aviを、連結した一本のAVI相当と見なして読み込みます。\n");
	fprintf(stderr, "    LEDシーケンサスクリプトの'data'部分を作成し、LedData.txtに出力します。\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "FORMAT:\n");
	fprintf(stderr, "  □ドット定義の行書式は、以下の通りです。\n");
	fprintf(stderr, "  │^([[:blank:]]*([[:digit:]]+)[[:blank:]]+([[:digit:]]+)[[:blank:]]+([[:digit:]]+)[[:blank:]]*)?(#.*)?$\n");
	fprintf(stderr, "  │              |<--Ｘ座標-->|            |<--Ｙ座標-->|            |<--色種別-->|              |ｺﾒﾝﾄ|\n");
	fprintf(stderr, "  │  X座標   0〜(AVIの幅-1)\n");
	fprintf(stderr, "  │  Y座標   0〜(AVIの高さ-1)\n");
	fprintf(stderr, "  │  色種別  1=単色,3=フルカラー\n");
	fprintf(stderr, "  □<例>\n");
	fprintf(stderr, "  │ 50 100 1 #//No.0   単色ﾄﾞｯﾄ\n");
	fprintf(stderr, "  │150 200 3 #//No.1~3 ﾌﾙｶﾗｰﾄﾞｯﾄ\n");
	fprintf(stderr, "  │123 456 1 #//No.4   単色ﾄﾞｯﾄ\n");
	fprintf(stderr, "  │ 78  90 3 #//No.5~7 ﾌﾙｶﾗｰﾄﾞｯﾄ\n");
	exit(EXIT_FAILURE);
}
/****************************************************************************
 *	subr
 ****************************************************************************/
#define MAX_DOT 65536	//ドット数の上限(調整可)
typedef struct _ST_DOT {
	int	x;	//X座標		0〜(AVIの幅-1)
	int	y;	//Y座標		0〜(AVIの高さ-1)
	int	c;	//色種別	1=単色,3=フルカラー
} ST_DOT;
static ST_DOT TBL_DOT[MAX_DOT];	//ドット定義
static nDOT;
/*--------------------------------------------------------------------------*/
static int subr(int argc, char* argv[]) {
	char *opt_i = NULL, *opt_o = NULL;
	int opt_q = 0, opt_v = 0;
	{
		int opt;
		opterr = 0; //オプションエラーは当プログラム内で処理するので、getopt()内でのエラー表示は不要。
		while((opt = getopt(argc, argv, "i:o:qvh")) != -1) {	//コマンドラインオプションを処理する。
			switch(opt) {
			case 'i':
				if(opt_i) { usage(); }	//二重指定エラー
				opt_i = optarg;
				break;
			case 'o':
				if(opt_o) { usage(); }	//二重指定エラー
				opt_o = optarg;
				break;
			case 'q':
				opt_q = 1;
				break;
			case 'v':
				opt_v = 1;
				break;
			case 'h':
			default:
				usage();
			}
		}
		if(optind >= argc) { usage(); }	//AVIファイル名が一つも指定されていないエラー
	}
	{
		char* glbuf = NULL;	//┬getline()用バッファ
		int   glcap = 0;	//┘
		FILE* fp = stdin;	//'-i'が指定されていなければ、標準入力からドット定義を読み込む。
		if(opt_i) {		//'-i'が指定されていたら、ファイルからドット定義を読み込む。
			fp = fopen(opt_i, "r");
			if(!fp) { error(EXIT_FAILURE, errno, "%sが開けません。", opt_i); }
		}
		{
			int iLine = 0;
			while(getline(&glbuf, &glcap, fp) != EOF) {	//一行読み込む。ファイル終端でなければ…
				char* p;
				int x, y, c;
				//行番号を進める。
				iLine++;
				//^([[:blank:]]*([[:digit:]]+)[[:blank:]]+([[:digit:]]+)[[:blank:]]+([[:digit:]]+)[[:blank:]]*)?(#.*)?$
				//              |<--Ｘ座標-->|            |<--Ｙ座標-->|            |<--色種別-->|              |ｺﾒﾝﾄ|
				//'#'以降をコメントと見なして切り捨てる。
				p = strchr(glbuf, '#');
				if(p) { *p = '\0'; }
				//^([[:blank:]]*([[:digit:]]+)[[:blank:]]+([[:digit:]]+)[[:blank:]]+([[:digit:]]+)[[:blank:]]*)?$
				//              |<--Ｘ座標-->|            |<--Ｙ座標-->|            |<--色種別-->|
				//先頭と末尾の空白を切り捨てる。
				strstrip(glbuf);
				//^(([[:digit:]]+)[[:blank:]]+([[:digit:]]+)[[:blank:]]+([[:digit:]]+))?$
				//  |<--Ｘ座標-->|            |<--Ｙ座標-->|            |<--色種別-->|
				//空行ならば飛ばす。
				if(!*glbuf) { continue; }
				//^([[:digit:]]+)[[:blank:]]+([[:digit:]]+)[[:blank:]]+([[:digit:]]+)$
				// |<--Ｘ座標-->|            |<--Ｙ座標-->|            |<--色種別-->|
				if(!(p = strtok(glbuf, " \t")) || ((x = strtoul(p, &p, 10)), *p)) { error(EXIT_FAILURE, 0, "%s(%d): X座標が読み取れません。",      (opt_i ? opt_i : "-"), iLine); }
				if(!(p = strtok( NULL, " \t")) || ((y = strtoul(p, &p, 10)), *p)) { error(EXIT_FAILURE, 0, "%s(%d): Y座標が読み取れません。",      (opt_i ? opt_i : "-"), iLine); }
				if(!(p = strtok( NULL, " \t")) || ((c = strtoul(p, &p, 10)), *p)) { error(EXIT_FAILURE, 0, "%s(%d): 色種別が読み取れません。",     (opt_i ? opt_i : "-"), iLine); }
				if(      strtok(NULL,  " \t")                                   ) { error(EXIT_FAILURE, 0, "%s(%d): ドット定義の書式が不正です。", (opt_i ? opt_i : "-"), iLine); }
				if((c != 1/*単色*/) && (c != 3/*フルカラー*/))                    { error(EXIT_FAILURE, 0, "%s(%d): 色種別が不正です。",           (opt_i ? opt_i : "-"), iLine); }
				if(nDOT >= MAX_DOT)                                               { error(EXIT_FAILURE, 0, "%s(%d): ドット数が上限を超えました。", (opt_i ? opt_i : "-"), iLine); }
				TBL_DOT[nDOT].x = x;
				TBL_DOT[nDOT].y = y;
				TBL_DOT[nDOT].c = c;
				nDOT++;
			}
			if(!nDOT) { error(EXIT_FAILURE, 0, "%s(%d): ドット定義が有りません。", (opt_i ? opt_i : "-"), iLine); }
		}
		if(opt_i) { fclose(fp); }	//ファイルからドット定義を読み込んだ場合は、ファイルを閉じる。
		free(glbuf);	//getline()用バッファを開放する。
	}
	{
		FILE* fp = stdout;	//'-o'が指定されていなければ、標準出力にデータを出力する。
		if(opt_o) {		//'-o'が指定されていたら、ファイルにデータを出力する。
			fp = fopen(opt_o, "w");
			if(!fp) { error(EXIT_FAILURE, errno, "%sが作成出来ません。", opt_o); }
		}
		if(isatty(fileno(fp))) { opt_q = 1; }	//データ出力先がターミナルならば(即ち'-o'が指定されておらずリダイレクトもされていなければ)、暗黙に進捗表示を抑制する事にしました。進捗表示とデータ出力が混ざって見辛いからです。実際にはデータを標準出力に出力するような使い方はしないと思います。
		//VFWを初期化する。
		AVIFileInit();
		//各AVIファイルについて…
		do {
			/*static const*/ BITMAPINFOHEADER biWanted = {	//AVIStreamGetFrameOpen()が非constポインタを要求するので仕方無く…
				sizeof(BITMAPINFOHEADER),	//biSize
				0,	//biWidth	⇒ 指定しない
				0,	//biHeight	⇒ 指定しない
				1,	//biPlanes
				24,	//biBitCount
			//	BI_RGB,	//biCompression	⇒ BI_RGB=0なので明示不要
			};
			const char* const	pAviFileName = argv[optind];
			PAVIFILE		pAviFile;
			PAVISTREAM		pAviStream;
			AVISTREAMINFO		AviStreamInfo;
			PGETFRAME		pGetFrame;
			BITMAPINFOHEADER*	pBitmapInfoHeader;
			HRESULT			hr;
			//AVIファイルを開く。
			hr = AVIFileOpen(&pAviFile, pAviFileName, OF_READ|OF_SHARE_DENY_WRITE, NULL);
			if(hr) { error(EXIT_FAILURE, 0, "%s: AVIFileOpenが失敗しました。", pAviFileName); }
			hr = AVIFileGetStream(pAviFile, &pAviStream, streamtypeVIDEO, 0);
			if(hr) { error(EXIT_FAILURE, 0, "%s: AVIFileGetStreamが失敗しました。", pAviFileName); }
			hr = AVIStreamInfo(pAviStream, &AviStreamInfo, sizeof AviStreamInfo);
			if(hr) { error(EXIT_FAILURE, 0, "%s: AVIStreamInfoが失敗しました。", pAviFileName); }
			pGetFrame = AVIStreamGetFrameOpen(pAviStream, &biWanted);	//&biWantedの代りにNULLを指定して、無圧縮RGB以外にデコードされた場合は自力で解釈する方法も有るが、簡単のために、無圧縮RGBにデコード出来ない場合はエラーとすることにした。
			if(!pGetFrame) { error(EXIT_FAILURE, 0, "%s: AVIStreamGetFrameOpenが失敗しました。", pAviFileName); }
			//各フレームについて…
			if(opt_v) { fprintf(fp, "#//%s\n", pAviFileName); }	//コメント付加
			{
				int per1 = -1;	//進捗表示用
				int iFrameNumber, nLineBytes, iDOT;
				const uint8_t *pPixelData;
				for(iFrameNumber = 0; iFrameNumber < (int)AviStreamInfo.dwLength; iFrameNumber++) {
					if(!opt_q) {											//進捗表示用
						int per2 = (iFrameNumber + 1) * 100 / AviStreamInfo.dwLength;				//進捗表示用
						if(per1 != per2) { fprintf(stderr, "%s: %3d%%\r", pAviFileName, (per1 = per2)); }	//進捗表示用
					}												//進捗表示用
					//1フレーム分をデコードする。
					pBitmapInfoHeader = AVIStreamGetFrame(pGetFrame, iFrameNumber);
					if(!pBitmapInfoHeader) { error(EXIT_FAILURE, 0, "%s(%d): AVIStreamGetFrameが失敗しました。", pAviFileName, iFrameNumber); }
					//1ラインのバイト数を求める。
					nLineBytes = (pBitmapInfoHeader->biWidth * (pBitmapInfoHeader->biBitCount / 8) + 3) & ~3;
					//ピクセルデータへのポインタを求める。
					pPixelData = (uint8_t*)pBitmapInfoHeader + pBitmapInfoHeader->biSize;
					pPixelData += nLineBytes * (pBitmapInfoHeader->biHeight - 1);	//ボトムアップ形式で、(x,y)=(0,0)を指す
					//各ドットについて…
					fputs("data ", fp);
					for(iDOT = 0; iDOT < nDOT; iDOT++) {
						int x = TBL_DOT[iDOT].x;
						int y = TBL_DOT[iDOT].y;
						int c = TBL_DOT[iDOT].c;
						const uint8_t* p;
						int v[3/*R,G,B*/], i;
						//ドットの輝度を取得する。
						if((unsigned)x >= (unsigned)pBitmapInfoHeader->biWidth ) { error(EXIT_FAILURE, 0, "%s(%d): X座標(%d)が範囲外です。", pAviFileName, iFrameNumber, x); }
						if((unsigned)y >= (unsigned)pBitmapInfoHeader->biHeight) { error(EXIT_FAILURE, 0, "%s(%d): Y座標(%d)が範囲外です。", pAviFileName, iFrameNumber, y); }
						p = pPixelData - (nLineBytes * y) + (3 * x);
						v[2/*B*/] = *p++;	//┐
						v[1/*G*/] = *p++;	//├ピクセルデータのバイト順は{B,G,R}です。
						v[0/*R*/] = *p++;	//┘
						//単色ならば(i=0)について,フルカラーならば(i=0,1,2)について…
						for(i = 0; i < c; i++) {
							int w = 0;
							//一要素でも0でなければ、不透明と見なす。
							if(v[0/*R*/] || v[1/*G*/] || v[2/*B*/]) {
								switch(c) {
								default:DIE();	//バグ
								case 1://単色
									                  { w = v[0/*R*/]; }	//┐
									if(v[1/*G*/] > w) { w = v[1/*G*/]; }	//├単色ならば、R,G,Bのうちの最大輝度を採用する。((r*0.30)+(g*0.59)+(b*0.11))で計算すると、AVIが単色に相当する色(赤など)で作成されていた場合に100%にならず、デザイナの意図とずれる問題が生じるからです。
									if(v[2/*B*/] > w) { w = v[2/*B*/]; }	//┘
									break;
								case 3://フルカラー
									w = v[i];
									break;
								}
								w = (int)floor((double)w * ((double)LedSeq_Dimmer_Max / (double)UINT8_MAX)) + 1;	//[0,255]を[1,31]へ均等に変換する。
								if(w > LedSeq_Dimmer_Max) { w = LedSeq_Dimmer_Max; }	//変換前が(w=255)だった場合のみ変換結果が32になるので31に補正する。
							}
							if(iDOT || i) { fputc(',', fp); }
						    #if   (LedSeq_Dimmer_Max <=  9)
							fprintf(fp, "%1d", w);
						    #elif (LedSeq_Dimmer_Max <= 99)
							fprintf(fp, "%2d", w);
						    #else
							fprintf(fp, "%3d", w);
						    #endif
						}
					}
					if(opt_v) { fprintf(fp, "\t#//%d", iFrameNumber); }	//コメント付加
					fputc('\n', fp);
				}
				if(!opt_q) { fprintf(stderr, "\n"); }	//進捗表示用
			}
			//AVIファイルを閉じる。
			hr = AVIStreamGetFrameClose(pGetFrame);
			if(hr) { error(EXIT_FAILURE, 0, "%s: AVIStreamGetFrameCloseが失敗しました。", pAviFileName); }
			hr = AVIStreamRelease(pAviStream);
			if(hr) { error(EXIT_FAILURE, 0, "%s: AVIStreamReleaseが失敗しました。", pAviFileName); }
			hr = AVIFileRelease(pAviFile);
			if(hr) { error(EXIT_FAILURE, 0, "%s: AVIFileReleaseが失敗しました。", pAviFileName); }
		} while(++optind < argc);
		if(opt_o) { fclose(fp); }	//ファイルにデータを出力した場合は、ファイルを閉じる。
		//VFWを終了する。
		AVIFileExit();
	}
	return EXIT_SUCCESS;
}
