<?php

namespace SilverStripe\ORM\Connect;

use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\FieldType\DBPrimaryKey;
use SilverStripe\ORM\FieldType\DBField;
use Exception;

/**
 * Represents and handles all schema management for a database
 */
abstract class DBSchemaManager
{

    /**
     *
     * @config
     * Check tables when running /dev/build, and repair them if necessary.
     * In case of large databases or more fine-grained control on how to handle
     * data corruption in tables, you can disable this behaviour and handle it
     * outside of this class, e.g. through a nightly system task with extended logging capabilities.
     *
     * @var bool
     */
    private static $check_and_repair_on_build = true;

    /**
     * Check if tables should be renamed in a case-sensitive fashion.
     * Note: This should still work even on case-insensitive databases.
     *
     * @var bool
     */
    private static $fix_table_case_on_build = true;

    /**
     * Instance of the database controller this schema belongs to
     *
     * @var Database
     */
    protected $database = null;

    /**
     * If this is false, then information about database operations
     * will be displayed, eg creation of tables.
     *
     * @var boolean
     */
    protected $supressOutput = false;

    /**
     * @var array
     */
    protected static $table_name_warnings = [];

    /**
     * @param string
     * @deprecated 4.0..5.0
     */
    public static function showTableNameWarning($table, $class)
    {
        static::$table_name_warnings[$table] = $class;
    }

    /**
     * Injector injection point for database controller
     *
     * @param Database $database
     */
    public function setDatabase(Database $database)
    {
        $this->database = $database;
    }

    /**
     * The table list, generated by the tableList() function.
     * Used by the requireTable() function.
     *
     * @var array
     */
    protected $tableList;

    /**
     * Keeps track whether we are currently updating the schema.
     *
     * @var boolean
     */
    protected $schemaIsUpdating = false;

    /**
     * Large array structure that represents a schema update transaction
     *
     * @var array
     */
    protected $schemaUpdateTransaction;

    /**
     * Enable supression of database messages.
     */
    public function quiet()
    {
        $this->supressOutput = true;
    }

    /**
     * Execute the given SQL query.
     * This abstract function must be defined by subclasses as part of the actual implementation.
     * It should return a subclass of SS_Query as the result.
     *
     * @param string $sql The SQL query to execute
     * @param int $errorLevel The level of error reporting to enable for the query
     * @return Query
     */
    public function query($sql, $errorLevel = E_USER_ERROR)
    {
        return $this->database->query($sql, $errorLevel);
    }


    /**
     * Execute the given SQL parameterised query with the specified arguments
     *
     * @param string $sql The SQL query to execute. The ? character will denote parameters.
     * @param array $parameters An ordered list of arguments.
     * @param int $errorLevel The level of error reporting to enable for the query
     * @return Query
     */
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
    {
        return $this->database->preparedQuery($sql, $parameters, $errorLevel);
    }

    /**
     * Initiates a schema update within a single callback
     *
     * @param callable $callback
     */
    public function schemaUpdate($callback)
    {
        // Begin schema update
        $this->schemaIsUpdating = true;

        // Update table list
        $this->tableList = array();
        $tables = $this->tableList();
        foreach ($tables as $table) {
            $this->tableList[strtolower($table)] = $table;
        }

        // Clear update list for client code to mess around with
        $this->schemaUpdateTransaction = array();

        /** @var Exception $error */
        $error = null;
        try {
            // Yield control to client code
            $callback();

            // If the client code has cancelled the update then abort
            if (!$this->isSchemaUpdating()) {
                return;
            }

            // End schema update
            foreach ($this->schemaUpdateTransaction as $tableName => $changes) {
                $advancedOptions = isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null;
                switch ($changes['command']) {
                    case 'create':
                        $this->createTable(
                            $tableName,
                            $changes['newFields'],
                            $changes['newIndexes'],
                            $changes['options'],
                            $advancedOptions
                        );
                        break;

                    case 'alter':
                        $this->alterTable(
                            $tableName,
                            $changes['newFields'],
                            $changes['newIndexes'],
                            $changes['alteredFields'],
                            $changes['alteredIndexes'],
                            $changes['alteredOptions'],
                            $advancedOptions
                        );
                        break;
                }
            }
        } finally {
            $this->schemaUpdateTransaction = null;
            $this->schemaIsUpdating = false;
        }
    }

    /**
     * Cancels the schema updates requested during (but not after) schemaUpdate() call.
     */
    public function cancelSchemaUpdate()
    {
        $this->schemaUpdateTransaction = null;
        $this->schemaIsUpdating = false;
    }

    /**
     * Returns true if we are during a schema update.
     *
     * @return boolean
     */
    function isSchemaUpdating()
    {
        return $this->schemaIsUpdating;
    }

    /**
     * Returns true if schema modifications were requested during (but not after) schemaUpdate() call.
     *
     * @return boolean
     */
    public function doesSchemaNeedUpdating()
    {
        return (bool) $this->schemaUpdateTransaction;
    }

    // Transactional schema altering functions - they don't do anything except for update schemaUpdateTransaction

    /**
     * Instruct the schema manager to record a table creation to later execute
     *
     * @param string $table Name of the table
     * @param array $options Create table options (ENGINE, etc.)
     * @param array $advanced_options Advanced table creation options
     */
    public function transCreateTable($table, $options = null, $advanced_options = null)
    {
        $this->schemaUpdateTransaction[$table] = array(
            'command' => 'create',
            'newFields' => array(),
            'newIndexes' => array(),
            'options' => $options,
            'advancedOptions' => $advanced_options
        );
    }

    /**
     * Instruct the schema manager to record a table alteration to later execute
     *
     * @param string $table Name of the table
     * @param array $options Create table options (ENGINE, etc.)
     * @param array $advanced_options Advanced table creation options
     */
    public function transAlterTable($table, $options, $advanced_options)
    {
        $this->transInitTable($table);
        $this->schemaUpdateTransaction[$table]['alteredOptions'] = $options;
        $this->schemaUpdateTransaction[$table]['advancedOptions'] = $advanced_options;
    }

    /**
     * Instruct the schema manager to record a field to be later created
     *
     * @param string $table Name of the table to hold this field
     * @param string $field Name of the field to create
     * @param string $schema Field specification as a string
     */
    public function transCreateField($table, $field, $schema)
    {
        $this->transInitTable($table);
        $this->schemaUpdateTransaction[$table]['newFields'][$field] = $schema;
    }

    /**
     * Instruct the schema manager to record an index to be later created
     *
     * @param string $table Name of the table to hold this index
     * @param string $index Name of the index to create
     * @param array $schema Already parsed index specification
     */
    public function transCreateIndex($table, $index, $schema)
    {
        $this->transInitTable($table);
        $this->schemaUpdateTransaction[$table]['newIndexes'][$index] = $schema;
    }

    /**
     * Instruct the schema manager to record a field to be later updated
     *
     * @param string $table Name of the table to hold this field
     * @param string $field Name of the field to update
     * @param string $schema Field specification as a string
     */
    public function transAlterField($table, $field, $schema)
    {
        $this->transInitTable($table);
        $this->schemaUpdateTransaction[$table]['alteredFields'][$field] = $schema;
    }

    /**
     * Instruct the schema manager to record an index to be later updated
     *
     * @param string $table Name of the table to hold this index
     * @param string $index Name of the index to update
     * @param array $schema Already parsed index specification
     */
    public function transAlterIndex($table, $index, $schema)
    {
        $this->transInitTable($table);
        $this->schemaUpdateTransaction[$table]['alteredIndexes'][$index] = $schema;
    }

    /**
     * Handler for the other transXXX methods - mark the given table as being altered
     * if it doesn't already exist
     *
     * @param string $table Name of the table to initialise
     */
    protected function transInitTable($table)
    {
        if (!isset($this->schemaUpdateTransaction[$table])) {
            $this->schemaUpdateTransaction[$table] = array(
                'command' => 'alter',
                'newFields' => array(),
                'newIndexes' => array(),
                'alteredFields' => array(),
                'alteredIndexes' => array(),
                'alteredOptions' => ''
            );
        }
    }

    /**
     * Generate the following table in the database, modifying whatever already exists
     * as necessary.
     *
     * @todo Change detection for CREATE TABLE $options other than "Engine"
     *
     * @param string $table The name of the table
     * @param array $fieldSchema A list of the fields to create, in the same form as DataObject::$db
     * @param array $indexSchema A list of indexes to create. See {@link requireIndex()}
     * The values of the array can be one of:
     *   - true: Create a single column index on the field named the same as the index.
     *   - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
     *     control over the index.
     * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
     * @param array $options Create table options (ENGINE, etc.)
     * @param array|bool $extensions List of extensions
     */
    public function requireTable(
        $table,
        $fieldSchema = null,
        $indexSchema = null,
        $hasAutoIncPK = true,
        $options = array(),
        $extensions = false
    ) {
        if (!isset($this->tableList[strtolower($table)])) {
            $this->transCreateTable($table, $options, $extensions);
            $this->alterationMessage("Table $table: created", "created");
        } else {
            if (Config::inst()->get(static::class, 'fix_table_case_on_build')) {
                $this->fixTableCase($table);
            }
            if (Config::inst()->get(static::class, 'check_and_repair_on_build')) {
                $this->checkAndRepairTable($table);
            }

            // Check if options changed
            $tableOptionsChanged = false;
            // Check for DB constant on the schema class
            $dbIDName = sprintf('%s::ID', static::class);
            $dbID = defined($dbIDName) ? constant($dbIDName) : null;
            if ($dbID && isset($options[$dbID])) {
                if (preg_match('/ENGINE=([^\s]*)/', $options[$dbID], $alteredEngineMatches)) {
                    $alteredEngine = $alteredEngineMatches[1];
                    $tableStatus = $this->query(sprintf('SHOW TABLE STATUS LIKE \'%s\'', $table))->first();
                    $tableOptionsChanged = ($tableStatus['Engine'] != $alteredEngine);
                }
            }

            if ($tableOptionsChanged || ($extensions && $this->database->supportsExtensions($extensions))) {
                $this->transAlterTable($table, $options, $extensions);
            }
        }

        //DB ABSTRACTION: we need to convert this to a db-specific version:
        if (!isset($fieldSchema['ID'])) {
            $this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK));
        }

        // Create custom fields
        if ($fieldSchema) {
            foreach ($fieldSchema as $fieldName => $fieldSpec) {
                //Is this an array field?
                $arrayValue = '';
                if (strpos($fieldSpec, '[') !== false) {
                    //If so, remove it and store that info separately
                    $pos = strpos($fieldSpec, '[');
                    $arrayValue = substr($fieldSpec, $pos);
                    $fieldSpec = substr($fieldSpec, 0, $pos);
                }

                /** @var DBField $fieldObj */
                $fieldObj = Injector::inst()->create($fieldSpec, $fieldName);
                $fieldObj->setArrayValue($arrayValue);

                $fieldObj->setTable($table);

                if ($fieldObj instanceof DBPrimaryKey) {
                    /** @var DBPrimaryKey $fieldObj */
                    $fieldObj->setAutoIncrement($hasAutoIncPK);
                }

                $fieldObj->requireField();
            }
        }

        // Create custom indexes
        if ($indexSchema) {
            foreach ($indexSchema as $indexName => $indexSpec) {
                $this->requireIndex($table, $indexName, $indexSpec);
            }
        }

        // Check and display notice about $table_name
        static $table_name_info_sent = false;

        if (isset(static::$table_name_warnings[$table])) {
            if (!$table_name_info_sent) {
                $this->alterationMessage(
                    <<<'MESSAGE'
<strong>Please note:</strong> It is strongly recommended to define a
table_name for all namespaced models. Not defining a table_name may cause generated table
names to be too long and may not be supported by your current database engine. The generated
naming scheme will also change when upgrading to SilverStripe 5.0 and potentially break.
MESSAGE
                    ,
                    'error'
                );
                $table_name_info_sent = true;
            }

            $this->alterationMessage('table_name not set for class ' . static::$table_name_warnings[$table], 'notice');
        }
    }

    /**
     * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
     * @param string $table The table name.
     */
    public function dontRequireTable($table)
    {
        if (isset($this->tableList[strtolower($table)])) {
            $suffix = '';
            while (isset($this->tableList[strtolower("_obsolete_{$table}$suffix")])) {
                $suffix = $suffix
                        ? ((int)$suffix + 1)
                        : 2;
            }
            $this->renameTable($table, "_obsolete_{$table}$suffix");
            $this->alterationMessage("Table $table: renamed to _obsolete_{$table}$suffix", "obsolete");
        }
    }

    /**
     * Generate the given index in the database, modifying whatever already exists as necessary.
     *
     * The keys of the array are the names of the index.
     * The values of the array can be one of:
     *  - true: Create a single column index on the field named the same as the index.
     *  - array('type' => 'index|unique|fulltext', 'value' => 'FieldA, FieldB'): This gives you full
     *    control over the index.
     *
     * @param string $table The table name.
     * @param string $index The index name.
     * @param string|array|boolean $spec The specification of the index in any
     * loose format. See requireTable() for more information.
     */
    public function requireIndex($table, $index, $spec)
    {
        // Detect if adding to a new table
        $newTable = !isset($this->tableList[strtolower($table)]);

        // Force spec into standard array format
        $specString = $this->convertIndexSpec($spec);

        // Check existing index
        $oldSpecString = null;
        $indexKey = null;
        if (!$newTable) {
            $indexKey = $this->indexKey($table, $index, $spec);
            $indexList = $this->indexList($table);
            if (isset($indexList[$indexKey])) {
                // $oldSpec should be in standard array format
                $oldSpec = $indexList[$indexKey];
                $oldSpecString = $this->convertIndexSpec($oldSpec);
            }
        }

        // Initiate either generation or modification of index
        if ($newTable || !isset($indexList[$indexKey])) {
            // New index
            $this->transCreateIndex($table, $index, $spec);
            $this->alterationMessage("Index $table.$index: created as $specString", "created");
        } elseif ($oldSpecString != $specString) {
            // Updated index
            $this->transAlterIndex($table, $index, $spec);
            $this->alterationMessage(
                "Index $table.$index: changed to $specString <i class=\"build-info-before\">(from $oldSpecString)</i>",
                "changed"
            );
        }
    }

    /**
     * Splits a spec string safely, considering quoted columns, whitespace,
     * and cleaning brackets
     *
     * @param string $spec The input index specification string
     * @return array List of columns in the spec
     */
    protected function explodeColumnString($spec)
    {
        // Remove any leading/trailing brackets and outlying modifiers
        // E.g. 'unique (Title, "QuotedColumn");' => 'Title, "QuotedColumn"'
        $containedSpec = preg_replace('/(.*\(\s*)|(\s*\).*)/', '', $spec);

        // Split potentially quoted modifiers
        // E.g. 'Title, "QuotedColumn"' => array('Title', 'QuotedColumn')
        return preg_split('/"?\s*,\s*"?/', trim($containedSpec, '(") '));
    }

    /**
     * Builds a properly quoted column list from an array
     *
     * @param array $columns List of columns to implode
     * @return string A properly quoted list of column names
     */
    protected function implodeColumnList($columns)
    {
        if (empty($columns)) {
            return '';
        }
        return '"' . implode('","', $columns) . '"';
    }

    /**
     * Given an index specification in the form of a string ensure that each
     * column name is property quoted, stripping brackets and modifiers.
     * This index may also be in the form of a "CREATE INDEX..." sql fragment
     *
     * @param string $spec The input specification or query. E.g. 'unique (Column1, Column2)'
     * @return string The properly quoted column list. E.g. '"Column1", "Column2"'
     */
    protected function quoteColumnSpecString($spec)
    {
        $bits = $this->explodeColumnString($spec);
        return $this->implodeColumnList($bits);
    }

    /**
     * Given an index spec determines the index type
     *
     * @param array|string $spec
     * @return string
     */
    protected function determineIndexType($spec)
    {
        // check array spec
        if (is_array($spec) && isset($spec['type'])) {
            return $spec['type'];
        } elseif (!is_array($spec) && preg_match('/(?<type>\w+)\s*\(/', $spec, $matchType)) {
            return strtolower($matchType['type']);
        } else {
            return 'index';
        }
    }

    /**
     * This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
     * and turns it into a proper string.
     * Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
     * arrays to be created. See {@link requireTable()} for details on the index format.
     *
     * @see http://dev.mysql.com/doc/refman/5.0/en/create-index.html
     * @see parseIndexSpec() for approximate inverse
     *
     * @param string|array $indexSpec
     * @return string
     */
    protected function convertIndexSpec($indexSpec)
    {
        // Return already converted spec
        if (!is_array($indexSpec)
            || !array_key_exists('type', $indexSpec)
            || !array_key_exists('columns', $indexSpec)
            || !is_array($indexSpec['columns'])
            || array_key_exists('value', $indexSpec)
        ) {
            throw new \InvalidArgumentException(
                sprintf(
                    'argument to convertIndexSpec must be correct indexSpec, %s given',
                    var_export($indexSpec, true)
                )
            );
        }

        // Combine elements into standard string format
        return sprintf('%s (%s)', $indexSpec['type'], $this->implodeColumnList($indexSpec['columns']));
    }

    /**
     * Returns true if the given table is exists in the current database
     *
     * @param string $tableName Name of table to check
     * @return boolean Flag indicating existence of table
     */
    abstract public function hasTable($tableName);

    /**
     * Return true if the table exists and already has a the field specified
     *
     * @param string $tableName - The table to check
     * @param string $fieldName - The field to check
     * @return bool - True if the table exists and the field exists on the table
     */
    public function hasField($tableName, $fieldName)
    {
        if (!$this->hasTable($tableName)) {
            return false;
        }
        $fields = $this->fieldList($tableName);
        return array_key_exists($fieldName, $fields);
    }

    /**
     * Generate the given field on the table, modifying whatever already exists as necessary.
     *
     * @param string $table The table name.
     * @param string $field The field name.
     * @param array|string $spec The field specification. If passed in array syntax, the specific database
     *  driver takes care of the ALTER TABLE syntax. If passed as a string, its assumed to
     *  be prepared as a direct SQL framgment ready for insertion into ALTER TABLE. In this case you'll
     *  need to take care of database abstraction in your DBField subclass.
     */
    public function requireField($table, $field, $spec)
    {
        //TODO: this is starting to get extremely fragmented.
        //There are two different versions of $spec floating around, and their content changes depending
        //on how they are structured.  This needs to be tidied up.
        $fieldValue = null;
        $newTable = false;

        // backwards compatibility patch for pre 2.4 requireField() calls
        $spec_orig = $spec;

        if (!is_string($spec)) {
            $spec['parts']['name'] = $field;
            $spec_orig['parts']['name'] = $field;
            //Convert the $spec array into a database-specific string
            $spec = $this->{$spec['type']}($spec['parts'], true);
        }

        // Collations didn't come in until MySQL 4.1.  Anything earlier will throw a syntax error if you try and use
        // collations.
        // TODO: move this to the MySQLDatabase file, or drop it altogether?
        if (!$this->database->supportsCollations()) {
            $spec = preg_replace('/ *character set [^ ]+( collate [^ ]+)?( |$)/', '\\2', $spec);
        }

        if (!isset($this->tableList[strtolower($table)])) {
            $newTable = true;
        }

        if (is_array($spec)) {
            $specValue = $this->$spec_orig['type']($spec_orig['parts']);
        } else {
            $specValue = $spec;
        }

        // We need to get db-specific versions of the ID column:
        if ($spec_orig == $this->IdColumn() || $spec_orig == $this->IdColumn(true)) {
            $specValue = $this->IdColumn(true);
        }

        if (!$newTable) {
            $fieldList = $this->fieldList($table);
            if (isset($fieldList[$field])) {
                if (is_array($fieldList[$field])) {
                    $fieldValue = $fieldList[$field]['data_type'];
                } else {
                    $fieldValue = $fieldList[$field];
                }
            }
        }

        // Get the version of the field as we would create it. This is used for comparison purposes to see if the
        // existing field is different to what we now want
        if (is_array($spec_orig)) {
            $spec_orig = $this->{$spec_orig['type']}($spec_orig['parts']);
        }

        if ($newTable || $fieldValue == '') {
            $this->transCreateField($table, $field, $spec_orig);
            $this->alterationMessage("Field $table.$field: created as $spec_orig", "created");
        } elseif ($fieldValue != $specValue) {
            // If enums/sets are being modified, then we need to fix existing data in the table.
            // Update any records where the enum is set to a legacy value to be set to the default.
            foreach (array('enum', 'set') as $enumtype) {
                if (preg_match("/^$enumtype/i", $specValue)) {
                    $newStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $spec_orig);
                    $new = preg_split("/'\\s*,\\s*'/", $newStr);

                    $oldStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $fieldValue);
                    $old = preg_split("/'\\s*,\\s*'/", $oldStr);

                    $holder = array();
                    foreach ($old as $check) {
                        if (!in_array($check, $new)) {
                            $holder[] = $check;
                        }
                    }
                    if (count($holder)) {
                        $default = explode('default ', $spec_orig);
                        $default = $default[1];
                        $query = "UPDATE \"$table\" SET $field=$default WHERE $field IN (";
                        for ($i = 0; $i + 1 < count($holder); $i++) {
                            $query .= "'{$holder[$i]}', ";
                        }
                        $query .= "'{$holder[$i]}')";
                        $this->query($query);
                        $amount = $this->database->affectedRows();
                        $this->alterationMessage("Changed $amount rows to default value of field $field"
                                . " (Value: $default)");
                    }
                }
            }
            $this->transAlterField($table, $field, $spec_orig);
            $this->alterationMessage(
                "Field $table.$field: changed to $specValue <i class=\"build-info-before\">(from {$fieldValue})</i>",
                "changed"
            );
        }
    }

    /**
     * If the given field exists, move it out of the way by renaming it to _obsolete_(fieldname).
     *
     * @param string $table
     * @param string $fieldName
     */
    public function dontRequireField($table, $fieldName)
    {
        $fieldList = $this->fieldList($table);
        if (array_key_exists($fieldName, $fieldList)) {
            $suffix = '';
            while (isset($fieldList[strtolower("_obsolete_{$fieldName}$suffix")])) {
                $suffix = $suffix
                        ? ((int)$suffix + 1)
                        : 2;
            }
            $this->renameField($table, $fieldName, "_obsolete_{$fieldName}$suffix");
            $this->alterationMessage(
                "Field $table.$fieldName: renamed to $table._obsolete_{$fieldName}$suffix",
                "obsolete"
            );
        }
    }

    /**
     * Show a message about database alteration
     *
     * @param string $message to display
     * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
     */
    public function alterationMessage($message, $type = "")
    {
        if (!$this->supressOutput) {
            if (Director::is_cli()) {
                switch ($type) {
                    case "created":
                    case "changed":
                    case "repaired":
                        $sign = "+";
                        break;
                    case "obsolete":
                    case "deleted":
                        $sign = '-';
                        break;
                    case "notice":
                        $sign = '*';
                        break;
                    case "error":
                        $sign = "!";
                        break;
                    default:
                        $sign = " ";
                }
                $message = strip_tags($message);
                echo "  $sign $message\n";
            } else {
                switch ($type) {
                    case "created":
                        $class = "success";
                        break;
                    case "obsolete":
                        $class = "error";
                        break;
                    case "notice":
                        $class = "warning";
                        break;
                    case "error":
                        $class = "error";
                        break;
                    case "deleted":
                        $class = "error";
                        break;
                    case "changed":
                        $class = "info";
                        break;
                    case "repaired":
                        $class = "info";
                        break;
                    default:
                        $class = "";
                }
                echo "<li class=\"$class\">$message</li>";
            }
        }
    }

    /**
     * This returns the data type for the id column which is the primary key for each table
     *
     * @param boolean $asDbValue
     * @param boolean $hasAutoIncPK
     * @return string
     */
    abstract public function IdColumn($asDbValue = false, $hasAutoIncPK = true);

    /**
     * Checks a table's integrity and repairs it if necessary.
     *
     * @param string $tableName The name of the table.
     * @return boolean Return true if the table has integrity after the method is complete.
     */
    abstract public function checkAndRepairTable($tableName);


    /**
     * Ensure the given table has the correct case
     *
     * @param string $tableName Name of table in desired case
     */
    public function fixTableCase($tableName)
    {
        // Check if table exists
        $tables = $this->tableList();
        if (!array_key_exists(strtolower($tableName), $tables)) {
            return;
        }

        // Check if case differs
        $currentName = $tables[strtolower($tableName)];
        if ($currentName === $tableName) {
            return;
        }

        $this->alterationMessage(
            "Table $tableName: renamed from $currentName",
            "repaired"
        );

        // Rename via temp table to avoid case-sensitivity issues
        $tempTable = "__TEMP__{$tableName}";
        $this->renameTable($currentName, $tempTable);
        $this->renameTable($tempTable, $tableName);
    }

    /**
     * Returns the values of the given enum field
     *
     * @param string $tableName Name of table to check
     * @param string $fieldName name of enum field to check
     * @return array List of enum values
     */
    abstract public function enumValuesForField($tableName, $fieldName);


    /*
     * This is a lookup table for data types.
     * For instance, Postgres uses 'INT', while MySQL uses 'UNSIGNED'
     * So this is a DB-specific list of equivilents.
     *
     * @param string $type
     * @return string
     */
    abstract public function dbDataType($type);

    /**
     * Retrieves the list of all databases the user has access to
     *
     * @return array List of database names
     */
    abstract public function databaseList();

    /**
     * Determine if the database with the specified name exists
     *
     * @param string $name Name of the database to check for
     * @return boolean Flag indicating whether this database exists
     */
    abstract public function databaseExists($name);

    /**
     * Create a database with the specified name
     *
     * @param string $name Name of the database to create
     * @return boolean True if successful
     */
    abstract public function createDatabase($name);

    /**
     * Drops a database with the specified name
     *
     * @param string $name Name of the database to drop
     */
    abstract public function dropDatabase($name);

    /**
     * Alter an index on a table.
     *
     * @param string $tableName The name of the table.
     * @param string $indexName The name of the index.
     * @param array $indexSpec The specification of the index, see Database::requireIndex() for more details.
     * @todo Find out where this is called from - Is it even used? Aren't indexes always dropped and re-added?
     */
    abstract public function alterIndex($tableName, $indexName, $indexSpec);

    /**
     * Determines the key that should be used to identify this index
     * when retrieved from DBSchemaManager->indexList.
     * In some connectors this is the database-visible name, in others the
     * usercode-visible name.
     *
     * @param string $table
     * @param string $index
     * @param array $spec
     * @return string Key for this index
     */
    abstract protected function indexKey($table, $index, $spec);

    /**
     * Return the list of indexes in a table.
     *
     * @param string $table The table name.
     * @return array[array] List of current indexes in the table, each in standard
     * array form. The key for this array should be predictable using the indexKey
     * method
     */
    abstract public function indexList($table);

    /**
     * Returns a list of all tables in the database.
     * Keys are table names in lower case, values are table names in case that
     * database expects.
     *
     * @return array
     */
    abstract public function tableList();

    /**
     * Create a new table.
     *
     * @param string $table The name of the table
     * @param array $fields A map of field names to field types
     * @param array $indexes A map of indexes
     * @param array $options An map of additional options.  The available keys are as follows:
     *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
     *   - 'temporary' - If true, then a temporary table will be created
     * @param array $advancedOptions Advanced creation options
     * @return string The table name generated.  This may be different from the table name, for example with temporary
     * tables.
     */
    abstract public function createTable(
        $table,
        $fields = null,
        $indexes = null,
        $options = null,
        $advancedOptions = null
    );

    /**
     * Alter a table's schema.
     *
     * @param string $table The name of the table to alter
     * @param array $newFields New fields, a map of field name => field schema
     * @param array $newIndexes New indexes, a map of index name => index type
     * @param array $alteredFields Updated fields, a map of field name => field schema
     * @param array $alteredIndexes Updated indexes, a map of index name => index type
     * @param array $alteredOptions
     * @param array $advancedOptions
     */
    abstract public function alterTable(
        $table,
        $newFields = null,
        $newIndexes = null,
        $alteredFields = null,
        $alteredIndexes = null,
        $alteredOptions = null,
        $advancedOptions = null
    );

    /**
     * Rename a table.
     *
     * @param string $oldTableName The old table name.
     * @param string $newTableName The new table name.
     */
    abstract public function renameTable($oldTableName, $newTableName);

    /**
     * Create a new field on a table.
     *
     * @param string $table Name of the table.
     * @param string $field Name of the field to add.
     * @param string $spec The field specification, eg 'INTEGER NOT NULL'
     */
    abstract public function createField($table, $field, $spec);

    /**
     * Change the database column name of the given field.
     *
     * @param string $tableName The name of the tbale the field is in.
     * @param string $oldName The name of the field to change.
     * @param string $newName The new name of the field
     */
    abstract public function renameField($tableName, $oldName, $newName);

    /**
     * Get a list of all the fields for the given table.
     * Returns a map of field name => field spec.
     *
     * @param string $table The table name.
     * @return array
     */
    abstract public function fieldList($table);

    /**
     *
     * This allows the cached values for a table's field list to be erased.
     * If $tablename is empty, then the whole cache is erased.
     *
     * @param string $tableName
     * @return boolean
     */
    public function clearCachedFieldlist($tableName = null)
    {
        return true;
    }


    /**
     * Returns data type for 'boolean' column
     *
     * @param array $values Contains a tokenised list of info about this data type
     * @return string
     */
    abstract public function boolean($values);

    /**
     * Returns data type for 'date' column
     *
     * @param array $values Contains a tokenised list of info about this data type
     * @return string
     */
    abstract public function date($values);

    /**
     * Returns data type for 'decimal' column
     *
     * @param array $values Contains a tokenised list of info about this data type
     * @return string
     */
    abstract public function decimal($values);

    /**
     * Returns data type for 'set' column
     *
     * @param array $values Contains a tokenised list of info about this data type
     * @return string
     */
    abstract public function enum($values);

    /**
     * Returns data type for 'set' column
     *
     * @param array $values Contains a tokenised list of info about this data type
     * @return string
     */
    abstract public function set($values);

    /**
     * Returns data type for 'float' column
     *
     * @param array $values Contains a tokenised list of info about this data type
     * @return string
     */
    abstract public function float($values);

    /**
     * Returns data type for 'int' column
     *
     * @param array $values Contains a tokenised list of info about this data type
     * @return string
     */
    abstract public function int($values);

    /**
     * Returns data type for 'datetime' column
     *
     * @param array $values Contains a tokenised list of info about this data type
     * @return string
     */
    abstract public function datetime($values);

    /**
     * Returns data type for 'text' column
     *
     * @param array $values Contains a tokenised list of info about this data type
     * @return string
     */
    abstract public function text($values);

    /**
     * Returns data type for 'time' column
     *
     * @param array $values Contains a tokenised list of info about this data type
     * @return string
     */
    abstract public function time($values);

    /**
     * Returns data type for 'varchar' column
     *
     * @param array $values Contains a tokenised list of info about this data type
     * @return string
     */
    abstract public function varchar($values);

    /*
     * Returns data type for 'year' column
     *
     * @param array $values Contains a tokenised list of info about this data type
     * @return string
     */
    abstract public function year($values);
}
