/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-2006, Geotools Project Managment Committee (PMC)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.data.oracle.sdo;

import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.Iterator;

import oracle.jdbc.OracleConnection;
import oracle.sql.ARRAY;
import oracle.sql.ArrayDescriptor;
import oracle.sql.CHAR;
import oracle.sql.CharacterSet;
import oracle.sql.Datum;
import oracle.sql.NUMBER;
import oracle.sql.STRUCT;
import oracle.sql.StructDescriptor;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateList;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
 * Sample use of SDO class for simple JTS Geometry.
 * <p>
 * If needed I can make a LRSGeometryConverter that allows
 * JTS Geometries with additional ordinates beyond xyz.
 * </p>
 * @author jgarnett
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/oracle-spatial/src/main/java/org/geotools/data/oracle/sdo/GeometryConverter.java $
 */
public class GeometryConverter {
	protected OracleConnection connection;
	GeometryFactory geometryFactory;
    
	public GeometryConverter( OracleConnection connection ){
		this( connection, new GeometryFactory() );		
	}
	public GeometryConverter( OracleConnection connection, GeometryFactory geometryFactory ){
		this.geometryFactory = geometryFactory;
		this.connection = connection;
	}
    public static final String DATATYPE = "MDSYS.SDO_GEOMETRY";
    /**
     * Used to handle MDSYS.SDO_GEOMETRY.
     *  
     * @return <code>MDSYS.SDO_GEOMETRY</code>
     * 
     * @see net.refractions.jspatial.Converter#getDataType()
     */
    public String getDataTypeName() {
        return DATATYPE;
    }

    /**
     * Ensure that obj is a JTS Geometry (2D or 3D) with no LRS measures.
     * <p>
     * This Converter does not support SpatialCoordinates</p>
     * 
     * @param geom    the Geometry to be converted
     * @return <code>true</code> if <code>obj</code> is a JTS Geometry
     * @see net.refractions.jspatial.Converter#isCapable(java.lang.Object)
     */
    public boolean isCapable(Geometry geom) {
        if( geom == null ) return true;
        if( geom instanceof Point      || geom instanceof MultiPoint ||
        	geom instanceof LineString || geom instanceof MultiLineString ||
        	geom instanceof Polygon    || geom instanceof MultiPolygon ||
        	geom instanceof GeometryCollection )
        {
            int d = SDO.D( geom );
            int l = SDO.L( geom );
            return l == 0 && ( d == 2 || d == 3);
        }
        return false;
    }

    /**
     * Convert provided SDO_GEOMETRY to JTS Geometry.
     * <p>
     * Will return <code>null</code> as <code>null</code>.
     * </p>
     * 
     * @param sdoGeometry datum STRUCT to be converted to a double[]
     * 
     * @return JTS <code>Geometry</code> representing the provided <code>datum</code>
     * @throws SQLException
     * 
     * @see net.refractions.jspatial.Converter#toObject(oracle.sql.STRUCT)
     */
    public Geometry asGeometry(STRUCT sdoGeometry) throws SQLException {
        // Note Returning null for null Datum
        if( sdoGeometry == null ) return null;
        ResultSetMetaData meta = sdoGeometry.getDescriptor().getMetaData();
        
        Datum data[] = sdoGeometry.getOracleAttributes();
        final int GTYPE = asInteger( data[0], 0 );
        final int SRID = asInteger( data[1], SDO.SRID_NULL );
        final double POINT[] = asDoubleArray( (STRUCT) data[2], Double.NaN );
        final int ELEMINFO[] = asIntArray( (ARRAY) data[3], 0 );
        final double ORDINATES[] = asDoubleArray( (ARRAY) data[4], Double.NaN );;
                
        return SDO.create( geometryFactory,
                           GTYPE,
                           SRID,
                           POINT,
                           ELEMINFO,
                           ORDINATES );
    }
    
    /**
     * Used to convert double[] to SDO_ODINATE_ARRAY.
     * <p>
     * Will return <code>null</code> as an empty <code>SDO_GEOMETRY</code></p>
     * 
     * @param geom Map to be represented as a STRUCT
     * @return STRUCT representing provided Map
     * @see net.refractions.jspatial.Converter#toDataType(java.lang.Object)
     */
    public STRUCT toSDO(Geometry geom) throws SQLException {
        if( geom == null) return asEmptyDataType();
        
        int gtype = SDO.gType( geom );
        NUMBER SDO_GTYPE = new NUMBER( gtype );
        
        int srid = SDO.SRID( geom );
        NUMBER SDO_SRID = (srid == SDO.SRID_NULL || srid == 0) ? null :
                          new NUMBER( srid );
        
        double[] point = SDO.point( geom );
        STRUCT SDO_POINT;
        
        ARRAY SDO_ELEM_INFO;
        ARRAY SDO_ORDINATES;
        
        if( point == null ){
            int elemInfo[] = SDO.elemInfo( geom );
            double ordinates[] = SDO.ordinates( geom );
            
            SDO_POINT = null;
            SDO_ELEM_INFO = toARRAY( elemInfo, "MDSYS.SDO_ELEM_INFO_ARRAY" );
            SDO_ORDINATES = toARRAY( ordinates, "MDSYS.SDO_ORDINATE_ARRAY" );                        
        }
        else { // Point Optimization
            Datum data[] = new Datum[]{
                toNUMBER( point[0] ),
                toNUMBER( point[1] ),
                toNUMBER( point[2] ),
            };
            SDO_POINT = toSTRUCT( data, "MDSYS.SDO_POINT_TYPE"  );
            SDO_ELEM_INFO = null;
            SDO_ORDINATES = null;
        }                
        Datum attributes[] = new Datum[]{
            SDO_GTYPE,
            SDO_SRID,
            SDO_POINT,
            SDO_ELEM_INFO,
            SDO_ORDINATES
        };
        return toSTRUCT( attributes, DATATYPE );        
    }
    /**
     * Representation of <code>null</code> as an Empty <code>SDO_GEOMETRY</code>.
     * 
     * @return <code>null</code> as a SDO_GEOMETRY
     */
    protected STRUCT asEmptyDataType() throws SQLException{
        return toSTRUCT( null, DATATYPE );
    }
    
    /** Convience method for STRUCT construction. */
    protected final STRUCT toSTRUCT( Datum attributes[], String dataType )
            throws SQLException
    {
    	if( dataType.startsWith("*.")){
    		dataType = "DRA."+dataType.substring(2);//TODO here
    	}
        StructDescriptor descriptor =
            StructDescriptor.createDescriptor( dataType, connection );
    
         return new STRUCT( descriptor, connection, attributes );
    }
    
    /** 
     * Convience method for ARRAY construction.
     * <p>
     * Compare and contrast with toORDINATE - which treats <code>Double.NaN</code>
     * as<code>NULL</code></p>
     */
    protected final ARRAY toARRAY( double doubles[], String dataType )
            throws SQLException
    {
        ArrayDescriptor descriptor =
            ArrayDescriptor.createDescriptor( dataType, connection );
        
         return new ARRAY( descriptor, connection, doubles );
    }
    
    /**
     * Convience method for ARRAY construction.
     * <p>
     * Forced to burn memory here - only way to actually place
     * <code>NULL</code> numbers in the ordinate stream.</p>
     * <ul>
     * <li>JTS: records lack of data as <code>Double.NaN</code></li>
     * <li>SDO: records lack of data as <code>NULL</code></li>
     * </ul>
     * <p>
     * The alternative is to construct the array from a array of
     * doubles, which does not record <code>NULL</code> NUMBERs.</p>
     * <p>
     * The results is an "MDSYS.SDO_ORDINATE_ARRAY"</p>
     * <code><pre>
     * list     = c1(1,2,0), c2(3,4,Double.NaN)
     * measures = {{5,6},{7,8}
     * 
     * toORDINATE( list, measures, 2 )
     * = (1,2,5,7, 3,4,6,8)
     * 
     * toORDINATE( list, measures, 3 )
     * = (1,2,0,5,7, 3,4,NULL,6,8)
     * 
     * toORDINATE( list, null, 2 )
     * = (1,2, 3,4)
     * </pre></code>
     * 
     * @param list     CoordinateList to be represented
     * @param measures Per Coordiante Measures, <code>null</code> if not required
     * @param D        Dimension of Coordinates (limited to 2d, 3d)         
     */
    protected final ARRAY toORDINATE( CoordinateList list,
                                      double measures[][],
                                      final int D)
            throws SQLException
    {
        ArrayDescriptor descriptor = ArrayDescriptor.createDescriptor(
            "MDSYS.SDO_ORDINATE_ARRAY",
            connection
        );
        
        final int LENGTH = measures != null ? measures.length : 0;
        final int LEN = D + LENGTH;
        Datum data[] = new Datum[ list.size()*LEN];
        int offset = 0;
        int index = 0;
        Coordinate coord;
        
        for( Iterator i=list.iterator(); i.hasNext(); index++)
        {
            coord = (Coordinate) i.next();
            
            data[ offset++ ] = toNUMBER( coord.x );
            data[ offset++ ] = toNUMBER( coord.y );
            if( D == 3 )
            {
                data[ offset++ ] = toNUMBER( coord.x );
            }
            for( int j=0; j<LENGTH; j++)
            {
                data[ offset++ ] = toNUMBER( measures[ j][ index ] );
            }
        }
        return new ARRAY( descriptor, connection, data );
    }
    protected final ARRAY toORDINATE( double ords[] )
        throws SQLException
    {
        ArrayDescriptor descriptor = ArrayDescriptor.createDescriptor(
            "MDSYS.SDO_ORDINATE_ARRAY",
            connection
        );
    
        final int LENGTH = ords.length;
        
        Datum data[] = new Datum[ LENGTH ];
        
        for( int i=0; i<LENGTH; i++ )
        {
            data[ i ] = toNUMBER( ords[i] );            
        }
        return new ARRAY( descriptor, connection, data );
    }
    protected final ARRAY toATTRIBUTE( double ords[], String desc )
    throws SQLException
    {
    	ArrayDescriptor descriptor = ArrayDescriptor.createDescriptor(
    			desc,
    			connection
    	);
    	
    	final int LENGTH = ords.length;
    	
    	Datum data[] = new Datum[ LENGTH ];
    	
    	for( int i=0; i<LENGTH; i++ )
    	{
    		data[ i ] = toNUMBER( ords[i] );            
    	}
    	return new ARRAY( descriptor, connection, data );
    }

    /** 
     * Convience method for NUMBER construction.
     * <p>
     * Double.NaN is represented as <code>NULL</code> to agree
     * with JTS use.</p>
     */
    protected final NUMBER toNUMBER( double number ) throws SQLException{
        if( Double.isNaN( number )){
            return null;
        }
        return new NUMBER( number );
    }
    
    /** 
     * Convience method for ARRAY construction.
     */
    protected final ARRAY toARRAY( int ints[], String dataType )
        throws SQLException
    {
        ArrayDescriptor descriptor =
            ArrayDescriptor.createDescriptor( dataType, connection );
            
         return new ARRAY( descriptor, connection, ints );
    }
    
    /** Convience method for NUMBER construction */
    protected final NUMBER toNUMBER( int number )
    {
        return new NUMBER( number );
    }
    
    /** Convience method for CHAR construction */
    protected final CHAR toCHAR( String s )
    {
    	
    	// make sure if the string is larger than one character, only take the first character
    	if (s.length() > 1)
    		s = new String((new Character(s.charAt(0))).toString());
    	try {
			//BUG: make sure I am correct
			return new CHAR(s, CharacterSet.make(CharacterSet.ISO_LATIN_1_CHARSET));
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return null;
    }
    
    //
    // These functions present Datum as a Java type
    //     
    /** Presents datum as an int */
    protected int asInteger( Datum datum, final int DEFAULT )
            throws SQLException
    {
        if( datum == null) return DEFAULT;
        return ((NUMBER) datum).intValue();
    }
    /** Presents datum as a double */
    protected double asDouble( Datum datum, final double DEFAULT )
            throws SQLException
    {
        if( datum == null) return DEFAULT;
        return ((NUMBER) datum).doubleValue();
    }
    
    /** Presents struct as a double[] */
    protected double[] asDoubleArray( STRUCT struct, final double DEFAULT )
        throws SQLException
    {
        if( struct == null ) return null;
        return asDoubleArray( struct.getOracleAttributes(), DEFAULT );
    }
    
    /** Presents array as a double[] */
    protected double[] asDoubleArray( ARRAY array, final double DEFAULT )
        throws SQLException
    {
        if( array == null ) return null;
        if( DEFAULT == 0 ) return array.getDoubleArray();
        
        return asDoubleArray( array.getOracleArray(), DEFAULT );
    }
    
    /** Presents Datum[] as a double[] */
    protected double[] asDoubleArray( Datum data[], final double DEFAULT )
        throws SQLException
    {
        if( data== null ) return null;
        double array[] = new double[ data.length ];
        for( int i=0; i<data.length; i++ )
        {
            array[ i ] = asDouble( data[ i ], DEFAULT );
        }
        return array;
    }
    protected int[] asIntArray( ARRAY array, int DEFAULT )
        throws SQLException
    {
        if( array == null ) return null;
        if( DEFAULT == 0 ) return array.getIntArray();
        
        return asIntArray( array.getOracleArray(), DEFAULT );
    }
    /** Presents Datum[] as a int[] */
    protected int[] asIntArray( Datum data[], final int DEFAULT )
        throws SQLException
    {
        if( data== null ) return null;
        int array[] = new int[ data.length ];
        for( int i=0; i<data.length; i++ )
        {
            array[ i ] = asInteger( data[ i ], DEFAULT );
        }
        return array;
    }
}
