<?php
App::uses('Model', 'Model');
App::uses('Stash', 'Model');
App::uses('Upload', 'Model');
App::uses('Module', 'Model');
App::uses('Folder', 'Utility');
App::uses('Runner', 'Cronner.Handler');

App::uses('AssociatedSetBehavior', 'Model/Behavior');

/**
 * Class AppModel
 * @method \ModuleRelationships\Definition[] getRelationships
 * @method \ModuleRelationships\Definition getRelationship
 * @method array getRelated
 *
 * @property array $relationships The list of all relationships.
 */
class AppModel extends Model {

	// Approval statuses
	const STATUS_PENDING = 'Pending';
	const STATUS_ACCEPTED = 'Accepted';
	const STATUS_REJECTED = 'Rejected';
	const STATUS_POSTPONED = 'Postponed';

	var $actsAs = [];

	var $orderingGroups = [];

	var $isMultiple = false;

	var $polyglotFields = [];
	var $sideFields = [];

	var $_locked = [ 'modified_by', 'created_by', 'modified', 'created', 'assigned_to', 'owned_by', 'slug', 'counter', 'is_translated', 'meta_description', 'meta_title', 'meta_image', 'meta_keyword', 'meta', 'seo_metatags', 'seo_generated_metatags', 'head_end', 'body_start', 'body_end', 'priority', 'change_frequency' ];
	var $_invisible = [ 'id', 'model', 'parent', 'lft', 'rght', 'assigned_to', 'owned_by', 'meta' ];
	var $_invisibleOnEdit = [ 'id', 'model', 'parent', 'lft', 'rght', 'assigned_to', 'owned_by', 'meta' ];

	var $restrictions = [];

	var $singles = [];

	var $hasSet = [];

	var $moduleId = false;

	var $exportEnabled = true;

	var $ignoreApproval = false;

	# Reserved fields
	var $reserved = [ 'Fields' => [ 'id', 'category_id', 'page_id', 'slug', 'status', 'ordering', 'is_active', 'is_deleted', 'is_translated', 'approval_status', 'model', 'parent', 'lft', 'rght', 'counter', 'modified_by', 'created_by', 'modified', 'created', 'assigned_to', 'owned_by',

		# Seo
		'meta_title', 'meta_keyword', 'meta_description', 'meta', 'change_frequency', 'priority', 'seo_metatags', 'seo_generated_metatags', 'head_end', 'body_start', 'body_end' ] ];

	# List of all possible display fields and images, ordered by priority
	var $displayImages = [ 'Image', 'MainImage', 'Photo', 'MainPhoto', 'Logo' ];
	var $displayFields = [ 'name', 'full_name', 'username', 'title', 'main_title', 'last_name', 'first_name', 'surname', 'caption', 'label', 'email', 'number', 'author', 'question', 'url' ];

	# Models that are excluded from Log
	var $logExcludeList = [ 'Log', 'LogDetails', 'LogApi', 'Dictionary', 'Content', 'Block', 'AppModel', 'BackgroundJobLog', 'UrlLog', 'Visitor' ];

	# Default validation rules
	var $validate = [];

	# Default values
	public $cache = true;              // Enable or disable caching engin.

	public $isCopyable = true;         // Allow or deny non-superadmin administrator to copy records.
	public $isEditable = true;         // Allow or deny non-superadmin administrator to edit records.
	public $isHardcoded = false;       // Allow or deny non-superadmin administrator to delete or add records.
	public $isSingleItem = false;      // Allow or deny adding more than one item.

	public $slugSource = null;         // Set to null to inherit from displayField.
	public $displayField = null;       // Set to null for auto value.
	public $displayImage = null;       // Set to null for auto value.

	public $paginate = 20;             // Items per page on index view
	public $locked = [];               // Fields that cannot be edited by non-superadmin administrators.
	public $invisible = [];            // Fields that are invisible to non-superadmin administrators.
	public $invisibleOnEdit = [];      // Fields that are invisible to non-superadmin administrators on edit.
	public $readOnly = [];             // Fields that are not editable.
	public $skipFilter = [];           // Fields that will not be visible in filter

	public $tabs = [];                 // Tabs
	public $fieldTabs = [];            // Fields <=> tab relation
	public $elementTabs = [];          // Elements <=> tab relation

	public $preventCallbacks = false;

	/**
	 * Default constructor.
	 */
	function __construct($id = false, $table = null, $ds = null) {
		Timer::startLoop("AppModel::__construct()");
		{

			$this->name = get_class($this);

			# Set default behaviors
			if (empty($this->actsAs))
				$this->actsAs = [];
			array_unshift($this->actsAs, 'Containable');
			if (!in_array($this->name, $this->logExcludeList)) {
				if (!$this->preventLogging()) {
					array_unshift($this->actsAs, 'Logged');
				}
			}
			array_unshift($this->actsAs, 'Default');

			# SEO optimizations
			if ($this->isModule && $this->hasDetails) {
				$this->actsAs[] = 'Seo.LastMod';
				$this->actsAs[] = 'Seo.MetaTagged';
				$this->actsAs[] = 'Seo.MetaGenerated';
				$this->actsAs[] = 'Seo.SlugLogged';
				$this->hasOne['MetaImage'] = 'Upload jpeg jpg png gif';
			}

			# Easy tabs in form
			if (method_exists($this, 'initTabs')) {
				$tabs = $this->initTabs($id);
				if (is_array($tabs) && !empty($tabs)) {
					foreach ($tabs as $tab => $fields) {
						if (is_string($fields)) {
							$this->fieldTabs[$tab] = $fields;
							$this->elementTabs[$tab] = $fields;
						} else {
							foreach ($fields as $oneField) {
								$this->fieldTabs[$oneField] = $tab;
							}
						}
						$this->tabs[] = $tab;
					}
				} else {
					$this->log("Wrongly set initTabs() method in {$this->name} model.", 'warning');
				}
			}

			# Easy side fields
			if (method_exists($this, 'initSideFields')) {
				$this->sideFields = array_merge($this->initSideFields(), $this->sideFields);
			}

			# Allow uploads to be set with one word
			foreach ([ 'hasOne', 'hasMany' ] as $type) {
				foreach ($this->$type as $alias => $params) {

					# Allow uploads to be set with one word
					if ((is_string($params) && substr($params, 0, 6) == 'Upload')) {

						# Check for polyglot
						$polyglot = substr($alias, -2) === '__';
						if ($polyglot) {
							unset($this->{$type}[$alias]);
							$alias = substr($alias, 0, -2);
						}

						# Create valid association
						$detachedComments = explode(' | ', $params);
						$explode = explode(' ', $detachedComments[0]);
						$this->{$type}[$alias] = $params = array_merge(
							[
								'order'      => $alias . '.ordering ASC',
								'polyglot'   => $polyglot,
								'dependent'  => true,
								'className'  => 'Upload',
								'foreignKey' => 'foreign_key',
								'extensions' => is_string($params) ? array_splice($explode, 1) : [],
								'comment'    => count($detachedComments) > 1 ? $detachedComments[1] : '',
								'conditions' => [
									$alias . '.association' => $alias,
									$alias . '.model'       => $this->name,
									$alias . '.filename <>' => '',
									$alias . '.is_deleted'  => false ] ],
							(array) $params
						);

						Upload::$config[$this->name][$alias]['extensions'] = $params['extensions'];
					}

					# Multilingular associations
					if (is_array($params) && !empty($params['polyglot'])) {

						# Set new association
						$this->{$type}[$alias]['conditions']["{$alias}.locale"] = Configure::read('Config.language');

						if (CMS) {

							# Miltilingular associations for CMS
							$original = removeLocale($alias);
							foreach (Configure::read('Config.Languages') as $locale => $language) {
								$alias = "{$original}__{$locale}";
								$this->{$type}[$alias] = $params;
								$this->{$type}[$alias]['order'] = $alias . '.ordering ASC';
								$this->{$type}[$alias]['conditions'] = [
									$alias . '.association' => $original,
									$alias . '.model'       => $this->name,
									$alias . '.filename <>' => '',
									$alias . '.locale'      => $locale
								];
							}
						}
					}
				}
			}

			# Load relationships
			$relationships = $this->initRelationships();
			if (!empty($relationships)) {
				$this->actsAs['ModuleRelationships.Related'] = $relationships;
			}

			# Set up model variables
			$this->invisible = array_merge($this->_invisible, $this->invisible);
			$this->invisibleOnEdit = array_merge($this->_invisibleOnEdit, $this->invisibleOnEdit);
			$this->locked = array_merge($this->_locked, $this->locked, array_slice($this->invisible, 1));

			# Declare as under construction
			if (!empty($this->liveField)) {
				$this->actsAs[] = 'Live';
				$this->tablePrefix = 'live_';
			}

			parent::__construct($id, $table, $ds);

			# Use table fix
			if ($this->useTable === 'app_models' || empty($this->useTable)) {
				$this->useTable = Inflector::tableize($this->alias);
			}

			# Schema manipulation
			$i = 0;
			$this->_schema = $this->schema();

			foreach ($this->_schema as $field => $schema) {

				# Get field parameters
				if (!isset($schema['comment'])) {
					$schema['comment'] = null;
				}
				$schema['params'] = @array_filter((array) explode(' ', $schema['comment']));

				# Customize model schema for polyglot
				if ($pin = strpos($field, '__')) {
					$name = substr($field, 0, $pin);
					if (!in_array($name, $this->polyglotFields)) {
						$this->polyglotFields[] = $name;
						$tail = array_splice($this->_schema, $i, sizeof($this->_schema));
						$this->_schema[$name] = array_merge($schema, [ 'polyglot' => true ]);
						$this->_schema += $tail;
						$i++;
					}

					# Enum schema
				} else if (substr($schema['type'], 0, 4) == 'enum') {
					$this->_schema[$field]['type'] = 'enum';
					$enum = explode(',', substr($schema['type'], 5, -1));
					foreach ($enum as $option) {
						$this->_schema[$field]['enum'][substr($option, 1, -1)] = substr($option, 1, -1);
					}

					# Auto set-fields
				} else if (substr($field, -4) == '_set') {
					AssociatedSetBehavior::handleColumn($this, $field);

					# Hide cached fields
				} else if (substr($field, 0, 7) == 'cached_') {
					$this->invisible[] = $field;

					# Handle parent_id fields
				} else if ($field == 'parent_id') {

					if (!isset($this->belongsTo['Parent']) && !in_array('Parent', $this->belongsTo)) {

						# belongsTo Parent
						// TODO: Make this work
						if ($this->name != 'Page') {
							$this->bindModel([
								'belongsTo' => [
									'Parent' => [
										'className' => $this->alias ] ]
							]);
						}
					}

					if (!isset($this->hasMany['Children'])) {
						# hasMany Children
						$this->bindModel([ 'hasMany' => [ 'Children' => [ 'className' => $this->alias, 'conditions' => [], 'foreignKey' => $field ] ] ], false);
					}

					# Auto belongsTo
				} else if (substr($field, -3) == '_id' && $field != 'android_id') {
					$master = Inflector::camelize(preg_replace('~^cms_~', '', substr($field, 0, -3)));
					if (!isset($this->belongsTo[$master]) && !in_array($master, $this->belongsTo)) {
						$this->bindModel([
							'belongsTo' => [
								$master => [
									'className'  => $master,
									'conditions' => [],
									'foreignKey' => $field ] ]
						], false);
					}

					# Act as tree
				} else if ($field === 'lft') {
					$this->Behaviors->load('Tree');
				}

				# Ordering groups
				if (substr($field, -3) === '_id') {
					$this->orderingGroups[] = $field;
				}

				# Get field parameters
				if (!isset($schema['comment'])) {
					$schema['comment'] = null;
				}
				$this->_schema[$field]['params'] = @array_filter((array) explode(' ', $schema['comment']));

				# Index to properly insert polyglot fields
				$i++;
			}

			# Custom ordering groups
			if (method_exists($this, 'getOrderingGroups')) {
				$this->orderingGroups = array_merge($this->orderingGroups, $this->getOrderingGroups());
				$this->orderingGroups = array_unique($this->orderingGroups);
			}

			# Virtual fields for Polyglot
			$locale = Configure::read('Config.language');
			foreach ($this->polyglotFields as $field) {
				$this->virtualFields[$field] = "{$this->alias}.{$field}__{$locale}";
			}

			# Multilingular uploads
			foreach ([ 'hasOne', 'hasMany' ] as $association) {
				foreach ($this->$association as $alias => $params) {
					if (!empty($params['polyglot'])) {
						$this->{$association}[$alias]['conditions'] += [ "{$alias}.locale" => $locale ];
					}
				}
			}

			# Display fields
			foreach ($this->schema(true) as $field => $params) {
				if ($this->displayField != 'id') {
					continue;
				} else if (in_array($field, $this->displayFields)) {
					$this->displayField = $field;
				}
			}
			foreach ($this->hasOne as $association => $params) {
				if (!empty($this->displayImage)) {
					continue;
				} else if (in_array($association, $this->displayImages)) {
					$this->displayImage = $association;
				}
			}

			# Slug Source
			if (empty($this->slugSource)) {
				$this->slugSource = $this->getSlugSource();
			}

			# Single active fields
			if ($this->schema('is_default')) {
				$this->singles[] = 'is_default';
			}

			# Ordering
			if (empty($this->order)) {
				foreach ([ 'lft' => ASC, 'release_date' => DESC, 'start_date' => DESC, 'start' => DESC, 'date' => DESC, 'ordering' => ASC ] as $field => $direction) {
					if (!empty($this->_schema[$field])) {
						$this->order["{$this->alias}.{$field}"] = $direction;
						break;
					}
				}
			}

			# All uneditable models are considered hardcoded
			if (!$this->isEditable) {
				$this->isHardcoded = true;
			}
			if ($this->isHardcoded) {
				$this->isCopyable = false;
			}

			# Set
			if (!empty($this->hasSet)) {
				$this->hasSet = Set::normalize($this->hasSet);
				foreach ($this->hasSet as $association => $params) {
					$this->$association = ClassRegistry::init($association);
					$this->hasSet[$association] = array_merge([ 'className' => $association, 'conditions' => null ], (array) $this->hasSet[$association]);
				}
			}

			# Additional validation rules
			$schema = $this->schema();
			if (method_exists($this, 'initValidation')) {
				$this->validate = Set::merge($this->validate, $this->initValidation());
			}

			# Easy validation rules
			foreach ($this->validate as $field => $rules) {
				if (is_array($rules)) {
					foreach ($rules as $key => $options) {
						if (is_string($key) && is_string($options)) {
							$this->validate[$field][$key] = [
								'rule'    => $key,
								'message' => __($options)
							];
						}
					}
				}
			}

			# Custom initialization
			$this->init();

		}
		Timer::endLoop("AppModel::__construct()");
	}

	protected function getSlugSource() {
		return $this->displayField;
	}

	function invalidate($field, $value = true) {
		return parent::invalidate($field, __($value));
	}

	public function customIndexRestrictions() {
		return [];
	}

	public function getIndexConditions() {
		return [];
	}

	public function preventSidebar() {
		return false;
	}

	public function preventLogging() {
		return false;
	}

	public function getDefaultIndexOrder() {
		return [];
	}

	/**
	 * Save all data, including all related models.
	 *
	 * @param array $data    The data to save.
	 * @param array $options Additional options.
	 *
	 * @return boolean True on success, false otherwise.
	 */
	public function saveAll($data = null, $options = []) {

		// Remove upload placeholder
		if (key_exists(Upload::POST_PLACEHOLDER, $data)) {
			unset($data[Upload::POST_PLACEHOLDER]);
		}

		// Set the proper orderings
		foreach ($data as $model => $values) {
			if (key_exists($model, $this->hasMany) && $this->hasMany[$model]['className'] == 'Upload') {
				$ordering = 1;
				foreach ($values as $key => $upload) {
					$data[$model][$key]['ordering'] = $ordering++;
				}
			}
		}

		return parent::saveAll($data, $options);
	}

	# Returns the list of all fields that can be shown in index view #
	function getVisibleFields() {
		$fields = [];
		$visible = array_diff_key($this->schema(true), array_flip($this->invisible));
		foreach ($visible as $field => $schema) {
			if (isset($_SESSION['Auth']['Administrator']['customization'][$this->alias][$field]['ordering'])) {
				$ordering = $_SESSION['Auth']['Administrator']['customization'][$this->alias][$field]['ordering'];
				while (isset($fields[$ordering]))
					$ordering++;
				$fields[$ordering] = [ $field => $schema ];
				unset($visible[$field]);
			}
		}

		ksort($fields);
		foreach ($fields as $i => $field) {
			unset($fields[$i]);
			$fields[key($field)] = reset($field);
		}

		return array_merge($fields, $visible);
	}

	# Returns the list of all alegible fields #
	function getAvailableFields() {
		$skip = [ 'slug', 'modified', 'modified_by', 'created', 'created_by' ];
		return array_diff_key($this->schema(true), array_merge(array_flip($this->invisible), array_flip($skip)));
	}

	# ~ Create the model with default data - - - - - - - - - - - - - - - - - - - - #
	public function create($data = [], $filterKey = false) {
		$fresh = parent::create($data, $filterKey);

		# Attach content
		if ($this->schema('content')) {
			$fresh['Content'] = ClassRegistry::init('Content')->packContent([], $this->alias);
		}

		return $fresh;
	}

	# ~ Highlight path in tree - - - - - - - - - - - - - - - - - - - - - - - - - - #
	protected function highlightPath($tree, $field, $value, $marker = 'selected', &$marked = false) {
		foreach ($tree as &$data) {
			$model = key($data);
			$marked = false;
			if (isset($data[$model][$field]) && !strcasecmp($data[$model][$field], $value)) {
				$marked = true;
				$data[$model][$marker] = true;
				break;
			} else if (!empty($data['children'])) {
				$data['children'] = $this->highlightPath($data['children'], $field, $value, $marker, $marked);
				$data[$model][$marker] = (bool) Set::extract("/{$model}[{$marker}=1]", array_values($data['children']));
			}
		}
		unset($data);

		return $tree;
	}

	# ~ Highlight nodes in tree	 - - - - - - - - - - - - - - - - - - - - - - - - - #
	protected function highlightNodes($tree, $field, $path, $marker = 'selected', &$marked = false) {
		$value = array_shift($path);
		foreach ($tree as &$data) {
			$model = key($data);
			$marked = $marked || false;

			if (isset($data[$model][$field]) && !strcasecmp($data[$model][$field], $value)) {
				$marked = true;
				$data[$model][$marker] = true;
				$data['children'] = $this->highlightNodes($data['children'], $field, $path, $marker, $marked);
			}
		}

		return $tree;
	}

	/**
	 * Get the condition for the search across all textual fields of the module.
	 *
	 * @param string $value The value search for.
	 *
	 * @return array The created conditions.
	 */
	public function getSearchConditions($value) {
		$search = [];
		foreach ($this->_schema as $fieldName => $schema) {
			if (in_array($schema['type'], [ 'string', 'text' ])) {
				$search[$this->name . '.' . $fieldName . ' LIKE'] = "%{$value}%";
			}
		}

		return [ 'OR' => $search ];
	}

	/**
	 * Generates a slug from the supplied string.
	 *
	 * @param  source
	 *                  The string to be slugged.
	 * @param  target
	 *                  Name of the field to put the result.
	 *
	 * @return string
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function createSlug($source, $target, $fullSource = null, $locale = null) {

		# Check for overriden slug
		if (!empty($fullSource)) {
			if (!empty($locale) && isset($fullSource['overriden_slug__' . $locale]) && !empty($fullSource['overriden_slug__' . $locale])) {
				$source = $fullSource['overriden_slug__' . $locale];
			}

			if (empty($locale) && isset($fullSource['overriden_slug']) && !empty($fullSource['overriden_slug'])) {
				$source = $fullSource['overriden_slug'];
			}

		}

		# Valid slug source
		if (!empty($source)) {

			$slug = preg_replace('~\s+~', '-', trim($source));
			$slug = preg_replace('~-+~', '-', $slug);
			$slug = preg_replace('~[^-\p{L}0-9_]+~ u', '', $slug);

			# Invalid slug source
		} else {

			# Try to set the id as slug
			$slug = !empty($this->id) ? $this->id : microtime(true);
		}

		# Make sure that slug is unique in the database
		$tries = 10;
		while ($tries-- && $this->hasAny(array_merge([ $this->alias . '.' . $target => $slug, $this->alias . '.id <> ' => $this->id ], $this->getSlugConditions()))) {
			$slug .= '-';
		}
		if (!$tries)
			$slug = microtime(true);

		return $this->data[$this->alias][$target] = mb_strtolower($slug);
	}

	protected function getSlugConditions() {
		return [];
	}

	/**
	 * Clear all data from the table.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function truncate() {
		$this->query("TRUNCATE {$this->tablePrefix}{$this->useTable}");
	}

	/**
	 * Get the objects as found in the set field.
	 *
	 * @param mixed data Data that is holding the set information.
	 *
	 * @return mixed Objects as defined in the data set.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function getSet($data = null) {
		$data = !empty($data) ? $data : $this->data;
		foreach (array_keys($this->hasSet) as $association) {
			if (empty($data[$association])) {
				$data[$association] = [];
			}
		}
		if (isset($data[$this->alias])) {
			foreach ($data[$this->alias] as $field => $value) {
				if (substr($field, -4) == '_set' && !empty($value)) {
					$value = preg_replace('/[^0-9,]/', '', $value);
					$association = Inflector::classify(substr($field, 0, -4));
					$set = $this->$association->find('all', [ 'conditions' => "{$association}.id IN({$value})", 'recursive' => -1 ]);
					foreach ($set as $item) {
						$data[$association][] = $item[$association];
					}
				}
			}
		}
		return $data;
	}

	/**
	 * Get the definition of a field in database.
	 *
	 * @param    field
	 *                Set to true to get schema with polyglot fields.
	 *
	 * @return    MySQL field definition for the choose field.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function schema($field = false) {
		if ($field !== true) {
			return parent::schema($field);
		}

		$schema = parent::schema();
		foreach ($schema as $field => $params) {
			if (strpos($field, '__')) {
				unset($schema[$field]);
			}
		}
		return $schema;
	}

	/**
	 * Get all parents.
	 *
	 * @param    conditions
	 *                Conditions for a query.
	 * @param    spacer
	 *                Spacer used for flatting the image.
	 *
	 * @return    Flatten list for a specified conditions.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function getNodeList($conditions = null, $spacer = ' . . . ') {
		return $this->generateTreeList($conditions, null, null, $spacer, null);
	}

	/**
	 * Reassign all virtualFields.
	 *
	 * @param    locale
	 *                Language for which the virtual fields are created.
	 *                - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function setVirtual($locale = null) {

		# Default value
		if ($locale === null) {
			$locale = Configure::read('Config.language');
		}

		Configure::write('Config.language', $locale);
		CakeSession::write('Config.language', $locale);

		# Iterate and reassign
		foreach ($this->virtualFields as $alias => $field) {
			$this->virtualFields[$alias] = preg_replace('~__.+$~', "__{$locale}", $field);
		}
	}

	/**
	 * Clears the cache from the application.
	 *
	 * @params    cache
	 *                Internally used.
	 *
	 * @todo      clear all thumbnails as well
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	static public function clearCache($cache = null) {
		if (empty($cache)) {
			$cache = CACHE;
		}

		# Delete all files in cache folder
		$files = scandir($cache);
		foreach ($files as $filename)
			if (isset($filename{0}) && $filename{0} != '.') {
				$file = $cache . $filename;
				if (is_file($file)) {
					@unlink($file);
				} else if (is_dir($file)) {
					self::clearCache($file . '/');
				}
			}

		# Clear APC cache
		if (extension_loaded('apc')) {
			apc_clear_cache();
		}

		# Clear CakePHP cache as well, for just in case
		if ($cache === CACHE) {
			clearCache();
			Cache::clear();
		}

		# Clear all cake cache
		Cache::clear(false, 'default');
		Cache::clear(false, '_cake_core_');
		Cache::clear(false, '_cake_model_');
		Cache::clear(false, 'format');

		// Files: Temporal upload
		(new \Intellex\Filesystem\Dir(TMP . 'upload'))->clear([ '~.empty$~' ]);

		// Files: Thumbnails
		(new \Intellex\Filesystem\Dir(WWW_ROOT . 'thumbs'))->clear([ '~.empty$~' ]);
	}

	/**
	 * Refresh ACL table.
	 *
	 * @params    cache
	 *                Internally used.
	 *
	 * @todo      clear all thumbnails as well
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	static public function refreshAcl() {
		$acos = [ [ 'alias' => 'controllers' ] ];

		# Default access control objects
		$controllers = [ 'Pages', 'Layouts', 'Elements', 'Stylesheets', 'Javascripts', 'Configs', 'Languages', 'Administrators', 'Categories', 'Tools', 'Modules' ];
		foreach ($controllers as $controller)
			if ($controller != 'App') {
				$acos[] = [ 'parent_id' => 1, 'alias' => $controller ];
			}

		# Put modules as access control objects
		$modules = ClassRegistry::init('Module')->find('list');
		foreach ($modules as $module) {
			$acos[] = [ 'parent_id' => 1, 'alias' => Inflector::camelize(Inflector::tableize($module)) ];
		}

		# Build the database
		$Aco = ClassRegistry::init('Aco');
		$Aco->query("TRUNCATE `{$Aco->useTable}`");
		$Aco->saveAll($acos);
	}

	/**
	 * Repair ordering, tree structure and MySQL recovery. Also clears cache.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function repair() {

		# Avoid recursion
		if (!empty($this->repaired)) {
			return false;
		}
		$this->repaired = true;

		Timer::start("Repairing model: {$this->alias}");
		{

			# Repair ordering
			Timer::start('Ordering');
			{
				if ($this->schema('ordering')) {

					# Prepare fields
					$group = null;
					$fields = [ 'id', 'ordering' ];
					$order = $this->order;

					# Find grouping field, if any
					foreach ($this->orderingGroups as $fieldName) {
						if ($this->schema($fieldName)) {
							$group = -1;
							$order = [ "{$this->alias}.{$fieldName}" => ASC ] + $order;
							$fields[] = $fieldName;
							break;
						}
					}

					# Make sure that table has any records
					$list = $this->find('all', compact('fields', 'order'));

					if ($list) {
						$i = 1;
						$records = [];
						foreach ($list as $record) {

							# Make sure we perform grouping by grouping field
							if ($group !== null && $record[$this->alias][$fieldName] != $group) {
								$i = 1;
								$group = $record[$this->alias][$fieldName];
							}

							# Set new ordering
							$records[] = [ 'id' => $record[$this->alias]['id'], 'ordering' => $i++ ];
						}
						$this->skipAutoOrdering = true;
						$this->saveAll($records);
					}
				}
			}
			Timer::end('Ordering');

			# Repair tree
			if ($this->schema('parent_id')) {
				Timer::start('Tree');
				{
					$this->recover();
				}
				Timer::end('Tree');
			}

			# Re-save
			if ($this->schema('slug') || $this->schema('slug__' . Configure::read('Config.language'))) {
				$records = $this->find('all');
				foreach ($records as $record) {

					# Make sure we do not have the slug
					if (!empty($record[$this->alias]['slug__']) || !empty($record[$this->alias]['slug'])) {
						continue;
					}

					# Save
					$record[$this->alias]['modified'] = false;
					$this->save($record);

					# Callback
					if (method_exists($this, 'afterSaveAll')) {
						$this->afterSaveAll($this->request->data);
					}
				}
			}

		}
		Timer::end("Repairing model: {$this->alias}");

		$this->clearCache();
	}

	# ~ Populate module with test values - - - - - - - - - - - - - - - - - - - - - #
	function populate($count = 7) {

		# Allowed only for CMS modules
		if (!$this->moduleId) {
			return false;
		}

		# Initialize LoremIpsumGenerator
		require_once(VECTORCMS_ROOT . '/Vendor/lorem_ipsum/lorem_ipsum.php');
		$loremIpsum = new LoremIpsumGenerator();

		# Predefined values
		$predefined = [
			'is_deleted'      => false,
			'is_active'       => true,
			'approval_status' => 'Accepted',
			'url'             => [ 'http://www.google.com', 'http://www.yahoo.com', 'http://www.microsoft.com', 'http://www.intellex.rs', 'http://www.apple.com' ],
			'website'         => [ 'http://www.google.com', 'http://www.yahoo.com', 'http://www.microsoft.com', 'http://www.intellex.rs', 'http://www.apple.com' ],
			'website address' => [ 'http://www.google.com', 'http://www.yahoo.com', 'http://www.microsoft.com', 'http://www.intellex.rs', 'http://www.apple.com' ],
			'email'           => [ 'ivan@intellex.rs', 'sabo@intellex.rs', 'ivan.sabo@intellex.rs', 'ivansabo3@gmail.com' ],
		];

		# Skip these fields
		$ignoreList = [
			'id', 'modified', 'modified_by', 'created', 'created_by',
			'ordering', 'slug',
			'meta_description', 'meta_keyword', 'meta_title', 'meta',
			'change_frequency', 'priority', 'seo_metatags', 'seo_generated_metatags',
			'head_end', 'body_start', 'body_end'
		];

		$count = min(abs($count), 200);
		while ($count--) {
			$row = [];

			# Fields #
			foreach ($this->_schema as $name => $field)
				if (!in_array($name, $ignoreList) && empty($field['polyglot']) && substr($name, 0, 15) != 'is_translated__') {
					if (isset($predefined[$name])) {
						$value = is_array($predefined[$name]) ? $predefined[$name][array_rand($predefined[$name])] : $predefined[$name];

					} else if (in_array($name, [ 'gps_location' ]))
						switch ($name) {
							case 'gps_location':
								$lat = str_replace(',', '.', round((rand(0, 747) + 447551) / 10000, 4));
								$lon = str_replace(',', '.', round((rand(0, 1485) + 204069) / 10000, 4));
								$value = "{$lat},{$lon}";
								break;

						} else if (in_array($name, [ 'gps_locations' ]))
						switch ($name) {
							case 'gps_locations':
								$value = '';
								for ($i = 0; $i < rand(1, 10); $i++) {
									$lat = str_replace(',', '.', round((rand(0, 747) + 447551) / 10000, 4));
									$lon = str_replace(',', '.', round((rand(0, 1485) + 204069) / 10000, 4));
									$value .= "{$lat},{$lon};";
								}
								$value = substr($value, 0, -1);
								break;

						} else if (in_array($name, [ 'page_id' ]))
						switch ($name) {
							case 'page_id':
								$value = $this->pageId;
								break;

						} else switch ($field['type']) {

						case 'biginteger':
						case 'integer':

							# Belongs to
							if (substr($name, -3) == '_id') {

								# Default conditions and model
								$more = [];
								$conditions = [];
								$relatedModel = Inflector::classify(substr($name, 0, -3));

								# Handle special associations
								switch (substr($name, 0, -3)) {
									case 'category':
										$conditions = [ 'Category.model' => $this->name ];
										break;

									case 'page':
										$conditions = [ 'Page.id' => $this->pageId ];
										break;

									case 'parent':
										$more = [ null ];
										$relatedModel = $this->alias;
										break;
								}

								# Get list of available records
								$list = ClassRegistry::init($relatedModel)->find('list', [ 'fields' => [ 'id', 'id' ], 'conditions' => $conditions ]);

								if ($list) {
									$value = array_rand($more + $list);
								} else if ($field['null']) {
									$value = null;
								}

								# Tree structure
							} else if ($name == 'lft' || $name == 'rght') {
								$value = 0;

								# Normal integer
							} else {
								$value = rand(0, 200);
							}
							break;

						case 'string':

							# Sets
							if (substr($name, -4) == '_set') {

								# Get list of available records
								$relatedModel = Inflector::classify(substr($name, 0, -4));
								$list = ClassRegistry::init($relatedModel)->find('list', [ 'fields' => [ 'id', 'id' ] ]);

								# Generate random set string
								$array = [];
								$setCount = rand(0, min(7, count($list)));
								while ($setCount--) {
									$relatedId = array_rand($list);
									if (!isset($array[$relatedId])) {
										$array[$relatedId] = $relatedId;
									}
								}
								$value = implode(',', $array);

								# Normal string
							} else {
								$value = trim($loremIpsum->getContent(rand(1, 3), 'plain'), '. ');
							}

							break;

						case 'text':
							$type = isset($this->_schema[$name]['comment']) && $this->_schema[$name]['comment'] == 'plain' ? 'plain' : 'html';
							$value = $loremIpsum->getContent(rand(40, 200), $type);
							break;

						case 'boolean':
							$value = rand(0, 1);
							break;

						case 'enum':
							$value = array_rand($field['enum']);
							break;

						case 'date':
						case 'time':
						case 'datetime':
							$value = date('Y-m-d H:i:s', strtotime('-' . rand(0, 30 * 24 * 60 * 60) . ' seconds'));
							break;

						default:
							die('Unknown type: ' . $field['type']);
					}
					$row[$name] = $value;
				}
			$this->id = null;
			$this->save($row);

			# Handle uploads #
			$associations = array_merge($this->hasOne, $this->hasMany);
			foreach ($associations as $alias => $params)
				if ($params['className'] == 'Upload') {
					if ($alias == 'MetaImage')
						continue;

					# Use .jpg as default extension
					if (empty($params['extensions'])) {
						$params['extensions'] = [ 'jpg' ];
					}

					# Find available sources by extension
					$exts = implode(',', $params['extensions']);
					$path = VECTORCMS_ROOT . "Vendor/sources/{{$exts}}/*";
					$sources = glob($path, GLOB_BRACE);

					# Skip if no valid sources
					if (empty($sources)) {
						continue;
					}

					# Attach the file(s)
					$multi = isset($this->hasMany[$alias]) ? rand(2, 10) : 1;
					while ($multi--) {
						$id = rand(0, sizeof($sources) - 1);
						$this->$alias->createFromLocalFile($sources[$id], $this->id, $this->alias, $alias);
					}
				}
		}

		# Repair the table to avoid problems
		$this->repair();
		return true;
	}

	# Get table size in bytes #
	function size() {
		$sqlConfig = ConnectionManager::$config->{$this->useDbConfig};
		$table = $this->tablePrefix . $this->useTable;
		$query = "
			SELECT DATA_LENGTH
			FROM `information_schema`.`tables`
			WHERE TABLE_SCHEMA = '{$sqlConfig['database']}' AND TABLE_NAME = '{$table}'";

		return res(res(res($this->$query())));
	}

	# Sort the tree #
	function moveNode($id, $amount) {
		if (!ctype_digit($id) || !is_numeric($amount) || !$amount) {
			return false;
		}

		$func = $amount > 0 ? 'moveDown' : 'moveUp';
		$this->$func($id, abs($amount));
	}

	function not($field, $options) {
		foreach ($options as $i => $option) {
			$options[$i] = strtolower($option);
		}
		$value = strtolower(reset($field));
		return !in_array($value, $options);
	}

	# Parse dynamic cms tags from HTML #
	function parseDynamicTags($html, $type = null) {
		if (empty($html))
			return [];

		Timer::startLoop('AppModel::parseDynamicTags');
		{

			# All of just some tags
			if ($type === null) {
				$type = '[a-z]+';
			}

			# List all possible regexps
			$regexp = [ // '\<cms:(?P<type>' . $type . ') *(?P<params1>(?:[^ =\>]+ *= *["\']? *[^"]* *["\']? *)*) *(?<params2>##DUMMY##)?\>(?P<content>.*)\</cms:\1\>',
				// '\<code +(?P<params1>(?:[^ =\>]+ *= *["\']? *[^"]* *["\']? *)*) *cms="(?P<type>' . $type . ')" *(?P<params2>(?:[^ =\>]+ *= *["\']? *[^"]* *["\']? *)*) *\>(?P<content>.*(?:(?P<inside>\<code.*\</code\>)|.)*)\</code\>',
				'<(?P<tag>[a-z1-6]+)
					\s+

					# Params
					(?P<params1>(?:[^\s=>]+\s*=\s*["\']?\s*[^"]*\s*["\']?\s*)*)
					\s*

					# CMS tag
					cms\s*=\s*"(?P<type>' . $type . ')"
					\s*

					# Additional params
					(?P<params2>(?:[^\s=>]+\s*=\s*["\']?\s*.*?\s*["\']?\s*)*)
					\s*

				# End tag
				>
				\s*

						# Content
						(?P<content>
							.*?

							# Repeating tags
							(?P<inside><\2\b.*?>(?P>inside)*?</\2>|.)*?

						)?
					\s*
				</\2>',
				// '\<img +(?P<params1>(?:[^ =\>]+ *= *["\']? *[^"]* *["\']? *)*) *cms="(?P<type>' . $type . ')" *(?P<params2>(?:[^ =\>]+ *= *["\']? *[^"]* *["\']? *)*) *(?<content>/\>)'
			];

			# Go though different regexps
			$result = [];
			foreach ($regexp as $exp) {
				preg_match_all("~(?P<match>{$exp})~ uisx", $html, $matches, PREG_SET_ORDER);

				foreach ($matches as $i => $match) {
					$match = removeIntKeys($match);
					$match['type'] = strtolower($match['type']);

					# Unify params
					$match['params'] = trim("{$match['params1']} {$match['params2']}");
					unset($match['params1'], $match['params2']);

					# Extract attributes
					preg_match_all('~ *(?P<name>[^ =]+) *= *"? *?(?P<value>[^"]+)? *?"?~ i', $match['params'], $attributes);
					$match['attributes'] = !empty($attributes['name']) ? array_combine($attributes['name'], $attributes['value']) : [];

					$matches[$i] = $match;
				}

				$result = Set::merge($result, $matches);
			}

		}
		Timer::startLoop('AppModel::parseDynamicTags');

		return $result;
	}

	static function getLog($source = 'default') {
		return ConnectionManager::getDataSource($source)->getLog(false, false);
	}

	/**
	 * Get module link
	 *
	 * @param $moduleData
	 *
	 * @return mixed|null|string
	 */
	public function getLink($moduleData, $locale = null) {

		if ($locale) {
			$this->setVirtual($locale);
		}
		$debug = Configure::read('debug');

		if (is_numeric($moduleData)) {
			$moduleData = $this->find('first', $moduleData);
		}

		if (isset($moduleData[$this->alias])) {
			$moduleData = reset($moduleData);
		}

		$locale = Configure::read('Config.language');

		$hash = md5('module-path-' . $this->alias . '-' . $moduleData['id'] . '-' . $moduleData['slug']) . '.' . $this->alias . '.' . $moduleData['id'] . '.' . $locale;

		# Try to get from cache
		$cached = Cache::read($hash, 'format');
		if (empty($debug) && !empty($cached)) {
			return $cached;
		}

		$link = "";

		$customLink = $this->getOverridenLink($moduleData);
		if (!empty($customLink)) {
			$link = $customLink;
		} else {

			$pageInstance = ClassRegistry::init('Page');
			if (isset($moduleData['page_id'])) {
				$link = $pageInstance->getPageLink($moduleData['page_id']) . '/' . $moduleData['slug'];
			} else {

				$page = $pageInstance->find('first', [ 'fields' => [ 'id' ], 'conditions' => [ 'Page.type NOT' => Page::TYPE_MODULE, 'Page.module_id' => $this->moduleId ] ]);

				if (!empty($page)) {
					$link = $pageInstance->getPageLink($page['Page']['id'], null, true) . '/' . $moduleData['slug'];
				}

			}

		}

		if (empty($debug)) {
			Cache::write($hash, $link, 'format');
		}
		return $link;
	}

	public function getOverridenLink($data) {
		return null;
	}

	# ~ Pack records by id - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function packResults($data = null, $key = null, $children = false) {
		if (empty($data))
			$data = $this->data;
		if (empty($key))
			$key = $this->primaryKey;
		if ($children === true)
			$children = 'children';

		# Bail if empty or on count
		if (empty($data) || empty($data[0]) || Set::extract('0/0/count', $data)) {
			return $data;
		}

		# Construct the proper key for extraction
		if (strpos($key, '.')) {
			$path = str_replace('.', '/', $key);
		} else {
			$path = key($data[0]) . '/' . $key;
		}

		# Connect keys with the data
		$keys = Set::extract("/{$path}", $data);
		$data = array_combine($keys, $data);

		# Go into the depths
		if ($children) {
			foreach ($data as $i => $record) {
				if (!empty($record[$children])) {
					$data[$i][$children] = $this->packResults($record[$children], $key, $children);
				}
			}
		}

		return $data;
	}

	/**
	 * Additional custom initialization.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function init() {
	}

	public function initRelationships() {

		// 		An example:
		//
		//		return [
		//			'Child'   => [              // The name of the first relationship
		//				'Kid'                   // The name of the module to relate with
		//			],
		//			'Roommate' => [             // The name of the second relationship
		//				'Person',	            // The name of the module to relate with
		//				'symmetrical' => true   // bool (default false) True if the relationship goes both ways, false to go in one way only
		//			]
		//		];

		return [];
	}

	/**
	 * Get the restrictions for this model.
	 *
	 * @return    The current restrictions.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function getRestrictions() {
		return $this->restrictions;
	}

	/**
	 * Get the packed errors for response.
	 *
	 * @return    array The packed errors, or empty array if there was no errors.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function getErrors($validationErrors = null) {
		if (empty($validationErrors)) {
			$validationErrors = $this->validationErrors;
		}
		$errors = [];
		foreach ($validationErrors as $field => $messages) {
			$errors[$this->alias][$field] = reset($messages);
		}

		return $errors;
	}

	/**
	 * Validate the supplied data against the validation rules.
	 *
	 * @return array The array of found errors.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function validateData($data) {
		$this->set($data);
		return $this->getValidationErrors();
	}

	/**
	 * Get validation errors from model
	 *
	 * @return array The array of found errors.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function getValidationErrors() {
		$errors = $this->getErrors($this->invalidFields());

		if ($errors && isset($errors[$this->alias])) {
			$errors = $errors[$this->alias];
		}

		return $errors;
	}

	/**
	 * Get a single feed from a model.
	 *
	 * @params    field
	 *                Name of the field.
	 * @params    id
	 *                Specify id of the record.
	 * @params    order
	 *                SQL order to use.
	 *
	 * @returns    Value of the field.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function field($field, $id = null, $order = []) {
		if ($id !== null && !ctype_digit($id)) {
			return parent::field($field, $id, $order);
		}

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

		$field = preg_replace('~__[a-z]+$~', '', $field);
		if ($this->isSingleItem) {
			$read = $this->find('first', [ 'fields' => [ $field ] ]);
		} else {
			$read = $this->find('first', [ 'fields' => [ $field ], 'conditions' => [ $this->alias . '.' . $this->primaryKey => $id ] ]);
		}
		$x = reset($read);
		return $read ? reset($x) : null;
	}

	/**
	 * Get the largest ordering for the model based on the content of data.
	 *
	 * @params    data
	 *                Data used to determine grouping field.
	 *
	 * @returns    Maximum ordering for this model.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function getMaxOrdering($data = null) {
		if (!$this->schema('ordering'))
			return null;

		# Get grouping field
		$conditions = [];
		foreach ($this->orderingGroups as $fieldName)
			if (isset($data[$this->alias][$fieldName])) {
				$conditions = [ "{$this->alias}.{$fieldName}" => $data[$this->alias][$fieldName] ];
			}

		# Edit conditions if ordering field is type set
		if (substr(key($conditions), -4) == '_set') {
			$locale = Configure::read('Config.language');
			$model = lastFromDot(substr(key($conditions), 0, -4));
			$model = Inflector::camelize($model);
			$itemId = ClassRegistry::init($model)->find('first', [
				'recursive'  => -1,
				'fields'     => [ 'id' ],
				'conditions' => [
					$model . '.title__' . $locale => end(array_values($conditions)) ]
			]);

			$conditions = [ key($conditions) . ' LIKE' => '%' . $itemId[$model]['id'] . '%' ];
		}

		# Get the maxiumum ordering
		$max = $this->find('first', [
			'limit'      => 1,
			'recursive'  => -1,
			'order'      => [
				'ordering' => DESC ],
			'fields'     => [
				'ordering' ],
			'conditions' => $conditions
		]);

		return !empty($max) ? $max[$this->alias]['ordering'] : 0;
	}

	/**
	 * Enable or disable foreign key checks on database.
	 *
	 * @param    enable
	 *                Set to true to enable key check, false to disable.
	 *                - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function setForeignKeyChecks($enable) {
		if ($enable) {
			$this->query("
				/*!40014 SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS IS NULL, 1, @OLD_FOREIGN_KEY_CHECKS) */;
				/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
				/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */
			");
		} else {
			$this->query("
				/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
				/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
				/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */
			");
		}
	}

	/**
	 * Get the last n queries for the model
	 *
	 * @param    last
	 *                Number of queries to be returned. Defaults to 1.
	 * @param    nl
	 *                Line break to use. Defaults to '<br />'.
	 *
	 * @return    The formatted SQL query.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function getLastQuery($last = 1, $nl = '<br />') {
		$row = str_repeat('-', 80);
		$log = $this->getDataSource()->getLog(false, false);
		$log = $last ? array_slice($log['log'], -$last, 1) : $log['log'];

		$spliters = implode('|', [ 'WHERE', 'ORDER', 'FROM', 'INNER', 'LEFT', 'LIMIT', 'VALUES' ]);
		foreach ($log as $i => $entry)
			$log[$i] = preg_replace("/ ({$spliters})/", "{$nl}$1", $entry['query']);
		return $last ? (isset($log[0]) ? $log[0] : false) : implode("{$nl}{$nl}{$row}{$nl}{$nl}", $log);
	}

	/**
	 * Check if there is table with similar name (same or same without prefix)
	 *
	 * @param    prefix
	 *                Prefix to use.
	 * @param    tableName
	 *                Name to check.
	 *
	 * @return    True if similar table exists, false if not.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function similarTableExists($prefix, $tableName) {
		try {

			# Check if table with prefix+tableName exists.
			$this->tableExists($prefix . $tableName);
		} catch (Exception $e) {
			try {

				# Check if table with same name exists, withou prefix.
				$this->tableExists($tableName);
			} catch (Exception $e) {

				# There is no table with similar name.
				return false;
			}
		}

		# There is table with similar name.
		return true;
	}

	/**
	 * Check if there is table with same name
	 *
	 * @param    tableName
	 *                Name to check.
	 *
	 * @return    True if same table exists, false if not.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function tableExists($tableName) {

		# Simple and fastest way to check
		$query = "SELECT 1 FROM `{$tableName}`";
		$this->query($query);
		return true;
	}

	/**
	 * Should prefer view instead of edit in scaffold index
	 *
	 * @param    tableName
	 *                Name to check.
	 *
	 * @return    True if same table exists, false if not.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function preferView($id = null) {
		return false;
	}

	/**
	 * Check if the record from this model can be deleted.
	 *
	 * @return    True if the model is not hardcoded or administrator is a superadmin.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function isAddable() {
		return !$this->isSingleItem && (!$this->isHardcoded || AuthComponent::user('group_id') == GROUP_SUPERADMINS);
	}

	/**
	 * Check if the record from this model can be editer.
	 *
	 * @return    True if the model is editable or administrator is a superadmin.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function isEditable($id = null) {
		return $this->isEditable || AuthComponent::user('group_id') == GROUP_SUPERADMINS;
	}

	/**
	 * Check if the model supports addind new records.
	 *
	 * @return    True if the model is not hardcoded or administrator is a superadmin.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function isDeletable($id = null) {
		return !$this->isSingleItem && (!$this->isHardcoded || AuthComponent::user('group_id') == GROUP_SUPERADMINS);
	}

	/**
	 * Check if the record from this model can be copied.
	 *
	 * @return    True if the model can be copied.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function isCopyable($id = null) {
		return $this->isCopyable && !$this->isSingleItem;
	}

	/**
	 * Check if the record from this model can be listed.
	 *
	 * @return    True if the model can be listed.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function isListable() {
		return !$this->isSingleItem;
	}

	/**
	 * Get additional actions for index list
	 *
	 * @return    Actions array
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function getIndexActions($record) {
		return [];
	}

	/**
	 * Get additional actions for item view
	 *
	 * @return    Actions array
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function getViewActions($record) {
		return [];
	}

	/**
	 * Get additional actions for item form view
	 *
	 * @return    Actions array
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function getFormActions($record) {
		return [];
	}

	/**
	 * Is treetable expandable for current module
	 *
	 * @return    True if its expandable
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function treetableExpandable() {
		return true;
	}

	/**
	 * Check if this model can be edited.
	 *
	 * @return    True if the model can be edited.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function isModuleEditable() {
		return true;
	}

	/**
	 * Called on approval status change
	 *
	 * @param $fromStatus
	 * @param $toStatus
	 */
	public function approvalStatusChanged($fromStatus, $toStatus) {
	}

	# ~ COMMON VALIDATION METHODS- - - - - - - - - - - - - - - - - - - - - - - - - #

	/**
	 * Define the validation for a color field.
	 *
	 * @return array The validation rule in for of a CakePHP validation array.
	 */
	function colorValidation() {
		return [
			'color' => [
				'rule'     => '~^#[a-fA-F0-9]{6}$~',
				'required' => true,
				'message'  => __('Please supply a color in format #RRGGBB') ]
		];
	}

	# ~ CACHING	 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #

	# ~ Invalidate cache based on the model name - - - - - - - - - - - - - - - - - #
	public function invalidateCache($model = null) {

		# For file caching
		if (CACHE_ENGINE == 'File') {
			$models = preg_replace('~\bCms~', '', implode(',', array_merge([ $this->name ], (array) $model)));
			$files = array_unique((array) glob(CACHE . "data/*.{{$models}}.*", GLOB_BRACE));
			foreach ($files as $file)
				@unlink($file);

			# Fot Redis caching
		} else if (CACHE_ENGINE == 'VectorRedis') {
			$models = preg_replace('~\bCms~', '', implode('_', array_merge([ $this->name ], (array) $model)));
			Cache::delete('*' . strtolower($models) . '*');
		}
	}

	# ~ Intercept model functions  - - - - - - - - - - - - - - - - - - - - - - - - #
	function intercept($args) {
		$debug = debug_backtrace();
		$function = $debug[1]['function'];

		//!!$this->construct();
		Timer::startLoop("Model::{$function}()");
		$ret = call_user_func_array([ 'parent', $function ], $args);

		if (!$this->preventCallbacks) {
			$this->invalidateCache();
			$this->refreshUploadCache($this->id);
		}
		Timer::endLoop("Model::{$function}()");

		return $ret;
	}

	function save($data = null, $validate = true, $fieldList = []) {
		$args = func_get_args();
		return $this->intercept($args);
	}

	function delete($id = null, $cascade = true) {
		$args = func_get_args();
		return $this->intercept($args);
	}

	function deleteAll($conditions, $cascade = true, $callbacks = false) {
		$args = func_get_args();
		return $this->intercept($args);
	}

	function updateAll($fields, $conditions = true) {
		$args = func_get_args();
		return $this->intercept($args);
	}

	/**
	 * Set the manual order based on the values of a certain column.
	 *
	 * @param string  $field The field to use for the ordering.
	 * @param array[] $order The value of the column to sort the results.
	 *
	 * @return string The resulting rules for the order.
	 */
	public static function manualOrder($field, $order) {
		$rules = [];
		foreach ($order as $item) {
			$rules[] = "{$field} = {$item} DESC";
		}

		return implode(', ', $rules);
	}

	# ~ This find triggers all beforeFind callbacks, but creates some problems - - -#
	public function find($type = 'first', $params = []) {

		# Quick find via id
		if ($type === 'first' && is_numeric($params)) {
			$params = [ 'conditions' => [ $this->alias . '.id' => $params ] ];
		}

		# Cache
		$enabled = $this->cache && (!isset($params['cache']) || $params['cache']);

		# !!TODO: Check this!!! We are resetting bindings here to fix errors with missing belongsTo, hasMany, hasOne
		if ($this->alias == 'Page') {
			$this->resetBindings();
		}

		$this->findQueryType = $type;
		$this->id = $this->getID();

		$params = $this->buildQuery($type, $params);

		if ($params === null) {
			return null;
		}

		# Check if apc is used
		$useStandardCache = false;
		if (CACHE_ENGINE == 'VectorRedis') {
			$useStandardCache = true;
		}

		$separator = "\n\n------------------------------------------------------------------------------------------------------------------------\n\n";

		# Create a list of tags
		$tags = implode('.', $this->getUsedModels($params));

		# Generate path for caching
		$path = CACHE . 'data';
		$salt = [ $this->name, $type, serialize($params), serialize($this->virtualFields), CMS ? 'CMS' : 'FRONT', Configure::read('Config.language') ];
		$file = md5(implode(';', $salt));

		# Read from apc cache
		$cache = null;
		if ($enabled) {
			if ($useStandardCache) {
				$file = "{$file}.{$tags}";
				$cachedValue = Cache::read($file);

				if ($cachedValue !== false) {
					$cache = $cachedValue;
				}

			} else {
				$file = "{$path}/{$file}.{$tags}.cache";

				if (is_readable($file)) {
					$cache = file_get_contents($file);
				}
			}

			# Found
			if ($cache) {
				list($query, $result) = explode($separator, $cache, 2);
				$data = unserialize($result);
			}
		}

		# Read from database
		if (empty($data)) {
			Timer::startLoop('Model::find()');
			{

				$this->getDatasource()->fullDebug = false;
				$data = $this->_readDataSource($type, $params);

				if ($enabled) {

					# Save the cache
					$lastQuery = $this->getLastQuery(true, "\n");
					if ($lastQuery) {
						if ($useStandardCache) {
							Cache::write($file, $lastQuery . $separator . serialize($data));

						} else {
							if (!is_dir($path = dirname($file))) {
								Folder::create($path, 0755);
							}
							file_put_contents($file, $lastQuery . $separator . serialize($data));
						}
					}
				}
			}
			Timer::endLoop('Model::find()');
		}

		return $data;
	}

	/**
	 * Finds the list of all models used in the query.
	 *
	 * @param    params
	 *                Parameters used for creating a query.
	 *
	 * @return    Array of all used models.
	 */
	function getUsedModels($params) {
		$models = [ $this->alias ];

		# Get models from fields name
		if (!empty($params['fields']) && is_array($params['fields'])) {
			foreach ($params['fields'] as $field) {
				preg_match_all('/(?P<model>[a-z]+)`?\.`?[a-z*]/i', $field, $match);
				$models[] = reset($match['model']);
			}
		}

		# Automatically add all parent models
		if (!empty($this->belongsTo)) {
			foreach ($this->belongsTo as $parent => $params) {
				$models[] = $parent;
			}
		}

		# Automatically add all contained models
		if (!empty($params['contain'])) {
			foreach ((array) $params['contain'] as $assoc) {
				$models[] = $assoc;
			}
		}

		# Manually added models that are used for caching purposes
		if (!empty($params['cache']) && is_array($params['cache'])) {
			$models = array_merge($models, $params['cache']);
		}

		# Clear Cms prefix
		foreach ($models as $i => $model) {
			$models[$i] = preg_replace('~^Cms~', '', $model);
		}

		return array_filter(array_unique($models));
	}

	# ~ Get the icon for the menu  - - - - - - - - - - - - - - - - - - - - - - - - #
	public function getMenuIcon($module) {

		# Menu icon
		if (!empty($module['Module']['menu_icon'])) {
			return $module['Module']['menu_icon'];
		}

		return "<i class=\"fa fa-fw fa-{$module['Module']['icon']}\"></i>";
	}

	# ~ Get the name for the menu  - - - - - - - - - - - - - - - - - - - - - - - - #
	public function getMenuName($module) {

		# Custom name
		if (!empty($module['Module']['menu_name'])) {
			return __($module['Module']['menu_name']);
		}

		# Name by modulename
		$name = __(Inflector::humanize(Inflector::tableize($module['Module']['name'])));

		# Sigularize for single intems
		if ($module['Module']['is_single_item']) {
			$name = Inflector::singularize($name);
		}

		return $name;
	}

	# ~ Get the content of the badge - - - - - - - - - - - - - - - - - - - - - - - #
	public function getMenuBadge($module) {
		if ($this->schema('approval_status')) {

			# Get the count
			$count = $this->find('count', [
				'recursive'  => -2,
				'conditions' => [
					$this->alias . '.approval_status' => 'Pending',
					$this->alias . '.is_deleted'      => false ]
			]);

			# Handle the max
			if ($count) {
				return $count < 100 ? $count : '...';
			}
		}

		return null;
	}

	# ~ Get custom class for scaffolds table row - - - - - - - - - - - - - - - - - #
	public function getIndexRowClass($record) {
		return null;
	}

	# ~ Decorate cell in scaffolds index - - - - - - - - - - - - - - - - - - - - - #
	public function decorateCellValue($value, $field, $record) {
		return $value;
	}

	# ~ Default select conditions- - - - - - - - - - - - - - - - - - - - - - - - - #
	public function getSelectConditions() {
		return [];
	}

	# ~ Default select conditions- - - - - - - - - - - - - - - - - - - - - - - - - #
	public function getSelectOrder() {
		return '';
	}

	# ~ Get the name of the default tab	 - - - - - - - - - - - - - - - - - - - - - #
	public function getDefaultTab() {
		return __('Content');
	}

	# ~ Check whether tab with supplied name should be visible - - - - - - - - - - #
	public function shouldShowTab($tabName, $data) {
		return true;
	}

	# ~ Get conditions for front filter- - - - - - - - - - - - - - - - - - - - - - #
	public function getRequestConditions(CakeRequest $request) {
		return [];
	}

	# ~ Alter pagination params - - - - - - - - - - - - - - - - - - - - - - - - - -#
	public function alterPaginationParams($params) {
		return $params;
	}

	# ~ Get children conditions - - - - - - - - - - - - - - - - - - - - - - - - - -#
	public function getChildrenConditions() {
		return [];
	}

	# ~ Should this model be included in sitemap  - - - - - - - - - - - - - - - - -#
	public function includeInSitemap() {
		if ($this->isModule && $this->hasDetails) {
			return true;
		}

		return false;
	}

	# ~ Replace some string in database - - - - - - - - - - - - - - - - - - - - - -#
	public function replaceInDb($replaceFrom, $replaceTo) {
		die('dangerous - intentionally disabled at the start of AppModel::replaceInDb');

		$db = $this->getDataSource();
		$tables = $db->fetchAll('SHOW TABLES');

		$tableFields = [];

		foreach ($tables as $table) {
			$table = reset($table['TABLE_NAMES']);

			$columns = $db->fetchAll("SHOW COLUMNS FROM " . $table);

			$matchingColumns = [];

			foreach ($columns as $column) {
				$column = reset($column);
				if (strpos('varchar', $column['Type']) !== false || strpos('text', $column['Type']) !== false) {
					$matchingColumns[] = $column['Field'];
				}
			}

			if (empty($matchingColumns)) {
				continue;
			}

			$tableFields[$table] = $matchingColumns;

		}

		$updated = [];
		$found = [];
		$sqls = [];
		foreach ($tableFields as $table => $fields) {
			$sql = "UPDATE `" . $table . "` SET ";

			$updates = [];
			$checks = [];
			foreach ($fields as $field) {
				$updates[] = "`" . $field . "` = REPLACE(`" . $field . "`, '" . $replaceFrom . "', '" . $replaceTo . "')";
				$checks[] = "`" . $field . "` LIKE '%" . $replaceFrom . "%'";

			}
			$checkSql = "SELECT * FROM " . $table . " WHERE " . join(' OR ', $checks);

			$sqls[] = $checkSql;

			$found[$table] = $db->fetchAll($checkSql);

			if (empty($found[$table])) {
				continue;
			}

			$sql .= join(',', $updates);

			if ($db->execute($sql)) {
				$updated[$table] = $db->affected;
			}

		}

		$this->clearCache();
	}

	public function getConditionForSet($column, $value) {
		return [
			'OR' => [
				"{$this->alias}.{$column}"        => $value,
				"{$this->alias}.{$column} LIKE"   => "%,{$value},%",
				"{$this->alias}.{$column}  LIKE"  => "{$value},%",
				"{$this->alias}.{$column}   LIKE" => "%,{$value}"
			]
		];
	}
}

# ~ Load configuration - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
Timer::start('setupConfiguration');
{
	$params = [];
	$config = ClassRegistry::init('Config')->find('all', [ 'callbacks' => false, 'fields' => [ 'group', 'name', 'value' ] ]);
	foreach ($config as $param) {
		extract($param['Config']);

		# Check for debug level parameter
		if ($name == 'debug') {
			Configure::write("debug", (int) $value);
		} else {
			Configure::write("{$group}.{$name}", $value);
		}
	}

}
Timer::end('setupConfiguration');
