<?php
/*
 *  $Id$
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.phpdoctrine.com>.
 */
/**
 * Doctrine_Db
 * A thin wrapper layer on top of PDO / Doctrine_Adapter
 *
 * Doctrine_Db provides the following things to underlying database hanlder
 *
 * 1. Event listeners
 *    An easy to use, pluggable eventlistener architecture. Aspects such as
 *    logging, query profiling and caching can be easily implemented through
 *    the use of these listeners
 *
 * 2. Lazy-connecting
 *    Creating an instance of Doctrine_Db does not connect
 *    to database. Connecting to database is only invoked when actually needed
 *    (for example when query() is being called)
 *
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @package     Doctrine
 * @category    Object Relational Mapping
 * @link        www.phpdoctrine.com
 * @since       1.0
 * @version     $Revision$
 */
class Doctrine_Db implements Countable, IteratorAggregate, Doctrine_Adapter_Interface
{
    /**
     * @var array $instances            all the instances of this class
     */
    protected static $instances   = array();
    /**
     * @var array $isConnected          whether or not a connection has been established
     */
    protected $isConnected        = false;
    /**
     * @var PDO $dbh                    the database handler
     */
    protected $dbh;
    /**
     * @var array $options
     */
    protected $options            = array('dsn'      => null,
                                          'username' => null,
                                          'password' => null,
                                          );
    /**
     * @var array $pendingAttributes    An array of pending attributes. When setting attributes
     *                                  no connection is needed. When connected all the pending
     *                                  attributes are passed to the underlying PDO instance.
     */
    protected $pendingAttributes  = array();
    /**
     * @var Doctrine_Db_EventListener_Interface|Doctrine_Overloadable $listener
     *                              listener for listening events
     */
    protected $listener;
    /**
     * @var string $name                name of this connection
     * @see Doctrine_Manager::openConnection()
     */
    protected $name;
    
    protected $count = 0;


    private static $driverMap = array('oracle'     => 'oci8',
                                      'postgres'   => 'pgsql',
                                      'oci'        => 'oci8',
                                      'sqlite2'    => 'sqlite',
                                      'sqlite3'    => 'sqlite');

    /**
     * constructor
     *
     * @param string $dsn           data source name
     * @param string $user          database username
     * @param string $pass          database password
     */
    public function __construct($dsn, $user = null, $pass = null)
    {
    	// check if dsn is PEAR-like or not
        if ( ! isset($user) || strpos($dsn, '://')) {
            $a = self::parseDSN($dsn);

            extract($a);
        } else {
            $e = explode(':', $dsn);

            if($e[0] == 'uri') {
                $e[0] = 'odbc';
            }

            $this->pendingAttributes[Doctrine::ATTR_DRIVER_NAME] = $e[0];

        }
        $this->options['dsn']      = $dsn;
        $this->options['username'] = $user;
        $this->options['password'] = $pass;
        $this->listener = new Doctrine_Db_EventListener();
    }
    
    public function incrementQueryCount() 
    {
        $this->count++;
    }
    /**
     * getDbh
     */
    public function getDbh()
    {
        return $this->dbh;
    }
    /**
     * getAdapter
     *
     * @return Doctrine_Adapter_Interface|PDO $adapter
     */
    public function getAdapter()
    {
        return $this->dbh;
    }
    /**
     * setAdapter
     *
     * @param Doctrine_Adapter_Interface|PDO $adapter
     * @return void
     */
    public function setAdapter($adapter)
    {
        $this->dbh = $adapter;
    }
    /**
     * setName
     * this method should only be used by doctrine internally and
     * also for testing purposes
     *
     * @param string $name      connection name
     * @return void
     */
    public function setName($name)
    {
        $this->name = $name;
    }
    /**
     * getName
     *
     * @return string           the name of the associated connection
     */
    public function getName()
    {
        return $this->name;
    }
    public function getOption($name)
    {
        if ( ! array_key_exists($name, $this->options)) {
            throw new Doctrine_Db_Exception('Unknown option ' . $name);
        }
        return $this->options[$name];
    }
    /**
     * addListener
     *
     * @param Doctrine_Db_EventListener_Interface|Doctrine_Overloadable $listener
     * @return Doctrine_Db
     */
    public function addListener($listener, $name = null)
    {
        if ( ! ($this->listener instanceof Doctrine_Db_EventListener_Chain)) {
            $this->listener = new Doctrine_Db_EventListener_Chain();
        }
        $this->listener->add($listener, $name);

        return $this;
    }
    /**
     * getListener
     *
     * @return Doctrine_Db_EventListener_Interface|Doctrine_Overloadable
     */
    public function getListener()
    {
        return $this->listener;
    }
    /**
     * setListener
     *
     * @param Doctrine_Db_EventListener_Interface|Doctrine_Overloadable $listener
     * @return Doctrine_Db
     */
    public function setListener($listener)
    {
        if ( ! ($listener instanceof Doctrine_Db_EventListener_Interface)
            && ! ($listener instanceof Doctrine_Overloadable)
        ) {
            throw new Doctrine_Db_Exception("Couldn't set eventlistener for database handler. EventListeners should implement either Doctrine_Db_EventListener_Interface or Doctrine_Overloadable");
        }
        $this->listener = $listener;

        return $this;
    }

    /**
     * connect
     * connects into database
     *
     * @return boolean
     */
    public function connect()
    {
        if ($this->isConnected)
            return false;

        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::CONNECT);

        $this->listener->onPreConnect($event);

        $e     = explode(':', $this->options['dsn']);
        $found = false;
        
        if (extension_loaded('pdo')) {
            if (in_array($e[0], PDO::getAvailableDrivers())) {
                $this->dbh = new PDO($this->options['dsn'], $this->options['username'], $this->options['password']);
                $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                //$this->dbh->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('Doctrine_Db_Statement', array($this)));
                $found = true;
            }
        }

        if ( ! $found) {
            $class = 'Doctrine_Adapter_' . ucwords($e[0]);

            if (class_exists($class)) {
                $this->dbh = new $class($this->options['dsn'], $this->options['username'], $this->options['password']);
            } else {
                throw new Doctrine_Db_Exception("Couldn't locate driver named " . $e[0]);      	
            }
        }

        
        foreach($this->pendingAttributes as $attr => $value) {
            // some drivers don't support setting this so we just skip it
            if($attr == Doctrine::ATTR_DRIVER_NAME) {
                continue;
            }
            $this->dbh->setAttribute($attr, $value);
        }

        $this->isConnected = true;

        $this->listener->onConnect($event);
        return true;
    }

    /**
     * getConnection
     *
     * @param string $dsn               PEAR::DB like DSN or PDO like DSN
     * format for PEAR::DB like DSN:    schema://user:password@address/dbname
     *
     * @return
     */
    public static function getConnection($dsn = null, $username = null, $password = null)
    {
        return new self($dsn, $username, $password);
    }
    /**
     * driverName
     * converts a driver name like (oracle) to appropriate PDO
     * driver name (oci8 in the case of oracle)
     *
     * @param string $name
     * @return string
     */
    public static function driverName($name)
    {
        if (isset(self::$driverMap[$name])) {
            return self::$driverMap[$name];
        }
        return $name;
    }
    /**
     * parseDSN
     *
     * @param string $dsn
     * @return array Parsed contents of DSN
     */
    public function parseDSN($dsn)
    {
        // silence any warnings
        $parts = @parse_url($dsn);

        $names = array('scheme', 'host', 'port', 'user', 'pass', 'path', 'query', 'fragment');

        foreach ($names as $name) {
            if ( ! isset($parts[$name])) {
                $parts[$name] = null;
            }
        }

        if (count($parts) == 0 || ! isset($parts['scheme'])) {
            throw new Doctrine_Db_Exception('Empty data source name');
        }
        $drivers = self::getAvailableDrivers();

        $parts['scheme'] = self::driverName($parts['scheme']);
        /**
        if ( ! in_array($parts['scheme'], $drivers)) {
            throw new Doctrine_Db_Exception('Driver '.$parts['scheme'].' not availible or extension not loaded');
        }
        */
        switch ($parts['scheme']) {
            case 'sqlite':
                if (isset($parts['host']) && $parts['host'] == ':memory') {
                    $parts['database'] = ':memory:';
                    $parts['dsn']      = 'sqlite::memory:';
                }

                break;
            case 'mysql':
            case 'informix':
            case 'oci8':
            case 'mssql':
            case 'firebird':
            case 'dblib':
            case 'pgsql':
            case 'odbc':
            case 'mock':
                if ( ! isset($parts['path']) || $parts['path'] == '/') {
                    throw new Doctrine_Db_Exception('No database availible in data source name');
                }
                if (isset($parts['path'])) {
                    $parts['database'] = substr($parts['path'], 1);
                }
                if ( ! isset($parts['host'])) {
                    throw new Doctrine_Db_Exception('No hostname set in data source name');
                }
                $parts['dsn'] = $parts["scheme"].":host=".$parts["host"].";dbname=".$parts["database"];
                
                if (isset($parts['port'])) {
                    // append port to dsn if supplied
                    $parts['dsn'] .= ';port=' . $parts['port'];
                }
                break;
            default:
                throw new Doctrine_Db_Exception('Unknown driver '.$parts['scheme']);
        }
        $this->pendingAttributes[PDO::ATTR_DRIVER_NAME] = $parts['scheme'];

        return $parts;
    }
    /**
     * clear
     * clears all instances from the memory
     *
     * @return void
     */
    public static function clear()
    {
        self::$instances = array();
    }

    /**
     * errorCode
     * Fetch the SQLSTATE associated with the last operation on the database handle
     *
     * @return integer
     */
    public function errorCode()
    {
        return $this->dbh->errorCode();
    }
    /**
     * errorInfo
     * Fetch extended error information associated with the last operation on the database handle
     *
     * @return array
     */
    public function errorInfo()
    {
        return $this->dbh->errorInfo();
    }
    /**
     * prepare
     *
     * @param string $statement
     */
    public function prepare($statement)
    {
        $this->connect();

        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::PREPARE, $statement);

        $this->listener->onPrePrepare($event);

        $stmt = $this->dbh->prepare($statement);

        $this->listener->onPrepare($event);

        return new Doctrine_Db_Statement($this, $stmt);
    }
    /**
     * query
     *
     * @param string $statement
     * @param array $params
     * @return Doctrine_Db_Statement|boolean
     */
    public function query($statement, array $params = array()) {
        $this->connect();

        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::QUERY, $statement);

        $skip  = $this->listener->onPreQuery($event);

        if ( ! empty($params)) {
            $stmt = $this->dbh->prepare($statement);

            return $stmt->execute($params);

        } else {
            if ( ! $skip) {
                $stmt = $this->dbh->query($statement);
                $this->count++;
            } else {
                $stmt = new stdClass;
                $stmt->queryString = $statement;
            }
            $stmt = new Doctrine_Db_Statement($this, $stmt);
        }

        $this->listener->onQuery($event);

        return $stmt;
    }
    /**
     * quote
     * quotes a string for use in a query
     *
     * @param string $input
     * @return string
     */
    public function quote($input)
    {
        $this->connect();

        return $this->dbh->quote($input);
    }
    /**
     * exec
     * executes an SQL statement and returns the number of affected rows
     *
     * @param string $statement
     * @return integer
     */
    public function exec($statement)
    {
        $this->connect();

        $args = func_get_args();

        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::EXEC, $statement);

        $this->listener->onPreExec($event);

        $rows = $this->dbh->exec($statement);

        $this->count++;

        $this->listener->onExec($event);

        return $rows;
    }

    /**
     * lastInsertId
     *
     * @return integer
     */
    public function lastInsertId()
    {
        $this->connect();

        return $this->dbh->lastInsertId();
    }
    /**
     * begins a transaction
     *
     * @return boolean
     */
    public function beginTransaction()
    {
        $this->connect();

        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::BEGIN);

        $this->listener->onPreBeginTransaction($event);

        $return = $this->dbh->beginTransaction();

        $this->listener->onBeginTransaction($event);

        return $return;
    }
    /**
     * commits a transaction
     *
     * @return boolean
     */
    public function commit()
    {
        $this->connect();

        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::COMMIT);

        $this->listener->onPreCommit($event);

        $return = $this->dbh->commit();

        $this->listener->onCommit($event);

        return $return;
    }
    /**
     * rollBack
     *
     * @return boolean
     */
    public function rollBack()
    {
        $this->connect();

        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::ROLLBACK);

        $this->listener->onPreRollback($event);

        $this->dbh->rollBack();

        $this->listener->onRollback($event);
    }
    /**
     * getAttribute
     * retrieves a database connection attribute
     *
     * @param integer $attribute
     * @return mixed
     */
    public function getAttribute($attribute)
    {
        if ($this->isConnected) {
            return $this->dbh->getAttribute($attribute);
        } else {
            if ( ! isset($this->pendingAttributes[$attribute])) {
                throw new Doctrine_Db_Exception('Attribute ' . $attribute . ' not found.');
            }
            
            return $this->pendingAttributes[$attribute];
        }
    }
    /**
     * returns an array of available PDO drivers
     */
    public static function getAvailableDrivers()
    {
        return PDO::getAvailableDrivers();
    }
    /**
     * setAttribute
     * sets an attribute
     *
     * @param integer $attribute
     * @param mixed $value
     * @return boolean
     */
    public function setAttribute($attribute, $value)
    {
        if ($this->isConnected) {
            $this->dbh->setAttribute($attribute, $value);
        } else {
            $this->pendingAttributes[$attribute] = $value;
        }
    }
    /**
     * getIterator
     *
     * @return ArrayIterator
     */
    public function getIterator()
    {
        if ($this->listener instanceof Doctrine_Db_Profiler) {
            return $this->listener;
        }
    }
    /**
     * count
     * returns the number of executed queries
     *
     * @return integer
     */
    public function count()
    {
        return $this->count;
    }
}
