package daruma.geometry;

import java.util.List;
import java.util.ArrayList;

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

import daruma.geometry.DrmPoint;
import daruma.geometry.DrmLineString;
import daruma.geometry.DrmPolygon;
import daruma.geometry.DrmBox;

import daruma.xml.URI;
import daruma.xml.util.ElementUtil;
import daruma.xml.util.XMLParseErrorException;

import daruma.storage_manager.type_definition.ElementName;

import daruma.util.LogWriter;


public class GeometryFormatConverter
{
	private	GeometryFormatConverter()
	{
	}

	public	static	DrmPoint parseGMLPoint( String  coordinatesString )
						    throws GeometryException
	{
		String[]	values = coordinatesString.trim().split( "," );

		if ( values.length < 2 )
		{
			throw new GeometryException
			  ( "expected at least 2 values: "
			    + "\"" + coordinatesString + "\"" );
		}


		double	x;
		try
		{
			x = Double.parseDouble( values[0] );
		}
		catch( NumberFormatException  e )
		{
			throw new GeometryException
			  ( "\"" + values[0] + "\""
			    + " is not a valid number" );
		}

		double	y;
		try
		{
			y = Double.parseDouble( values[1] );
		}
		catch( NumberFormatException  e )
		{
			throw new GeometryException
			  ( "\"" + values[1] + "\""
			    + " is not a valid number" );
		}

		if ( values.length <= 2 )
		{
			return( new DrmPoint( x , y ) );
		}
		else
		{
			double	z;
			try
			{
				z = Double.parseDouble( values[2] );
			}
			catch( NumberFormatException  e )
			{
				throw new GeometryException
				  ( "\"" + values[2] + "\""
				    + " is not a valid number" );
			}

			return( new DrmPoint( x , y , z ) );
		}
	}

	public	static	DrmLineString	parseGMLLineString
					  ( String  coordinatesString )
						    throws GeometryException
	{
		DrmLineString	ret = new DrmLineString();

		String[]	points = coordinatesString
						.trim().split( "[ \t\n\r]+" );

		for ( String  pointString : points )
		{
			try
			{
				ret.add( GeometryFormatConverter
					 .parseGMLPoint( pointString ) );
			}
			catch( GeometryException  e )
			{
				throw e;
			}
		}

		return( ret );
	}


	private static	List<DrmPoint>	parseCoordinatesElement
					  ( Element  coordinatesElement )
						    throws GeometryException
	{
		List<DrmPoint>	ret = new ArrayList<DrmPoint>();

		if ( coordinatesElement.getNamespaceURI() == null
		  || ! coordinatesElement.getNamespaceURI()
			.equals( URI.GML )
		  || ! coordinatesElement.getLocalName()
			.equals( "coordinates" ) )
		{
			throw new GeometryException
				( "unexpected element \""
				  + coordinatesElement.getLocalName()
				  + "\" in namespace "
				  + coordinatesElement.getNamespaceURI()
				  + ", expexted was "
				  + "\"coordinates\" element in namespace "
				  + URI.GML );
		}

		String	coordinatesString
			  = ElementUtil.getChildNodesWholeText
						( coordinatesElement );

		String[]	points = coordinatesString
						.trim().split( "[ \t\n\r]+" );

		for ( String  pointString : points )
		{
			try
			{
				ret.add( GeometryFormatConverter
					 .parseGMLPoint( pointString ) );
			}
			catch( GeometryException  e )
			{
				throw e;
			}
		}

		return( ret );
	}

	public	static	DrmPolygon	parseGMLBox( String  coordinatesString )
						    throws GeometryException
	{
		String[]	points = coordinatesString
						.trim().split( "[ \t\n\r]+" );

		List<DrmPoint>	pointList = new ArrayList<DrmPoint>();

		for ( String  pointString : points )
		{
			try
			{
				pointList.add( GeometryFormatConverter
					       .parseGMLPoint( pointString ) );
			}
			catch( GeometryException  e )
			{
				throw e;
			}
		}

		if ( pointList.size() != 2 )
		{
			throw new GeometryException
				  ( "invalid Box format,"
				    + " 2 points expected" );
		}

		DrmPoint	pMin = pointList.get(0);
		DrmPoint	pMax = pointList.get(1);

		return( new DrmBox( pMin, pMax ) );
	}


	public	static	String	convertGeometryToWKTString( DrmGeometry  geom ,
							    boolean dropTo2D )
	{
		if ( geom instanceof DrmPoint )
		{
			return( GeometryFormatConverter
				.convertPointToWKTString( (DrmPoint)geom ,
							  dropTo2D ) );
		}
		else if ( geom instanceof DrmLineString )
		{
			return( GeometryFormatConverter
				.convertLineStringToWKTString
				 ( (DrmLineString)geom ,
				   dropTo2D ) );
		}
		else if ( geom instanceof DrmPolygon )
		{
			return( GeometryFormatConverter
				.convertPolygonToWKTString
				 ( (DrmPolygon)geom ,
				   dropTo2D ) );
		}
		else
		{
			return( null );
		}
	}

	public	static	String	convertCoordinatesToWKTString
					( List<DrmPoint>  points ,
					  boolean dropTo2D )
	{
		StringBuffer	buf = new StringBuffer();

		buf.append( "(" );

		boolean	firstPoint = true;
		for ( DrmPoint  p : points )
		{
			if ( ! firstPoint )
			{
				buf.append( "," );
			}

			buf.append( p.getX() );
			buf.append( " " );
			buf.append( p.getY() );

			if ( ! dropTo2D && p.getDimension() >= 3 )
			{
				buf.append( " " );
				buf.append( p.getZ() );
			}

			firstPoint = false;
		}

		buf.append( ")" );

		return( buf.toString() );
	}

	public	static	String	convertPointToWKTString( DrmPoint  point ,
							 boolean dropTo2D )
	{
		StringBuffer	buf = new StringBuffer();

		buf.append( "POINT(" );
		buf.append( point.getX() );
		buf.append( " " );
		buf.append( point.getY() );

		if ( ! dropTo2D && point.getDimension() >= 3 )
		{
			buf.append( " " );
			buf.append( point.getZ() );
		}

		buf.append( ")" );

		return( buf.toString() );
	}

	public	static	String	convertLineStringToWKTString
						( DrmLineString  lineString ,
						  boolean dropTo2D )
	{
		StringBuffer	buf = new StringBuffer();

		buf.append( "LINESTRING" );

		buf.append( convertCoordinatesToWKTString
			    ( lineString.getPoints() , dropTo2D ) );

		return( buf.toString() );
	}

	public	static	String	convertPolygonToWKTString
						( DrmPolygon  polygon ,
						  boolean dropTo2D )
	{
		StringBuffer	buf = new StringBuffer();

		buf.append( "POLYGON(" );

		buf.append( convertCoordinatesToWKTString
			    ( polygon.getOuterBoundary().getPoints() ,
			      dropTo2D ) );

		for ( DrmLinearRing  r : polygon.getInnerBoundaries() )
		{
			buf.append( "," );

			buf.append( convertCoordinatesToWKTString
				    ( r.getPoints() , dropTo2D ) );
		}

		buf.append( ")" );

		return( buf.toString() );
	}

	public	static	String	convertCoordinateToGMLString
						( DrmPoint  point )
	{
		StringBuffer	buf = new StringBuffer();

		buf.append( point.getX() );
		buf.append( "," );
		buf.append( point.getY() );

		if ( point.getDimension() >= 3 )
		{
			buf.append( "," );
			buf.append( point.getZ() );
		}

		return( buf.toString() );
	}

	public	static	String	convertCoordinatesToGMLString
					( List<DrmPoint>  points )
	{
		StringBuffer	buf = new StringBuffer();

		boolean	firstPoint = true;
		for ( DrmPoint  p : points )
		{
			if ( ! firstPoint )
			{
				buf.append( " " );
			}

			buf.append( convertCoordinateToGMLString( p ) );

			firstPoint = false;
		}

		return( buf.toString() );
	}


	public	static	DrmGeometry  parseFromGMLElement
					    ( Element  geometryElement )
						    throws GeometryException
	{
		ElementName	geometryTypeElementName
					= new ElementName( geometryElement );

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


		// XXX
		Element	childElement = null;
		if ( ! geometryTypeElementName.getLocalName()
				  .equals( "Polygon" )
		  && ! geometryTypeElementName.getLocalName()
				  .equals( "MultiPolygon" )
		  && ! geometryTypeElementName.getLocalName()
				  .equals( "MultiLineString" )
		  && ! geometryTypeElementName.getLocalName()
				  .equals( "MultiPoint" ) )
		{
			try
			{
				childElement
					= ElementUtil.getSingleChildElement
							( geometryElement );
			}
			catch( XMLParseErrorException  e )
			{
				throw new GeometryException
					( e.getMessage() , e );
			}
		}


		DrmGeometry	geom;

		String	coordinatesString;
		try
		{
			if ( geometryTypeElementName.getLocalName()
			     .equals( "Point" ) )
			{
				coordinatesString
				  = GeometryFormatConverter
				    .getCoordinatesString( childElement );

				geom = GeometryFormatConverter
					.parseGMLPoint
					 ( coordinatesString );
			}
			else if ( geometryTypeElementName.getLocalName()
				  .equals( "LineString" ) )
			{
				coordinatesString
				  = GeometryFormatConverter
				    .getCoordinatesString( childElement );

				geom = GeometryFormatConverter
					.parseGMLLineString
					 ( coordinatesString );
			}
			else if ( geometryTypeElementName.getLocalName()
				  .equals( "Polygon" ) )
			{
				geom = GeometryFormatConverter
					.parseGMLPolygonElement
					 ( geometryElement );
			}
			else if ( geometryTypeElementName.getLocalName()
				  .equals( "MultiPolygon" ) )
			{
				geom = GeometryFormatConverter
					.parseGMLMultiPolygonElement
					 ( geometryElement );
			}
			else if ( geometryTypeElementName.getLocalName()
				  .equals( "MultiLineString" ) )
			{
				// XXX
				geom = new DrmPoint( 0.0 , 0.0 );
			}
			else if ( geometryTypeElementName.getLocalName()
				  .equals( "MultiPoint" ) )
			{
				// XXX
				geom = new DrmPoint( 0.0 , 0.0 );
			}
			else if ( geometryTypeElementName.getLocalName()
				  .equals( "Box" ) )
			{
				coordinatesString
				  = GeometryFormatConverter
				    .getCoordinatesString( childElement );

				geom = GeometryFormatConverter
					.parseGMLBox( coordinatesString );
			}
			else
			{
				throw new GeometryException
					( "unexpected geometry type \""
					  + geometryTypeElementName + "\"" );
			}
		}
		catch( GeometryException  e )
		{
			throw e;
		}


		//if ( XmlConst.GmlAttrSrsName.hasAttributeNS( e ) )
		//if ( ElementUtil.hasAttributeNS( XmlConst.GmlAttrSrsName ) )
		if ( geometryElement.hasAttributeNS( null , "srsName" ) )
		{
			geom.setSrsName( geometryElement.getAttributeNS
					 ( null , "srsName" ) );
		}

		return( geom );
	}

	private	static	String	getCoordinatesString( Element  element )
						      throws GeometryException
	{
		if ( element.getNamespaceURI() == null
		  || ! element.getNamespaceURI().equals( URI.GML )
		  || ! element.getLocalName().equals( "coordinates" ) )
		{
			throw new GeometryException
				( "unexpected element \""
				  + element.getLocalName()
				  + "\" in namespace "
				  + element.getNamespaceURI()
				  + ", expexted was "
				  + "\"coordinates\" element in namespace "
				  + URI.GML );
		}

		return( ElementUtil.getChildNodesWholeText( element ) );
	}


	private	static	String	getPolygonCoordinatesString
					( Element  linearRingElement )
						      throws GeometryException
	{
		Element	coordinatesElement;

		try
		{
			if ( linearRingElement.getNamespaceURI() == null
			  || ! linearRingElement.getNamespaceURI()
			       .equals( URI.GML )
			  || ! linearRingElement.getLocalName()
			       .equals( "LinearRing" ) )
			{
				throw new GeometryException
				( "unexpected element \""
				  + linearRingElement.getLocalName()
				  + "\" in namespace "
				  + linearRingElement.getNamespaceURI()
				  + ", expexted was "
				  + "\"LinearRing\" element in namespace "
				  + URI.GML );
			}

			coordinatesElement
				= ElementUtil.getSingleChildElement
						( linearRingElement );
		}
		catch( XMLParseErrorException  e )
		{
			throw new GeometryException( e.getMessage() , e );
		}

		if ( coordinatesElement.getNamespaceURI() == null
		  || ! coordinatesElement.getNamespaceURI()
			.equals( URI.GML )
		  || ! coordinatesElement.getLocalName()
			.equals( "coordinates" ) )
		{
			throw new GeometryException
				( "unexpected element \""
				  + linearRingElement.getLocalName()
				  + "\" in namespace "
				  + linearRingElement.getNamespaceURI()
				  + ", expexted was "
				  + "\"coordinates\" element in namespace "
				  + URI.GML );
		}

		return( ElementUtil.getChildNodesWholeText
						( coordinatesElement ) );
	}


	private static	DrmLinearRing	parseGMLLinearRingElement
							( Element  element )
						      throws GeometryException
	{
		Element	coordinatesElement;

		try
		{
			Element	linearRingElement
				= ElementUtil.getSingleChildElement( element );

			LogWriter.qwrite("DEBUG",  "linearRingElement = ",
					 linearRingElement.getLocalName());

			if ( linearRingElement.getNamespaceURI() == null
			  || ! linearRingElement.getNamespaceURI()
			       .equals( URI.GML )
			  || ! linearRingElement.getLocalName()
			       .equals( "LinearRing" ) )
			{
				throw new GeometryException
				( "unexpected element \""
				  + linearRingElement.getLocalName()
				  + "\" in namespace "
				  + linearRingElement.getNamespaceURI()
				  + ", expexted was "
				  + "\"LinearRing\" element in namespace "
				  + URI.GML );
			}

			coordinatesElement
				= ElementUtil.getSingleChildElement
						( linearRingElement );
		}
		catch( XMLParseErrorException  e )
		{
			throw new GeometryException( e.getMessage() , e );
		}


		List<DrmPoint>	points = GeometryFormatConverter
					   .parseCoordinatesElement
					      ( coordinatesElement );

		if ( points.size() == 0 )
		{
			throw new GeometryException
				( "invalid LinearRing format,"
				  + " no points" );
		}

		return( new DrmLinearRing( points ) );
	}

	private static	DrmPolygon	parseGMLPolygonElement
					    ( Element  polygonElement )
						      throws GeometryException
	{
		if ( polygonElement.getNamespaceURI() == null
		  || ! polygonElement.getNamespaceURI().equals( URI.GML )
		  || ! polygonElement.getLocalName().equals( "Polygon" ) )
		{
			throw new GeometryException
				( "unexpected element \""
				  + polygonElement.getLocalName()
				  + "\" in namespace "
				  + polygonElement.getNamespaceURI()
				  + ", expexted was "
				  + "\"Polygon\" element in namespace "
				  + URI.GML );
		}

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

		if ( childElements.size() == 0 )
		{
			throw new GeometryException
				( "too few child elements of \""
				  + polygonElement.getLocalName()
				  + "\" in namespace "
				  + polygonElement.getNamespaceURI() );
		}


		Element	outerBoundaryIsElement = childElements.get(0);

		if ( outerBoundaryIsElement.getNamespaceURI() == null
		  || ! outerBoundaryIsElement
			.getNamespaceURI().equals( URI.GML )
		  || ! outerBoundaryIsElement
			.getLocalName().equals( "outerBoundaryIs" ) )
		{
			throw new GeometryException
				( "unexpected element \""
				  + outerBoundaryIsElement.getLocalName()
				  + "\" in namespace "
				  + outerBoundaryIsElement.getNamespaceURI()
				  + ", expexted was "
				  + "\"outerBoundaryIs\" element in namespace "
				  + URI.GML );
		}


		Element	outerBoundaryLinearRingElement;

		try
		{
			outerBoundaryLinearRingElement
				= ElementUtil.getSingleChildElement
						( outerBoundaryIsElement );
		}
		catch( XMLParseErrorException  e )
		{
			throw new GeometryException( e.getMessage() , e );
		}

		DrmLinearRing	outerBoundary = GeometryFormatConverter
						  .parseGMLLinearRingElement
						   ( outerBoundaryIsElement );

		if ( ! outerBoundary.closed() )
		{
			throw new GeometryException
			  ( "invalid Polygon format,"
			    + "unclosed Polygon outer boundary" );
		}


		DrmPolygon	ret = new DrmPolygon( outerBoundary );

		for ( int  i = 1  ;  i < childElements.size()  ;  ++ i )
		{
			Element	innerBoundaryIsElement = childElements.get(i);

			if ( innerBoundaryIsElement.getNamespaceURI() == null
			  || ! innerBoundaryIsElement.getNamespaceURI()
			       .equals( URI.GML )
			  || ! innerBoundaryIsElement.getLocalName()
			       .equals( "innerBoundaryIs" ) )
			{
				throw new GeometryException
					( "unexpected child elements of \""
					  + polygonElement.getLocalName()
					  + "\" in namespace "
					  + polygonElement.getNamespaceURI() );
			}

			DrmLinearRing	innerBoundary
					= GeometryFormatConverter
						.parseGMLLinearRingElement
						  ( innerBoundaryIsElement );

			if ( ! innerBoundary.closed() )
			{
				throw new GeometryException
				  ( "invalid Polygon format,"
				    + "unclosed Polygon inner boundary" );
			}

			ret.addInnerBoundary( innerBoundary );
		}

		return( ret );
	}


//	private static	DrmPolygon	parseGMLMultiPolygonElement
	private static	DrmGeometry	parseGMLMultiPolygonElement
					    ( Element  multiPolygonElement )
						      throws GeometryException
	{
		if ( multiPolygonElement.getNamespaceURI() == null
		  || ! multiPolygonElement.getNamespaceURI()
		       .equals( URI.GML )
		  || ! multiPolygonElement.getLocalName()
		       .equals( "MultiPolygon" ) )
		{
			throw new GeometryException
				( "unexpected element \""
				  + multiPolygonElement.getLocalName()
				  + "\" in namespace "
				  + multiPolygonElement.getNamespaceURI()
				  + ", expexted was "
				  + "\"MultiPolygon\" element in namespace "
				  + URI.GML );
		}


		/*
		<gml:MultiPolygon>
		  <gml:PolygonMember>

		  </gml:PolygonMember>
		</gml:MultiPolygon>
		*/


		/*
		List<Element>	childElements
				= ElementUtil.getChildElements
						( multiPolygonElement );

		if ( childElements.size() == 0 )
		{
			throw new GeometryException
				( "too few child elements of \""
				  + multiPolygonElement.getLocalName()
				  + "\" in namespace "
				  + multiPolygonElement.getNamespaceURI() );
		}


		Element	outerBoundaryIsElement = childElements.get(0);

		if ( outerBoundaryIsElement.getNamespaceURI() == null
		  || ! outerBoundaryIsElement
			.getNamespaceURI().equals( URI.GML )
		  || ! outerBoundaryIsElement
			.getLocalName().equals( "outerBoundaryIs" ) )
		{
			throw new GeometryException
				( "unexpected element \""
				  + outerBoundaryIsElement.getLocalName()
				  + "\" in namespace "
				  + outerBoundaryIsElement.getNamespaceURI()
				  + ", expexted was "
				  + "\"outerBoundaryIs\" element in namespace "
				  + URI.GML );
		}


		Element	outerBoundaryLinearRingElement;

		try
		{
			outerBoundaryLinearRingElement
				= ElementUtil.getSingleChildElement
						( outerBoundaryIsElement );
		}
		catch( XMLParseErrorException  e )
		{
			throw new GeometryException( e.getMessage() , e );
		}
		*/

		return( new DrmPoint( 0.0 , 0.0 ) );
	}


	public	static	Element convertToGMLElement( Document doc ,
						     DrmGeometry  geom )
						      throws GeometryException
	{
		Element g = null;

		if ( geom instanceof DrmPoint )
		{
			DrmPoint c = (DrmPoint)geom;

			g = doc.createElementNS( URI.GML , "Point" );

			Element coordinatesElement
				 = doc.createElementNS( URI.GML ,
							"coordinates" );
			g.appendChild( coordinatesElement );

			String coordinates = convertCoordinateToGMLString( c );

			coordinatesElement
			    .appendChild( doc.createTextNode( coordinates ) );
		}
		else if ( geom instanceof DrmBox )
		{
			DrmBox c = (DrmBox)geom;

			g = doc.createElementNS( URI.GML , "Box" );

			Element coordinatesElement
				 = doc.createElementNS( URI.GML ,
							"coordinates" );
			g.appendChild( coordinatesElement );

			String coordinates = Double.toString(c.getMinX())
					     + ","
					     + Double.toString(c.getMinY())
					     + " "
					     + Double.toString(c.getMaxX())
					     + ","
					     + Double.toString(c.getMaxY());

			coordinatesElement
			    .appendChild( doc.createTextNode( coordinates ) );
		}
		else if ( geom instanceof DrmLineString )
		{
			DrmLineString c = (DrmLineString)geom;

			g = doc.createElementNS( URI.GML , "LineString" );

			Element coordinatesElement
				 = doc.createElementNS( URI.GML ,
							"coordinates" );
			g.appendChild( coordinatesElement );

			String coordinates = convertCoordinatesToGMLString
						    ( c.getPoints() );

			coordinatesElement
			    .appendChild( doc.createTextNode( coordinates ) );
		}
		else if ( geom instanceof DrmPolygon )
		{
			DrmPolygon c = (DrmPolygon)geom;

			g = doc.createElementNS( URI.GML , "Polygon" );

			Element outerBoundaryIsElement
				 = doc.createElementNS( URI.GML ,
							"outerBoundaryIs" );
			g.appendChild( outerBoundaryIsElement );

			Element linearRingElement
				 = doc.createElementNS( URI.GML ,
							"LinearRing" );
			outerBoundaryIsElement
			    .appendChild( linearRingElement );

			Element coordinatesElement
				 = doc.createElementNS( URI.GML ,
							"coordinates" );
			linearRingElement
			    .appendChild( coordinatesElement );

			String coordinates = convertCoordinatesToGMLString
						( c.getOuterBoundary()
						    .getPoints() );

			coordinatesElement
			    .appendChild( doc.createTextNode( coordinates ) );
		}
		// XXX: else if ( geom instanceof DrmMultiPoint )
		// XXX: else if ( geom instanceof DrmMultiLineString )
		// XXX: else if ( geom instanceof DrmMultiPolygon )
		else
		{
			throw new GeometryException
				  ( "internal error: "
				      + "unexpected geometry type" );
		}

		if ( geom.getSrsName() != null )
		{
			g.setAttributeNS( null, "srsName" ,
					     geom.getSrsName() );
		}

		return( g );
	}
}
