//	
//	DumpXlsx.cs
//	
//	DumpXlsx.exe - xlsx/xlsmファイルのシートデータをダンプするツール(.NET版)
//	Copyright (C) 2016 Naoyuki Sawa
//	
//	* Sun Jul 17 21:03:07 JST 2016 Naoyuki Sawa
//	- 1st リリース。
//	  使用したツールは、以下の通りです:
//	  ・Microsoft Visual Studio Community 2015 Version 14.0.25123.00 Update 2
//	  ・Microsoft .NET Framework Version 4.6.01038
//	  ・Open XML SDK 2.5 (https://www.microsoft.com/en-us/download/details.aspx?id=30425)	┬Yahoo!ボックスの、/マイボックス/書類/archive/Tool/Office/OpenXML/ に保存しておきました。
//	  ・ClosedXML 0.76.0 (https://closedxml.codeplex.com/)					┘
//	  Open XML SDKは、上記からダウンロードしたインストーラでインストールしました。特に設定は行っていません。多分、Open XML SDKのインストーラがGACに登録する(?)か何かの設定を行ってくれているのだと思います。もしOpen XML SDKをインストールしていないPCでDumpXlsx.exeを実行する時は、C:\Program Files (x86)\Open XML SDK\V2.5\lib\DocumentFormat.OpenXml.dll も同じフォルダにコピーする必要が有るかも知れません。
//	  ClosedXMLは、インストーラは無いので、上記からダウンロードしたClosedXML.dllとClosedXML.XMLをC:/Home/Share/Piece/clip/tool/にコピーしました。他のPCでDumpXlsx.exeを実行する時は、ClosedXML.dllも同じフォルダにコピーする必要が有ると思います。
//	  VC++2015の参照設定に追加したdllは、ClosedXML.dllのみです。DocumentFormat.OpenXml.dllを明示的に参照設定に追加する必要は有りませんでした。
//	- 先日、同じ目的のツールをTclとtDOMで作成したのですが、当ツールで置き換える事にしました。
//	  TclとtDOMでXMLを直接パースするよりも、Open XML SDKとClosedXMLを使う方が確実だからです。
//	  実際に、前者の方法では稀に上手くパース出来ないxlsxが有ったのですが、後者の方法ならばパース出来るようになりました。
//	  尚、TclとtDOM版のツールはもう使いませんが、今後tDOMを使う時の参考のために、/clip/tool/フォルダ直下から、/clip/tool/DumpXlsx/old/サブフォルダへ移動して保存しておく事にしました。
//	* Sat Sep 24 21:37:23 JST 2016 Naoyuki Sawa
//	- '-c'オプションを追加しました。
//	  これまでは、当ツールが無条件に行コメントを削除していましたが、今後は、'-c'オプションが指定された場合のみ行コメントを削除するように変更しました。
//	  デフォルトの挙動を変えた理由は、垂直コメントの仕様として、行コメント内のセルに意味を持たせる仕様を追加した事に伴い、当ツールの段階で行コメントを削除しない方が望ましいからです。
//	  (詳細は、/clip/tool/DeleteVerticalComment.awkを参照して下さい。)
//	  だたし、垂直コメントの書式を使用しない事が判っている場合は、当ツールが行コメントを削除しても問題無いので、'-c'オプションとして行コメントを削除する機能も残す事にしました。
//	
using System;
using System.IO;			//Path
using System.Text;			//StringBuilder
using System.Collections.Generic;	//List<T>
//using System.Diagnostics;		//Debug,Trace
using ClosedXML.Excel;
/****************************************************************************
 *	
 ****************************************************************************/
class DumpXlsx {
	const string	VERSION		= "20160924";	//最終更新日
	static int	opt_addr	= 0;		//0=アドレスを付与しない。1=「セル名」を付与する。2=「'シート名'!セル名」を付与する。3=「'[ブック名]シート名'!セル名」を付与する。4=「'パス名[ブック名]シート名'!セル名」を付与する。
//{{2016/09/24追加:'-c'オプションを追加しました。
	static bool	opt_delCmt	= false;	//0=コメントを削除しない。1=コメントを削除する。
//}}2016/09/24追加:'-c'オプションを追加しました。
	static string	opt_outDir	= "out";	//ルート出力フォルダ名
	/*--------------------------------------------------------------------------*/
	static void Main(string[] args) {
		int optind = 0;
		//コマンドラインオプションを解析する。
		while(optind < args.Length) {
			var s = args[optind];
			if((s.Length < 2) || (s[0] != '-')) { break; }
			var optopt = s[1];
			var optarg = s.Substring(2);
			switch(optopt) {
			default: Usage(); break;
			case 'a':
				if(optarg == "") { optarg = "4"; }
				if(!Int32.TryParse(optarg, out opt_addr) || ((uint)opt_addr > 4)) { Usage(); }
				break;
//{{2016/09/24追加:'-c'オプションを追加しました。
			case 'c':
				if(optarg != "") { Usage(); }
				opt_delCmt = true;
				break;
//}}2016/09/24追加:'-c'オプションを追加しました。
			case 'o':
				if(optarg == "") { Usage(); }
				opt_outDir = optarg;
				break;
			}
			//コマンドライン引数を次へ進める。
			optind++;
		}
		if(optind == args.Length) { Usage(); }
		//コマンドライン引数で指定された各xlsx,及び,xlsmファイルについて…
		while(optind < args.Length) {
			var bookPath = args[optind];
			//このファイルを処理する。
			ProcessFile(bookPath);
			//コマンドライン引数を次へ進める。
			optind++;
		}
	}
	/*--------------------------------------------------------------------------*/
	static void Usage() {
		Console.Error.WriteLine("DumpXlsx.exe - xlsx/xlsmファイルのシートデータをダンプするツール(.NET版) ({0})", VERSION);
		Console.Error.WriteLine("Copyright (C) 2016 Naoyuki Sawa");
		Console.Error.WriteLine("");
		Console.Error.WriteLine("USAGE:");
		Console.Error.WriteLine("  DumpXlsx.exe [option ...] filename [filename ...]");
		Console.Error.WriteLine("");
		Console.Error.WriteLine("OPTIONS:");
		Console.Error.WriteLine("  -a0          セルの文字列に、アドレスを付与しません。(デフォルト)");
		Console.Error.WriteLine("  -a1          セルの文字列に、「セル名」を付与します。");
		Console.Error.WriteLine("  -a2          セルの文字列に、「'シート名'!セル名」を付与します。");
		Console.Error.WriteLine("  -a3          セルの文字列に、「'[ブック名]シート名'!セル名」を付与します。");
		Console.Error.WriteLine("  -a4          セルの文字列に、「'パス名[ブック名]シート名'!セル名」を付与します。");
		Console.Error.WriteLine("  -a           -a4 と同じです。");
//{{2016/09/24追加:'-c'オプションを追加しました。
		Console.Error.WriteLine("  -c           セルの文字列が\"#\",又は,\"//\"で開始していたら、行コメントの開始と見なして、その行の、それ以降のセルを削除します。");
		Console.Error.WriteLine("               このオプションを指定しなければ、行コメントも削除せずに出力します。");
		Console.Error.WriteLine("               垂直コメント(=行コメント内の\"#～#\"でマークされた列を削除する)を使う場合は、このオプションを指定しないで下さい。");
		Console.Error.WriteLine("               ※垂直コメントを削除する機能は、DeleteVerticalComment.awkが提供する機能です。当ツールが提供する機能ではありません。");
//}}2016/09/24追加:'-c'オプションを追加しました。
		Console.Error.WriteLine("  -oフォルダ名 出力フォルダを設定します。");
		Console.Error.WriteLine("               このオプションを指定しなければ、outフォルダに出力します。");
		Console.Error.WriteLine("");
		Console.Error.WriteLine("EXAMPLE:");
		Console.Error.WriteLine("  DumpXlsx.exe -a2 Sample1.xlsx Sample2.xlsm");
		Console.Error.WriteLine("  Sample1.xlsxとSample2.xlsmを読み込んで、各シートのデータをoutフォルダに出力します。");
		Console.Error.WriteLine("  同時に、「ブックのパス名(タブ)シート名」を、標準出力へ出力します。");
		Environment.Exit(1);
	}
	/*--------------------------------------------------------------------------*/
	static void ProcessFile(string bookPath) {
		//ワークブックのドライブ名とフォルダ名を取得する。
		var bookDir = Path.GetDirectoryName(bookPath);
		if(bookDir != "") {			//ドライブ名とフォルダ名が空文字でなければ…
			if(!bookDir.EndsWith("\\") &&
			   !bookDir.EndsWith("/")) {
				bookDir += "\\";	//確実にパスセパレータを補う。Path.GetDirectoryName()は_splitpath()と違って、フォルダ名の末尾にパスセパレータを追加しないので、このように明示的に追加する必要が有る。Path.Combine()を使うだけならばこの処理は不要なのだが、「'パス名[ブック名]シート名'!セル名」で出力する場合にパス名の末尾にパスセパレータが必須なのでこの処理が必要です。
			}
		}
		//ワークブックのファイル名と拡張子を取得する。
		var bookFname = Path.GetFileName(bookPath);
		//ワークブックを開く。
		XLWorkbook book = null;	//警告抑制
		try {
			book = new XLWorkbook(bookPath, XLEventTracking.Disabled);
		} catch(Exception) {
			Console.Error.WriteLine("{0}が開けません。", bookPath);
			Environment.Exit(1);
		}
		using(book) {
			//出力フォルダ名を作成する。
			string outDir = Path.Combine(opt_outDir, Path.GetFileNameWithoutExtension(bookFname));	//出力フォルダ名は、「ルート出力フォルダ名\ワークブックのベース名」とする事にした。
			//出力フォルダを確実に作成する。
			try {
				Directory.CreateDirectory(outDir);	//既に同じ名前のフォルダが存在していても、Directory.CreateDirectory()はエラーにならない。
			} catch(Exception) {
				Console.Error.WriteLine("{0}フォルダが作成出来ません。", outDir);
				Environment.Exit(1);
			}
			//各ワークシートについて…
			foreach(var sheet in book.Worksheets) {
				//ワークシート名を取得する。
				var sheetName = sheet.Name;
				//標準出力に、「ワークブックのパス名(タブ)ワークシート名」を出力する。
				Console.WriteLine(bookPath + "\t" + sheetName);
				//出力ファイルのパス名を作成する。
				var outPath = Path.Combine(outDir, Path.ChangeExtension(sheetName, ".txt"));	//出力パス名は、「出力フォルダ名\ワークシート名+.txt」とする事にした。
				//出力ファイルを作成する。
				StreamWriter writer = null;	//警告抑制
				try {
					writer = new StreamWriter(outPath, false, Encoding.Default/*シフトJIS*/);
				} catch(Exception) {
					Console.Error.WriteLine("{0}が作成出来ません。", outPath);
					Environment.Exit(1);
				}
				using(writer) {
					//各行について…
					foreach(var row in sheet.RowsUsed()) {
						var list = new List<string>();	//各セルの出力文字列を格納するためのリスト
						var oldCol = 0;			//前のセルの列番号
						//各セルについて…
						foreach(var cell in row.CellsUsed()) {		//Cells()ではなくCellsUsed()を呼び出す事に注意せよ。Cells()でも動作はするが、Cells()はCellsUsed(true)と等価であり、書式だけ設定された不要なセルも列挙してしまう可能性が有る。
							//セルの文字列を取得する。
							var value = cell.ValueCached;		//まず、ValueではなくValueCachedを取得する事に注意せよ。もし無条件にValueを取得すると、式が含まれていた場合にClosedXMLが自力で再集計等を行うようで、極端に遅くなったり、式にエラーが有る場合例外が投入されたりする。
							if(value == null) {
								value = cell.GetString();	//式でない場合は、(ValueCached==null) となっているようだ。その場合は、直値,又は,文字列を、cell.Valueから文字列として取得する。
							}
							//セルの文字列の前後の空白を削除する。
							value = value.Trim();
//{{2016/09/24追加:'-c'オプションを追加しました。
//							//セルの文字列が"#",又は,"//"で開始していたら…
//							if(value.StartsWith("#") || value.StartsWith("//")) {
//								//この行の、これ以降のセルを無視する。
//								break;	//ここまで
//							}
//↓2016/09/24追加:'-c'オプションを追加しました。
							//'-c'オプションが指定されていたら…
							if(opt_delCmt) {
								//セルの文字列が"#",又は,"//"で開始していたら…
								if(value.StartsWith("#") || value.StartsWith("//")) {
									//この行の、これ以降のセルを無視する。
									break;	//ここまで
								}
							}
//}}2016/09/24追加:'-c'オプションを追加しました。
							//セルのアドレスを取得する。
							var addr = cell.Address;
							//列番号を取得する。
							var col = addr.ColumnNumber;
							//このセルの列番号が、前のセルの列番号以下である事は無いはず。
							if(oldCol >= col) { throw new ApplicationException(); }
							//列番号が連続していない場合は、空セルの出力文字列を挿入する。
							while(++oldCol < col) { list.Add(""); }
							//このセルの出力文字列を作成する。
							var sb = new StringBuilder();
							if(!String.IsNullOrEmpty(value)) {	//セルの文字列がnullでも""でもなければ…
								//セルの文字列の各文字について…
								foreach(var c in value) {
									var t = c;
									//制御文字,又は,','又は,'"'ならば…
									if(Char.IsControl(t) || (t == ',') || (t == '"')) {
										//'?'に置き換える。
										t = '?';
									}
									//この文字を追加する。
									sb.Append(t);
								}
								//セルの文字列が"0"でなければ…
								// - "0"でもアドレスを付与しても構いませんが、dLotTblC.exeが無視するので無駄です。
								//   出力サイズを減らすために、"0"ならばアドレスを付与しない事にしました。
								if(sb.ToString() != "0") {
									//opt_addr:1=「セル名」を付与する。
									if(opt_addr >= 1) {
										sb.Append(' ');
										sb.Append('"');
										//opt_addr:2=「'シート名'!セル名」を付与する。
										if(opt_addr >= 2) {
											sb.Append('\'');
											//opt_addr:3=「'[ブック名]シート名'!セル名」を付与する。
											if(opt_addr >= 3) {
												//opt_addr:4=「'パス名[ブック名]シート名'!セル名」を付与する。
												if(opt_addr >= 4) {
													sb.Append(bookDir);	//ワークブックのドライブ名とフォルダ名
												}
												sb.Append('[');
												sb.Append(bookFname);	//ワークブックのファイル名と拡張子
												sb.Append(']');
											}
											sb.Append(sheetName);	//ワークシート名
											sb.Append('\'');
											sb.Append('!');
										}
										sb.Append(addr.ToString());
										sb.Append('"');
									}
								}
							}
							//このセルの出力文字列を、リストに追加する。
							list.Add(sb.ToString());
						}
						//各セルの出力文字列を格納したリストを、カンマで区切って連結する。
						var s = String.Join(",", list);
						//行の末尾の、連続する空セルを削除する。
						s = s.TrimEnd(',');
						//空行でなければ…
						if(s != "") {
							//この行を出力する。
							writer.WriteLine(s);
						}
					}
				}
			}
		}
	}
}
