package liquibase.ext.spatial.sqlgenerator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;

import liquibase.database.Database;
import liquibase.database.core.OracleDatabase;
import liquibase.ext.spatial.statement.CreateSpatialIndexStatement;
import liquibase.sql.Sql;
import liquibase.sql.UnparsedSql;
import liquibase.sqlgenerator.SqlGeneratorChain;
import liquibase.structure.core.Column;
import liquibase.structure.core.Table;
import liquibase.structure.core.View;
import liquibase.util.StringUtils;

/**
 * <code>CreateSpatialIndexGeneratorOracle</code> generates the SQL for creating a spatial index in
 * Oracle.
 */
public class CreateSpatialIndexGeneratorOracle extends AbstractCreateSpatialIndexGenerator {
   @Override
   public boolean supports(final CreateSpatialIndexStatement statement, final Database database) {
      return database instanceof OracleDatabase;
   }

   @Override
   public Sql[] generateSql(final CreateSpatialIndexStatement statement, final Database database,
         final SqlGeneratorChain sqlGeneratorChain) {
      final View metadataView = new View().setName("USER_SDO_GEOM_METADATA");
      final String deleteMetadataSql = generateDeleteMetadataSql(statement, database);
      final Sql deleteMetadata = new UnparsedSql(deleteMetadataSql, metadataView);
      final String insertMetadataSql = generateInsertMetadataSql(statement, database);
      final Sql insertMetadata = new UnparsedSql(insertMetadataSql, metadataView);
      final String createIndexSql = generateCreateIndexSql(statement, database);
      final Sql createIndex = new UnparsedSql(createIndexSql, getAffectedIndex(statement));
      return new Sql[] { deleteMetadata, insertMetadata, createIndex };
   }

   /**
    * Generates the SQL for deleting any existing record from the
    * <code>USER_SDO_GEOM_METADATA</code> table. Typically this record shouldn't be present but we
    * must ensure that it does not already exist.
    * 
    * @param statement
    *           the create spatial index statement.
    * @param database
    *           the database instance.
    * @return the SQL to delete any existing metadata record.
    */
   protected String generateDeleteMetadataSql(final CreateSpatialIndexStatement statement,
         final Database database) {
      final StringBuilder sql = new StringBuilder();
      sql.append("DELETE FROM user_sdo_geom_metadata ");
      final String tableName = statement.getTableName().trim();
      sql.append("WHERE table_name = '").append(database.correctObjectName(tableName, Table.class));
      final String columnName = statement.getColumns()[0].trim();
      sql.append("' AND column_name = '").append(
            database.correctObjectName(columnName, Column.class));
      sql.append("'");
      return sql.toString();
   }

   /**
    * Generates the SQL for inserting the necessary record into the
    * <code>USER_SDO_GEOM_METADATA</code> table. This record must be present prior to creating the
    * spatial index.
    * 
    * @param statement
    *           the create spatial index statement.
    * @param database
    *           the database instance.
    * @return the SQL to insert the metadata record.
    */
   protected String generateInsertMetadataSql(final CreateSpatialIndexStatement statement,
         final Database database) {
      final StringBuilder sql = new StringBuilder();
      sql.append("INSERT INTO user_sdo_geom_metadata ");
      sql.append("(table_name, column_name, diminfo, srid) ");
      final String tableName = statement.getTableName().trim();
      sql.append("VALUES ('").append(database.correctObjectName(tableName, Table.class));
      final String columnName = statement.getColumns()[0].trim();
      sql.append("', '").append(database.correctObjectName(columnName, Column.class));
      sql.append("', SDO_DIM_ARRAY(");
      sql.append("SDO_DIM_ELEMENT('Longitude', -180, 180, 0.005), ");
      sql.append("SDO_DIM_ELEMENT('Latitude', -90, 90, 0.005))");
      final Integer srid = statement.getSrid();
      if (srid == null) {
         sql.append(", NULL");
      } else {
         sql.append(", ").append(OracleSpatialUtils.EPSG_TO_ORACLE_FUNCTION).append("(")
               .append(srid).append(")");
      }
      sql.append(")");
      return sql.toString();
   }

   /**
    * Generates the SQL for creating the spatial index.
    * 
    * @param statement
    *           the create spatial index statement.
    * @param database
    *           the database instance.
    * @return the SQL to create a spatial index.
    */
   protected String generateCreateIndexSql(final CreateSpatialIndexStatement statement,
         final Database database) {
      final StringBuilder sql = new StringBuilder();
      sql.append("CREATE INDEX ");
      final String schemaName = statement.getTableSchemaName();
      final String catalogName = statement.getTableCatalogName();
      final String indexName = statement.getIndexName();
      sql.append(database.escapeIndexName(catalogName, schemaName, indexName));
      sql.append(" ON ");
      final String tableName = statement.getTableName();
      sql.append(database.escapeTableName(catalogName, schemaName, tableName)).append(" (");
      final Iterator<String> iterator = Arrays.asList(statement.getColumns()).iterator();
      final String column = iterator.next();
      sql.append(database.escapeColumnName(catalogName, statement.getTableSchemaName(), tableName,
            column));
      sql.append(") INDEXTYPE IS mdsys.spatial_index");

      // Generate and add the optional parameters.
      final Collection<String> parameters = getParameters(statement);
      if (parameters != null && !parameters.isEmpty()) {
         sql.append(" PARAMETERS ('");
         sql.append(StringUtils.join(parameters, " "));
         sql.append("')");
      }
      return sql.toString();
   }

   /**
    * Creates the parameters to the spatial index creation statement.
    * 
    * @param statement
    *           the statement.
    * @return the optional parameters for the <code>CREATE INDEX</code> statement.
    */
   protected Collection<String> getParameters(final CreateSpatialIndexStatement statement) {
      final Collection<String> parameters = new ArrayList<String>();
      if (StringUtils.trimToNull(statement.getGeometryType()) != null) {
         final String gType = getGtype(statement.getGeometryType().trim());
         if (gType != null) {
            parameters.add("layer_gtype=" + gType);
         }
      }
      if (StringUtils.trimToNull(statement.getTablespace()) != null) {
         parameters.add("tablespace=" + statement.getTablespace().trim());
      }
      return parameters;
   }

   /**
    * Converts the OGC geometry type to Oracle's <code>SDO_GTYPE</code>.
    * 
    * @param ogcGeometryType
    *           the OGC geometry type.
    * @return the corresponding Oracle <code>SDO_GTYPE</code>.
    */
   protected String getGtype(final String ogcGeometryType) {
      final String gType;
      if (ogcGeometryType == null) {
         gType = null;
      } else if ("LineString".equalsIgnoreCase(ogcGeometryType)) {
         gType = "LINE";
      } else if ("MultiLineString".equalsIgnoreCase(ogcGeometryType)) {
         gType = "MULTILINE";
      } else if ("Triangle".equalsIgnoreCase(ogcGeometryType)) {
         gType = "POLYGON";
      } else if ("Point".equalsIgnoreCase(ogcGeometryType)
            || "MultiPoint".equalsIgnoreCase(ogcGeometryType)
            || "Curve".equalsIgnoreCase(ogcGeometryType)
            || "MultiCurve".equalsIgnoreCase(ogcGeometryType)
            || "Polygon".equalsIgnoreCase(ogcGeometryType)
            || "MultiPolygon".equalsIgnoreCase(ogcGeometryType)) {
         gType = ogcGeometryType.toUpperCase();
      } else {
         gType = "COLLECTION";
      }
      return gType;
   }
}