/*
 * 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.plugin.develop;

import org.opengion.fukurou.system.OgBuilder ;				// 6.4.4.1 (2016/03/18)
import org.opengion.hayabusa.develop.AbstractJspCreate;
import org.opengion.hayabusa.develop.JspEnumeration.GROUPING_FUNCTIONS ;
import org.opengion.hayabusa.develop.JspEnumeration.WHERE_OPERATORS ;
import org.opengion.hayabusa.develop.JspConvertEntity;
import org.opengion.fukurou.xml.OGElement;
import static org.opengion.fukurou.util.StringUtil.isNull;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * result.jspの&lt;og:query &gt;タグを作成します。
 *
 * ●使用例
 *      &lt;og:query
 *              command     = "{&#064;command}"
 *              debug       = "{&#064;debug}
 *              dbid        = "{&#064;FROM_DBID}"
 *              maxRowCount = "{&#064;maxRowCount}" &gt;
 *          select A1.xx , A1.xx ,・・・
 *          from   xxx A1 inner join xxx B1
 *          where  ・・・
 *          group by  ・・・
 *          having    ・・・
 *          ORDER BY  ・・・
 *      &lt;/og:query&gt;
 *
 * @og.rev 5.6.1.2 (2013/02/22) 文字列連結から、XML処理するように変更します。
 * @author Takeshi.Takada
 *
 */
public class JspCreate_QUERY extends AbstractJspCreate {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "6.4.4.1 (2016/03/18)" ;

	private static final String SPACE = "                              " ;	// カラムの位置合わせ用

	// 6.3.9.1 (2015/11/27) Variables should start with a lowercase character(PMD)
	private List<JspConvertEntity> queryROWS	;
	private List<JspConvertEntity> resultROWS	;
	private List<JspConvertEntity> constROWS	;
	private List<JspConvertEntity> joinROWS		;
	private List<JspConvertEntity> joinOnROWS	;
	private List<JspConvertEntity> havingROWS	;

	private String ns = "";		// 5.2.1.0 (2010/10/01) 名前空間

	/**
	 * コンストラクター
	 *
	 * インスタンス構築時に、タグ名(key)とファイル名(names)を指定します。
	 *
	 * @og.rev 6.3.9.1 (2015/11/27) コンストラクタを用意して、KEY,NAME をセットするように変更します。
	 */
	public JspCreate_QUERY() {
		super( ":query" , "result" );
	}

	/**
	 * 初期化メソッド
	 *
	 * 内部で使用する JspConvertEntity の リスト のマップを受け取り、初期化を行います。
	 *
	 * @og.rev 5.2.1.0 (2010/10/01) 名前空間を、og 決め打ちから、名前空間指定無しに変更します。
	 *
	 * @param	master	JspConvertEntityのリストのマップ
	 */
	@Override
	protected void init( final Map<String,List<JspConvertEntity>> master ) {
		// 6.3.9.1 (2015/11/27) Variables should start with a lowercase character(PMD)
		queryROWS	= master.get( "QUERY" );
		resultROWS	= master.get( "RESULT" );
		constROWS	= master.get( "CONST" );
		joinROWS	= master.get( "JOIN" );
		joinOnROWS	= master.get( "JOIN_ON" );
		havingROWS	= master.get( "HAVING" );

	}

	/**
	 * JSPに出力するタグの内容を作成します。
	 * 引数より作成前のタグの属性内容を確認するする事が出来ます。
	 *
	 * @og.rev 5.2.1.0 (2010/10/01) メソッドの引数を、OGAttributes から OGElement に変更します。
	 * @og.rev 5.2.1.0 (2010/10/01) 名前空間を、og 決め打ちから、引数を使用するように変更します。
	 * @og.rev 5.6.4.4 (2013/05/31) select カラムに、コメントを付与します。
	 *
	 * @param ele OGElementエレメントオブジェクト
	 * @param	nameSpace	このドキュメントのnameSpace( og とか mis とか )
	 *
	 * @return	変換された文字列
	 * @og.rtnNotNull
	 * @throws Throwable 変換時のエラー
	 */
	@Override
	protected String execute( final OGElement ele , final String nameSpace )  throws Throwable {
		ns = nameSpace.isEmpty() ? "" : nameSpace + ":" ;	// 5.2.1.0 (2010/10/01) 名前空間

		// この OGElement の階層の深さを探ります。
		// ele.getText( para ) とすることでXML全体を階層表示できる。
	//	int para = ele.getParentCount();

		// TODO Auto-generated method stub
		//書き出す文字列を作成開始。

		final List<String> selects		= new ArrayList<>();
		final List<String> clmCmnt		= new ArrayList<>();		// 5.6.4.4 (2013/05/31) select カラムに、コメントを付与します。
		final List<String> tables		= new ArrayList<>();
		final List<String> orders		= new ArrayList<>();
		final List<String> group		= new ArrayList<>();
		final List<String> having_part	= new ArrayList<>();
		final List<String> grp_clm		= new ArrayList<>();		// 6.0.2.5 (2014/10/31) refactoring

		//HAVING情報から<og:query>タグのテキスト部を生成する準備をします。
		// 6.3.9.1 (2015/11/27) Variables should start with a lowercase character(PMD)
		if( isNotEmpty(havingROWS) ){
			for( final JspConvertEntity row : havingROWS ){
				having_part.add(row.getRemarks());
				if( GROUPING_FUNCTIONS.search( row.getRemarks() ) ){
					grp_clm.add( row.getFullColumnName() );		// 6.0.2.5 (2014/10/31) refactoring
				}
			}
		}
		//RESULT情報から<og:query>タグのテキスト部を生成する準備をします。
		boolean grouping = false;
		// 6.3.9.1 (2015/11/27) Variables should start with a lowercase character(PMD)
		if( isNotEmpty(resultROWS) ){
			for( int i=0 ; i<resultROWS.size() ; i++ ){
				final JspConvertEntity result = resultROWS.get(i);
				//Select句の情報を作成
				selects.add( result.getSelectPartColumnName() );
				// 5.6.4.4 (2013/05/31) select カラムに、コメントを付与します。
				clmCmnt.add( result.getTableName() + "." + result.getColumnCommentName() );
				//テーブル名を検証して、テーブル数のみの情報にします。
				if( tables != null && !tables.contains( result.getTableName() ) ) {
					tables.add( result.getFromPartTableName() );
				}
				//並び順に利用するカラムを取得する。
				if( "1".equals( result.getUseOrder() ) ) {
					orders.add(Integer.toString( i + 1 ));
				}
				//GROUP BYに必要な情報を取得します。
				if( GROUPING_FUNCTIONS.contains( result.getRemarks() ) ){
					grouping = true;
		//		}else if(having_grouping_column.indexOf( result.getFullColumnName() ) > -1 ){
		//			group.add( result.getFullColumnName() );
				}else{
					group.add( result.getFullColumnName() );
				}
			}
		}

		//JOIN情報から<og:query>タグのテキスト部(join句)を生成する準備をします。
		// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)

		final JspConvertEntity join_on = isNotEmpty(joinOnROWS) ? joinOnROWS.get( 0 ) : null;		// 6.3.9.1 (2015/11/27)

		//JOIN情報から<og:query><og:where><og:and>タグの検索句を生成する準備をします。
		// 6.3.9.1 (2015/11/27) Variables should start with a lowercase character(PMD)
		if( queryROWS != null && isNotEmpty( constROWS ) ){
			queryROWS.addAll( constROWS );
		}

		final OGElement queryEle  = new OGElement( ns + "query" );
		queryEle.addAttr( "command"		,"{@command}" );
		queryEle.addAttr( "debug"		,"{@debug}" );
		queryEle.addAttr( "dbid"		,"{@FROM_DBID}" );
		queryEle.addAttr( "maxRowCount"	,"{@maxRowCount}" );

		// 6.3.9.1 (2015/11/27) Variables should start with a lowercase character(PMD)
		queryEle.addNode( queryText(selects , clmCmnt , tables , joinROWS  , join_on ) );	// 5.6.4.4 (2013/05/31) select カラムに、コメントを付与

		if( isNotEmpty(queryROWS) ){		// 6.3.9.1 (2015/11/27)
			final OGElement whereEle = new OGElement( ns + "where" );

			for( final JspConvertEntity where : queryROWS ) {		// 6.3.9.1 (2015/11/27)
				if( "QUERY".equals(where.getType()) ){
					whereEle.addNode( andWhereQuery(where.getFullColumnName() , where.getRemarks() ,"{@"+ where.getColumnName() +"}" ,where.isNumber()) );
				}
				if( "CONST".equals(where.getType()) ) {
					whereEle.addNode( andWhereConst( where.getFullColumnName(), where.getRemarks() , where.isNumber()) );
				}
			}
			queryEle.addNode( whereEle );
		}
		if( grouping || !grp_clm.isEmpty() ) {					// 6.0.2.5 (2014/10/31) refactoring
			queryEle.addNode( T2 + "group by " + chainChar(group,",") + CR );
		}
		if( !grp_clm.isEmpty() ){								// 6.0.2.5 (2014/10/31) refactoring
			queryEle.addNode( T2 + "having " + chainChar(having_part ," and ") + CR );
		}
		if( !orders.isEmpty() ){
			queryEle.addNode( apperEle( "ORDER BY" , "ORDER_BY" , orders ) );
		}

		return queryEle.getText(0);
	}

	/**
	 * result.jspのog:queryタグのテキスト部を生成します。
	 *
	 * 補足１
	 * 引数のjoin_onがnullでないときは、優先的にjoin_onの内容でJOIN句を生成します。
	 *
	 * @og.rev 5.6.4.4 (2013/05/31) select カラムに、コメントを付与します。
	 * @og.rev 6.3.9.1 (2015/11/27) 継承されていないため、protected → private に変更します。
	 * @og.rev 6.4.4.1 (2016/03/18) StringBuilderの代わりに、OgBuilderを使用する。
	 *
	 * @param	selects	検索SQLのリスト
	 * @param	clmCmnt	カラムコメントのリスト
	 * @param	tables	テーブル名のリスト
	 * @param	joins	JspConvertEntityのリスト
	 * @param	join_on	JspConvertEntityオブジェクト
	 *
	 * @return	og:queryタグのテキスト部
	 * @og.rtnNotNull
	 */
	private String queryText( final List<String> selects , final List<String> clmCmnt , final List<String> tables ,
								final List<JspConvertEntity> joins ,final JspConvertEntity join_on ) {

		final OgBuilder buf = new OgBuilder()
				.appendCR()
				.appendCR( T2 , "select" );

		// 5.6.4.4 (2013/05/31) select カラムに、コメントを付与します。
		final int size = selects.size();
		for( int i=0; i<size; i++ ) {
			final String clm = selects.get(i) ;
			buf.append( T3 )
				.appendCase( i == 0  , " " , "," )		// flag , true , false
				.appendCR( clm , SPACE.substring( clm.length() )
							, T3 , T3 , T3 , T3
							, "<!-- " , clmCmnt.get(i) , " -->" );
		}

		buf.append( T2 , "from " );

		if( join_on != null ) {
			//JOIN_ONが存在する場合は、直接SQLを組み立てて処理を終了する。
			return buf.append( T2 , join_on.getRemarks() ).toString();
		}

		if( !isNotEmpty( joins ) ) {
			return buf.append( tables.get(0) ).toString();
		}

		//テーブルの内容を構造化します。
		final TableStruct structs = createStruct(joins);

		String beforeLeft  = "";
		String beforeRight = "";
		final StringBuilder sbPre = new StringBuilder( BUFFER_MIDDLE );	// 6.1.0.0 (2014/12/26) refactoring

		final Map<String,String> joinPartsMap = new HashMap<>();

		boolean isStartJoin = false;

		for( int i=0 ; i<joins.size() ; i++ ){
			//join句を作るのとネスト構造を作るのは処理を分離させる。
			final JspConvertEntity join = joins.get( i );

			if( beforeLeft.equals(join.getFromPartTableName()) ){
				//前の処理と左側のテーブルは同じ
				if( ! beforeRight.equals(join.getJoinColumn().getFromPartTableName()) ) {
					//前の処理と右側のテーブルが違う
					sbPre.append( sqlJoinOn( "", join.getJoinColumn().getFromPartTableName() , join.getJoinType()) );
					isStartJoin = true;
				}
			} else {
				//前の処理と左側のテーブルが違う
				if( ! beforeRight.equals(join.getJoinColumn().getFromPartTableName()) ) {
					//前の処理と右側のテーブルが違う
					//前の処理のJoin句をテーブル名別にセット
					final String str = sbPre.toString();
					joinPartsMap.put( beforeLeft, str );
					//バッファを初期化
					sbPre.setLength(0);
					sbPre.append( sqlJoinOn( join.getFromPartTableName() , join.getJoinColumn().getFromPartTableName() , join.getJoinType()) );
					isStartJoin = true;
				}
			}
			if( !isStartJoin  ) {
				sbPre.append( CR ).append( T3 ).append( "and").append( T1 );
			}
			sbPre.append( join.getFullColumnName() )
				.append( T2 ).append( '=' ).append( T1 )		// 6.0.2.5 (2014/10/31) char を append する。
				.append( join.getJoinColumn().getFullColumnName() );
			beforeLeft  = join.getFromPartTableName();
			beforeRight = join.getJoinColumn().getFromPartTableName();
			isStartJoin = false;
		}
		//最終分
		joinPartsMap.put( beforeLeft, sbPre.toString() );

		final StringBuilder sbJoin = new StringBuilder( BUFFER_MIDDLE );
		//Join句を組み立てます。
		createJoinPart(structs.getJoinTables(),joinPartsMap,sbJoin);

		return buf.append( sbJoin.toString() ).toString();

	}

	/**
	 * join句の一部を作成する。
	 *
	 * @og.rev 6.4.4.1 (2016/03/18) StringBuilderの代わりに、OgBuilderを使用する。
	 *
	 * @param	left		join句のレフト
	 * @param	right		join句のライト
	 * @param	join_type	[1:inner join/その他:left outer join]
	 *
	 * @return	join句の一部
	 * @og.rtnNotNull
	 */
	private String sqlJoinOn(final String left , final String right ,final String join_type){
		return new OgBuilder()
				.append( " " , left )
				.appendCase( "1".equals( join_type )
								, " inner join "			// true
								, " left outer join " )		// false
				.appendCR( right )
				.append( T3 , "on" , T1 )
				.toString();

	}

	/**
	 * JOIN句を組み立てます。
	 *
	 * JOIN句は、内部で再帰処理されます。引数の StringBuilder に最終的な JOIN句が格納されます。
	 *
	 * @param	structs	テーブル内容の構造化TableStructのリスト
	 * @param	joinMap	JOIN句マップ
	 * @param	buff	StringBuilderオブジェクト
	 */
	private void createJoinPart( final List<TableStruct> structs , final Map<String,String> joinMap ,final StringBuilder buff ) {
		// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
		for( int i=0 ; i<structs.size() ; i++ ){
			final TableStruct struct = structs.get(i);
			final String part = joinMap.get(struct.getTableName());
			if( part != null ){
				final Matcher matcher = Pattern.compile( "join " + struct.getTableName() ).matcher( buff );
				if( matcher.find()) {
					final int start = matcher.start();
					buff.delete(start,matcher.end());
					buff.insert(start , "join ( " + part + " )" );
				}else{
					buff.append( part );
				}
			}
			createJoinPart(struct.getJoinTables(),joinMap,buff);
		}
	}

	/**
	 * result.jspの og:query og:appear タグを生成します。
	 *
	 * @og.rev 5.2.1.0 (2010/10/01) 名前空間を、og 決め打ちから、引数を使用するように変更します。
	 * @og.rev 6.3.9.1 (2015/11/27) 継承されていないため、protected → private に変更します。
	 *
	 * @param start_key		開始キー
	 * @param value			値
	 * @param default_value 初期値リスト
	 *
	 * @return	og:query og:appear タグ
	 * @og.rtnNotNull
	 */
	private OGElement apperEle( final String start_key , final String value , final List<String> default_value ){

		final OGElement apper = new OGElement( ns + "appear" );
		apper.addAttr( "startKey"		,start_key );
		apper.addAttr( "value"			,"{@" + value + "}" );
		apper.addAttr( "defaultVal"		,chainChar( default_value , "," ) );
		return apper;
	}

	/**
	 * result.jspの og:query og:where og:and タグを生成します。
	 * 処理グループ：QUERY
	 *
	 * @og.rev 6.3.9.1 (2015/11/27) 継承されていないため、protected → private に変更します。
	 *
	 * @param	left		左側式
	 * @param	operator	オペレーター
	 * @param	right		右側式
	 * @param	is_number	数字かどうか[true/false]
	 *
	 * @return	og:and タグ
	 * @og.rtnNotNull
	 */
	private OGElement andWhereQuery( final String left , final String operator , final String right , final boolean is_number){

		final String ope = isNull(operator) ? "eq" : operator ;

		final WHERE_OPERATORS wrOpe = WHERE_OPERATORS.valueOf( ope );

		final OGElement and = new OGElement( ns + "and" );
		and.addAttr( "value" , wrOpe.apply( left , right , is_number ) );
		return and;
	}

	/**
	 * result.jspのog:query og:where og:and タグを生成します。
	 * 処理グループ：CONST
	 *
	 * @og.rev 6.3.9.1 (2015/11/27) 継承されていないため、protected → private に変更します。
	 *
	 * @param	left		左側式
	 * @param	right		右側式
	 * @param	is_number	数字かどうか[true/false]
	 *
	 * @return	og:and タグ
	 */
	private OGElement andWhereConst( final String left , final String right , final boolean is_number ){
		final String operator = right.indexOf( ',' ) >= 0 ? "in" : "eq";
		return  andWhereQuery( left , operator , right , is_number );
	}

	/**
	 * query.jspの og:column タグを生成します。
	 *
	 * @param name			タグのname
	 * @param default_value	初期値
	 *
	 * @return	og:columnタグ
	 */

	/**
	 * テーブルの結合関係を再現する構造体につめ直すメソッド。
	 *
	 * @param	joins	JspConvertEntityのリスト
	 *
	 * @return	テーブルの結合関係を再現する構造体
	 * @og.rtnNotNull
	 */
	private TableStruct createStruct( final List<JspConvertEntity> joins ) {
		final TableStruct st = new TableStruct();
		for( int i=0 ; i<joins.size() ; i++ ){
			final JspConvertEntity join = joins.get( i );
			final String left_name = join.getFromPartTableName();
			final String right_name = join.getJoinColumn().getFromPartTableName();

			TableStruct left = st.getJoinTable( left_name );
			TableStruct right = st.getJoinTable( right_name );

			if( left == null && right == null ) {
				//全くの新規。
				left = new TableStruct();
				left.setTableName( left_name );
				right = new TableStruct();
				right.setTableName(right_name);
				left.addJoinTable( right );
				st.addJoinTable( left );
			}else{
				if( left != null && right == null ){
					right = new TableStruct();
					right.setTableName(right_name);
					left.addJoinTable( right );
				}
			}
		}
		return st;
	}

	/**
	 * テーブルの結合状態を階層構図にする為のオブジェクト
	 *
	 * @author Administrator
	 *
	 */
	private static final class TableStruct {
		private final List<TableStruct> _joins = new ArrayList<>();
		private String tableName;

		/**
		 * テーブル名を設定。
		 *
		 * @param table_name テーブル名
		 */
		public void setTableName( final String table_name ) {
			tableName = table_name;
		}

		/**
		 * テーブル名を取得。
		 *
		 * @return テーブル名
		 */
		public String getTableName() {
			return tableName;
		}

		/**
		 * 結合テーブルを追加。
		 *
		 * @param join_table 結合テーブル
		 */
		public void addJoinTable( final TableStruct join_table ) {
			_joins.add(join_table);
		}

		/**
		 * 結合テーブルを全て取得。
		 *
		 * @return 全ての結合テーブル
		 */
		public List<TableStruct> getJoinTables() {
			return _joins;
		}

		/**
		 * 指定したテーブルを取得。
		 *
		 * @param table_name テーブル名
		 * @return 指定したテーブル
		 */
		public TableStruct getJoinTable( final String table_name ) {
			return search(_joins,table_name);
		}

		/**
		 * テーブル同士が一致しているか検証する。
		 *
		 * @param table_name テーブル名
		 * @return 検証した結果の真偽
		 */
		public boolean equalTable( final String table_name ) {
			return tableName != null && tableName.equals( table_name ) ;
		}

		/**
		 * 指定したテーブルが存在しているか検証する。
		 *
		 * @param table_name テーブル名
		 * @return  検証した結果の真偽
		 */
		public boolean constains( final String table_name ) {
			for( int i=0; i<_joins.size() ; i++ ){
				final TableStruct join = _joins.get( i );
				if( join.equalTable(table_name) ){
					return true;
				}
			}
			return false;
		}

		/**
		 * 結合先を含めて指定したテーブルを取得する。
		 *
		 * @param joins 結合先Listオブジェクト
		 * @param table_name テーブル名
		 * @return 指定したテーブル
		 */
		private TableStruct search( final List<TableStruct> joins , final String table_name ) {
			TableStruct join =  null;
			for( int i=0; i<joins.size() ; i++ ){
				join = joins.get( i );
				if( join.equalTable(table_name) ){
					return join;
				} else {
					join = search(join.getJoinTables(),table_name);
				}
			}
			return join;
		}
	}
}
