using System;
using System.IO;
using System.Collections.Generic; // For List<>
using System.Linq; // For Enumration
using System.Reflection; // Assembly.
using System.Text.RegularExpressions;

using System.Threading;
using System.Threading.Tasks;

using System.Runtime.Serialization;
using System.ServiceModel;				// for WCF
using System.Diagnostics;               // for Process

// no need
//using System.Runtime.InteropServices;

// 作成クラス
using CSRender;
using CSRender.UtHash;
using static CSRender.RenderPDF;

//  *重要* デスクトップアプリで UWP Api を呼び出す  プロジェクトの設定方法が記載されている
// https://docs.microsoft.com/ja-jp/windows/apps/desktop/modernize/desktop-to-uwp-enhance
// 参照はwindows. winmd	:C:\Program Files (x86) \Windows Kits\10\UnionMetadata\<sdk バージョン>/ファサード


namespace CSRenderMain {
	[DataContract]
	//[Serializable()]
	public class ParamData {
		[DataMember]	
		public string pdfPath = "";     //対象ファイル
		[DataMember]
		public string pdfPathRef = "";       //比較ファイル
		[DataMember]
		public string outuptImageDir = "";      // /O
        [DataMember]
        public string dpi = "72.0";   // /D 
        [DataMember]
        public int para = 4;		// 並行数(プロセス数) 
		[DataMember]
		public int paraPage = 4;	// 並行数(page処理スレッド) 
		[DataMember]
		public string boxSelect = "Crop";   // /B<x>
		[DataMember]
		public string pageRange = "1-*";    // /P
		[DataMember]
		public string mode = "page";   // /MODE
		[DataMember]
		public string imageType = "JPG";    // /JPG or /PNG
		[DataMember]
		public string jpegQ = "91";     // JPEGQ 
		[DataMember]
		public bool bHash = false;    // ハッシュ値を生成する
		[DataMember]
		public bool bMkHash = false;    // ハッシュファイルを作成する
		[DataMember]
		public bool bFC = false;
		[DataMember]
		public string resultPath = "";
		[DataMember]
		public bool bPDFium = true;
		//
		[DataMember]
		public bool bExeSepa = true;// 実行分離
		[DataMember]
		public string subExe = null;// "Sub"が指定されたら、処理をExe分離する。内部コマンド
									// データはWCFの通信データを使う。引数はPipeName:PortAddressNameとする
		[DataMember]
		public bool bVerbose = false;// 詳細デバッグ

		public ParamData Clone() {
			return (ParamData)MemberwiseClone();
		}
	}	
	public class Program {
		static void DispHelpNoArg() {
			var asmName = Assembly.GetExecutingAssembly().GetName();
			var pgName = asmName.Name;// "アセンブリ名"
			var v1 = asmName.Version; // "1.0.0.0"
// 逐語的文字列リテラル $@, ダブルクウォートは２つでエスケープされる
// HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
			Console.Write( $@"
Version={ v1}
{pgName} [/<Opts>] <PDFPath or PDFDir>          :Rendering(to JPEG,TIFF,...)
{pgName} /MkHash [/<Opts>] <PDFPath or PDFDir>  :Make Imaged hash keys
{pgName} /FC [/<Opts>] <TgtPDF Dir> <RefPDF Dir>:Compare files

Render of PDF file.available 3 command mode:[Basic Rendering] [Make Hash command] [Compare command], and[Render Options]
	PDFの画像化は[Basic Rendering]。
	比較は/MkHash([Make Hash command]) 後に/FC([Compare command]) で高速実行できます。
For more information,see /H /? or /?
");
// HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
		}
		static void DispHelpDetail() {
            var asmName = Assembly.GetExecutingAssembly().GetName();
            var pgName = asmName.Name;// "アセンブリ名"
            var v1 = asmName.Version; // "1.0.0.0"
			DispHelpNoArg();
// 逐語的文字列リテラル $@, ダブルクウォートは２つでエスケープされる
// HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
			Console.Write($@"
[Basic Rendering] 基本的なレンダリング
	{pgName} [/<Render Options>] <PDFPath|PDFDir>
	/F <pdfPath|pdfDir> : pdfPath(pdfファイル名|ディレクトリ) /Fは省略可能
	/O|Output <output directory> : 出力ディレクトリ。
        省略時はPDF指定の場合は同一階層の"".IMG""フォルダ。
        ディレクトリ指定の場合は""<ディレクトリ名>.IMG""、差分画像は""<ディレクトリ名>.DiffImage""。

[Render Options] レンダリングオプション
	/D|Dpi <解像度> : 解像度指定 9 - 300dpi(default=72dpi)
	/JPG,/JPEG,/PNG,/TIF,/TIFF,/GIF,/BMP: Select one output format.(default=/JPG)
	/JPEGQ <quality>: Jpegの品質指定1-100(default=91)

	/P|Page <PageRange> : ページの範囲を指定する(省略時は全ページ)
		連続した範囲を指定する場合は、ハイフン（'-'）を用いる。終了側を省略すると最終pageまで。
		複数のページを指定する場合は、カンマ（','）を用いる
		Ex. /P ""1,2,30-100"" //1,2pages and 30-100pages.
	[Unsupport] 未対応↓
	/L <input List text> : 入力PDFファイルリスト(*unsupport)
	/T  <tempPath> : テンポラリフォルダを指定（省略時は出力先フォルダと同じ(*unsupport no need)
	/OP <0|1> : オーバープリントのOn/Off (省略時は1)(*unsupport allways on[1])
	/U <0|1> : 同名上書き設定 0:上書きしない 1:上書き(*unsupport allways overwrite[1])
	/OFFSET <X> <Y> : ミリ単位でオフセットを指定する（省略時は共に0mm)(*unsupport)
	[Obs]/PDFium <0or1>: GoogoleのPDFiumViewerエンジンを使用する(default=1>
	[未]/BM,/BT,/BA,/BA,/BC: Select one box.(default=/BC:CrobBox): Boxies:MediaBox/BleedBox/TrimBox/ArtBox/CropBox

[Make Hash command] 比較用ハッシュ値作成コマンド
	{pgName} /MkHash ...<Render Options>... <PDF|PDF dir>
	/MKHash : ハッシュ値を出力する。前記の[Render Options]を指定すること

[Compare command] 比較コマンド
	{pgName} /FC ...<Render Options>... [/F]<Target PDF|PDF dir> [/Ref|Refernce] <Reference PDF|PDF dir>
	/FC	: 2つのPDFを比較する。前記の[Render Options]を指定すること。無名引数が2つ必要です
		  事前に/MkHashを実行しておくことで高速に処理できる
	/Ref|Reference <<Reference PDF|PDF dir>> : 比較ファイル指定 
	/Result <result file> : 比較結果を格納するファイルパス
		/FCコマンドを指定すると一致したら0，不一致なら1を返却するようになる
		<result file>は、<pageNum(1始まり)>,<[OK] or [@Difference]>の行で構成される

[ELSE ] その他のオプション
	/Para     <プロセス並行数>:本Exeの並行数を指定（デフォルト4)
	/ParaPage <ページ処理スレッド数>:ページ処理のスレッド数を指定（デフォルト4)
	[obs]/NoExeSepa :実行分離しない(遅い)

/H or /?  : This help");
// HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
		}

		/// <summary>
		/// PDF render main
		/// </summary>
		/// <param name="args"></param>
		/// <returns>0 正常終了</returns>
		static int Main(string[] args) {
			var argsStr = String.Join(@" ", args);
			var pm = new ParamData();   // 引数の保持
            bool bDirMode = false;      // ディレクト指定の場合
			string[] pdfPathLst = null;
			string[] pdfPathLst2 = null;
			var pdfPathLstBoth = new List<string>();
			var pdfPathLstNoBoth = new List<string>();
			//ハッシュ関係
			string hashBaseName = "RenderHash";
			
            // No CSRender.Check.GetDPI();

            // 未使用
            //{// ベースハッシュ値の計算テスト
            //	var streamPDF = ResData.GetBaseHashPDF();
            //	RenderPDF.RenderPdfInitCheck(streamPDF);
            //}

			var logicalProcessNum = Environment.ProcessorCount;

			if ( logicalProcessNum >= 10 ) {// 元々 4(exe)+4(page)なので＋2 = プロセス数が10以上の時にexe数を増やす
				pm.para = logicalProcessNum - pm.paraPage;
			}
            /*
             * ↓引数解析
             * */
            var qu = new Queue<string>(args); // 引数をQueに登録 (qu.Enqueue(a)でも可能)
			while (qu.Count > 0) {
				// 引数のパース 
				var wd = qu.Dequeue();  // 取り出しqu.Dequeue()で次の要素を取得する
				if (wd.First() == '-') {
					// 先頭-(ハイフンもオプション扱いにする)→"/"に置換
					wd = Regex.Replace(wd, @"^\-", "/");
				}
				//大文字小文字無視でオプションチェック
				var eIgnoreCase = StringComparer.OrdinalIgnoreCase;
				// オプションチェックローカル関数
				bool isOpt(params string[] opts) => opts.Contains<string>(wd, eIgnoreCase);
				// ボックスオプション辞書
				var BoxSelOptDic = new Dictionary<string, string>(eIgnoreCase) { ["/BM"] = "Media", ["/BT"] = "Trim",
					["/BB"] = "Bleed", ["/BC"] = "Crop",
					["/BA"] = "Art"
				};
				if (isOpt("/?", "/H","/Help")) {
					DispHelpDetail();
					return -1;
				} else if (isOpt("/F")) {
					pm.pdfPath = (qu.Count > 0) ? qu.Dequeue() : "";// next word.
				} else if (isOpt("/Ref","/Reference")) {
					pm.pdfPathRef = (qu.Count > 0) ? qu.Dequeue() : "";// next word.
				} else if (isOpt("/O","/Output")) {
					pm.outuptImageDir = (qu.Count > 0) ? qu.Dequeue() : "";// next word.
				} else if (isOpt("/D","/DPI")) {
					pm.dpi = (qu.Count > 0) ? qu.Dequeue() : "";// next word.
					if (!double.TryParse(pm.dpi, out double dmy)) {
						Console.WriteLine($"解像度が不正です:/D {pm.dpi}");
						DispHelpNoArg();
						return -1;
					}
                } else if (isOpt("/Para")) {
                    string paraNum = (qu.Count > 0) ? qu.Dequeue() : "";// next word.
                    if (!int.TryParse(paraNum, out pm.para)) {
                        Console.WriteLine($"並行数が不正です:/para {paraNum}");
						DispHelpNoArg();
                        return -1;
                    }
				} else if (isOpt("/ParaPage")) {
					string paraNum = (qu.Count > 0) ? qu.Dequeue() : "";// next word.
					if (!int.TryParse(paraNum, out pm.paraPage)){
						Console.WriteLine($"並行数(ページスレッド数が不正です:/ParaPage {paraNum}");
						DispHelpNoArg();
						return -1;
					}
				} else if (isOpt("/P","/Page")) {
					pm.pageRange = (qu.Count > 0) ? qu.Dequeue() : "";// next word.
				} else if (isOpt("/JPG", "/JPEG")) {
					pm.imageType = "JPG";
				} else if (isOpt("/PNG")) {
					pm.imageType = "PNG";
				} else if (isOpt("/TIF", "/TIFF")) {
					pm.imageType = "TIFF";
				} else if (isOpt("/GIF", "/GIFF")) {
					pm.imageType = "GIF";
				} else if (isOpt("/BMP")) {
					pm.imageType = "BMP";
				} else if (isOpt("/JPEGQ")) {
					pm.jpegQ = (qu.Count > 0) ? qu.Dequeue() : "";// next word.
					if (!int.TryParse(pm.jpegQ, out int dmy)) {
						Console.WriteLine($"JPEG Qualityが不正です:/JPEGQ {pm.jpegQ}");
						DispHelpNoArg();
						return -1;
					}
					if (!(0 < dmy && dmy <= 100)) {
						Console.WriteLine($"JPEG Qualityが不正です(not 1-100):/JPEGQ {dmy}");
						return -1;
					}
				} else if (isOpt("/M")) {
					pm.mode = (qu.Count > 0) ? qu.Dequeue() : "";// next word.
				} else if (BoxSelOptDic.ContainsKey(wd)) {
					pm.boxSelect = BoxSelOptDic[wd];// "/BT" -> "Trim",...
				} else if (isOpt("/HASH")) {
					pm.bHash = true;
				} else if (isOpt("/MKHASH")) {
					pm.bMkHash = true;
					pm.bHash = true;
				} else if (isOpt("/FC")) {
					pm.bFC = true;
				} else if (isOpt("/PDFium")) {
					pm.bPDFium = true;
					var flgStr = (qu.Count > 0) ? qu.Dequeue() : "";// next word.
					if (!int.TryParse(flgStr, out int dmy)) {
						Console.WriteLine($"PDFiumフラグが不正です:/PDFium {flgStr}");
						return -1;
					}
					if (dmy == 0) {
						pm.bPDFium = false;
					} else if (dmy == 1) {
						pm.bPDFium = true;
					} else {
						Console.WriteLine($"PDFiumフラグが不正です:/PDFium {flgStr}");
						return -1;
					}
				} else if (isOpt("/Verbose")) {
					var arg = (qu.Count > 0) ? qu.Dequeue() : "True";// next word.
					string[] sel = {"True","1","ON"};
					pm.bVerbose = sel.Contains(arg, eIgnoreCase);
				} else if (isOpt("/SubExe")) {
					pm.subExe = (qu.Count > 0) ? qu.Dequeue() : null;// next word.
					var sp = pm.subExe.Split(':');
					if (sp.Length < 2 ) {
						Console.WriteLine($"SubExe指定は\":\"区切りが必要: {pm.subExe}");
						return -1;
					}
				} else if (isOpt("/NoExeSepa")) {
					pm.bExeSepa = false;
				} else if (isOpt("/RESULT")) {
					pm.resultPath = (qu.Count > 0) ? qu.Dequeue() : "";// next word.
				} else if (wd.First() == '/') {
					// 処理の無いオプションを明示的に無視する
					Console.WriteLine($"Warning::Ignore opt:{wd}");
				} else {
					// 引数をファイル名として取得する
					if (pm.pdfPath == "") {
						pm.pdfPath = wd;
						continue;
					}
					if (pm.bFC && (pm.pdfPathRef == "")) {
						pm.pdfPathRef = wd;// 比較時のみ取得
						continue;
					}
				}
			}
			// ↑引数解析終わり
     
			setEcho(pm.bVerbose);
			if (pm.bVerbose)
				echo("Varbose Mode!");
			//
			bool bSubExe = (pm.subExe != null);// SubExeで起動されている場合。
			xChangeWCFPipe.IXData cltSrv = null;
			if (!bSubExe) {
				/// サーバー側
				echo("++MainProcess start");
				Console.WriteLine($"LogicalProcNum={logicalProcessNum},ProcessParallelTasks={pm.para},PageParallelTasks={pm.paraPage}");
			} else {
				// SubExe側の動作に差し替える
				var sp = pm.subExe.Split(':');
				var pipeName = sp[0]; var  pipeAddr = sp[1];
				cltSrv = xChangeWCFPipeGEN.CLT_T.makeCLT<xChangeWCFPipe.IXData>(pipeName:pipeName,pipeAddress:pipeAddr);
				Ut.Ut.SetAutoSelfKillByMainProcEnd(); // 起動元が終了した場合に自身を終了させる
				pm = cltSrv.GetParam(); // パラメータを書き換える
				setEcho(pm.bVerbose);
				echo("++SubProcess start:" + pm.subExe);
			}

			if (pm.pdfPath == "") {
				Console.WriteLine("pdfファイルが指定されてません");
				DispHelpNoArg();
				return -1;
			}
			if (!(File.Exists(pm.pdfPath) || Directory.Exists(pm.pdfPath))) {
				// ファイルもしくはディレクトも見つからない場合
				Console.WriteLine($"ファイルが存在しません:{pm.pdfPath}");
				return -1;
			}
			var rdCond = new RenderPDF.RenderConditionParams {
				Dpi = double.Parse(pm.dpi),
				ImageType = pm.imageType,
				BoxType = pm.boxSelect,
				JpegQ = int.Parse(pm.jpegQ)
			};
			bool bDir = File.GetAttributes(pm.pdfPath).HasFlag(FileAttributes.Directory);
			if (bDir) {
				// Directoryが指定されたのでファイルリストアップ
				bDirMode = true;
				pdfPathLst = System.IO.Directory.GetFiles(pm.pdfPath, "*.pdf"/*, System.IO.SearchOption.AllDirectories*/);
				pdfPathLst = Array.ConvertAll(pdfPathLst, f => Path.GetFileName(f)); 
				// 配列書き換え（ファイル名のみにする
				// var enumLst  = pdfPathLst.Select(f => Path.GetFileName(f)); LINQ式に置き換えることも可能(返り値は配列ではない）
				//foreach ( var f in pdfPathLst) { 
				//	Console.WriteLine($@"path1={f}");
				//}
				//Console.WriteLine($@"path1.Len={pdfPathLst.Count()}");
			}

			var resultData = new SortedDictionary<int, string>();

			if ((!pm.bFC) && bDir) {
				// 単純レンダリング時 かつ ディレクト時に 対象リストに追加
				foreach (var f in pdfPathLst) {
					pdfPathLstBoth.Add(f);
				}
			}
			if (pm.bFC) {// 比較モード時
				if (pm.pdfPathRef == "") {
					Console.WriteLine("比較モードで2つ目のpdfファイルが指定されてません");
					return -1;
				}
				if (!(File.Exists(pm.pdfPathRef) || Directory.Exists(pm.pdfPathRef))) {
					Console.WriteLine($"比較ファイルが存在しません:{pm.pdfPathRef}");
					return -1;
				}
				bool bDir2 = File.GetAttributes(pm.pdfPathRef).HasFlag(FileAttributes.Directory);
				if (bDir2) {
					if (!bDir) {
						Console.WriteLine($"比較対象はファイルパスでないといけません:{pm.pdfPathRef}");
						return -1;
					}
					// 2つ目のファイルリストアップ
					pdfPathLst2 = System.IO.Directory.GetFiles(pm.pdfPathRef, "*.pdf"/*, System.IO.SearchOption.AllDirectories*/);
					pdfPathLst2 = Array.ConvertAll(pdfPathLst2, f => Path.GetFileName(f));// 配列の書き換え

					//  共通のファイルを見つける。Lstの要素がLst2に含まれているかどうか
					foreach (var f in pdfPathLst) {
						if (pdfPathLst2.Contains(f)) {
							pdfPathLstBoth.Add(f);
						} else {
							pdfPathLstNoBoth.Add("tgt:"+f);// LstがLst2に含まれていない
						}
					}
					// Lst2のみファイルをNoBothに登録
					foreach (var f in pdfPathLst2) {
						if (!pdfPathLstBoth.Contains(f)) {
							pdfPathLstNoBoth.Add("ref:"+f);
						}
					}
					// pdfPathBoth,pdfPathNoBothが作成済み
					if (pdfPathLstNoBoth.Count() != 0) {
						Console.WriteLine($@"不一致のファイル={pdfPathLstNoBoth.Count()}");
						foreach (var f in pdfPathLstNoBoth) {
							Console.WriteLine($@"warning [no match]={f}");
						}
					}
				}
			}
			var watch = System.Diagnostics.Stopwatch.StartNew(); // 時間の生成と計測開始を同時に行う
			var otDir = pm.outuptImageDir;
			if (otDir == "") {
				if ( bDir ) {
					var suffix = pm.bFC ? ".DiffImage": ".IMG";
					otDir = Path.Combine( Directory.GetParent(pm.pdfPath).FullName, Path.GetFileName( pm.pdfPath )+suffix);
					echo($"****Output dir={otDir} ******\n");
					if (!Directory.Exists(otDir) && (!pm.bMkHash) ) { // 
						Directory.CreateDirectory(otDir);
					}
				} else {
					// 出力ディレクトリの作成 // 元PDFの同一フォルダにIMGフォルダを作成する
					otDir = Path.Combine(Directory.GetParent(pm.pdfPath).FullName, "IMG");
				}
				pm.outuptImageDir = otDir;// pmに戻す
			}
			var otHashPath = "";
			var otHashPath2 = "";
			int ret = -1;
			//Console.WriteLine($@"Use bPDFium={pm.bPDFium}");

			if (!bDirMode) {
				if (pm.bFC) {
					//比較モード
					// ハッシュデータの読み込み
					echo($@"pre pdfPath={pm.pdfPath}");
					echo($@"pre pdfPathRef={pm.pdfPathRef}");

					var hashDataTgt = HashData.load(pm.pdfPath, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath);
					var hashDataRef = HashData.load(pm.pdfPathRef, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath2);
					//Console.WriteLine("<Compare Sync(Paralles) Version!>");

					if (pm.resultPath != "") {// 初期化
						File.WriteAllText(Path.GetFullPath(pm.resultPath), "");
					}

					ret = RenderPDF.RenderPdfDocCompare(
							pdfPath: Path.GetFullPath(pm.pdfPath),
							refPdfPath: Path.GetFullPath(pm.pdfPathRef),
							resultDataPath: pm.resultPath,
							inDir		: otDir,
							pm			: rdCond,
							inPageRange	: pm.pageRange,
							inHashDataTgt: hashDataTgt,
							inHashDataRef: hashDataRef,
							bPDFium		: pm.bPDFium,
							nPageThreadNum: pm.paraPage
					);
				} else {
					// Sync version.
					// Console.WriteLine("<Render Sync(Paralles) Version!>");
					// シングル指定では既存のハッシュファイルを読みださない → 単独名のハッシュファイル<pdfFName+".hash")
					// 空のハッシュデータを作成
					var hashDataTgt = new HashData();
					hashDataTgt.Head.SetRenderInfo(pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ);
					//retはページ数
					// rdCond

					ret = RenderPDF.RenderPdfDoc(
							pdfPath			: Path.GetFullPath(pm.pdfPath),
							inDir			: otDir,
							pm				: rdCond,
							inPageRange		: pm.pageRange,
							bSaveImage		: pm.bMkHash ? false : true,// ハッシュ値生成ではイメージ保存しない。
							bHash			: pm.bHash,
							inHashData		: pm.bMkHash ? hashDataTgt : null, //ハッシュコマンドモードのみDataを渡す
							bPDFium			: pm.bPDFium,
							nPageThreadNum	: pm.paraPage
					);
					if (pm.bMkHash) {
						/// ここでMain側に通信データを返せばよい
						if (bSubExe) {
							cltSrv.SetHashData(hashDataTgt);
						} else {
							// 個別のファイル名＋"hash"拡張子で保存します。
							var othashPath = Path.Combine(otDir, Path.GetFileName(pm.pdfPath) + ".hash");
							hashDataTgt.save(othashPath);//ハッシュモードで保存する(Dutyチェックもいるでしょう
														 //不要:hashDataTgt.save(otHashPath);//比較モード,MakeHashに限定すること
														 //表示のみ まだやめておく：
						}
					}
					// retはページ数を返す
				}
			} else {
				// 対象がディレクトリの場合
				echo("For Directory mode");
				HashData hashDataTgt = null, hashDataRef = null;
				hashDataTgt = HashData.load(pm.pdfPath, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath);
				if (pm.bMkHash) {
					hashDataTgt = null;// MKHashの時にはLoadしない → 一旦nullで除去して、otHashPathを利用する
				}
				if (hashDataTgt == null) {
					// 空のハッシュデータを作成
					hashDataTgt = new HashData();
					hashDataTgt.Head.SetRenderInfo(pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ);
				}
				if (pm.bFC) {
					// リファレンス側のハッシュの読み込み
					hashDataRef = HashData.load(pm.pdfPathRef, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath2);
					if (hashDataRef == null) {
						// 空のハッシュデータを作成
						hashDataRef = new HashData();
						hashDataRef.Head.SetRenderInfo(pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ);
					}
				}
				echo($@"Start render:Count={pdfPathLstBoth.Count}");
				//Console.WriteLine("<Render Sync(Paralles) Version!>");[
				var count = pdfPathLstBoth.Count;
				if (pm.bExeSepa &&(!pm.bFC)) { // FCモードは除外します。
					var tokenSource = new CancellationTokenSource();
					ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = pm.para };
					ret = 0;//Success
					var loopResult = Parallel.For(0,pdfPathLstBoth.Count, options, (index,lpState) => {
						//for (var index = 0; index < pdfPathLstBoth.Count; index++) {
						var tgt = Path.Combine(pm.pdfPath, pdfPathLstBoth[index]);
						var tgtID = $@"{index+1}/{pdfPathLstBoth.Count}";

						Console.WriteLine($@"**Remain({count})****tgt({tgtID})={tgt}");
						if (pm.bFC) {
							//比較モード
							var reff = Path.Combine(pm.pdfPathRef, pdfPathLstBoth[index]);
						} else { // Sync version(Paralles).
							var svrData = new xChangeWCFPipe.XData();
							var pmClone = pm.Clone();
							pmClone.pdfPath = tgt;// 引数のターゲットのみを書き換える
							svrData.SetParam(pmClone);
							string guidStr = Guid.NewGuid().ToString("N");
							string pipeName = $"PN_{guidStr}";
							string pipeAddress = $"P_{index}_{guidStr}";

							var svrHost = xChangeWCFPipeGEN.SVR_T.makeSVR(
											baseIf		: typeof(xChangeWCFPipe.IXData), 
											obj			: svrData,
											pipeName	: pipeName,
											pipeAddress	: pipeAddress
										);
							if ( svrHost == null) {
								Console.WriteLine("WCF cannot make");
								//tokenSource.Cancel();// 処理のキャンセル( throwされる）
								lpState.Stop();
								return;
							}
							var myExePath = Assembly.GetExecutingAssembly().Location;
							var proc = Ut.Ut.DoCmd(
									cmdAndArgs: new string[] { myExePath, "/SubExe",$"{pipeName}:{pipeAddress}" }
									, bEcho: false
									, bSameConsole: true
								);
							proc.WaitForExit();
							var retProc = proc.ExitCode;
							echo($@"retProc={retProc}");
							if ( retProc < 1 ) {
								//PDFページ数以外が返った時
								ret = -1;
								Console.WriteLine( $@"Warning ::Cannot get PDFPage:{retProc},tgt={tgt},{argsStr}");
							} else {
								;// 初期値 ret=0; 
							}
							//while (!proc.HasExited) {
							//	System.Threading.Thread.Sleep(2 * 1000);
							//}
							var tmpH = svrData.GetHashData();
							if (tmpH == null) {
								echo("tmpH is null");
							}
							if ( (hashDataTgt!=null)&&(tmpH!=null)) {
								lock(hashDataTgt) {
									//hashDataTgt.Files += tmpH.Files;
									if ( tmpH.Files.Count() < 1 ) {// 中身が空なら警告
										Console.WriteLine($@"Warning tempF.Count={tmpH.Files.Count()}");
									}
									var merged = hashDataTgt.Files
										  .Concat(tmpH.Files.Where(pair =>
											!hashDataTgt.Files.ContainsKey(pair.Key))
										  ).ToDictionary(
											pair => pair.Key,
											pair => pair.Value
										  );
									hashDataTgt.Files = new SortedDictionary<string, HashFile>(merged);// hashを合成した。
								}
							}
							proc.Dispose();
							svrHost.Close();
							svrData = null;
							echo($@"End tgt({tgtID})");
						}
						//GC.Collect();// これでメモリリークが解決した Paraの中ではやめておく
						count--;
					});
					if ( !loopResult.IsCompleted ) {
						Console.WriteLine("Abort!");
						pm.bMkHash = false;// Hash値保存抑制
						ret=-1;
					}
					////tokenSource.Cancel();// 処理のキャンセル
					//if ( tokenSource.IsCancellationRequested ) {
					//	pm.bMkHash = false;// Hash値保存抑制
					//}					
					//}; // Non Para Block
					echo($"********** コマンド終了*****{argsStr} ");

				} else if (pm.bExeSepa &&(pm.bFC)) { // セパのFCモード
					/*
						refのハッシュが存在しなければWCFで作成
						tgtのハッシュは強制的にWCFで作成
					*/
					echo($"****************セパのFCモード******{argsStr}************");
					hashDataRef = HashData.load(pm.pdfPathRef, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath2);
					if ( hashDataRef == null ) {
						var myExePath = Assembly.GetExecutingAssembly().Location;
						var proc = Ut.Ut.DoCmd(
							cmdAndArgs: new string[] { myExePath, "/MkHash",pm.pdfPathRef,"/D",pm.dpi,$"/{pm.imageType }",$"/JPEGQ",pm.jpegQ }
							, bEcho: true
							, bSameConsole: true
						);
						proc.WaitForExit();
						hashDataRef = HashData.load(pm.pdfPathRef, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath2);
						if ( hashDataRef != null ) {
							echo($" OK hash ref");
						}
					} else {
							echo($" OK hash ref(already exist)");
					}
					if (true){
						var myExePath = Assembly.GetExecutingAssembly().Location;
						var proc = Ut.Ut.DoCmd(
							cmdAndArgs: new string[] { myExePath, "/MkHash",pm.pdfPath,"/D",pm.dpi, $"/{pm.imageType }",$"/JPEGQ",pm.jpegQ }
							, bEcho: true
							, bSameConsole: true
						);
						proc.WaitForExit();
						hashDataTgt = HashData.load(pm.pdfPath, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath);
						if ( hashDataTgt != null ) {
							echo($" OK hash tgt");
						}
					}
					echo($@"****************FC開始*****{argsStr}**************");
					if (pm.resultPath != "") {// 初期化
						File.WriteAllText(Path.GetFullPath(pm.resultPath), "");
					}
					int noMatchPageNum=0;
					int noMatchFileNum=0;
					for (var index = 0; index < pdfPathLstBoth.Count; index++) {
						var tgt = Path.Combine(pm.pdfPath, pdfPathLstBoth[index]);
						Console.WriteLine($@"tgt={index+1}/{pdfPathLstBoth.Count}:{tgt}");
						var reff = Path.Combine(pm.pdfPathRef, pdfPathLstBoth[index]);
						ret = RenderPDF.RenderPdfDocCompare(
							pdfPath: Path.GetFullPath(tgt),
							refPdfPath: Path.GetFullPath(reff),
							resultDataPath: pm.resultPath,
							inDir: otDir,
							pm: rdCond,
							inPageRange: pm.pageRange,
							inHashDataTgt: hashDataTgt,
							inHashDataRef: hashDataRef,
							bPDFium: pm.bPDFium,
							nPageThreadNum:pm.paraPage
						);
						if ( ret > 0 ) {
							// retは不一致ページ数
							noMatchFileNum++;
							noMatchPageNum+= ret;
						}
					};
					watch.Stop();

					var SummaryResult = $"\n[結果]\n";
					SummaryResult += $"Args     ={argsStr}\n";
					SummaryResult += $"Target   ={pm.pdfPath}\n";
					SummaryResult += $"Reference={pm.pdfPathRef}\n";
					SummaryResult += $"不整合ファイル群={pdfPathLstNoBoth.Count()}\n";
					foreach (var f in pdfPathLstNoBoth) {
						SummaryResult += $"\tWarning [no match]={f}\n";
					}
					SummaryResult += $"NGファイル数={noMatchFileNum}\n";
					SummaryResult += $"NGページ数  ={noMatchPageNum}\n";
					SummaryResult += $"Result={ret},time={ watch.ElapsedMilliseconds / 1000.0 }[sec]";
					Console.WriteLine(SummaryResult);// 結果標準出力
					if (pm.resultPath != "") {// ファイル出力の先頭に追記
						var rData = File.ReadAllText(pm.resultPath);
						SummaryResult += "\n[詳細] *find @Difference\n";
						File.WriteAllText(Path.GetFullPath(pm.resultPath), (SummaryResult + rData));
					}
					echo($"****************[終了]セパのFCモード***{argsStr}***************");
					ret = noMatchPageNum;// 返値 0:OK,!0=不一致Page数
				} else { // NoExeSepa -> Obsolate
					echo($@"****************NoExeSepa*****{argsStr}**************");
					if (pm.resultPath != "") {// 初期化
						File.WriteAllText(Path.GetFullPath(pm.resultPath), "");
					}
					for (var index = 0; index < pdfPathLstBoth.Count; index++) {
						var tgt = Path.Combine(pm.pdfPath, pdfPathLstBoth[index]);
						Console.WriteLine($@"tgt={index+1}/{pdfPathLstBoth.Count}:{tgt}");
						if (pm.bFC) {
							//比較モード
							var reff = Path.Combine(pm.pdfPathRef, pdfPathLstBoth[index]);
							//Console.WriteLine("<Compare Sync(Paralles) Version!>");
							ret = RenderPDF.RenderPdfDocCompare(
								pdfPath: Path.GetFullPath(tgt),
								refPdfPath: Path.GetFullPath(reff),
								resultDataPath: pm.resultPath,
								inDir: otDir,
								pm: rdCond,
								inPageRange: pm.pageRange,
								inHashDataTgt: hashDataTgt,
								inHashDataRef: hashDataRef,
								bPDFium: pm.bPDFium,
								nPageThreadNum:pm.paraPage
							);

						} else { // Sync version(Paralles).
							ret = RenderPDF.RenderPdfDoc(
								pdfPath: Path.GetFullPath(tgt),
								inDir: otDir,
								pm: rdCond,
								inPageRange: pm.pageRange,
								bSaveImage: pm.bMkHash ? false : true,// ハッシュ値生成ではイメージ保存しない。
								bHash: pm.bHash,// bHash,dumy
								inHashData: pm.bMkHash ? hashDataTgt : null, //ハッシュコマンドモードのみDataを渡す
								bPDFium: pm.bPDFium,
								nPageThreadNum: pm.paraPage
							);
						}
						count--;
						GC.Collect();// これでメモリリークが解決した
					};
				}
				if (pm.bMkHash) {
					hashDataTgt.save(otHashPath);//ハッシュモードで保存する(Dutyチェックもいるでしょう
				}
			}
			watch.Stop();
			Console.WriteLine($"result={ret},time={ watch.ElapsedMilliseconds / 1000.0 }[sec]");
			return ret;//success. // 返値 0:OK,!0=不一致Page数
		}

		// リソースからの取得
		public static class ResData {
			public static Stream GetBaseHashPDF(string resName = "CSRender.RES.BaseHash.pdf") {
				System.Reflection.Assembly asm = System.Reflection.Assembly.GetExecutingAssembly();
				var sel = from x in asm.GetManifestResourceNames() select resName;
				if (sel.Count() == 1) {
					return asm.GetManifestResourceStream(sel.First());
				}
				return null;
			}
		}

	}
}
/// <summary>
/// WCF通信モジュールのラッパクラス。
/// ２つのExe間をWCFパイプモードで通信を行う
/// </summary>
namespace xChangeWCFPipe
{
	/// シリアライズ可能にすること
	/// </summary>
	[ServiceContract]
	public interface IXData {
		[OperationContract(IsOneWay = true)]
		void Execute();

		[OperationContract]
		bool SetMessage(string msg);

		[OperationContract]
		bool SetProgress(int per);//0-100.

		[OperationContract]
		HashData GetHashData();

		[OperationContract]
		void SetHashData(HashData d);

		[OperationContract]
		CSRenderMain.ParamData GetParam();
		//CSRenderMain.Program.ParamData GetParam();
	}
	// ServiceContract https://tnakamura.hatenablog.com/entry/20080606/1220023868
	// https://devlights.hatenablog.com/entry/20111023/p2

	/// <summary>
	/// 通信用複雑データ。スカラー型(int,double,,,.)以外は[DataCOntract]属性を
	/// つけて、通知用Interfaceの引数や、返値で利用する
	/// </summary>
	[DataContract]
	public class DataContainer
	{
		[DataMember]
		public CSRenderMain.ParamData	pm;
		[DataMember]
		public HashData					hdata;
	}
}

namespace xChangeWCFPipe
{
	/// <summary>
	/// Main(サーバー)側 データ定義
	/// </summary>
	[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)]
	public class XData : IXData
	{
		private readonly Object LockObj = new object();// 排他制御用
		public delegate void callFunc();
		public callFunc callDelegate;
		public XData() {
			_dc.pm = new CSRenderMain.ParamData();
		}
		public void Execute() {
			lock (LockObj) {
				callDelegate();    // 実装はデリゲートします
			}
		}
		public bool SetMessage(string msg) {
			lock (LockObj) {
				Console.WriteLine($"SVR:called setMessage({msg}) from CLT");
				_msg = msg;
			}
			return true;
		}
		public bool SetProgress(int per) {//0-100.
			lock (LockObj) {
				Console.WriteLine($"SVR:called setProgress({per}) from CLT");
				_progress = per;
			}
			return true;
		}

		public void SetHashData(HashData h) {
			lock (LockObj) {
				_dc.hdata = h;
			}
		}
		public HashData GetHashData() {
			lock (LockObj) {
				return _dc.hdata;
			}
		}
		public CSRenderMain.ParamData GetParam() {
			lock (LockObj) {
				return _dc.pm;
			}
		}
		// クライアントに公開しなくてもよいI/F
		public void SetParam(CSRenderMain.ParamData pm) {
			lock (LockObj) {
					_dc.pm = pm;
			}
		}
		// private data
		private string _msg = "init msg";
		private int _progress = 0;// 0-100 
		private  DataContainer _dc = new DataContainer();
	}
}

namespace xChangeWCFPipeGEN
{
	/// <summary>
	/// Main(サーバー)側 データ定義
	/// </summary>
	//[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)]
	//public class XData : IXData
	//{
	//	...
	//}
	/// <summary>
	///  Main(サーバー)側クラス
	/// </summary>
	/// 
	public class SVR_T
	{
		// - static -

		/// <summary>
		/// 
		/// </summary>
		/// <param name="baseIf"> 公開するインターフェースクラス </param>
		/// <param name="x">実装インスタンス（baseIfを含むこと)</param>
		/// <param name="pipeName"></param>
		/// <param name="pipeAddress"></param>
		/// <returns></returns>
		static public SVR_T makeSVR(Type baseIf, object obj/*XData*/ , string pipeName = "PDFormstudioESorGS", string pipeAddress = "SubModuleAddress") {

			Type oType = obj.GetType();
			bool hasBaseIf = oType.GetInterfaces().Any(t => (t == baseIf) );
			if ( !hasBaseIf) {
				Console.WriteLine($@"Error:'{oType.FullName}' have not the interface '{baseIf.FullName}'");//A have not  the interface B.
				return null;
			}
			return (new SVR_T(baseIf, obj, pipeName, pipeAddress));
		}
		// - public I/F -
		public void Close() {
			echo($"Close Service:{_service != null},{_serviceHost != null}");
			_service = null;
			_serviceHost.Close();
		}
		// - private -
		private object _service = null;// 要らないかも
		private Type _baseIf = null;
		private ServiceHost _serviceHost = null;
		private SVR_T() { }
		private SVR_T(Type baseIf, object oInstance, string pipeName, string pipeAddress) {
			const string pipeBase = "net.pipe://localhost";
			var uri = pipeBase + "/" + pipeName;
			_baseIf = baseIf;
			_service = oInstance;
			// デリゲート登録
			//callDelegate = () => { Console.WriteLine("svr:Delegate func called"); }
			_serviceHost = new ServiceHost(_service, new Uri(uri));
			try {
				_serviceHost.AddServiceEndpoint(_baseIf/*typeof(Ti)*/, new NetNamedPipeBinding(), pipeAddress);
				_serviceHost.Open();
			} catch (AddressAlreadyInUseException) {
				Console.WriteLine("既にサービスは起動しています。");
			} catch (Exception e) {
				Console.WriteLine(e.ToString());
			}
		}
	}
	/// <summary>
	///  Sub(クライアント)側クラス
	/// </summary>
	public class CLT_T
	{
		/// <summary>
		/// Sub側の初期化処理。Main側とpipeName,pipeAddressを同じにすること
		/// </summary>
		/// <param name="pipeName"></param>
		/// <param name="pipeAddress"></param>
		/// <returns></returns>
		static public Ti makeCLT<Ti>(string pipeName = "PDFormstudioESorGS", string pipeAddress = "SubModuleAddress") {
			const string pipeBase = "net.pipe://localhost";
			var address = pipeBase + "/" + pipeName + "/" + pipeAddress;
			var factory = (new ChannelFactory<Ti>(new NetNamedPipeBinding(), new EndpointAddress(address)));
			Ti toMain = factory.CreateChannel();
			return toMain;
		}
	}
}

namespace Ut
{
	public class Ut
	{
		// Util
		// --------------------------------------------------------------
		/// <summary>
		/// 親プロセスが終了したら、自身を終了させる
		/// </summary>
		/// <param name="bEnable"></param>
		public static void SetAutoSelfKillByMainProcEnd(bool bEnable = true) {
			if (_SingletonProc != null)
				return;//一度しか呼び出せない
			if (bEnable == false)
				return;
			const string fname = "SetAutoSelfKillByMainProcEnd";
			// 自動的に親のプロセスがいなくなったら自動的にキルモードを設定する
			// メインプロセスの終了チェック
			// 親プロセスIDでProcessハンドルを取得、そのExitedイベントに自身の終了関数を設定(Environment.Exit())
			int paProcID = GetParentProcessId();
			var paProc = Process.GetProcessById(paProcID);
			paProc.EnableRaisingEvents = true;
			//Console.WriteLine($@"{fname}:ParrentProcessName ={paProc.ProcessName}({paProcID})");
			paProc.Exited += new EventHandler(
				(object s, EventArgs a) => {
					Console.WriteLine($@"{fname}:Exited Event!!!!");
					var ss = s as Process;
					Console.WriteLine($@"Sure ProcName is {ss.ProcessName}({ss.Id}) {ss.StartInfo.Arguments}"); ;
					System.Threading.Thread.Sleep(10 * 1000);
					ss.Close();
					ss.Dispose();
					Console.WriteLine($@"{fname}:Exited Event!!!!(afer10sec)");
				//Environment.Exit(-1);
			});
			//
			_SingletonProc = paProc;
		}
		private static System.Diagnostics.Process _SingletonProc = null;
		/// <summary>
		/// コマンドライン引数複数個をエンコードして、スペースで結合
		/// </summary>
		/// <param name="values">string[] コマンドライン引数</param>
		/// <returns>コマンドライン文字列(Escaped)</returns>
		public static string makeCmdLine(IEnumerable<string> args) {
			if (args == null)
				throw new ArgumentNullException("args");
			string EscapeCmdLineArg(string v) {
				if (string.IsNullOrEmpty(v)) return "";
				var containsSpace = v.IndexOfAny(new[] { ' ', '\t' }) != -1;
				v = ReCommandLineEscapePattern.Replace(v, @"$1\$&");//「\…\"」をエスケープ.「"」直前の「\」の数を 2倍+1
				if (containsSpace) {
					v = "\"" + ReLastBackSlashPattern.Replace(v, "$1$1") + "\"";
				}
				return v;
			}
			return string.Join(" ", args.Select(v => EscapeCmdLineArg(v)));
		}
		private static Regex ReCommandLineEscapePattern = new Regex("(\\\\*)\"");
		private static Regex ReLastBackSlashPattern = new Regex(@"(\\+)$");

		/// <summary>
		/// 親のプロセスIDを取得する
		/// </summary>
		/// <returns>親ProcessID</returns>
		static int GetParentProcessId() {
			var myProcId = GetCurrentProcessId();
			var query = string.Format($@"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {myProcId}");
			//クエリから結果を取得
			using (var search = new System.Management.ManagementObjectSearcher(@"root\CIMV2", query))
			using (var results = search.Get().GetEnumerator()) {
				if (!results.MoveNext())
					throw new ApplicationException("Couldn't Get ParrentProcessId.");
				var queryResult = results.Current;
				//親プロセスのPIDを取得
				uint pa = (uint)(queryResult["ParentProcessId"]);
				return (int)(pa);
			}
		}
		/// <summary>
		/// 自身のプロセスIDを取得する
		/// </summary>
		/// <returns>ProcessID</returns>
		static int GetCurrentProcessId() { return Process.GetCurrentProcess().Id; }

		/// <summary>
		/// 外部プログラムを起動する
		/// </summary>
		/// <param name="cmdAndArgs">引数文字列の配列</param>
		/// <param name="bSameConsole">true:コンソールを呼び出しと共有 false:別Console Windows</param>
		/// <param name="bEcho">標準出力(未実装）</param>
		/// <param name="bWait">コマンドが終了するまで待つ（false:未実装,常に待つ）</param>
		/// <param name="nWaitLimitMSec">タイムアウト時間ms(未実装)</param>
		/// <returns></returns>
		public static Process DoCmd(IEnumerable<string> cmdAndArgs, bool bSameConsole = true, bool bEcho = true, bool bWait = false, int nWaitLimitMSec = -1) {
			var cmdName = cmdAndArgs.First();//[0];
			var argStr = makeCmdLine(cmdAndArgs.Skip(1));
            // 先頭を除外してコピー new ArraySegment<string>(cmdAndArgs.ToArray(), 1, cmdAndArgs.Count()-1)
			//Console.WriteLine($@"DoCmd:{cmdName}:args[{argStr}]");
			//using (var p = new Process()) {
			var p = new Process();
			{
				var info = p.StartInfo;
				p.StartInfo.FileName = cmdName;
				p.StartInfo.Arguments = argStr;
				if (bSameConsole) {
					//Console.WriteLine("Same windows mode");
					p.StartInfo.UseShellExecute = false; // Shell経由で実行しない（必須） true: 別ウインドウ。false:コンソールを共有
					p.StartInfo.CreateNoWindow = false;   // ウィンドウを作成しない
				} else {
					//Console.WriteLine("New windows mode");
					p.StartInfo.UseShellExecute = true;
					p.StartInfo.CreateNoWindow = true;
				}
				p.Start();// 実行
				if (bWait == false) {
					return p;
				}
				if (nWaitLimitMSec == -1) {
					p.WaitForExit();// タイムアウト無し
				} else {
					bool bExit = p.WaitForExit(nWaitLimitMSec);
					if (!bExit) {// 処理の終了を待つ
						Console.WriteLine("処理が終了しないので強制終了");
						p.Kill(); // 強制終了する
						return p;
					}
				}
				return p;
			}
		}
	}
}