package daruma.storage_manager.type_definition.types;

import daruma.storage_manager.type_definition.TypeDefinition;
import daruma.storage_manager.type_definition.TypedInstance;
import daruma.storage_manager.type_definition.TypeException;
import daruma.storage_manager.type_definition.NoNeedToConvertTypeException;
import daruma.storage_manager.type_definition.ColumnNameFactory;
import daruma.storage_manager.type_definition.ElementName;
import daruma.storage_manager.StorageManager;
import daruma.storage_manager.StorageException;
import daruma.storage_manager.ElementInfo;

import daruma.geometry.TransformationContext;
import daruma.geometry.TransformationException;
import daruma.geometry.CoordinateSystem;

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

import daruma.xml.UniversalName;
import daruma.xml.SimpleXPath;
import daruma.xml.URI;
import daruma.xml.Lexicon;
import daruma.xml.util.ElementUtil;
import daruma.xml.util.XMLParseErrorException;
import daruma.xml.util.XMLFormatConverter;

import daruma.geometry.DrmGeometry;
import daruma.geometry.DrmPoint;
import daruma.geometry.DrmLineString;
import daruma.geometry.DrmPolygon;
import daruma.geometry.GeometryFormatConverter;
import daruma.geometry.GeometryException;
import daruma.util.Itk;
import daruma.util.Pair;
import daruma.util.EqualChecker;
import daruma.util.LogWriter;
import daruma.global_switch.ImplementationSwitches;

import org.w3c.dom.Element;
import org.w3c.dom.Text;
import org.w3c.dom.Document;

import javax.xml.transform.TransformerException;
import java.util.List;
import java.util.ArrayList;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.xml.transform.TransformerException;

import javax.xml.transform.stream.StreamSource;
import java.io.StringReader;


public class GeometryPropertyTypeDefinition extends TypeDefinition
{
	/* !!! [06/08/16 19:12 I.Noda] !!! 
	 * カラム名の拡張部分の文字列定数
	 */
	final static private String ColNameGeoValueSuffix   = "_geo_" ;
	final static public  String ColNameGeoSrsNameSuffix = "_srs_name_" ;
	final static private String ColNameGeoXmlSuffix     = "_xml_" ;

	final static private int ColIndexGeoValue  = 0;
	final static private int ColIndexSrsName   = 1;
	final static private int ColIndexGeoXml    = 2;

	final static private int NUMBER_OF_COLUMNS = 3;


	/**
	 * a set of element name to restrict geometry types such as gml:Polygon.
	 * if null, all geometry types are allowed.
	 */
	private SortedSet<ElementName> geometryTypeCandidates = null;

	public	GeometryPropertyTypeDefinition()
	{
		super( true , true );
	}

	public	GeometryPropertyTypeDefinition
		    ( SortedSet<ElementName> geometryTypeCandidates )
	{
		this();
		this.geometryTypeCandidates = geometryTypeCandidates;
	}

	public	GeometryPropertyTypeDefinition
		    ( ElementName geometryTypeCandidate )
	{
		this();
		this.geometryTypeCandidates = new TreeSet<ElementName>();
		this.geometryTypeCandidates.add( geometryTypeCandidate );
	}

	@Override
	public SQLDataType	getSingleSQLDataType()
	{
		return( null );
	}

	//------------------------------------------------------------
	/**
	 * Column の定義のリストを返す。
	 */
	@Override
	public	List<TableColumnDefinition>	getCompositeSQLDataType
						  ( StorageManager  storage ,
						    SimpleXPath  path ,
						    ElementInfo  elementInfo )
							throws TypeException
	{
	    /* --------------------
	     * return value は最初は空リスト 
	     */
	    List<TableColumnDefinition>
		ret = new ArrayList<TableColumnDefinition>();

	    /* --------------------
	     * カラム名の本体の取得 
	     */
	    String colNameBody = null ;
	    
	    if( path == null )
	    {
		/* top level element の場合 */
		colNameBody = ColumnNameFactory.getTopLevelElementColumnName();
		if ( elementInfo != null )
		{
		    elementInfo.registerSubElement( new SimpleXPath(), this );
		}
	    }
	    else
	    {
		/* sub level element の場合 */
		SimpleXPath	newPath = new SimpleXPath( path );

		try
		{
		    colNameBody = storage.getShortXPathStringForDB( newPath );
		}
		catch( StorageException  ex )
		{
		    throw new TypeException( ex );
		}

		if ( elementInfo != null )
		{
		    elementInfo.registerSubElement( newPath, this );
		}
	    }

	    /* --------------------
	     * 補助カラムの生成
	     * 別の箇所で参照するため、順序が大事。
	     */
	    /* <<< [09/08/26 21:00 I.Noda] <<<
	     * geometry type に index をつける場合, NOT NULL でないといけない。
	     */
	    String optionString = null;

	    if ( ImplementationSwitches.instance().getCreateGeometryIndex() )
	    {
		optionString = "NOT NULL" ;
	    }
	    ret.add(new TableColumnDefinition
		    (ColumnNameFactory
		     .getColumnName(colNameBody,
				    this.ColNameGeoValueSuffix),
		     new SQLDataType("geometry", optionString,
				     "AsText",DrmGeometry.class)));
	    /* >>> [09/08/26 21:00 I.Noda] >>> */

	    ret.add(new TableColumnDefinition
		    (ColumnNameFactory
		     .getColumnName(colNameBody, this.ColNameGeoSrsNameSuffix),
		     SQLDataTypeConstant.BLOB ) );

	    ret.add(new TableColumnDefinition
		    (ColumnNameFactory
		     .getColumnName(colNameBody, this.ColNameGeoXmlSuffix),
		     SQLDataTypeConstant.BLOB ) );
	    /* >>> [06/08/24 22:36 I.Noda] >>> */

	    return( ret );
	}

    //------------------------------------------------------------
    /**
     * SQL へ入力するための情報を返す。
     * 返す情報は、TypedInstance と、element の子ノードの数のペア。
     * TypedInstance はカラム情報のリストとカラム定義（このクラスインタンス）
     * のペア。
     */
	@Override
	public	Pair<TypedInstance, Integer>
				createInstance( Element  element ,
						ElementName  topLevelElement ,
						SimpleXPath  path ,
						StorageManager  storage ,
						int  elementIndex )
							throws TypeException
	{
		//
		// debug print
		//
		LogWriter.qwrite( "DEBUG", this.getClass().getName(),
				  ": createInstance()" );
		LogWriter.qwrite( "DEBUG", "elementIndex = ", elementIndex );
		LogWriter.qwrite( "DEBUG", element);

		Element	geometryElement;

		try
		{
			geometryElement = ElementUtil.getSingleChildElement
								  ( element );
		}
		catch( XMLParseErrorException  e )
		{
			throw new TypeException( e.getMessage() , e );
		}


		ElementName	geometryTypeName
					= new ElementName( geometryElement );

		if ( geometryElement.getNamespaceURI() == null
		  || ! geometryElement.getNamespaceURI().equals( URI.GML ) )
		{
			throw new TypeException( "unexpected geometry type \""
						 + geometryTypeName
						 + "\"" );
		}

		if ( this.geometryTypeCandidates != null
		     && ! this.geometryTypeCandidates
				   .contains( geometryTypeName ) )
		{
			StringBuilder buf = new StringBuilder();
			buf.append( "geometry type \""
				    + geometryTypeName
				    + "\" is not allowed" );
			buf.append( ": should be " );

			boolean isFirst = true;
			for ( ElementName candidate
				  : this.geometryTypeCandidates )
			{
				if ( ! isFirst )
				{
					buf.append( " or " );
				}
	
				buf.append( '"' );
				buf.append( candidate );
				buf.append( '"' );
				isFirst = false;
			}

			throw new TypeException( buf.toString() );
		}


		String	wktValue;
		String	srsName;
		try
		{
			DrmGeometry	geometry;

			geometry = GeometryFormatConverter
				   .parseFromGMLElement( geometryElement );

			wktValue = GeometryFormatConverter
				   .convertGeometryToWKTString( geometry ,
								true,
								true );

			srsName  = geometry.getSrsName();
		}
		catch( GeometryException  e )
		{
			throw new TypeException
				( "unexpected geometry type \""
				  + geometryTypeName + "\": "
				  + e.getMessage() );
		}


		String	rawXMLString;

		try
		{
			rawXMLString = XMLFormatConverter
					.toString( geometryElement , false );
		}
		catch( TransformerException  e )
		{
			throw new TypeException
				( "unexpected exception,"
				  + " Can't convert XML to string: "
				  + e.getMessage() , e );
		}


		//
		// create return value
		//
		SimpleXPath	p;
		if ( path == null )
		{
			p = null;
		}
		else
		{
			p = new SimpleXPath( path ,
					     new UniversalName( element ) );
		}

		List<TableColumnDefinition>
			defs = this.getCompositeSQLDataType
				    ( storage , p , null );

		List<TableColumn>	columns = new ArrayList<TableColumn>();

		columns.add( new TableColumn
			     ( defs.get(this.ColIndexGeoValue) , wktValue ) );
		
		columns.add( new TableColumn
			     ( defs.get(this.ColIndexSrsName) ,
			       srsName ) );

		columns.add( new TableColumn
			     ( defs.get(this.ColIndexGeoXml) ,
			       rawXMLString ) );

		return( new Pair<TypedInstance, Integer>
			( new TypedInstance( columns , this ) ,
			  element.getChildNodes().getLength() ) );
	}


	@Override
	public	int	convertToXMLElement( Element  element ,
					     Document  doc ,
					     StorageManager  storage ,
					     TransformationContext  trans ,
					     List<TableColumn>  columns ,
					     int  index ,
					     long  id ) throws TypeException
	{
		LogWriter.qwrite("DEBUG",  this.getClass().getName(),
				 ": convertToXMLElement()" );

		if ( index + NUMBER_OF_COLUMNS - 1 >= columns.size() )
		{
		    throw new TypeException
			( "internal error in " + this.getClass().getName()
			  + ": unexpected instantiation" );
		}


		String stringValue = columns.get( index + ColIndexGeoXml )
								.getValue();

		if ( stringValue == null )
		{
		    // means occures == 0
		    return( index + NUMBER_OF_COLUMNS );
		}


		Element	geometryElement;
		try
		{
			geometryElement = XMLFormatConverter.stringToElement
					  ( stringValue, doc );
		}
		catch( TransformerException  e )
		{
			throw new TypeException
				( "unexpected exception,"
				  + " Can't convert string to XML: "
				  + e.getMessage() , e );
		}


		if ( ImplementationSwitches.instance()
		     .getEnableGeometryConversion() )
		{
			DrmGeometry	geometry;
	
			try
			{
				geometry = GeometryFormatConverter
					   .parseFromGMLElement
					    ( geometryElement );

				geometry = transformGeometry( geometry ,
							      trans );

				element.appendChild( GeometryFormatConverter
						     .convertToGMLElement
						      ( doc , geometry ) );
			}
			catch( GeometryException  e )
			{
				// check was done when inserting, will not
				// occur error except version mismatch
				throw new TypeException( "internal error: "
							 + e.getMessage() );
			}
		}
		else
		{
			element.appendChild( geometryElement );
		}

		return( index + NUMBER_OF_COLUMNS );
	}


	public	static	String	convertShortXPathToColumnName
						( String  shortXPathString )
	{
	    return( ColumnNameFactory.getColumnName(shortXPathString,
						    ColNameGeoValueSuffix)) ;
	}

	public	static	String	convertShortXPathToSrsColumnName
						( String  shortXPathString )
	{
	    return( ColumnNameFactory.getColumnName(shortXPathString,
						    ColNameGeoSrsNameSuffix)) ;
	}

	@Override
	public	boolean	shouldSkipColumns( List<TableColumn> columns,
					   int columnIndex )
							throws TypeException
	{
	    if ( columnIndex + NUMBER_OF_COLUMNS - 1 >= columns.size() )
	    {
		throw new TypeException
		    ( "internal error in " + this.getClass().getName()
		      + ": unexpected instantiation" );
	    }

	    String stringValue = columns.get( columnIndex + ColIndexGeoXml )
								.getValue();

	    return (stringValue == null);
	}


	private static DrmGeometry transformGeometry
				    ( DrmGeometry  geometry ,
				      TransformationContext  trans )
					throws NoNeedToConvertTypeException
	{
	    LogWriter.qwrite( "DEBUG",
			      "target SRS = " ,
			      trans.getTargetCoordinateSystem().getSrsName() );

	    LogWriter.qwrite( "DEBUG",
			      "object SRS = " , geometry.getSrsName() );

	    switch( trans.getTransformationType() )
	    {
	    case Selective:
	    case Conv:
		CoordinateSystem target = trans.getTargetCoordinateSystem();
		String objCoord = geometry.getSrsName();

		LogWriter.qwrite( "DEBUG",
				  "target.getSrsName() = ",
				  target.getSrsName() );

		LogWriter.qwrite( "DEBUG",
				  "objCoord = ",
				  objCoord );

		if ( ! EqualChecker.equals( target.getSrsName() , objCoord ) )
		{
			try
			{
				geometry = trans.transform( geometry );
			}
			catch( TransformationException  e )
			{
				// geometry has not changed
				LogWriter.qwrite( "DEBUG",
						  "transform failed:"
						  + e.getMessage() );
			}
		}
		break;

	    case NoConv:
		break;
	    }

	    return geometry;
	}
}
