<?php

namespace RedBeanPHP;

use RedBeanPHP\Adapter\DBAdapter as DBAdapter;
use RedBeanPHP\QueryWriter as QueryWriter;
use RedBeanPHP\BeanHelper as BeanHelper;
use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter;
use RedBeanPHP\Repository as Repository;
use RedBeanPHP\Repository\Fluid as FluidRepo;
use RedBeanPHP\Repository\Frozen as FrozenRepo;

/**
 * RedBean Object Oriented DataBase.
 *
 * The RedBean OODB Class is the main class of RedBeanPHP.
 * It takes OODBBean objects and stores them to and loads them from the
 * database as well as providing other CRUD functions. This class acts as a
 * object database.
 *
 * @file    RedBeanPHP/OODB.php
 * @author  Gabor de Mooij and the RedBeanPHP community
 * @license BSD/GPLv2
 *
 * @copyright
 * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community
 * This source file is subject to the BSD/GPLv2 License that is bundled
 * with this source code in the file license.txt.
 */
class OODB extends Observable
{
	/**
	 * @var array
	 */
	private static $sqlFilters = array();

	/**
	 * @var array
	 */
	protected $chillList = array();

	/**
	 * @var array
	 */
	protected $stash = NULL;

	/*
	 * @var integer
	 */
	protected $nesting = 0;

	/**
	 * @var QueryWriter
	 */
	protected $writer;

	/**
	 * @var boolean
	 */
	protected $isFrozen = FALSE;

	/**
	 * @var BeanHelper|NULL
	 */
	protected $beanhelper = NULL;

	/**
	 * @var AssociationManager|NULL
	 */
	protected $assocManager = NULL;

	/**
	 * @var Repository
	 */
	protected $repository = NULL;

	/**
	 * @var FrozenRepo|NULL
	 */
	protected $frozenRepository = NULL;

	/**
	 * @var FluidRepo|NULL
	 */
	protected $fluidRepository = NULL;

	/**
	 * @var boolean
	 */
	protected static $autoClearHistoryAfterStore = FALSE;

	/**
	 * If set to TRUE, this method will call clearHistory every time
	 * the bean gets stored.
	 *
	 * @param boolean $autoClear auto clear option
	 *
	 * @return void
	 */
	public static function autoClearHistoryAfterStore( $autoClear = TRUE )
	{
		self::$autoClearHistoryAfterStore = (boolean) $autoClear;
	}

	/**
	 * Unboxes a bean from a FUSE model if needed and checks whether the bean is
	 * an instance of OODBBean.
	 *
	 * @param OODBBean|SimpleModel $bean bean you wish to unbox
	 *
	 * @return OODBBean
	 */
	protected function unboxIfNeeded( $bean )
	{
		if ( $bean instanceof SimpleModel ) {
			$bean = $bean->unbox();
		}
		if ( !( $bean instanceof OODBBean ) ) {
			throw new RedException( 'OODB Store requires a bean, got: ' . gettype( $bean ) );
		}

		return $bean;
	}

	/**
	 * Constructor, requires a query writer.
	 * Most of the time, you do not need to use this constructor,
	 * since the facade takes care of constructing and wiring the
	 * RedBeanPHP core objects. However if you would like to
	 * assemble an OODB instance yourself, this is how it works:
	 *
	 * Usage:
	 *
	 * <code>
	 * $database = new RPDO( $dsn, $user, $pass );
	 * $adapter = new DBAdapter( $database );
	 * $writer = new PostgresWriter( $adapter );
	 * $oodb = new OODB( $writer, FALSE );
	 * $bean = $oodb->dispense( 'bean' );
	 * $bean->name = 'coffeeBean';
	 * $id = $oodb->store( $bean );
	 * $bean = $oodb->load( 'bean', $id );
	 * </code>
	 *
	 * The example above creates the 3 RedBeanPHP core objects:
	 * the Adapter, the Query Writer and the OODB instance and
	 * wires them together. The example also demonstrates some of
	 * the methods that can be used with OODB, as you see, they
	 * closely resemble their facade counterparts.
	 *
	 * The wiring process: create an RPDO instance using your database
	 * connection parameters. Create a database adapter from the RPDO
	 * object and pass that to the constructor of the writer. Next,
	 * create an OODB instance from the writer. Now you have an OODB
	 * object.
	 *
	 * @param QueryWriter   $writer writer
	 * @param array|boolean $frozen mode of operation: TRUE (frozen), FALSE (default, fluid) or ARRAY (chilled)
	 */
	public function __construct( QueryWriter $writer, $frozen = FALSE )
	{
		if ( $writer instanceof QueryWriter ) {
			$this->writer = $writer;
		}

		$this->freeze( $frozen );
	}

	/**
	 * Toggles fluid or frozen mode. In fluid mode the database
	 * structure is adjusted to accommodate your objects. In frozen mode
	 * this is not the case.
	 *
	 * You can also pass an array containing a selection of frozen types.
	 * Let's call this chill mode, it's just like fluid mode except that
	 * certain types (i.e. tables) aren't touched.
	 *
	 * @param boolean|array $toggle TRUE if you want to use OODB instance in frozen mode
	 *
	 * @return void
	 */
	public function freeze( $toggle )
	{
		if ( is_array( $toggle ) ) {
			$this->chillList = $toggle;
			$this->isFrozen  = FALSE;
		} else {
			$this->isFrozen = (boolean) $toggle;
		}

		if ( $this->isFrozen ) {
			if ( !$this->frozenRepository ) {
				$this->frozenRepository = new FrozenRepo( $this, $this->writer );
			}

			$this->repository = $this->frozenRepository;

		} else {
			if ( !$this->fluidRepository ) {
				$this->fluidRepository = new FluidRepo( $this, $this->writer );
			}

			$this->repository = $this->fluidRepository;
		}

		if ( count( self::$sqlFilters ) ) {
			AQueryWriter::setSQLFilters( self::$sqlFilters, ( !$this->isFrozen ) );
		}

	}

	/**
	 * Returns the current mode of operation of RedBean.
	 * In fluid mode the database
	 * structure is adjusted to accommodate your objects.
	 * In frozen mode
	 * this is not the case.
	 *
	 * @return boolean
	 */
	public function isFrozen()
	{
		return (bool) $this->isFrozen;
	}

	/**
	 * Determines whether a type is in the chill list.
	 * If a type is 'chilled' it's frozen, so its schema cannot be
	 * changed anymore. However other bean types may still be modified.
	 * This method is a convenience method for other objects to check if
	 * the schema of a certain type is locked for modification.
	 *
	 * @param string $type the type you wish to check
	 *
	 * @return boolean
	 */
	public function isChilled( $type )
	{
		return (boolean) ( in_array( $type, $this->chillList ) );
	}

	/**
	 * Dispenses a new bean (a OODBBean Bean Object)
	 * of the specified type. Always
	 * use this function to get an empty bean object. Never
	 * instantiate a OODBBean yourself because it needs
	 * to be configured before you can use it with RedBean. This
	 * function applies the appropriate initialization /
	 * configuration for you.
	 *
	 * @param string  $type              type of bean you want to dispense
	 * @param string  $number            number of beans you would like to get
	 * @param boolean $alwaysReturnArray if TRUE always returns the result as an array
	 *
	 * @return OODBBean|OODBBean[]
	 */
	public function dispense( $type, $number = 1, $alwaysReturnArray = FALSE )
	{
		if ( $number < 1 ) {
			if ( $alwaysReturnArray ) return array();
			return NULL;
		}

		return $this->repository->dispense( $type, $number, $alwaysReturnArray );
	}

	/**
	 * Sets bean helper to be given to beans.
	 * Bean helpers assist beans in getting a reference to a toolbox.
	 *
	 * @param BeanHelper $beanhelper helper
	 *
	 * @return void
	 */
	public function setBeanHelper( BeanHelper $beanhelper )
	{
		$this->beanhelper = $beanhelper;
	}

	/**
	 * Returns the current bean helper.
	 * Bean helpers assist beans in getting a reference to a toolbox.
	 *
	 * @return BeanHelper|NULL
	 */
	public function getBeanHelper()
	{
		return $this->beanhelper;
	}

	/**
	 * Checks whether a OODBBean bean is valid.
	 * If the type is not valid or the ID is not valid it will
	 * throw an exception: Security.
	 *
	 * @param OODBBean $bean the bean that needs to be checked
	 *
	 * @return void
	 */
	public function check( OODBBean $bean )
	{
		$this->repository->check( $bean );
	}

	/**
	 * Searches the database for a bean that matches conditions $conditions and sql $sql
	 * and returns an array containing all the beans that have been found.
	 *
	 * Conditions need to take form:
	 *
	 * <code>
	 * array(
	 *    'PROPERTY' => array( POSSIBLE VALUES... 'John', 'Steve' )
	 *    'PROPERTY' => array( POSSIBLE VALUES... )
	 * );
	 * </code>
	 *
	 * All conditions are glued together using the AND-operator, while all value lists
	 * are glued using IN-operators thus acting as OR-conditions.
	 *
	 * Note that you can use property names; the columns will be extracted using the
	 * appropriate bean formatter.
	 *
	 * @param string      $type       type of beans you are looking for
	 * @param array       $conditions list of conditions
	 * @param string|NULL $sql        SQL to be used in query
	 * @param array       $bindings   a list of values to bind to query parameters
	 *
	 * @return array
	 */
	public function find( $type, $conditions = array(), $sql = NULL, $bindings = array() )
	{
		return $this->repository->find( $type, $conditions, $sql, $bindings );
	}

	/**
	 * Same as find() but returns a BeanCollection.
	 *
	 * @param string      $type     type of beans you are looking for
	 * @param string|NULL $sql      SQL to be used in query
	 * @param array       $bindings a list of values to bind to query parameters
	 *
	 * @return BeanCollection
	 */
	public function findCollection(  $type, $sql = NULL, $bindings = array() )
	{
		return $this->repository->findCollection( $type, $sql, $bindings );
	}

	/**
	 * Checks whether the specified table already exists in the database.
	 *
	 * @param string $table table name
	 *
	 * @return boolean
	 */
	public function tableExists( $table )
	{
		return $this->repository->tableExists( $table );
	}

	/**
	 * Stores a bean in the database. This method takes a
	 * OODBBean Bean Object $bean and stores it
	 * in the database. If the database schema is not compatible
	 * with this bean and RedBean runs in fluid mode the schema
	 * will be altered to store the bean correctly.
	 * If the database schema is not compatible with this bean and
	 * RedBean runs in frozen mode it will throw an exception.
	 * This function returns the primary key ID of the inserted
	 * bean.
	 *
	 * The return value is an integer if possible. If it is not possible to
	 * represent the value as an integer a string will be returned. We use
	 * explicit casts instead of functions to preserve performance
	 * (0.13 vs 0.28 for 10000 iterations on Core i3).
	 *
	 * @param OODBBean|SimpleModel $bean bean to store
	 *
	 * @return integer|string
	 */
	public function store( $bean )
	{
		$bean = $this->unboxIfNeeded( $bean );
		$id = $this->repository->store( $bean );
		if ( self::$autoClearHistoryAfterStore ) {
				$bean->clearHistory();
		}
		return $id;
	}

	/**
	 * Loads a bean from the object database.
	 * It searches for a OODBBean Bean Object in the
	 * database. It does not matter how this bean has been stored.
	 * RedBean uses the primary key ID $id and the string $type
	 * to find the bean. The $type specifies what kind of bean you
	 * are looking for; this is the same type as used with the
	 * dispense() function. If RedBean finds the bean it will return
	 * the OODB Bean object; if it cannot find the bean
	 * RedBean will return a new bean of type $type and with
	 * primary key ID 0. In the latter case it acts basically the
	 * same as dispense().
	 *
	 * Important note:
	 * If the bean cannot be found in the database a new bean of
	 * the specified type will be generated and returned.
	 *
	 * @param string  $type type of bean you want to load
	 * @param integer $id   ID of the bean you want to load
	 *
	 * @return OODBBean
	 */
	public function load( $type, $id )
	{
		return $this->repository->load( $type, $id );
	}

	/**
	 * Removes a bean from the database.
	 * This function will remove the specified OODBBean
	 * Bean Object from the database.
	 *
	 * @param OODBBean|SimpleModel $bean bean you want to remove from database
	 *
	 * @return int
	 */
	public function trash( $bean )
	{
		$bean = $this->unboxIfNeeded( $bean );
		return $this->repository->trash( $bean );
	}

	/**
	 * Returns an array of beans. Pass a type and a series of ids and
	 * this method will bring you the corresponding beans.
	 *
	 * important note: Because this method loads beans using the load()
	 * function (but faster) it will return empty beans with ID 0 for
	 * every bean that could not be located. The resulting beans will have the
	 * passed IDs as their keys.
	 *
	 * @param string $type type of beans
	 * @param array  $ids  ids to load
	 *
	 * @return array
	 */
	public function batch( $type, $ids )
	{
		return $this->repository->batch( $type, $ids );
	}

	/**
	 * This is a convenience method; it converts database rows
	 * (arrays) into beans. Given a type and a set of rows this method
	 * will return an array of beans of the specified type loaded with
	 * the data fields provided by the result set from the database.
	 *
	 * @param string $type type of beans you would like to have
	 * @param array  $rows rows from the database result
	 * @param string $mask mask to apply for meta data
	 *
	 * @return array
	 */
	public function convertToBeans( $type, $rows, $mask = NULL )
	{
		return $this->repository->convertToBeans( $type, $rows, $mask );
	}

	/**
	 * Counts the number of beans of type $type.
	 * This method accepts a second argument to modify the count-query.
	 * A third argument can be used to provide bindings for the SQL snippet.
	 *
	 * @param string $type     type of bean we are looking for
	 * @param string $addSQL   additional SQL snippet
	 * @param array  $bindings parameters to bind to SQL
	 *
	 * @return integer
	 */
	public function count( $type, $addSQL = '', $bindings = array() )
	{
		return $this->repository->count( $type, $addSQL, $bindings );
	}

	/**
	 * Trash all beans of a given type. Wipes an entire type of bean.
	 *
	 * @param string $type type of bean you wish to delete all instances of
	 *
	 * @return boolean
	 */
	public function wipe( $type )
	{
		return $this->repository->wipe( $type );
	}

	/**
	 * Returns an Association Manager for use with OODB.
	 * A simple getter function to obtain a reference to the association manager used for
	 * storage and more.
	 *
	 * @return AssociationManager
	 */
	public function getAssociationManager()
	{
		if ( !isset( $this->assocManager ) ) {
			throw new RedException( 'No association manager available.' );
		}

		return $this->assocManager;
	}

	/**
	 * Sets the association manager instance to be used by this OODB.
	 * A simple setter function to set the association manager to be used for storage and
	 * more.
	 *
	 * @param AssociationManager $assocManager sets the association manager to be used
	 *
	 * @return void
	 */
	public function setAssociationManager( AssociationManager $assocManager )
	{
		$this->assocManager = $assocManager;
	}

	/**
	 * Returns the currently used repository instance.
	 * For testing purposes only.
	 *
	 * @return Repository
	 */
	public function getCurrentRepository()
	{
		return $this->repository;
	}

	/**
	 * Clears all function bindings.
	 *
	 * @return void
	 */
	public function clearAllFuncBindings()
	{
		self::$sqlFilters = array();
		AQueryWriter::setSQLFilters( self::$sqlFilters, FALSE );
	}

	/**
	 * Binds an SQL function to a column.
	 * This method can be used to setup a decode/encode scheme or
	 * perform UUID insertion. This method is especially useful for handling
	 * MySQL spatial columns, because they need to be processed first using
	 * the asText/GeomFromText functions.
	 *
	 * @param string       $mode       mode to set function for, i.e. read or write
	 * @param string       $field      field (table.column) to bind SQL function to
	 * @param string|NULL  $function   SQL function to bind to field
	 * @param boolean      $isTemplate TRUE if $function is an SQL string, FALSE for just a function name
	 *
	 * @return void
	 */
	public function bindFunc( $mode, $field, $function, $isTemplate = FALSE )
	{
		list( $type, $property ) = explode( '.', $field );
		$mode = ($mode === 'write') ? QueryWriter::C_SQLFILTER_WRITE : QueryWriter::C_SQLFILTER_READ;

		if ( !isset( self::$sqlFilters[$mode] ) ) self::$sqlFilters[$mode] = array();
		if ( !isset( self::$sqlFilters[$mode][$type] ) ) self::$sqlFilters[$mode][$type] = array();

		if ( is_null( $function ) ) {
			unset( self::$sqlFilters[$mode][$type][$property] );
		} else {
			if ($mode === QueryWriter::C_SQLFILTER_WRITE) {
				if ($isTemplate) {
					$code = sprintf( $function, '?' );
				} else {
					$code = "{$function}(?)";
				}
				self::$sqlFilters[$mode][$type][$property] = $code;
			} else {
				if ($isTemplate) {
					$code = sprintf( $function, $field );
				} else {
					$code = "{$function}({$field})";
				}
				self::$sqlFilters[$mode][$type][$property] = $code;
			}
		}
		AQueryWriter::setSQLFilters( self::$sqlFilters, ( !$this->isFrozen ) );
	}
}
