package liquibase.ext.spatial.preconditions;

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.database.core.MySQLDatabase;
import liquibase.database.core.OracleDatabase;
import liquibase.database.core.PostgresDatabase;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.exception.PreconditionErrorException;
import liquibase.exception.PreconditionFailedException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.exception.ValidationErrors;
import liquibase.exception.Warnings;
import liquibase.executor.ExecutorService;
import liquibase.ext.spatial.xml.XmlConstants;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.precondition.AbstractPrecondition;
import liquibase.precondition.ErrorPrecondition;
import liquibase.precondition.core.TableExistsPrecondition;
import liquibase.precondition.core.ViewExistsPrecondition;
import liquibase.resource.ResourceAccessor;
import liquibase.statement.core.RawSqlStatement;

/**
 * <code>SpatialSupportedPrecondition</code> checks the state of the database and determines if it
 * has spatial support.
 *
 * @author Lonny Jacobson
 */
public class SpatialSupportedPrecondition extends AbstractPrecondition {
   @Override
   public String getName() {
      return "spatialSupported";
   }

   @Override
   public Warnings warn(final Database database) {
      final Warnings warnings = new Warnings();
      if (!(database instanceof DerbyDatabase || database instanceof H2Database
            || database instanceof MySQLDatabase || database instanceof OracleDatabase || database instanceof PostgresDatabase)) {
         warnings.addWarning(database.getDatabaseProductName()
               + " is not supported by this extension");
      }
      return warnings;
   }

   @Override
   public ValidationErrors validate(final Database database) {
      final ValidationErrors errors = new ValidationErrors();
      if (!(database instanceof DerbyDatabase || database instanceof H2Database
            || database instanceof MySQLDatabase || database instanceof OracleDatabase || database instanceof PostgresDatabase)) {
         errors.addError(database.getDatabaseProductName() + " is not supported by this extension");
      }
      return errors;
   }

   @Override
   public void check(final Database database, final DatabaseChangeLog changeLog,
         final ChangeSet changeSet) throws PreconditionFailedException, PreconditionErrorException {
      if (database instanceof DerbyDatabase || database instanceof H2Database) {
         final TableExistsPrecondition precondition = new TableExistsPrecondition();
         precondition.setTableName("geometry_columns");
         precondition.check(database, changeLog, changeSet);
      } else if (database instanceof PostgresDatabase) {
         final ViewExistsPrecondition precondition = new ViewExistsPrecondition();
         precondition.setSchemaName("public");
         precondition.setViewName("geometry_columns");
         precondition.check(database, changeLog, changeSet);
      } else if (database instanceof OracleDatabase) {
         // Explicitly query the database due to CORE-2198.
         final RawSqlStatement sql = new RawSqlStatement(
               "SELECT count(*) FROM ALL_VIEWS WHERE upper(VIEW_NAME)='USER_SDO_GEOM_METADATA' AND OWNER='MDSYS'");
         try {
            final Integer result = ExecutorService.getInstance().getExecutor(database)
                  .queryForObject(sql, Integer.class);
            if (result == null || result.intValue() == 0) {
               throw new PreconditionFailedException(
                     "The view MDSYS.USER_SDO_GEOM_METADATA does not exist. This database is not spatially enabled",
                     changeLog, this);
            }
         } catch (final DatabaseException e) {
            throw new PreconditionErrorException(e, changeLog, this);
         }
      } else if (!(database instanceof MySQLDatabase)) {
         final Throwable exception = new LiquibaseException(database.getDatabaseProductName()
               + " is not supported by this extension");
         final ErrorPrecondition errorPrecondition = new ErrorPrecondition(exception, changeLog,
               this);
         throw new PreconditionErrorException(errorPrecondition);
      }
   }

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

   /**
    * @see liquibase.serializer.LiquibaseSerializable#getSerializableFields()
    */
   @Override
   public Set<String> getSerializableFields() {
      return new LinkedHashSet<String>();
   }

   /**
    * @see liquibase.serializer.LiquibaseSerializable#getSerializableFieldValue(java.lang.String)
    */
   @Override
   public Object getSerializableFieldValue(final String field) {
      throw new UnexpectedLiquibaseException("Unexpected field request on "
            + getSerializedObjectName() + ": " + field);
   }

   /**
    * @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 {
      return new ParsedNode(getSerializedObjectNamespace(), getSerializedObjectName());
   }

   /**
    * @see liquibase.precondition.Precondition#load(liquibase.parser.core.ParsedNode,
    *      liquibase.resource.ResourceAccessor)
    */
   @Override
   public void load(final ParsedNode parsedNode, final ResourceAccessor resourceAccessor)
         throws ParsedNodeException {
   }
}