/*
 * 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.util.Set;									// 7.2.5.0 (2020/06/01)
import java.util.HashSet;								// 7.2.5.0 (2020/06/01)

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.getSimpleName() );		// ログ出力

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

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

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

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

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

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

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

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

	// 指定された遅延時間後または定期的にコマンドを実行するようにスケジュールできるExecutorService
	// 7.2.5.0 (2020/06/01)
	private ScheduledExecutorService scheduler;

	private boolean isError ;		// 7.2.5.0 (2020/06/01) 直前に、処理エラーが発生していれば、true にします。

	// 7.2.5.0 (2020/06/01) ｲﾍﾞﾝﾄが同時に発生する可能性があるので、Setで重複を除外します。
	private final Set<Path> pathSet = new HashSet<>();

	/**
	 * スキャンパスを引数に作成される、コンストラクタです。
	 *
	 * ここでは、階層検索しない(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 ) {
		pathMchSet.addPathMatcher( pathMch );
	}

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

	/**
	 * 内部でScheduledExecutorServiceを作成して、ScheduledFuture に、自身をスケジュールします。
	 *
	 * スキャン開始の遅延時間と、スキャン間隔、ファイルのタイムスタンプとの比較を指定して、スキャンを開始します。
	 * ファイルのタイムスタンプとの差とは、ある一定時間経過したファイルのみ、action をcall します。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) ScheduledExecutorServiceをインスタンス変数にする。
	 *
	 * @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 );
		LOGGER.debug( () -> "DirWatch Start: " + sPath + " Tree=" + useTree + " Delay=" + initDelay + " Period=" + period + " TimeDiff=" + timeDiff );

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

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

	//	7.2.5.0 (2020/06/01)
	//	final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
		if( scheduler == null ) {
			scheduler = Executors.newSingleThreadScheduledExecutor();
		}
		stFuture = scheduler.scheduleAtFixedRate( this , initDelay , period , TimeUnit.SECONDS );
	}

	/**
	 * 内部で作成した ScheduledFutureをキャンセルします。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) ScheduledExecutorServiceを初期化する。
	 */
	public void stop() {
		if( stFuture != null && !stFuture.isDone() ) {			// 完了(正常終了、例外、取り消し)以外は、キャンセルします。
			LOGGER.info( () -> "DirWatch Stop: [" + sPath  + "]" );
			stFuture.cancel(true);								// true は、実行しているスレッドに割り込む必要がある場合。
	//		stFuture.cancel(false);								// false は、実行中のタスクを完了できる。
	//		try {
	//			stFuture.get();									// 必要に応じて計算が完了するまで待機します。
	//		}
	//		catch( InterruptedException | ExecutionException ex) {
	//			LOGGER.info( () -> "DirWatch Stop  Error: [" + sPath  + "]" + ex.getMessage() );
	//		}
		}
		//	7.2.5.0 (2020/06/01)
		// stop 漏れが発生した場合、どれかがstop を呼べば、初期化されるようにしておきます。
		if( scheduler != null ) {
			scheduler.shutdownNow();							// 実行中のアクティブなタスクすべての停止を試みます。
			scheduler = null;
		}
	}

	/**
	 * このフォルダスキャンで、最後に処理した結果が、エラーの場合に、true を返します。
	 *
	 * 対象フォルダが見つからない場合や、検索時にエラーが発生した場合に、true にセットされます。
	 * 正常にスキャンできた場合は、false にリセットされます。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) 新規追加。
	 *
	 * @return	エラー状態(true:エラー,false:正常)
	 */
	public boolean isErrorStatus() {
		return isError;
	}

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

//			if( Files.exists( sPath ) ) {				// 6.8.2.2 (2017/11/02) ネットワークパスのチェック
			if( FileUtil.exists( sPath ) ) {			// 7.2.5.0 (2020/06/01) ネットワークパスのチェック
				execute( sPath );
				isError = false;						// エラーをリセットします。
			}
			else {
				isError = true;							// エラーをセットします。

				// 7.2.5.0 (2020/06/01)
//				MsgUtil.errPrintln( "MSG0002" , sPath );
				// MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞは存在しません。file=[{0}]
				final String errMsg = "DirWatch#run : sPath=" + sPath ;
				LOGGER.warning( "MSG0002" , errMsg );
				stop();
			}
		}
		catch( final Throwable th ) {
			isError = true;								// エラーをセットします。

			// 7.2.5.0 (2020/06/01)
//			MsgUtil.errPrintln( th , "MSG0021" , toString() );
			// MSG0021 = 予期せぬｴﾗｰが発生しました。\n\tﾒｯｾｰｼﾞ=[{0}]
			final String errMsg = "DirWatch#run : Path=" + sPath ;
			LOGGER.warning( th , "MSG0021" , errMsg );
		}
	}

	/**
	 * フォルダ階層を順番にスキャンする再帰定義用の関数です。
	 *
	 * run() メソッドから呼ばれます。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) 大量のﾌｧｲﾙがある場合、FileWatchで重複する部分を削除する
	 *
	 * @param	inPpath	検索対象となるパス
	 */
	private void execute( final Path inPpath ) {
		try( DirectoryStream<Path> stream = Files.newDirectoryStream( inPpath, filter ) ) {
			LOGGER.debug( () -> "DirWatch execute: " + inPpath );
			for( final Path path : stream ) {
				if( Files.isDirectory( path ) ) {
					if( useTree ) { execute( path ); }		// 階層スキャンする場合のみ、再帰処理する。
				}
				else {
					synchronized( action ) {
//						action.accept( path );
						// 7.2.5.0 (2020/06/01) 大量のﾌｧｲﾙがある場合、FileWatchで重複する
						if( setAdd( path ) ) {			// このセット内に、指定された要素がなかった場合はtrue
							action.accept( path );
						}
					}
				}
			}
			setClear();								// 7.2.5.0 (2020/06/01)
		}
		catch( final IOException ex ) {
			// MSG0005 = ﾌｫﾙﾀﾞのﾌｧｲﾙ読み込み時にｴﾗｰが発生しました。file=[{0}]
			throw MsgUtil.throwException( ex , "MSG0005" , inPpath );
		}
	}

	/**
	 * ｽｷｬﾝﾌｧｲﾙの重複ﾁｪｯｸ用SetにPathを追加します。
	 *
	 * このセット内に、指定された要素がなかった場合はtrueを返します。
	 *
	 * @og.rev 1.3.0 (2019/04/01) ｲﾍﾞﾝﾄが同時に発生する可能性があるので、Setで重複を除外します。
	 *
	 * @param	path	登録対象となるパス
	 * @return	このセット内に、指定された要素がなかった場合はtrue
	 */
	public boolean setAdd( final Path path ) {
		return pathSet.add( path );
	}

	/**
	 * ｽｷｬﾝﾌｧｲﾙの重複ﾁｪｯｸ用Setをクリアします。
	 *
	 * 短時間に大量のﾌｧｲﾙを処理する場合にｲﾍﾞﾝﾄとDirWatchが重複したり、
	 * DirWatch 自身が繰返しで重複処理する場合を想定して、同じﾌｧｲﾙ名は処理しません。
	 * ただし、DATﾌｧｲﾙは、基本同じﾌｧｲﾙ名で来るので、あるタイミングでクリアする必要があります。
	 *
	 * @og.rev 1.3.0 (2019/04/01) ｲﾍﾞﾝﾄが同時に発生する可能性があるので、Setで重複を除外します。
	 */
	public void setClear() {
		pathSet.clear();
	}

	/**
	 *このオブジェクトの文字列表現を返します。
	 *
	 * @return	このオブジェクトの文字列表現
	 */
	@Override
	public String toString() {
		return getClass().getSimpleName() + ":" + sPath + " , Tree=[" + useTree + "]" ;
	}
}
