/*
 * Copyright (c) 2017 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.fileexec;

import java.io.File;
import java.io.IOException;

import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Files;
import java.nio.file.DirectoryStream;

import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer;

/**
 * フォルダに残っているファイルを再実行するためのプログラムです。
 *
 * 通常は、FileWatch で、パスを監視していますが、場合によっては、
 * イベントを拾いそこねることがあります。それを、フォルダスキャンして、拾い上げます。
 * １０秒間隔で繰り返しスキャンします。条件は、３０秒以上前のファイルです。
 *
 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
 *
 * @version  7.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK1.8,
 */
public class DirWatch implements Runnable {
	private static final XLogger LOGGER= XLogger.getLogger( DirWatch.class.getName() );		// ログ出力

	/** 最初にスキャンを実行するまでの遅延時間(秒) の初期値 */
	public static final long INIT_DELAY	= 5;			// (秒)

	/** スキャンする間隔(秒) の初期値 */
	public static final long PERIOD		= 10;			// (秒)

	/** ファイルのタイムスタンプとの差のチェック(秒) の初期値 */
	public static final long TIME_DIFF	= 30;			// (秒)

	private final	Path		sPath;						// スキャンパス
	private final	boolean		useTree;					// フォルダ階層をスキャンするかどうか

	// callbackするための、関数型インターフェース(メソッド参照)
	private Consumer<Path> action = path -> System.out.println( "DirWatch Path=" + path ) ;

	// DirectoryStreamで、パスのフィルタに使用します。
	private final PathMatcherSet pathMch = new PathMatcherSet();		// PathMatcher インターフェースを継承

	// フォルダスキャンする条件
	private DirectoryStream.Filter<Path> filter;

	// スキャンを停止する場合に使用します。
	private ScheduledFuture<?> stFuture ;

	/**
	 * スキャンパスを引数に作成される、コンストラクタです。
	 *
	 * ここでは、階層検索しない(useTree=false)で、インスタンス化します。
	 *
	 * @param	sPath	検索対象となるスキャンパス
	 */
	public DirWatch( final Path sPath ) {
		this( sPath , false );
	}

	/**
	 * スキャンパスと関数型インターフェースフォルダを引数に作成される、コンストラクタです。
	 *
	 * @param	sPath	検索対象となるスキャンパス
	 * @param	useTree	階層スキャンするかどうか(true:する/false:しない)
	 */
	public DirWatch( final Path sPath, final boolean useTree ) {
		this.sPath		= sPath;
		this.useTree	= useTree;
	}

	/**
	 * 指定のパスの照合操作で、パターンに一致したパスのみ、callback されます。
	 *
	 * ここで指定したパターンの一致を判定し、一致した場合は、callback されます。
	 * 指定しない場合は、すべて許可されたことになります。
	 * なお、#setPathEndsWith(String...) と、この設定は同時には行うことは出来ません。
	 *
	 * @param	pathMch パスの照合操作のパターン
	 * @see		java.nio.file.PathMatcher
	 * @see		#setPathEndsWith(String...)
	 */
	public void setPathMatcher( final PathMatcher pathMch ) {
		this.pathMch.addPathMatcher( pathMch );
	}

	/**
	 * 指定のパスが、指定の文字列と、終端一致(endsWith) したパスのみ、callback されます。
	 *
	 * これは、#setPathMatcher(PathMatcher) の簡易指定版です。
	 * 指定の終端文字列(一般には拡張子)のうち、ひとつでも一致すれば、true となりcallback されます。
	 * 指定しない場合(null)は、すべて許可されたことになります。
	 * 終端文字列の判定には、大文字小文字の区別を行いません。
	 * なお、#setPathMatcher(PathMatcher) と、この設定は同時には行うことは出来ません。
	 *
	 * @param	endKey パスの終端一致のパターン
	 * @see		#setPathMatcher(PathMatcher)
	 */
	public void setPathEndsWith( final String... endKey ) {
		pathMch.addEndsWith( endKey );
	}

	/**
	 * ファイルパスを、引数に取る Consumer ダオブジェクトを設定します。
	 *
	 * これは、関数型インタフェースなので、ラムダ式またはメソッド参照の代入先として使用できます。
	 * イベントが発生したときの ファイルパス(監視フォルダで、resolveされた、正式なフルパス)を引数に、
	 * accept(Path) メソッドが呼ばれます。
	 *
	 * @param	act 1つの入力(ファイルパス) を受け取る関数型インタフェース
	 * @see		Consumer#accept(Object)
	 */
	public void callback( final Consumer<Path> act ) {
		if( act != null ) {
			action = act ;
		}
	}

	/**
	 * 内部でScheduledExecutorServiceを作成して、ScheduledFuture に、自身をスケジュールします。
	 *
	 * 初期値( initDelay={@value #INIT_DELAY} , period={@value #PERIOD} , timeDiff={@value #TIME_DIFF} ) で、
	 * スキャンを開始します。
	 *
	 * #start( {@value #INIT_DELAY} , {@value #PERIOD} , {@value #TIME_DIFF} ) と同じです。
	 *
	 */
	public void start() {
		start( INIT_DELAY , TIME_DIFF , PERIOD );
	}

	/**
	 * 内部でScheduledExecutorServiceを作成して、ScheduledFuture に、自身をスケジュールします。
	 *
	 * スキャン開始の遅延時間と、スキャン間隔、ファイルのタイムスタンプとの比較を指定して、スキャンを開始します。
	 * ファイルのタイムスタンプとの差とは、ある一定時間経過したファイルのみ、action をcall します。
	 *
	 * @param	initDelay 最初にスキャンを実行するまでの遅延時間(秒)
	 * @param	period    スキャンする間隔(秒)
	 * @param	timeDiff  ファイルのタイムスタンプとの差のチェック(秒)
	 */
	public void start( final long initDelay , final long period , final long timeDiff ) {
		LOGGER.info( () -> "DirWatch Start: " + sPath + " Tree=" + useTree + " Delay=" + initDelay + " Period=" + period + " TimeDiff=" + timeDiff );

		// DirectoryStream.Filter<Path> インターフェースは、#accept(Path) しかメソッドを持っていないため、ラムダ式で代用できる。
		filter = path -> Files.isDirectory( path ) || pathMch.matches( path ) && timeDiff*1000 < ( System.currentTimeMillis() - path.toFile().lastModified() );

	//	filter = path -> Files.isDirectory( path ) || 
	//						pathMch.matches( path ) &&
	//						FileTime.fromMillis( System.currentTimeMillis() - timeDiff*1000L )
	//								.compareTo( Files.getLastModifiedTime( path ) ) > 0 ;

		final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
		stFuture = scheduler.scheduleAtFixedRate( this , initDelay , period , TimeUnit.SECONDS );
	}

	/**
	 * 内部で作成した ScheduledFutureをキャンセルします。
	 */
	public void stop() {
		if( stFuture != null && !stFuture.isDone() ) {			// 完了(正常終了、例外、取り消し)以外は、キャンセルします。
			LOGGER.info( () -> "DirWatch Stop: [" + sPath  + "]" );
			stFuture.cancel(true);
	//		try {
	//			stFuture.get();									// 必要に応じて計算が完了するまで待機します。
	//		}
	//		catch( InterruptedException | ExecutionException ex) {
	//			LOGGER.info( () -> "DirWatch Stop  Error: [" + sPath  + "]" + ex.getMessage() );
	//		}
		}
	}

	/**
	 * Runnableインターフェースのrunメソッドです。
	 *
	 * 規定のスケジュール時刻が来ると、呼ばれる runメソッドです。
	 *
	 * ここで、条件に一致したPathオブジェクトが存在すれば、コンストラクタで渡した
	 * 関数型インターフェースがcallされます。
	 *
	 * @og.rev 6.8.2.2 (2017/11/02) ネットワークパスのチェックを行います。
	 */
	@Override
	public void run() {
		try {
			LOGGER.debug( () -> "DirWatch Running: " + sPath + " Tree=" + useTree );

			if( Files.exists( sPath ) ) {				// 6.8.2.2 (2017/11/02) ネットワークパスのチェック
				execute( sPath );
			}
			else {
				// MSG0002 = ファイル/フォルダは存在しません。file=[{0}]
				MsgUtil.errPrintln( "MSG0002" , sPath );
			}
		}
		catch( final Throwable th ) {
			// MSG0021 = 予期せぬエラーが発生しました。\n\tﾒｯｾｰｼﾞ=[{0}]
			MsgUtil.errPrintln( th , "MSG0021" , toString() );
		}
	}

	/**
	 * フォルダ階層を順番にスキャンする再帰定義用の関数です。
	 *
	 * run() メソッドから呼ばれます。
	 *
	 * @param	inPpath	検索対象となるパス
	 */
	private void execute( final Path inPpath ) {
		try( final DirectoryStream<Path> stream = Files.newDirectoryStream( inPpath, filter ) ) {
			for( final Path path : stream ) {
				if( Files.isDirectory( path ) ) {
					if( useTree ) { execute( path ); }		// 階層スキャンする場合のみ、再帰処理する。
				}
				else {
					synchronized( action ) {
						action.accept( path );
					}
				}
			}
		}
		catch( final IOException ex ) {
			// MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}]
			throw MsgUtil.throwException( ex , "MSG0005" , inPpath );
		}
	}

	/** main メソッドから呼ばれる ヘルプメッセージです。 {@value}  */
	public static final String USAGE = "Usage: java jp.euromap.eu63.util.DirWatch dir [-SF sufix]... [-S] [-D delay(s)] [-P period(s)] [-T timeDiff(s)]" ;

	/**
	 * 引数に監視対象のフォルダと、拡張子を指定します。
	 *
	 * スキャンパスの初期値は、起動ﾌｫﾙﾀﾞです。
	 *
	 * -D 最初にスキャンを実行するまでの遅延時間(秒)
	 * -P スキャンする間隔(秒)
	 * -T ファイルのタイムスタンプとの差のチェック(秒)
	 * -S 階層スキャンする場合は、-S を付けます。
	 * -SF ファイルの拡張子(後ろからのマッチング)を指定します。複数指定することが出来ます。
	 *
	 * テストなので、６０秒後に、終了します。
	 *
	 * {@value #USAGE}
	 *
	 * @param	args	コマンド引数配列
	 */
	public static void main( final String[] args ) {
		// ********** 【整合性チェック】 **********
		if( args.length < 1 ) {
			System.out.println( USAGE );
			return;
		}

		// ********** 【引数定義】 **********
		long delay		= DirWatch.INIT_DELAY;		// 最初にスキャンを実行するまでの遅延時間(秒) の初期値
		long period		= DirWatch.PERIOD;			// スキャンする間隔(秒) の初期値
		long timeDiff	= DirWatch.TIME_DIFF;		// ファイルのタイムスタンプとの差のチェック(秒) の初期値

		Path	sPath	= new File( "." ).toPath();	// スキャンパス の初期値

		boolean useTree	= false;
		final java.util.List<String> sufixList = new java.util.ArrayList<>();	// main でしか使わないので、import しない。

		// ********** 【引数処理】 **********
		for( int i=0; i<args.length; i++ ) {
			final String arg = args[i];

			// 6.9.8.0 (2018/05/28) FindBugs:ボクシング/アンボクシングはプリミティブを解析する( valueOf → parseLong )
			if(      "-help".equalsIgnoreCase( arg ) ) { System.out.println( USAGE ); return ; }
			else if( "-D".equalsIgnoreCase( arg ) ) { delay    = Long.parseLong( args[++i] ); }		// 記号を見つけたら、次の引数をセットします。
			else if( "-P".equalsIgnoreCase( arg ) ) { period   = Long.parseLong( args[++i] ); }		// 記号を見つけたら、次の引数をセットします。
			else if( "-T".equalsIgnoreCase( arg ) ) { timeDiff = Long.parseLong( args[++i] ); }		// 記号を見つけたら、次の引数をセットします。
			else if( "-S".equalsIgnoreCase( arg ) ) { useTree  = true; continue; }					// 階層処理
			else if( "-SF".equalsIgnoreCase( arg ) ) {
				sufixList.add( args[++i] );															// スキャン対象の拡張子のリスト
			}
			else {
				sPath = new File( arg ).toPath();
			}
		}

		// ********** 【本体処理】 **********
		final DirWatch watch = new DirWatch( sPath , useTree );			// 監視先

		if( !sufixList.isEmpty() ) {
			watch.setPathEndsWith( sufixList.toArray( new String[sufixList.size()] ) );
		}

		watch.start( delay,period,timeDiff );

		try{ Thread.sleep( 60000 ); } catch( final InterruptedException ex ){}		// テスト的に60秒待ちます。

		watch.stop();

		System.out.println( "done." );
	}
}
