/*
 * 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.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 = "5.6.4.4 (2013/05/31)" ;

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

	private List<JspConvertEntity> QUERY_ROWS ;
	private List<JspConvertEntity> RESULT_ROWS ;
	private List<JspConvertEntity> CONST_ROWS ;
	private List<JspConvertEntity> JOIN_ROWS ;
	private List<JspConvertEntity> JOIN_ON_ROWS ;
	private List<JspConvertEntity> HAVING_ROWS ;

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

	/**
	 * 初期化メソッド
	 *
	 * 内部で使用する JspConvertEntity の リスト のマップを受け取り、初期化を行います。
	 *
	 * @og.rev 5.2.1.0 (2010/10/01) 名前空間を、og 決め打ちから、名前空間指定無しに変更します。
	 *
	 * @param	master	JspConvertEntityのリストのマップ
	 */
	@Override
	protected void init( final Map<String,List<JspConvertEntity>> master ) {
		QUERY_ROWS	= master.get( "QUERY" );
		RESULT_ROWS	= master.get( "RESULT" );
		CONST_ROWS	= master.get( "CONST" );
		JOIN_ROWS	= master.get( "JOIN" );
		JOIN_ON_ROWS= master.get( "JOIN_ON" );
		HAVING_ROWS	= master.get( "HAVING" );

		KEY  = ":query";		// 5.2.1.0 (2010/10/01) 名前空間指定無し
		NAME = "result";
	}

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

		//HAVING情報から<og:query>タグのテキスト部を生成する準備をします。
		if ( isNotEmpty(HAVING_ROWS) ){
			for( final JspConvertEntity row : HAVING_ROWS ){
				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;
		if ( isNotEmpty(RESULT_ROWS) ){
			for(int i = 0 ; i < RESULT_ROWS.size() ; i++){
				final JspConvertEntity result = RESULT_ROWS.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句)を生成する準備をします。
		JspConvertEntity join_on = null;
		if ( isNotEmpty(JOIN_ON_ROWS) ){
			join_on = JOIN_ON_ROWS.get( 0 );
		}
		//JOIN情報から<og:query><og:where><og:and>タグの検索句を生成する準備をします。
		if ( QUERY_ROWS != null && isNotEmpty(CONST_ROWS) ){
			QUERY_ROWS.addAll( CONST_ROWS );
		}

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

		queryEle.addNode( queryText(selects , clmCmnt , tables , JOIN_ROWS , join_on ) );	// 5.6.4.4 (2013/05/31) select カラムに、コメントを付与

		if ( isNotEmpty(QUERY_ROWS) ){

			final OGElement whereEle = new OGElement( ns + "where" );

			for( final JspConvertEntity where : QUERY_ROWS ) {
				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 カラムに、コメントを付与します。
	 *
	 * @param	selects	検索SQLのリスト
	 * @param	clmCmnt	カラムコメントのリスト
	 * @param	tables	テーブル名のリスト
	 * @param	joins	JspConvertEntityのリスト
	 * @param	join_on	JspConvertEntityオブジェクト
	 *
	 * @return	og:queryタグのテキスト部
	 * @og.rtnNotNull
	 */
	protected String queryText( final List<String> selects , final List<String> clmCmnt , final List<String> tables ,
								final List<JspConvertEntity> joins ,final JspConvertEntity join_on ) {
		final StringBuilder sb = new StringBuilder( BUFFER_MIDDLE );
		sb.append( CR ).append( T2 ).append( "select" ).append( CR );
		// 5.6.4.4 (2013/05/31) select カラムに、コメントを付与します。
		final int size = selects.size();
		for( int i=0; i<size; i++ ) {
			sb.append( T3 );
			if( i == 0 ) { sb.append( ' ' ); }		// 6.0.2.5 (2014/10/31) char を append する。
			else         { sb.append( ',' ); }		// 6.0.2.5 (2014/10/31) char を append する。
			final String clm = selects.get(i) ;
			sb.append( clm ).append( SPACE.substring( clm.length() ) )
				.append( T3 ).append( T3 ).append( T3 ).append( T3 )
				.append( "<!-- " ).append( clmCmnt.get(i) ).append( " -->" ).append( CR );
		}

		sb.append( T2 ).append( "from " );

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

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

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

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

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

		boolean isStartJoin = false;

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

			if(before_left.equals(join.getFromPartTableName())){
				//前の処理と左側のテーブルは同じ
				if( ! before_right.equals(join.getJoinColumn().getFromPartTableName()) ) {
					//前の処理と右側のテーブルが違う
					sbPre.append( sqlJoinOn( "", join.getJoinColumn().getFromPartTableName() , join.getJoinType()) );
					isStartJoin = true;
				}
			}else {
				//前の処理と左側のテーブルが違う
				if( ! before_right.equals(join.getJoinColumn().getFromPartTableName()) ) {
					//前の処理と右側のテーブルが違う
					//前の処理のJoin句をテーブル名別にセット
					final String str = sbPre.toString();
					mapJoinParts.put( before_left, 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() );
			before_left  = join.getFromPartTableName();
			before_right = join.getJoinColumn().getFromPartTableName();
			isStartJoin = false;
		}
		//最終分
		mapJoinParts.put( before_left, sbPre.toString() );

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

		return sb.toString();
	}

	/**
	 * join句の一部を作成する。
	 *
	 * @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){
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
			.append( ' ' ).append( left );
		if("1".equals( join_type )){
			buf.append( " inner join " );
		}else{
			buf.append( " left outer join " );
		}
		buf.append( right ).append( CR )
			.append( T3 ).append( "on" ).append( T1 );
		return buf.toString();
	}

	/**
	 * JOIN句を組み立てます。
	 *
	 * JOIN句は、内部で再帰処理されます。引数の StringBuilder に最終的な JOIN句が格納されます。
	 *
	 * @param	structs	テーブル内容の構造化TableStructのリスト
	 * @param	join	JOIN句マップ
	 * @param	buff	StringBuilderオブジェクト
	 */
	private void createJoinPart( final List<TableStruct> structs , final Map<String,String> join ,final StringBuilder buff ) {
		Matcher matcher = null;
		for(int i = 0 ; i < structs.size() ; i++){
			final TableStruct struct = structs.get(i);
			final String part = join.get(struct.getTableName());
			if ( part != null ){
				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(),join,buff);
		}
	}

	/**
	 * result.jspの og:query og:appear タグを生成します。
	 *
	 * @og.rev 5.2.1.0 (2010/10/01) 名前空間を、og 決め打ちから、引数を使用するように変更します。
	 *
	 * @param start_key		開始キー
	 * @param value			値
	 * @param default_value 初期値リスト
	 *
	 * @return	og:query og:appear タグ
	 * @og.rtnNotNull
	 */
	protected 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
	 *
	 * @param	left		左側式
	 * @param	operator	オペレーター
	 * @param	right		右側式
	 * @param	is_number	数字かどうか[true/false]
	 *
	 * @return	og:and タグ
	 * @og.rtnNotNull
	 */
	protected 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
	 *
	 * @param	left		左側式
	 * @param	right		右側式
	 * @param	is_number	数字かどうか[true/false]
	 *
	 * @return	og:and タグ
	 */
	protected 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 class TableStruct {

		private final List<TableStruct> _joins = new ArrayList<TableStruct>();
		private String _table_name;

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

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

		/**
		 * 結合テーブルを追加。
		 *
		 * @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 _table_name != null && _table_name.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;
		}
	}
}
