<?php
/**
 * @copyright NetMonsters <team@netmonsters.ru>
 * @link http://netmonsters.ru
 * @package Majestic
 * @subpackage db
 * @since 2010-02-19
 * @version SVN: $Id$
 * @filesource $URL$
 */

abstract class DbStatement
{
    
    /**
     * @var DbDriver
     */
    protected $driver;
    
    /**
     * @var string
     */
    protected $sql;
    
    protected $map = null;
    
    protected $params = array();
    
    protected $result;
    
    public function __construct($driver, $sql)
    {
        $this->driver = $driver;
        $this->sql    = $sql;
    }
    
    public function bindParam($param, &$value)
    {
        if ($this->map === null) {
            $this->mapPlaceholders();
        }
        if (count($this->map) > 0) {
            if (!is_string($param) && !is_int($param)) {
                throw new Exception('Placeholder must be an array or string');
            }
            if (is_object($value) && ! ($value instanceof DbExpr)) {
                throw new Exception('Objects excepts DbExpr not allowed.');
            }
            if (isset($this->map[$param])) {
                $this->params[$param] = &$value;
                return true;
            }
        }
        return false;
    }
    
    /**
     * @param array $params
     * @return bool
     */
    public function execute($params = null)
    {
        if (is_array($params)) {
            foreach ($params as $param => &$value) {
                $this->bindParam($param, $value);
            }
        }
        return $this->driverExecute($this->assemble());
    }
    
    protected function mapPlaceholders()
    {
        $matches = array();
        if(preg_match_all('/(\?|:[A-z0-9_]+)/u', $this->sql, $matches, PREG_OFFSET_CAPTURE)) {
            $noname = 0;
            foreach ($matches[0] as $id=>$match) {
                $match[2] = $matches[1][$id][0];                
                $name = ($match[2][0] === ':') ? ltrim($match[2], ':') : $noname++;
                $this->map[$name]['placeholder'] = $match[0];
                $this->map[$name]['offset'][]    = $match[1];
            }
        }
    }
    
    protected function assemble()
    {
        if (empty($this->map)) {
            return $this->sql;
        }
        
        $query = $this->sql;
        $placeholders = array();
        foreach($this->map as $name => $place) {
            $value = $this->driver->quote($this->params[$name]);
            foreach ($place['offset'] as $offset) {
                $placeholders[$offset] = array('placeholder' => $place['placeholder'], 'value' => $value);
            }
        }
        
        ksort($placeholders);
        
        $increment = 0;
        foreach($placeholders as $current_offset => $placeholder) {
            $offset = $current_offset + $increment;
            $length = mb_strlen($placeholder['placeholder']);
            $query = mb_substr($query, 0, $offset) . $placeholder['value'] . mb_substr($query, $offset + $length);
            $increment = (($increment - $length) + mb_strlen($placeholder['value']));
        }
        return $query;
    }
    
    public function __destruct()
    {
        $this->close();
    }

    /**
     * @param mixed $style
     * @return array
     */
    public function fetchAll($style = Db::FETCH_OBJ)
    {
        $data = array();
        while ($row = $this->fetch($style)) {
            $data[] = $row;
        }
        return $data;
    }

    /**
     * @param string $field
     */
    public function fetchColumn($field)
    {
        $data = array();
        while ($row = $this->fetch(Db::FETCH_ASSOC)) {
            $data[] = $row[$field];
        }
        return $data;
    }
    
    /**
     * @param string $field
     */
    public function fetchField($field)
    {
        $row = $this->fetch(Db::FETCH_ASSOC);
        if (isset($row[$field])) {
            return $row[$field];
        }
        return false;
    }
    
    /**
     * @return array
     */
    public function fetchPairs()
    {
        $data = array();
        while ($row = $this->fetch(Db::FETCH_NUM)) {
            $data[$row[0]] = $row[1];
        }
        return $data;
    }
    
    /* Abstract methods */
    
    abstract public function fetch($style = Db::FETCH_OBJ);
    
    abstract public function fetchObject($class = 'stdClass');
    
    abstract public function close();
    
    /**
     * @return int
     */
    abstract public function affectedRows();
    
    abstract public function numRows();
    
    /**
     * @return bool
     */
    abstract protected function driverExecute($sql);
}