/*
 * 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.WatchEvent;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.FileSystem;
import java.nio.file.WatchKey;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchService;

import java.util.function.BiConsumer;

/**
 * FileWatch は、ファイル監視を行うクラスです。
 *
 *<pre>
 * ファイルが、追加(作成)、変更、削除された場合に、イベントが発生します。
 * このクラスは、Runnable インターフェースを実装しているため、Thread で実行することで、
 * 個々のフォルダの監視を行います。
 *
 *</pre>
 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
 *
 * @version  7.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK1.8,
 */
public class FileWatch implements Runnable {
	private static final XLogger LOGGER= XLogger.getLogger( FileWatch.class.getName() );		// ログ出力

	/** Path に、WatchService を register するときの作成イベントの簡易指定できるように。 */
	public static final WatchEvent.Kind<Path> CREATE = StandardWatchEventKinds.ENTRY_CREATE ;

	/** Path に、WatchService を register するときの変更イベントの簡易指定できるように。 */
	public static final WatchEvent.Kind<Path> MODIFY = StandardWatchEventKinds.ENTRY_MODIFY ;

	/** Path に、WatchService を register するときの削除イベントの簡易指定できるように。  */
	public static final WatchEvent.Kind<Path> DELETE = StandardWatchEventKinds.ENTRY_DELETE ;

	/** Path に、WatchService を register するときの特定不能時イベントの簡易指定できるように。 */
	public static final WatchEvent.Kind<?>    OVERFLOW = StandardWatchEventKinds.OVERFLOW ;

	// Path に、WatchService を register するときのイベント
	private static final WatchEvent.Kind<?>[] WE_KIND = new WatchEvent.Kind<?>[] {
			CREATE , MODIFY , DELETE , OVERFLOW
	};  

	// Path に、WatchService を register するときの登録方法の修飾子(修飾子 なしの場合)
	private static final WatchEvent.Modifier[] WE_MOD_ONE  = new WatchEvent.Modifier[0];	// Modifier なし

	// Path に、WatchService を register するときの登録方法の修飾子(以下の階層も監視対象にします)
	private static final WatchEvent.Modifier[] WE_MOD_TREE = new WatchEvent.Modifier[] {	// ツリー階層
					com.sun.nio.file.ExtendedWatchEventModifier.FILE_TREE
			};

	/** DirWatch でスキャンした場合のｲﾍﾞﾝﾄ名 {@value} */
	public static final String DIR_WATCH_EVENT = "DirWatch";

	// 監視対象のフォルダ
	private final Path dirPath ;

	// 監視方法
	private final boolean	useTree ;
	private final WatchEvent.Modifier[] extModifiers ;

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

	// Path に、WatchService を register するときのイベント
	private WatchEvent.Kind<?>[] weKind = WE_KIND ;						// 初期値は、すべて

	// パスの照合操作を行うPathMatcher の初期値
	private final PathMatcherSet pathMch = new PathMatcherSet();		// PathMatcher インターフェースを継承

	// DirWatchのパスの照合操作を行うPathMatcher の初期値
	private final PathMatcherSet dirWatchMch = new PathMatcherSet();	// PathMatcher インターフェースを継承

	// 何らかの原因でイベントもれした場合、フォルダスキャンを行います。
	private boolean		useDirWatch	= true;								// 初期値は、イベント漏れ監視を行います。
	private DirWatch	dWatch ;										// DirWatch のstop時に呼び出すための変数

	private Thread		thread ;										// 停止するときに呼び出すため
	private boolean		running ;

	/**
	 * 処理対象のフォルダのパスオブジェクトを指定して、ファイル監視インスタンスを作成します。
	 *
	 * ここでは、指定のフォルダの内のファイルのみ監視します。
	 * これは、new FileWatch( dir , false ) とまったく同じです。
	 *
	 * @param dir	処理対象のフォルダオブジェクト
	 */
	public FileWatch( final Path dir ) {
		this( dir , false );
	}

	/**
	 * 処理対象のフォルダのパスオブジェクトと、監視対象方法を指定して、ファイル監視インスタンスを作成します。
	 *
	 * useTree を true に設定すると、指定のフォルダの内のフォルダ階層を、すべて監視対象とします。
	 *
	 * @param dir	処理対象のフォルダのパスオブジェクト
	 * @param useTree	フォルダツリーの階層をさかのぼって監視するかどうか(true:フォルダ階層を下る)
	 */
	public FileWatch( final Path dir , final boolean useTree ) {
		dirPath		 = dir ;
		this.useTree = useTree;
		extModifiers = useTree ? WE_MOD_TREE : WE_MOD_ONE ;
	}

	/**
	 * 指定のイベントの種類のみ、監視対象に設定します。
	 *
	 * ここで指定したイベントのみ、監視対象になり、callback されます。
	 * 第一引数は、イベントの種類(ENTRY_CREATE,ENTRY_MODIFY,ENTRY_DELETE,OVERFLOW)
	 *
	 * @param	kind 監視対象に設定するイベントの種類
	 * @see		java.nio.file.StandardWatchEventKinds
	 */
	public void setEventKinds( final WatchEvent.Kind<?>... kind ) {
		if( kind != null && kind.length > 0 ) {
			weKind = kind;
		}
	}

	/**
	 * 指定のパスの照合操作で、パターンに一致したパスのみ、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 );
	}

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

	/**
	 * 何らかの原因でイベントを掴み損ねた場合に、フォルダスキャンするかどうかを指定します。
	 *
	 * スキャン開始の遅延時間と、スキャン間隔、ファイルのタイムスタンプとの比較時間等は、
	 * DirWatch の初期値をそのまま使用するため、ここでは指定できません。
	 * 個別に指定したい場合は、このﾌﾗｸﾞをfalse にセットして、個別に、DirWatch を作成してください。
	 * このメソッドでは、#setPathEndsWith( String... )や、#setPathMatcher( PathMatcher ) で
	 * 指定した条件が、そのまま適用されます。
	 *
	 * @param	flag フォルダスキャンするかどうか(true:する/false:しない)
	 * @see		DirWatch
	 */
	public void setUseDirWatch( final boolean flag ) {
		useDirWatch = flag;
	}

	/**
	 * 何らかの原因でイベントを掴み損ねた場合の、フォルダスキャンの対象ファイルの拡張子を指定します。
	 *
	 * このメソッドを使用する場合は、useDirWatch は、true にセットされます。
	 * スキャン開始の遅延時間と、スキャン間隔、ファイルのタイムスタンプとの比較時間等は、
	 * DirWatch の初期値をそのまま使用するため、ここでは指定できません。
	 * このメソッドでは、DirWatch 対象の終端パターンを独自に指定できますが、FileWatch で
	 * で指定した条件も、クリアされるので、含める必要があります。
	 *
	 * @param	endKey パスの終端一致のパターン
	 * @see		DirWatch
	 */
	public void setDirWatchEndsWith( final String... endKey ) {
		if( endKey != null && endKey.length > 0 ) {
			useDirWatch = true;					// 対象があれば、実行するが、true になる。

			dirWatchMch.addEndsWith( endKey );
		}
	}

	/**
	 * フォルダの監視を開始します。
	 *
	 * 自身を、Threadに登録して、Thread#start() を実行します。
	 * 内部の Thread オブジェクトがなければ、新しく作成します。
	 * すでに、実行中の場合は、何もしません。
	 * 条件を変えて、実行したい場合は、stop() メソッドで、一旦スレッドを
	 * 停止させてから、再び、#start() メソッドを呼び出してください。
	 */
	public void start() {
		if( thread == null ) {
			thread = new Thread( this );
			running = true;
			thread.start();
		}

		// 監視漏れのファイルを、一定時間でスキャンする
		if( useDirWatch ) {
			dWatch = new DirWatch( dirPath,useTree );
			if( dirWatchMch.isEmpty() ) {			// 初期値は、未登録時は、本体と同じPathMatcher を使用します。
				dWatch.setPathMatcher( pathMch );
			}
			else {
				dWatch.setPathMatcher( dirWatchMch );
			}
			dWatch.callback( path -> action.accept( DIR_WATCH_EVENT , path ) ) ;	// BiConsumer<String,Path> を Consumer<Path> に変換しています。
			dWatch.start();
		}
	}

	/**
	 * フォルダの監視を終了します。
	 *
	 * 自身を登録しているThreadに、割り込みをかけるため、
	 * Thread#interrupt() を実行します。
	 * フォルダ監視は、ファイル変更イベントが発生するまで待機していますが、
	 * interrupt() を実行すると、強制的に中断できます。
	 * 内部の Thread オブジェクトは、破棄するため、再び、start() メソッドで
	 * 実行再開することが可能です。
	 */
	public void stop() {
		if( thread != null ) {
			thread.interrupt();
			running = false;
			thread = null;
		}

		if( dWatch != null ) {
			dWatch.stop();
			dWatch = null;
		}
	}

	/**
	 * Runnableインターフェースのrunメソッドです。
	 *
	 * 規定のスケジュール時刻が来ると、呼ばれる runメソッドです。
	 */
	@Override
	public void run() {
		try {
			execute();
		}
		catch( final IOException ex ) {
			// MSG3002 = ファイル監視に失敗しました。 Path=[{0}]
			MsgUtil.errPrintln( ex , "MSG3002" , dirPath );
		}
		catch( final Throwable th ) {
			// MSG0021 = 予期せぬエラーが発生しました。\n\tﾒｯｾｰｼﾞ=[{0}]
			MsgUtil.errPrintln( th , "MSG0021" , toString() );
		}
	}

	/**
	 * runメソッドから呼ばれる、実際の処理。
	 *
	 * @og.rev 6.8.1.5 (2017/09/08) LOGGER.debug 情報の追加
	 *
	 * try ･･･ catch( Throwable ) 構文を、runメソッドの標準的な作りにしておきたいため、
	 * あえて、実行メソッドを分けているだけです。
	 */
	private void execute() throws IOException {
		// ファイル監視などの機能は新しいNIO2クラスで拡張されたので
		// 旧File型から、新しいPath型に変換する.
		LOGGER.info( () -> "FileWatch Start: " + dirPath );

		// デフォルトのファイル・システムを閉じることはできません。(UnsupportedOperationException がスローされる)
		// なので、try-with-resources 文 (AutoCloseable) に、入れません。
		final FileSystem fs = dirPath.getFileSystem();			// フォルダが属するファイルシステムを得る()
		// try-with-resources 文 (AutoCloseable)
		// ファイルシステムに対応する監視サービスを構築する.
		// (一つのサービスで複数の監視が可能)
		try( final WatchService watcher = fs.newWatchService() ) {
			// フォルダに対して監視サービスを登録する.
			final WatchKey watchKey = dirPath.register( watcher , weKind , extModifiers );

			// 監視が有効であるかぎり、ループする.
			// (監視がcancelされるか、監視サービスが停止した場合はfalseとなる)
			try{
				boolean flag = true;
				while( flag && running ) {
					// スレッドの割り込み = 終了要求を判定する.
			//		if( Thread.currentThread().isInterrupted() ) {
			//			throw new InterruptedException();
			//		}

					// ファイル変更イベントが発生するまで待機する.
					final WatchKey detecedtWatchKey = watcher.take();

					// イベント発生元を判定する
					if( detecedtWatchKey.equals( watchKey ) ) {
						// 発生したイベント内容をプリントする.
						for( final WatchEvent<?> event : detecedtWatchKey.pollEvents() ) {
							// 追加・変更・削除対象のファイルを取得する.
							// (ただし、overflow時などはnullとなることに注意)
							final Path path = (Path)event.context();
							if( path != null && pathMch.matches( path ) ) {
			//					synchronized( action ) {
									action.accept( event.kind().name() , dirPath.resolve( path ) );
			//					}
							}
						}
					}

					// イベントの受付を再開する.
					detecedtWatchKey.reset();

					// 監視サービスが活きている、または、スレッドの割り込み( = 終了要求)がないことを、をチェックする。
					flag = watchKey.isValid() && !Thread.currentThread().isInterrupted() ;
				}
			}
			catch( final InterruptedException ex ) {
				LOGGER.warning( () -> "【WARNING】 FileWatch Canceled:" + dirPath );
			}
			finally {
				// スレッドの割り込み = 終了要求なので監視をキャンセルしループを終了する.
				if( watchKey != null ) {
					watchKey.cancel();
				}
			}
		}

		LOGGER.info( () -> "FileWatch End: " + dirPath );
	}

	/** main メソッドから呼ばれる ヘルプメッセージです。 {@value}  */
	public static final String USAGE = "Usage: java jp.euromap.eu63.util.FileWatch [[-S] dir]..." ;

	/**
	 * 引数に監視対象のフォルダを複数指定します。
	 *
	 * -S の直後のフォルダは、階層構造を、監視対象にします。
	 * 通常は、直下のフォルダのみの監視です。
	 *
	 * {@value #USAGE}
	 *
	 * @param	args	コマンド引数配列
	 */
	public static void main( final String[] args ) {
		// ********** 【整合性チェック】 **********
		if( args.length < 1 ) {
			System.out.println( USAGE );
			return;
		}

		// ********** 【本体処理】 **********
		final java.util.List<Thread> thList = new java.util.ArrayList<>();

		boolean useTree = false;
		for( final String arg : args ) {
			if(   "-help".equalsIgnoreCase( arg ) ) { System.out.println( USAGE ); return ; }
			else if( "-S".equalsIgnoreCase( arg ) ) { useTree=true; continue; }	// 階層処理

			final Path dir = new File( arg ).toPath();
			final FileWatch watch = new FileWatch( dir ,useTree );	// 監視先

			watch.callback( (event,path) -> {
					System.out.println(  event  +								// イベントの種類
							": path="  + path.toString() );						// ファイルパス(絶対パス)
				} );

			final Thread thread = new Thread( watch );
			thread.start();
			thList.add( thread );

		//	new Thread( watch ).start();
			useTree=false;						// フラグのクリア
		}

		try{ Thread.sleep( 30000 ); } catch( final InterruptedException ex ){}		// テスト的に３０秒待ちます。
		thList.forEach( th -> th.interrupt() );		// テスト的に停止させます。

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