<?php
class Database extends Buyers
{
    private $mysqli;
    private $res;
    public $queries;
    /**
     * В конструкторе подключаем базу
     */
    public function __construct()
    {
        $this->connect();
    }

    /**
     * Подключение к базе данных
     */
    public function connect()
    {
        // При повторном вызове возвращаем существующий линк
        if(!empty($this->mysqli))
            return $this->mysqli;
        else
            $this->mysqli = new mysqli($this->config->db_server, $this->config->db_user, $this->config->db_password, $this->config->db_name);

        // Выводим сообщение, в случае ошибки
        if($this->mysqli->connect_error)
            throw new Exception("Could not connect to the database: ".$this->mysqli->connect_error);
        else
        {
            if($this->config->db_charset)
                $this->mysqli->query('SET NAMES '.$this->config->db_charset);
            if($this->config->db_sql_mode)
                $this->mysqli->query('SET SESSION SQL_MODE = "'.$this->config->db_sql_mode.'"');
        }
        return $this->mysqli;
    }

    /**
     * Запрос к базе. Обазятелен первый аргумент - текст запроса.
     * При указании других аргументов автоматически выполняется placehold() для запроса с подстановкой этих аргументов
     */
    public function query()
    {
        if(is_object($this->res))
            $this->res->free();

        $args = func_get_args();
        $q = call_user_func_array(array($this, 'placehold'), $args);

        if(!$this->res = $this->mysqli->query($q))
            exit($this->mysqli->error . $q);

        //запись запросов для дебага
        $this->queries[] = $q;

        return $this;
    }

    /**
     *  Экранирование
     */
    public function escape($str)
    {
        return $this->mysqli->real_escape_string($str);
    }


    /**
     * Плейсхолдер для запросов. Пример работы: $query = $db->placehold('SELECT name FROM products WHERE id=?', $id);
     */
    public function placehold($tmpl, ...$args)
    {
        if(!empty($args))
        {
            $result = $this->sql_placeholder_ex($tmpl, $args, $error);
            if ($result === false)
            {
                print "Placeholder substitution error. Diagnostics: \"$error\"";
                return false;
            }
            return $result;
        }
        else
            return $tmpl;
    }


    /**
     * Возвращает результаты запроса. Необязательный второй аргумент указывает какую колонку возвращать вместо всего массива колонок
     * Если в выборке есть id, то он будет являться ключом в массиве
     */
    public function results($field = null, $field2 = null)
    {
        $results = array();

        if(!$this->res)
            return false;

        if($this->res->num_rows == 0)
            return $results;

        while($row = $this->res->fetch_object())
        {
            //получить числовой массив со значением поля
            if(isset($row->$field) && empty($row->$field2))
                $results[] = $row->$field;
            //получить ассоциативный массив
            elseif(isset($row->$field) && isset($row->$field2))
                $results[$row->$field] = $row->$field2;
            //получить числовой массив объектов с ключами в виде ID
            elseif(isset($row->id))
                $results[$row->id] = $row;
            //получить числовой массив объектов
            else
                $results[] = $row;
        }
        return $results;
    }

    /**
     * Возвращает первый результат запроса. Необязательный второй аргумент указывает какую колонку возвращать вместо всего массива колонок
     */
    public function result($field = null)
    {
        $result = array();
        if(!$this->res)
            return 0;

        $row = $this->res->fetch_object();
        if(!empty($field) && isset($row->$field))
            return $row->$field;
        elseif(!empty($field) && !isset($row->$field))
            return false;
        else
            return $row;
    }

    /**
     * Возвращает последний вставленный id
     */
    public function insert_id()
    {
        return $this->mysqli->insert_id;
    }

    /**
     * Возвращает количество выбранных строк
     */
    public function num_rows()
    {
        return $this->res->num_rows;
    }

    /*
    * Возвращает количество затронутых строк
    */
    public function affected_rows()
    {
       return $this->mysqli->affected_rows;
    }

    /**
     * Компиляция плейсхолдера
     */
    private function sql_compile_placeholder($tmpl)
    {
        $compiled = array();
        $p = 0;       // текущая позиция в строке
        $i = 0;       // счетчик placeholder-ов
        while(false !== ($start = $p = strpos($tmpl, "?", $p)))
        {
            // Определяем тип placeholder-а.
            switch ($c = substr($tmpl, ++$p, 1))
            {
                case '%':   //объект
                case '@':   //массив
                case '~':   //значение для LIKE запросов
                    $type = $c; ++$p;
                break;
                default:    //переменная
                    $type = '';
                break;
            }

            $key = $i;
            $i++;
            // Сохранить запись о placeholder-е.
            $compiled[] = array($key, $type, $start, $p - $start);
        }
        return array($compiled, $tmpl);
    }

    /**
     * Выполнение плейсхолдера
     */
    private function sql_placeholder_ex($tmpl, $args, &$errormsg)
    {
        $compiled = $this->sql_compile_placeholder($tmpl);
        list ($compiled, $tmpl) = $compiled;

        // Выполняем все замены в цикле.
        $p   = 0;       // текущее положение в строке
        $out = '';      // результирующая строка
        $error = false; // были ошибки?

        foreach ($compiled as $num=>$e)
        {
            list ($key, $type, $start, $length) = $e;

            // Pre-string.
            $out .= substr($tmpl, $p, $start - $p);
            $p = $start + $length;

            $repl = '';        // текст для замены текущего placeholder-а
            $errmsg = ''; // сообщение об ошибке для этого placeholder-а
            do
            {
                // Обрабатываем ошибку.
                if (!isset($args[$key]))
                {
                    $error = $errmsg = "UNKNOWN_PLACEHOLDER_$key";
                    break;
                }

                // Вставляем значение в соответствии с типом placeholder-а.
                $a = $args[$key];

                if ($type === '')
                {
                    // Скалярный placeholder.
                    if (is_array($a) || is_object($a))
                    {
                        $error = $errmsg = "NOT_A_SCALAR_PLACEHOLDER_$key";
                        break;
                    }
                    $repl = is_int($a) || is_float($a) ? str_replace(',', '.', $a) : "'".addslashes($a)."'";
                    break;
                }
                elseif ($type === '~')
                {
                    // Скалярный плейсхолдер, значение для условия LIKE
                    if (is_array($a) || is_object($a))
                    {
                        $error = $errmsg = "NOT_A_SCALAR_PLACEHOLDER_$key";
                        break;
                    }
                    //escape % and _ too
                    $repl = addcslashes($this->escape($a), "%_");
                    $repl = "'%".$repl."%'";
                    break;
                }
                elseif ($type === '@')
                {
                    if (!is_array($a))
                    {
                        $error = $errmsg = "NOT_AN_ARRAY_PLACEHOLDER_$key";
                        break;
                    }
                    // Это список.
                    foreach ($a as $v)
                    {
                        if(is_null($v))
                            $r = "NULL";
                        else
                            $r = "'".@addslashes($v)."'";

                        $repl .= ($repl===''? "" : ",").$r;
                    }
                }
                elseif ($type === '%')
                {
                    // Это набор пар ключ=>значение.
                    $lerror = array();
                    foreach ($a as $k=>$v)
                    {
                        if (!is_string($k))
                            $lerror[$k] = "NOT_A_STRING_KEY_{$k}_FOR_PLACEHOLDER_$key";
                        else
                            $k = preg_replace('/[^a-zA-Z0-9_]/', '_', $k);

                        if(is_null($v))
                            $r = "=NULL";
                        else
                            $r = "='".@addslashes($v)."'";

                        $repl .= ($repl===''? "" : ", ").$k.$r;
                    }
                    // Если была ошибка, составляем сообщение.
                    if (count($lerror))
                    {
                        $repl = '';
                        foreach ($a as $k=>$v)
                        {
                            if (isset($lerror[$k]))
                            {
                                $repl .= ($repl===''? "" : ", ").$lerror[$k];
                            }
                            else
                            {
                                $k = preg_replace('/[^a-zA-Z0-9_-]/', '_', $k);
                                $repl .= ($repl===''? "" : ", ").$k."=?";
                            }
                        }
                        $error = $errmsg = $repl;
                    }
                }
            }
            while (false);

            if ($errmsg)
                $compiled[$num]['error'] = $errmsg;

            if (!$error)
                $out .= $repl;
        }
        $out .= substr($tmpl, $p);

        // Если возникла ошибка, переделываем результирующую строку
        // в сообщение об ошибке (расставляем диагностические строки
        // вместо ошибочных placeholder-ов).
        if ($error)
        {
            $out = '';
            $p     = 0; // текущая позиция
            foreach ($compiled as $num=>$e)
            {
                list ($key, $type, $start, $length) = $e;
                $out .= substr($tmpl, $p, $start - $p);
                $p = $start + $length;
                if (isset($e['error']))
                {
                    $out .= $e['error'];
                }
                else
                {
                    $out .= substr($tmpl, $start, $length);
                }
            }
            // Последняя часть строки.
            $out .= substr($tmpl, $p);
            $errormsg = $out;
            return false;
        }
        else
        {
            $errormsg = false;
            return $out;
        }
    }

    protected function get_database_tables()
    {
        $database_tables = array();
        $q = $this->placehold("SHOW FULL TABLES;");
        $result = $this->mysqli->query($q);
        while($row = $result->fetch_row())
        {
            if($row[1] == 'BASE TABLE')
                $database_tables[] = $row[0];
        }
        return $database_tables;
    }

    public function dump_database($filename)
    {
        $h = fopen($filename, 'w');
        fwrite($h, "SET FOREIGN_KEY_CHECKS=0;\n");

        $database_tables = $this->get_database_tables();

        foreach($database_tables AS $table)
            $this->dump_table($table, $h);

        fwrite($h, "SET FOREIGN_KEY_CHECKS=1;\n");
        fclose($h);
    }

    function restore($filename)
    {
        $database_tables = $this->get_database_tables();
        foreach($database_tables AS $table)
            $this->mysqli->query("DROP TABLE `$table`");

        $templine = '';
        $h = fopen($filename, 'r');

        // Loop through each line
        if($h)
        {
            while(!feof($h))
            {
                $line = fgets($h);
                // Only continue if it's not a comment
                if (substr($line, 0, 2) != '--' && $line != '')
                {
                    // Add this line to the current segment
                    $templine .= $line;
                    // If it has a semicolon at the end, it's the end of the query
                    if (substr(trim($line), -1, 1) == ';')
                    {
                        // Perform the query
                        $this->mysqli->query($templine) or print('Error performing query \'<b>'.$templine.'</b>\': '.$this->mysqli->error.'<br/><br/>');
                        // Reset temp variable to empty
                        $templine = '';
                    }
                }
            }
        }
        fclose($h);
        //удаляем файл после восстановления
        unlink($filename);
    }


    private function dump_table($table, $h)
    {
        $sql = "SHOW CREATE TABLE `$table`;";
        $result = $this->mysqli->query($sql);
        $row = $result->fetch_row();

        fwrite($h, "/* Structure for table $table */\n");
        fwrite($h, $row[1].";\n\n");

        $sql = "SELECT * FROM `$table`;";
        $result = $this->mysqli->query($sql);
        if($result)
        {
            fwrite($h, "/* Data for table $table */\n");

            $num_rows = $result->num_rows;
            $num_fields = $this->mysqli->field_count;

            if($num_rows > 0)
            {
                $field_type=array();
                $field_name = array();
                $meta = $result->fetch_fields();
                foreach($meta as $m)
                {
                    array_push($field_type, $m->type);
                    array_push($field_name, $m->name);
                }
                $fields = implode('`, `', $field_name);
                fwrite($h,  "INSERT INTO `$table` (`$fields`) VALUES\n");
                $index=0;
                while( $row = $result->fetch_row())
                {
                    fwrite($h, "(");
                    for( $i=0; $i < $num_fields; $i++)
                    {
                        if( is_null( $row[$i]))
                            fwrite($h, "null");
                        else
                        {
                            switch( $field_type[$i])
                            {
                                case 'int':
                                    fwrite($h,  $row[$i]);
                                    break;
                                case 'string':
                                case 'blob' :
                                default:
                                    fwrite($h, "'". $this->mysqli->real_escape_string($row[$i])."'");

                            }
                        }
                        if( $i < $num_fields-1)
                            fwrite($h,  ",");
                    }
                    fwrite($h, ")");

                    if( $index < $num_rows-1)
                        fwrite($h,  ",");
                    else
                        fwrite($h, ";");
                    fwrite($h, "\n");

                    $index++;
                }
            }
            $result->free();
        }
    }
}
