<?php
/**
 * @package   awf
 * @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU GPL version 3 or later
 */

namespace Awf\Mvc;

use Awf\Application\Application;
use Awf\Container\Container;
use Awf\Database\Driver;
use Awf\Database\Query;
use Awf\Date\Date;
use Awf\Event\Dispatcher as EventDispatcher;
use Awf\Inflector\Inflector;
use Awf\Mvc\DataModel\Collection as DataCollection;
use Awf\Mvc\DataModel\Collection;
use Awf\Mvc\DataModel\Exception\InvalidSearchMethod;
use Awf\Mvc\DataModel\Exception\NoTableColumns;
use Awf\Mvc\DataModel\Exception\RecordNotLoaded;
use Awf\Mvc\DataModel\Exception\SpecialColumnMissing;
use Awf\Mvc\DataModel\RelationManager;
use Awf\Text\Text;

/**
 * Data-aware model, implementing a convenient ORM
 *
 * Type hinting -- start
 *
 * @method DataModel hasOne() hasOne(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null)
 * @method DataModel belongsTo() belongsTo(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null)
 * @method DataModel hasMany() hasMany(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null)
 * @method DataModel belongsToMany() belongsToMany(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null, string $pivotTable = null, string $pivotLocalKey = null, string $pivotForeignKey = null)
 *
 * Type hinting -- end
 *
 * Based on ideas and code from FOF, Joomla! Platform, Joomla! Framework and Laravel 4
 *
 * @package Awf\Mvc
 */
class DataModel extends Model
{
	/** @var   array  A list of tables in the database */
	protected static $tableCache = [];

	/** @var   array  A list of table fields, keyed per table */
	protected static $tableFieldCache = [];

	/** @var   array  A list of permutations of the prefix with upper/lowercase letters */
	protected static $prefixCasePermutations = [];

	/** @var   array  Table field name aliases, defined as aliasFieldName => actualFieldName */
	protected $aliasFields = [];

	/** @var   boolean  Should I run automatic checks on the table data? */
	protected $autoChecks = true;

	/** @var   boolean  Should I auto-fill the fields of the model object when constructing it? */
	protected $autoFill = false;

	/** @var   EventDispatcher  An event dispatcher for model behaviours */
	protected $behavioursDispatcher = null;

	/** @var   Driver  The database driver for this model */
	protected $dbo = null;

	/** @var   array  Which fields should be exempt from automatic checks when autoChecks is enabled */
	protected $fieldsSkipChecks = [];

	/** @var   array  Which fields should be auto-filled from the model state (by extent, the request)? */
	protected $fillable = [];

	/** @var   array  Which fields should never be auto-filled from the model state (by extent, the request)? */
	protected $guarded = [];

	/** @var   string  The identity field's name */
	protected $idFieldName = '';

	/** @var   array  A hash array with the table fields we know about and their information. Each key is the field name, the value is the field information */
	protected $knownFields = [];

	/** @var   array  The data of the current record */
	protected $recordData = [];

	/** @var   boolean  What will delete() do? True: trash (enabled set to -2); false: hard delete (remove from database) */
	protected $softDelete = false;

	/** @var   string  The name of the database table we connect to */
	protected $tableName = '';

	/** @var   array  A collection of custom, additional where clauses to apply during buildQuery */
	protected $whereClauses = [];

	/** @var   RelationManager  The relation manager of this model */
	protected $relationManager = null;

	/** @var   array  A list of all eager loaded relations and their attached callbacks */
	protected $eagerRelations = [];

	/** @var   array  A list of the relation filter definitions for this model */
	protected $relationFilters = [];

	/** @var   array  A list of the relations which will be auto-touched by save() and touch() methods */
	protected $touches = [];

	/**
	 * Public constructor. Overrides the parent constructor, adding support for database-aware models.
	 *
	 * You can use the $container['mvc_config'] array to pass some configuration values to the object:
	 *
	 * tableName            String. The name of the database table to use. Default: #__appName_viewNamePlural (Ruby on
	 * Rails convention) idFieldName            String. The table key field name. Default: appName_viewNameSingular_id
	 * (Ruby on Rails convention) knownFields            Array. The known fields in the table. Default: read from the
	 * table itself autoChecks            Boolean. Should I turn on automatic data validation checks? fieldsSkipChecks
	 *       Array. List of fields which should not participate in automatic data validation checks. aliasFields
	 *           Array. Associative array of "magic" field aliases. behavioursDispatcher    EventDispatcher. The model
	 *           behaviours event dispatcher. behaviourObservers    Array. The model behaviour observers to attach to
	 *           the behavioursDispatcher. behaviours            Array. A list of behaviour names to instantiate and
	 *           attach to the behavioursDispatcher. fillable_fields        Array. Which fields should be auto-filled
	 *           from the model state (by extent, the request)? guarded_fields        Array. Which fields should never
	 *           be auto-filled from the model state (by extent, the request)? relations            Array (hashed). The relations to autoload on model creation.
	 *
	 * Setting either fillable_fields or guarded_fields turns on automatic filling of fields in the constructor. If
	 * both
	 * are set only guarded_fields is taken into account. Fields are not filled automatically outside the constructor.
	 *
	 * @param   Container  $container
	 *
	 * @see \Awf\Mvc\Model::__construct()
	 *
	 */
	public function __construct(\Awf\Container\Container $container = null)
	{
		if (!is_object($container))
		{
			$container = Application::getInstance()->getContainer();
		}

		// First call the parent constructor. It also populates $this->config from $container['mvc_config']
		parent::__construct($container);

		// Should I use a different database object?
		$this->dbo = $container->db;

		// Do I have a table name?
		if (isset($this->config['tableName']))
		{
			$this->tableName = $this->config['tableName'];
		}
		elseif (empty($this->tableName))
		{
			// The table name is by default: #__appName_viewNamePlural (Ruby on Rails convention)
			$viewPlural      = Inflector::pluralize($this->getName());
			$this->tableName = '#__' . strtolower($this->container->application->getName()) . '_' . strtolower($viewPlural);
		}

		// Do I have a table key name?
		if (isset($this->config['idFieldName']))
		{
			$this->idFieldName = $this->config['idFieldName'];
		}
		elseif (empty($this->idFieldName))
		{
			// The default ID field is: appName_viewNameSingular_id (Ruby on Rails convention)
			$viewSingular      = Inflector::singularize($this->getName());
			$this->idFieldName = strtolower($this->container->application->getName()) . '_' . strtolower($viewSingular) . '_id';
		}

		// Do I have a list of known fields?
		if (isset($this->config['knownFields']))
		{
			$this->knownFields = $this->config['knownFields'];
		}
		else
		{
			// By default the known fields are fetched from the table itself (slow!)
			$this->knownFields = $this->getTableFields();
		}

		if (empty($this->knownFields))
		{
			throw new NoTableColumns(sprintf('Model %s could not fetch column list for the table %s', $this->getName(), $this->tableName));
		}

		// Should I turn on autoChecks?
		if (isset($this->config['autoChecks']))
		{
			$this->autoChecks = $this->config['autoChecks'];
		}

		// Should I exempt fields from autoChecks?
		if (isset($this->config['fieldsSkipChecks']))
		{
			$this->fieldsSkipChecks = $this->config['fieldsSkipChecks'];
		}

		// Do I have alias fields?
		if (isset($this->config['aliasFields']))
		{
			$this->aliasFields = $this->config['aliasFields'];
		}

		// Do I have a behaviours dispatcher?
		if (isset($this->config['behavioursDispatcher']) && ($this->config['behavioursDispatcher'] instanceof EventDispatcher))
		{
			$this->behavioursDispatcher = $this->config['behavioursDispatcher'];
		}
		// Otherwise create the model behaviours dispatcher
		else
		{
			$this->behavioursDispatcher = new EventDispatcher($this->container);
		}

		// Do I have an array of behaviour observers
		if (isset($this->config['behaviourObservers']) && is_array($this->config['behaviourObservers']))
		{
			foreach ($this->config['behaviourObservers'] as $observer)
			{
				$this->behavioursDispatcher->attach($observer);
			}
		}

		// Do I have a list of behaviours?
		if (isset($this->config['behaviours']) && is_array($this->config['behaviours']))
		{
			foreach ($this->config['behaviours'] as $behaviour)
			{
				$this->addBehaviour($behaviour);
			}
		}

		// Do I have a list of fillable fields?
		if (isset($this->config['fillable_fields']) && is_array($this->config['fillable_fields']))
		{
			$this->fillable = [];
			$this->autoFill = true;

			foreach ($this->config['fillable_fields'] as $field)
			{
				if (array_key_exists($field, $this->knownFields))
				{
					$this->fillable[] = $field;
				}
				elseif (isset($this->aliasFields[$field]))
				{
					$this->fillable[] = $this->aliasFields[$field];
				}
			}
		}

		// Do I have a list of guarded fields?
		if (isset($this->config['guarded_fields']) && is_array($this->config['guarded_fields']))
		{
			$this->guarded  = [];
			$this->autoFill = true;

			foreach ($this->config['guarded_fields'] as $field)
			{
				if (array_key_exists($field, $this->knownFields))
				{
					$this->guarded[] = $field;
				}
				elseif (isset($this->aliasFields[$field]))
				{
					$this->guarded[] = $this->aliasFields[$field];
				}
			}
		}

		// Do I have to auto-fill the fields?
		if ($this->autoFill)
		{
			// If I have guarded fields, I'll try to fill everything, using such fields as a "blacklist"
			if (!empty($this->guarded))
			{
				$fields = array_keys($this->knownFields);
			}
			else
			{
				// Otherwise I'll fill only the fillable ones (act like having a "whitelist")
				$fields = $this->fillable;
			}

			foreach ($fields as $field)
			{
				if (in_array($field, $this->guarded))
				{
					// Do not set guarded fields
					continue;
				}

				$stateValue = $this->getState($field, null);

				if (!is_null($stateValue))
				{
					$this->setFieldValue($field, $stateValue);
				}
			}
		}

		// Create a relation manager
		$this->relationManager = new RelationManager($this);

		// Do I have a list of relations?
		if (isset($this->config['relations']) && is_array($this->config['relations']))
		{
			foreach ($this->config['relations'] as $name => $relConfig)
			{
				if (!is_array($relConfig))
				{
					continue;
				}

				$defaultRelConfig = [
					'type'              => 'hasOne',
					'foreignModelClass' => null,
					'localKey'          => null,
					'foreignKey'        => null,
					'pivotTable'        => null,
					'pivotLocalKey'     => null,
					'pivotForeignKey'   => null,
				];

				$relConfig = array_merge($defaultRelConfig, $relConfig);

				$this->relationManager->addRelation($name, $relConfig['type'], $relConfig['foreignModelClass'],
					$relConfig['localKey'], $relConfig['foreignKey'], $relConfig['pivotTable'],
					$relConfig['pivotLocalKey'], $relConfig['pivotForeignKey']);
			}
		}

		// Initialise the data model
		foreach ($this->knownFields as $fieldName => $information)
		{
			// Initialize only the null or not yet set records
			if (!isset($this->recordData[$fieldName]))
			{
				$this->recordData[$fieldName] = $information->Default;
			}
		}
	}

	/**
	 * Magic caller. It works like the magic setter and returns ourselves for chaining. If no arguments are passed we'll
	 * only look for a scope filter.
	 *
	 * @param   string  $name
	 * @param   mixed   $arguments
	 *
	 * @return  static
	 */
	public function __call($name, $arguments)
	{
		if (empty($arguments))
		{
			$methodName = 'scope' . ucfirst($name);
			if (method_exists($this, $methodName))
			{
				$this->{$methodName}();

				return $this;
			}
		}

		if ($this->relationManager->isMagicMethod($name))
		{
			return call_user_func_array([$this->relationManager, $name], $arguments);
		}

		$arg1        = array_shift($arguments);
		$this->$name = $arg1;

		return $this;
	}

	/**
	 * Magic checker on a property. It follows the same logic of the __get magic method, however, if nothing is found,
	 * it won't return the state of a variable (we are checking if a property is set)
	 *
	 * @param   string  $name  The name of the field to check
	 *
	 * @return  bool    Is the field set?
	 */
	public function __isset($name)
	{
		$value   = null;
		$isState = false;

		if (substr($name, 0, 3) == 'flt')
		{
			$isState = true;
			$name    = strtolower(substr($name, 3, 1)) . substr($name, 4);
		}

		// If $name is a field name, get its value
		if (!$isState && array_key_exists($name, $this->recordData))
		{
			$value = $this->getFieldValue($name);
		}
		elseif (!$isState && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData))
		{
			$name = $this->aliasFields[$name];

			$value = $this->getFieldValue($name);
		}
		elseif ($this->relationManager->isMagicProperty($name))
		{
			$value = $this->relationManager->$name;
		}

		// As the core function isset, the property must exists AND must be NOT null
		return ($value !== null);
	}

	/**
	 * Magic getter. It will return the value of a field or, if no such field is found, the value of the relevant state
	 * variable.
	 *
	 * Tip: Trying to get fltSomething will always return the value of the state variable "something"
	 *
	 * Tip: You can define custom field getter methods as getFieldNameAttribute, where FieldName is your field's name,
	 *      in CamelCase (even if the field name itself is in snake_case).
	 *
	 * @param   string  $name  The name of the field / state variable to retrieve
	 *
	 * @return  static|mixed
	 */
	public function __get($name)
	{
		$isState = false;

		if (substr($name, 0, 3) == 'flt')
		{
			$isState = true;
			$name    = strtolower(substr($name, 3, 1)) . substr($name, 4);
		}

		// If $name is a field name, get its value
		if (!$isState && array_key_exists($name, $this->recordData))
		{
			return $this->getFieldValue($name);
		}
		elseif (!$isState && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData))
		{
			$name = $this->aliasFields[$name];

			return $this->getFieldValue($name);
		}
		elseif ($this->relationManager->isMagicProperty($name))
		{
			return $this->relationManager->$name;
		}
		// If $name is not a field name, get the value of a state variable
		else
		{
			return $this->getState($name);
		}
	}

	/**
	 * Magic setter. It will set the value of a field or the value of a dynamic scope filter, or the value of the
	 * relevant state variable.
	 *
	 * Tip: Trying to set fltSomething will always return the value of the state variable "something"
	 *
	 * Tip: Trying to set scopeSomething will always return the value of the dynamic scope filter "something"
	 *
	 * Tip: You can define custom field setter methods as setFieldNameAttribute, where FieldName is your field's name,
	 *      in CamelCase (even if the field name itself is in snake_case).
	 *
	 * @param   string  $name   The name of the field / scope / state variable to set
	 * @param   mixed   $value  The value to set
	 *
	 * @return  void
	 */
	public function __set($name, $value)
	{
		$isState = false;
		$isScope = false;

		if (substr($name, 0, 3) == 'flt')
		{
			$isState = true;
			$name    = strtolower(substr($name, 3, 1)) . substr($name, 4);
		}
		elseif (substr($name, 0, 5) == 'scope')
		{
			$isScope = true;
			$name    = strtolower(substr($name, 5, 1)) . substr($name, 5);
		}

		// If $name is a field name, set its value
		if (!$isState && !$isScope && array_key_exists($name, $this->recordData))
		{
			$this->setFieldValue($name, $value);
		}
		elseif (!$isState && !$isScope && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData))
		{
			$name = $this->aliasFields[$name];
			$this->setFieldValue($name, $value);
		}
		// If $name is a dynamic scope filter, set its value
		elseif ($isScope || method_exists($this, 'scope' . ucfirst($name)))
		{
			$method = 'scope' . ucfirst($name);
			$this->{$method}($value);
		}
		// If $name is not a field name, set the value of a state variable
		else
		{
			$this->setState($name, $value);
		}
	}

	/**
	 * Adds a known field to the DataModel. This is only necessary if you are using a custom buildQuery with JOINs or
	 * field aliases. Please note that you need to make further modifications for bind() and save() to work in this
	 * case. Please refer to the documentation blocks of these methods for more information. It is generally considered
	 * a very BAD idea using JOINs instead of relations. It complicates your life and is bound to cause bugs that are
	 * very hard to track back.
	 *
	 * @param   string  $fieldName  The name of the field
	 * @param   mixed   $default    Default value, used by reset() (default: null)
	 * @param   string  $type       Database type for the field. If unsure use 'integer', 'float' or 'text'.
	 * @param   bool    $replace    Should we replace an existing known field definition?
	 *
	 * @return  $this  Self, for chaining
	 */
	public function addKnownField($fieldName, $default = null, $type = 'integer', $replace = false)
	{
		if (array_key_exists($fieldName, $this->knownFields) && !$replace)
		{
			return $this;
		}

		$info = (object) [
			'Default' => $default,
			'Type'    => $type,
		];

		$this->knownFields[$fieldName] = $info;

		// Initialize only the null or not yet set records
		if (!isset($this->recordData[$fieldName]))
		{
			$this->recordData[$fieldName] = $default;
		}

		return $this;
	}

	/**
	 * Get the columns from a database table.
	 *
	 * @param   string  $tableName  Table name. If null current table is used
	 *
	 * @return  mixed  An array of the field names, or false if an error occurs.
	 */
	public function getTableFields($tableName = null)
	{
		// Make sure we have a list of tables in this db
		if (empty(static::$tableCache))
		{
			static::$tableCache = $this->getDbo()->getTableList();
		}

		if (!$tableName)
		{
			$tableName = $this->tableName;
		}

		// Try to load again column specifications if the table is not loaded OR if it's loaded and
		// the previous call returned an error
		if (!array_key_exists($tableName, static::$tableFieldCache) ||
			(isset(static::$tableFieldCache[$tableName]) && !static::$tableFieldCache[$tableName])
		)
		{
			// Lookup the fields for this table only once.
			$name = $tableName;

			$prefix = $this->getDbo()->getPrefix();

			if (substr($name, 0, 3) == '#__')
			{
				$checkName = $prefix . substr($name, 3);
			}
			else
			{
				$checkName = $name;
			}

			// Iterate through all lower/uppercase permutations of the prefix if we have a prefix with at least one uppercase letter
			if (!in_array($checkName, static::$tableCache) && preg_match('/[A-Z]/', $prefix) && (substr($name, 0, 3) == '#__'))
			{
				$prefixPermutations = $this->getPrefixCasePermutations();
				$partialCheckName   = substr($name, 3);

				foreach ($prefixPermutations as $permutatedPrefix)
				{
					$checkName = $permutatedPrefix . $partialCheckName;

					if (in_array($checkName, static::$tableCache))
					{
						break;
					}
				}
			}

			if (!in_array($checkName, static::$tableCache))
			{
				// The table doesn't exist. Return false.
				static::$tableFieldCache[$tableName] = false;
			}
			else
			{
				$fields = $this->getDbo()->getTableColumns($name, false);

				if (empty($fields))
				{
					$fields = false;
				}

				static::$tableFieldCache[$tableName] = $fields;
			}

			// PostgreSQL date type compatibility
			if (($this->getDbo()->name == 'postgresql') && (static::$tableFieldCache[$tableName] != false))
			{
				foreach (static::$tableFieldCache[$tableName] as $field)
				{
					if (strtolower($field->type) == 'timestamp without time zone')
					{
						if (stristr($field->Default, '\'::timestamp without time zone'))
						{
							list ($date,) = explode('::', $field->Default, 2);
							$field->Default = trim($date, "'");
						}
					}
				}
			}
		}

		return static::$tableFieldCache[$tableName];
	}

	/**
	 * Get the database connection associated with this data Model
	 *
	 * @return  Driver
	 */
	public function getDbo()
	{
		if (!is_object($this->dbo))
		{
			$this->dbo = $this->container->db;
		}

		return $this->dbo;
	}

	/**
	 * Returns the data currently bound to the model in an array format
	 *
	 * @return array
	 */
	public function getData()
	{
		$ret = [];

		foreach ($this->knownFields as $field => $info)
		{
			$ret[$field] = $this->getFieldValue($field);
		}

		return $ret;
	}

	/**
	 * Return the value of the identity column of the currently loaded record
	 *
	 * @return   mixed
	 */
	public function getId()
	{
		return $this->{$this->idFieldName};
	}

	/**
	 * Returns the name of the table's id field (primary key) name
	 *
	 * @return  string
	 */
	public function getIdFieldName()
	{
		return $this->idFieldName;
	}

	/**
	 * Returns the database table name this model talks to
	 *
	 * @return  string
	 */
	public function getTableName()
	{
		return $this->tableName;
	}

	/**
	 * Returns the value of a field. If a field is not set it uses the $default value. Automatically uses magic
	 * getter variables if required.
	 *
	 * @param   string  $name     The name of the field to retrieve
	 * @param   mixed   $default  Default value, if the field is not set and doesn't have a getter method
	 *
	 * @return  mixed  The value of the field
	 */
	public function getFieldValue($name, $default = null)
	{
		if (array_key_exists($name, $this->aliasFields))
		{
			$name = $this->aliasFields[$name];
		}

		$method = Inflector::camelize('get_' . $name . '_attribute');

		if (method_exists($this, $method))
		{
			return $this->{$method}();
		}
		elseif (!isset($this->recordData[$name]))
		{
			$this->recordData[$name] = $default;
		}

		return $this->recordData[$name];
	}

	/**
	 * Sets the value of a field.
	 *
	 * @param   string  $name   The name of the field to set
	 * @param   mixed   $value  The value to set it to
	 *
	 * @return  void
	 */
	public function setFieldValue($name, $value = null)
	{
		if (array_key_exists($name, $this->aliasFields))
		{
			$name = $this->aliasFields[$name];
		}

		$method = Inflector::camelize('set_' . $name . '_attribute');

		if (method_exists($this, $method))
		{
			$this->{$method}($value);
		}
		else
		{
			$this->recordData[$name] = $value;
		}
	}

	/**
	 * Does this model know about a field called $fieldName? Automatically uses aliases when necessary.
	 *
	 * @param   string  $fieldName  Field name to check
	 *
	 * @return  boolean  True if the field exists
	 */
	public function hasField($fieldName)
	{
		$realFieldName = $this->getFieldAlias($fieldName);

		return array_key_exists($realFieldName, $this->knownFields);
	}

	/**
	 * Is this field known to the model and marked as nullable in the database?
	 *
	 * Automatically uses aliases when necessary.
	 *
	 * @param   string  $fieldName  Field name to check
	 *
	 * @return  bool  True if the field is nullable or doesn't exist
	 */
	public function isNullableField($fieldName)
	{
		if (!$this->hasField($fieldName))
		{
			return true;
		}

		$realFieldName = $this->getFieldAlias($fieldName);
		$nullState     = property_exists($this->knownFields[$realFieldName], 'Null') ? $this->knownFields[$realFieldName]->Null : 'YES';

		return strtolower($nullState) == 'yes';
	}

	/**
	 * Get the real name of a field name based on its alias. If the field is not aliased $alias is returned
	 *
	 * @param   string  $alias  The field to get an alias for
	 *
	 * @return  string  The real name of the field
	 */
	public function getFieldAlias($alias)
	{
		if (array_key_exists($alias, $this->aliasFields))
		{
			return $this->aliasFields[$alias];
		}
		else
		{
			return $alias;
		}
	}

	/**
	 * Save a record, creating it if it doesn't exist or updating it if it exists. By default it uses the currently set
	 * data, unless you provide a $data array.
	 *
	 * @param   null|array  $data            [Optional] Data to bind
	 * @param   string      $orderingFilter  A WHERE clause used to apply table item reordering
	 * @param   array       $ignore          A list of fields to ignore when binding $data
	 *
	 * @return   DataModel  Self, for chaining
	 */
	public function save($data = null, $orderingFilter = '', $ignore = null)
	{
		// Stash the primary key
		$oldPKValue = $this->getId();

		// Call the onBeforeSave event
		if (method_exists($this, 'onBeforeSave'))
		{
			$this->onBeforeSave($data);
		}

		$this->behavioursDispatcher->trigger('onBeforeSave', [&$this, &$data]);

		// Bind any (optional) data. If no data is provided, the current record data is used
		if (!is_null($data))
		{
			$this->bind($data, $ignore);
		}

		// Is this a new record?
		if (empty($oldPKValue))
		{
			$isNewRecord = true;
		}
		else
		{
			$isNewRecord = $oldPKValue != $this->getId();
		}

		// Check the validity of the data
		$this->check();

		// Get the database object
		$db       = $this->getDbo();
		$nullDate = $db->getNullDate();
		$date     = new Date();

		// Update the created_on / modified_on
		if ($isNewRecord && $this->hasField('created_on'))
		{
			$created_on = $this->getFieldAlias('created_on');

			if (empty($this->$created_on) || ($this->$created_on == $nullDate))
			{
				$this->$created_on = $date->toSql(false, $db);
			}
		}
		elseif (!$isNewRecord && $this->hasField('modified_on'))
		{
			$modified_on        = $this->getFieldAlias('modified_on');
			$this->$modified_on = $date->toSql(false, $db);
		}

		// Get the user manager for this application and retrieve the user
		$userManager = $this->container->userManager;
		$userId      = $userManager->getUser()->getId();

		// Update the created_by / modified_by values if necessary
		if ($isNewRecord && $this->hasField('created_by'))
		{
			$created_by = $this->getFieldAlias('created_by');

			if (empty($this->$created_by))
			{
				$this->$created_by = $userId;
			}
		}
		elseif (!$isNewRecord && $this->hasField('modified_by'))
		{
			$modified_by        = $this->getFieldAlias('modified_by');
			$this->$modified_by = $userId;
		}

		// Unlock the record if necessary
		if ($this->hasField('locked_by'))
		{
			$locked_by        = $this->getFieldAlias('locked_by');
			$this->$locked_by = 0;
		}

		if ($this->hasField('locked_on'))
		{
			$locked_on        = $this->getFieldAlias('locked_on');
			$this->$locked_on = $this->isNullableField('locked_on') ? null : $db->getNullDate();
		}

		// Insert or update the record
		$dataObject = (object) $this->recordData;

		if ($isNewRecord)
		{
			if (method_exists($this, 'onBeforeCreate'))
			{
				$this->onBeforeCreate($dataObject);
			}

			$this->behavioursDispatcher->trigger('onBeforeCreate', [&$this, &$dataObject]);

			// Insert the new record
			$db->insertObject($this->tableName, $dataObject, $this->idFieldName);

			// Update ourselves with the new ID field's value
			$this->{$this->idFieldName} = $db->insertid();

			if (method_exists($this, 'onAfterCreate'))
			{
				$this->onAfterCreate();
			}

			$this->behavioursDispatcher->trigger('onAfterCreate', [&$this]);
		}
		else
		{
			if (method_exists($this, 'onBeforeUpdate'))
			{
				$this->onBeforeUpdate($dataObject);
			}

			$this->behavioursDispatcher->trigger('onBeforeUpdate', [&$this, &$dataObject]);

			$db->updateObject($this->tableName, $dataObject, $this->idFieldName, true);

			if (method_exists($this, 'onAfterUpdate'))
			{
				$this->onAfterUpdate();
			}

			$this->behavioursDispatcher->trigger('onAfterUpdate', [&$this]);
		}

		// If an ordering filter is set, attempt reorder the rows in the table based on the filter and value.
		if ($orderingFilter)
		{
			$filterValue = $this->$orderingFilter;
			$this->reorder($orderingFilter ? $db->qn($orderingFilter) . ' = ' . $db->q($filterValue) : '');
		}

		// One more thing... Touch all relations in the $touches array
		if (!empty($this->touches))
		{
			foreach ($this->touches as $relation)
			{
				$records = $this->getRelations()->getData($relation);

				if (!empty($records))
				{
					if ($records instanceof DataModel)
					{
						$records = [$records];
					}

					/** @var DataModel $record */
					foreach ($records as $record)
					{
						$record->touch();
					}
				}
			}
		}

		// Finally, call the onAfterSave event
		if (method_exists($this, 'onAfterSave'))
		{
			$this->onAfterSave();
		}

		$this->behavioursDispatcher->trigger('onAfterSave', [&$this]);

		return $this;
	}

	/**
	 * Save a record, creating it if it doesn't exist or updating it if it exists. By default it uses the currently set
	 * data, unless you provide a $data array. On top of that, it also saves all specified relations. If $relations is
	 * null it will save all relations known to this model.
	 *
	 * @param   null|array  $data            [Optional] Data to bind
	 * @param   string      $orderingFilter  A WHERE clause used to apply table item reordering
	 * @param   array       $ignore          A list of fields to ignore when binding $data
	 * @param   array       $relations       Which relations to save with the model's record. Leave null for all
	 *                                       relations
	 *
	 * @return $this Self, for chaining
	 */
	public function push($data = null, $orderingFilter = '', $ignore = null, array $relations = null)
	{
		// Store the model's $touches definition
		$touches = $this->touches;

		// If $relations is non-null, remove $relations from $this->touches. Since $relations will be saved, they are
		// implicitly touched. We don't want to double-touch those records, do we?
		if (is_array($relations))
		{
			$this->touches = array_diff($this->touches, $relations);
		}
		// Otherwise empty $this->touches completely as we'll be pushing all relations
		else
		{
			$this->touches = [];
		}

		// Save this record
		$this->save($data, $orderingFilter, $ignore);

		// Push all relations specified (or all relations if $relations is null)
		$relManager   = $this->getRelations();
		$allRelations = $relManager->getRelationNames();

		if (!empty($allRelations))
		{
			foreach ($allRelations as $relationName)
			{
				if (!is_null($relations) && !in_array($relationName, $relations))
				{
					continue;
				}

				$relManager->save($relationName);
			}
		}

		// Restore the model's $touches definition
		$this->touches = $touches;

		// Return self for chaining
		return $this;
	}

	/**
	 * Method to bind an associative array or object to the DataModel instance. This
	 * method optionally takes an array of properties to ignore when binding.
	 *
	 * @param   mixed  $data    An associative array or object to bind to the DataModel instance.
	 * @param   mixed  $ignore  An optional array or space separated list of properties to ignore while binding.
	 *
	 * @return  static  Self, for chaining
	 *
	 * @throws  \InvalidArgumentException
	 * @throws    \Exception
	 */
	public function bind($data, $ignore = [])
	{
		if (method_exists($this, 'onBeforeBind'))
		{
			$this->onBeforeBind($data);
		}

		$this->behavioursDispatcher->trigger('onBeforeBind', [&$this, &$data]);

		// If the source value is not an array or object return false.
		if (!is_object($data) && !is_array($data))
		{
			throw new \InvalidArgumentException(sprintf('%s::bind(*%s*)', get_class($this), gettype($data)));
		}

		// If the ignore value is a string, explode it over spaces.
		if (!is_array($ignore))
		{
			$ignore = explode(' ', $ignore);
		}

		// Bind the source value, excluding the ignored fields.
		foreach ($this->recordData as $k => $currentValue)
		{
			// Only process fields not in the ignore array.
			if (!in_array($k, $ignore))
			{
				if (is_array($data) && isset($data[$k]))
				{
					$this->setFieldValue($k, $data[$k]);
				}
				elseif (is_object($data) && isset($data->$k))
				{
					$this->setFieldValue($k, $data->$k);
				}
			}
		}

		if (method_exists($this, 'onAfterBind'))
		{
			$this->onAfterBind($data);
		}

		$this->behavioursDispatcher->trigger('onAfterBind', [&$this, &$data]);

		return $this;
	}

	/**
	 * Check the data for validity. By default it only checks for fields declared as NOT NULL
	 *
	 * @return  static  Self, for chaining
	 *
	 * @throws \RuntimeException  When the data bound to this record is invalid
	 */
	public function check()
	{
		if (!$this->autoChecks)
		{
			return $this;
		}

		foreach ($this->knownFields as $fieldName => $field)
		{
			// Never check the key if it's empty; an empty key is normal for new records
			if ($fieldName == $this->idFieldName)
			{
				continue;
			}

			$value = $this->$fieldName;

			if (($field->Null == 'NO') && empty($value) && !is_numeric($value) && !in_array($fieldName, $this->fieldsSkipChecks))
			{
				$text = $this->container->application->getName() . '_' . Inflector::singularize($this->getName()) . '_ERR_'
					. $fieldName . '_EMPTY';

				throw new \RuntimeException(Text::_($text), 500);
			}
		}

		return $this;
	}

	/**
	 * Change the ordering of the records of the table
	 *
	 * @param   string  $where  The WHERE clause of the SQL used to fetch the order
	 *
	 * @return  static  Self, for chaining
	 *
	 * @throws  \UnexpectedValueException
	 */
	public function reorder($where = '')
	{
		// If there is no ordering field set an error and return false.
		if (!$this->hasField('ordering'))
		{
			throw new SpecialColumnMissing(sprintf('%s does not support ordering.', $this->tableName));
		}

		if (method_exists($this, 'onBeforeReorder'))
		{
			$this->onBeforeReorder($where);
		}

		$this->behavioursDispatcher->trigger('onBeforeReorder', [&$this, &$where]);

		$order_field = $this->getFieldAlias('ordering');
		$k           = $this->getIdFieldName();
		$db          = $this->getDbo();

		// Get the primary keys and ordering values for the selection.
		$query = $db->getQuery(true)
			->select($db->qn($k) . ', ' . $db->qn($order_field))
			->from($db->qn($this->getTableName()))
			->where($db->qn($order_field) . ' >= ' . $db->q(0))
			->order($db->qn($order_field));

		// Setup the extra where and ordering clause data.
		if ($where)
		{
			$query->where($where);
		}

		$rows = $db->setQuery($query)->loadObjectList();

		// Compact the ordering values.
		foreach ($rows as $i => $row)
		{
			// Make sure the ordering is a positive integer.
			if ($row->$order_field >= 0)
			{
				// Only update rows that are necessary.
				if ($row->$order_field != $i + 1)
				{
					// Update the row ordering field.
					$query = $db->getQuery(true)
						->update($db->qn($this->getTableName()))
						->set($db->qn($order_field) . ' = ' . $db->q($i + 1))
						->where($db->qn($k) . ' = ' . $db->q($row->$k));
					$db->setQuery($query)->execute();
				}
			}
		}

		if (method_exists($this, 'onAfterReorder'))
		{
			$this->onAfterReorder();
		}

		$this->behavioursDispatcher->trigger('onAfterReorder', [&$this]);

		return $this;
	}

	/**
	 * Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause.
	 * Negative numbers move the row up in the sequence and positive numbers move it down.
	 *
	 * @param   integer  $delta  The direction and magnitude to move the row in the ordering sequence.
	 * @param   string   $where  WHERE clause to use for limiting the selection of rows to compact the
	 *                           ordering values.
	 *
	 * @return  static  Self, for chaining
	 *
	 * @throws  \UnexpectedValueException  If the table does not support reordering
	 * @throws  \RuntimeException  If the record is not loaded
	 */
	public function move($delta, $where = '')
	{
		if (!$this->hasField('ordering'))
		{
			throw new SpecialColumnMissing(sprintf('%s does not support ordering.', $this->tableName));
		}

		if (method_exists($this, 'onBeforeMove'))
		{
			$this->onBeforeMove($delta, $where);
		}

		$this->behavioursDispatcher->trigger('onBeforeMove', [&$this, &$delta, &$where]);

		$ordering_field = $this->getFieldAlias('ordering');

		// If the change is none, do nothing.
		if (empty($delta))
		{
			if (method_exists($this, 'onAfterMove'))
			{
				$this->onAfterMove();
			}

			$this->behavioursDispatcher->trigger('onAfterMove', [&$this]);

			return $this;
		}

		$k     = $this->idFieldName;
		$row   = null;
		$db    = $this->getDbo();
		$query = $db->getQuery(true);

		// If the table is not loaded, return false
		if (empty($this->$k))
		{
			throw new RecordNotLoaded(sprintf("Model %s does not have a loaded record", $this->getName()));
		}

		// Select the primary key and ordering values from the table.
		$query->select([
				$db->qn($this->idFieldName), $db->qn($ordering_field),
			]
		)->from($db->qn($this->tableName));

		// If the movement delta is negative move the row up.
		if ($delta < 0)
		{
			$query->where($db->qn($ordering_field) . ' < ' . $db->q((int) $this->$ordering_field));
			$query->order($db->qn($ordering_field) . ' DESC');
		}
		// If the movement delta is positive move the row down.
		elseif ($delta > 0)
		{
			$query->where($db->qn($ordering_field) . ' > ' . $db->q((int) $this->$ordering_field));
			$query->order($db->qn($ordering_field) . ' ASC');
		}

		// Add the custom WHERE clause if set.
		if ($where)
		{
			$query->where($where);
		}

		// Select the first row with the criteria.
		$row = $db->setQuery($query, 0, 1)->loadObject();

		// If a row is found, move the item.
		if (!empty($row))
		{
			// Update the ordering field for this instance to the row's ordering value.
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($db->qn($ordering_field) . ' = ' . $db->q((int) $row->$ordering_field))
				->where($db->qn($k) . ' = ' . $db->q($this->$k));
			$db->setQuery($query)->execute();

			// Update the ordering field for the row to this instance's ordering value.
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($db->qn($ordering_field) . ' = ' . $db->q((int) $this->$ordering_field))
				->where($db->qn($k) . ' = ' . $db->q($row->$k));
			$db->setQuery($query)->execute();

			// Update the instance value.
			$this->$ordering_field = $row->$ordering_field;
		}

		if (method_exists($this, 'onAfterMove'))
		{
			$this->onAfterMove();
		}

		$this->behavioursDispatcher->trigger('onAfterMove', [&$this]);

		return $this;
	}

	/**
	 * Process a large collection of records a few at a time.
	 *
	 * @param   integer   $chunkSize  How many records to process at once
	 * @param   callable  $callback   A callable to process each record
	 *
	 * @return  $this  Self, for chaining
	 */
	public function chunk($chunkSize, $callback)
	{
		$totalItems = $this->count();

		if (!$totalItems)
		{
			return $this;
		}

		$start = 0;

		while ($start < ($totalItems - 1))
		{
			$this->get(true, $start, $chunkSize)->transform($callback);

			$start += $chunkSize;
		}

		return $this;
	}

	/**
	 * Get the number of all items
	 *
	 * @return  integer
	 */
	public function count()
	{
		// Get a "count all" query
		$db    = $this->getDbo();
		$query = $this->buildQuery(true);
		$query->select(null)->select('COUNT(*)');

		// Run the "before build query" hook and behaviours
		if (method_exists($this, 'buildCountQuery'))
		{
			$this->buildCountQuery($query);
		}

		$this->behavioursDispatcher->trigger('buildCountQuery', [&$this, &$query]);

		$total = $db->setQuery($query)->loadResult();

		return $total;
	}

	/**
	 * Build the query to fetch data from the database
	 *
	 * @param   boolean  $overrideLimits  Should I override limits
	 *
	 * @return  Query  The database query to use
	 */
	public function buildQuery($overrideLimits = false)
	{
		// Get a "select all" query
		$db    = $this->getDbo();
		$query = $db->getQuery(true)
			->select('*')
			->from($this->getTableName());

		// Run the "before build query" hook and behaviours
		if (method_exists($this, 'onBeforeBuildQuery'))
		{
			$this->onBeforeBuildQuery($query);
		}

		$this->behavioursDispatcher->trigger('onBeforeBuildQuery', [&$this, &$query]);

		// Apply custom WHERE clauses
		if (count($this->whereClauses))
		{
			foreach ($this->whereClauses as $clause)
			{
				$query->where($clause);
			}
		}

		// Apply ordering unless we are called to override limits
		if (!$overrideLimits)
		{
			$order = $this->getState('filter_order', null, 'cmd');

			if (!array_key_exists($order, $this->knownFields))
			{
				$order = $this->getIdFieldName();
			}

			$order = $db->qn($order);

			$dir = strtoupper($this->getState('filter_order_Dir', 'ASC', 'cmd'));

			if (!in_array($dir, ['ASC', 'DESC']))
			{
				$dir = 'ASC';
			}

			$query->order($order . ' ' . $dir);
		}

		// Run the "before after query" hook and behaviours
		if (method_exists($this, 'onAfterBuildQuery'))
		{
			$this->onAfterBuildQuery($query);
		}

		$this->behavioursDispatcher->trigger('onAfterBuildQuery', [&$this, &$query]);

		return $query;
	}

	/**
	 * Returns a DataCollection iterator based on your currently set Model state
	 *
	 * @param   boolean  $overrideLimits  Should I ignore limits set in the Model?
	 * @param   integer  $limitstart      How many items to skip from the start, only when $overrideLimits = true
	 * @param   integer  $limit           How many items to return, only when $overrideLimits = true
	 *
	 * @return  DataCollection  The data collection
	 */
	public function get($overrideLimits = false, $limitstart = 0, $limit = 0)
	{
		if (!$overrideLimits)
		{
			$limitstart = $this->getState('limitstart', 0);
			$limit      = $this->getState('limit', 0);
		}

		$dataCollection = DataCollection::make($this->getItemsArray($limitstart, $limit));

		$this->eagerLoad($dataCollection, null);

		return $dataCollection;
	}

	/**
	 * Returns a raw array of DataModel instances based on your currently set Model state
	 *
	 * @param   integer  $limitstart  How many items from the start to skip (0 = do not skip)
	 * @param   integer  $limit       How many items to return (0 = all)
	 *
	 * @return  array  Array of DataModel objects
	 */
	public function &getItemsArray($limitstart = 0, $limit = 0)
	{
		$limitstart     = max($limitstart, 0);
		$limit          = max($limit, 0);
		$overrideLimits = ($limitstart == 0) && ($limit == 0);

		$query = $this->buildQuery($overrideLimits);

		$db = $this->getDbo();
		$db->setQuery($query, $limitstart, $limit);

		$itemsTemp = $db->loadAssocList();
		$items     = [];
		$className = get_class($this);

		while (!empty($itemsTemp))
		{
			$data = array_shift($itemsTemp);
			/** @var DataModel $item */
			$item = new $className($this->container);
			$item->bind($data);
			$items[$item->getId()] = $item;
			$item->relationManager = clone $this->relationManager;
			$item->relationManager->rebase($item);
		}

		if (method_exists($this, 'onAfterGetItemsArray'))
		{
			$this->onAfterGetItemsArray($items);
		}

		$this->behavioursDispatcher->trigger('onAfterGetItemsArray', [&$this, &$items]);

		return $items;
	}

	/**
	 * Eager loads the provided relations and assigns their data to a data collection
	 *
	 * @param   Collection  $dataCollection  The data collection on which the eager loaded relations will be applied
	 * @param   array|null  $relations       The relations to eager load. Leave empty to use the already defined
	 *                                       relations
	 *
	 * @return $this for chaining
	 */
	public function eagerLoad(Collection &$dataCollection, array $relations = null)
	{
		if (empty($relations))
		{
			$relations = $this->eagerRelations;
		}

		// Apply eager loaded relations
		if ($dataCollection->count() && !empty($relations))
		{
			$relationManager = $this->getRelations();

			foreach ($relations as $relation => $callback)
			{
				// Did they give us a relation name without a callback?
				if (!is_callable($callback) && is_string($callback) && !empty($callback))
				{
					$relation = $callback;
					$callback = null;
				}

				$relationData  = $relationManager->getData($relation, $callback, $dataCollection);
				$foreignKeyMap = $relationManager->getForeignKeyMap($relation);

				/** @var DataModel $item */
				foreach ($dataCollection as $item)
				{
					$item->getRelations()->setDataFromCollection($relation, $relationData, $foreignKeyMap);
				}
			}
		}

		return $this;
	}

	/**
	 * Archive the record, i.e. set enabled to 2
	 *
	 * @return   $this  For chaining
	 */
	public function archive()
	{
		if (!$this->getId())
		{
			throw new RecordNotLoaded("Can't archive a not loaded DataModel");
		}

		if (!$this->hasField('enabled'))
		{
			return $this;
		}

		if (method_exists($this, 'onBeforeArchive'))
		{
			$this->onBeforeArchive();
		}

		$this->behavioursDispatcher->trigger('onBeforeArchive', [&$this]);

		$enabled = $this->getFieldAlias('enabled');

		$this->$enabled = 2;
		$this->save();

		if (method_exists($this, 'onAfterArchive'))
		{
			$this->onAfterArchive();
		}

		$this->behavioursDispatcher->trigger('onAfterArchive', [&$this]);

		return $this;
	}

	/**
	 * Trashes a record, either the currently loaded one or the one specified in $id. If an $id is specified that record
	 * is loaded before trying to trash it. Unlike a hard delete, trashing is a "soft delete", only setting the enabled
	 * field to -2.
	 *
	 * @param   mixed  $id  Primary key (id field) value
	 *
	 * @return  $this  for chaining
	 */
	public function trash($id = null)
	{
		if (!empty($id))
		{
			$this->findOrFail($id);
		}

		$id = $this->getId();

		if (!$id)
		{
			throw new RecordNotLoaded("Can't trash a not loaded DataModel");
		}

		if (!$this->hasField('enabled'))
		{
			throw new SpecialColumnMissing("DataModel::trash method needs an 'enabled' field");
		}

		if (method_exists($this, 'onBeforeTrash'))
		{
			$this->onBeforeTrash($id);
		}

		$this->behavioursDispatcher->trigger('onBeforeTrash', [&$this, &$id]);

		$enabled        = $this->getFieldAlias('enabled');
		$this->$enabled = -2;
		$this->save();

		if (method_exists($this, 'onAfterTrash'))
		{
			$this->onAfterTrash($id);
		}

		$this->behavioursDispatcher->trigger('onAfterTrash', [&$this, $id]);

		return $this;
	}

	/**
	 * Change the publish state of a record. By default it will set it to 1 (published) unless you specify a different
	 * value.
	 *
	 * @param   int  $state  The publish state. Default: 1 (published).
	 *
	 * @return   $this  For chaining
	 */
	public function publish($state = 1)
	{
		if (!$this->getId())
		{
			throw new RecordNotLoaded("Can't change the state of a not loaded DataModel");
		}

		if (!$this->hasField('enabled'))
		{
			return $this;
		}

		if (method_exists($this, 'onBeforePublish'))
		{
			$this->onBeforePublish();
		}

		$this->behavioursDispatcher->trigger('onBeforePublish', [&$this]);

		$enabled = $this->getFieldAlias('enabled');

		$this->$enabled = $state;
		$this->save();

		if (method_exists($this, 'onAfterPublish'))
		{
			$this->onAfterPublish();
		}

		$this->behavioursDispatcher->trigger('onAfterPublish', [&$this]);

		return $this;
	}

	/**
	 * Unpublish the record, i.e. set enabled to 0
	 *
	 * @return   $this  For chaining
	 */
	public function unpublish()
	{
		if (!$this->getId())
		{
			throw new RecordNotLoaded("Can't unlock a not loaded DataModel");
		}

		if (!$this->hasField('enabled'))
		{
			return $this;
		}

		if (method_exists($this, 'onBeforeUnpublish'))
		{
			$this->onBeforeUnpublish();
		}

		$this->behavioursDispatcher->trigger('onBeforeUnpublish', [&$this]);

		$enabled = $this->getFieldAlias('enabled');

		$this->$enabled = 0;
		$this->save();

		if (method_exists($this, 'onAfterUnpublish'))
		{
			$this->onAfterUnpublish();
		}

		$this->behavioursDispatcher->trigger('onAfterUnpublish', [&$this]);

		return $this;
	}

	/**
	 * Untrashes a record, either the currently loaded one or the one specified in $id. If an $id is specified that
	 * record is loaded before trying to untrash it. Please note that enabled is set to 0 (unpublished) when you untrash
	 * an item.
	 *
	 * @param   mixed  $id  Primary key (id field) value
	 *
	 * @return  $this  for chaining
	 */
	public function restore($id = null)
	{
		if (!$this->hasField('enabled'))
		{
			return $this;
		}

		if (!empty($id))
		{
			$this->findOrFail($id);
		}

		$id = $this->getId();

		if (!$id)
		{
			throw new RecordNotLoaded("Can't change the state of a not loaded DataModel");
		}

		if (method_exists($this, 'onBeforeRestore'))
		{
			$this->onBeforeRestore($id);
		}

		$this->behavioursDispatcher->trigger('onBeforeRestore', [&$this, &$id]);

		$enabled = $this->getFieldAlias('enabled');

		$this->$enabled = 0;
		$this->save();

		if (method_exists($this, 'onAfterRestore'))
		{
			$this->onAfterRestore($id);
		}

		$this->behavioursDispatcher->trigger('onAfterRestore', [&$this, $id]);

		return $this;
	}

	/**
	 * Creates a copy of the current record. After the copy is performed, the data model contains the data of the new
	 * record.
	 *
	 * @return   DataModel
	 */
	public function copy()
	{
		$this->{$this->idFieldName} = null;

		return $this->save();
	}

	/**
	 * Reset the record data
	 *
	 * @param   boolean  $useDefaults     Should I use the default values? Default: yes
	 * @param   boolean  $resetRelations  Should I reset the relations too? Default: no
	 *
	 * @return  static  Self, for chaining
	 */
	public function reset($useDefaults = true, $resetRelations = false)
	{
		$this->recordData = [];

		foreach ($this->knownFields as $fieldName => $information)
		{
			if ($useDefaults)
			{
				$this->recordData[$fieldName] = $information->Default;
			}
			else
			{
				$this->recordData[$fieldName] = null;
			}
		}

		if ($resetRelations)
		{
			$this->relationManager->resetRelations();
			$this->eagerRelations = [];
		}

		$this->relationFilters = [];

		return $this;
	}

	/**
	 * Automatically performs a hard or soft delete, based on the value of $this->softDelete. A soft delete simply sets
	 * enabled to -2 whereas a hard delete removes the data from the database. If you want to force a specific behaviour
	 * directly call trash() for a soft delete or forceDelete() for a hard delete.
	 *
	 * @param   mixed  $id  Primary key (id field) value
	 *
	 * @return  $this  for chaining
	 */
	public function delete($id = null)
	{
		if ($this->softDelete)
		{
			return $this->trash($id);
		}
		else
		{
			return $this->forceDelete($id);
		}
	}

	/**
	 * Delete a record, either the currently loaded one or the one specified in $id. If an $id is specified that record
	 * is loaded before trying to delete it. In the end the data model is reset.
	 *
	 * @param   mixed  $id  Primary key (id field) value
	 *
	 * @return  $this  for chaining
	 */
	public function forceDelete($id = null)
	{
		if (!empty($id))
		{
			$this->findOrFail($id);
		}

		$id = $this->getId();

		if (!$id)
		{
			throw new RecordNotLoaded("Can't delete a not loaded DataModel object");
		}

		if (method_exists($this, 'onBeforeDelete'))
		{
			$this->onBeforeDelete($id);
		}

		$this->behavioursDispatcher->trigger('onBeforeDelete', [&$this, &$id]);

		$db = $this->getDbo();

		$query = $db->getQuery(true)
			->delete()
			->from($this->tableName)
			->where($db->qn($this->idFieldName) . ' = ' . $db->q($id));
		$db->setQuery($query)->execute();

		if (method_exists($this, 'onAfterDelete'))
		{
			$this->onAfterDelete($id);
		}

		$this->behavioursDispatcher->trigger('onAfterDelete', [&$this]);

		$this->reset();

		return $this;
	}

	/**
	 * Find and load a single record based on the provided key values. If the record is not found an exception is thrown
	 *
	 * @param   array|mixed  $keys  An optional primary key value to load the row by, or an array of fields to match.
	 *                              If not set the "id" state variable or, if empty, the identity column's value is used
	 *
	 * @return  static  Self, for chaining
	 *
	 * @throws  \RuntimeException  When the row is not found
	 */
	public function findOrFail($keys = null)
	{
		$this->find($keys);

		// We have to assign the value, since empty() is not triggering the __get magic method
		// http://stackoverflow.com/questions/2045791/php-empty-on-get-accessor
		$value = $this->getId();

		if (empty($value))
		{
			throw new \RuntimeException('Could not load record', 404);
		}

		return $this;
	}

	/**
	 * Find and load a single record based on the provided key values
	 *
	 * @param   array|mixed  $keys  An optional primary key value to load the row by, or an array of fields to match.
	 *                              If not set the "id" state variable or, if empty, the identity column's value is used
	 *
	 * @return  static  Self, for chaining
	 */
	public function find($keys = null)
	{
		// Execute the onBeforeLoad event
		if (method_exists($this, 'onBeforeLoad'))
		{
			$this->onBeforeLoad($keys);
		}

		$this->behavioursDispatcher->trigger('onBeforeLoad', [&$this, &$keys]);

		// If we are not given any keys, try to get the ID from the state or the table data
		if (empty($keys))
		{
			$id = $this->getState('id', 0);

			if (empty($id))
			{
				$id = $this->getId();
			}

			if (empty($id))
			{
				if (method_exists($this, 'onAfterLoad'))
				{
					$this->onAfterLoad(false);
				}

				$this->behavioursDispatcher->trigger('onAfterLoad', [&$this, false, $keys]);

				$this->reset();

				return $this;
			}

			$keys = [$this->idFieldName => $id];
		}
		elseif (!is_array($keys))
		{
			if (empty($keys))
			{
				if (method_exists($this, 'onAfterLoad'))
				{
					$this->onAfterLoad(false);
				}

				$this->behavioursDispatcher->trigger('onAfterLoad', [&$this, false, $keys]);

				$this->reset();

				return $this;
			}

			$keys = [$this->idFieldName => $keys];
		}

		// Reset the table
		$this->reset();

		// Get the query
		$db    = $this->getDbo();
		$query = $db->getQuery(true)
			->select('*')
			->from($db->qn($this->tableName));

		// Apply key filters
		foreach ($keys as $filterKey => $filterValue)
		{
			if ($filterKey == 'id')
			{
				$filterKey = $this->getIdFieldName();
			}

			if (array_key_exists($filterKey, $this->recordData))
			{
				$query->where($db->qn($filterKey) . ' = ' . $db->q($filterValue));
			}
		}

		// Get the row
		$db->setQuery($query);

		try
		{
			$row = $db->loadAssoc();
		}
		catch (\Exception $e)
		{
			$row = null;
		}

		if (empty($row))
		{
			if (method_exists($this, 'onAfterLoad'))
			{
				$this->onAfterLoad(false, $keys);
			}

			$this->behavioursDispatcher->trigger('onAfterLoad', [&$this, false, $keys]);

			return $this;
		}

		// Bind the data
		$this->bind($row);

		// Execute the onAfterLoad event
		if (method_exists($this, 'onAfterLoad'))
		{
			$this->onAfterLoad(true, $keys);
		}

		$this->behavioursDispatcher->trigger('onAfterLoad', [&$this, true, $keys]);

		return $this;
	}

	/**
	 * Create a new record with the provided data
	 *
	 * @param   array  $data  The data to use in the new record
	 *
	 * @return  static  Self, for chaining
	 */
	public function create($data)
	{
		return $this->reset()->bind($data)->save();
	}

	/**
	 * Return the first item found or create a new one based on the provided $data
	 *
	 * @param   array  $data  Data for the newly created item
	 *
	 * @return  static
	 */
	public function firstOrCreate($data)
	{
		$item = $this->get(true, 0, 1)->first();

		if (is_null($item))
		{
			$item = clone $this;
			$item->create($data);
		}

		return $item;
	}

	/**
	 * Return the first item found or throw a \RuntimeException
	 *
	 * @return  static
	 *
	 * @throws  \RuntimeException
	 */
	public function firstOrFail()
	{
		$item = $this->get(true, 0, 1)->first();

		if (is_null($item))
		{
			throw new \RuntimeException('No items found in ' . get_class($this));
		}

		return $item;
	}

	/**
	 * Return the first item found or create a new, blank one
	 *
	 * @return  static
	 */
	public function firstOrNew()
	{
		$item = $this->get(true, 0, 1)->first();

		if (is_null($item))
		{
			$item = clone $this;
			$item->reset();
		}

		return $item;
	}

	/**
	 * Adds a behaviour by its name. It will search the following classes, in this order:
	 * \appName\Model\modelName\Behaviour\behaviourName
	 * \appName\Model\DataModel\Behaviour\behaviourName
	 * \Awf\Mvc\DataModel\Behaviour\behaviourName
	 * where:
	 * appName            is the application's name, first character uppercase, e.g. Foo
	 * modelName        is the model's name, first character uppercase, e.g. Baz
	 * behaviourName    is the $behaviour parameter, first character uppercase, e.g. Something
	 *
	 * @param   string  $behaviour  The behaviour's name
	 *
	 * @return  $this  Self, for chaining
	 */
	public function addBehaviour($behaviour)
	{
		$prefixes = [
			'\\' . ucfirst($this->container->application->getName()) . '\\Model\\' . ucfirst($this->getName()) . '\\Behaviour',
			'\\' . ucfirst($this->container->application->getName()) . '\\Model\\DataModel\\Behaviour',
			'\\Awf\\Mvc\\DataModel\\Behaviour',
		];

		foreach ($prefixes as $prefix)
		{
			$className = $prefix . '\\' . ucfirst($behaviour);

			if (class_exists($className, true))
			{
				/** @var \Awf\Event\Observer $o */
				$observer = new $className($this->behavioursDispatcher);
				$this->behavioursDispatcher->attach($observer);

				return $this;
			}
		}

		return $this;
	}

	/**
	 * Gives you access to the behaviours dispatcher, allowing to attach/detach behaviour observers
	 *
	 * @return \Awf\Event\Dispatcher
	 */
	public function &getBehavioursDispatcher()
	{
		return $this->behavioursDispatcher;
	}

	/**
	 * Set the field and direction of ordering for the query returned by buildQuery.
	 * Alias of $this->setState('filter_order', $fieldName) and $this->setState('filter_order_Dir', $direction)
	 *
	 * @param   string  $fieldName  The field name to order by
	 * @param   string  $direction  The direction to order by (ASC for ascending or DESC for descending)
	 *
	 * @return  $this  For chaining
	 */
	public function orderBy($fieldName, $direction = 'ASC')
	{
		$direction = strtoupper($direction);

		if (!in_array($direction, ['ASC', 'DESC']))
		{
			$direction = 'ASC';
		}

		$this->setState('filter_order', $fieldName);
		$this->setState('filter_order_Dir', $direction);

		return $this;
	}

	/**
	 * Set the limitStart for the query, i.e. how many records to skip.
	 * Alias of $this->setState('limitstart', $limitStart);
	 *
	 * @param   integer  $limitStart  Records to skip from the start
	 *
	 * @return  $this  For chaining
	 */
	public function skip($limitStart = null)
	{
		// Only positive integers are allowed
		if (!is_int($limitStart) || $limitStart < 0 || !$limitStart)
		{
			$limitStart = 0;
		}

		$this->setState('limitstart', $limitStart);

		return $this;
	}

	/**
	 * Set the limit for the query, i.e. how many records to return.
	 * Alias of $this->setState('limit', $limit);
	 *
	 * @param   integer  $limit  Maximum number of records to return
	 *
	 * @return  $this  For chaining
	 */
	public function take($limit = null)
	{
		// Only positive integers are allowed
		if (!is_int($limit) || $limit < 0 || !$limit)
		{
			$limit = 0;
		}

		$this->setState('limit', $limit);

		return $this;
	}

	/**
	 * Return the record's data as an array
	 *
	 * @return  array
	 */
	public function toArray()
	{
		return $this->recordData;
	}

	/**
	 * Returns the record's data as a JSON string
	 *
	 * @param   boolean  $prettyPrint  Should I format the JSON for pretty printing
	 *
	 * @return  string
	 */
	public function toJson($prettyPrint = false)
	{
		if (defined('JSON_PRETTY_PRINT'))
		{
			$options = $prettyPrint ? JSON_PRETTY_PRINT : 0;
		}
		else
		{
			$options = 0;
		}

		return json_encode($this->recordData, $options);
	}

	/**
	 * Touch a record, updating its modified_on and/or modified_by columns
	 *
	 * @param   integer  $userId  Optional user ID of the user touching the record
	 *
	 * @return  $this  Self, for chaining
	 */
	public function touch($userId = null)
	{
		if (!$this->getId())
		{
			throw new RecordNotLoaded("Can't touch a not loaded DataModel");
		}

		if (!$this->hasField('modified_on') && !$this->hasField('modified_by'))
		{
			return $this;
		}

		$db   = $this->getDbo();
		$date = new Date();

		// Update the created_on / modified_on
		if ($this->hasField('modified_on'))
		{
			$modified_on        = $this->getFieldAlias('modified_on');
			$this->$modified_on = $date->toSql(false, $db);
		}

		// Update the created_by / modified_by values if necessary
		if ($this->hasField('modified_by'))
		{
			if (empty($userId))
			{
				$userManager = $this->container->userManager;
				$userId      = $userManager->getUser()->getId();
			}

			$modified_by        = $this->getFieldAlias('modified_by');
			$this->$modified_by = $userId;
		}

		$this->save();

		return $this;
	}

	/**
	 * Lock a record by setting its locked_on and/or locked_by columns
	 *
	 * @param   integer  $userId
	 *
	 * @return  $this  Self, for chaining
	 */
	public function lock($userId = null)
	{
		if (!$this->getId())
		{
			throw new \RuntimeException("Can't lock a not loaded DataModel");
		}

		if (!$this->hasField('locked_on') && !$this->hasField('locked_by'))
		{
			return $this;
		}

		if (method_exists($this, 'onBeforeLock'))
		{
			$this->onBeforeLock();
		}

		$this->behavioursDispatcher->trigger('onBeforeLock', [&$this]);

		$db = $this->getDbo();

		if ($this->hasField('locked_on'))
		{
			$date             = new Date();
			$locked_on        = $this->getFieldAlias('locked_on');
			$this->$locked_on = $date->toSql(false, $db);
		}

		if ($this->hasField('locked_by'))
		{
			if (empty($userId))
			{
				$userManager = $this->container->userManager;
				$userId      = $userManager->getUser()->getId();
			}

			$locked_by        = $this->getFieldAlias('locked_by');
			$this->$locked_by = $userId;
		}

		$this->save();

		if (method_exists($this, 'onAfterLock'))
		{
			$this->onAfterLock();
		}

		$this->behavioursDispatcher->trigger('onAfterLock', [&$this]);

		return $this;
	}

	/**
	 * Unlock a record by resetting its locked_on and/or locked_by columns
	 *
	 * @return  $this  Self, for chaining
	 */
	public function unlock()
	{
		if (!$this->getId())
		{
			throw new RecordNotLoaded("Can't unlock a not loaded DataModel");
		}

		if (!$this->hasField('locked_on') && !$this->hasField('locked_by'))
		{
			return $this;
		}

		if (method_exists($this, 'onBeforeUnlock'))
		{
			$this->onBeforeUnlock();
		}

		$this->behavioursDispatcher->trigger('onBeforeUnlock', [&$this]);

		$db = $this->getDbo();

		if ($this->hasField('locked_on'))
		{
			$locked_on        = $this->getFieldAlias('locked_on');
			$this->$locked_on = $this->isNullableField('locked_on') ? null : $db->getNullDate();
		}

		if ($this->hasField('locked_by'))
		{
			$locked_by        = $this->getFieldAlias('locked_by');
			$this->$locked_by = 0;
		}

		$this->save();

		if (method_exists($this, 'onAfterUnlock'))
		{
			$this->onAfterUnlock();
		}

		$this->behavioursDispatcher->trigger('onAfterUnlock', [&$this]);

		return $this;
	}

	/**
	 * Automatically uses the Filters behaviour to filter records in the model based on your criteria.
	 *
	 * @param   string  $fieldName  The field name to filter on
	 * @param   string  $method     The filtering method, e.g. <>, =, != and so on
	 * @param   mixed   $values     The value you're filtering on. Some filters (e.g. interval or between) require an
	 *                              array of values
	 *
	 * @return  $this  For chaining
	 */
	public function where($fieldName, $method = '=', $values = null)
	{
		// Make sure the Filters behaviour is added to the model
		if (!$this->behavioursDispatcher->hasObserverClass('Awf\Mvc\DataModel\Behaviour\Filters'))
		{
			$this->addBehaviour('filters');
		}

		// If we are dealing with the primary key, let's set the field name to "id". This is a convention and it will
		// be used inside the Filters behaviour
		if ($fieldName == $this->getIdFieldName())
		{
			$fieldName = 'id';
		}

		$options = [
			'method' => $method,
			'value'  => $values,
		];

		// Handle method aliases
		switch ($method)
		{
			case '<>':
				$options['method']   = 'search';
				$options['operator'] = '!=';
				break;

			case 'lt':
				$options['method']   = 'search';
				$options['operator'] = '<';
				break;

			case 'le':
				$options['method']   = 'search';
				$options['operator'] = '<=';
				break;

			case 'gt':
				$options['method']   = 'search';
				$options['operator'] = '>';
				break;

			case 'ge':
				$options['method']   = 'search';
				$options['operator'] = '>=';
				break;

			case 'eq':
				$options['method']   = 'search';
				$options['operator'] = '=';
				break;

			case 'neq':
			case 'ne':
				$options['method']   = 'search';
				$options['operator'] = '!=';
				break;

			case '<':
			case '!<':
			case '<=':
			case '!<=':
			case '>':
			case '!>':
			case '>=':
			case '!>=':
			case '!=':
			case '=':
				$options['method']   = 'search';
				$options['operator'] = $method;
				break;

			case 'like':
			case '~':
			case '%':
				$options['method'] = 'partial';
				break;

			case '==':
			case '=[]':
			case '=()':
			case 'in':
				$options['method'] = 'exact';
				break;

			case '()':
			case '[]':
			case '[)':
			case '(]':
				$options['method'] = 'between';
				break;

			case ')(':
			case ')[':
			case '](':
			case '][':
				$options['method'] = 'outside';
				break;

			case '*=':
			case 'every':
				$options['method'] = 'interval';
				break;

			case '?=':
				$options['method'] = 'search';
				break;

			default:

				throw new InvalidSearchMethod('Method ' . $method . ' is unsupported');

				break;
		}

		// Handle real methods
		switch ($options['method'])
		{
			case 'between':
			case 'outside':
				if (is_array($values) && (count($values) > 1))
				{
					// Get the from and to values from the $values array
					if (isset($values['from']) && isset($values['to']))
					{
						$options['from'] = $values['from'];
						$options['to']   = $values['to'];
					}
					else
					{
						$options['from'] = array_shift($values);
						$options['to']   = array_shift($values);
					}

					unset($options['value']);
				}
				else
				{
					// $values is not a from/to array. Treat as = (between) or != (outside)
					if (is_array($values))
					{
						$values = array_shift($values);
					}

					$options['operator'] = ($options['method'] == 'between') ? '=' : '!=';
					$options['value']    = $values;
					$options['method']   = 'search';
				}

				break;

			case 'interval':
				if (is_array($values) && (count($values) > 1))
				{
					// Get the value and interval from the $values array
					if (isset($values['value']) && isset($values['interval']))
					{
						$options['value']    = $values['value'];
						$options['interval'] = $values['interval'];
					}
					else
					{
						$options['value']    = array_shift($values);
						$options['interval'] = array_shift($values);
					}
				}
				else
				{
					// $values is not a value/interval array. Treat as =
					if (is_array($values))
					{
						$values = array_shift($values);
					}

					$options['value']    = $values;
					$options['method']   = 'search';
					$options['operator'] = '=';
				}
				break;

			case 'search':
				// We don't have to do anything if the operator is already set
				if (isset($options['operator']))
				{
					break;
				}

				if (is_array($values) && (count($values) > 1))
				{
					// Get the operator and value from the $values array
					if (isset($values['operator']) && isset($values['value']))
					{
						$options['operator'] = $values['operator'];
						$options['value']    = $values['value'];
					}
					else
					{
						$options['operator'] = array_shift($values);
						$options['value']    = array_shift($values);
					}
				}
				break;
		}

		$this->setState($fieldName, $options);

		return $this;
	}

	/**
	 * Add custom, pre-compiled WHERE clauses for use in buildQuery. The raw WHERE clause you specify is added as is to
	 * the query generated by buildQuery. You are responsible for quoting and escaping the field names and data found
	 * inside the WHERE clause.
	 *
	 * Using this method is a generally bad idea. You are better off overriding buildQuery and using state variables to
	 * customise the query build built instead of using this method to push raw SQL to the query builder. Mixing your
	 * business logic with raw SQL makes your application harder to maintain and refactor as dependencies to your
	 * database schema creep in areas of your code that should have nothing to do with it.
	 *
	 * @param   string  $rawWhereClause  The raw WHERE clause to add
	 *
	 * @return  $this  For chaining
	 */
	public function whereRaw($rawWhereClause)
	{
		$this->whereClauses[] = $rawWhereClause;

		return $this;
	}

	/**
	 * Instructs the model to eager load the specified relations. The $relations array can have the format:
	 *
	 * array('relation1', 'relation2')
	 *        Eager load relation1 and relation2 without any callbacks
	 * array('relation1' => $callable1, 'relation2' => $callable2)
	 *        Eager load relation1 with callback $callable1 etc
	 * array('relation1', 'relation2' => $callable2)
	 *        Eager load relation1 without a callback, relation2 with callback $callable2
	 *
	 * The callback must have the signature function(\Awf\Database\Query $query) and doesn't return a value. It is
	 * supposed to modify the query directly.
	 *
	 * Please note that eager loaded relations produce their queries without going through the respective model. Instead
	 * they generate a SQL query directly, then map the loaded results into a DataCollection.
	 *
	 * @param   array  $relations  The relations to eager load. See above for more information.
	 *
	 * @return $this For chaining
	 */
	public function with(array $relations)
	{
		if (empty($relations))
		{
			$this->eagerRelations = [];

			return $this;
		}

		$knownRelations = $this->relationManager->getRelationNames();

		foreach ($relations as $k => $v)
		{
			if (is_callable($v))
			{
				$relName  = $k;
				$callback = $v;
			}
			else
			{
				$relName  = $v;
				$callback = null;
			}

			if (in_array($relName, $knownRelations))
			{
				$this->eagerRelations[$relName] = $callback;
			}
		}

		return $this;
	}

	/**
	 * Filter the model based on the fulfilment of relations. For example:
	 * $posts->has('comments', '>=', 10)->get();
	 * will return all posts with at least 10 comments.
	 *
	 * @param   string  $relation  The relation to query
	 * @param   string  $operator  The comparison operator. Same operators as the where() method.
	 * @param   mixed   $value     The value(s) to compare against.
	 * @param   bool    $replace   When true (default) any existing relation filters for the same relation will be
	 *                             replaced
	 *
	 * @return $this
	 */
	public function has($relation, $operator = '>=', $value = 1, $replace = true)
	{
		// Make sure the Filters behaviour is added to the model
		if (!$this->behavioursDispatcher->hasObserverClass('Awf\Mvc\DataModel\Behaviour\RelationFilters'))
		{
			$this->addBehaviour('relationFilters');
		}

		$filter = [
			'relation' => $relation,
			'method'   => $operator,
			'operator' => $operator,
			'value'    => $value,
		];

		// Handle method aliases
		switch ($operator)
		{
			case '<>':
				$filter['method']   = 'search';
				$filter['operator'] = '!=';
				break;

			case 'lt':
				$filter['method']   = 'search';
				$filter['operator'] = '<';
				break;

			case 'le':
				$filter['method']   = 'search';
				$filter['operator'] = '<=';
				break;

			case 'gt':
				$filter['method']   = 'search';
				$filter['operator'] = '>';
				break;

			case 'ge':
				$filter['method']   = 'search';
				$filter['operator'] = '>=';
				break;

			case 'eq':
				$filter['method']   = 'search';
				$filter['operator'] = '=';
				break;

			case 'neq':
			case 'ne':
				$filter['method']   = 'search';
				$filter['operator'] = '!=';
				break;

			case '<':
			case '!<':
			case '<=':
			case '!<=':
			case '>':
			case '!>':
			case '>=':
			case '!>=':
			case '!=':
			case '=':
				$filter['method']   = 'search';
				$filter['operator'] = $operator;
				break;

			case 'like':
			case '~':
			case '%':
				$filter['method'] = 'partial';
				break;

			case '==':
			case '=[]':
			case '=()':
			case 'in':
				$filter['method'] = 'exact';
				break;

			case '()':
			case '[]':
			case '[)':
			case '(]':
				$filter['method'] = 'between';
				break;

			case ')(':
			case ')[':
			case '](':
			case '][':
				$filter['method'] = 'outside';
				break;

			case '*=':
			case 'every':
				$filter['method'] = 'interval';
				break;

			case '?=':
				$filter['method'] = 'search';
				break;

			case 'callback':
				$filter['method']   = 'callback';
				$filter['operator'] = 'callback';
				break;

			default:
				throw new InvalidSearchMethod('Operator ' . $operator . ' is unsupported');
				break;
		}

		// Handle real methods
		switch ($filter['method'])
		{
			case 'between':
			case 'outside':
				if (is_array($value) && (count($value) > 1))
				{
					// Get the from and to values from the $value array
					if (isset($value['from']) && isset($value['to']))
					{
						$filter['from'] = $value['from'];
						$filter['to']   = $value['to'];
					}
					else
					{
						$filter['from'] = array_shift($value);
						$filter['to']   = array_shift($value);
					}

					unset($filter['value']);
				}
				else
				{
					// $value is not a from/to array. Treat as = (between) or != (outside)
					if (is_array($value))
					{
						$value = array_shift($value);
					}

					$filter['operator'] = ($filter['method'] == 'between') ? '=' : '!=';
					$filter['value']    = $value;
					$filter['method']   = 'search';
				}

				break;

			case 'interval':
				if (is_array($value) && (count($value) > 1))
				{
					// Get the value and interval from the $value array
					if (isset($value['value']) && isset($value['interval']))
					{
						$filter['value']    = $value['value'];
						$filter['interval'] = $value['interval'];
					}
					else
					{
						$filter['value']    = array_shift($value);
						$filter['interval'] = array_shift($value);
					}
				}
				else
				{
					// $value is not a value/interval array. Treat as =
					if (is_array($value))
					{
						$value = array_shift($value);
					}

					$filter['value']    = $value;
					$filter['method']   = 'search';
					$filter['operator'] = '=';
				}
				break;

			case 'search':
				// We don't have to do anything if the operator is already set
				if (isset($filter['operator']))
				{
					break;
				}

				if (is_array($value) && (count($value) > 1))
				{
					// Get the operator and value from the $value array
					if (isset($value['operator']) && isset($value['value']))
					{
						$filter['operator'] = $value['operator'];
						$filter['value']    = $value['value'];
					}
					else
					{
						$filter['operator'] = array_shift($value);
						$filter['value']    = array_shift($value);
					}
				}
				break;

			case 'callback':
				if (!is_callable($filter['value']))
				{
					$filter['method']   = 'search';
					$filter['operator'] = '=';
					$filter['value']    = 1;
				}
				break;
		}

		if ($replace && !empty($this->relationFilters))
		{
			foreach ($this->relationFilters as $k => $v)
			{
				if ($v['relation'] == $relation)
				{
					unset ($this->relationFilters[$k]);
				}
			}
		}

		$this->relationFilters[] = $filter;

		return $this;
	}

	/**
	 * Advanced model filtering on the fulfilment of relations. Unlike has() you can provide your own callback which
	 * modifies the COUNT subquery used to compare against the relation. The $callBack has the signature
	 * function(\Awf\Database\Query $query)
	 * and MUST return a string. The $query you are passed is the COUNT subquery of the relation, e.g.
	 * SELECT COUNT(*) FROM #__comments AS reltbl WHERE reltbl.user_id = user_id
	 * You have to return a WHERE clause for the model's query, e.g.
	 * (SELECT COUNT(*) FROM #__comments AS reltbl WHERE reltbl.user_id = user_id) BETWEEN 1 AND 20
	 *
	 * @param   string    $relation  The relation to query against
	 * @param   callable  $callBack  The callback to use for filtering
	 * @param   bool      $replace   When true (default) any existing relation filters for the same relation will be
	 *                               replaced
	 *
	 * @return $this
	 */
	public function whereHas($relation, $callBack, $replace = true)
	{
		$this->has($relation, 'callback', $callBack, $replace);

		return $this;
	}

	/**
	 * Returns the relations manager of the model
	 *
	 * @return RelationManager
	 */
	public function &getRelations()
	{
		return $this->relationManager;
	}

	/**
	 * Gets the relation filter definitions, for use by the RelationFilters behaviour
	 *
	 * @return array
	 */
	public function getRelationFilters()
	{
		return $this->relationFilters;
	}

	/**
	 * Returns the list of relations which are touched by save() and touch()
	 *
	 * @return array
	 */
	public function &getTouches()
	{
		return $this->touches;
	}

	/**
	 * Returns all lower and upper case permutations of the database prefix
	 *
	 * @return  array
	 */
	protected function getPrefixCasePermutations()
	{
		if (empty(self::$prefixCasePermutations))
		{
			$prefix = $this->getDbo()->getPrefix();
			$suffix = '';

			if (substr($prefix, -1) == '_')
			{
				$suffix = '_';
				$prefix = substr($prefix, 0, -1);
			}

			$letters      = str_split($prefix, 1);
			$permutations = [''];

			foreach ($letters as $nextLetter)
			{
				$lower = strtolower($nextLetter);
				$upper = strtoupper($nextLetter);
				$ret   = [];

				foreach ($permutations as $perm)
				{
					$ret[] = $perm . $lower;

					if ($lower != $upper)
					{
						$ret[] = $perm . $upper;
					}

					$permutations = $ret;
				}
			}

			$permutations = array_merge([
				strtolower($prefix),
				strtoupper($prefix),
			], $permutations);
			$permutations = array_map(function ($x) use ($suffix) {
				return $x . $suffix;
			}, $permutations);

			self::$prefixCasePermutations = array_unique($permutations);
		}

		return self::$prefixCasePermutations;
	}
}
