/*
 * 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.nio.file.Path;
import java.nio.file.PathMatcher;

import java.util.Set;
import java.util.Locale;
import java.util.Arrays;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * PathMatcherSet は、ファイル監視を行うクラスで利用する、ファイルの選別(PathMatcher)を管理するクラスです。
 *
 *<pre>
 * PathMatcherオブジェクトを複数持っており（Set)それらが、その、判定によって、
 * イベントを起こすかどうか、フィルタリングします。
 *
 *</pre>
 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
 *
 * @version  7.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK1.8,
 */
public class PathMatcherSet implements PathMatcher {
	private static final XLogger LOGGER= XLogger.getLogger( PathMatcherSet.class.getSimpleName() );		// ログ出力

	// パスの照合操作を行うPathMatcher のSetオブジェクト
	private final Set<PathMatcher> pathMchSet = new CopyOnWriteArraySet<>();		// 未設定のときは、すべてにマッチングさせる。(startメソッドで)

	/**
	 * デフォルトコンストラクター
	 *
	 */
	public PathMatcherSet() { super(); }

	/**
	 * すべてのPathMatcherSet を、追加登録します。
	 *
	 * 引数が、null の場合は、登録しません。
	 *
	 * @param	pmSet パスの照合操作のパターン
	 * @return	このセットが変更された場合はtrue
	 */
	public boolean addAll( final PathMatcherSet pmSet ) {
		return pmSet != null && pathMchSet.addAll( pmSet.pathMchSet );
	}

	/**
	 * 内部の PathMatcherに、要素が含まれてい無い場合に、true を返します。
	 *
	 * @return	このセットに要素が1つも含まれていない場合はtrue
	 */
	public boolean isEmpty() {
		return pathMchSet.isEmpty();
	}

	/**
	 * すべての要素をセットから削除します。
	 *
	 */
	public void clear() {
		pathMchSet.clear();
	}

	/**
	 * PathMatcher を、追加登録します。
	 *
	 * 引数が、null の場合は、登録しません。
	 *
	 * @param	pathMch パスの照合操作のパターン
	 * @return	自分自身
	 * @see		java.nio.file.PathMatcher
	 * @see		#addStartsWith(String...)
	 * @see		#addEndsWith(String...)
	 */
	public PathMatcherSet addPathMatcher( final PathMatcher pathMch ) {
	//	LOGGER.debug( () -> "addPathMatcher : PathMatcher=" + pathMch );

		if( pathMch != null ) {
			pathMchSet.add( pathMch );
		}

		return this;
	}

	/**
	 * 指定のパスが、指定の文字列と、先頭一致(startsWith) したパスのみ、有効とします。
	 *
	 * これは、#addPathMatcher(PathMatcher) の簡易指定版です。
	 * 指定の先頭一致(一般にはファイル名の先頭)のうち、ひとつでも一致すれば、true となります。
	 * 先頭文字列の判定には、大文字小文字の区別を行いません。
	 *
	 * @og.rev 7.2.9.4 (2020/11/20) spotbugs:null になっている可能性があるメソッドの戻り値を利用している
	 *
	 * @param	startKey パスの先頭一致のパターン
	 * @return	自分自身
	 * @see		#addPathMatcher(PathMatcher)
	 * @see		#addEndsWith(String...)
	 */
	public PathMatcherSet addStartsWith( final String... startKey ) {
		if( startKey != null ) {
			LOGGER.debug( () -> "addStartsWith : String[]=" + Arrays.toString ( startKey ) );

			pathMchSet.add(
				path -> {
					// 大文字小文字の区別を行いません。
//					final String fname = path.getFileName().toString().toUpperCase(Locale.JAPAN);
					final String fname = FileUtil.pathFileName( path ).toUpperCase(Locale.JAPAN);		// 7.2.9.4 (2020/11/20) Path.getFileName().toString()
					for( final String key : startKey ) {
						if( key == null || key.isEmpty() || fname.startsWith( key.toUpperCase(Locale.JAPAN) ) ) { return true; }
					}
					return false;
				}
			);
		}

		return this;
	}

	/**
	 * 指定のパスが、指定の文字列と、終端一致(endsWith) したパスのみ、有効とします。
	 *
	 * これは、#addPathMatcher(PathMatcher) の簡易指定版です。
	 * 指定の終端文字列(一般には拡張子)のうち、ひとつでも一致すれば、true となります。
	 * 指定しない場合(null)は、すべて許可されたことになります。
	 * 終端文字列の判定には、大文字小文字の区別を行いません。
	 *
	 * @og.rev 7.2.9.4 (2020/11/20) spotbugs:null になっている可能性があるメソッドの戻り値を利用している
	 *
	 * @param	endKey パスの終端一致のパターン
	 * @return	自分自身
	 * @see		#addPathMatcher(PathMatcher)
	 * @see		#addStartsWith(String...)
	 */
	public PathMatcherSet addEndsWith( final String... endKey ) {
		if( endKey != null ) {
			LOGGER.debug( () -> "addEndsWith : String[]=" + Arrays.toString ( endKey ) );

			pathMchSet.add(
				path -> {
					// 大文字小文字の区別を行いません。
//					final String fname = path.getFileName().toString().toUpperCase(Locale.JAPAN);
					final String fname = FileUtil.pathFileName( path ).toUpperCase(Locale.JAPAN);			// 7.2.9.4 (2020/11/20) Path.getFileName().toString()
					for( final String key : endKey ) {
						if( key == null || key.isEmpty() || fname.endsWith( key.toUpperCase(Locale.JAPAN) ) ) { return true; }
					}
					return false;
				}
			);
		}

		return this;
	}

	/**
	 * 指定のパスが、指定の文字列と、あいまい条件で一致したパスのみ、有効とします。
	 *
	 * PREFIX*SUFIX 形式で、'*' を前後に、StartsWithとEndsWithに登録します。
	 * '*'は、一つしか使用できません。正規表現ではなく、簡易的なあいまい検索です。
	 * そのため、ﾌｧｲﾙ名の指定は、一つのみとします。
	 * '*' が存在しない場合は、先頭一致とします。
	 * 指定しない場合(null)は、すべて許可されたことになります。
	 * 終端文字列の判定には、大文字小文字の区別を行いません。
	 *
	 * @og.rev 6.8.1.5 (2017/09/08) ﾌｧｲﾙ名の'*'の処理の見直し
	 * @og.rev 7.2.9.4 (2020/11/20) spotbugs:null になっている可能性があるメソッドの戻り値を利用している
	 *
	 * @param	filename パスの一致のパターン
	 * @return	自分自身
	 * @see		#addStartsWith(String...)
	 * @see		#addEndsWith(String...)
	 */
	public PathMatcherSet addFileName( final String filename ) {
		if( filename != null && !filename.isEmpty() ) {
			LOGGER.debug( () -> "addFileName : filename=" + filename );

			final int ad = filename.indexOf( '*' );		// 分割するためのキーワード

			// 暫定的なチェック：'*' は１箇所しか指定できません。
			if( ad != filename.lastIndexOf( '*' ) ) {	// つまり、２個以上ある。
				// MSG3005 = 検索条件のﾌｧｲﾙ名の指定に、'*'を複数使用することは出来ません。 File=[{0}]
				MsgUtil.errPrintln( "MSG3005" , filename );
				return this;
			}

			if( ad < 0 ) {									// '*'が無い場合は、先頭一致で判定します。
				addStartsWith( filename );
			}
			else if( ad == 0 ) {							// 先頭が、'*' の場合は、後方一致で判定します。
				addEndsWith( filename.substring( 1 ) );
			}
			else if( ad == filename.length()-1 ) {			// 最後が '*'の場合は、先頭一致で判定します。
				addStartsWith( filename.substring( 0,filename.length()-1 ) );
			}
			else {
				final String prefix = filename.substring( 0,ad ).toUpperCase(Locale.JAPAN);
				final String sufix  = filename.substring( ad+1 ).toUpperCase(Locale.JAPAN);

				pathMchSet.add(
					path -> {
						// 大文字小文字の区別を行いません。
//						final String fname = path.getFileName().toString().toUpperCase(Locale.JAPAN);
						final String fname = FileUtil.pathFileName( path ).toUpperCase(Locale.JAPAN);	// 7.2.9.4 (2020/11/20) Path.getFileName().toString()

						if( fname.startsWith( prefix ) && fname.endsWith( sufix ) ) { return true; }	// 両方成立が条件
						return false;
					}
				);

	//			addStartsWith( prefix );				// ゼロ文字列の場合は、true になります。
	//			addEndsWith(   sufix  );				// ゼロ文字列の場合は、true になります。
			}
		}

		return this;
	}

	/**
	 * 指定されたパスがこのマッチャのパターンに一致するかどうかを示します。
	 *
	 * 内部の PathMatcher が、すべて true を返す場合のみ、true を返します。
	 * 未登録の場合は、true が返され、評価されません。
	 * これは、#allMatch( Path ) と同じ結果を返します。
	 *
	 * @param	path 照合するパス
	 * @return	パスがこのマッチャのパターンに一致した場合にのみtrue
	 * @see		#allMatch( Path )
	 */
	@Override
	public boolean matches( final Path path ) {
		return allMatch( path );
	}

	/**
	 * すべての要素が、条件を満たす場合にのみ、有効となります。
	 *
	 * 内部の PathMatcher が、すべて true を返す場合のみ、true を返します。
	 * 未登録の場合は、true が返され、評価されません。
	 * これは、#matches( Path ) と同じ結果を返します。
	 *
	 * @param	path 判定対象の Pathオブジェクト
	 * @return	内部の PathMatcher が、すべて true を返す場合のみ、true
	 * @see		#matches( Path )
	 */
	public boolean allMatch( final Path path ) {
		// stream().allMatch で、Collectionが未登録時も、true ですが、明示的に示しておきます。
		final boolean flag = pathMchSet.isEmpty() || pathMchSet.stream().allMatch( pMch -> pMch.matches( path ) );

		if( flag ) {
			LOGGER.debug( () -> "allMatch : Path=" + path );
		}

		return flag;
	}

	/**
	 * いずれかの要素が、条件を満たす場合に、有効となります。
	 *
	 * 内部の PathMatcher の、いずれかが、 true を返す場合に、true を返します。
	 * 未登録の場合は、true が返され、評価されません。
	 * この動きは、Set#anyMatch(java.util.function.Predicate)とは異なりますので、ご注意ください。
	 *
	 * @param	path 判定対象の Pathオブジェクト
	 * @return	内部の PathMatcher の、いずれかが、 true を返す場合に、true
	 */
	public boolean anyMatch( final Path path ) {
		// stream().anyMatch の場合は、Collectionが未登録時は、false が返る為、明示的に処理が必要です。
		final boolean flag = pathMchSet.isEmpty() || pathMchSet.stream().anyMatch( pMch -> pMch.matches( path ) );

		if( flag ) {
			LOGGER.debug( () -> "anyMatch : Path=" + path );
		}

		return flag;
	}

	/**
	 * 一致する要素が、ひとつも存在しない場合に、有効となります。
	 *
	 * 内部の PathMatcher の要素のすべてに、false を返す場合に、true を返します。
	 * 未登録の場合は、true が返され、評価されません。
	 *
	 * @param	path 判定対象の Pathオブジェクト
	 * @return 内部の PathMatcher の要素のすべてに、false を返す場合に、true
	 */
	public boolean noneMatch( final Path path ) {
		// stream().noneMatch で、Collectionが未登録時も、true ですが、明示的に示しておきます。
		final boolean flag = pathMchSet.isEmpty() || pathMchSet.stream().noneMatch( pMch -> pMch.matches( path ) );

		if( flag ) {
			LOGGER.debug( () -> "noneMatch : Path=" + path );
		}

		return flag;
	}
}
