<?php
/**
 *
 * PgDB - future unified database access interface,
 * currently only wrapping some global functions
 */

// temporary workaround until new configuration framework will be in place.
class PgDB {
    static private $_db = null;
    static private $_master_db = null;
    static private $_slave_db = null;
    // counts how many times transaction has been started
    static private $_transaction_depth = 0;
    // states if current transaction has been rolled back at some point
    static private $_transaction_failed = false;

    static private $_config = null;

    static private $_me = null;

    /**
     * Private constructor prevents multiple objects creation.
     * Guarantees singleton pattern usage of class.
     */
    private function __construct() {
    }

    /**
     * returns object providing access to master database PDO functions
     * \return object behaving like PDO Database object
     */
    static function db() {
        self::_needsMasterDB();
        if (self::$_me == null) {
            self::$_me = new PgDB();
        }
        return self::$_me;
    }


    /**
     * Mapping method - thanks to it PgDB object behaves like PDO
     *
     * @param funcname PDO function to be mapped
     * @param args array of PDO function arguments
     * @return PDO function response
     */
    public function __call($funcname, $args) {
        return call_user_func_array([self::$_db,$funcname],$args);
    }

    /**
     * Declares database dependency, establishes connection if needed.
     * Guarantees availablility of master connection.
     */
    static private function _needsMasterDB() {
        self::$_db = &self::$_master_db;
        if (self::$_db == null) {
            self::_masterConnect();
        }
    }

    static private function _getConfig($key, $default = '') {
        if (self::$_config === NULL) {
            self::$_config = Env::get('db');
        }
        if (isset(self::$_config[$key])) {
            return self::$_config[$key];
        }
        return $default;
    }

    /**
     * Establishes connection to master server
     * (currently there's no multimaster support available).
     */
	static private function _masterConnect() {
        self::$_master_db = new PDO('pgsql:host='.self::_getConfig('host').';port='.self::_getConfig('port', 5432).';dbname='.self::_getConfig('database').'', self::_getConfig('user'), self::_getConfig('password'));
        self::$_master_db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        self::$_db = &self::$_master_db;
	}

    /** returns value from first column of first row of query result.
     * accepts two types of variable substitution: '?' or ':var',
     * '?' expects variable parameter list, ':var' expects second argument
     * to be array working as dictionary
     * \param $query PgSQL query with optional parameter substitution
     * \param $params (optional) array or first parameter for '?' type query
     * \param $... - (optional) following parameters for '?' type query
     */
    public static function fetchValue($query, $params = []) {
        self::_needsMasterDB();
        $sth = self::$_db->prepare($query.';');
        if (!is_array($params)) {
            $params = array_slice(func_get_args(),1);
        }
        $sth->execute($params);
        $result = $sth->fetch(PDO::FETCH_NUM);
        return isset($result[0]) ? $result[0] : null;
    }

    /** returns first first row of query result as assiociative array.
     * accepts two types of variable substitution: '?' or ':var',
     * '?' expects variable parameter list, ':var' expects second argument
     * to be array working as dictionary
     * \param $query PgSQL query with optional parameter substitution
     * \param $params (optional) array or first parameter for '?' type query
     * \param $... - (optional) following parameters for '?' type query
     */
    public static function fetchRow($query, $params = []) {
        self::_needsMasterDB();
        $sth = self::$_db->prepare($query.';');
        if (!is_array($params)) {
            $params = array_slice(func_get_args(),1);
        }

        $sth->execute($params);
        return $sth->fetch(PDO::FETCH_ASSOC);
    }

    /** returns whole query result as numbered array of assiociative arrays.
     * accepts two types of variable substitution: '?' or ':var',
     * '?' expects variable parameter list, ':var' expects second argument
     * to be array working as dictionary
     * \param $query PgSQL query with optional parameter substitution
     * \param $params (optional) array or first parameter for '?' type query
     * \param $... - (optional) following parameters for '?' type query
     */
    static public function fetchAll($query, $params = []) {
        self::_needsMasterDB();
        $sth = self::$_db->prepare($query.';');
        if (!is_array($params)) {
            $params = array_slice(func_get_args(),1);
        }

        $sth->execute($params);
        return $sth->fetchAll(PDO::FETCH_ASSOC);
    }


    static public function execute($query, $params = []) {
        self::_needsMasterDB();
        $sth = self::$_db->prepare($query.';');
        if (!is_array($params)) {
            $params = array_slice(func_get_args(),1);
        }
        $sth->execute($params);
        return $sth->rowCount();

    }

    static public function insert($table, $values, $functions = []) {
        self::_needsMasterDB();
        list($query,$args) = self::_insert_prepare($table,$values,$functions);
        $sth = self::$_db->prepare($query.';');
        $sth->execute($args);
        return $sth->rowCount();
    }


    static public function update($table,$values,$whereclause,$functions="") {
        self::_needsMasterDB();
        $query = 'UPDATE "'. $table .'" SET ';
        $cnt=0;
        $where = "";
        $args = [];

        foreach ($values as $key => $val) {
            if ($cnt) {
                $query .=', ';
            }
            $query .= '"'. $key .'"=?';
            if (is_bool($val)) {
                $args[] = ($val) ? 'TRUE' : 'FALSE';
            }
            else {
                $args[] = $val;
            }
            $cnt++;
        }

        if ($functions!="") {
            foreach ($functions as $key => $val) {
                if ($cnt) {
                    $query .=', ';
                }
                $query .= '"'. $key .'"'. $val;
                $cnt++;
            }
        }

        if (empty($values)) {
            $query = 'SELECT * FROM '.$table;
        }

        if (is_array($whereclause)) {
            $cnt=0;
            foreach ($whereclause as $key => $val) {
                if ($cnt) {
                    $where .=' AND ';
                }
                $where .= '("'. $key .'"=?)';
                $args[] = $val;
                $cnt++;
            }
            $query .= " WHERE ".$where .";";
        }
        else {
            $query .= " WHERE ". $whereclause .";";
        }

        $sth = self::$_db->prepare($query.';');
        $sth->execute($args);
        return $sth->rowCount();
    }


    public static function set($table,$values,$wherelist,$functions = []) {
        $rowsAffected = self::update($table,$values,$wherelist,$functions);
        if ($rowsAffected == 0) {
            return self::insert($table,array_merge($values,$wherelist),$functions);
        }
        return $rowsAffcted;
    }


    public static function delete($table, $values, $extrarules="") {
        $query = 'DELETE FROM "'. $table .'" WHERE ';
        $cnt=0;
        $args = [];
        foreach ($values as $key => $val) {
            if ($cnt) {
                $query .= 'AND';
            }
            $query .= ' ("'. $key .'" = ?)';
            $args[] = $val;
            $cnt++;
        }

        if ($extrarules !== "") {
            $query .= ' AND '.$extrarules;
        }
        return self::execute($query,$args);
    }

    static private function _insert_prepare($table,$values,$functions = []) {
        $query = 'INSERT INTO "'. $table .'"(';
        $cnt=0;
        $valbuf = "";
        $args = [];
        foreach ($values as $key => $val) {
            if ($cnt) {
                $query .=',';
                $valbuf.=',';
            }
            $query .= '"'. $key .'"';
            $valbuf .= '?';

            if (is_bool($val)) {
                $args[] = ($val) ? 'TRUE' : 'FALSE';
            }
            elseif ($val===null) {
                $args[] = NULL;
            }
            else {
                $args[] = $val;
            }
            $cnt++;
        }

        foreach ($functions as $key => $val) {
            if ($cnt) {
                $query .=',';
                $valbuf .=',';
            }
            $query .= '"'. $key .'"';
            $valbuf .= $val;

            $cnt++;
        }

        $query .= ") VALUES (". $valbuf .");";

        return [$query, $args];
    }


    public static function begin() {
        self::_needsMasterDB();
        if (self::$_transaction_depth == 0) {
            self::$_db->beginTransaction();
            self::$_transaction_depth++;
        }
        elseif (self::$_transaction_failed) {
            throw new Exception("Trying to begin transaction inside of already stopped transaction!");
        }
        else {
            self::$_transaction_depth++;
        }
    }

    public static function commit() {
        if (self::$_transaction_depth>0) {
            if (self::$_transaction_failed) {
                // I'm not convinced that it's good idea,
                // but we need to be sure transaction won't go to database.
                self::rollback();
                throw new Exception("Transaction already stopped, couldn't be commited");
            }
            self::$_transaction_depth--;
            if (self::$_transaction_depth==0) {
                return self::$_db->commit();
            }
        }
        else {
            throw new Exception("To commit transaction it needs to be started first");
        }
    }

    public static function rollback() {
        self::$_transaction_failed = true;
        self::$_transaction_depth--;
        if (self::$_transaction_depth == 0) {
            self::$_db->rollback();
            self::$_transaction_failed = false;
        }
    }

    public static function inTransaction() {
        return (self::$_transaction_depth>0);
    }

    public static function transactionStateFailed() {
        return self::$_transaction_failed;
    }
}

?>