<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package clearFusionCMS\module\property
 * @copyright Copyright (c) 2016 - 2017 clearFusionCMS. All rights reserved.
 * @link http://clearfusioncms.com
 */

/**
 * propertyBackendController Class
 *
 * Backend controller for editing & managing properties.
 *
 * @package clearFusionCMS\module\property
 */
class propertyBackendController extends cfBackendController {

	/**
	 * Access to property types.
	 *
	 * @var propertyTypes
	 */
	protected $types;

	/**
	 * The search form.
	 *
	 * @var flForm
	 */
	protected $searchForm;

	/**
	 * The config object.
	 *
	 * @var flConfig
	 */
	protected $cfg;

	/**
	 * @inheritdoc
	 */
	function __construct($router, $request, $permission = null) {
		parent::__construct($router, $request, $permission);

		$this->cfg = clearFusionCMS::getSiteConfig('cfProperty');
		$this->types = new propertyTypes($this->db);
	}

	/**
	 * Get the available regions.
	 *
	 * @param int $id The ID to get the child regions of.
	 * @param int $depth The current depth.
	 * @param bool $skipSelect true to skip select menu item.
	 * @return array The album ids and names.
	 */
	protected function getRegions($id = 0, $depth = 0, $skipSelect = false) {
		$prefix = $depth ? str_repeat('&nbsp;&nbsp;', $depth - 1) . '+ &nbsp;' : '';

		$regions = array();

		if(!$id && !$skipSelect)
			$regions[0] = __('[Select Region]', 'propertyBackend');

		$results = $this->db->query('SELECT regionid,title FROM cf_property_region_' . clearFusionCMS::getSite() . ' WHERE parentid=? ORDER BY title DESC', $id);
		while($row = $results->fetch()) {
			$regions[$row['regionid']] = $prefix . $row['title'];
			$regions = $regions + $this->getRegions($row['regionid'], $depth + 1);
		}
		return $regions;
	}

	/**
	 * Get the list of property types.
	 *
	 * @return array The list of types.
	 */
	protected function getTypes() {
		$cats = array(
			0 => __('[Unknown]', 'propertyBackend')
		);

		$results = $this->types->getChildren(0, array('typeid', 'title'));
		foreach ($results as $row) {
			$cats[$row['typeid']] = $row['title'];
		}

		return $cats;
	}

	/**
	 * Build the search form.
	 */
	function buildForm() {
		$this->sessionData = $this->session->getNamespace('cfSProperty.property.search');
		if(!isset($this->sessionData->search))
			$this->sessionData->search = '';
		if(!isset($this->sessionData->regions))
			$this->sessionData->regions = array();
		if(!isset($this->sessionData->category))
			$this->sessionData->category = '';
		if(!isset($this->sessionData->lastRegion))
			$this->sessionData->lastRegion = 0;
		if(!isset($this->sessionData->onPage))
			$this->sessionData->onPage = 0;
		if(!isset($this->sessionData->lastCountry)) {
			$pcfg = clearFusionCMS::getSiteConfig('cfProperty');
			$this->sessionData->lastCountry = $pcfg->get('default_country', 'US');
		}

		$this->searchForm = new flForm($this->request, 'searchform', $this->assemble('property', 'properties'));

		$searchfor = new flFormElementText($this->searchForm, 'searchfor', $this->sessionData->search);
		$searchfor->setLabel(__('Search For', 'propertyBackend'));

		$searchin = new flFormElementChecklist($this->searchForm, 'searchin',  $this->sessionData->regions, $this->getRegions(0, 0, true));
		$searchin->setLabel(__('Restrict Search To', 'propertyBackend'));

		$searchCat = new flFormElementSelect($this->searchForm, 'searchcats', $this->sessionData->category, array(
			'' => __('Any', 'propertyBackend'),
			'buy' => __('Buy', 'propertyBackend'),
			'rent' => __('Rent', 'propertyBackend')
		));
		$searchCat->setLabel(__('Buy or Rent', 'propertyBackend'));

		$submit = new flFormElementSubmit($this->searchForm, 'Search');
		$submit->setCaption(__('Search', 'propertyBackend'))->setCSS('primary');

		if($this->searchForm->isSubmitted() && $this->searchForm->isValid()) {
			$this->sessionData->search = $searchfor->getValue();
			$this->sessionData->regions = $searchin->getValue();
			$this->sessionData->category = $searchCat->getValue();
			$this->sessionData->onPage = 0;
		}
	}

	/**
	 * Generate table from a list of properties.
	 *
	 * @param array $results The list of results to render.
	 * @return string The property list.
	 * @throws flWidgetException
	 */
	protected function listProperties($results) {
		$content = '';

		if ($results && count($results)) {

			$types = $this->getTypes();
			$table = new flWidgetTable();
			$table->setClass('tablewidget bordered striped rowhover center responsive')
				->setHeadings(__('ID', 'propertyBackend'), __('Image', 'propertyBackend'), __('Reference / Type', 'propertyBackend'), __('Address', 'propertyBackend'), __('Type / Price', 'propertyBackend'), __('Added / Published', 'propertyBackend'), __('Operation', 'galleryBackend'))
				->setColData('propertyid', 'img', 'reference', 'address', 'priceInfo', 'addedat', 'ops')
				->skipEncode('ops', 'addedat', 'img', 'address', 'priceInfo', 'reference');

			foreach ($results as $item) {
				$f = new flDate($item['addedat'] . ' UTC', clearFusionCMS::getSysTimezone());
				$item['addedat'] = $f->toString('%d %M %Y');

if($item['published'])
					$item['addedat'] .= '<br /><span class="propertyActive">' . __('Published', 'propertyBackend') . '</span>';
				else
					$item['addedat'] .= '<br /><span class="propertyInactive">' . __('Unpublished', 'propertyBackend') . '</span>';

				$item['ops'] = "<a href=\"" . $this->assemble('property', 'edit', array($item['propertyid'])) . "\">" . __('Edit', 'propertyBackend') . "</a>";

				$item['category'] = __(ucfirst($item['category']), 'propertyBackend');

				$addr = array();
				if(!empty($item['street1']))
					$addr[] = $item['street1'];
				if(!empty($item['street2']))
					$addr[] = $item['street2'];
				if(!empty($item['city']))
					$addr[] = $item['city'];
				if(!empty($item['state']))
					$addr[] = $item['state'];
				if(!empty($item['postcode']))
					$addr[] = $item['postcode'];
				if(!empty($item['countrycode']))
					$addr[] = $item['countrycode'];
				$item['address'] = '<address>' . join(',<br />', $addr) . '</address>';

				$item['reference'] .= '<br />' . $types[$item['typeid']];

				$period = '';
				if($item['price_period'] == 'per_week')
					$period = __(' p/w', 'propertyBackend');
				else if($item['price_period'] == 'per_month')
					$period = __(' p/m', 'propertyBackend');

				$item['priceInfo'] = $item['category'] . ' / <br />' . $this->cfg->curleft . number_format($item['price'], $this->cfg->numdecimals, $this->cfg->decpoint, $this->cfg->thoussep) . $this->cfg->curright . $period;

				if(!empty($item['fileext'])) {
					$item['img'] = '<img src="' . $this->assemble('property', 'thumbnail', array($item['propertyid'])) . '" style="width: 150px" />';
				}
				else {
					$item['img'] = '';
				}

				if($item['featured'])
					$item['propertyid'] = '(' . $item['propertyid'] . ')';

				$table->addRow($item);
			}

			$content = $table->render();
		}

		return $content;
	}
	
	/**
	 * Render the page.
	 *
	 * @param string $content The page content.
	 */
	protected function renderPropertiesPage($content) {

		$cfg = clearFusionCMS::getSiteConfig('cfProperty');
		$this->tpl->getCodeBuffer()->bufferHeadJS('https://maps.googleapis.com/maps/api/js?key=' . cfGetApiKey::googleMaps())
			->bufferBodyJS('system/modules/property/js/propertyadmin.js')
			->bufferCSS('system/modules/property/css/propertyAdmin.css');

		// Create the toolbar
		$fam = new flWidgetFloatingActionMenu();
		$fam->setCSS('[[+css]]');
		$fam->addAction('add', __('Add Property', 'propertyBackend'), $this->assemble('property', 'add'));

		$this->tpl->registerChunk('fam', $fam->render());

		// Render the page
		$this->renderPage(
			__('Properties', 'propertyBackend'),
			$content,
			$this->renderBlock(
				'',
				$this->searchForm->render($this->tpl->getCodeBuffer())
			)
		);
	}

	/**
	 * Store products action.
	 */
	function propertiesAction() {
		$this->testPermission('cfProperty.properties');
		$this->breadcrumbTrail->addCrumb(__('Properties', 'propertyBackend'), $this->assemble('property', 'properties'));

		$content = '';

		$this->buildForm();

		// Get the page that we're currently on
		if($this->request->getParam('*')) {
			$this->sessionData->onPage = $this->request->getParam('*');
			if($this->sessionData->onPage !== null && count($this->sessionData->onPage) == 1 && preg_match('/^\d+$/', $this->sessionData->onPage[0]))
				$this->sessionData->onPage = $this->sessionData->onPage[0];
			else
				$this->sessionData->onPage = 0;
		}

		// Build the list of search regions
		$regions = new propertyRegions($this->db);
		$rgns = array();
		foreach($this->sessionData->regions as $r) {
			if(!in_array($r, $rgns))
				$rgns[] = $r;

			$cr = $regions->getChildren($r, array('regionid'));
			foreach($cr as $c) {
				if(!in_array($c['regionid'], $rgns))
					$rgns[] = $c['regionid'];
			}
		}

		// Build the SQL
		$sql = count($rgns) ? 'regionid IN(' . str_repeat('?,', count($rgns) - 1) . '?)' : '';
		$qparams = $rgns;

		if(!empty($this->sessionData->search)) {
			$qparams[] = $term = '%' . str_replace(array('_', '%'), array('\_', '\%'), $this->sessionData->search) . '%';
			$qparams[] = $term;
			$qparams[] = $term;
			$qparams[] = $term;
			$qparams[] = $term;
			$qparams[] = $term;
			$qparams[] = $term;
			if(!empty($sql))
				$sql .= ' && ';
			$sql .= '(title LIKE ? || reference LIKE ? || city LIKE ? || state LIKE ? || postcode LIKE ? || street1 LIKE ? || street2 LIKE ?)';
		}
		if($this->sessionData->category != '') {
			$qparams[] = $this->sessionData->category;
			if(!empty($sql))
				$sql .= ' && ';
			$sql .= 'category=?';
		}
		if(!empty($sql))
			$sql = ' WHERE ' . $sql;

		$propCount = $this->db->fetchOne('SELECT COUNT(*) FROM cf_property_' . clearFusionCMS::getSite() . $sql, $qparams);

		// Build pagination
		$pagination = new flWidgetPagination($this->sessionData->onPage, $propCount, 10);
		$pagination->setRoute($this->router->getRoute('cfManagerModule'), array('module' => 'property', 'action' => 'properties'), '*')->withPage0ID();

		if($this->sessionData->onPage && $this->sessionData->onPage >= $pagination->getNumPages()) {
			$this->sessionData->onPage = 0;
			$this->response->redirectTo($this->assemble('property', 'properties', array(), true));
		}

		if($propCount) {
			$results = $this->db->fetchAll('SELECT propertyid,fileext,street1,street2,city,state,postcode,countrycode,addedat,category,featured,price,price_period,reference,typeid,published FROM cf_property_' . clearFusionCMS::getSite() . $sql . ' ORDER BY addedat ASC, title ASC LIMIT ' . $pagination->getStart() . ',' . $pagination->getRows(), $qparams);
			$content = $this->listProperties($results);
		}

		$content .= sprintf(__('<p>Found: %d properties</p>', 'propertyBackend'), $propCount);
		$content = $this->renderBlock(__('Properties', 'propertyBackend'), $content, '', '', $pagination->render());

		// Render the page
		$this->renderPropertiesPage($content);
	}

	/**
	 * Add property to region action.
	 */
	function addAction() {
		$this->testPermission('cfProperty.properties');
		$this->breadcrumbTrail->addCrumb(__('Properties', 'propertyBackend'), $this->assemble('property', 'properties'));
		$this->breadcrumbTrail->addCrumb(__('Add Property', 'propertyBackend'), $this->assemble('property', 'add'));

		$this->buildForm();

		$regionid = $this->sessionData->lastRegion;

		list($msg, $form) = $this->propertyForm(
			0,
			array(
				'slug' => null,
				'regionid' => $regionid,
				'category' => 'buy',
				'typeid' => null,
				'published' =>  null,
				'addedat' => null,
				'features' => array(),
				'fileext' => ''
			),
			array(),
			$this->assemble('property', 'add')
		);

		$content = $this->renderBlock(__('Add Property', 'propertyBackend'), $form->render($this->tpl->getCodeBuffer()), '', $msg);

		$this->renderPropertiesPage($content);
	}

	/**
	 * Edit property action.
	 */
	function editAction() {
		$this->testPermission('cfProperty.properties');
		$this->breadcrumbTrail->addCrumb(__('Properties', 'propertyBackend'), $this->assemble('property', 'properties'));
		$this->breadcrumbTrail->addCrumb(__('Edit Property', 'propertyBackend'), $this->assemble('property', 'edit'));

		$this->buildForm();

		$msg = $this->session->flashGet('cfManagerOpSuccess') ? $this->session->flashGet('cfManagerOpSuccess') : '';

		$route = new flRouterRoute(':id', array(), array('id' => '\d+'));
		if(($params = $route->match($this->request->getParam('*', array()))) && ($data = $this->db->fetchRow('SELECT * FROM cf_property_' . clearFusionCMS::getSite() . ' WHERE propertyid=?', $params['id']))) {

			if(($content = $this->lockObject('property', $params['id'])) === true) {

				// Get the features list
				$features = array();
				$results = $this->db->fetchAll('SELECT featureid FROM cf_property_features_map_' . clearFusionCMS::getSite() . ' WHERE propertyid=?', $params['id']);
				foreach($results as $f) {
					$features[] = (int)$f['featureid'];
				}

				list($m, $form) = $this->propertyForm($params['id'], $data, $features, $this->assemble('property', 'edit', array($params['id'])));
				if(!empty($m))
					$msg = $m;

				$content = $this->renderBlock(__('Edit Property', 'propertyBackend'), $form->render($this->tpl->getCodeBuffer()), '', $msg);
			}

			$this->renderPropertiesPage($content);
		}
		else
			$this->response->redirectTo($this->assemble('property', 'properties', array(), true));
	}
	
	/**
	 * Action for deleting a property.
	 */
	function deleteAction() {
		$this->testPermission('cfProperty.properties');
		$this->breadcrumbTrail->addCrumb(__('Properties', 'propertyBackend'), $this->assemble('property', 'properties'));
		$this->breadcrumbTrail->addCrumb(__('Delete Property', 'propertyBackend'), $this->assemble('property', 'delete'));

		$this->buildForm();

		$route = new flRouterRoute(':id', array(), array('id' => '\d+'));
		if($params = $route->match($this->request->getParam('*', array()))) {
			if(($content = $this->lockObject('property', $params['id'])) === true && ($data = $this->db->fetchRow('SELECT * FROM cf_property_' . clearFusionCMS::getSite() . ' WHERE propertyid=?', $params['id']))) {
				$form = new flForm($this->request, 'deletepropform', $this->assemble('property', 'delete', array($params['id'])), true);

				$name = new flFormElementText($form, 'name', $data['title']);
				$name->setLabel(__('Delete Property', 'propertyBackend'))
					->setDisabled()
					->setReadonly();

				$delSubmit = new flFormElementSubmit($form, 'deletesubmit');
				$delSubmit->setCaption(__('Permanently Delete The Property', 'propertyBackend'))
					->setCSS('danger')
					->setDescription($this->alertMessage('Warning', __('This operation cannot be undone and will delete the property.', 'propertyBackend')));

				$msg = '';
				if($form->isSubmitted() && $form->isValid()) {
					propertyAudit::deleteProperty($params['id'], $data);

					// Delete from search
					$regions = new propertyRegions($this->db);
					$res = $regions->getPath($data['regionid'], array('regionid'));
					$segs = array('property');
					foreach($res as $row)
						$segs[] = $row['regionid'];
					$segs[] = $params['id'];

					clearFusionCMS::getEventManager()->notify(new flEvent('system.searchDelete', array(
							'path' => $segs
						))
					);

					$this->db->query('DELETE FROM cf_property_' . clearFusionCMS::getSite() . ' WHERE propertyid=?', $params['id']);
					$this->db->query('DELETE FROM cf_property_features_map_' . clearFusionCMS::getSite() . ' WHERE propertyid=?', $params['id']);

					$this->getFilename($params['id'], '');
					$this->getFilename($params['id'], '.pdf');
					$this->getFilename($params['id'], '.pdf', 'epc_');

					$this->cache->flush();
					$this->response->redirectTo($this->assemble('property', 'properties', array(), true));
				}

				$content = $this->renderBlock(__('Delete Property', 'propertyBackend'), $form->render($this->tpl->getCodeBuffer()), '', $msg);

				$this->renderPropertiesPage($content);
			}
			else
				$this->response->redirectTo($this->assemble('property', 'properties', array(), true));
		}
		else
			$this->response->redirectTo($this->assemble('property', 'properties', array(), true));
	}
	
	/**
	 * Render and process the property add/edit form.
	 *
	 * @param int $id The ID of the property being edited or 0 for add.
	 * @param array $data The data.
	 * @param array $featureData The featuers for the property.
	 * @param string $path The form submit path.
	 * @return array [ msg, form ]
	 * @throws flFormException
	 */
	protected function propertyForm($id, $data, $featureData, $path) {
		$msg = '';

		$form = new flForm($this->request, 'contentform', $path, true);
		$form->setCSS('metaSSubmit');

		// Capture ctrl+s for save
		$this->tpl->getCodeBuffer()->bufferReadyJS("
	var slugEdited = \$fl('#slug').val().length;
	function slugUpdate() {
		if(!slugEdited || \$fl('#slug').val() == '') {
			slugEdited = false;
			\$fl('#slug').val(\$fl('#title').val().toLowerCase().replace(/&amp;/g, ' ').replace(/ /g, '-').replace(/[^a-z0-9_-]/g, '').replace(/-+/g, '-').replace(/^-/, '').replace(/-$/, ''));
		}
	}

	\$fl('#slug')
		.bind('blur', slugUpdate)
		.bind('change', function(e) { slugEdited = true; });
	\$fl('#title').bind('blur', slugUpdate).bind('keyup', slugUpdate);
")->bufferHeadJS('system/app/js/cfMetaS.js');
		$this->confirmLeavePage();

		$title = new flFormElementText($form, 'title', isset($data['title']) ? $data['title'] : '');
		$title->setLabel(__('Property Title'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorMaxLength(255));

		$slug = new flFormElementText($form, 'slug', $data['slug']);
		$slug->setLabel(__('Slug', 'propertyBackend'))
			->setDescription(__('The property slug, used in the page address.', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorMaxLength(255))
			->addValidator(new flFormValidatorRegex('/^[a-z0-9_-]+$/', __('The slug may only contain letters a to z, numbers 0 to 9, _ and -.', 'propertyBackend')))
			->addValidator(new propertySlugValidator($data['regionid'], $data['slug']));

		$region = new flFormElementSelect($form, 'region', $data['regionid'], $this->getRegions());
		$region->setLabel(__('Property Region', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorNotEquals(0, __('A region must be selected.', 'propertyBackend')));

		$shortDesc = new flFormElementTextArea($form, 'shortdesc', isset($data['shortdesc']) ? $data['shortdesc'] : null);
		$shortDesc->setLabel(__('Short Description', 'propertyBackend'))
			->setDescription(__('The short description of the property', 'propertyBackend'))
			->setRows(5)
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorMaxLength(65535));

		$type = new flFormElementSelect($form, 'type', $data['typeid'], $this->getTypes());
		$type->setLabel(__('Property Type', 'propertyBackend'));

		$pricePeriod = new flFormElementSelect($form, 'price_period', isset($data['price_period']) ? $data['price_period'] : '', array(
			'buy' => __('Buy', 'peropertyBackend'),
			'per_week' => __('Per Week', 'propertyBackend'),
			'per_month' => __('Per Month', 'propertyBackend')
		));
		$pricePeriod->setLabel(__('Price Period', 'propertyBackend'));

		$tenure = new flFormElementSelect($form, 'tenure', isset($data['tenure']) ? $data['tenure'] : null, array(
			'freehold' => __('Freehold', 'propertyBackend'),
			'leasehold' => __('Leasehold', 'propertyBackend'),
			'sharedfreehold' => __('Shared Freehold', 'propertyBackend'),
			'rental' => __('Rental', 'propertyBackend')
		));
		$tenure->setLabel(__('Tenure', 'propertyBackend'));

		$published = new flFormElementCheckbox($form, 'published', $data['published']);
		$published->setLabel(__('Published', 'propertyBackend'));

		$featured = new flFormElementCheckbox($form, 'featured', isset($data['featured']) ? $data['featured'] : null);
		$featured->setLabel(__('Featured Property', 'propertyBackend'));

		$sold = new flFormElementCheckbox($form, 'sold', isset($data['sold']) ? $data['sold'] : null);
		$sold->setLabel(__('Property Sold / Rented', 'propertyBackend'));

		$imgDate = new flDate($data['addedat'] == null ? null : $data['addedat'] . ' UTC', clearFusionCMS::getSysTimezone());
		$tz = flDateTzMenu::get();

		$addedat = new flFormElementText($form, 'addedat', $imgDate->toString(flDate::DATETIMESHORT));
		$addedat->setLabel(__('Date of Property (year-month-day hh:mm)', 'propertyBackend'))
			->setRequired()
			->setDescription($this->alertMessage('Info', sprintf(__('Dates and times are %s', 'propertyBackend'), $tz[clearFusionCMS::getSysTimezone()])))
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorRegex('/^\d{4}-(0?[0-9]|1[0-2])-\d{1,2} \d{1,2}:\d{1,2}$/', __('The date must be in the form yyyy-mm-dd hh:mm.', 'propertyBackend')));

		$features = new propertyFeatures($this->db);
		$groups = $features->getChildren(0, array('featureid', 'title'));
		$featureElems = array();
		foreach ($groups as $n => $g) {
			$items = $features->getChildren($g['featureid'], array('featureid', 'title'));
			foreach($items as $f) {
				$groups[$n]['features'][$f['featureid']] = str_replace('[', '&#91;', $f['title']);
			}

			if(count($groups[$n]['features'])) {
				$cl = new flFormElementChecklist($form, 'featureset' . $n, $featureData, $groups[$n]['features']);
				$cl->setLabel($g['title']);
				$featureElems[] = $cl;
			}
		}

		$imgPane = new flFormElementPane($form, 'imgthumb', '');
		if($id && !empty($data['fileext'])) {
			$imgPane->setContent('<img src="' . $this->assemble('property', 'thumbnail', array($id)) . '" />');
			$imgPane->setLabel(__('Featured Image', 'propertyBackend'));
		}

		$imgFile = new flFormElementFile($form, 'imgfile');
		$imgFile->setLabel(__('Featured Image File', 'propertyBackend'))->setCSS('primary');

		$imgURI = new flFormElementUri($form, 'imguri');
		$imgURI->setLabel(__('Location to Download Featured Image From', 'propertyBackend'))
			->setDescription(__('Leave these fields blank to keep the current image.', 'propertyBackend'));

		$brochure = new flFormElementFile($form, 'brochure');
		$brochure->setLabel(__('Property Brochure PDF', 'propertyBackend'))->setCSS('primary');

		$epc = new flFormElementFile($form, 'epc');
		$epc->setLabel(__('EPC PDF', 'propertyBackend'))->setCSS('primary');

		// Get content fields and build content inputs
		$flds = $this->db->fetchAll('SELECT fldid,fieldname,fldtype,editor FROM cf_property_flds_' . clearFusionCMS::getSite() . ' ORDER BY disporder');
		$contents = array();
		foreach($flds as $f) {
			if($f['fldtype'] == 2) {
				$contents[$f['fldid']] = new flFormElementText($form, 'content' . $f['fldid'], isset($data["content$f[fldid]"]) ? $data["content$f[fldid]"] : '');
				$contents[$f['fldid']]->setLabel($f['fieldname'])
					->addValidator(new flFormValidatorMaxLength(255));
			}
			else {
				$contents[$f['fldid']] = cfEditor::create(
					$f['fldtype'] < 2 ? $this->getEditorModule($f['editor']) : $this->getCodeEditorModule(),
					$form,
					'content' . $f['fldid'],
					isset($data["content$f[fldid]"]) ? $data["content$f[fldid]"] : '',
					$f['fldtype'] == 1 || $f['fldtype'] == 4 ? false : true
				);
				$contents[$f['fldid']]->setLabel(sprintf(__('%s (HTML)', 'propertyBackend'), $f['fieldname']))
					->addValidator(new flFormValidatorMaxLength(2097152));
			}
		}

		$country = new flFormElementSelect($form, 'countrycode', isset($data['countrycode']) ? $data['countrycode'] : $this->sessionData->lastCountry, cfCountries::getList());
		$country->setLabel(__('Country', 'propertyBackend'))
			->setRequired();

		$price = new flFormElementText($form, 'price', floatval(isset($data['price']) ? $data['price'] : 0));
		$price->setLabel(__('Price', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorNumber());

		$depost = new flFormElementText($form, 'deposit', floatval(isset($data['deposit']) ? $data['deposit'] : 0));
		$depost->setLabel(__('Deposit', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorNumber());

		$bedrooms = new flFormElementText($form, 'bedrooms', floatval(isset($data['bedrooms']) ? $data['bedrooms'] : 1));
		$bedrooms->setLabel(__('Number of Bedrooms', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorInteger())
			->addValidator(new flFormValidatorMin(1))
			->addValidator(new flFormValidatorMax(200));

		$bathrooms = new flFormElementText($form, 'bathrooms', floatval(isset($data['bathrooms']) ? $data['bathrooms'] : 1));
		$bathrooms->setLabel(__('Number of Bathrooms', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorInteger())
			->addValidator(new flFormValidatorMin(1))
			->addValidator(new flFormValidatorMax(200));

		$ensuite = new flFormElementText($form, 'ensuite', floatval(isset($data['ensuite']) ? $data['ensuite'] : 0));
		$ensuite->setLabel(__('Number of Ensuite Bathrooms', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorInteger())
			->addValidator(new flFormValidatorMin(0))
			->addValidator(new flFormValidatorMax(200));

		$parking = new flFormElementText($form, 'parking', floatval(isset($data['parking']) ? $data['parking'] : 0));
		$parking->setLabel(__('Number of Parking Spots', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorInteger())
			->addValidator(new flFormValidatorMin(0))
			->addValidator(new flFormValidatorMax(200));

		$reception = new flFormElementText($form, 'reception', floatval(isset($data['reception']) ? $data['reception'] : 0));
		$reception->setLabel(__('Number of Reception Rooms', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorInteger())
			->addValidator(new flFormValidatorMin(0))
			->addValidator(new flFormValidatorMax(200));

		$reference = new flFormElementText($form, 'reference', isset($data['reference']) ? $data['reference'] : '');
		$reference->setLabel(__('Reference', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorMaxLength(200));

		$street1 = new flFormElementText($form, 'street1', isset($data['street1']) ? $data['street1'] : '');
		$street1->setLabel(__('Address Line 1', 'propertyBackend'))
			->addValidator(new flFormValidatorMaxLength(200))
			->addValidator(new flFormValidatorCustomJS('function(e, options) { addressToLocation(); return true; }'));

		$street2 = new flFormElementText($form, 'street2', isset($data['street2']) ? $data['street2'] : '');
		$street2->setLabel(__('Address Line 2', 'propertyBackend'))
			->addValidator(new flFormValidatorMaxLength(200))
			->addValidator(new flFormValidatorCustomJS('function(e, options) { addressToLocation(); return true; }'));

		$city = new flFormElementText($form, 'city', isset($data['city']) ? $data['city'] : '');
		$city->setLabel(__('City', 'propertyBackend'))
			->addValidator(new flFormValidatorMaxLength(50))
			->addValidator(new flFormValidatorCustomJS('function(e, options) { addressToLocation(); return true; }'));

		$state = new flFormElementText($form, 'state', isset($data['state']) ? $data['state'] : '');
		$state->setLabel(__('State / Province', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorMaxLength(50))
			->addValidator(new flFormValidatorCustomJS('function(e, options) { addressToLocation(); return true; }'));

		$zipcode = new flFormElementText($form, 'postcode', isset($data['postcode']) ? $data['postcode'] : '');
		$zipcode->setLabel(__('ZIP Code / Postcode', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorMaxLength(20))
			->addValidator(new flFormValidatorCustomJS('function(e, options) { addressToLocation(); return true; }'));

		$lat = new flFormElementText($form, 'lat', isset($data['lat']) ? $data['lat'] : 0);
		$lat->setLabel(__('Latitude of Property', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorNumber())
			->addValidator(new flFormValidatorCustomJS('function(e, options) { latlngChange(); return true; }'));

		$lng = new flFormElementText($form, 'lng', isset($data['lng']) ? $data['lng'] : 0);
		$lng->setLabel(__('Longitude of Property', 'propertyBackend'))
			->setRequired()
			->addValidator(new flFormValidatorRequired())
			->addValidator(new flFormValidatorNumber())
			->addValidator(new flFormValidatorCustomJS('function(e, options) { latlngChange(); return true; }'));

		$map = new flFormElementPane($form, 'map', '');

		$metaTitle = new flFormElementText($form, 'metaTitle', isset($data['meta_title']) ? $data['meta_title'] : null);
		$metaTitle->setLabel(__('Meta Title', 'propertyBackend'))
			->setDescription(__('Meta title for the product document', 'propertyBackend'))
			->addValidator(new flFormValidatorMaxLength(255));

		$metaDesc = new flFormElementTextArea($form, 'metaDesc', isset($data['meta_description']) ? $data['meta_description'] : null);
		$metaDesc->setLabel(__('Meta Description', 'propertyBackend'))
			->setDescription(__('Meta description for the product document', 'propertyBackend'))
			->addValidator(new flFormValidatorMaxLength(255))
			->setRows(2);

		$metaKey = new flFormElementText($form, 'metaKey', isset($data['meta_keywords']) ? $data['meta_keywords'] : null);
		$metaKey->setLabel(__('Meta Keywords', 'propertyBackend'))
			->setDescription(__('Meta keywords for the product document', 'propertyBackend'))
			->addValidator(new flFormValidatorMaxLength(255));


		list($ogTitle, $ogDesc, $twitterTitle, $twitterDesc) = cfSocialImagesMgr::buildFormElement($this->tpl->getCodeBuffer(), $form, $data, false);

		$submit = new flFormElementSubmit($form, 'update');
		$submit->setCaption($id ? __('Update Property', 'propertyBackend') : __('Add Property', 'propertyBackend'))->setCSS('primary');

		if($id) {
			$del = new flFormElementSubmit($form, 'delete');
			$del->setCaption(__('Delete Property', 'propertyBackend'))
				->setCSS('danger');

			$gal = new flFormElementSubmit($form, 'gallery');
			$gal->setCaption(__('Gallery', 'propertyBackend'))->setCSS('primary');
		}

		$form->groupElements('leftcola', array($title, $slug, $price, $pricePeriod, $depost, $region, $type, $tenure, $published, $featured, $sold))
			->groupElements('rightcola', array($reference, $shortDesc, $bedrooms, $bathrooms, $ensuite, $reception, $parking))
			->groupElements('leftcolb', array($metaTitle, $metaDesc, $metaKey))
			->groupElements('rightcolb', array($addedat))
			->groupElements('fullcoldetail', $contents)
			->groupElements('leftcoladdr', array($street1, $street2, $city))
			->groupElements('rightcoladdr', array($state, $zipcode, $country))
			->groupElements('leftcolmap', array($lat))
			->groupElements('rightcolmap', array($lng))
			->groupElements('fullcolmap', array($map))
			->groupElements('leftcolsocial', array($ogTitle, $ogDesc))
			->groupElements('rightcolsocial', array($twitterTitle, $twitterDesc))
			->groupElements('leftcolimg', array($imgFile, $imgURI))
			->groupElements('rightcolimg', array($imgPane))
			->groupElements('leftcolpdf', array($brochure))
			->groupElements('rightcolpdf', array($epc))
			->addTab(__('Details', 'propertyBackend'), array('leftcola', 'rightcola'))
			->addTab(__('Image', 'propertyBackend'), array('leftcolimg', 'rightcolimg'))
			->addTab(__('Description', 'propertyBackend'), array('fullcoldetail'))
			->addTab(__('PDF', 'propertyBackend'), array('leftcolpdf', 'rightcolpdf'));

		if(count($featureElems)) {
			$form->groupElements('fullcolf', $featureElems)
				->addTab(__('Features', 'propertyBackend'), array('fullcolf'));
		}

		$form->addTab(__('Location', 'propertyBackend'), array('leftcoladdr', 'rightcoladdr', 'leftcolmap', 'rightcolmap', 'fullcolmap'))
			->addTab(__('Meta', 'propertyBackend'), array('leftcolb', 'rightcolb'))
			->addTab(__('Social Meta', 'storeBackend'), array('leftcolsocial', 'rightcolsocial'));

		if($id) {
			$form->groupElements('submit', array($submit, $gal, $del), new flFormDecoratorList(new flFormDecoratorDl()));
		} else {
			$form->groupElements('submit', array($submit), new flFormDecoratorList(new flFormDecoratorDl()));
		}

		$form->focusElement($title);

		if($form->isSubmitted() && $form->isValid()) {
			if(isset($del) && $del->getValue())
				$this->response->redirectTo($this->assemble('property', 'delete', array($id), true));

			$imgDate = new flDate($addedat->getValue(), clearFusionCMS::getSysTimezone());
			$imgDate->setTimezone('UTC');

			$d = array(
				'title' => $title->getValue(),
				'regionid' => $this->sessionData->lastRegion = $region->getValue(),
				'addedat' => $imgDate->toString(),
				'slug' => $slug->getValue(),
				'published' => $published->getValue(),
				'category' => $pricePeriod->getValue() == 'buy' ? 'buy' : 'rent',
				'typeid' => $type->getValue(),
				'fileext' => $data['fileext'],
				'price' => $price->getValue(),
				'deposit' => $depost->getValue(),
				'bedrooms' => $bedrooms->getValue(),
				'bathrooms' => $bathrooms->getValue(),
				'ensuite' => $ensuite->getValue(),
				'reception' => $reception->getValue(),
				'parking' => $parking->getValue(),
				'reference' => $reference->getValue(),
				'street1' => $street1->getValue(),
				'street2' => $street2->getValue(),
				'city' => $city->getValue(),
				'state' => $state->getValue(),
				'postcode' => $zipcode->getValue(),
				'shortdesc' => $shortDesc->getValue(),
				'tenure' => $tenure->getValue(),
				'featured' => $featured->getValue(),
				'price_period' => $pricePeriod->getValue(),
				'lat' => $lat->getValue(),
				'lng' => $lng->getValue(),
				'countrycode' => $this->sessionData->lastCountry = $country->getValue(),
				'ogtitle' => $ogTitle->getValue(),
				'ogdesc' => $ogDesc->getValue(),
				'twittertitle' => $twitterTitle->getValue(),
				'twitterdesc' => $twitterDesc->getValue(),
				'sold' => $sold->getValue(),
				'meta_title' => $metaTitle->getValue(),
				'meta_description' => $metaDesc->getValue(),
				'meta_keywords' => $metaKey->getValue()
			);

			$productContent = ''; // Combined content for use in search
			foreach($contents as $cid => $elem) {
				$d["content$cid"] = $elem->getValue();
				$productContent .= $elem->getValue() . ' ';
			}

			// Update
			if($id) {
				if ($this->db->update('cf_property_' . clearFusionCMS::getSite(), $d, 'propertyid=?', $id)) {
					propertyAudit::updateProperty($id, $title->getValue(), $data);

					// Write the features for the property
					$this->db->query('DELETE FROM cf_property_features_map_' . clearFusionCMS::getSite() . ' WHERE propertyid=?', $id);
					foreach($featureElems as $el) {
						/** @var flFormElement $el */
						$propFeatures = $el->getValue();
						foreach($propFeatures as $f) {
							$this->db->insert('cf_property_features_map_' . clearFusionCMS::getSite(), array(
								'propertyid' => $id,
								'featureid' => $f
							));
						}
					}

					// Store the image
					$this->storeImage($id, $imgFile, $imgURI);
					$this->storePDF($id, $brochure);
					$this->storePDF($id, $epc, 'epc_');

					// Purge the caches
					$content = new cfContent($this->db, clearFusionCMS::getSite());
					if (!empty($ext) && ($container = $content->getContainer('property.region.' . $d['regionid']))) {
						$route = $content->getContainerRoute($container, 'image/:size/:slug', array(), array(), $ext);

						clearFusionCMS::getEventManager()->notify(new flEvent('system.cachePurge', array('files' => array(
							$route->assemble(array('size' => 'o', 'slug' => $d['slug'])),
							$route->assemble(array('size' => 't', 'slug' => $d['slug'])),
							$route->assemble(array('size' => 's', 'slug' => $d['slug'])),
							$route->assemble(array('size' => 'm', 'slug' => $d['slug'])),
							$route->assemble(array('size' => 'l', 'slug' => $d['slug']))
						))));
					}

					$regions = new propertyRegions($this->db);

					// If region changed then delete the old location
					if($d['regionid'] != $data['regionid']) {
						$res = $regions->getPath($data['regionid'], array('regionid'));
						$segs = array('property');
						foreach($res as $row)
							$segs[] = $row['regionid'];
						$segs[] = $id;

						clearFusionCMS::getEventManager()->notify(new flEvent('system.searchDelete', array(
								'path' => $segs
							))
						);
					}

					// Index into search
					$res = $regions->getPath($d['regionid'], array('regionid'));
					$segs = array('property');
					foreach($res as $row)
						$segs[] = $row['regionid'];
					$segs[] = $id;

					if(!$published->getValue()) {
						$e = new flEvent('system.searchDelete', array(
							'path' => $segs
						));
					}
					else {
						// Send to search
						$e = new flEvent('system.searchIndex', array(
							'path' => $segs,
							'accessPerm' => '',
							'publishedAt' => $addedat->getValue(),
							'fields' => array(
								'property.title' => $d['title'],
								'property.address' => $d['street1'] . ' ' . $d['street2'] . ' ' . $d['city'] . ' ' . $d['state'] . ' ' . $d['postcode'],
								'property.price' => $price->getValue(),
								'property.category' => $d['category'] == 'buy' ? 1 : 0,
								'property.bedrooms' => $d['bedrooms'],
								'property.bathrooms' => $d['bathrooms'],
								'property.parking' => $d['parking']
							)
						));
					}
					clearFusionCMS::getEventManager()->notify($e);

					$this->cache->flush();

					if($gal->getValue())
						$this->response->redirectTo($this->assemble('property', 'gallery', array($id), true));

					$this->session->flashSet('cfManagerOpSuccess', $this->alertMessage('Success', __('The property has been updated.', 'propertyBackend')));
					$this->response->redirectTo($this->assemble('property', 'edit', array($id), true));
				} else
					$msg = $this->alertMessage('Error', __('Unable to update the property, please try again.', 'propertyBackend'));
			}
			// Add
			else {
				if($this->db->insert('cf_property_' . clearFusionCMS::getSite(), $d)) {
					$id = $this->db->insertID();
					propertyAudit::createProperty($id, $title->getValue());

					// Write the features for the property
					foreach($featureElems as $el) {
						/** @var flFormElement $el */
						$propFeatures = $el->getValue();
						foreach($propFeatures as $f) {
							$this->db->insert('cf_property_features_map_' . clearFusionCMS::getSite(), array(
								'propertyid' => $id,
								'featureid' => $f
							));
						}
					}

					// Store the image
					$this->storeImage($id, $imgFile, $imgURI);
					$this->storePDF($id, $brochure);
					$this->storePDF($id, $epc, 'epc_');

					$this->cache->flush();

					$this->session->flashSet('cfManagerOpSuccess', $this->alertMessage('Success', __('The property has been added to the region.', 'propertyBackend')));
					$this->response->redirectTo($this->assemble('property', 'edit', array($id), true));
				} else {
					$msg = $this->alertMessage('Error', __('Unable to create the new property, please try again.', 'propertyBackend'));
				}
			}
		}

		return array($msg, $form);
	}

	/**
	 * Write the main image to storage.
	 *
	 * @param int $id Id of the property
	 * @param flFormElementFile $imgFile The upload object.
	 * @param flFormElementUri $imgURI The URI to grab the image from.
	 */
	protected function storeImage($id, $imgFile, $imgURI) {
		$fileCfg = clearFusionCMS::getSiteConfig('cfSystemFileStore');
		$ext = '';

		// If upload file given
		if($imgFile->fileUploaded()) {
			try {
				$img = new flImage($imgFile->getTmpFileName());
				switch($img->getType()) {
					case IMAGETYPE_GIF: $ext = '.gif'; break;
					case IMAGETYPE_JPEG: $ext = '.jpg'; break;
					case IMAGETYPE_PNG: $ext = '.png'; break;
				}
			} catch(flException $e) { }
		}
		// Else if download file give
		else if($imgURI->getValue() != '') {

			// Get the image from a remote source
			$http = new flHttpClient($imgURI->getValue());
			if($http->sendRequest() && $http->getResponseCode() == 200) {
				// Save the file data
				$tmpFile = tempnam($fileCfg->privateStorage, 'property');
				@file_put_contents($tmpFile, $http->getBody());
				unset($http); // Release the object & memory

				try {
					$img = new flImage($tmpFile);
					switch($img->getType()) {
						case IMAGETYPE_GIF: $ext = '.gif'; break;
						case IMAGETYPE_JPEG: $ext = '.jpg'; break;
						case IMAGETYPE_PNG: $ext = '.png'; break;
					}
				} catch(flException $e) {
					@unlink($tmpFile);
				}
			}
		}

		// if new file given
		if(!empty($ext)) {
			// If upload file given
			if($imgFile->fileUploaded())
				$imgFile->moveTo($this->getFilename($id, $ext));
			// Else download file
			else
				@rename($tmpFile, $this->getFilename($id, $ext));
			$this->db->update('cf_property_' . clearFusionCMS::getSite(), array('fileext' => $ext), 'propertyid=?', $id);
		}
	}

	/**
	 * Write the PDF to storage.
	 *
	 * @param int $id Id of the property
	 * @param flFormElementFile $file The PDF file.
	 * @param string $prefix Optional file prefix.
	 */
	protected function storePDF($id, $file, $prefix = null) {
		$ext = '';

		// If upload file given
		if($file->fileUploaded()) {
			try {
				$type = pathinfo($file->getFileName(), PATHINFO_EXTENSION);
				switch($type) {
					case 'pdf': $ext = '.pdf'; break;
				}
			} catch(flException $e) { }
		}

		// if new file given
		if(!empty($ext)) {
			$file->moveTo($this->getFilename($id, $ext, $prefix));
		}
	}

	/**
	 * Get the filename to store an image under.
	 *
	 * Folders are created if required and old images are removed.
	 *
	 * @param int $id The ID of the product.
	 * @param string $ext The file extension.
	 * @param string $prefix Optional file prefix.
	 * @return bool|string The filename to use or false if failed.
	 */
	protected function getFilename($id, $ext, $prefix = '') {
		$idHash = md5($id);

		// Calculate the storage path and file name
		$cfg = clearFusionCMS::getSiteConfig('cfSystemFileStore');
		$path = '/property/' . clearFusionCMS::getSite() . '/' . $idHash[0] . '/' . $idHash[1] . '/';
		$file = $path . $prefix . $id . $ext;
		$store = $ext == '.pdf' ? $cfg->publicStorage : $cfg->privateStorage;

		// Create the folder if not already present
		if(!is_writable($store . $path)) {
			if(!@mkdir($store . $path, 0777, true))
				return false;
		}

		// Remove the old file if any
		if($ext == '.pdf') {
			@unlink($store . $file);
		}
		else {
			foreach(glob($store . $path . $id . '.*') as $f)
				@unlink($f);

			foreach(glob($store . $path . $id . '_*.*') as $f)
				@unlink($f);
		}

		return $store . $file;
	}

	/**
	 * Get the thumb of the property image.
	 *
	 * @throws flExceptionNotFound
	 */
	function thumbnailAction() {
		$this->testPermission('cfProperty.properties');

		$route = new flRouterRoute(':id/:galID', array('galID' => ''), array('id' => '\d+', 'galID' => '\d+'));
		if($params = $route->match($this->request->getParam('*', array()))) {

			if(empty($params['galID']))
				$ext = $this->db->fetchOne('SELECT fileext FROM cf_property_' . clearFusionCMS::getSite() . ' WHERE propertyid=?', $params['id']);
			else
				$ext = $this->db->fetchOne('SELECT fileext FROM cf_property_gal_' . clearFusionCMS::getSite() . ' WHERE propertyid=? && galid=?', $params['id'], $params['galID']);

			if($ext) {
				$cfg = clearFusionCMS::getSiteConfig('cfSystemFileStore');

				$idHash = md5($params['id']);
				$sub = $params['galID'] ? "-$params[galID]" : '';
				$file = $cfg->privateStorage . '/property/' . clearFusionCMS::getSite() . '/' . $idHash[0] . '/' . $idHash[1] . "/$params[id]{$sub}{$ext}";

				// Send the file out
				$this->response->setContentType(flMimeTypes::getType(strtolower(basename($file))))
					->compressOutput(false)
					->noCache()
					->setHeader('Content-Disposition', 'inline; filename=' . basename($file))
					->setLastModified(filemtime($file))
					->sendHeaders();

				$img = new flImage($file);
				$img->resizeCrop(300, 225);
				$img->output();
			}
			else
				throw new flExceptionNotFound();
		}
		else
			throw new flExceptionNotFound();
	}
}
