<?php
/**
 * @package   awf
 * @copyright Copyright (c)2014-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU GPL version 3 or later
 */

namespace Awf\Database\Driver;

/**
 * MySQL database driver supporting PDO based connections
 *
 * This class is adapted from the Joomla! Framework
 *
 * @see    http://php.net/manual/en/ref.pdo-mysql.php
 * @since  1.0
 */
class Pdomysql extends Pdo
{
    /**
     * The name of the database driver.
     *
     * @var    string
     * @since  1.0
     */
    public $name = 'pdomysql';

    /**
     * The character(s) used to quote SQL statement names such as table names or field names,
     * etc. The child classes should define this as necessary.  If a single character string the
     * same character is used for both sides of the quoted name, else the first character will be
     * used for the opening quote and the second for the closing quote.
     *
     * @var    string
     * @since  1.0
     */
    protected $nameQuote = '`';

    /**
     * The null or zero representation of a timestamp for the database driver.  This should be
     * defined in child classes to hold the appropriate value for the engine.
     *
     * @var    string
     * @since  1.0
     */
    protected $nullDate = '0000-00-00 00:00:00';

    /**
     * The minimum supported database version.
     *
     * @var    string
     * @since  1.0
     */
    protected static $dbMinimum = '5.0.4';

	/**
     * Constructor.
     *
     * @param   array  $options  Array of database options with keys: host, user, password, database, select.
     *
     * @since   1.0
     */
    public function __construct($options)
    {
        // Get some basic values from the options.
        $options['driver']        = 'mysql';
        $options['charset']       = (isset($options['charset'])) ? $options['charset'] : 'utf8';

        // As soon as PDO connect set the sql_mode to '' (avoids issues with the date being NULL/zero
	    $MySQLAttrInitCommand = version_compare(PHP_VERSION, '8.4.9999', '>=')
		    ? \Pdo\Mysql::ATTR_INIT_COMMAND
		    : \PDO::MYSQL_ATTR_INIT_COMMAND;

        $options['driverOptions'] = [$MySQLAttrInitCommand => 'SET sql_mode=""'];

        $this->charset = $options['charset'];

        // Finalize initialisation.
        parent::__construct($options);
    }

    /**
     * Connects to the database if needed.
     *
     * @return  void  Returns void if the database connected successfully.
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function connect()
    {
        parent::connect();

        $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
        $this->connection->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true);
    }

    /**
     * Test to see if the MySQL connector is available.
     *
     * @return  boolean  True on success, false otherwise.
     *
     * @since   1.0
     */
    public static function isSupported()
    {
        if (!class_exists('\PDO') || !class_exists('\PDOException') || !class_exists('\PDOStatement'))
        {
            return false;
        }

        return in_array('mysql', \PDO::getAvailableDrivers());
    }

    /**
     * Drops a table from the database.
     *
     * @param   string   $tableName  The name of the database table to drop.
     * @param   boolean  $ifExists   Optionally specify that the table must exist before it is dropped.
     *
     * @return  MysqlDriver  Returns this object to support chaining.
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function dropTable($tableName, $ifExists = true)
    {
        $this->connect();

        $query = $this->getQuery(true);

        $query->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $this->quoteName($tableName));

        $this->setQuery($query);

        $this->execute();

        return $this;
    }

    /**
     * Select a database for use.
     *
     * @param   string  $database  The name of the database to select for use.
     *
     * @return  boolean  True if the database was successfully selected.
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function select($database)
    {
        $this->connect();

        $this->setQuery('USE ' . $this->quoteName($database));

        $this->execute();

        return $this;
    }

    /**
     * Method to get the database collation in use by sampling a text field of a table in the database.
     *
     * @return  mixed  The collation in use by the database (string) or boolean false if not supported.
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function getCollation()
    {
        $this->connect();

        $tables = $this->getTableList();

        $this->setQuery('SHOW FULL COLUMNS FROM ' . $tables[0]);
        $array = $this->loadAssocList();

        foreach ($array as $field)
        {
            if (!is_null($field['Collation']))
            {
                return $field['Collation'];
            }
        }

        return null;
    }

    /**
     * Shows the table CREATE statement that creates the given tables.
     *
     * @param   mixed  $tables  A table name or a list of table names.
     *
     * @return  array  A list of the create SQL for the tables.
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function getTableCreate($tables)
    {
        $this->connect();

        // Initialise variables.
        $result = array();

        // Sanitize input to an array and iterate over the list.
        settype($tables, 'array');

        foreach ($tables as $table)
        {
            $this->setQuery('SHOW CREATE TABLE ' . $this->quoteName($table));

            $row = $this->loadRow();

            // Populate the result array based on the create statements.
            $result[$table] = $row[1];
        }

        return $result;
    }

    /**
     * Retrieves field information about a given table.
     *
     * @param   string   $table     The name of the database table.
     * @param   boolean  $typeOnly  True to only return field types.
     *
     * @return  array  An array of fields for the database table.
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function getTableColumns($table, $typeOnly = true)
    {
        $this->connect();

        $result = array();

        // Set the query to get the table fields statement.
        $this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($table));

        $fields = $this->loadObjectList();

        // If we only want the type as the value add just that to the list.
        if ($typeOnly)
        {
            foreach ($fields as $field)
            {
                $result[$field->Field] = preg_replace("/[(0-9)]/", '', $field->Type);
            }
        }
        // If we want the whole field data object add that to the list.
        else
        {
            foreach ($fields as $field)
            {
                $result[$field->Field] = $field;
            }
        }

        return $result;
    }

    /**
     * Get the details list of keys for a table.
     *
     * @param   string  $table  The name of the table.
     *
     * @return  array  An array of the column specification for the table.
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function getTableKeys($table)
    {
        $this->connect();

        // Get the details columns information.
        $this->setQuery('SHOW KEYS FROM ' . $this->quoteName($table));

        $keys = $this->loadObjectList();

        return $keys;
    }

    /**
     * Method to get an array of all tables in the database.
     *
     * @return  array  An array of all the tables in the database.
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function getTableList()
    {
        $this->connect();

        // Set the query to get the tables statement.
        $this->setQuery('SHOW TABLES');
        $tables = $this->loadColumn();

        return $tables;
    }

    /**
     * Get the version of the database connector.
     *
     * @return  string  The database connector version.
     *
     * @since   1.0
     */
    public function getVersion()
    {
        $this->connect();

        return $this->getOption(\PDO::ATTR_SERVER_VERSION);
    }

    /**
     * Locks a table in the database.
     *
     * @param   string  $table  The name of the table to unlock.
     *
     * @return  MysqlDriver  Returns this object to support chaining.
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function lockTable($table)
    {
        $this->setQuery('LOCK TABLES ' . $this->quoteName($table) . ' WRITE');

        $this->execute();

        return $this;
    }

    /**
     * Renames a table in the database.
     *
     * @param   string  $oldTable  The name of the table to be renamed
     * @param   string  $newTable  The new name for the table.
     * @param   string  $backup    Not used by MySQL.
     * @param   string  $prefix    Not used by MySQL.
     *
     * @return  MysqlDriver  Returns this object to support chaining.
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
    {
        $this->setQuery('RENAME TABLE ' . $this->quoteName($oldTable) . ' TO ' . $this->quoteName($newTable));

        $this->execute();

        return $this;
    }

    /**
     * Method to escape a string for usage in an SQL statement.
     *
     * Oracle escaping reference:
     * http://www.orafaq.com/wiki/SQL_FAQ#How_does_one_escape_special_characters_when_writing_SQL_queries.3F
     *
     * SQLite escaping notes:
     * http://www.sqlite.org/faq.html#q14
     *
     * Method body is as implemented by the Zend Framework
     *
     * Note: Using query objects with bound variables is
     * preferable to the below.
     *
     * @param   mixed    $text   The string to be escaped.
     * @param   boolean  $extra  Unused optional parameter to provide extra escaping.
     *
     * @return  string  The escaped string.
     *
     * @since   1.0
     */
    public function escape($text, $extra = false)
    {
        $this->connect();

        if (is_int($text) || is_float($text))
        {
            return $text;
        }

		if ($text === NULL)
		{
			return 'NULL';
		}

        $result = substr($this->connection->quote($text), 1, -1);

        if ($extra)
        {
            $result = addcslashes($result, '%_');
        }

        return $result;
    }

    /**
     * Unlocks tables in the database.
     *
     * @return  MysqlDriver  Returns this object to support chaining.
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function unlockTables()
    {
        $this->setQuery('UNLOCK TABLES')->execute();

        return $this;
    }

    /**
     * Method to commit a transaction.
     *
     * @param   boolean  $toSavepoint  If true, commit to the last savepoint.
     *
     * @return  void
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function transactionCommit($toSavepoint = false)
    {
        $this->connect();

        if (!$toSavepoint || $this->transactionDepth <= 1)
        {
            parent::transactionCommit($toSavepoint);
        }
        else
        {
            $this->transactionDepth--;
        }
    }

    /**
     * Method to roll back a transaction.
     *
     * @param   boolean  $toSavepoint  If true, rollback to the last savepoint.
     *
     * @return  void
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function transactionRollback($toSavepoint = false)
    {
        $this->connect();

        if (!$toSavepoint || $this->transactionDepth <= 1)
        {
            parent::transactionRollback($toSavepoint);
        }
        else
        {
            $savepoint = 'SP_' . ($this->transactionDepth - 1);
            $this->setQuery('ROLLBACK TO SAVEPOINT ' . $this->quoteName($savepoint));

            if ($this->execute())
            {
                $this->transactionDepth--;
            }
        }
    }

    /**
     * Method to initialize a transaction.
     *
     * @param   boolean  $asSavepoint  If true and a transaction is already active, a savepoint will be created.
     *
     * @return  void
     *
     * @since   1.0
     * @throws  \RuntimeException
     */
    public function transactionStart($asSavepoint = false)
    {
        $this->connect();

        if (!$asSavepoint || !$this->transactionDepth)
        {
            parent::transactionStart($asSavepoint);
        }
        else
        {
            $savepoint = 'SP_' . $this->transactionDepth;
            $this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint));

            if ($this->execute())
            {
                $this->transactionDepth++;
            }
        }
    }
}
