<?php
/**
 * Database functions
 *
 * Standard: PSR-2
 *
 * @package SC\DUPX\DB
 * @link http://www.php-fig.org/psr/psr-2/
 *
 */
defined('ABSPATH') || defined('DUPXABSPATH') || exit;

class DUPX_DB_Functions
{

    /**
     *
     * @var self
     */
    protected static $instance = null;

    /**
     *
     * @var database connection
     */
    private $dbh = null;

    /**
     * current data connection
     * @var database connection
     */
    private $dataConnection = null;

    /**
     * supported charset and collation data
     * 
     * @var array 
     */
    private $charsetData = null;

    /**
     * default charset in dwtabase connection
     * 
     * @var string 
     */
    private $defaultCharset = null;

    private function __construct()
    {
        $this->timeStart = DUPX_U::getMicrotime();
    }

    /**
     *
     * @return self
     */
    public static function getInstance()
    {
        if (is_null(self::$instance)) {
            self::$instance = new self;
        }
        return self::$instance;
    }

    /**
     * open db connection if is closed
     * 
     * @return database connection handle
     * 
     */
    public function dbConnection($customConnection = null)
    {
        if (is_null($this->dbh)) {
            if (is_null($customConnection)) {
                $paramsManager = DUPX_Paramas_Manager::getInstance();

                if (!DUPX_Validation_manager::isValidated()) {
                    throw new Exception('Installer isn\'t validated');
                }

                $dbhost = $paramsManager->getValue(DUPX_Paramas_Manager::PARAM_DB_HOST);
                $dbname = $paramsManager->getValue(DUPX_Paramas_Manager::PARAM_DB_NAME);
                $dbuser = $paramsManager->getValue(DUPX_Paramas_Manager::PARAM_DB_USER);
                $dbpass = $paramsManager->getValue(DUPX_Paramas_Manager::PARAM_DB_PASS);
            } else {
                $dbhost = $customConnection['dbhost'];
                $dbname = $customConnection['dbname'];
                $dbuser = $customConnection['dbuser'];
                $dbpass = $customConnection['dbpass'];
            }

            //MYSQL CONNECTION
            if (($dbh = DUPX_DB::connect($dbhost, $dbuser, $dbpass, $dbname)) == false) {
                $dbConnError = (mysqli_connect_error()) ? 'Error: '.mysqli_connect_error() : 'Unable to Connect';
                $msg         = "Unable to connect with the following parameters:<br/>"
                    ."HOST: ".DUPX_Log::varToString($dbhost)."\n"
                    ."DBUSER: ".DUPX_Log::varToString($dbuser)."\n"
                    ."DATABASE: ".DUPX_Log::varToString($dbname)."\n"
                    ."MESSAGE: ".$dbConnError;
                DUPX_Log::error($msg);
            } else {
                $this->dbh            = $dbh;
                $this->dataConnection = array(
                    'dbhost' => $dbhost,
                    'dbname' => $dbname,
                    'dbuser' => $dbuser,
                    'dbpass' => $dbpass
                );
            }

            if (is_null($customConnection)) {
                $db_max_time = mysqli_real_escape_string($this->dbh, $GLOBALS['DB_MAX_TIME']);
                DUPX_DB::mysqli_query($this->dbh, "SET wait_timeout = ".mysqli_real_escape_string($this->dbh, $db_max_time));
                DUPX_DB::setCharset($this->dbh, $paramsManager->getValue(DUPX_Paramas_Manager::PARAM_DB_CHARSET), $paramsManager->getValue(DUPX_Paramas_Manager::PARAM_DB_COLLATE));
            }
        }
        return $this->dbh;
    }

    /**
     * close db connection if is open
     */
    public function closeDbConnection()
    {
        if (!is_null($this->dbh)) {
            mysqli_close($this->dbh);
            $this->dbh            = null;
            $this->dataConnection = null;
            $this->charsetData    = null;
            $this->defaultCharset = null;
        }
    }

    public function getDefaultCharset()
    {
        if (is_null($this->defaultCharset)) {
            $this->dbConnection();

            // SHOW VARIABLES LIKE "character_set_database"
            if (($result = DUPX_DB::mysqli_query($this->dbh, "SHOW VARIABLES LIKE 'character_set_database'")) === false) {
                throw new Exception('SQL ERROR:'.mysqli_error($this->dbh));
            }

            if ($result->num_rows != 1) {
                throw new Exception('DEFAULT CHARSET NUMBER NOT VALID NUM '.$result->num_rows);
            }

            while ($row = $result->fetch_array()) {
                $this->defaultCharset = $row[1];
            }

            $result->free();
        }
        return $this->defaultCharset;
    }

    /**
     * 
     * @param string $charset
     * @return string|bool // false if charset don't exists
     */
    public function getDefaultCollateOfCharset($charset)
    {
        $this->getCharsetAndCollationData();
        return isset($this->charsetData[$charset]) ? $this->charsetData[$charset]['defCollation'] : false;
    }

    /**
     * 
     * @return array
     * @throws Exception
     */
    public function getCharsetAndCollationData()
    {
        if (is_null($this->charsetData)) {
            $this->dbConnection();

            if (($result = DUPX_DB::mysqli_query($this->dbh, "SHOW COLLATION")) === false) {
                throw new Exception('SQL ERROR:'.mysqli_error($this->dbh));
            }

            while ($row = $result->fetch_array()) {
                $collation = $row[0];
                $charset   = $row[1];
                $default   = filter_var($row[3], FILTER_VALIDATE_BOOLEAN);
                $compiled  = filter_var($row[4], FILTER_VALIDATE_BOOLEAN);

                if (!$compiled) {
                    continue;
                }

                if (!isset($this->charsetData[$charset])) {
                    $this->charsetData[$charset] = array(
                        'defCollation' => false,
                        'collations'   => array()
                    );
                }

                $this->charsetData[$charset]['collations'][] = $collation;
                if ($default) {
                    $this->charsetData[$charset]['defCollation'] = $collation;
                }
            }

            $result->free();

            ksort($this->charsetData);
            foreach (array_keys($this->charsetData) as $charset) {
                sort($this->charsetData[$charset]['collations']);
            }
        }
        return $this->charsetData;
    }

    /**
     * 
     * @return string[]
     */
    public function getCharsetsList()
    {
        return array_keys($this->getCharsetAndCollationData());
    }

    /**
     * 
     * @return string[]
     */
    public function getCollationsList()
    {
        $result = array();
        foreach ($this->getCharsetAndCollationData() as $charsetInfo) {
            $result = array_merge($result, $charsetInfo['collations']);
        }
        return array_unique($result);
    }

    public function getRealCharsetByParam()
    {
        $this->getCharsetAndCollationData();
        //$sourceCharset = DUPX_ArchiveConfig::getInstance()->getWpConfigDefineValue('DB_CHARSET', '');
        $sourceCharset = DUPX_Paramas_Manager::getInstance()->getValue(DUPX_Paramas_Manager::PARAM_DB_CHARSET);
        return (array_key_exists($sourceCharset, $this->charsetData) ? $sourceCharset : $this->getDefaultCharset());
    }

    public function getRealCollateByParam()
    {
        $this->getCharsetAndCollationData();
        $charset       = $this->getRealCharsetByParam();
        //$sourceCollate = DUPX_ArchiveConfig::getInstance()->getWpConfigDefineValue('DB_COLLATE', '');
        $sourceCollate = DUPX_Paramas_Manager::getInstance()->getValue(DUPX_Paramas_Manager::PARAM_DB_COLLATE);
        return (strlen($sourceCollate) == 0 || !in_array($sourceCollate, $this->charsetData[$charset]['collations'])) ?
            $this->getDefaultCollateOfCharset($charset) :
            $sourceCollate;
    }

    /**
     * regex managed examples
     *  - `meta_value` longtext CHARACTER SET utf16 COLLATE utf16_slovak_ci DEFAULT NULL,
     *  - `comment_author` tinytext COLLATE utf8mb4_unicode_ci NOT NULL,
     *  - ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci_test;
     *  - ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4;
     * 
     * accept ['"`]charset['"`]
     * 
     * @return boolean|array
     */
    public function getLegacyCharsetAndCollationRegexes()
    {
        $regResult = null;

        if (is_null($regResult)) {
            $invalidCharsets   = DUPX_ArchiveConfig::getInstance()->invalidCharsets();
            $invalidCollations = DUPX_ArchiveConfig::getInstance()->invalidCollations();
            $defCharsetRegex   = addcslashes($this->getRealCharsetByParam(), '\\$');
            $defCollateRegex   = addcslashes($this->getRealCollateByParam(), '\\$');

            $regResult = array(
                'search'  => array(),
                'replace' => array()
            );

            if (count($invalidCharsets) > 0) {
                $invalidChrRegex = '(?:'.implode('|', array_map(function ($val) {
                            return preg_quote($val, '/');
                        }, $invalidCharsets)).')';

                $regResult['search'][]  = '/(^.*(?:CHARSET|CHARACTER SET)\s*[\s=]\s*[`\'"]?)('.$invalidChrRegex.')([`\'"]?\s.*COLLATE\s*[\s=]\s*[`\'"]?)([^`\'"\s;]+)([`\'"]?.*$)/m';
                $regResult['replace'][] = '$1'.$defCharsetRegex.'$3'.$defCollateRegex.'$5';
                $regResult['search'][]  = '/(^.*COLLATE\s*[\s=]\s*[`\'"]?)([^`\'"\s;]+)([`\'"]?\s.*(?:CHARSET|CHARACTER SET)\s*[\s=]\s*[`\'"]?)('.$invalidChrRegex.')([`\'"]?.*$)/m';
                $regResult['replace'][] = '$1'.$defCollateRegex.'$3'.$defCharsetRegex.'$5';
                $regResult['search'][]  = '/(^.*(?:CHARSET|CHARACTER SET)\s*[\s=]\s*[`\'"]?)('.$invalidChrRegex.')([`\'"]?.*$)/m';
                $regResult['replace'][] = '$1'.$defCharsetRegex.'$3';
            }

            if (count($invalidCollations) > 0) {
                $invalidColRegex = '(?:'.implode('|', array_map(function ($val) {
                            return preg_quote($val, '/');
                        }, $invalidCollations)).')';

                $regResult['search'][]  = '/(^.*(?:CHARSET|CHARACTER SET)\s*[\s=]\s*[`\'"]?)([^`\'"\s;]+)([`\'"]?\s.*COLLATE\s*[\s=]\s*[`\'"]?)('.$invalidColRegex.')([`\'"]?.*$)/m';
                $regResult['replace'][] = '$1'.$defCharsetRegex.'$3'.$defCollateRegex.'$5';
                $regResult['search'][]  = '/(^.*COLLATE\s*[\s=]\s*[`\'"]?)('.$invalidColRegex.')([`\'"]?\s.*(?:CHARSET|CHARACTER SET)\s*[\s=]\s*[`\'"]?)([^`\'"\s;]+)([`\'"]?.*$)/m';
                $regResult['replace'][] = '$1'.$defCollateRegex.'$3'.$defCharsetRegex.'$5';
                $regResult['search'][]  = '/(^.*COLLATE\s*[\s=]\s*[`\'"]?)('.$invalidColRegex.')([`\'"]?.*$)/m';
                $regResult['replace'][] = '$1'.$defCollateRegex.'$3';
            }

            if (count($regResult['search']) > 0) {
                DUPX_Log::info('LEGACY CHARSET REPLACE');

                for ($i = 0; $i < count($regResult['search']); $i++) {
                    DUPX_Log::info('SEARCH  '.$regResult['search'][$i]);
                    DUPX_Log::info('REPLACE '.$regResult['replace'][$i]);
                }
            } else {
                $regResult = false;
            }
        }

        return $regResult;
    }

    /**
     * 
     * @return string
     */
    public static function getPostsTableName($prefix = null)
    {
        if (is_null($prefix)) {
            $prefix = DUPX_Paramas_Manager::getInstance()->getValue(DUPX_Paramas_Manager::PARAM_DB_TABLE_PREFIX);
        }
        return $prefix.'posts';
    }

    /**
     * 
     * @return string
     */
    public static function getUserTableName($prefix = null)
    {
        if (is_null($prefix)) {
            $prefix = DUPX_Paramas_Manager::getInstance()->getValue(DUPX_Paramas_Manager::PARAM_DB_TABLE_PREFIX);
        }
        return $prefix.'users';
    }

    /**
     * 
     * @return string
     */
    public static function getUserMetaTableName($prefix = null)
    {
        if (is_null($prefix)) {
            $prefix = DUPX_Paramas_Manager::getInstance()->getValue(DUPX_Paramas_Manager::PARAM_DB_TABLE_PREFIX);
        }
        return $prefix.'usermeta';
    }

    /**
     * 
     * @param string $userLogin
     * @return boolean return true if user login name exists in users table
     * @throws Exception
     */
    public function checkIfUserNameExists($userLogin)
    {
        if (!$this->tablesExist(self::getUserTableName())) {
            return false;
        }

        $query = 'SELECT ID FROM `'.mysqli_real_escape_string($this->dbh, self::getUserTableName()).'` '
            .'WHERE user_login="'.mysqli_real_escape_string($this->dbh, $userLogin).'"';

        if (($result = DUPX_DB::mysqli_query($this->dbh, $query)) === false) {
            throw new Exception('SQL ERROR:'.mysqli_error($this->dbh));
        }

        return ($result->num_rows > 0);
    }

    public function userPwdReset($userId, $newPassword)
    {
        $tableName = mysqli_real_escape_string($this->dbh, self::getUserTableName());
        $query     = 'UPDATE `'.$tableName.'` '
            .'SET `user_pass` = MD5("'.mysqli_real_escape_string($this->dbh, $newPassword).'") '
            .'WHERE `'.$tableName.'`.`ID` = '.$userId;
        if (($result    = DUPX_DB::mysqli_query($this->dbh, $query)) === false) {
            throw new Exception('SQL ERROR:'.mysqli_error($this->dbh));
        } else {
            return true;
        }
    }

    /**
     * return true if all tables passed in list exists
     * 
     * @param string|array $tables
     */
    public function tablesExist($tables)
    {
        //SHOW TABLES FROM c1_temptest WHERE Tables_in_c1_temptest IN ('i5tr4_users','i5tr4_usermeta') 
        $this->dbConnection();

        if (is_scalar($tables)) {
            $tables = array($tables);
        }
        $dbName = mysqli_real_escape_string($this->dbh, $this->dataConnection['dbname']);
        $dbh    = $this->dbh;

        $escapedTables = array_map(function ($table) use ($dbh) {
            return "'".mysqli_real_escape_string($dbh, $table)."'";
        }, $tables);

        $sql    = 'SHOW TABLES FROM `'.$dbName.'` WHERE `Tables_in_'.$dbName.'` IN ('.implode(',', $escapedTables).')';
        if (($result = DUPX_DB::mysqli_query($this->dbh, $sql)) === false) {
            return false;
        }

        return $result->num_rows === count($tables);
    }

    /**
     * 
     * @param type $newPrefix
     * @param type $options
     * @throws Exception
     */
    public function pregReplaceTableName($pattern, $replacement, $options = array())
    {
        $this->dbConnection();

        $options = array_merge(array(
            'exclude'              => array(), // exclude table list,
            'prefixFilter'         => false,
            'regexFilter'          => false, // filter tables with regexp
            'notRegexFilter'       => false, // filter tables with not regexp
            'regexTablesDropFkeys' => false
            ), $options);

        $escapedDbName = mysqli_real_escape_string($this->dbh, DUPX_Paramas_Manager::getInstance()->getValue(DUPX_Paramas_Manager::PARAM_DB_NAME));

        $tablesIn = 'Tables_in_'.$escapedDbName;

        $where = ' WHERE TRUE';

        if ($options['prefixFilter'] !== false) {
            $where .= ' AND `'.$tablesIn.'` NOT REGEXP "^'.mysqli_real_escape_string($this->dbh, preg_quote($options['prefixFilter'], null /* no delimiter */)).'.+"';
        }

        if ($options['regexFilter'] !== false) {
            $where .= ' AND `'.$tablesIn.'` REGEXP "'.mysqli_real_escape_string($this->dbh, preg_quote($options['regexFilter'], null /* no delimiter */)).'"';
        }

        if ($options['notRegexFilter'] !== false) {
            $where .= ' AND `'.$tablesIn.'` NOT REGEXP "'.mysqli_real_escape_string($this->dbh, preg_quote($options['notRegexFilter'], null /* no delimiter */)).'"';
        }

        if (($tablesList = DUPX_DB::queryColumnToArray($this->dbh, 'SHOW TABLES FROM `'.$escapedDbName.'`'.$where)) === false) {
            DUPX_Log::error('SQL ERROR:'.mysqli_error($this->dbh));
        }

        $this->rename_tbl_log = 0;

        if (count($tablesList) > 0) {
            DUPX_DB::mysqli_query($this->dbh, "SET FOREIGN_KEY_CHECKS = 0;");

            foreach ($tablesList as $table_name) {
                if (in_array($table_name, $options['exclude'])) {
                    continue;
                }

                $newTableName    = preg_replace($pattern, $replacement, $table_name);
                $escapedOldTable = mysqli_real_escape_string($this->dbh, $table_name);
                $ecapedNewTable  = mysqli_real_escape_string($this->dbh, substr($newTableName, 0, 64));

                if (!DUPX_DB::mysqli_query($this->dbh, "RENAME TABLE `".$escapedDbName."`.`".$escapedOldTable."` TO  `".$escapedDbName."`.`".$ecapedNewTable."`")) {
                    DUPX_Log::error(sprintf(ERR_DBTRYRENAME, "{$this->post['dbname']}.{$table_name}"));
                }

                $this->rename_tbl_log++;
            }

            if ($options['regexTablesDropFkeys'] !== false) {
                DUPX_Log::info('DROP FOREING KEYS');
                $this->dropForeignKeys($options['regexTablesDropFkeys']);
            }

            DUPX_DB::mysqli_query($this->dbh, "SET FOREIGN_KEY_CHECKS = 1;");
        }
    }

    /**
     * 
     * @param string $tableNamePatten
     * @return array
     */
    public function getForeinKeysData($tableNamePatten = false)
    {
        $this->dbConnection();

        //SELECT CONSTRAINT_NAME FROM information_schema.table_constraints WHERE `CONSTRAINT_TYPE` = 'FOREIGN KEY AND constraint_schema = 'temp_db_test_1234' AND `TABLE_NAME` = 'renamed''
        $escapedDbName = mysqli_real_escape_string($this->dbh, DUPX_Paramas_Manager::getInstance()->getValue(DUPX_Paramas_Manager::PARAM_DB_NAME));
        $escapePattenr = mysqli_real_escape_string($this->dbh, $tableNamePatten);

        $where = " WHERE `CONSTRAINT_TYPE` = 'FOREIGN KEY' AND constraint_schema = '".$escapedDbName."'";
        if ($tableNamePatten !== false) {
            $where .= " AND `TABLE_NAME` REGEXP '".$escapePattenr."'";
        }

        if (($result = DUPX_DB::mysqli_query($this->dbh, "SELECT TABLE_NAME as tableName, CONSTRAINT_NAME as fKeyName FROM information_schema.table_constraints ".$where)) === false) {
            DUPX_Log::error('SQL ERROR:'.mysqli_error($this->dbh));
        }


        return $result->fetch_all(MYSQLI_ASSOC);
    }

    /**
     * 
     * @param string $tableNamePatten
     * @return boolean
     */
    public function dropForeignKeys($tableNamePatten = false)
    {
        foreach ($this->getForeinKeysData($tableNamePatten) as $fKeyData) {
            $escapedTableName = mysqli_real_escape_string($this->dbh, $fKeyData['tableName']);
            $escapedFKeyName  = mysqli_real_escape_string($this->dbh, $fKeyData['fKeyName']);
            if (DUPX_DB::mysqli_query($this->dbh, 'ALTER TABLE `'.$escapedTableName.'` DROP CONSTRAINT `'.$escapedFKeyName.'`') === false) {
                DUPX_Log::error('SQL ERROR:'.mysqli_error($this->dbh));
            }
        }

        return true;
    }

    public function copyTable($existing_name, $new_name, $delete_if_conflict = false)
    {
        $this->dbConnection();
        return DUPX_DB::copyTable($this->dbh, $existing_name, $new_name, $delete_if_conflict);
    }

    public function renameTable($existing_name, $new_name, $delete_if_conflict = false)
    {
        $this->dbConnection();
        return DUPX_DB::renameTable($this->dbh, $existing_name, $new_name, $delete_if_conflict);
    }

    public function dropTable($name)
    {
        $this->dbConnection();
        return DUPX_DB::dropTable($this->dbh, $name);
    }

    /**
     * 
     * @param string $prefix
     * @return boolean
     */
    public function getAdminUsers($prefix)
    {
        $escapedPrefix = mysqli_real_escape_string($this->dbh, $prefix);
        $userTable     = mysqli_real_escape_string($this->dbh, $this->getUserTableName($prefix));
        $userMetaTable = mysqli_real_escape_string($this->dbh, $this->getUserMetaTableName($prefix));

        $sql         = 'SELECT `'.$userTable.'`.`id` AS id, `'.$userTable.'`.`user_login` AS user_login FROM `'.$userTable.'` '
            .'INNER JOIN `'.$userMetaTable.'` ON ( `'.$userTable.'`.`id` = `'.$userMetaTable.'`.`user_id` ) '
            .'WHERE `'.$userMetaTable.'`.`meta_key` = "'.$escapedPrefix.'capabilities" AND `'.$userMetaTable.'`.`meta_value` LIKE "%\"administrator\"%" '
            .'ORDER BY user_login ASC';
        if (($queryResult = DUPX_DB::mysqli_query($this->dbh, $sql)) === false) {
            return false;
        }

        $result = array();
        while ($row    = $queryResult->fetch_assoc()) {
            $result[] = $row;
        }
        return $result;
    }

    /**
     * 
     * @param int $userId
     * @param null|string $prefix
     * @return boolean
     */
    public function updatePostsAuthor($userId, $prefix = null)
    {
        $this->dbConnection();
        //UPDATE `i5tr4_posts` SET `post_author` = 7 WHERE TRUE
        $postsTable = mysqli_real_escape_string($this->dbh, $this->getPostsTableName($prefix));
        $sql        = 'UPDATE `'.$postsTable.'` SET `post_author` = '.((int) $userId).' WHERE TRUE';
        if (($result     = DUPX_DB::mysqli_query($this->dbh, $sql)) === false) {
            return false;
        }

        return true;
    }
}