<?php
App::uses('Sanitize', 'Utility');

class Module extends AppModel {

	var $modified = [];

	var $order = [ 'Module.ordering' => ASC ];

	var $displayField = 'name';

	var $belongsTo = [
		'ModuleGroup', 'Page'
	];

	public $hasMany = [
		'ModuleFields' => [
			'className' => 'ModuleField' ]
	];

	var $tagsArray = [
		'meta_description' => [
			'type' => 'text',
			'size' => 500 ],
		'meta_keyword'     => [
			'type' => 'text',
			'size' => 500 ],
		'meta_title'       => [
			'type' => 'varchar',
			'size' => 200 ] ];

	var $metaImage = [
		'type'    => 'file',
		'name'    => 'MetaImage',
		'options' => [
			'extensions' => 'jpeg jpg png gif',
			'multiple'   => '1',
			'modules'    => 'Upload'
		],
		'helper'  => [
			'extensions' => 'jpeg jpg png gif'
		] ];

	/**
	 * Dynamically generates a module model. The module must exist in `modules` table.
	 *
	 * @param    class
	 *                Name of the class to be built.
	 * @param    return
	 *                If set to true returns instance of the class.
	 *
	 * @return    New instance of the model if requested, otherwise void.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function generate($class, $return = false) {

		# Check if module exists
		$modules = Stash::read('ModulesValidation');
		foreach ($modules as $each)
			if ($each['Module']['name'] === $class) {
				$module = $each;
				break;
			}
		if (empty($module)) {
			return null;
		}

		# Check if exists in cache
		if (Configure::read('debug') <= 0) {
			$cached = Cache::read('DynamicEntity.' . $class);
			if ($cached && !class_exists("Cms{$class}")) {
				App::uses("Cms{$class}", 'Model');
				eval($cached);
				return $return ? new $class() : true;
			}
		}

		# Special fields
		$skip = [ 'id', 'modified', 'modified_by', 'created', 'created_by' ];
		$serialized = [ 'hasOne', 'hasMany' ];

		# Create a list of all class members
		$members['isModule'] = true;
		$members['moduleId'] = $module['Module']['id'];
		foreach ($module['Module'] as $field => $value)
			if (!in_array($field, $skip)) {
				$field = Inflector::variable($field);

				# Serialized
				if (in_array($field, $serialized)) {
					$value = @unserialize($value);
					if (empty($value)) {
						$value = [];
					}

					# Ordering
				} else if ($field == 'order') {
					$temp = explode(' ', trim($value));
					$value = [ $temp[0] => isset($temp[1]) ? $temp[1] : ASC ];
				}

				$members[$field] = $value;
			}

		# Generate validation
		$validation = $this->ModuleFields->generateValidation($module);
		if (!empty($validation)) {
			$members['validate'] = $validation;
		}

		# Get the model
		App::uses("Cms{$class}", 'Model');
		$parent = class_exists("Cms{$class}") ? "Cms{$class}" : 'AppModel';

		# Build the class
		return $this->buildClass("{$class}", $parent, $members, $return);
	}

	/**
	 * Dynamically generates a module controller. The module must exist in `modules` table.
	 *
	 * @param    class
	 *                Name of the class to be built.
	 * @param    return
	 *                If set to true returns instance of the class.
	 *
	 * @return    New instance of the controller if requested, otherwise void.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function genericController($class, $return = false) {

		# Trim controller at the end of the class name
		if (substr($class, -10) == 'Controller') {
			$class = substr($class, 0, -10);
		}

		# Check if module exists
		$module = Inflector::classify($class);
		$hardcoded = [ 'Layouts', 'Elements', 'Stylesheets', 'Javascripts', 'Configs' ];
		if (false && !in_array($class, $hardcoded) && !$module = $this->findByName($module)) {
			return null;
		}

		# Check if exists in cache
		$cached = Cache::read('DynamicEntity.' . $class . 'Controller');
		if ($cached) {
			$class = $class . 'Controller';
			eval($cached);
			return $return ? new $class() : true;
		}

		# Create a list of class members
		$members['name'] = $class;
		$members['paginate'] = [];

		# Build the class
		$class = Inflector::pluralize(Inflector::singularize($class));
		return $this->buildClass("{$class}Controller", 'AppBackendController', $members, $return);
	}

	/**
	 * Dynamically creates a class based on the arguments.
	 *
	 * @param    class
	 *                Name of the class.
	 * @param    extends
	 *                Name of the parent class.
	 * @param    members
	 *                Associative array of memberName => value.
	 * @param    return
	 *                If set to true returns instance of the class.
	 *
	 * @return    New instance of the class if requested, otherwise void.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function buildClass($class, $extends = null, $members = [], $return = false) {

		# Check if class exists
		if (class_exists($class)) {
			return null;
		}

		# Pack members into PHP readable format
		$list = '';
		foreach ((array) $members as $member => $value) {

			# Fix var_export problem with sequential arrays
			$val = preg_replace("/[0-9]+ \=\>/i", '', var_export($value, true));
			$list .= "public \${$member} = {$val}; ";
		}

		# Extends
		$extend = '';
		if (!empty($extends)) {
			$extend .= " extends {$extends}";
		}

		# Create the class
		$classContent = "class {$class}{$extend} { {$list} }";
		Cache::write('DynamicEntity.' . $class, $classContent);
		eval($classContent);
		return $return ? new $class() : true;
	}

	/**
	 * Get the list of avilable switches.
	 *
	 * @return
	 *            The list of switches.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public static function getSwitches() {
		return [
			'has_details' => [
				'adjust'  => null,
				'name'    => 'has_details',
				'icon'    => 'list-alt',
				'field'   => null,
				'default' => false ],

			'soft_deletable' => [
				'adjust'  => null,
				'name'    => 'soft_deletable',
				'icon'    => 'ambulance',
				'field'   => 'is_deleted',
				'default' => true ],

			'activity_state' => [
				'adjust'  => null,
				'name'    => 'activity_state',
				'icon'    => 'check-square-o',
				'field'   => 'is_active',
				'default' => true ],

			'is_hardcoded' => [
				'adjust'  => null,
				'name'    => 'is_hardcoded',
				'icon'    => 'lock',
				'field'   => null,
				'default' => false ],

			'is_single_item' => [
				'adjust'  => null,
				'name'    => 'is_single_item',
				'icon'    => 'tag',
				'field'   => null,
				'default' => false ],

			'requires_approval' => [
				'adjust'  => null,
				'name'    => 'requires_approval',
				'icon'    => 'black-tie',
				'field'   => 'approval_status',
				'default' => false ]
		];
	}

	/**
	 * Get the list of modules, grouped in.
	 *
	 * @return
	 *            The grouped list of modules.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function getGrouped($showEmpty = false) {

		# Get the groups
		$groups = ClassRegistry::init('ModuleGroup')->find('all', [
			'Get module groups',
			'pack'     => true,
			'ordering' => [ 'ordering' => ASC ],
			'cache'    => false,
			'fields'   => [
				'id', 'name', 'ordering' ]
		]);
		foreach ($groups as $i => $group)
			$groups[$i] = reset($group);

		# Append default group
		$ungrouped = [ null => [
			'id'       => null,
			'name'     => null,
			'ordering' => 0
		] ];
		$groups = $ungrouped + $groups;

		# Craete placeholders
		foreach ($groups as $i => $group) {
			$groups[$i]['modules'] = [];
		}

		# Get the modules
		$modules = $this->find('all', []);
		foreach ($modules as $module) {
			$groups[$module['Module']['module_group_id']]['modules'][] = $module;
		}

		# Clear empty groups
		if (!$showEmpty) {
			foreach ($groups as $i => $group)
				if (empty($group['modules'])) {
					unset($groups[$i]);
				}
		}

		return $groups;
	}

	# ~ Get fields and validation rules for form - - - - - - - - - - - - - - - - - #
	public function getFieldsValidations($id) {
		$fields = $this->ModuleFields->getModuleFields($id);

		# Format output
		$formatted = [];
		foreach ($fields as $field) {
			$formatted[$field['ModuleFields']['field_name']] = $field;
		}
		return $formatted;
	}

	# ~ Get modules which have email field - - - - - - - - - - - - - - - - - - - - #
	public function getEmailModules() {
		$modules = Stash::read('modules');
		$emailModules = [];
		foreach ($modules as $module) {
			$moduleInstance = ClassRegistry::init($module['Module']['name']);
			if ($moduleInstance->schema('email')) {
				$emailModules[$module['Module']['id']] = __($module['Module']['name']);
			}
		}

		return $emailModules;
	}

	/**
	 * Prepares the module for save.
	 *
	 * @param    options
	 *                User suppplied options for build.
	 *                - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function beforeSave($options = []) {
		$this->fieldsBackup = [];

		# Skip if not neccessary
		if (!array_key_exists('Fields', $this->data['Module'])) {
			return true;
		}

		# Basic validations
		$this->modified = [ 'id' => 'id' ];
		if (empty($this->data['Module']['Fields'])) {
			$this->invalidate('name', __('You need to enter at least one field'));
			return false;
		}

		# Read fields
		$slug = 'id';
		$names = [];
		$errors = [];
		$fields = [];
		$polyglots = [];
		$translated = false;
		$hasOne = $hasMany = [];
		$sqlConfig = ConnectionManager::$config->{$this->useDbConfig};
		$languages = array_keys(Configure::read('Config.Languages'));
		foreach ($this->data['Module']['Fields'] as $i => $field) {
			$field['name'] = Inflector::underscore(Inflector::variable($field['name']));

			# Validate fieldnames
			$name = preg_replace('/\s+/', '_', strtolower(trim($field['name'])));
			if (empty($name)) {
				$errors[$i]['name'] = __('Please specify fieldname');

			} else if (preg_match('/[^a-z0-9_ ]/i', $name)) {
				$errors[$i]['name'] = __('Only alphanumerical characters and spaces are allowed in fieldname');

			} else if (in_array($name, $names)) {
				$errors[$i]['name'] = __('Fieldnames must be unique within module');

			} else if (in_array($name, $this->reserved['Fields'])) {
				$errors[$i]['name'] = __('This is a system keyword and cannot be used as a fieldname');
			}
			$names[] = $field['name'];

			# Set slug
			if ($slug == 'id' && in_array($field['name'], $this->displayFields)) {
				$slug = $field;
			}

			# Defaults
			$size = null;
			$null = false;
			$default = null;
			$fk = '';

			# Handle the type
			$type = $field['type'];
			switch ($field['type']) {
				case 'int':
					if (preg_match('/[_ ](id|set)$/i', $field['name'])) {
						$errors[$i]['options']['length'] = __('Last word cannot be id or set');
						continue;
					}
					$type = empty($field['options']['bigint']) ? 'INT' : 'BIGINT';
					$default = '0';
					break;

				case 'tinyint':
					$size = 1;
					$default = !empty($field['options']['checked']) ? '1' : '0';
					break;

				case 'varchar':
					if (!ctype_digit($field['options']['length'])) {
						$errors[$i]['options']['length'] = __('Please enter integer value in this field');
						continue;
					}
					$size = (int) $field['options']['length'];
					break;

				case 'enum':
					if (trim($field['options']['list']) == '') {
						$errors[$i]['options']['list'] = __('You must define at least one option');
						continue;
					}
					$list = str_replace([ "'", '\\' ], [ "\\'", '\\\\' ], $field['options']['list']);
					$list = explode("\n", $list);
					$list = array_unique($list);
					foreach ($list as $i => $item) {
						$list[$i] = trim($item);
						if (empty($list[$i])) {
							unset($list[$i]);
						}
					}
					$size = "'" . implode("','", $list) . "'";
					break;

				case 'text':
					break;

				case 'datetime':
					$type = !empty($field['options']['justdate']) ? 'date' : 'datetime';
					break;

				case 'module':
					$name = "cms_" . Inflector::singularize(Inflector::tableize($field['module_name'])) . "_id";
					$field['name'] = $name;
					$field['type'] = 'module';
					$type = 'BIGINT(20) UNSIGNED';
					$fk = "FOREIGN KEY ({$name}) REFERENCES cms_" . Inflector::tableize($field['module_name']) . "(id)";
					break;

				case 'file':
					$fieldname = Inflector::classify(preg_replace('/\s+/', '_', $field['name']));

					# Validate extensions
					if (preg_match('/[^a-z0-9 ,]/', $extensions = $field['options']['extensions'])) {
						$errors[$i]['options']['extensions'] = __('Please use only alphanumerical characters and spaces for separating extensions');
					}
					$extensions = strtolower(trim($extensions));
					$extensions = preg_replace('/[^a-z0-9 ]+/', ' ', $extensions);

					if (empty($field['options']['multiple'])) {
						$fieldname = Inflector::singularize($fieldname);
						$association = 'hasOne';
					} else {
						$fieldname = Inflector::pluralize($fieldname);
						$association = 'hasMany';
					}
					${$association}[$fieldname] = 'Upload ' . $extensions;

					if (!empty($field['options']['comment'])) {
						${$association}[$fieldname] .= ' | ' . $field['options']['comment'];
					}
					continue(2);

				default:
					$errors[$i]['name'] = __('Unknown field type');
					continue;
			}

			$size = !empty($size) ? '(' . $size . ')' : '';
			$null = !empty($field['required']) ? 'NOT NULL' : 'NULL';
			$default = !empty($default) ? " DEFAULT '{$default}'" : (!empty($field['required']) ? '' : ' DEFAULT NULL');
			$comment = $type == 'text' && empty($field['options']['richtext']) ? " COMMENT 'plain'" : '';

			# Save field with polyglot
			if (in_array($type, [ 'varchar', 'text' ]) && !empty($field['options']['polyglot'])) {
				$this->polyglots = [];
				foreach ($languages as $locale) {
					$fields[] = "`{$name}__{$locale}` {$type}{$size} {$null}{$default}{$comment}";
					if (!$translated) {
						$polyglots[] = "`is_translated__{$locale}` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1";
					}
				}
				$translated = true;

				# Save field without polyglot
			} else {
				$fields[] = "`{$name}` {$type}{$size} {$null}{$default}{$comment}";
			}
			if (!empty($fk)) {
				$fields[] = $fk;
			}

			$this->fieldsBackup[] = $field;
		}
		$fields = array_merge($fields, $polyglots);

		# Only for items with details
		if (!empty($this->data['Module']['has_details'])) {

			# Slug
			if ($slug == 'id') {
				$fields[] = '`slug` BIGINT(20) UNSIGNED NOT NULL';
			} else {
				$size = !empty($slug['options']['length']) ? '(' . $slug['options']['length'] . ')' : '';
				if (!empty($slug['options']['polyglot'])) {
					foreach ($languages as $locale) {
						$fields[] = "`slug__{$locale}` {$slug['type']}{$size}";
					}
				} else {
					$fields[] = "`slug` {$slug['type']}{$size} {$null}{$default}";
				}
			}

			# Meta image
			$this->data['Module']['Fields'][] = $this->metaImage;

			# Meta tags
			foreach ($this->tagsArray as $tag => $item) {
				$comment = "";
				if ($item['type'] == 'text') {
					$comment = " COMMENT 'plain'";
				}

				# Handle polyglot modules
				if (count($languages) > 1) {
					foreach ($languages as $locale) {
						$fields[] = "`{$tag}__{$locale}` {$item['type']}({$item['size']}){$comment}";
					}
				} else {
					$fields[] = "`{$tag}` {$item['type']}({$item['size']}){$comment}";
				}
			}

			# Meta algorithm
			$fields[] = "`meta` varchar(50) DEFAULT NULL";

			# Add change frequency and priority fields
			$fields[] = "`change_frequency` enum('always','hourly','daily','weekly','monthly','yearly','never') NOT NULL DEFAULT 'monthly'";
			$fields[] = "`priority` enum('0.0','0.1','0.2','0.3','0.4','0.5 - default','0.6','0.7','0.8','0.9','1.0') NOT NULL DEFAULT '0.5 - default'";
			$fields[] = "`seo_metatags` TEXT NULL DEFAULT NULL";
			$fields[] = "`seo_generated_metatags` TEXT NULL DEFAULT NULL";
			$fields[] = "`head_end` TEXT NULL DEFAULT NULL";
			$fields[] = "`body_start` TEXT NULL DEFAULT NULL";
			$fields[] = "`body_end` TEXT NULL DEFAULT NULL";
		}

		# Category
		if (!empty($this->data['Module']['has_categories'])) {
			$fields[] = "`category_id` BIGINT(20) UNSIGNED NOT NULL";
		}

		# Page
		if (!empty($this->data['Module']['page_id'])) {
			$fields[] = "`page_id` BIGINT(20) UNSIGNED NOT NULL";
		}

		# Ordering
		$dir = 'ASC';
		switch ($this->data['Module']['sort_by']) {

			case 'ordering':
				$fields[] = "`ordering` BIGINT(20) UNSIGNED NOT NULL";
				break;

			default:
				$type = res(Set::extract('[name=' . $this->data['Module']['sort_by'] . ']/type', $this->data['Module']['Fields']));
				if (!in_array($type, [ 'varchar', 'text' ])) {
					$dir = in_array($type, [ 'datetime' ]) ? 'DESC' : 'ASC';
					$fields[] = "INDEX (`{$this->data['Module']['sort_by']}`)";
				}
		}
		if ($this->data['Module']['sort_by'] === 'id')
			$dir = 'DESC';
		$ordering = "{$this->data['Module']['sort_by']} {$dir}";

		# Requires approval
		if (!empty($this->data['Module']['requires_approval'])) {
			$fields[] = "`approval_status` enum('Pending','Postponed','Rejected','Accepted') DEFAULT 'Pending' NOT NULL";
		}

		# Activity State
		if ($this->data['Module']['activity_state']) {
			$fields[] = '`is_active` TINYINT(1) UNSIGNED DEFAULT 1';
		}

		# Soft Delete
		if (!empty($this->data['Module']['soft_deletable'])) {
			$fields[] = "`is_deleted` TINYINT(1) UNSIGNED DEFAULT 0 NOT NULL";
		}

		# Modified and Created
		$fields[] = '`modified_by` INT UNSIGNED NOT NULL';
		$fields[] = '`modified` DATETIME NOT NULL';
		$fields[] = '`created_by` INT UNSIGNED NOT NULL';
		$fields[] = '`created` DATETIME NOT NULL';

		# Save fields for afterSave
		$this->tableFields = $fields;
		unset($this->data['Module']['Fields']);

		# Setup module
		$table = preg_replace('~\s+~', '_', Inflector::tableize($this->data['Module']['name']));

		# Check if table exists
		if ($this->similarTableExists('cms_', $table) && $this->previousData['Module']['use_table'] != $table) {
			$this->invalidate('name', __('Table name is already used by another entity'));
			return false;
		}

		$this->data['Module']['name'] = Inflector::classify(preg_replace('/\s+/', '_', strtolower(trim($this->data['Module']['name']))));
		$this->data['Module']['use_table'] = $table;
		$this->data['Module']['table_prefix'] = 'cms_';

		if (empty($this->data['Module']['paginate']))
			$this->data['Module']['paginate'] = 20;

		$this->data['Module']['order'] = $this->data['Module']['name'] . '.' . $ordering;
		$this->data['Module']['has_one'] = serialize($hasOne);
		$this->data['Module']['has_many'] = serialize($hasMany);

		# Validate module name/table
		if (empty($this->data['Module']['name'])) {
			$this->invalidate('name', __('Module name cannot be empty'));
		} else if (preg_match('/[^a-z0-9_ ]/i', $this->data['Module']['name'])) {
			$this->invalidate('name', __('Only alphanumeric characters and spaces are allowed in module name'));
		} else if ($this->hasAny([ 'Module.name' => $this->data['Module']['name'], 'Module.id <>' => (int) $this->data['Module']['id'] ])) {
			$this->invalidate('name', __('This name is already used by another module'));
		} else if ((class_exists($this->data['Module']['name']) && !ClassRegistry::init($this->data['Module']['name'])->isModule)) {
			$this->invalidate('name', __('This reserved keyword in the CMS and cannot be used as a module name'));
		} else if (in_array(strtolower($this->data['Module']['name']), $this->RESERVED_WORDS)) {
			$this->invalidate('name', __('This is a system keyword and cannot be used as a module name'));
		}

		# Icon is mandatory
		if (empty($this->data['Module']['icon'])) {
			$this->invalidate('icon', __('Please select icon for the module'));
		}

		# Check for errors
		if (!empty($errors) || !empty($this->validationErrors)) {
			$this->validationErrors['Fields'] = $errors;
			return false;
		}

		# Perserve module name
		if (!empty($this->data['Module']['id'])) {
			$this->originalModule = $this->find('first', [ 'recursive' => -1, 'conditions' => [ 'id' => $this->data['Module']['id'] ] ]);
		}

		return true;
	}

	/**
	 * Cleans up after the module has been saved.
	 *
	 * @param    created
	 *                True if the module is just created, false if it was edited.
	 *                - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function afterSave($created, $options = []) {

		# If we need to change module data
		if (empty($this->data['Module']['name'])) {
			return;
		}

		$this->clearCache();
		$this->__saveFields();

		# Get the connection managed and the table
		$sqlConfig = ConnectionManager::$config->{$this->useDbConfig};
		$table = $this->data['Module']['table_prefix'] . $this->data['Module']['use_table'];

		# If module has been edited, backup the old data
		if (!$created) {

			# Create relations between old and new fields
			$oldTable = $this->originalModule['Module']['table_prefix'] . $this->originalModule['Module']['use_table'];
			$schema = $this->query("DESCRIBE `{$sqlConfig['database']}`.`{$oldTable}`");
			$schema = Set::extract('/COLUMNS/Field', $schema);
			foreach ($this->tableFields as $field) {
				preg_match_all('/^`(?P<fieldname>[^`]+)`/', $field, $match);
				$fieldname = reset($match['fieldname']);
				if (array_search($fieldname, $this->modified) === false) {

					# New field
					if (!array_search($fieldname, $schema)) {
						$this->modified[] = $fieldname;

						# Edited field
					} else {
						$this->modified[$fieldname] = $fieldname;
					}
				}
			}

			# Use outfile for temporary backup
			if (false) {

				# Pack relations in lists
				foreach ($this->modified as $old => $new) {
					$newList[] = "`{$new}`";
					$oldList[] = !is_numeric($old) ? "`{$old}`" : "''";
				}

				# Generate unique tmp file
				while (empty($outfile) || is_file($outfile)) {
					$outfile = sys_get_temp_dir() . '/vcms_' . md5(microtime());
				}

				# Export fields and destroy table
				$oldFieldList = implode(', ', $oldList);
				$this->query("
					SELECT {$oldFieldList}
					FROM `{$sqlConfig['database']}`.`{$table}`
					INTO OUTFILE '{$outfile}'
						FIELDS TERMINATED BY ','
						ENCLOSED BY '\"'
						LINES TERMINATED BY '\n'"
				);

				# Use query for temporary backup
			} else {

				# Build query
				foreach ($this->modified as $old => $new)
					if (!is_numeric($old)) {
						$query[] = "`{$old}` AS `{$new}`";
					}
				$query = implode(', ', $query);

				# Get all data from the table
				$dump = $this->query("SELECT {$query} FROM `{$sqlConfig['database']}`.`{$oldTable}` AS `{$this->data['Module']['name']}`");
				foreach ($dump as $record)
					$backup[] = reset($record);
			}
		}

		$this->begin();

		try {

			# Drop the table
			$this->setForeignKeyChecks(false);
			if (!$created) {
				$this->query("DROP TABLE IF EXISTS `{$sqlConfig['database']}`.`{$oldTable}`");
			}

			# Create table for the model
			$fields = implode(", \n", $this->tableFields);
			$query = "
				CREATE TABLE  `{$sqlConfig['database']}`.`{$table}` (
					`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
					{$fields}
				) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;";
			$this->query($query);

			# Repopulate the table from the backup
			if (!$created) {

				# Populate the module with old data from backup outfile
				if (!empty($outfile)) {
					$this->query("
						LOAD DATA INFILE '{$outfile}'
						INTO TABLE `{$sqlConfig['database']}`.`{$table}`
						CHARACTER SET BINARY
						FIELDS TERMINATED BY ',' ENCLOSED BY '\"'
						LINES TERMINATED BY '\n'");
					@unlink($outfile);

					# Populate the module with old data from backup variable
				} else if (!empty($backup)) {
					$fields = '`' . implode('`, `', array_keys(reset($backup))) . '`';

					# Build values string
					foreach ($backup as $record) {
						$row = [];
						foreach ($record as $value) {
							switch (true) {
								case is_null($value):
									$row[] = 'NULL';
									break;

								case is_bool($value);
									$row[] = $value ? 'TRUE' : 'FALSE';
									break;

								default:
									$row[] = "'" . Sanitize::escape($value) . "'";
							}
						}
						$values[] = '(' . implode(', ', $row) . ')';
					}
					$values = implode(",\n", $values);

					# Execute the query
					$this->query("INSERT INTO `{$table}`\n({$fields}) VALUES\n{$values};");

					// # Repopulate the module table with backup data
					// $Module = $this->generate($this->data['Module']['name'], true);
					// $Module->saveMany($backup);
				}
			}

			# Enable foreign key checks
			$this->setForeignKeyChecks(true);
			$this->commit();

		} catch (Exception $e) {
			$this->rollback();
			$this->clearCache();
			return false;
		}

		$this->clearCache();
	}

	/**
	 * Get the list of all font awesome icons.
	 *
	 * @return    The list of available icons.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public static function getIcons() {

		# Get the list
		$file = file_get_contents(VECTORCMS_ROOT . WEBROOT_DIR . DS . '/plugins/font-awesome-4.7.0/list.csv');
		$icons = array_filter(preg_split("[\s+]", $file));

		# Sort
		sort($icons);
		$icons = array_combine($icons, $icons);
		return $icons;
	}

	# ~ Save module fields in aftesave - - - - - - - - - - - - - - - - - - - - - - #
	private function __saveFields() {

		# Do it all in a single transaction
		$dataSource = $this->getDataSource();
		$dataSource->begin();

		try {

			# Delete old fields (validation rules are deleted too)
			$this->ModuleFields->recursive = -2;
			$this->ModuleFields->deleteAll([
				'ModuleFields.module_id' => $this->id
			]);

			# Build fields
			foreach ($this->fieldsBackup as $index => $field) {

				# Initial field data
				$dbField = [
					'module_id'   => $this->id,
					'module_name' => $this->data['Module']['name'],
					'field_name'  => $field['name'],
					'field_type'  => $field['type'],
					'required'    => empty($field['required']) ? false : true
				];

				# Save field
				$this->ModuleFields->create();
				$dbField = $this->ModuleFields->save($dbField);
				if (!$dbField)
					throw new Exception(' not saved');

				# Save validation rules
				foreach ($field['ModuleFieldRules'] as $ruleIndex => $validationRule) {

					# Skip template
					if (!is_numeric($ruleIndex))
						continue;
					$dbRule = [
						'rule_id'            => $validationRule['rule_id'],
						'module_field_id'    => $dbField['ModuleFields']['id'],
						'validation_message' => $validationRule['validation_message']
					];

					# Save serialized additional data
					unset($validationRule['rule_id']);
					unset($validationRule['validation_message']);
					$dbRule['additional_data'] = serialize($validationRule);

					# Save validation rule for field
					$this->ModuleFields->ModuleFieldRules->create();
					$ruleSaved = $this->ModuleFields->ModuleFieldRules->save($dbRule);
					if (!$ruleSaved) {
						throw new Exception('Rule not saved');
					}
				}
			}
			$dataSource->commit();

		} catch (Exception $e) {
			$dataSource->rollback();
			throw $e;
		}
	}

	/**
	 * Cleans up after the module has been deleted.
	 *
	 * @todo    This should go to the afterDelete but there is a problem with
	 *            generating deleted modules.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	function beforeDelete($cascade = true) {
		$this->read();

		# Create foreign key.
		$foreignKey = 'cms_' . Inflector::singularize(Inflector::tableize($this->data['Module']['name'])) . '_id';

		# Read all modules.
		$modules = Stash::read('modules');
		foreach ($modules as $module) {

			# Skip this current module.
			if ($module['Module']['name'] == $this->data['Module']['name'])
				continue;

			# Instantiate, check all fields and abort if there is a match
			$instance = ClassRegistry::init($module['Module']['name']);
			foreach ($instance->schema() as $key => $field) {
				if ($key == $foreignKey) {
					throw new Exception(__('Delete failed.') . ' ' . __('Module is connected to another module') . '. (' . $module['Module']['name'] . ')');
					return false;
				}
			}
		}

		# Truncate table and remove all hard-associated entries
		$Module = $this->generate($this->data['Module']['name'], true);
		$Module->deleteAll(true);

		# Drop the table
		$table = $this->data['Module']['table_prefix'] . $this->data['Module']['use_table'];
		$this->query("DROP TABLE `{$table}`");

		# Clear the complete cache
		$this->clearCache();

		return true;
	}

	/**
	 * Get model for set field
	 *
	 * @param $field
	 *
	 * @return string
	 */
	public static function getModelForSetField($field) {
		$tmpField = substr($field, 0, -4);
		if (substr($tmpField, 0, 4) == 'cms_') {
			$tmpField = substr($tmpField, 4);
		}
		return Inflector::camelize($tmpField);
	}

	private $RESERVED_WORDS = [ '__halt_compiler', 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'finally', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor', 'yield' ];

}
