/*
 * 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 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;

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;

/**
 * result.jspのog:queryタグを作成します。
 *
 * ●使用例
 * 		JspConvertEntity e = new JspConvertEntity();
 * 		e.setTableName("GF92");
 * 		e.setColumnName("CLM");
 * 		List< JspConvertEntity> a = new ArrayList< JspConvertEntity>();
 * 		a.add( e );
 * 		Map< String , List< JspConvertEntity> > m = new HashMap< String , List<JspConvertEntity> >();
 * 		m.put( "RESULT", a );
 * 		JspCreate  j = JspCreateFactory.newInstance( "QUERY",m );
 * 		j.execute("&lt;og:query /&gt;");
 *
 * @author Takeshi.Takada
 *
 */
public class JspCreate_QUERY extends AbstractJspCreate {
	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  = "og:query";
		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 決め打ちから、引数を使用するように変更します。
	 *
	 * @param ele OGElementエレメントオブジェクト
	 * @param	nameSpace	このドキュメントのnameSpace( og とか mis とか )
	 *
	 * @return	変換された文字列
	 * @throws Throwable 変換時のエラー
	 */
	@Override
	protected String execute( final OGElement ele , final String nameSpace )  throws Throwable {
		ns = (nameSpace.length() == 0) ? "" : nameSpace + ":" ;	// 5.2.1.0 (2010/10/01) 名前空間

		// TODO Auto-generated method stub
		//書き出す文字列を作成開始。
		StringBuilder tag = new StringBuilder();

		List<String> selects = new ArrayList<String>();
		List<String> tables = new ArrayList<String>();
		List<String> orders = new ArrayList<String>();
		List<String> group = new ArrayList<String>();
		List<String> having_part = new ArrayList<String>();
		List<String> having_grouping_column = new ArrayList<String>();

		//HAVING情報から<og:query>タグのテキスト部を生成する準備をします。
//		if ( HAVING_ROWS != null && HAVING_ROWS.size() > 0 ){
		if ( isNotEmpty(HAVING_ROWS) ){
			for( JspConvertEntity row : HAVING_ROWS ){
				having_part.add(row.getRemarks());
				if( GROUPING_FUNCTIONS.search( row.getRemarks() ) ){
					having_grouping_column.add( row.getFullColumnName() );
				}
			}
		}
		//RESULT情報から<og:query>タグのテキスト部を生成する準備をします。
		boolean grouping = false;
//		if( RESULT_ROWS != null ) {
		if ( isNotEmpty(RESULT_ROWS) ){
			for(int i = 0 ; i < RESULT_ROWS.size() ; i++){
				JspConvertEntity result = RESULT_ROWS.get(i);
				//Select句の情報を作成
				selects.add( result.getSelectPartColumnName() );
				//テーブル名を検証して、テーブル数のみの情報にします。
				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 (JOIN_ON_ROWS != null && JOIN_ON_ROWS.size() > 0 ) {
		if ( isNotEmpty(JOIN_ON_ROWS) ){
			join_on = JOIN_ON_ROWS.get( 0 );
		}
		//JOIN情報から<og:query><og:where><og:and>タグの検索句を生成する準備をします。
//		if( QUERY_ROWS != null && CONST_ROWS != null && CONST_ROWS.size() > 0 ) {
		if ( QUERY_ROWS != null && isNotEmpty(CONST_ROWS) ){
			QUERY_ROWS.addAll( CONST_ROWS );
		}

//		tag.append("<og:query command=\"{@command}\" debug=\"{@debug}\" dbid=\"{@FROM_DBID}\" maxRowCount=\"{@maxRowCount}\">").append( CR );
		tag.append("<" ).append( ns ).append( "query command=\"{@command}\" debug=\"{@debug}\" dbid=\"{@FROM_DBID}\" maxRowCount=\"{@maxRowCount}\">").append( CR );

		tag.append( queryText(selects , tables , JOIN_ROWS , join_on ) );

		tag.append( CR );
//		if ( QUERY_ROWS != null && QUERY_ROWS.size() > 0 ){
		if ( isNotEmpty(QUERY_ROWS) ){
//			tag.append("\t<og:where>").append( CR );
			tag.append("\t<").append( ns ).append("where>").append( CR );
			for ( JspConvertEntity where : QUERY_ROWS ) {
				if ("QUERY".equals(where.getType())){
					tag.append(andWhereQuery(where.getFullColumnName() , where.getRemarks() ,"{@"+ where.getColumnName() +"}" ,where.isNumber()));
				}
				if ("CONST".equals(where.getType())) {
					tag.append(andWhereConst( where.getFullColumnName(), where.getRemarks() , where.isNumber()));
				}
			}
//			tag.append("\t</og:where>").append( CR );
			tag.append("\t</").append( ns ).append( "where>").append( CR );
		}
//		if ( grouping == true || having_grouping_column.size() > 0 ) {
//		if ( grouping || having_grouping_column.size() > 0 ) {
		if ( grouping || !having_grouping_column.isEmpty() ) {
			tag.append( "\t\tgroup by " ).append(chainChar(group,",")).append( CR );
		}
//		if ( having_grouping_column.size() > 0 ){
		if ( !having_grouping_column.isEmpty() ){
			tag.append( "\t\thaving " ).append(chainChar(having_part ," and ")).append( CR );
		}
//		if ( orders.size() > 0 ){
		if ( !orders.isEmpty() ){
			tag.append( apperText("ORDER BY" , "ORDER_BY" , orders ) );
		}
		tag.append( CR );
//		tag.append("</og:query>").append( CR );
		tag.append("</").append( ns ).append( "query>").append( CR );

		return tag.toString();
	}

	/**
	 * result.jspのog:queryタグのテキスト部を生成します。
	 *
	 * 補足１
	 * 引数のjoin_onがnullでないときは、優先的にjoin_onの内容でJOIN句を生成します。
	 *
	 * @param	selects	検索SQLのリスト
	 * @param	tables	テーブル名のリスト
	 * @param	joins	JspConvertEntityのリスト
	 * @param	join_on	JspConvertEntityオブジェクト
	 *
	 * @return	og:queryタグのテキスト部
	 */
	protected String queryText(
			final List<String> selects , final List<String> tables ,
			final List<JspConvertEntity> joins ,final JspConvertEntity join_on ) {
		StringBuilder sb = new StringBuilder();
		sb.append( "\t\tselect" ).append( CR );
		sb.append( "\t\t\t" );
		sb.append( chainChar( selects , "," ) );
		sb.append( CR );
		sb.append( "\t\tfrom " );

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

//		if( joins == null || joins.isEmpty() ) {
		if( !isNotEmpty( joins ) ) {
			sb.append( tables.get(0) );
			return sb.toString();
		}

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

		String before_left = "";
		String before_right = "";
		StringBuilder sbPre = new StringBuilder("");

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

		boolean isStartJoin = false;

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

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

		StringBuilder sbJoin = new StringBuilder();
		//Join句を組み立てます。
//		sb.append( createJoinPart(structs.getJoinTables(),mapJoinParts,sbJoin).toString() );
		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 join]
	 *
	 * @return	join句の一部
	 */
	private String sqlJoinOn(final String left , final String right ,final String join_type){
		StringBuilder sb = new StringBuilder();
		sb.append( " " ).append( CR );
		sb.append( left );
		if("1".equals( join_type )){
			sb.append( " inner join " );
		}else{
			sb.append( " left join " );
		}
		sb.append( right );
		sb.append( " on" ).append( CR );
		return sb.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++){
			TableStruct struct = structs.get(i);
			String part = join.get(struct.getTableName());
			if ( part != null ){
				matcher = Pattern.compile( "join " + struct.getTableName() + " on").matcher( buff );
				if( matcher.find()) {
					int start = matcher.start();
					buff.delete(start,matcher.end());
					buff.insert(start , "join ( " + part + " ) on");
				}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 タグ
	 */
	protected String apperText( final String start_key , final String value , final List<String> default_value ){
		StringBuilder sb = new StringBuilder();
//		sb.append( "\t<og:appear startKey = \"" );
		sb.append( "\t<").append( ns ).append( "appear startKey = \"" );
		sb.append( start_key );
		sb.append( "\" value = \"{@" );
		sb.append( value );
		sb.append( "}\"" ).append( CR );
		sb.append( "\t\t\tdefaultVal = \"" );
		sb.append( chainChar( default_value , "," ) );
		sb.append( "\" />" ).append( CR );
		return sb.toString();
	}

	/**
	 * result.jspの og:query og:where og:and タグを生成します。
	 * 処理グループ：QUERY
	 *
	 * @param	left		左側式
	 * @param	operator	オペレーター
	 * @param	right		右側式
	 * @param	is_number	数字かどうか[true/false]
	 *
	 * @return	og:and タグ
	 */
	protected String andWhereQuery( final String left , final String operator , final String right , final boolean is_number){
		StringBuilder sb = new StringBuilder();
//		if ( operator == null || operator.trim().length() == 0 ){
//		if ( operator == null || operator.trim().isEmpty() ){
//			operator = "eq";
//		}

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

		WHERE_OPERATORS wrOpe = WHERE_OPERATORS.valueOf( ope );
//		sb.append( "\t\t<og:and value = \"" ).append( wrOpe.apply( left , right , is_number )).append( "\"\t/>" ).append( CR );
		sb.append( "\t\t<").append( ns ).append( "and value = \"" ).append( wrOpe.apply( left , right , is_number )).append( "\"\t/>" ).append( CR );
		return sb.toString();
	}

	/**
	 * result.jspのog:query og:where og:and タグを生成します。
	 * 処理グループ：CONST
	 *
	 * @param	left		左側式
	 * @param	right		右側式
	 * @param	is_number	数字かどうか[true/false]
	 *
	 * @return	og:and タグ
	 */
	protected String andWhereConst( final String left , final String right , final boolean is_number ){
//		String operator = "";
//		if ( right.indexOf( "," ) >= 0 ) {
//			operator = "in";
//		}else{
//			operator = "eq";
//		}
		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タグ
	 */
	protected String columnText( final String name , final String default_value ){
		StringBuilder sb = new StringBuilder();
//		sb.append( "\t<og:column name=\"" );
		sb.append( "\t<").append( ns ).append( "column name=\"" );
		sb.append( name );
		sb.append( "\"" );
		if ( default_value != null ) {
			sb.append( " defaultVal=\"" );
			sb.append( default_value );
			sb.append( "\" " );
		}
		sb.append("/>");
		return sb.toString();
	}

	/**
	 * テーブルの結合関係を再現する構造体につめ直すメソッド
	 *
	 * @param	joins	JspConvertEntityのリスト
	 *
	 * @return	テーブルの結合関係を再現する構造体
	 */
	private TableStruct createStruct( final List<JspConvertEntity> joins ) {
		TableStruct st = new TableStruct();
		for(int i = 0 ; i < joins.size() ; i++){
			JspConvertEntity join = joins.get( i );
			String left_name = join.getFromPartTableName();
			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 String
		 */
		public void setTableName( final String table_name ) {
			_table_name = table_name;
		}

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

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

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

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

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

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

		/**
		 * 結合先を含めて指定したテーブルを取得する。
		 *
		 * @param joins List<TableStruct>
		 * @param table_name String
		 * @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.equals(table_name)){
				if(join.equalTable(table_name)){
					return join;
				}else{
					join = search(join.getJoinTables(),table_name);
				}
			}
			return join;
		}
	}
}
