<?php
namespace FastFrame;
/**
 * 
// {{{ requires

require_once dirname(__FILE__) . '/DataAccess.php';

// }}}
// {{{ \FastFrame\AbstractModel

/**
 * The \FastFrame\AbstractModel:: class is the abstract class to represent a model.  
 *
 * A model is a conception of a thing or item (such as a user) made into a class to
 * represent that thing.  A model is made persistent by using to DataAccess class to save or
 * edit it.  Likewise a new model is objtained through methods in the DataAccess class.
 *
 * @author  David Lundgren <dlundgren@syberisle.net>
 * @version SVN: $Id$ 
 * @access  public
 * @package FastFrame
 * @since   4.0 
 */

// }}}
// @todo move this to it's own file?
interface ModelInterface
{
    
/**
     * Initializes the data access object
     *
     * @access private
     * @return void
     */
    
function _initDataAccess();
}
abstract class 
AbstractModel implements \ArrayAccess
{
    
// {{{ properties

    /**
     * The data access object. 
     * @var object
     */
    
protected $o_dataAccess;

    
/**
     * The data for the model
     * @var array
     */
    
protected $_data = array();

    
/**
     * Whether or not the data has changed
     * @var boolean
     */
    
protected $is_modified false;

    
// }}}
    // {{{ functions: PHP

    
public function __construct()
    {
        
$this->_initDataAccess();
    }

    
/**
     * Generic function call to get/set the parameters.
     *
     * @access public
     * @param  string $in_method The name of the method being called.
     * @param  array  $in_params The parameters being passwd to the function.
     * @return mixed  On get it returns the value of the variable, on set it
     *                returns nothing.
     */   
    
public function __call($in_method$in_params)
    {
        
$s_method   substr($in_method03);
        
$s_property $this->_convertToUnderscore(substr($in_method3));
        if (
$s_method == 'get') {
            if (isset(
$this->_data[$s_property])) {
                return 
$this->_data[$s_property];
            }
            return isset(
$in_params[0]) ? $in_params[0] : null;
        }
        elseif (
$s_method == 'set') {
            return 
$this->_set($s_property, isset($in_params[0]) ? $in_params[0] : null);
        }
        elseif (
$s_method == 'has') {
            return isset(
$this->_data[$s_property]);
        }

        
/** XXX: we should throw an error? **/
        
return null;
    }

    
/**
     * Implementation of PHP's __get method
     *
     * @param  string $in_name
     * @return mixed  The value of the property or null
     */
    
public function __get($in_name)
    {
        
$s_var $this->_convertToUnderscore($in_name);
        return isset(
$this->_data[$s_var]) ? $this->_data[$s_var] : null;
    }

    
/**
     * Implementation of PHP's __set method
     *
     * @param  string $in_name
     * @param  mixed  $in_value
     *
     * @return object The current object.
     */
    
public function __set($in_name$in_value)
    {
        return 
$this->_set($in_name$in_value);
    }

    
/**
     * Implementation of PHP's __isset method
     *
     * @param  string  $in_name
     *
     * @return boolean
     */
    
public function __isset($in_name)
    {
        return isset(
$this->_data[$in_name]);
    }

    
/**
     * Implementation of PHP's __unset method
     *
     * @param string $in_name
     */
    
public function __unset($in_name)
    {
        
$this->is_modified true;
        unset(
$this->_data[$in_name]);
    }

    
/**
     * Implementation of PHP __toString method
     *
     * @return string The name of the class
     */
    
public function __toString()
    {
        return 
get_class($this);
    }

    
// }}}
    // {{{ functions: ArrayAccess implementations

    /**
     * Implementation of ArrayAccess::offsetSet()
     *
     * @param string $in_offset
     * @param mixed  $in_value
     */
    
public function offsetSet($in_offset$in_value)
    {
        if (
is_null($this->_data[$in_offset])) {
            
$this->_data[$in_offset] = $in_value;
        }
        else {
            
$this->_data[$in_offset] = $in_value;
        }
    }

    
/*
     * Implementation of ArrayAccess::offsetExists()
     *
     * @param  string  $in_offset
     * @return boolean
     */
    
public function offsetExists($in_offset)
    {
        return isset(
$this->_data[$in_offset]);
    }

    
/**
     * Implementation of ArrayAccess::offsetUnset()
     *
     * @param string $in_offset
     */
    
public function offsetUnset($in_offset)
    {
        unset(
$this->_data[$in_offset]);
    }

    
/**
     * Implementation of ArrayAccess::offsetGet()
     *
     * @param  string $in_offset
     * @return mixed  $in_offset value or null
     */
    
public function offsetGet($in_offset)
    {
        return isset(
$this->_data[$in_offset]) ? $this->_data[$in_offset] : null;
    }

    
// }}}
    // {{{ functions

    /**
     * Converts the specified string from CamelCase to underscores. It maintains
     * a cache of the names to reduce the need to call preg_replace each time
     *
     * @param  string $in_string
     * @return string The converted string
     */
    
protected function _convertToUnderscore($in_string)
    {
        static 
$a_cachedNames = array();

        if (isset(
$a_cachedNames[$in_string])) {
            return 
$a_cachedNames[$in_string];
        }

        
$s_name strtolower(preg_replace('/([A-Z])/''_\1'$in_string));
        if (
substr($s_name01) === '_') {
            
$a_cachedNames[$in_string] = substr($s_name1);
        }
        else {
            
$a_cachedNames[$in_string] = $s_name;
        }

        return 
$a_cachedNames[$in_string];
    }

    
/**
     * Sets the value of the property and whether or not it has changed.
     *
     * @param  string $in_name
     * @param  mixed  $in_value
     * @return object The current object
     */
    
protected function _set($in_name$in_value)
    {
        
$s_var $this->_convertToUnderscore($in_name);
        if ( ! isset(
$this->_data[$s_var]) || $this->_data[$s_var] !== $in_value) {
            
$this->is_modified true;
            
$this->_data[$s_var] = $in_value;
        }
        return 
$this;
    }

    
// }}}
    // {{{ functions: interface

    /**
     * Retets the model's data properties to their initial values (usually null)
     *
     * @access public
     * @return void
     */
    
public function reset()
    {
        
$this->_data = array();
        
$this->is_modified false;
    }

    
/**
     * Imports an array of data used the dataccess constants as keys into the model's
     * properties
     *
     * @param array $in_data The data array
     *
     * @access public
     * @return void
     */
    
public function importFromArray($in_data)
    {
        
$this->is_modified false;
        if (isset(
$this->_data['id'])) {
            
$this->is_modified true;
        }

        
$this->_data $in_data;
    }

    
/**
     * Exports the data properties to an array using the dataaccess constants as keys
     *
     * @param array $in_data The data array
     *
     * @access public
     * @return array The array of data 
     */
    
public function exportToArray()
    {
        return 
$this->_data;
    }

    
// }}}
    // {{{ functions

    /**
     * Returns whether there are data modifications
     *
     * @return boolean
     */
    
public function hasModifiedData()
    {
        return 
$this->is_modified;
    }

    
/**
     * Removes the current model or the one specified
     *
     * @param int|null $in_id (optional) The id to delete. If not specified then
     *                                   the current id is used
     *
     * @access public
     * @return boolean
     */
    
public function remove($in_id null)
    {
        
$s_id is_null($in_id) ? $this->getId() : $in_id;
        return 
$this->o_dataAccess->remove($s_id);
    }

    
/**
     * Saves the current model to the persistent layer
     *
     * @param bool $in_isUpdate Is this an update?  Otherwise we add.
     *
     * @access public
     * @return boolean
     */
    
public function save($in_isUpdate)
    {
        
$s_method 'update';
        if ( ! 
$in_isUpdate) {
            
$this->setId($this->o_dataAccess->getNextId());
            
$s_method 'add';
        }

        if ( ! 
$this->is_modified) {
                return 
true;
        }

        return 
$this->o_dataAccess->$s_method($this->exportToArray());
    }

    
/**
     * Fills the models data in based on the id field
     *
     * @param int $in_id The id field
     *
     * @access public
     * @return boolean True if fill succeeded, false otherwise
     */
    
public function fillById($in_id)
    {
        
$this->reset();
        
$a_data $this->o_dataAccess->getDataByPrimaryKey($in_id);
        if (
count($a_data) == || $a_data['id'] != $in_id) {
            return 
false;
        }
        else {
            
$this->importFromArray($a_data);
            return 
true;
        }
    }

    
/**
     * Gets the data access object
     *
     * @access public
     * @return object The data access object
     */
    
public function &getDataAccessObject()
    {
        return 
$this->o_dataAccess;
    }

    
/**
     * Gets the next id for when we are adding a user
     *
     * @access public
     * @return int The next available id
     */
    
public function getNextId()
    {
        return 
$this->o_dataAccess->getNextId();
    }

    
// }}}
}