/*
 * 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.hayabusa.taglib;

import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.fukurou.util.XHTMLTag;
import org.opengion.fukurou.util.Attributes;
import org.opengion.fukurou.util.ToString;						// 6.1.1.0 (2015/01/17)
import org.opengion.fukurou.util.ArraySet;						// 6.4.3.4 (2016/03/11)

import static org.opengion.fukurou.util.StringUtil.nval ;

import java.io.File;
import java.io.FileFilter;
import java.io.Serializable;
import java.util.Set;											// 6.4.3.4 (2016/03/11)
import java.util.Arrays;
import java.util.Comparator;

/**
 * ファイルのプルダウンリストの作成するタグです。
 *
 * SelectタグのBODY部に指定します。
 * 並び替えについては、このタグで指定しますが、ファイルの選別は、
 * BODY 部に記述する fileWhere タグで指定します。
 *
 * @og.formSample
 * ●形式：&lt;og:fileOption from="…" value="[…]" ･･･ &gt;･･･&lt;/og:fileOption&gt;
 * ●body：あり(EVAL_BODY_BUFFERED:BODYを評価し、{&#064;XXXX} を解析します)
 *
 * ●Tag定義：
 *   &lt;og:fileOption
 *       from               【TAG】ファイルの検索元となるディレクトリを指定します (初期値:FILE_URL[=filetemp/])
 *       value              【TAG】Optionの初期値で選ばれる値を指定します
 *       useDir             【TAG】optionリストの作成を、ディレクトリの値で行います。
 *       groupDir           【TAG】optgroupを、ディレクトリの値で作成します(１レベルのみ)。
 *       orderBy            【TAG】検索した結果を表示する表示順をファイル属性名で指定します(初期値:自然順序)
 *       desc               【TAG】表示順を逆転するかどうか[true/false]を指定します(初期値:false)
 *       caseKey            【TAG】このタグ自体を利用するかどうかの条件キーを指定します(初期値:null) 6.8.0.0 (2017/06/02)
 *       caseVal            【TAG】このタグ自体を利用するかどうかの条件値を指定します(初期値:null) 6.8.0.0 (2017/06/02)
 *       caseNN             【TAG】指定の値が、null/ゼロ文字列 でない場合(Not Null=NN)は、このタグは使用されます(初期値:判定しない) 6.8.0.0 (2017/06/02)
 *       caseNull           【TAG】指定の値が、null/ゼロ文字列 の場合は、このタグは使用されます(初期値:判定しない) 6.8.0.0 (2017/06/02)
 *       caseIf             【TAG】指定の値が、true/TRUE文字列の場合は、このタグは使用されます(初期値:判定しない) 6.8.0.0 (2017/06/02)
 *       debug              【TAG】デバッグ情報を出力するかどうか[true/false]を指定します(初期値:false)
 *   &gt;   ... Body ...
 *   &lt;/og:fileOption&gt;
 *
 * ●使用例
 *      ・&lt;og:fileOption val1="ABCD" val2="{&#064;value}" &gt;
 *            &lt;og:fileWhere startsWith="ABCD" ･･･ /&gt;
 *        &lt;/og:fileOption&gt;
 *
 * @og.rev 2.1.1.0 (2002/11/11) 新規作成
 * @og.rev 4.0.0.0 (2005/01/31) 内部ロジック改定
 * @og.group その他入力
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class FileOptionTag extends CommonTagSupport {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "6.8.0.0 (2017/06/02)" ;
	private static final long serialVersionUID = 680020170602L ;

	private String		orderBy		;				// ｿｰﾄ項目
	private boolean		useDir		;				// 6.3.4.0 (2015/08/01) 
	private boolean		groupDir	;				// 6.3.4.0 (2015/08/01) 
	private boolean		desc		;				// 降順ﾌﾗｸﾞ
	private String      from		= HybsSystem.sys( "FILE_URL" );	// 検索起点ﾌｧｲﾙ
	private String		selValue	;				// 選択済み初期値にする場合
	private transient	FileFilter	filter	;		// FileWhere で指定したフィルター

	// 6.4.3.4 (2016/03/11) String配列 から、Setに置き換えます。
	private static final Set<String> ORDER_BY_SET = new ArraySet<>( "NAME","LASTMODIFIED","FILE_LENGTH","LENGTH" );

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
	 */
	public FileOptionTag() { super(); }		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。

	/**
	 * Taglibの開始タグが見つかったときに処理する doStartTag() を オーバーライドします。
	 *
	 * @og.rev 6.8.0.0 (2017/06/02) caseKey,caseVal,caseNN,caseNull 属性を追加
	 *
	 * @return	後続処理の指示( EVAL_BODY_BUFFERED )
	 */
	@Override
	public int doStartTag() {
		// 6.8.0.0 (2017/06/02) caseKey,caseVal,caseNN,caseNull 属性を追加
		return useTag() ? EVAL_BODY_BUFFERED : SKIP_BODY ;
	}

	/**
	 * Taglibのタグ本体を処理する doAfterBody() を オーバーライドします。
	 *
	 * @return	後続処理の指示(SKIP_BODY)
	 */
	@Override
	public int doAfterBody() {
		return SKIP_BODY ;
	}

	/**
	 * Taglibの終了タグが見つかったときに処理する doEndTag() を オーバーライドします。
	 *
	 * @og.rev 3.1.1.2 (2003/04/04) Tomcat4.1 対応。release2() を doEndTag()で呼ぶ。
	 * @og.rev 6.3.4.0 (2015/08/01) useDir 属性と、groupDir 属性 を追加します。
	 * @og.rev 6.8.0.0 (2017/06/02) caseKey,caseVal,caseNN,caseNull 属性を追加
	 *
	 * @return	後続処理の指示
	 */
	@Override
	public int doEndTag() {
		debugPrint();		// 4.0.0 (2005/02/28)
		// 6.8.0.0 (2017/06/02) caseKey,caseVal,caseNN,caseNull 属性を追加
		if( useTag() ) {
			final OptionAncestorIF select = (OptionAncestorIF)findAncestorWithClass( this, OptionAncestorIF.class );
			if( select == null ) {
				final String errMsg = "<b>" + getTagName() + "タグは、SelectTag または、DatalistTag のBODY に記述する必要があります。</b>";
				throw new HybsSystemException( errMsg );
			}
			final Comparator<File> comp = makeComparator( orderBy,desc );
	//		makeLabel( select,comp );
			makeLabel( new File( from ) , select , comp , groupDir );
		}

		return EVAL_PAGE ;
	}

	/**
	 * タグリブオブジェクトをリリースします。
	 * キャッシュされて再利用されるので、フィールドの初期設定を行います。
	 *
	 * @og.rev 3.1.1.2 (2003/04/04) Tomcat4.1 対応。release2() を doEndTag()で呼ぶ。
	 * @og.rev 6.3.4.0 (2015/08/01) useDir 属性と、groupDir 属性 を追加します。
	 */
	@Override
	protected void release2() {
		super.release2();
		orderBy		= null;		// ｿｰﾄ項目
		useDir		= false;	// 6.3.4.0 (2015/08/01) 
		groupDir	= false;	// 6.3.4.0 (2015/08/01) 
		desc		= false;	// 降順ﾌﾗｸﾞ
		from		= HybsSystem.sys( "FILE_URL" );
		filter		= null;
		selValue	= null;
	}

	/**
	 * オプションを作成します。
	 *
	 * ファイル名を "value" に、
	 * BODY属性 に登録するOptionを作成します。
	 *
	 * @og.rev 5.3.4.0 (2011/04/01) FILE_LENGTH 追加
	 *
	 * @param	orderBy	ソートする属性 [NAME/LASTMODIFIED/FILE_LENGTH/LENGTH]
	 * @param	desc	並び順 [true:昇順/false:降順]
	 *
	 * @return	ファイル比較用のComparatorオブジェクト
	 */
	private Comparator<File> makeComparator( final String orderBy,final boolean desc ) {
		if( orderBy == null ) { return null; }

		Comparator<File> comp = null ;

		if( "NAME".equalsIgnoreCase( orderBy ) ) {
			comp = new NameComparator( desc );
		}
		else if( "LASTMODIFIED".equalsIgnoreCase( orderBy ) ) {
			comp = new ModifiedComparator( desc );
		}
		// "LENGTH" を残すのは、互換性のため
		else if( "FILE_LENGTH".equalsIgnoreCase( orderBy ) || "LENGTH".equalsIgnoreCase( orderBy ) ) {
			comp = new LengthComparator( desc );
		}

		return comp ;
	}

	/**
	 * オプションを作成します。
	 *
	 * ﾌｧｲﾙ名を "value" と、BODY属性 に登録するOptionを作成します。
	 *
	 * @og.rev 3.8.0.9 (2005/10/17) 複数選択可能時に全選択を設定する。
	 * @og.rev 6.3.4.0 (2015/08/01) useDir 属性と、groupDir 属性 を追加します。
	 * @og.rev 6.8.0.0 (2017/06/02) フォルダ内のファイルが無い場合は、階層に加えない。
	 *
	 * @param	path	処理の開始パス名(ディレクトリ)
	 * @param	select	SelectTagオブジェクト
	 * @param	comp	並び順を指定するためのComparatorオブジェクト
	 * @param	grpDir	フォルダ階層を、optgroup タグで表すかどうか [true:階層/false:１層]
	 * @return	フォルダ内のファイルが無い場合は階層に加えないためのﾌﾗｸﾞ [true:階層/false:ファイルなし]
	 */
	private boolean makeLabel( final File path , final OptionAncestorIF select , final Comparator<File> comp , final boolean grpDir ) {

		final File[] list = path.listFiles( filter );

		final boolean multipleAll = select.isMultipleAll();		// 3.8.0.9 (2005/10/17)
		if( list != null )  {
			Arrays.sort( list, comp );
			boolean isSet = false;								// 6.8.0.0 (2017/06/02)
			for( int i=0; i<list.length; i++ ) {
				final String value = list[i].getName();
				// 6.3.4.0 (2015/08/01) useDir 属性と、groupDir 属性 を追加
				if( grpDir && list[i].isDirectory() ) {
					select.addOption( "<optgroup label=\"" + value + "\">" );
					if( makeLabel( list[i] , select , comp , false ) ) {	// ﾘｽﾄに追加されたかどうかを判定します。
						select.addOption( "</optgroup>" );
						isSet = true;								// 6.8.0.0 (2017/06/02)
					}
					else {
						select.removeLast();						// 6.8.0.0 (2017/06/02) ﾘｽﾄに追加されなかった場合は、最後に追加した optgroup を削除します。
					}

					continue;
				}
				else {
					// ディレクトリ時のuseDir=true と、ﾌｧｲﾙ時useDir=false でない場合( XOR )時は、処理しない。
					if( list[i].isDirectory() ^ useDir ) { continue; }
				}

				// 6.1.1.0 (2015/01/17) Attributesの連結記述
				final String selected = ( selValue != null && selValue.equalsIgnoreCase( value ) ) || multipleAll
											? "selected" : null ;

				select.addOption(
					XHTMLTag.option(
						new Attributes()
							.set( "value"	, value )
							.set( "selected", selected )
							.set( "body"	, value )
					)
				);
			}
			return isSet;	// 6.8.0.0 (2017/06/02)
		}
		return false;		// 6.8.0.0 (2017/06/02) フォルダ内のファイルが無い場合は、階層に加えない。
	}

	/**
	 * 【TAG】Optionの初期値で選ばれる値を指定します。
	 *
	 * @og.tag
	 * キーになるのは、ファイル属性の NAME です。(ディレクトリなしのファイル名)
	 * ここで value属性に指定した場合、このファイル名と(大文字小文字を無視して)
	 * 一致する場合に、プルダウンの初期値に表示されます。(selected 属性が設定される。)
	 *
	 * @param   val  初期値で選ばれる値
	 */
	public void setValue( final String val ) {
		selValue = nval( getRequestParameter( val ),selValue );
	}

	/**
	 * 【TAG】optionリストの作成を、ディレクトリの値で行います(初期値:false)。
	 *
	 * @og.tag
	 * ファイル検索で、ディレクトリ名のリストで、オプションを作成します。
	 * 初期値は、false (ファイル名でリスト) です。
	 *
	 * @og.rev 6.3.4.0 (2015/08/01) useDir 属性の追加
	 *
	 * @param	flag ディレクトリ名のリストで、オプションを作成するかどうか [true:ディレクトリ/false:ファイル]
	 */
	public void setUseDir( final String flag ) {
		useDir = nval( getRequestParameter( flag ),useDir );
	}

	/**
	 * 【TAG】optgroupを、ディレクトリの値で作成します(１レベルのみ)(初期値:false)。
	 *
	 * @og.tag
	 * optgroupをディレクトリで作成することで、階層のメニューを作成します。
	 * 初期値は、false(通常) です。
	 *
	 * @og.rev 6.3.4.0 (2015/08/01) groupDir 属性の追加
	 *
	 * @param	flag ディレクトリで階層メニューを作成するかどうか [true:階層/false:通常]
	 */
	public void setGroupDir( final String flag ) {
		groupDir = nval( getRequestParameter( flag ),groupDir );
	}

	/**
	 * 【TAG】ファイルの検索元となるディレクトリを指定します
	 *		(初期値:FILE_URL[={@og.value SystemData#FILE_URL}])。
	 *
	 * @og.tag ファイルの検索元となるディレクトリを指定します。
	 * (初期値:システム定数のFILE_URL[={@og.value SystemData#FILE_URL}])。
	 *
	 * @og.rev 4.0.0.0 (2007/11/20) 指定されたディレクトリ名の最後が"\"or"/"で終わっていない場合に、"/"を付加する。
	 * @og.rev 6.4.2.1 (2016/02/05) URLの最後に、"/" を追加する処理を廃止。
	 * @og.rev 6.4.2.1 (2016/02/05) HybsSystem.url2dir に引数追加。
	 *
	 * @param	url ファイルの検索元となるディレクトリ
	 * @see		org.opengion.hayabusa.common.SystemData#FILE_URL
	 */
	public void setFrom( final String url ) {
		final String furl = nval( getRequestParameter( url ),null );
		from = HybsSystem.url2dir( from,furl,"." );			// 6.4.2.1 (2016/02/05)
	}

	/**
	 * 【TAG】検索した結果を表示する表示順をファイル属性名[null/NAME/LASTMODIFIED/FILE_LENGTH]で指定します(初期値:自然順序)。
	 *
	 * @og.tag
	 * ファイルをソートする順(Comparator)を指定します。ソートに指定できる
	 * ファイル属性名は、"NAME","LASTMODIFIED","FILE_LENGTH" の内のどれかひとつです。
	 * 何も指定しない場合は、Fileオブジェクトの自然順序でのソートになります。
	 * (※ 下位互換性のため、LENGTH も残しますが、廃止予定です。)
	 *
	 * @og.rev 3.5.6.2 (2004/07/05) 文字列の連結にStringBuilderを使用します。
	 * @og.rev 4.0.0.0 (2005/01/31) 新規ロジックで改定
	 * @og.rev 5.3.4.0 (2011/04/01) ORDER_BYリストの出力方法 見直し
	 * @og.rev 6.3.4.0 (2015/08/01) Arrays.toString から String.join に置き換え。
	 * @og.rev 6.4.3.4 (2016/03/11) String配列 から、Setに置き換えます。
	 *
	 * @param	ordr  ソートキー [null/NAME/LASTMODIFIED/FILE_LENGTH]
	 */
	public void setOrderBy( final String ordr ) {
		orderBy = nval( getRequestParameter( ordr ),orderBy );

		if( orderBy != null && ! check( orderBy, ORDER_BY_SET ) ) {
			final String errMsg = "orderBy 属性に、下記の属性名以外の値が設定されました。"	+ CR
							+ "orderBy=[" + orderBy + "] " 								+ CR
							+ "orderBy List=" + String.join( ", " , ORDER_BY_SET ) ;
			throw new HybsSystemException( errMsg );

		}
	}

	/**
	 * 【TAG】表示順を逆転するかどうか[true/false]を指定します(初期値:false)。
	 *
	 * @og.tag
	 * orderBy 属性で指定した表示順を、逆順にするかどうかを指定できます。
	 * 初期値は、false (昇順) です。
	 *
	 * @param	flag 表示順を逆転するかどうか [true:逆順/false:昇順]
	 */
	public void setDesc( final String flag ) {
		desc = nval( getRequestParameter( flag ),desc );
	}

	/**
	 * FileFilterオブジェクトをセットします。
	 * これは、BODY 部に登録した、FileWhereタグによって設定された
	 * ファイルフィルターです。
	 *
	 * @param	filter	オブジェクト
	 */
	protected void setFileFilter( final FileFilter filter ) {
		this.filter = filter;
	}

	/**
	 * 名前順でのソート順を指定する Comparator の実体内部クラス
	 *
	 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、なし → private static final class に変更。
	 *
	 * @version  4.0
	 * @author   Kazuhiko Hasegawa
	 * @since    JDK5.0,
	 */
	private static final class NameComparator implements Comparator<File>,Serializable {
		private static final long serialVersionUID = 400020050131L ;	// 4.0.0.0 (2005/01/31)

		private final boolean desc ;

		/**
		 * 名前順での比較を行うオブジェクトを作成します。
		 *
		 * @param desc 表示順逆転 [true:昇順/false:降順]
		 */
		public NameComparator( final boolean desc ) { this.desc = desc; }

		/**
		 * Comparator インターフェースの compare( File,File ) メソッド。
		 *
		 * @param o1 比較元１のファイルオブジェクト
		 * @param o2 比較元２のファイルオブジェクト
		 * @return	最初の引数が 2 番目の引数より小さい場合は負の整数、両方が等しい場合は 0、最初の引数が 2 番目の引数より大きい場合は正の整数
		 */
		public int compare( final File o1, final File o2 ) {
			final File f1 = desc ? o2 : o1 ;
			final File f2 = desc ? o1 : o2 ;
			return f1.getName().compareTo( f2.getName() ) ;
		}
	}

	/**
	 * 更新日順でのソート順を指定する Comparator の実体内部クラス
	 *
	 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、なし → private static final class に変更。
	 *
	 * @version  4.0
	 * @author   Kazuhiko Hasegawa
	 * @since    JDK5.0,
	 */
	private static final class ModifiedComparator implements Comparator<File>,Serializable {
		private static final long serialVersionUID = 400020050131L ;	// 4.0.0.0 (2005/01/31)

		private final boolean desc ;

		/**
		 * 更新日順での比較を行うオブジェクトを作成します。
		 *
		 * @param desc 表示順逆順 [true:昇順/false:降順]
		 */
		public ModifiedComparator( final boolean desc ) { this.desc = desc; }

		/**
		 * Comparator インターフェースの compare( File,File ) メソッド。
		 *
		 * @param o1 比較元１のファイルオブジェクト
		 * @param o2 比較元２のファイルオブジェクト
		 * @return	最初の引数が 2 番目の引数より小さい場合は負の整数、両方が等しい場合は 0、最初の引数が 2 番目の引数より大きい場合は正の整数
		 */
		public int compare( final File o1, final File o2 ) {
			final File f1 = desc ? o2 : o1 ;
			final File f2 = desc ? o1 : o2 ;
			return (int)( f1.lastModified() - f2.lastModified() ) ;
		}
	}

	/**
	 * ファイルサイズ順でのソート順を指定する Comparator の実体内部クラス
	 *
	 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、なし → private static final class に変更。
	 *
	 * @version  4.0
	 * @author   Kazuhiko Hasegawa
	 * @since    JDK5.0,
	 */
	private static final class LengthComparator implements Comparator<File>,Serializable {
		private static final long serialVersionUID = 400020050131L ;	// 4.0.0.0 (2005/01/31)

		private final boolean desc ;

		/**
		 * ファイルサイズでの比較を行うオブジェクトを作成します。
		 *
		 * @param desc 表示順逆順 [true:昇順/false:降順]
		 */
		public LengthComparator( final boolean desc ) { this.desc = desc; }

		/**
		 * Comparator インターフェースの compare( File,File ) メソッド。
		 *
		 * @param o1 比較元１のファイルオブジェクト
		 * @param o2 比較元２のファイルオブジェクト
		 * @return	最初の引数が 2 番目の引数より小さい場合は負の整数、両方が等しい場合は 0、最初の引数が 2 番目の引数より大きい場合は正の整数
		 */
		public int compare( final File o1, final File o2 ) {
			final File f1 = desc ? o2 : o1 ;
			final File f2 = desc ? o1 : o2 ;
			return (int)( f1.length() - f2.length() ) ;
		}
	}

	/**
	 * このオブジェクトの文字列表現を返します。
	 * 基本的にデバッグ目的に使用します。
	 *
	 * @return このクラスの文字列表現
	 * @og.rtnNotNull
	 */
	@Override
	public String toString() {
		return ToString.title( this.getClass().getName() )
				.println( "VERSION"		,VERSION	)
				.println( "orderBy"		,orderBy	)
				.println( "desc"		,desc		)
				.println( "from"		,from		)
				.println( "selValue"	,selValue	)
				.println( "Other..."	,getAttributes().getAttribute() )
				.fixForm().toString() ;
	}
}
