package liquibase.ext.spatial.preconditions;

import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;

import liquibase.changelog.ChangeSet;
import liquibase.changelog.DatabaseChangeLog;
import liquibase.database.Database;
import liquibase.database.core.DerbyDatabase;
import liquibase.database.core.H2Database;
import liquibase.exception.PreconditionErrorException;
import liquibase.exception.PreconditionFailedException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.exception.ValidationErrors;
import liquibase.exception.Warnings;
import liquibase.ext.spatial.xml.XmlConstants;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.precondition.AbstractPrecondition;
import liquibase.precondition.Precondition;
import liquibase.precondition.core.IndexExistsPrecondition;
import liquibase.precondition.core.TableExistsPrecondition;
import liquibase.resource.ResourceAccessor;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Column;
import liquibase.structure.core.Index;
import liquibase.structure.core.Schema;
import liquibase.structure.core.Table;
import liquibase.util.StringUtils;

/**
 * <code>SpatialIndexExistsPrecondition</code> determines if a spatial index exists on a specified
 * table.
 */
public class SpatialIndexExistsPrecondition extends AbstractPrecondition {
   private String catalogName;
   private String schemaName;
   private String tableName;
   private String columnNames;
   private String indexName;

   public String getCatalogName() {
      return this.catalogName;
   }

   public void setCatalogName(final String catalogName) {
      this.catalogName = catalogName;
   }

   public String getSchemaName() {
      return this.schemaName;
   }

   public void setSchemaName(final String schemaName) {
      this.schemaName = schemaName;
   }

   public String getTableName() {
      return this.tableName;
   }

   public void setTableName(final String tableName) {
      this.tableName = tableName;
   }

   public String getIndexName() {
      return this.indexName;
   }

   public void setIndexName(final String indexName) {
      this.indexName = indexName;
   }

   public String getColumnNames() {
      return this.columnNames;
   }

   public void setColumnNames(final String columnNames) {
      this.columnNames = columnNames;
   }

   @Override
   public String getName() {
      return "spatialIndexExists";
   }

   @Override
   public Warnings warn(final Database database) {
      return new Warnings();
   }

   @Override
   public ValidationErrors validate(final Database database) {
      final ValidationErrors validationErrors;

      if ((database instanceof DerbyDatabase || database instanceof H2Database)
            && getTableName() == null) {
         validationErrors = new ValidationErrors();
         validationErrors
               .addError("tableName is required for " + database.getDatabaseProductName());
      } else {
         final IndexExistsPrecondition precondition = new IndexExistsPrecondition();
         precondition.setCatalogName(getCatalogName());
         precondition.setSchemaName(getSchemaName());
         precondition.setTableName(getTableName());
         precondition.setIndexName(getIndexName());
         precondition.setColumnNames(getColumnNames());
         validationErrors = precondition.validate(database);
      }
      return validationErrors;
   }

   @Override
   public void check(final Database database, final DatabaseChangeLog changeLog,
         final ChangeSet changeSet) throws PreconditionFailedException, PreconditionErrorException {
      Precondition delegatedPrecondition;
      if (database instanceof DerbyDatabase || database instanceof H2Database) {
         final TableExistsPrecondition precondition = new TableExistsPrecondition();
         precondition.setCatalogName(getCatalogName());
         precondition.setSchemaName(getSchemaName());
         final String tableName = getHatboxTableName();
         precondition.setTableName(tableName);
         delegatedPrecondition = precondition;
      } else {
         final IndexExistsPrecondition precondition = new IndexExistsPrecondition();
         precondition.setCatalogName(getCatalogName());
         precondition.setSchemaName(getSchemaName());
         precondition.setTableName(getTableName());
         precondition.setIndexName(getIndexName());
         precondition.setColumnNames(getColumnNames());
         delegatedPrecondition = precondition;
      }
      delegatedPrecondition.check(database, changeLog, changeSet);
   }

   /**
    * Generates the table name containing the Hatbox index.
    *
    * @return the Hatbox table name.
    */
   protected String getHatboxTableName() {
      final String tableName;
      if (!StringUtils.hasUpperCase(getTableName())) {
         tableName = getTableName() + "_hatbox";
      } else {
         tableName = getTableName() + "_HATBOX";
      }
      return tableName;
   }

   /**
    * Creates an example of the database object for which to check.
    *
    * @param database
    *           the database instance.
    * @param tableName
    *           the table name of the index.
    * @return the database object example.
    */
   public DatabaseObject getExample(final Database database, final String tableName) {
      final Schema schema = new Schema(getCatalogName(), getSchemaName());
      final DatabaseObject example;

      // For GeoDB, the index is another table.
      if (database instanceof DerbyDatabase || database instanceof H2Database) {
         final String correctedTableName = database.correctObjectName(getHatboxTableName(),
               Table.class);
         example = new Table().setName(correctedTableName).setSchema(schema);
      } else {
         example = getIndexExample(database, schema, tableName);
      }
      return example;
   }

   /**
    * Generates the {@link Index} example (taken from {@link IndexExistsPrecondition}).
    *
    * @param database
    *           the database instance.
    * @param schema
    *           the schema instance.
    * @param tableName
    *           the table name of the index.
    * @return the index example.
    */
   protected Index getIndexExample(final Database database, final Schema schema,
         final String tableName) {
      final Index example = new Index();
      if (tableName != null) {
         example.setTable((Table) new Table().setName(
               database.correctObjectName(getTableName(), Table.class)).setSchema(schema));
      }
      example.setName(database.correctObjectName(getIndexName(), Index.class));
      if (StringUtils.trimToNull(getColumnNames()) != null) {
         for (final String columnName : getColumnNames().split("\\s*,\\s*")) {
            final Column column = new Column(database.correctObjectName(columnName, Column.class));
            example.getColumns().add(column);
         }
      }
      return example;
   }

   /**
    * @see liquibase.serializer.LiquibaseSerializable#getSerializedObjectName()
    */
   @Override
   public String getSerializedObjectName() {
      return "spatialIndexExists";
   }

   /**
    * @see liquibase.serializer.LiquibaseSerializable#getSerializableFields()
    */
   @Override
   public Set<String> getSerializableFields() {
      return new LinkedHashSet<String>(Arrays.asList("catalogName", "schemaName", "tableName",
            "columnNames", "indexName"));
   }

   /**
    * @see liquibase.serializer.LiquibaseSerializable#getSerializableFieldValue(java.lang.String)
    */
   @Override
   public Object getSerializableFieldValue(final String field) {
      final Object value;
      if ("catalogName".equals(field)) {
         value = getCatalogName();
      } else if ("schemaName".equals(field)) {
         value = getSchemaName();
      } else if ("tableName".equals(field)) {
         value = getTableName();
      } else if ("columnNames".equals(field)) {
         value = getColumnNames();
      } else if ("indexName".equals(field)) {
         value = getIndexName();
      } else {
         throw new UnexpectedLiquibaseException("Unexpected field request on "
               + getSerializedObjectName() + ": " + field);
      }
      return value;
   }

   /**
    * @see liquibase.serializer.LiquibaseSerializable#getSerializableFieldType(java.lang.String)
    */
   @Override
   public SerializationType getSerializableFieldType(final String field) {
      return SerializationType.NAMED_FIELD;
   }

   /**
    * @see liquibase.serializer.LiquibaseSerializable#getSerializedObjectNamespace()
    */
   @Override
   public String getSerializedObjectNamespace() {
      return XmlConstants.SPATIAL_CHANGELOG_NAMESPACE;
   }

   /**
    * @see liquibase.serializer.LiquibaseSerializable#serialize()
    */
   @Override
   public ParsedNode serialize() throws ParsedNodeException {
      final String namespace = getSerializedObjectNamespace();
      final ParsedNode node = new ParsedNode(namespace, getSerializedObjectName());
      for (final String field : getSerializableFields()) {
         final Object value = getSerializableFieldValue(field);
         node.addChild(namespace, field, value);
      }
      return node;
   }

   /**
    * @see liquibase.precondition.Precondition#load(liquibase.parser.core.ParsedNode,
    *      liquibase.resource.ResourceAccessor)
    */
   @Override
   public void load(final ParsedNode parsedNode, final ResourceAccessor resourceAccessor)
         throws ParsedNodeException {
      final String namespace = null;
      this.catalogName = parsedNode.getChildValue(namespace, "catalogName", String.class);
      this.schemaName = parsedNode.getChildValue(namespace, "schemaName", String.class);
      this.tableName = parsedNode.getChildValue(namespace, "tableName", String.class);
      this.columnNames = parsedNode.getChildValue(namespace, "columnNames", String.class);
      this.indexName = parsedNode.getChildValue(namespace, "indexName", String.class);
   }
}