/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.process;

import org.opengion.fukurou.util.Argument;
import org.opengion.fukurou.util.HybsFileFilter;
import org.opengion.fukurou.util.LogWriter;

import java.io.File;
import java.util.Map ;
import java.util.LinkedHashMap ;
import java.util.Stack;

/**
 * Process_FileSearch は、指定のフォルダ以下のファイルを一覧する、FirstProcess
 * インターフェースと、ChainProcess インターフェースの実装クラスです。
 *
 * 指定の条件に合致するファイルを検索し、LineModel のサブクラスである、
 * FileLineModel オブジェクトを作成して、下流に渡します。
 * FileLineModel オブジェクトには、ファイル属性(Level,File,Length,Modify)
 * が設定されます。
 *
 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に
 * 繋げてください。
 *
 * @og.formSample
 *  Process_FileSearch -start=d:/ -suffix=jsp
 *
 *     -start=開始フォルダ       ：検索を開始するフォルダ
 *   [ -prefix=接頭辞         ]  ：File････,View････,など、指定の接頭辞で始まるファイルを検索
 *   [ -unprefix=不接頭辞     ]  ：File････,View････,など、指定の接頭辞で始まらないファイルを検索
 *   [ -suffix=接尾辞         ]  ：.txt,.java,.jsp.... など、指定の接尾辞で終わるファイルを検索
 *   [ -unsuffix=不接尾辞     ]  ：.txt,.java,.jsp.... など、指定の接尾辞で終わらないファイルを検索
 *   [ -instr=部分文字列      ]  ：ファイル名と一致する部分文字列を指定
 *   [ -uninstr=不部分文字列  ]  ：ファイル名と一致しな部分文字列を指定
 *   [ -equals=一致           ]  ：ファイル名と一致する文字列(大文字小文字は区別しない)を指定
 *   [ -notequals=不一致      ]  ：ファイル名と一致しない文字列(大文字小文字は区別しない)を指定
 *   [ -match=正規表現        ]  ：ファイル名と一致する正規表現を指定
 *   [ -unmatch=正規表現      ]  ：ファイル名と一致しない正規表現を指定
 *   [ -modify=YYYYMMDD       ]  ：指定日付け以降に変更されたファイルを検索
 *             YYYYMMDD   : YYYYMMDD 形式での指定日の 00:00:00 を基準時刻
 *             TODAY      : 実行日の 00:00:00 を基準時刻
 *             YESTERDAY  : 実行日前日の 00:00:00 を基準時刻
 *             LAST_WEEK  : 実行日の先週(7日前) 00:00:00 を基準時刻
 *             MONTH      : 実行月の 1日 00:00:00 を基準時刻
 *             LAST_MONTH : 実行前月の 同日 00:00:00 を基準時刻
 *             LAST_YEAR  : 実行前年の 同月同日 00:00:00 を基準時刻
 *   [ -useDIR=[false/true]   ]  ：判定をファイルだけでなく、ディレクトリでも行うかどうかを指定。(初期値：false)
 *   [ -larger=サイズ(KByte)  ]  ：ファイルの大きさが指定のＫバイト数より大きいファイルを検索
 *   [ -smaller=サイズ(KByte) ]  ：ファイルの大きさが指定のＫバイト数より小さいファイルを検索
 *   [ -maxLevel=最大階層数   ]  ：ディレクトリの階層を下がる最大数。(初期値：256)
 *   [ -useLineCnt=行数計算   ]  ：ファイルの行数をカウントするかどうかを指定。(初期値：false)
 *   [ -inPath=入力共通パス   ]  ：BIKO作成用のファイルパスから削除する部分(文字数のみ)
 *   [ -outPath=出力追加パス  ]  ：BIKO作成用のファイルパスに追加する部分
 *   [ -display=[false/true]  ]  ：trueは、検索状況を表示します。(初期値：false)
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class Process_FileSearch extends AbstractProcess
								implements FirstProcess , ChainProcess {

	private Stack<FileListStack>	dirs		= null;
	private File			file		= null;
	private HybsFileFilter	filter		= null;
	private FileLineModel	newData		= null;
	private int				level		= 1;

	private String			startDir	= null;
	private int				maxLevel	= 256;
	private boolean			display		= false;	// 表示しない
	private int				inCount		= 0;
	private int				outCount	= 0;
	private int				inPathLen	= 0;		// 4.2.3.0 (2008/05/26) BIKO欄用
	private String			outPath		= null;		// 4.3.1.1 (2008/08/23) BIKO欄用

	private final static Map<String,String> mustProparty   ;		// ［プロパティ］必須チェック用 Map
	private final static Map<String,String> usableProparty ;		// ［プロパティ］整合性チェック Map

	static {
		mustProparty = new LinkedHashMap<String,String>();
		mustProparty.put( "start",	"検索を開始するフォルダ(必須)" );

		usableProparty = new LinkedHashMap<String,String>();
		usableProparty.put( "prefix",	"File････,View････,など、指定の接頭辞で始まるファイルを検索" );
		usableProparty.put( "unprefix",	"File････,View････,など、指定の接頭辞で始まらないファイルを検索" );
		usableProparty.put( "suffix",	".txt,.java,.jsp.... など、指定の接尾辞で終わるファイルを検索" );
		usableProparty.put( "unsuffix",	".txt,.java,.jsp.... など、指定の接尾辞で終わらないファイルを検索" );
		usableProparty.put( "instr",	"ファイル名と一致する部分文字列を指定" );
		usableProparty.put( "uninstr",	"ファイル名と一致しない部分文字列を指定" );
		usableProparty.put( "equals",	"ファイル名と一致する文字列(大文字小文字は区別しない)を指定" );
		usableProparty.put( "notequals",	"ファイル名と一致しない文字列(大文字小文字は区別しない)を指定" );
		usableProparty.put( "match",	"ファイル名と一致する正規表現を指定" );
		usableProparty.put( "unmatch",	"ファイル名と一致しない正規表現を指定" );
		usableProparty.put( "modify",	"指定日付け以降に変更されたファイルを検索" +
					CR + "YYYYMMDD   : YYYYMMDD 形式での指定日の 00:00:00 を基準時刻" +
					CR + "TODAY      : 実行日の 00:00:00 を基準時刻" +
					CR + "YESTERDAY  : 実行日前日の 00:00:00 を基準時刻" +
					CR + "LAST_WEEK  : 実行日の先週(7日前) 00:00:00 を基準時刻" +
					CR + "MONTH      : 実行月の 1日 00:00:00 を基準時刻" +
					CR + "LAST_MONTH : 実行前月の 同日 00:00:00 を基準時刻" +
					CR + "LAST_YEAR  : 実行前年の 同月同日 00:00:00 を基準時刻"
		);
		usableProparty.put( "useDIR"	,"判定をファイルだけでなく、ディレクトリでも行うかどうかを指定。(初期値：false)" );
		usableProparty.put( "larger"	,"ファイルの大きさが指定のＫバイト数より大きいファイルを検索" );
		usableProparty.put( "smaller"	,"ファイルの大きさが指定のＫバイト数より小さいファイルを検索" );
		usableProparty.put( "maxLevel"	,"ディレクトリの階層を下がる最大数。(初期値：256)" );
		usableProparty.put( "useLineCnt","ファイルの行数をカウントするかどうかを指定。(初期値：false)" );
		usableProparty.put( "inPath"	,"BIKO作成用のファイルパスから削除する部分(文字数のみ)" );
		usableProparty.put( "outPath"	,"BIKO作成用のファイルパスに追加する部分" );
		usableProparty.put( "display"	,"trueは、検索状況を表示します。(初期値：false)" );
	}

	/**
	 * デフォルトコンストラクター。
	 * このクラスは、動的作成されます。デフォルトコンストラクターで、
	 * super クラスに対して、必要な初期化を行っておきます。
	 *
	 */
	public Process_FileSearch() {
		super( "org.opengion.fukurou.process.Process_FileSearch",mustProparty,usableProparty );
	}

	/**
	 * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
	 * 初期処理(ファイルオープン、ＤＢオープン等)に使用します。
	 *
	 * @og.rev 4.2.2.0 (2008/05/10) 行数カウントの使用有無
	 * @og.rev 4.3.1.1 (2008/08/23) BIKO 欄にoutPath 属性を追加します。
	 * @og.rev 5.1.2.0 (2010/01/01) useDIR 属性を追加します。
	 *
	 * @param   paramProcess ParamProcess
	 */
	public void init( final ParamProcess paramProcess ) {
		Argument arg = getArgument();

		startDir		= arg.getProparty("start" );

		String inPath	= arg.getProparty("inPath");
		if( inPath != null ) { inPathLen = inPath.length(); }

		String prefix	= arg.getProparty("prefix");
		String unprefix	= arg.getProparty("unprefix");		// 5.1.2.0 (2010/01/01) 追加
		String suffix	= arg.getProparty("suffix");
		String unsuffix	= arg.getProparty("unsuffix");		// 5.1.2.0 (2010/01/01) 追加
		String instr	= arg.getProparty("instr");
		String uninstr	= arg.getProparty("uninstr");		// 5.1.2.0 (2010/01/01) 追加
		String equals	= arg.getProparty("equals");
		String notequals= arg.getProparty("notequals");		// 5.1.2.0 (2010/01/01) 追加
		String match	= arg.getProparty("match");
		String unmatch	= arg.getProparty("unmatch");
		String modify	= arg.getProparty("modify");
		String larger	= arg.getProparty("larger");
		String smaller	= arg.getProparty("smaller");
		maxLevel		= arg.getProparty("maxLevel",maxLevel);
		outPath			= arg.getProparty("outPath");
		// 4.2.2.0 (2008/05/10) 行数カウントの使用有無
		boolean useLineCnt = arg.getProparty( "useLineCnt",false );

		// 5.1.2.0 (2010/01/01) 判定をディレクトリでも行うかどうか
		boolean useDIR = arg.getProparty( "useDIR",false );

		display	= arg.getProparty("display",display);
		if( display ) { println( startDir ); }		// 4.0.0 (2005/01/31)

		// 5.1.2.0 (2010/01/01) 反転処理の引数追加
		filter = new HybsFileFilter( useDIR );	// 5.1.2.0 (2010/01/01) 判定をディレクトリでも行うかどうか
		filter.startsWith( prefix   , false);
		filter.startsWith( unprefix , true );
		filter.endsWith( suffix   , false);
		filter.endsWith( unsuffix , true );
		filter.instr( instr   , false);
		filter.instr( uninstr , true );
		filter.fileEquals( equals   , false);
		filter.fileEquals( notequals , true );
		filter.matches( match   , false);
		filter.matches( unmatch , true );
//		filter.unMatches( unmatch );			// 5.1.2.0 (2010/01/01) 反転処理は、matches の 反転引数で対応
		filter.lastModified( modify );
		if( larger  != null ) { filter.isLarger( Integer.parseInt( larger ) ); }
		if( smaller != null ) { filter.isSmaller( Integer.parseInt( smaller ) ); }

		File tempFile = new File( startDir );
		if( tempFile.isDirectory() ) {
			dirs = new Stack<FileListStack>();
			File[] fileList = tempFile.listFiles( filter );
			dirs.push( new FileListStack( fileList, level ) );
		}
		else {
			dirs = new Stack<FileListStack>();
			File[] fileList = new File[] { tempFile };
			dirs.push( new FileListStack( fileList, level ) );
		}

//		newData = new FileLineModel();
		newData = new FileLineModel( useLineCnt );	// 4.2.2.0 (2008/05/10)
	}

	/**
	 * このデータの処理において、次の処理が出来るかどうかを問い合わせます。
	 * この呼び出し１回毎に、次のデータを取得する準備を行います。
	 *
	 * @return  boolean 処理できる:true / 処理できない:false
	 */
	public boolean next() {
		while( !dirs.empty() ) {
			FileListStack fStack = dirs.pop();

			level = fStack.getLevel();
			if( level > maxLevel ) { continue; }

			File[] fileList = fStack.getFileList();
			if( fileList == null ) { continue; }

			int address = fStack.getAddress();
			for( ; address < fileList.length; address++ ) {
				inCount++ ;
				if( fileList[address].isDirectory() ) {
					File[] newList = fileList[address].listFiles( filter );
					dirs.push( new FileListStack( newList,level+1) );
				}
				else {
					file = fileList[address];
					fStack.setAddress( address+1 );
					dirs.push( fStack );
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * 最初に、 行データである LineModel を作成します
	 * FirstProcess は、次々と処理をチェインしていく最初の行データを
	 * 作成して、後続の ChainProcess クラスに処理データを渡します。
	 *
	 * @og.rev 4.2.3.0 (2008/05/26) BIKO 欄に展開ファイル名を記述します。
	 * @og.rev 4.3.1.1 (2008/08/23) BIKO 欄にoutPath 属性を追加します。
	 *
	 * @param   rowNo int 処理中の行番号
	 * @return  LineModel  処理変換後のLineModel
	 */
	public LineModel makeLineModel( final int rowNo ) {
		outCount++ ;
		newData.setFileVals( level,file );

		// 4.3.1.1 (2008/08/23)
		String biko = null;
		// 4.2.3.0 (2008/05/26) BIKO 欄追加
		if( inPathLen > 0 ) {
			biko = file.getAbsolutePath().substring( inPathLen );
//			newData.setBiko( biko );
		}

		if( outPath != null ) {
			if( biko == null ) {
				biko = outPath + file.getName() ;
			}
			else {
				biko = outPath + biko ;
			}
		}
		if( biko != null ) {
			newData.setBiko( biko );
		}

		newData.setRowNo( rowNo );

//		if( display ) { println( newData.dataLine() ); }		// 5.1.2.0 (2010/01/01) display の条件変更

		return newData;
	}

	/**
	 * 引数の LineModel を処理するメソッドです。
	 * 変換処理後の LineModel を返します。
	 * 後続処理を行わない場合(データのフィルタリングを行う場合)は、
	 * null データを返します。つまり、null データは、後続処理を行わない
	 * フラグの代わりにも使用しています。
	 * なお、変換処理後の LineModel と、オリジナルの LineModel が、
	 * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。
	 * ドキュメントに明記されていない場合は、副作用が問題になる場合は、
	 * 各処理ごとに自分でコピー(クローン)して下さい。
	 *
	 * @param   data LineModel オリジナルのLineModel
	 * @return  LineModel  処理変換後のLineModel
	 */
	public LineModel action( final LineModel data ) {
		LineModel rtn = null;

		final FileLineModel fileData ;
		if( data instanceof FileLineModel ) {
			fileData = (FileLineModel)data ;
		}
		else {
			String errMsg = "データが FileLineModel オブジェクトではありません。" + CR ;
			throw new RuntimeException( errMsg );
		}

		File inFile = fileData.getFile() ;
		File[] fileList = inFile.listFiles( filter );

		if( fileList != null && fileList.length > 0 ) {
			rtn = data;
		}

		if( display && rtn != null ) { println( rtn.dataLine() ); }		// 5.1.2.0 (2010/01/01) display の条件変更
		return rtn ;
	}

	/**
	 * プロセスの終了を行います。最後に一度だけ、呼び出されます。
	 * 終了処理（ファイルクローズ、ＤＢクローズ等）に使用します。
	 *
	 * @param   isOK トータルで、OKだったかどうか(true:成功/false:失敗）
	 */
	public void end( final boolean isOK ) {
		dirs		= null;
		file		= null;
		filter		= null;
		newData		= null;
	}

	/**
	 * プロセスの処理結果のレポート表現を返します。
	 * 処理プログラム名、入力件数、出力件数などの情報です。
	 * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
	 * 形式で出してください。
	 *
	 * @return   処理結果のレポート
	 */
	public String report() {
		String report = "[" + getClass().getName() + "]" + CR
				+ TAB + "Start Folder : " + startDir  + CR
				+ TAB + "Search Count : " + inCount   + CR
				+ TAB + "Output Count : " + outCount ;

		return report ;
	}

	/**
	 * このクラスの使用方法を返します。
	 *
	 * @return  String このクラスの使用方法
	 */
	public String usage() {
		StringBuilder buf = new StringBuilder();

		buf.append( "Process_FileSearch は、指定のフォルダ以下のファイルを一覧する、FirstProcess"	).append( CR );
		buf.append( "インターフェースと、ChainProcess インターフェースの実装クラスです。"			).append( CR );
		buf.append( CR );
		buf.append( "指定の条件に合致するファイルを検索し、ファイル属性(Level,File,Length,Modify)"	).append( CR );
		buf.append( "を元に、LineModelを作成し、下流に渡します。"									).append( CR );
		buf.append( CR );
		buf.append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。"	).append( CR );
		buf.append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に"		).append( CR );
		buf.append( "繋げてください。"																).append( CR );
		buf.append( CR );
		buf.append( "  -start=開始フォルダ       ：検索を開始するフォルダ                                                         ").append( CR );
		buf.append( "[ -prefix=接頭辞         ]"													).append( CR );
		buf.append( "[ -unprefix=不接頭辞     ]"													).append( CR );
		buf.append( "[ -suffix=接尾辞         ]"													).append( CR );
		buf.append( "[ -unsuffix=不接尾辞     ]"													).append( CR );
		buf.append( "[ -instr=部分文字列      ]"													).append( CR );
		buf.append( "[ -uninstr=不部分文字列  ]"													).append( CR );
		buf.append( "[ -equals=一致           ]"													).append( CR );
		buf.append( "[ -notequals=不一致      ]"													).append( CR );
		buf.append( "[ -match=正規表現        ]"													).append( CR );
		buf.append( "[ -unmatch=正規表現      ]"													).append( CR );
		buf.append( "[ -modify=YYYYMMDD       ]  ：指定日付け以降に変更されたファイルを検索"		).append( CR );
		buf.append( "          YYYYMMDD   : YYYYMMDD 形式での指定日の 00:00:00 を基準時刻"			).append( CR );
		buf.append( "          TODAY      : 実行日の 00:00:00 を基準時刻                 "			).append( CR );
		buf.append( "          YESTERDAY  : 実行日前日の 00:00:00 を基準時刻             "			).append( CR );
		buf.append( "          LAST_WEEK  : 実行日の先週(7日前) 00:00:00 を基準時刻      "			).append( CR );
		buf.append( "          MONTH      : 実行月の 1日 00:00:00 を基準時刻             "			).append( CR );
		buf.append( "          LAST_MONTH : 実行前月の 同日 00:00:00 を基準時刻          "			).append( CR );
		buf.append( "          LAST_YEAR  : 実行前年の 同月同日 00:00:00 を基準時刻      "			).append( CR );
		buf.append( "[ -useDIR=[false/true]   ]  ："												).append( CR );
		buf.append( "[ -larger=サイズ(KByte)  ]  ："												).append( CR );
		buf.append( "[ -smaller=サイズ(KByte) ]  ："												).append( CR );
		buf.append( "[ -maxLevel=最大階層数   ]  ："												).append( CR );
		buf.append( "[ -useLineCnt=行数計算   ]  ："												).append( CR );
		buf.append( "[ -inPath=入力共通パス   ]  ："												).append( CR );
		buf.append( "[ -outPath=出力追加パス  ]  ："												).append( CR );
		buf.append( "[ -display=[false/true]  ]  ："												).append( CR );
		buf.append( CR ).append( CR );
		buf.append( getArgument().usage() ).append( CR );

		return buf.toString();
	}

	/**
	 * このクラスは、main メソッドから実行できません。
	 *
	 * @param   args String[]
	 */
	public static void main( final String[] args ) {
		LogWriter.log( new Process_FileSearch().usage() );
	}

	/**
	 * このクラスはファイルをスタックを使用して展開する場合の
	 * 個々の状態を保持する為のクラスです。
	 *
	 * @version  4.0
	 * @author   Kazuhiko Hasegawa
	 * @since    JDK5.0,
	 */
	private static final class FileListStack {
		private int address ;
		private final File[] files;
		private final int level;

		/**
		 * コンストラクター
		 * 初期値を設定します。
		 * ファイルの配列については、コピーせずそのまま内部配列にセットしています。
		 *
		 * @param files File[] ファイルの配列(ファイルリスト)
		 * @param level int    レベル(指定のstartフォルダからの階層数)
		 */
		FileListStack( final File[] files,final int level ) {
			this.files   = files;
			this.address = 0;
			this.level   = level;
		}

		/**
		 * ファイルリストのアドレスを設定します。
		 * スタックから取り出した後、配列を前回の続きからサーチする場合に使用します。
		 *
		 * @param address int ファイルリストのアドレス
		 */
		void setAddress( final int address ) {
			this.address = address;
		}

		/**
		 * ファイルリストのアドレスを取り出します。
		 *
		 * @return int ファイルリストのアドレス
		 */
		int getAddress() {
			return address;
		}

		/**
		 * ファイルリストを取り出します。
		 * ファイルの配列については、コピーせずそのまま内部配列を返しています。
		 *
		 * @return File[] ファイルリスト
		 */
		File[] getFileList() {
			return files;
		}

		/**
		 * 階層レベルを取り出します。
		 *
		 * @return int レベル
		 */
		int getLevel() {
			return level;
		}
	}
}
