<?php
/**
 * An abstract class for models.
 *
 * This class provides basic crud functions and a property-to-column mapper.
 */
abstract class model extends object
{
    /**
     * var array The modified properties.
     */
    protected $_modified;
    
    /**
     * var array The id property name.
     */
    protected static $_id = 'id';
    
    /**
     * var string The db connection name.
     */
    protected static $_db = 'default';
    
    /**
     * var string The table name.
     */
    protected static $_table = '';
    
    /**
     * var array The property-to-column map.
     */
    protected static $_map = array();
    
    ////////////////////////////////////////////////////////////////////////////
    // Construct and destruct functions.

    /**
     * @param array (optional) $property The properties.
     */
    public function __construct(array $property = array())
    {
        parent::__construct($property);
        $this->_modified = array();
    }

    ////////////////////////////////////////////////////////////////////////////
    // Overload functions.

    /**
     * Sets a property.
     * 
     * @param string $name The property name.
     * @param mixed (optional) $value The value.
     * @throws logicexception If the id property is attempted to be overwritten.
     */
    public function __set($name, $value)
    {
        if ($name === static::$_id && $this->$name !== null)
        {
            throw new logicexception('Id property cannot be overwritten.');
        }
        
        parent::__set($name, $value);
        $this->_modified[$name] = true;
    }
    
    /**
     * Transforms the model into an associative array.
     *
     * @return array The array.
     */
    public function __toarray()
    {
        $property = parent::__toarray();
        unset($property['_modified']);
        return $property;
    }
    
    ////////////////////////////////////////////////////////////////////////////
    // Interface functions.
    
    /**
     * Finds a model in the persistence layer.
     *
     * @param integer $id The model id.
     * @throws unexpectedvalueexception If the persistence layer fails to find
     * model.
     * @return model/boolean The model if it is found, else false.
     */
    public static function find($id)
    {
        try
        {
            $column = db::query(static::$_db)->select(static::$_table)
                                             ->where(static::get_column(static::$_id), $id)
                                             ->limit(1)
                                             ->execute()->fetch();
        }
        catch (pdoexception $e)
        {
            throw new unexpectedvalueexception('Persistence layer failed to find model (' . $e->getmessage() . ').', 0, $e);
        }
        
        if (!$column)
        {
            return false;
        }
        
        if (static::$_map)
        {
            $column = static::get_property($column);
        }
        
        return new get_called_class()($column);
    }
    
    /**
     * Saves a model to the persistence layer.
     *
     * @param model/array $model The model.
     * @return integer The model id.
     */
    public static function save($model)
    {
        $property = ($model instanceof model) ? $model->__toarray() : $model;
        
        if (array_key_exists(static::$_id, $property) && $property[static::$_id] === null)
        {
            unset($property[static::$_id]);
        }
        
        if (static::$_map)
        {
            $property = static::get_column($property);
        }
        
        try
        {
            $affected = count(db::query(static::$_db)->insert(static::$_table)
                                                     ->column($property)
                                                     ->execute());
            
            if (!$affected)
            {
                throw new unexpectedvalueexception('Persistence layer failed to save model.');
            }
            
            $id = db::get(static::$_db)->get_last_id();
        }
        catch (pdoexception $e)
        {
            throw new unexpectedvalueexception('Persistence layer failed to save model (' . $e->getmessage() . ').', 0, $e);
        }
        
        if ($model instanceof model)
        {
            if ($model->{static::$_id} === null)
            {
                $model->__set(static::$_id, $id);
            }
        }
        
        return $id;
    }
    
    /**
     * Updates a model in the persistence layer.
     *
     * @param model/array $model The model.
     */
    public static function update($model)
    {
        $property = ($model instanceof model) ? $model->__toarray() : $model;
        
        if (static::$_map)
        {
            $property = static::get_column($property);
        }
        
        try
        {
            $affected = count(db::query(static::$_db)->update(static::$_table)
                                                     ->column($property)
                                                     ->where(static::get_column(static::$_id), static::get_id($model))
                                                     ->limit(1)
                                                     ->execute());
            
            if (!$affected)
            {
                throw new unexpectedvalueexception('Persistence layer failed to update model.');
            }
        }
        catch (pdoexception $e)
        {
            throw new unexpectedvalueexception('Persistence layer failed to update model (' . $e->getmessage() . ').', 0, $e);
        }
    }
    
    /**
     * Deletes a model from the persistence layer.
     *
     * @param model/array/integer $model The model or model id.
     * @throws unexpectedvalueexception If the persistence layer fails to 
     * delete model.
     * @return boolean True if the model deleted successfully, else false.
     */
    public static function delete($model)
    {
        try
        {
            $affected = count(db::query(static::$_db)->delete(static::$_table)
                                                     ->where(static::get_column(static::$_id), static::get_id($model))
                                                     ->limit(1)
                                                     ->execute());
        }
        catch (pdoexception $e)
        {
            throw new unexpectedvalueexception('Persistence layer failed to delete model (' . $e->getmessage() . ').', 0, $e);
        }
        
        return (boolean) $affected;
    }
    
    ////////////////////////////////////////////////////////////////////////////
    // Internal functions.
    
    /**
     * Gets a property as a column.
     *
     * @param string/array $property The property.
     * @return string/array The column.
     */
    protected static function get_column($property)
    {
        if (is_array($property))
        {
            foreach (static::$_map as $p => $c)
            {
                if (array_key_exists($p, $property))
                {
                    $property[$c] = $property[$p];
                    unset($property[$p]);
                }
            }
        }
        else
        {
            if (array_key_exists($property, static::$_map))
            {
                $property = static::$_map[$property];
            }
        }
        
        return $property;
    }
    
    /**
     * Gets a column as a property.
     *
     * @param string/array $column The column.
     * @return string/array The property.
     */
    protected static function get_property($column)
    {
        if (is_array($column))
        {
            foreach (static::$_map as $p => $c)
            {
                if (array_key_exists($c, $column))
                {
                    $column[$p] = $column[$c];
                    unset($column[$c]);
                }
            }
        }
        else
        {
            $key = array_search($column, static::$_map);
            
            if ($key !== false)
            {
                $column = static::$_map[$key];
            }
        }
        
        return $column;
    }
    
    /**
     * Gets the model id value.
     *
     * @param model/array/integer $model The model.
     * @throws logicexception If the id property is not defined.
     * @return string/array The properties.
     */
    protected static function get_id($model)
    {
        if ($model instanceof model)
        {
            if ($model->{static::$_id} === null)
            {
                throw new logicexception('Id property is not defined.');
            }
            
            $model = $model->{static::$_id};
        }
        elseif (is_array($model))
        {
            if (!array_key_exists(static::$_id, $model) || $model[static::$_id] === null)
            {
                throw new logicexception('Id property is not defined');
            }
            
            $model = $model[static::$_id];
        }
        
        return $model;
    }
}
