package daruma.wfs.filter.predicates;

import daruma.wfs.filter.PredicateDescription;
import daruma.wfs.filter.PropertyPathConverter;

import daruma.xml.util.ElementUtil;
import daruma.xml.util.XMLParseErrorException;
import daruma.xml.DeclaredName ;
import daruma.xml.Lexicon ;
import daruma.xml.UniversalName;
import daruma.xml.URI;

import org.w3c.dom.Element;
import org.w3c.dom.Node;

import daruma.storage_manager.StorageManager;
import daruma.storage_manager.DBMSStorageManager;
import daruma.storage_manager.type_definition.TypeName;
import daruma.storage_manager.type_definition.TypeDefinition;
import daruma.storage_manager.type_definition.TypeException;

import daruma.geometry.TransformationContext;

import daruma.sql.SQLDataType;
import daruma.sql.SQLDataTypeConstant;
import daruma.sql.TableColumnDefinition;
import daruma.sql.SQLTimeFormatConverter;

import daruma.util.ISO8601DateFormat;
import daruma.util.LogWriter;

import daruma.util.Pair;

import java.util.Date;
import java.text.ParsePosition;

import java.util.List;
import java.util.Map;
import java.util.HashMap;

import daruma.util.Itk ;


public class PropertyComparationPredicateDescription
    extends PredicateDescription
{
    private	String		sqlOperator;
    private	String		sqlExpression;

    /* !!! [06/10/16 15:34 I.Noda] !!! 
     * FIXME:
     * 本当はこのあたりは、階層的なexpression対応できるようにしておかないと
     * まずい
     */
    private     String		propertyXPath ;
    private	Element		xpathContext ;
    private	String		value ;

    private	static Map<String, Pair<String, Integer> > operatorMap;

    static
    {
	operatorMap = new HashMap<String, Pair<String, Integer>>();
	operatorMap.put( Lexicon.MispFilterPropertyIsEqualTo.getUniversalName().getLocalName(),
			 new Pair<String, Integer>( "=", 2 ) );
	operatorMap.put( Lexicon.MispFilterPropertyIsLessThan.getUniversalName().getLocalName(),
			 new Pair<String, Integer>( "<", 2 ) );
	operatorMap.put( Lexicon.MispFilterPropertyIsGreaterThan.getUniversalName().getLocalName(),
			 new Pair<String, Integer>( ">", 2 ) );
	operatorMap.put( Lexicon.MispFilterPropertyIsLessThanOrEqualTo.getUniversalName().getLocalName(),
			 new Pair<String, Integer>( "<=", 2 ) );
	operatorMap.put( Lexicon.MispFilterPropertyIsGreaterThanOrEqualTo.getUniversalName().getLocalName(),
			 new Pair<String, Integer>( ">=", 2 ) );
	operatorMap.put( Lexicon.MispFilterPropertyIsNull.getUniversalName().getLocalName(), 
			 new Pair<String, Integer>( "IS NULL", 1 ) );
    }

    public	static	boolean	isAcceptablePredicate( String  localName )
    {
	/* <<< [06/10/16 15:34 I.Noda] <<< */
	return( PropertyComparationPredicateDescription
		.operatorMap.containsKey( localName ) );
	/* === [06/10/16 15:34 I.Noda] === */
	//	return( PropertyComparationPredicateDescription
	//		.operatorTable.getByLocalName( localName ) != null );
	/* >>> [06/10/16 15:34 I.Noda] >>> */
    }


    public	PropertyComparationPredicateDescription
		( Element  comparePredicateElement ,
		  TypeName  typeName ,
		  TypeDefinition  type ,
		  TransformationContext trans ,
		  StorageManager  storage )
	throws XMLParseErrorException
    {
	super( storage , typeName , type , trans );

	/* <<< [06/10/16 15:34 I.Noda] <<< */
	/* === [06/10/16 15:34 I.Noda] === */
	//DeclaredName.Table<String>.Entry entry = 
	//  PropertyComparationPredicateDescription
	//  .operatorTable.getEntry(comparePredicateElement) ;
	//this.sqlOperator = entry.getValue() ;
	//this.tagName = entry.getName() ;
	/* >>> [06/10/16 15:34 I.Noda] >>> */
	
	Pair<String, Integer> entry
	    = operatorMap.get( comparePredicateElement.getLocalName() );

	//this.sqlOperator = entry.getFirst();
	//int nArgs = entry.getSecond();

	//	if ( this.sqlOperator == null )
	if ( entry == null )
	{
		throw new XMLParseErrorException
		    ( "cannot handle filter \""
		      + comparePredicateElement.getLocalName() + "\"" );
	}

	this.sqlExpression = parsePropertyComparation
			     ( comparePredicateElement, entry );
    }

    public	String	getSQLExpression()
    {
	return( this.sqlExpression );
    }

    private	String	parsePropertyComparation( Element  comp,
						  Pair<String, Integer> entry )
	throws XMLParseErrorException
    {
	this.sqlOperator = entry.getFirst();
	int nArgs = entry.getSecond();

	List<Element>	childElements
	    = ElementUtil.getChildElements( comp );

	if ( childElements.size() != nArgs )
	    {
		throw new XMLParseErrorException
		    ( "filter \"" + comp.getLocalName() + "\""
		      + " has invalid number of child elements,"
		      + " expected was " + nArgs );
	    }

	/* !!! [06/10/16 15:34 I.Noda] !!! 
	 * ここでは、比較オペレーションの第1引数は propertyName、
	 * 第2引数は literal と決め打ちしているが、
	 * OGC の Filter Encoding の規格では、
	 * 比較オペレーションの引数は一般的な expression でしかない。
	 * なので、以下のチェック処理は間違い。
	 * 実はそもそも、これをやると、計算などを
	 * 記述することができなくなる。
	 * そういえば、add, sub などはどうなったのか？
	 */

	Element	propertyNameElement = childElements.get(0);
	Element	literalElement = null;
	if ( nArgs >= 2 )
	{
	    literalElement = childElements.get(1);
	}

	// tag name check
	Lexicon.MispPropertyName
	    .matchesOrXmlException(propertyNameElement,
				   "in Property Compariton Operator") ;
	String	literalString = null;
	if ( nArgs >= 2 )
	{
	    Lexicon.MispLiteral
		.matchesOrXmlException(literalElement,
				       "in Property Compariton Operator") ;

	    literalString = ElementUtil.getChildNodesWholeText
					( literalElement );
	}

	String	shortXPath;
	try
	    {
		shortXPath = PropertyPathConverter
		    .convertPropertyElementToShortXPath
		    ( propertyNameElement ,
		      super.getStorage() );
	    }
	catch( XMLParseErrorException  e )
	    {
		throw e;
	    }


	String	sqlValueString = null;

	if ( nArgs >= 2 )
	{
	    sqlValueString = this.convertValueToSQL( shortXPath ,
						     literalString );
	}

	/* !!! [06/08/17 17:28 I.Noda] !!! 
	 * あとあとのために、変数名もできるだけ general な
	 * ものにしておく。
	 */
	String leftArg = shortXPath ;

	String rightArg = null ;

	/* <<< [06/08/17 17:28 I.Noda] <<< 
	 * shortXPath がシステム管理カラムの場合、特別扱い。
	 * 特に _transaction_id_ の場合。
	 */
	if ( nArgs >= 2 )
	{
	    if(shortXPath.equals(DBMSStorageManager
				 .ColName_SystemTransactionID)) {
		rightArg =
		    genSubQueryForTransactionSN(literalString) ;
	    } else {
		rightArg = "'" + sqlValueString + "'" ;
	    }
	}
	/* >>> [06/08/17 17:28 I.Noda] >>> */

	/* <<< [06/10/16 15:34 I.Noda] <<< 
	 * 再利用のための情報を格納しておく
	 * 現状では特に使い道はない。
	 * 再帰的な expression を許すようにする場合、この部分も fix する
	 * 必要がある。
	 */
	this.propertyXPath = 
	    ElementUtil.getChildNodesWholeText(propertyNameElement) ;
	this.xpathContext = propertyNameElement ;
	this.value = literalString ;
	/* >>> [06/10/16 15:34 I.Noda] >>> */

	if ( nArgs >= 2 )
	{
	    return( leftArg + ' ' + this.sqlOperator + ' ' + rightArg ) ;
	}
	else
	{
	    return( leftArg + ' ' + this.sqlOperator );
	}
    }


    private	String	convertValueToSQL( String  columnName ,
					   String  value )
	throws XMLParseErrorException
    {
	SQLDataType sqlType = super.getTypeDefinition().getSingleSQLDataType();

	if ( sqlType == null )
	{
	    List<TableColumnDefinition>	columns;

	    try
	    {
		columns = super.getTypeDefinition()
				.getCompositeSQLDataType
				 ( super.getStorage() , null, null );
	    }
	    catch( TypeException  e )
	    {
		throw new XMLParseErrorException
		    ( "type definition error" , e );
	    }

	    for ( TableColumnDefinition  c  : columns )
	    {
		if ( c.getColumnName().equals( columnName ) )
		{
		    sqlType = c.getSQLDataType();
		    break;
		}
	    }
	}

	/* !!! [06/08/29 11:50 I.Noda] !!! 
	 * システムカラムの扱い
	 */
	if(sqlType == null) {
	    if (columnName.equals(DBMSStorageManager
				  .ColName_SystemTransactionID)) {
		sqlType = new SQLDataType("integer", Integer.class) ;
	    } 
	    else if (columnName.equals(DBMSStorageManager
				       .ColName_SystemCreateTime) ||
		     columnName.equals(DBMSStorageManager
				       .ColName_SystemUpdateTime)) {
		sqlType = SQLDataTypeConstant.TIMESTAMP;
	    }
	}

	if ( sqlType == null )
	{
	    LogWriter.qwrite( "DEBUG" ,
			      "columnName [", columnName, "] not found" );

	    throw new XMLParseErrorException( "property not found" );
	}

	if ( sqlType.isTimeFamilyType() )
	{
	    ISO8601DateFormat dateParser = new ISO8601DateFormat();

	    Date date = dateParser.parse( value , new ParsePosition( 0 ) );

	    if(date == null) {
		throw new XMLParseErrorException
		    ("Illegal datetime format: '" + value + "'"
		     + " It should be yyyy-mm-ddThh:mm:ss.sss"
		     + "[+/-]HH:MM") ;
	    }

	    String dateString = SQLTimeFormatConverter
				  .convertDateToString( date );

	    return( dateString );
	}
	else
	{
	    return( value );
	}
    }


    /* !!! [06/08/17 17:28 I.Noda] !!! */
    /**
     * 比較すべき属性が _transaction_id_ の場合、
     * __transaction_log__ で uri を serial number に変更する必要がある。
     * このための SQL sub query 文を生成
     */
    private String genSubQueryForTransactionSN(String uriString) {
	/* !!! [06/08/21 15:38 I.Noda] !!! 
	 * もし未知の uri が与えられると、null ではなく 0 を返すように変更。
	 * 未知の uri は無限の昔と見なす。
	 */
	return 
	    "ifnull("
	    + "(" + "select " 
	    + DBMSStorageManager.ColName_TransTabSerialNum
	    + " from " 
	    + DBMSStorageManager.TRANSACTION_LOG_TABLE_NAME
	    + " where " 
	    + DBMSStorageManager.ColName_TransTabURI
	    + "='" + uriString + "'"
	    + ")" 
	    + "," + "0" + ")" ;
    }

    //--------------------------------------------------
    /**
     * 詳細チェック
     * 地理検索などのチェックを精密に行う、2nd pass の処理
     */
    public boolean detailedCheck(Node feature) 
	throws XMLParseErrorException
    {
	// 詳細チェックの場合、comparison は必ず真
	return true ;
    }
}
