<?php
    /**
     * Class: Query
     * Handles a SQL query.
     */
    class Query {
        # Variable: $query
        # Holds the prepared query.
        public $query;

        # Boolean: $result
        # The result of execution.
        public $result;

        # Variable: $queryString
        # Holds the query statement.
        public $queryString = "";

        # Variable: $sql
        # Holds the current <SQL> instance.
        private $sql;

        # Array: $params
        # Holds the query parameters.
        private $params = array();

        # Boolean: $throw_exceptions
        # Throw exceptions instead of calling error()?
        private $throw_exceptions = false;

        /**
         * Function: __construct
         * Creates a query based on the <SQL.interface>.
         *
         * Parameters:
         *     $sql - <SQL> instance.
         *     $query - Query to execute.
         *     $params - An associative array of parameters used in the query.
         *     $throw_exceptions - Throw exceptions instead of calling error()?
         */
        public function __construct($sql, $query, $params = array(), $throw_exceptions = false) {
            $this->sql = $sql;

            # Don't count config setting queries.
            $count = !preg_match("/^SET /", strtoupper($query));

            if ($count)
                ++$this->sql->queries;

            $this->params = $params;
            $this->throw_exceptions = $throw_exceptions;
            $this->queryString = $query;

            if ($count and DEBUG) {
                $trace = debug_backtrace();
                $target = $trace[$index = 0];

                # Getting a trace from these files doesn't help much.
                while (
                    match_any(
                        array("/SQL\.php/", "/Model\.php/", "/\/model\//"),
                        $target["file"]
                    )
                ) {
                    if (isset($trace[$index + 1]["file"]))
                        $target = $trace[$index++];
                    else
                        break;
                }

                $logQuery = $query;

                foreach ($params as $name => $val)
                    $logQuery = preg_replace(
                        "/{$name}([^a-zA-Z0-9_]|$)/",
                        str_replace(
                            "\\",
                            "\\\\",
                            $this->sql->escape($val, !is_int($val))
                        )."$1",
                        $logQuery
                    );

                $this->sql->debug[] = array(
                    "number" => $this->sql->queries,
                    "file" => str_replace(MAIN_DIR."/", "", $target["file"]),
                    "line" => $target["line"],
                    "query" => $logQuery,
                    "time" => timer_stop()
                );
            }

            try {
                $this->query = $this->sql->db->prepare($query);
                $this->result = $this->query->execute($params);
                $this->queryString = $query;

                foreach ($params as $name => $val)
                    $this->queryString = preg_replace(
                        "/{$name}([^a-zA-Z0-9_]|$)/",
                        str_replace(
                            array("\\", "\$"),
                            array("\\\\", "\\\$"),
                            $this->sql->escape($val, !is_int($val))
                        )."$1",
                        $this->queryString
                    );

                if (!$this->result)
                    throw new PDOException(
                        __("PDO failed to execute the prepared statement.")
                    );

            } catch (PDOException $e) {
                $this->exception_handler($e);
            }
        }

        /**
         * Function: fetchColumn
         * Fetches a column of the current row.
         *
         * Parameters:
         *     $column - The offset of the column to grab. Default 0.
         */
        public function fetchColumn($column = 0): mixed {
            return $this->query->fetchColumn($column);
        }

        /**
         * Function: fetch
         * Returns the current row as an array.
         */
        public function fetch($mode = PDO::FETCH_ASSOC): mixed { # Can be PDO::FETCH_DEFAULT in PHP 8.0.7+
            return $this->query->fetch($mode);
        }

        /**
         * Function: fetchObject
         * Returns the current row as an object.
         */
        public function fetchObject(): object|false {
            return $this->query->fetchObject();
        }

        /**
         * Function: fetchAll
         * Returns an array of every result.
         */
        public function fetchAll($mode = PDO::FETCH_ASSOC): array { # Can be PDO::FETCH_DEFAULT in PHP 8.0.7+
            return $this->query->fetchAll($mode);
        }

        /**
         * Function: grab
         * Grabs all of the given column out of the full result of a query.
         *
         * Parameters:
         *     $column - Name of the column to grab.
         *
         * Returns:
         *     An array of all of the values of that column in the result.
         */
         public function grab($column): array {
            $all = $this->fetchAll();
            $result = array();

            foreach ($all as $row)
                $result[] = $row[$column];

            return $result;
         }

        /**
         * Function: exception_handler
         * Handles exceptions thrown by failed queries.
         */
        public function exception_handler($e): void {
            $this->sql->error = $e->getMessage();

            # Trigger an error if throws were not requested.
            if (!$this->throw_exceptions) {
                $message = (DEBUG) ?
                    fix($this->sql->error, false, true).
                    "\n".
                    "\n".
                    "<h2>".__("Query String")."</h2>".
                    "\n".
                    "<pre>".
                    fix(print_r($this->queryString, true), false, true).
                    "</pre>".
                    "\n".
                    "\n".
                    "<h2>".__("Parameters")."</h2>".
                    "\n".
                    "<pre>".
                    fix(print_r($this->params, true), false, true).
                    "</pre>"
                    :
                    fix($this->sql->error, false, true)
                    ;

                trigger_error(
                    _f("Database error: %s", $message),
                    E_USER_WARNING
                );
            }

            # Otherwise we chain the exception.
            throw new RuntimeException($this->sql->error, $e->getCode(), $e);
        }
    }