<?php

require_once 'common/Object.php';
require_once 'common/collection/HashMap.php';
require_once 'common/collection/ArrayList.php';
require_once 'common/bean/Bean.php';
require_once 'common/bean/BeanDynamic.php';
require_once 'common/bean/BeanUtils.php';

require_once 'orm/OrmIDao.php';
require_once 'orm/OrmBase.php';
require_once 'orm/OrmException.php';
require_once 'orm/OrmMap.php';
require_once 'orm/OrmMapReader.php';
require_once 'orm/OrmSql.php';

require_once 'orm/lazy/OrmLazyBean.php';
require_once 'orm/lazy/OrmLazyAttribute.php';

/**
 * Main Orm class.
 * 
 * This class is the core of the Orm module.
 * To have a full understand you should see @docs.
 * 
 * Core action methods are in two groups:<dl> 
 * <dt>Select actions</dt>
 * <dd>Methods to collect data: <em>get</em>, <em>load</em>, <em>search</em>, 
 * and <em>getList</em></dd>
 * <dt>Update actions</dt>
 * <dd>Methods to modify data: <em>save</em>, <em>insert</em>, 
 * <em>update</em>, and <em>remove</em>. This methods, based on bean's map 
 * definition, can "propagate" this actions to other beans. All this methods 
 * return the bean object. They are ready to handle database transactions, if 
 * they are supported by the <em>OrmIDb</em> implementation.</dd>
 * </dl>
 * 
 * <strong>Usage example:</strong><br />
 * You should have created a mapping file to some entity in your existent 
 * database. As an example, supose it's file name is book.map.xml
 * <code>
 * // add Orm folder to include_path in php.ini, or do: (changing first path!)
 * set_include_path('/path/to/cebolla/' . PATH_SEPARATOR . get_include_path());
 * // include this class
 * require_once 'orm/OrmCore.php';
 * // create an instance of this class (changing connection data!)
 * $orm = new OrmCore('book.map.xml', 'db_type://user:pass@host/db_name');
 * // and just use it!
 * $book = $orm->get(1);
 * $book->setTitle('new title');
 * $orm->update($book);
 * </code>
 * 
 * @abstract
 * @author  $Author: pruna $
 * @package orm
 * @version $Revision: 1.3 $
 * @created 19-Oct-2005
 * @modified $Date: 2005/12/06 02:56:45 $
 */
class OrmCore extends OrmBase implements OrmIDao {
    
    /**
     * Internal holder for <em>OrmMapReader</em> object for Bean.
     *
     * @var      OrmMapReader
     * @access   protected
     */
    protected $mapReader;
    
    /**
     * Constructor. 
     * 
     * First parameter is the <em>OrmMap</em> to be used, a string to create
     * it, or null. Second parameter is for database connection, and could be 
     * a <em>OrmIDb</em>, <em>OrmBase</em>, <em>Dsn</em> object or string, or 
     * null.
     *
     * @access  public
     * @param   mixed    $file    string or <em>OrmMap</em>
     * @param   mixed    $mixed   OrmIDb, OrmBase, Dsn object or string, or null
     */
    public function __construct($file = null, $mixed = null) {
        parent::__construct();
        $this->setMap($file);
        $this->bu = new BeanUtils();
        $this->processMixedConstructorParam($mixed);
    }
    
    /**
     * This method checks if a Bean has a valid value in the attribute that is 
     * persisted as it's primary key.
     * 
     * @access  protected
     * @param   Bean        $bean      Bean to check
     * @return  boolean
     */
    protected function beanHasDefinedPK($bean) {
        return (!$this->isNullValue($this->getBu()->getter($bean, 
                $this->getMap()->getId()->getName())));
    }
    
    /**
     * Returns the number of rows matching criteria.
     * 
     * The search is performed based on map definition, and performing joins 
     * only. This query uses <em>count(*)</em> instead of the other columns, 
     * so the query won't work when you need them.
     * 
     * @access    public
     * @param     OrmSqlSelect    $sql       SQL object to build
     * @return    int
     */
    public function count($sql = null) {
        $map = $this->getMap();
        
        // 1. Prepares basic SQL
        require_once 'orm/worker/OrmWorkerSelect.php';
        $worker = new OrmWorkerSelect($this);
        $sql = $worker->count($map, $sql);
        
        // 2. Completes specific search
        $sql->addToSelect('count(*) AS n');
        
        // 3. Executes
        return $this->getDb()->getValue($sql);
    }
    
    /**
     * Returns a Bean, based on PK lookup.
     * 
     * The bean is searched, and fully populated after lookup. (Of course, 
     * "fully" depends on <em>lazy</em> definitions).
     * This method throws an <em>OrmException</em> if the query returns no 
     * results, or more than one, because a primary-key based lookup shouldn't 
     * fail in fetching expected data.
     * 
     * @access    public
     * @param     mixed           $id    PK value to look for
     * @throws    OrmException    If not found
     * @see       load()
     * @return    object
     */
    public function get($id) {
        // 1. Gets populated bean(s)
        $ret = $this->getByPk($id);
        
        // 2. Returns ok or throws Exception
        if (is_array($ret)) {
            if (sizeof($ret) === 1) {
                return $ret[0];
            } elseif (sizeof($ret) > 1) {
                throw new OrmException("Multiple results for id $id in bean " . 
                        $this->getMap()->getBeanName(), 
                        OrmException::CODE_MULTIPLE_ROWS_FOR_ID, $this);
            }
        }
        throw new OrmException("Doesn't exists id $id in table " . 
                $this->getMap()->getBeanName(), 
                OrmException::CODE_UNEXISTANT_ID, $this);
    }
    
    /**
     * Returns a Bean, based on PK lookup.
     * 
     * The bean is searched, and fully populated after lookup. (Of course, 
     * "fully" depends on <em>lazy</em> definitions).
     * This method does the actual lookup for <em>get</em> and <em>load</em>.
     * 
     * @see       get(), load()
     * @access    protected
     * @param     mixed           $id    PK value to look for
     * @return    array
     */
    protected function getByPk($id) {
        $map = $this->getMap();
        
        // 1. Prepares basic SQL
        $sql = $this->prepareSelect($map);
        
        // 2. Completes specific search
        $idField = $map->getId();
        $sql->addToWhere($map->getTableAlias() . '.' 
            . $idField->getColumn() . ' = ?');
        
        // 3. Executes
        $res = $this->getDb()->select($sql, array($id));
        
        // 4. Creates Bean
        $bean = $this->createBean($map);
        
        // 5. Populates Bean and return
        return $this->getDb()->traverse($this, 'populateBean', $res, 
            array($this->getMap(), $bean));
    }
    
    /**
     * Returns stored <em>OrmMapReader</em> object.
     *
     * @access  public
     * @return  OrmMapReader
     */
    public function getMapReader() {
        return $this->mapReader;
    }

    /**
     * Returns an array of Beans.
     * 
     * This method can be used in 3 different ways:
     * <ol>
     * <li>Without parameters. In this case, it returns a list of all Beans in 
     * the database for this object.</li>
     * <li>With a OrmSql as the first parameter. In this case, it returns a 
     * list of Beans based on this criteria. This way is very similar to 
     * <em>search</em> method, but doesn't accept an extra parameter with 
     * values. In this case, second parameter will be silently ignored.</li>
     * <li>With one or two numerical parameters. In this case, this parameters 
     * will be used as boundarys to limit the resulting array.</li>
     * </ol>
     * Note that to be really useful, you should use an OrmSql, because there 
     * you can specify an <em>order</em> condition, and then be sure that the 
     * limits works properly.
     * 
     * @access   public
     * @param    mixed   $start      OrmSql as base query, or start index.
     * @param    int     $quantity   Quantity to return. Only if $start is int.
     * @return   array
     */
    public function getList($start = null, $quantity = null) {
        // 1. Prepares basic SQL
        $sql = $this->prepareSelect($this->getMap());
        
        // 2. Completes specific search
        if (ObjectInspector::isA($start, 'OrmSql')) {
            $this->getBu()->mergeCollectionValues($start, $sql);
        } else {
            if (!is_null($start)) {
                $sql->addToLimit($start);
                if (!is_null($quantity)) {
                    $sql->addToLimit($quantity);
                }
            }
        }
        
        // 3. Executes
        $res = $this->getDb()->select($sql, null);
        //$res = $this->getDb()->select($sql, $vars);
        
        // 4. Traverse results
        return $this->getDb()->traverse($this, 'populateBean', $res, 
                array($this->getMap()));
    }
    
    /**
     * Insert action.
     * 
     * Core update action to insert beans. This method could propagate this 
     * action across relations, according to map definition.
     * If you try to insert a Bean with a defined PK, it will silently ignore 
     * it, perform a new insertanyway, and return the resulting object.
     * If you want to do a update when PK is defined, you should use 
     * <em>save</em> instead. 
     * 
     * @access  public
     * @param   Bean         $bean      Bean where propagate action
     * @return  $bean
     */
    public function insert($bean) {
        require_once 'orm/worker/OrmWorkerSave.php';
        $worker = new OrmWorkerSave($this);
        
        // begin transaction. if not supported, keep on silently
        $transactionToken = $this->getDb()->beginTransaction();
        try {
            $addIdToColumns = FALSE;
            // calculates id - if previous (OrmIDb::getCalculatedPK())
            if (($id = $this->getDb()->getCalculatedPK($bean, $this)) != null) {
                $this->getBu()->setter($bean, 
                        $this->getMap()->getId()->getName(), $id);
                $addIdToColumns = TRUE;
            }
            
            // entity, m2o y o2o
            $this->getDb()->insert($worker->prepareSave($this->getMap(), $bean, 
                    'insert', $this->getDb()->createSqlInsert(), '', $addIdToColumns));
            
            // obtains id - if later (OrmIDb::getCreatedPK())
            if (!$this->beanHasDefinedPK($bean)) {
                if (($id = $this->getDb()->getCreatedPK($bean, $this)) != null) {
                    $this->getBu()->setter($bean, 
                            $this->getMap()->getId()->getName(), $id);
                } else {
                    // if neither getCalculatedPK nor beanHasDefinedPK 
                    // could get an id
                    throw new OrmException("Can't get a valid id for new Bean", 
                            OrmException::CODE_ID_GENERATION_ERROR, $this);
                }
            }
            
            // o2m y m2m
            $worker->saveRelations($this->getMap(), $bean, 'insert');
            
            // close transaction
            $this->getDb()->commit($transactionToken);
        } catch (OrmException $e) {
            // it founds problems, rollback transaction and re-throw Exception
            $this->getDb()->rollback($transactionToken);
            throw $e;
        }
        
        // returns $bean instead of $id, so save() returns allways $bean also.
        // this way, three methods have equivalent signature.
        return $bean;
    }
    
    /**
     * Returns a Bean, based on PK lookup.
     * 
     * The bean is searched, and fully populated after lookup. (Of course, 
     * "fully" depends on <em>lazy</em> definitions).
     * If the query returns no rows, method returns null; if returns more than 
     * one row, it will use the first (in any order), and silently discard the 
     * others.
     * 
     * @access    public
     * @param     mixed           $id    PK value to look for
     * @throws    OrmException    If not found
     * @see       get()
     * @return    object
     */
    public function load($id) {
        // 1. Gets populated bean(s)
        $ret = $this->getByPk($id);
        
        // 2. Returns ok or null
        if (is_array($ret) && sizeof($ret) >= 1) {
            return $ret[0];
        }
        return null;
    }
    
    /**
     * This method populates a Bean from an array of values returned from a 
     * database query. It creates a new Bean, if third parameter is not used.
     * 
     * <em>Note:</em> this method isn't defined as protected only because it's 
     * accedes from outside by the <em>OrmIDb</em>'s callback method.
     * 
     * @access  protected
     * @param   array     $values    Values to be stored in bean
     * @param   OrmMap    $map       Map to use
     * @param   Bean      $bean      Bean to populate, or null for a new one
     * @return  $bean
     */
    function populateBean($values, $map, $bean = null) {
        require_once 'orm/worker/OrmWorkerPopulate.php';
        $worker = new OrmWorkerPopulate($this);
        return $worker->populateBean($map, $values, $bean);
    }
    
    /**
     * This method prepares a Select to be performed, based on the Map.
     * 
     * @access  protected
     * @param   OrmMap        $map    Map to use
     * @param   OrmSqlSelect  $sql    Sql to use, or null for a new one
     * @return  OrmSqlSelect
     */
    protected function prepareSelect($map, $sql = null) {
        require_once 'orm/worker/OrmWorkerSelect.php';
        $worker = new OrmWorkerSelect($this);
        return $worker->select($map, $sql);
    }
    
    /**
     * Process second constructor parameter.
     * This method try to get a <em>OrmIDb</em> from this parameter. 
     * Options are:<dl>
     *   <dt>OrmIDb</dt>
     *     <dd>Use it, and store its dsn</dd>
     *   <dt>OrmBase</dt>
     *     <dd>Use its <em>OrmIDb</em>, and store its dsn</dd>
     *   <dt>Dsn</dt>
     *     <dd>Store it, and create a new <em>OrmIDb</em> using it</dd>
     *   <dt>string</dt>
     *     <dd>Treat it as a dsn string</dd>
     *   <dt>null</dt>
     *     <dd>Do nothing</dd>
     * </dl>
     * 
     * @access  public
     * @param   mixed   $mixed  OrmIDb, OrmBase, Dsn object or string, or null
     * @return  void
     */
    protected function processMixedConstructorParam($mixed) {
        if (is_null($mixed)) {
            return;
        } elseif (is_string($mixed)) {
            $this->setDsn($mixed);
        } elseif (is_object($mixed)) {
            if (ObjectInspector::isA($mixed, 'Dsn')) {
                $this->setDsn($mixed);
            } elseif (ObjectInspector::isA($mixed, 'OrmBase')) {
                $this->db = $mixed->getDb();
            } else {
                $this->db = $mixed;
            }
        }
    }
    
    /**
     * Removes a bean.
     * 
     * Core update action to remove beans. This method could propagate this 
     * action across relations, according to map definition.
     * @todo remove cascade in o2o, and removeIfEmpty cascade in m2o and m2m 
     * are not implemented yet!
     * 
     * @access  public
     * @param   Bean           $bean          Bean to save
     * @return  $bean
     */
    public function remove($bean) {
        require_once 'orm/worker/OrmWorkerRemove.php';
        $worker = new OrmWorkerRemove($this);
        
        // begin transaction. if not supported, keep on silently
        $transactionToken = $this->getDb()->beginTransaction();
        
        try {
            $ret = $worker->remove($bean);
        
            // close transaction
            $this->getDb()->commit($transactionToken);
        } catch (OrmException $e) {
            // it founds problems, rollback transaction and re-throw Exception
            $this->getDb()->rollback($transactionToken);
            throw $e;
        }
        
        return $ret;
    }
    
    /**
     * Save action.
     * 
     * Core update action to save beans. This method could propagate this 
     * action across relations, according to map definition.
     * Calls insert or update, depending on PK existence. Insert will be 
     * called if PK is null or is interpreted in PHP as false. Update will 
     * be called otherwise.
     * 
     * @access  public
     * @param   Bean         $bean      Bean where propagate action
     * @return  $bean
     */
    public function save($bean) {
        if ($this->beanHasDefinedPK($bean)) {
            return $this->update($bean);
        } else {
            return $this->insert($bean);
        }
    }
    
    /**
     * Returns an array of Beans, based on search criteria.
     * This method looks for all beans that meets the $search condition. This 
     * condition could be expresed either as an OrmSql object, or as a simple 
     * string expressing an SQL condition. In either case, a basic knowledge 
     * of the data available as the result of the mapping process is needed 
     * to ensure that the resulting query is valid, since this method doesn't 
     * perform any validation.
     * If you use values for the query, you can pass them as the second 
     * parameter, so they will be used through a prepared statement, with 
     * proper escaping.
     * If you need to limit the results, you should pass a OrmSql as the first 
     * parameter, with the limit in it.
     * 
     * @access   public
     * @param    mixed     $search    OrmSql as base query, or a condition
     * @param    mixed     $vars      Vars to use in query
     * @return   array
     */
    public function search($search, $vars = null) {
        // 1. Prepares basic SQL
        $sql = $this->prepareSelect($this->getMap());
        
        // 2. Completes specific search
        if (ObjectInspector::isA($search, 'OrmSql')) {
            $this->getBu()->mergeCollectionValues($search, $sql);
        } else {
            $sql->addToWhere($search);
        }
        
        // 3. Executes
        $res = $this->getDb()->select($sql, $vars);
        
        // 4. Traverse results
        return $this->getDb()->traverse($this, 'populateBean', $res, 
                array($this->getMap()));
    }
    
    /**
     * Stores <em>OrmMapReader</em> to be used.
     *
     * @access  public
     * @param   mixed  $mapReader  MapReader to use for bean operations
     */
    public function setMapReader($mapReader) {
        $this->mapReader = $mapReader;
    }
    
    /**
     * Update action.
     * 
     * Core update action to update beans. This method could propagate this 
     * action across relations, according to map definition.
     *
     * @access  public
     * @param   Bean         $bean      Bean where propagate action
     * @return  $bean
     */
    public function update($bean) {
        require_once 'orm/worker/OrmWorkerSave.php';
        $worker = new OrmWorkerSave($this);
        
        // begin transaction. if not supported, keep on silently
        $transactionToken = $this->getDb()->beginTransaction();
        
        try {
            // m2o, o2o and props
            $sql = $worker->prepareSave($this->getMap(), $bean, 'update', 
                $this->getDb()->createSqlUpdate(), ' = ?');
            
            // id
            $attr = $this->getMap()->getId();
            $sql->addToWhere($this->getMap()->getTable() . '.' 
                    . $attr->getColumn() . ' = ?');
            $sql->addToValues($this->getBu()->getter($bean, $attr->getName()));
            
            $this->getDb()->update($sql);
            
            // o2m y m2m
            $worker->saveRelations($this->getMap(), $bean, 'update');
        
            // close transaction
            $this->getDb()->commit($transactionToken);
        } catch (OrmException $e) {
            // it founds problems, rollback transaction and re-throw Exception
            $this->getDb()->rollback($transactionToken);
            throw $e;
        }
        
        // returns $bean instead of success, or affected_something, or diff, so 
        // save() returns allways $bean also.
        // this way, three methods have equivalent signature.
        return $bean;
    }
    
}

?>