/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) Copyright IBM Corporation, 2005-2007. All rights reserved.
 *
 *    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;
 *    version 2.1 of the License.
 *
 *    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.db2;

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;
import org.geotools.data.DataSourceException;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultQuery;
import org.geotools.data.EmptyFeatureReader;
import org.geotools.data.FeatureReader;
import org.geotools.data.FeatureSource;
import org.geotools.data.Query;
import org.geotools.data.ReTypeFeatureReader;
import org.geotools.data.Transaction;
import org.geotools.data.db2.filter.SQLEncoderDB2;
import org.geotools.data.db2.filter.SQLEncoderDB2;
import org.geotools.data.jdbc.ConnectionPool;
import org.geotools.data.jdbc.FeatureTypeHandler;
import org.geotools.data.jdbc.FeatureTypeInfo;
import org.geotools.data.jdbc.FilterToSQL;
import org.geotools.data.jdbc.JDBCDataStore;
import org.geotools.data.jdbc.JDBCDataStoreConfig;
import org.geotools.data.jdbc.JDBCFeatureWriter;
import org.geotools.data.jdbc.JDBCUtils;
import org.geotools.data.jdbc.QueryData;
import org.geotools.data.jdbc.SQLBuilder;
import org.geotools.data.jdbc.attributeio.AttributeIO;
import org.geotools.data.jdbc.attributeio.WKTAttributeIO;
import org.geotools.data.jdbc.fidmapper.FIDMapper;
import org.geotools.data.jdbc.fidmapper.FIDMapperFactory;
import org.geotools.feature.AttributeType;
import org.geotools.feature.AttributeTypeFactory;
import org.geotools.feature.FeatureType;
import org.geotools.feature.GeometryAttributeType;

import org.opengis.filter.Filter;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.sql.DataSource;


/**
 * DB2 DataStore implementation.
 * 
 * <p>
 * Instances of this class should only be obtained via
 * DB2DataStoreFactory.createDataStore or DataStoreFinder.getDataStore.
 * </p>
 *
 * @author David Adler - IBM Corporation
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/db2/src/main/java/org/geotools/data/db2/DB2DataStore.java $
 */
public class DB2DataStore extends JDBCDataStore {
    private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(
            "org.geotools.data.db2");

    /**
     * List of all the DB2 geometry type names and the corresponding JTS class
     */
    private static final Map DB2_GEOM_TYPE_MAPPING = new HashMap();

    /**
     * Populate list of geometry classes supported by DB2
     */
    static {
        DB2_GEOM_TYPE_MAPPING.put("ST_POINT", Point.class);
        DB2_GEOM_TYPE_MAPPING.put("ST_LINESTRING", LineString.class);
        DB2_GEOM_TYPE_MAPPING.put("ST_POLYGON", Polygon.class);
        DB2_GEOM_TYPE_MAPPING.put("ST_MULTIPOINT", MultiPoint.class);
        DB2_GEOM_TYPE_MAPPING.put("ST_MULTILINESTRING", MultiLineString.class);
        DB2_GEOM_TYPE_MAPPING.put("ST_MULTIPOLYGON", MultiPolygon.class);
    }

    /** Reference to DB2 in-memory spatial catalog */
    private DB2SpatialCatalog catalog;

    /** The URL for this particular data store */
    private String dbURL = null;
    
    /** Time since catalog was last reloaded */
	private long lastTypeNameRequestTime  = 0;
	
    
    /**
     * The only supported constructor for a DB2DataStore. This constructor is
     * mainly intended to be called from DB2DataStoreFactory.
     *
     * @param connectionPool the initialized DB2 connection pool
     * @param config the JDBCDataStoreConfiguration
     * @param dbURL the database URL of the form
     *        <code>jdbc:db2://hostname:hostport/dbname </code>
     *
     * @throws IOException
     */
    public DB2DataStore(DataSource dataSource,
        JDBCDataStoreConfig config, String dbURL) throws IOException {
        super(dataSource, config);

        if (dataSource == null) {
            throw new IOException("DataSource pool is null");
        }

        this.dbURL = dbURL;

        // Get an instance of the DB2 spatial catalog for this database.
        // We need to provide a database connection in case the spatial catalog instance
        // does not already exist and we need to issue SQL against the database.
        Connection conn = this.getConnection(Transaction.AUTO_COMMIT);

        try {
            this.catalog = DB2SpatialCatalog.getInstance(dbURL,
                    config.getDatabaseSchemaName(), conn);
            JDBCUtils.close(conn, Transaction.AUTO_COMMIT, null);
        } catch (SQLException e) {
            LOGGER.info("DB2SpatialCatalog create failed: " + e);
            JDBCUtils.close(conn, Transaction.AUTO_COMMIT, e);
            throw new IOException("DB2SpatialCatalog create failed");
        }
    }

    /**
     * Handles DB2-specific geometry types.  If it isn't one, just let the
     * parent method handle it.
     *
     * @param rs The ResultSet containing the result of a
     *        DatabaseMetaData.getColumns call.
     *
     * @return The AttributeType built from the ResultSet.
     *
     * @throws IOException If an error occurs processing the ResultSet.
     */
    protected AttributeType buildAttributeType(ResultSet rs)
        throws IOException {
        try {
        	String spatialTypePrefix = "\"DB2GSE\".\"ST_";

        	int dataType = rs.getInt("DATA_TYPE");
            String typeName = rs.getString("TYPE_NAME");
            String tableSchema = rs.getString("TABLE_SCHEM");
            String tableName = rs.getString("TABLE_NAME");
            String columnName = rs.getString("COLUMN_NAME");
            if (!typeName.startsWith(spatialTypePrefix)) {
                return super.buildAttributeType(rs);
            }
            
            String geomTypeName = this.catalog.getDB2GeometryTypeName(tableSchema,
                    tableName, columnName);

            // Look up the geometry type we just got back and create the
            // corresponding attribute type for the geometry class, if it was found.
            Class geomClass = (Class) DB2_GEOM_TYPE_MAPPING.get(geomTypeName);

            if (geomClass != null) {
                CoordinateReferenceSystem crs;

                try {
                    crs = this.catalog.getCRS(tableSchema, tableName, columnName);
                } catch (Exception e) {
                    throw new IOException("Exception: " + e.getMessage());
                }

                GeometryAttributeType geometryAttribute = (GeometryAttributeType) AttributeTypeFactory
                    .newAttributeType(columnName, geomClass, true, 0, null, crs);

                return geometryAttribute;
            }

            // It is some other unrecognized structured type - log what we found and return null
            LOGGER.fine("Type '" + geomTypeName + "' is not recognized");

            return null;
        } catch (SQLException e) {
            throw new IOException("SQL exception occurred: " + e.getMessage());
        }
    }

    /**
     * Creates a DB2-specific FIDMapperFactory.
     *
     * @param config not used.
     *
     * @return a DB2FIDMapperFactory
     */
    protected FIDMapperFactory buildFIDMapperFactory(JDBCDataStoreConfig config) {
        return new DB2FIDMapperFactory(config.getDatabaseSchemaName());
    }

    /**
     * Get the SRID associated with a geometry column.
     * 
     * <p>
     * The value returned is the EPSG coordinate system identifier, not the DB2
     * srs_id.
     * </p>
     *
     * @param tableName The name of the table to get the SRID for.
     * @param geometryColumnName The name of the geometry column within the
     *        table to get SRID for.
     *
     * @return The SRID for the geometry column  or -1.
     *
     * @throws IOException
     */
    protected int determineSRID(String tableName, String geometryColumnName)
        throws IOException {
        int srid = -1;

        // Not sure that this is actually the right value to use
        String tableSchema = this.config.getDatabaseSchemaName();
        srid = this.catalog.getCsId(tableSchema, tableName, geometryColumnName);
        LOGGER.fine(DB2SpatialCatalog.geomID(tableSchema, tableName,
                geometryColumnName) + " srid=" + srid);

        return srid;
    }

    /**
     * Gets the database URL.
     *
     * @return the database URL.
     */
    String getDbURL() {
        return this.dbURL;
    }

    /**
     * Create a DB2-specific FeatureTypeHandler.
     *
     * @param config a JDBCDataStoreConfig.
     *
     * @return a DB2FeatureTypeHandler.
     *
     * @throws IOException if the feature type handler could not be created.
     */
    protected FeatureTypeHandler getFeatureTypeHandler(
        JDBCDataStoreConfig config) throws IOException {
        return new DB2FeatureTypeHandler(this, buildFIDMapperFactory(config),
            config.getTypeHandlerTimeout());
    }

    /**
     * Gets the handler to convert a geometry database value to a JTS geometry.
     *
     * @param type not used.
     * @param queryData not used.
     *
     * @return AttributIO
     */
    protected AttributeIO getGeometryAttributeIO(AttributeType type,
        QueryData queryData) {
        return new WKTAttributeIO();
    }

    /**
     * Gets the instance of the DB2SpatialCatalog associated with this data
     * store.
     *
     * @return a DB2SpatialCatalog
     */
    DB2SpatialCatalog getSpatialCatalog() {
        return this.catalog;
    }

    /**
     * Gets the DB2-specific SQL builder object.
     *
     * @param typeName Name of the type to build the SQL for.
     *
     * @return DB2SQLBuilder
     *
     * @throws IOException
     */
    public SQLBuilder getSqlBuilder(String typeName) throws IOException {
        FeatureTypeInfo info = this.typeHandler.getFeatureTypeInfo(typeName);
        int srid = 0;
        SQLEncoderDB2 encoder = new SQLEncoderDB2();
        encoder.setSqlNameEscape("\"");
        FIDMapper mapper = getFIDMapper(typeName);
        encoder.setFIDMapper(mapper);

        if (info.getSchema().getDefaultGeometry() != null) {
            String geom = info.getSchema().getDefaultGeometry().getName();
            srid = this.catalog.getSRID(getTableSchema(), typeName, geom);
        }

        encoder.setSRID(srid);

        // We should probably get the table schema name from the feature type
        // information - not sure that it exists there.
        return new DB2SQLBuilder((FilterToSQL) encoder, getTableSchema(), info.getSchema());
    }

    /**
     * Gets the names of tables (types) that contain a spatial column.  Note
     * that there is still an issue concerning the ambiguity of spatial tables
     * that have the same table name but different table schema names.
     *
     * @return Array of type names as Strings.
     *
     * @throws IOException if the spatial catalog can not be accessed.
     */
    public String[] getTypeNames() throws IOException {

        long lastTime = lastTypeNameRequestTime;
        long now = System.currentTimeMillis();
        
        lastTypeNameRequestTime = now;
        
        if (lastTime < (now - config.getTypeHandlerTimeout())) {
        	refreshCatalog();
        }
        return getSpatialCatalog().getTypeNames();
    }
    
    
    /** 
     * Reloads the spatial catalog from the database.
     * <p>
     * This is useful in case anything changed after the datastore was initialized.
     * @throws IOException
     * @throws SQLException
     */
    public void refreshCatalog() throws IOException {
    	try {
    		Connection conn = getConnection(Transaction.AUTO_COMMIT);
			getSpatialCatalog().loadCatalog(conn,getTableSchema());
			lastTypeNameRequestTime = System.currentTimeMillis();
            JDBCUtils.close(conn, Transaction.AUTO_COMMIT, null);
			LOGGER.fine("Refreshing spatial catalog");
		} catch (SQLException e) {
			throw(new IOException(e.getLocalizedMessage()));
		}
    }

    /**
     * Gets the table schema associated with this data store.
     * 
     * <p>
     * At some point this may change if multiple schemas are supported by a
     * data store.
     * </p>
     *
     * @return the schema name that will prefix table names.
     */
    public String getTableSchema() {
        return this.config.getDatabaseSchemaName();
    }

    /**
     * Gets a DB2-specific feature source.
     *
     * @param typeName
     *
     * @return a DB2Feature Source, Store or Locking
     *
     * @throws IOException if the feature source could not be created.
     *
     * @see org.geotools.data.DataStore#getFeatureSource(java.lang.String)
     */
    public FeatureSource getFeatureSource(String typeName)
        throws IOException {
        if (!this.typeHandler.getFIDMapper(typeName).isVolatile()
                || this.allowWriteOnVolatileFIDs) {
            if (getLockingManager() != null) {
                // Use default JDBCFeatureLocking that delegates all locking
                // the getLockingManager
                //
                return new DB2FeatureLocking(this, getSchema(typeName));
            } else {
                // subclass should provide a FeatureLocking implementation
                // but for now we will simply forgo all locking
                return new DB2FeatureStore(this, getSchema(typeName));
            }
        } else {
            return new DB2FeatureSource(this, getSchema(typeName));
        }
    }

    /**
     * Overrides the method in JDBCDataStore because it includes
     * PostGIS-specific handling to setAutoCommit(false) which causes problems
     * for DB2 because the transaction is still uncommitted when the
     * connection is closed.
     *
     * @param featureTypeInfo
     * @param tableName
     * @param sqlQuery The SQL query to execute.
     * @param transaction The Transaction is included here for handling
     *        transaction connections at a later stage.  It is not currently
     *        used.
     * @param forWrite
     *
     * @return The QueryData object that contains the resources for the query.
     *
     * @throws IOException
     * @throws DataSourceException If an error occurs performing the query.
     */
    protected QueryData executeQuery(FeatureTypeInfo featureTypeInfo,
        String tableName, String sqlQuery, Transaction transaction,
        boolean forWrite) throws IOException {
        LOGGER.fine("About to execute query: " + sqlQuery);

        Connection conn = null;
        Statement statement = null;
        ResultSet rs = null;

        try {
            conn = getConnection(transaction);
            statement = conn.createStatement(getResultSetType(forWrite),
                    getConcurrency(forWrite));

            statement.setFetchSize(200);

            int rsc1 = statement.getResultSetConcurrency();
            rs = statement.executeQuery(sqlQuery);

            int rsc2 = statement.getResultSetConcurrency();
            int c = rs.getConcurrency();
            int update = ResultSet.CONCUR_UPDATABLE;
            int read = ResultSet.CONCUR_READ_ONLY;

            return new QueryData(featureTypeInfo, this, conn, statement, rs,
                transaction);
        } catch (SQLException e) {
            // if an error occurred we close the resources
            String msg = "Error Performing SQL query: " + sqlQuery;
            LOGGER.log(Level.SEVERE, msg, e);
            JDBCUtils.close(rs);
            JDBCUtils.close(statement);
            JDBCUtils.close(conn, transaction, e);
            throw new DataSourceException(msg, e);
        }
    }

    /**
     * Overrides the method in JDBCDataStore so that a DB2FeatureWriter is
     * created.
     *
     * @param featureReader
     * @param queryData
     *
     * @return The DB2FeatureWriter
     *
     * @throws IOException
     *
     * @see org.geotools.data.jdbc.JDBCDataStore#createFeatureWriter(org.geotools.data.FeatureReader,
     *      org.geotools.data.jdbc.QueryData)
     */
    protected JDBCFeatureWriter createFeatureWriter(
        FeatureReader featureReader, QueryData queryData)
        throws IOException {
        String featureName = queryData.getFeatureType().getTypeName();

        return new DB2FeatureWriter(featureReader, queryData,
            (DB2SQLBuilder) getSqlBuilder(featureName));
    }
	/**
	 * This is a public entry point to the DataStore.
	 * 
	 * <p>
	 * We have given some though to changing this api to be based on query.
	 * </p>
	 * 
	 * <p>
	 * Currently the is is the only way to retype your features to different
	 * name spaces.
	 * </p>
	 * (non-Javadoc)
	 * 
	 * @see org.geotools.data.DataStore#getFeatureReader(org.geotools.feature.FeatureType,
	 *      org.geotools.filter.Filter, org.geotools.data.Transaction)
	 */
	public FeatureReader getFeatureReader(final FeatureType requestType,
			final Filter filter, final Transaction transaction)
			throws IOException {
		String typeName = requestType.getTypeName();
		FeatureType schemaType = getSchema(typeName);
        LOGGER.fine("requestType: " + requestType);
        LOGGER.fine("schemaType: " + schemaType);

		int compare = DataUtilities.compare(requestType, schemaType);

		Query query;

		if (compare == 0) {
			// they are the same type
			//
			query = new DefaultQuery(typeName, filter);
		} else if (compare == 1) {
			// featureType is a proper subset and will require reTyping
			//
			String[] names = attributeNames(requestType, filter);
			query = new DefaultQuery(typeName, filter, Query.DEFAULT_MAX,
					names, "getFeatureReader");
		} else {
			// featureType is not compatiable
			//
			throw new IOException("Type " + typeName + " does match request");
		}

		if ((filter == Filter.INCLUDE) || filter.equals(Filter.INCLUDE)) {
			return new EmptyFeatureReader(requestType);
		}

		FeatureReader reader = getFeatureReader(query, transaction);

		if (compare == 1) {
			reader = new ReTypeFeatureReader(reader, requestType);
		}

		return reader;
	}
    
}
