/*
 * 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.fukurou.system.OgRuntimeException ;			// 6.4.2.0 (2016/01/29)
import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.hayabusa.db.DBTableModel;
import org.opengion.hayabusa.develop.JspCreateFactory;
import org.opengion.hayabusa.develop.JspConvertEntity;
import org.opengion.fukurou.system.ThrowUtil;					// 6.4.2.0 (2016/01/29)
import org.opengion.fukurou.util.ErrorMessage;
import org.opengion.fukurou.util.StringUtil;
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 org.opengion.fukurou.xml.JspSaxParser;

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

import java.io.File;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Locale ;
// import java.util.Arrays;			// 6.0.2.4 (2014/10/17)
import java.util.Set;											// 6.4.3.4 (2016/03/11)

// import java.io.ObjectOutputStream;
// import java.io.ObjectInputStream;
// import java.io.IOException;

/**
 * JspCreateTag は、画面定義情報より、JSP画面を自動作成するツールです。
 *
 * 画面作成するにあたり、３箇所の参照元が存在します。
 *
 *   １．画面属性の設定 ： 雛形   → 仮画面   ・・・ hpgid で指定の画面ID
 *   ２．仮画面修正     ： 仮画面 → 仮画面   ・・・ jsp/customUserDef/画面ID
 *   ３．本環境修正     ： 本画面 → 本画面   ・・・ jsp/画面ID
 *
 * それぞれ、作成先の画面IDフォルダが存在する場合は、取込元を作成先に設定します。
 * つまり、一度作成すると、それ以降は、作成された画面を基準に処理を行います。
 *
 * @og.formSample
 * ●形式：&lt;og:jspCreate outdir="…" pgid="…" /&gt;
 * ●body：なし
 *
 * ●Tag定義：
 *   &lt;og:jspCreate
 *       pgid             ○【TAG】プログラムIDを指定します(必須)。
 *       outdir             【TAG】作成先のディレクトリを指定します(初期値:HybsSystem#REAL_PATH + "jsp/")
 *       hpgid              【TAG】雛形のプログラムIDを指定します
 *       useUserDef         【TAG】仮環境を使用するかどうか[true/false]を指定します(初期値:true:使用する)
 *       tagNames           【TAG】処理を行うタグを部分指定します(初期値:内部登録タグすべて)
 *       tableId            【TAG】(通常は使いません)DBTableModel が登録されているメモリのキーを指定します
 *       scope              【TAG】キャッシュする場合のスコープ[request/page/session/application]を指定します(初期値:session)
 *       debug              【TAG】デバッグ情報を出力するかどうか[true/false]を指定します(初期値:false)
 *   /&gt;
 *
 * ●使用例
 *    ・先に、query タグでDBTableModel が作成済み
 *          &lt;og:jspCreate
 *              outdir      = "{&#064;SYS.REAL_PATH}jsp/"    ：出力先フォルダ
 *              pgid        = "{&#064;PGID}"                 ：作成画面ID
 *              hpgid       = "{&#064;HPGID}"                ：雛形画面ID
 *              useUserDef  = "true/false"                   ：仮環境の使用可否(初期値:true 使用する)
 *          /&gt;
 *
 * @og.group 開発補助
 * @og.rev 5.1.9.0 (2010/08/01) 新規作成
 *
 * @version  0.9.0  2000/10/17
 * @author   Kazuhiko Hasegawa
 * @since    JDK1.6,
 */
public class JspCreateTag extends CommonTagSupport {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "6.5.0.1 (2016/10/21)" ;
	private static final long serialVersionUID = 650120161021L ;

//	private static String JSP_DIR = HybsSystem.sys( "REAL_PATH" ) + "jsp/" ;
	private static final String JSP_DIR = HybsSystem.sys( "REAL_PATH" ) + "jsp/" ;		// 6.3.9.0 (2015/11/06) Variables should start with a lowercase character(PMD)

	private static final String CUST_DEF = "customUserDef/" ;

	private transient DBTableModel	table	;

	private String		tableId		= HybsSystem.TBL_MDL_KEY;
	private String		outdir		= JSP_DIR;						// 出力先フォルダ
	private String		pgid		;								// 作成画面ID
	private String		hpgid		;								// 雛形画面ID
	private String[]	tagNames	;								// 5.6.8.0 (2013/09/06) 処理を行うタグを部分指定
	private boolean		useUserDef	= true;							// 仮環境の使用可否(初期値:true 使用する)

//	private static final String[] JCF_LIST = { "COMMENT" , "HIDEMENU" , "COLUMN" , "ORDER_BY" , "QUERY" , "VIEW" , "TABLE_UPDATE" };	// 5.6.4.4 (2013/05/31)
	// 6.4.3.4 (2016/03/11) String配列 から、Setに置き換えます。
	private static final Set<String> JCF_SET = new ArraySet<>( "COMMENT" , "HIDEMENU" , "COLUMN" , "ORDER_BY" , "QUERY" , "VIEW" , "TABLE_UPDATE" );

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

	/**
	 * Taglibの開始タグが見つかったときに処理する doStartTag() を オーバーライドします。
	 *
	 * @return	後続処理の指示(SKIP_BODY)
	 */
	@Override
	public int doStartTag() {
		table = (DBTableModel)getObject( tableId );

		return SKIP_BODY ;				// Body を評価しない
	}

	/**
	 * Taglibの終了タグが見つかったときに処理する doEndTag() を オーバーライドします。
	 *
	 * @return	後続処理の指示
	 */
	@Override
	public int doEndTag() {
		// デバッグ時には、オブジェクト内部情報を表示する。
		if( isDebug() ) {
			debugPrint();
		}

		final boolean okFlag = execute();

		// 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
		return okFlag ? EVAL_PAGE : SKIP_PAGE ;

//		if( okFlag )  { 	// 正常
//			return EVAL_PAGE ;
//		}
//		else {
//			return SKIP_PAGE ;
//		}
	}

	/**
	 * タグリブオブジェクトをリリースします。
	 * キャッシュされて再利用されるので、フィールドの初期設定を行います。
	 *
	 * @og.rev 5.6.8.0 (2013/09/06) tagNames 追加
	 */
	@Override
	protected void release2() {
		super.release2();
		table		= null;
		tableId		= HybsSystem.TBL_MDL_KEY;
		outdir		= JSP_DIR;
		pgid		= null;
		hpgid		= null;
		useUserDef	= true;
		tagNames	= null;							// 5.6.8.0 (2013/09/06) 処理を行うタグを部分指定
	}

	/**
	 * DBTableModel処理を実行します。
	 *
	 * @og.rev 5.2.1.0 (2010/10/01) 実行クラスのクラス名を変更します。 _OG_ を削除
	 * @og.rev 5.5.2.6 (2012/05/25) findbugs対応。JspConvertEntity.DBKEY を、JspConvertEntity.getDBKEY() に変更。
	 * @og.rev 5.6.8.0 (2013/09/06) 処理対象タグの部分指定対応 (tagNames 追加)
	 * @og.rev 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
	 * @og.rev 6.4.2.0 (2016/01/29) ex.printStackTrace() を、ThrowUtil#ogStackTrace(Throwable) に置き換え。
	 * @og.rev 6.4.3.4 (2016/03/11) String配列 から、Setに置き換えます。
	 * @og.rev 6.5.0.1 (2016/10/21) ErrorMessage をまとめるのと、直接 Throwable を渡します。
	 *
	 * @return	処理の実行結果
	 */
	public boolean execute() {
//		boolean okFlag = false;

		// 出力先(セーブ先)のフォルダ
		final File saveDir = useUserDef ? new File( outdir + CUST_DEF , pgid ) : new File( outdir , pgid ) ;

		// 雛形(参照元)のフォルダ
		final File refDir ;
		if( saveDir.exists() ) {		// 出力先フォルダが存在した場合
			refDir = saveDir ;			// 出力先フォルダが、雛形フォルダになる。
		}
		else {							// 出力先フォルダが存在しない場合
			refDir = new File( outdir , hpgid );			// 雛形画面IDをそのまま使用します。
			if( ! saveDir.mkdirs() ) {
				final String errMsg = "出力先のフォルダが作成できませんでした。[" + saveDir + "]" ;
				throw new HybsSystemException( errMsg );
			}
		}

		if( ! refDir.exists() ) {	// 雛形(参照元)フォルダが存在しない場合、エラー
			final String errMsg = "雛形(参照元)フォルダが存在しません。[" + refDir + "]" ;
			throw new HybsSystemException( errMsg );
		}

		// 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
		if( table == null ) {
			final String errMsg = "DBTableModel が取得できていません。" + CR
							+ "   tableId=[" + tableId + "] や、scope=[" + getScope() + "] の確認を行ってください。" ;
			throw new OgRuntimeException( errMsg );
		}

		final int[] clmNo = getTableColumnNo( JspConvertEntity.getDBKEY() );		// 5.5.2.6 (2012/05/25) findbugs対応
		final int rowCnt  = table.getRowCount();

		int row = 0;
		boolean unCmntSet = true;
		String[] data = null;
		// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point.
		boolean okFlag = false;
		try {
			//JSPを書く為に必要な情報を設定
			final Map<String,List<JspConvertEntity>> jceLstMap = new HashMap<>();

			for( row=0; row<rowCnt; row++ ) {
				data = table.getValues( row );
				final JspConvertEntity entry = JspConvertEntity.newInstance( data, clmNo );

				if( entry != null ) {
					List<JspConvertEntity> array = jceLstMap.get( entry.getType() );
					if( array == null ) { array = new ArrayList<>(); }
					array.add( entry );
					jceLstMap.put( entry.getType(),array );
					if( unCmntSet ) {
						jceLstMap.put( "COMMENT",array );
						unCmntSet = false;
					}
				}
			}

			// 参照先フォルダから、出力先フォルダに、XMLコピー処理を行います。
			final JspSaxParser spar = new JspSaxParser();

//			// 5.2.1.0 (2010/10/01) 実行クラスのクラス名を変更します。 _OG_ を削除
//	 		// 5.6.8.0 (2013/09/06) 処理対象タグの部分指定対応 (tagNames 追加)
////			if( tagNames == null ) { tagNames = JCF_LIST; }
//			if( tagNames == null ) { tagNames = JCF_SET; }
//			for( final String jcf : tagNames ) {
//				spar.addFilter( JspCreateFactory.newInstance( jcf , jceLstMap ) );
//			}
			// 6.4.3.4 (2016/03/11) String配列 から、Setに置き換えます。
			if( tagNames == null ) {
				// tagnames が null の場合は、全項目を使用します。
				for( final String jcf : JCF_SET ) {
					spar.addFilter( JspCreateFactory.newInstance( jcf , jceLstMap ) );
				}
//				JCF_SET.forEach( jcf -> spar.addFilter( JspCreateFactory.newInstance( jcf , jceLstMap ) );
			}
			else {
				for( final String jcf : tagNames ) {
					spar.addFilter( JspCreateFactory.newInstance( jcf , jceLstMap ) );
				}
			}

			spar.copyDirectry( refDir,saveDir );

			okFlag = true;
		}
		catch( final RuntimeException ex ) {
//			ex.printStackTrace();
			System.err.println( ThrowUtil.ogStackTrace( ex ) );				// 6.4.2.0 (2016/01/29)
			// 6.5.0.1 (2016/10/21) ErrorMessage をまとめるのと、直接 Throwable を渡します。
			final ErrorMessage errMessage = new ErrorMessage( "JspCreateTag Error" )
				.addMessage( row+1,ErrorMessage.NG,"JSPOUT"
					, "PRGID=[" + pgid + "]"
					, StringUtil.array2csv( data )
					)
					.addMessage( ex );
//			final ErrorMessage errMessage = new ErrorMessage( "JspCreateTag Error" );
//			errMessage.addMessage( row+1,ErrorMessage.NG,"JSPOUT",ex.getMessage() );
//			errMessage.addMessage( row+1,ErrorMessage.NG,"JSPOUT",StringUtil.array2csv( data ) );
//			errMessage.addMessage( row+1,ErrorMessage.NG,"JSPOUT","PRGID=[" + pgid + "]" );
			// BAT から呼び出す場合があるため、標準エラー出力にも情報を出しておきます。
			System.out.println( errMessage );
		}

		return okFlag;
	}

	/**
	 * 【TAG】(通常は使いません)結果のDBTableModelを、sessionに登録するときのキーを指定します
	 *		(初期値:HybsSystem#TBL_MDL_KEY[={@og.value HybsSystem#TBL_MDL_KEY}])。
	 *
	 * @og.tag
	 * 検索結果より、DBTableModelオブジェクトを作成します。これを、下流のviewタグ等に
	 * 渡す場合に、通常は、session を利用します。その場合の登録キーです。
	 * query タグを同時に実行して、結果を求める場合、同一メモリに配置される為、
	 * この tableId 属性を利用して、メモリ空間を分けます。
	 *		(初期値:HybsSystem#TBL_MDL_KEY[={@og.value HybsSystem#TBL_MDL_KEY}])。
	 *
	 * @param	id テーブルID (sessionに登録する時のID)
	 */
	public void setTableId( final String id ) {
		tableId = nval( getRequestParameter( id ),tableId );
	}

	/**
	 * 【TAG】作成先のディレクトリを指定します(初期値:HybsSystem#REAL_PATH + "jsp/")。
	 * @og.tag
	 * 作成先のディレクトリを指定します。
	 * 初期値は、実際に実行しているアプリケーションの REAL_PATH + jsp フォルダ以下です。
	 * 作成先のフォルダも、useUserDef の設定によって異なります。
	 *
	 * @og.rev 6.0.2.4 (2014/10/17) メソッド名の統一の為
	 *
	 * @param	dir	出力先のディレクトリ
	 */
	public void setOutDir( final String dir ) {
		outdir = nval( getRequestParameter( dir ),outdir );
	}

	/**
	 * 【TAG】プログラムIDを指定します。
	 *
	 * @og.tag
	 * 作成先のプログラムIDを指定します。
	 * ただし、作成先の実際のフォルダは、useUserDef の設定によって異なります。
	 *
	 * @param	id	プログラムID
	 */
	public void setPgid( final String id ) {
		pgid = nval( getRequestParameter( id ),pgid );
	}

	/**
	 * 【TAG】雛形のプログラムIDを指定します。
	 *
	 * @og.tag
	 * 雛形のプログラムIDをパースして、実際のプログラムを作成します。
	 * ただし、作成先の実際のフォルダは、useUserDef の設定によって異なります。
	 * また、パースするのは、作成先の画面IDのフォルダが存在しない場合のみです。
	 * すでに、存在している場合は、元の画面IDのフォルダを読み取って、パースを
	 * 行います。基本的に、作成先のソースを手で修正した場合でも、パースと
	 * 無関係な箇所の修正はそのまま反映のこされます。
	 *
	 * @param	id	雛形のプログラムID
	 */
	public void setHpgid( final String id ) {
		hpgid = nval( getRequestParameter( id ),hpgid );
	}

	/**
	 * 【TAG】仮環境を使用するかどうか[true/false]を指定します(初期値:true:使用する)。
	 *
	 * @og.tag
	 * true:使用する を設定すると、"customUserDef" フォルダの下に、画面IDの
	 * フォルダを作成します。
	 * false:使用しない を設定すると、実際の リアルパス(REAL_PATH/jsp)の下に、
	 * 画面IDのフォルダを作成します。こちらは、実際の画面と同様に、画面リソース等を
	 * 作成してアクセスすることになります。
	 *
	 * @param	flag	仮環境の使用 [true:使用する/false:使用しない]
	 */
	public void setUseUserDef( final String flag ) {
		useUserDef = nval( getRequestParameter( flag ),useUserDef );
	}

	/**
	 * 【TAG】処理を行うタグを部分指定します(初期値:内部登録タグすべて)。
	 *
	 * @og.tag
	 * 処理を行うタグは、内部的に、COMMENT,HIDEMENU,COLUMN,ORDER_BY,QUERY,VIEW,TABLE_UPDATE だけ
	 * 予約されており、初期値は、すべてのタグを処理します。
	 * ここでは、その一部のみ実行できるように、CSV形式で指定できるようにします。
	 * 実行不可のタグ名を指定すると、エラーになります。
	 *
	 * @og.rev 5.6.8.0 (2013/09/06) 新規追加
	 * @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	tags	処理を行うタグを部分指定
	 */
	public void setTagNames( final String tags ) {
		final String tagNms = nval( getRequestParameter( tags ),null );

		if( tagNms != null ) {
			tagNames = tagNms.split( "," );

			for( int i=0; i<tagNames.length; i++ ) {
				final String tag = tagNames[i].trim().toUpperCase(Locale.JAPAN);
//				if( ! check( tag, JCF_LIST ) ) {
				if( ! check( tag, JCF_SET ) ) {
					final String errMsg = "指定の tagNames は、下記の範囲で指定してください。"	+ CR
									+ "tagNames=[" + tagNms + "] " 								+ CR
//									+ "tagNames List=" + String.join( ", " , JCF_LIST ) ;
									+ "tagNames List=" + String.join( ", " , JCF_SET ) ;
					throw new HybsSystemException( errMsg );

//					final String errMsg = "指定の tagNames は、下記の範囲で指定してください。"
//									+ "tagNames=" + tagNms + " (NG=" + tag + ") : "
//									+ Arrays.toString( JCF_LIST );	// 6.0.2.4 (2014/10/17) USELESS_STRING:配列で toString メソッドを呼び出している
//					throw new HybsSystemException( errMsg );
				}
				tagNames[i] = tag ;		// 大文字に変換した値を戻す。
			}
		}
	}

	/**
	 *  カラム名配列(String[])より、対応するカラムNo配列(int[])を作成します。
	 *
	 * @param	nameArray カラム名配列
	 *
	 * @return	カラムNo配列(可変長引数)
	 */
	private int[] getTableColumnNo( final String... nameArray ) {
		int[] clmNo = new int[nameArray.length];
		for( int i=0; i<clmNo.length; i++ ) {
			clmNo[i] = table.getColumnNo( nameArray[i] );
		}
		return clmNo;
	}

//	/**
//	 * シリアライズ用のカスタムシリアライズ書き込みメソッド。
//	 *
//	 * @param	strm	ObjectOutputStreamオブジェクト
//	 * @serialData 一部のオブジェクトは、シリアライズされません。
//	 *
//	 * @throws IOException	入出力エラーが発生した場合
//	 */
//	private void writeObject( final ObjectOutputStream strm ) throws IOException {
//		strm.defaultWriteObject();
//	}

//	/**
//	 * シリアライズ用のカスタムシリアライズ読み込みメソッド
//	 *
//	 * ここでは、transient 宣言された内部変数の内、初期化が必要なフィールドのみ設定します。
//	 *
//	 * @param	strm	ObjectInputStreamオブジェクト
//	 * @see #release2()
//	 * @serialData 一部のオブジェクトは、シリアライズされません。
//	 *
//	 * @throws IOException	シリアライズに関する入出力エラーが発生した場合
//	 * @throws ClassNotFoundException	クラスを見つけることができなかった場合
//	 */
//	private void readObject( final ObjectInputStream strm ) throws IOException , ClassNotFoundException {
//		strm.defaultReadObject();
//	}

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