<?php
App::uses('Comment', 'Model');

class Page extends AppModel {

	# Page types
	const TYPE_PAGE = 'Page';
	const TYPE_WRAPPER = 'Wrapper';
	const TYPE_LINK = 'Link';
	const TYPE_MODULE = 'Module';
	const TYPE_CUSTOM = 'Custom';
	const TYPE_CATEGORY = 'Category';

	var $actsAs = [ 'Tree', 'Seo.LastMod', 'Seo.MetaTagged', 'Seo.MetaGenerated', 'Seo.SlugLogged' ];

	var $displayField = 'title';

	var $virtualFields = [ 'selected' => 'FALSE' ];

	var $invisible = [ 'module_id' ];

	var $belongsTo = [
		'Layout',
		'Module'
	];

	var $hasMany = [
		'Images'    => 'Upload jpeg jpg png gif',
		'Documents' => 'Upload pdf doc docx xls xlsx ppt pptx odt txt jpg jpeg png gif'
	];

	var $hasOne = [
		'Image'     => 'Upload jpeg jpg png gif',
		'ListImage'     => 'Upload jpeg jpg png gif',
		'MetaImage' => 'Upload jpeg jpg png gif'
	];

	var $path = null;

	var $meta = [ 'title', 'keywords', 'description' ];

	# Sitemap variables
	public $sitemap;
	public $flatSitemap;
	private $internals;

	public $isCopyable = false;

	public $sitemapFields = [ 'Page.*' ];

	# ~ Retrieves the link for the supplied page - - - - - - - - - - - - - - - - - #
	function getPageLink($pageId, $locale = null, $wrapper = false, $appendLocale = true) {

		# By specific locale
		if ($locale !== null) {
			$this->setVirtual($locale);
		}

		# By pageId or module name
		$conditions = is_numeric($pageId)
			? [ 'Page.id' => $pageId ]
			: [
				'Page.type'   => 'Module',
				'Module.name' => $pageId ];

		$page = $this->find(
			'first',
			[
				'Get link for the single page',
				'contain'    => !is_numeric($pageId) ? [ 'Module' ] : [],
				'all'        => true,
				'conditions' => $conditions,
				'fields'     => [
					'Page.id', 'Page.type', 'Page.slug', 'Page.url', 'Page.parent_id' ] ]
		);

		# If no page found
		if (!$page) {
			return null;
		}

		# If page is actually a wrapper
		if (!$wrapper && $page['Page']['type'] == Page::TYPE_WRAPPER) {
			$id = $this->field('id', [ 'Page.parent_id' => $page['Page']['id'] ]);
			if ($id) {
				return $this->getPageLink($id, $locale);
			}

			# If page is actually a link
		} else if ($page['Page']['type'] == Page::TYPE_LINK) {

			# Reset virtual fields
			$this->setVirtual();

			# Page interlinking
			if (preg_match('~^\s*page:(?P<id>\d+)\s*(?P<hash>#.*)\s*$~', $page['Page']['url'], $url)) {
				return $this->getPageLink($url['id']) . (!empty($url['hash']) ? $url['hash'] : null);

				# External page
			} else {
				return $page['Page']['url'];
			}
		}

		# Get path of the page
		$path = $this->getPath($page['Page']['parent_id'], [ 'slug' ]);

		# Generate link
		$link = '/';
		if (!empty($path)) {
			foreach ($path as $i => $node) {

				# Skip invisible sitemap elements
				if ($i >= STRUCTURE_SKIP) {
					$link .= $node['Page']['slug'] . '/';
				}
			}
		}

		# Reset virtual fields
		$this->setVirtual();

		# Skip last slug for modules
		if (is_numeric($pageId)) {
			$link .= $page['Page']['slug'];
		}

		if ($appendLocale) {
			return Language::urlLocale($link);
		} else {
			return $link;
		}
	}

	# ~ Get page from sitemap - - - - - - - - - - - - - - - - - - - - - - - - - - -#
	public function getPageFromSitemap($pageId, $sitemap, $field = null) {
		$page = [];

		foreach ($sitemap as $onePage) {
			if ($onePage['Page']['id'] == $pageId) {
				$path = !empty($field) ? $onePage['Page'][$field] : $onePage;
				return $path;
			}
			if (!empty($onePage['children'])) {
				$childs = $this->getPageFromSitemap($pageId, $onePage['children'], $field);
				if (!empty($childs)) {
					return $childs;
				}
			}
		}
		return null;
	}

	function beforeSave($options = []) {

		# Make sure the page supports polyglot
		$languages = Configure::read('Config.Languages');
		if (!$this->polyglotFields) {
			$languages = [ '' => null ];
		}

		# Create short title and slug
		foreach ($languages as $locale => $params) {
			$suffix = $locale ? "__{$locale}" : null;

			# Save some default title for module pages
			if (!empty($this->data['Page']['module_id']) && isset($this->data['Page']['type']) && $this->data['Page']['type'] == 'Module') {
				$this->data['Page']['is_translated' . $suffix] = true;
				$this->data['Page']['title' . $suffix] = 'Module: ' . $this->data['Page']['module_id'];
				$this->data['Page']['slug' . $suffix] = 'Module: ' . $this->data['Page']['module_id'];
			}

			# Generate short title if none is provided
			if (isset($this->data['Page']['title_short' . $suffix])) {
				if (trim($this->data['Page']['title_short' . $suffix]) == '' && !empty($this->data['Page']['title' . $suffix])) {
					$this->data['Page']['title_short' . $suffix] = $this->data['Page']['title' . $suffix];
				} else {
					$this->createSlug($this->data['Page']['title_short' . $suffix], 'slug' . $suffix, $this->data['Page'], $locale);
				}
			}

			# Make sure that error page is always translated
			if ($this->data['Page']['id'] === '1') {
				foreach ($languages as $locale => $language) {
					$suffix = $locale ? "__{$locale}" : null;
					$this->data['Page']["is_translated{$suffix}"] = true;
				}
			}
		}

		return true;
	}

	function beforeFind($query) {
		if (empty($query['limit']) || $query['limit'] !== 1) {
			$query['conditions'][] = [
				'Page.id <>' => 1,
				'Page.type'  => [ Page::TYPE_PAGE, Page::TYPE_WRAPPER, Page::TYPE_LINK, Page::TYPE_CUSTOM ]
			];
		}

		# Hide all inactive pages on front
		if (!CMS) {
			$query['conditions']['Page.is_active'] = true;
		}

		return $query;
	}

	function afterFind($results, $primary = false) {

		$meta = [ 'keywords', 'description', 'title' ];
		foreach ($results as $i => $result) {

			# Apply default meta tags if no specified
			foreach ($meta as $tag) {
				if (isset($result[$this->alias]["meta_{$tag}"]) && empty($result[$this->alias]["meta_{$tag}"])) {
					$results[$i][$this->alias]["meta_{$tag}"] = Configure::read("Meta.{$tag}");
				}
			}
		}

		return $results;
	}

	# ~ Returns the complete sitemap - - - - - - - - - - - - - - - - - - - - - - - #
	public function getSitemap($page = null, $conditions = [], $locale = null) {
		setDefault($page, $this->data);

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

		# Check stash
		$stashed = Stash::read("Sitemap:{$locale}");
		if ($stashed) {
			return $stashed;
		}

		# Read the sitemap
		$this->sitemap = $this->find(
			'threaded',
			[
				'Read sitemap',
				'recursive'  => -1,
				'conditions' => $conditions,
				'fields'     => $this->sitemapFields ]
		);

		$this->sitemap = $this->packResults($this->sitemap, 'slug', true);

		# Append and mark categories
		//$this->sitemap = $this->appendCategories();
		//$this->sitemap = $this->markCategories();

		# Set paths for each page in sitemap
		$this->setPaths();

		# Appen module ids.
		//$this->sitemap = $this->markCategories();

		return Stash::write("Sitemap:{$locale}", $this->sitemap);
	}

	# ~ Mark categories with proper data - - - - - - - - - - - - - - - - - - - - - #
	public function markCategories($sitemap = null, $module = null) {
		setDefault($sitemap, $this->sitemap);
		foreach ($sitemap as $i => $page) {
			$sitemap[$i]['Page']['module_id'] = !empty($page['Page']['module_id']) ? $page['Page']['module_id'] : $module;
			$sitemap[$i]['children'] = $this->markCategories($sitemap[$i]['children'], $sitemap[$i]['Page']['module_id']);
		}

		return $sitemap;
	}

	# ~ Appends the categories to the sitemap  - - - - - - - - - - - - - - - - - - #
	public function appendCategories($sitemap = null, $modules = null) {
		setDefault($sitemap, $this->sitemap);
		setDefault($modules, Stash::read('Modules'));

		# For each page in the current level
		foreach ($sitemap as $slug => $page) {

			# Page found
			if ($page['Page']['type'] == 'Page' && $page['Page']['module_id'] != null) {
				foreach ($modules as $mod) {
					if ($mod['Module']['id'] == $page['Page']['module_id']) {
						$module = $mod['Module']['name'];
						continue;
					}
				}
				$children = Stash::read("Categories.{$module}");
				$sitemap[$slug]['children'] = $this->fromCategories($children, $page);

				# Apply recursively
			} else {
				$sitemap[$slug]['children'] = $this->appendCategories($page['children'], $modules);
			}
		}

		return $sitemap;
	}

	# ~ Iterates trought sitemap and sets the path for each page - - - - - - - - - #
	private function setPaths($tree = null, $path = null, &$parent = null, $level = 0) {
		setDefault($tree, $this->sitemap);

		foreach ($tree as $i => $node) {
			$pre = $level > STRUCTURE_SKIP && $path && $path != '/' ? $path . '/' : Language::urlLocale('/');
			$tree[$i]['Page']['path'] = $level ? $pre . $node['Page']['slug'] : '/';
			$tree[$i]['children'] = $this->setPaths($node['children'], $tree[$i]['Page']['path'], $tree[$i]['Page'], $level + 1);

			# For link type
			if ($tree[$i]['Page']['type'] === Page::TYPE_LINK) {
				if (preg_match('~^\s*page:(?P<id>\d+)\s*(?P<hash>#.*)?\s*$~', $node['Page']['url'], $url)) {
					$tree[$i]['Page']['path'] = $this->getPageLink($url['id']) . (!empty($url['hash']) ? $url['hash'] : null);
				} else {
					$tree[$i]['Page']['path'] = $tree[$i]['Page']['url'];
				}
			}
		}

		# Update parent node, if it is wrapper
		if ($parent && $parent['type'] === Page::TYPE_WRAPPER && !empty($tree)) {
			$page = reset($tree);
			$parent['path'] = $page['Page']['path'];
		}

		# Update sitemap
		if (!$level) {
			$this->sitemap = $tree;
		}

		return $tree;
	}

	# ~ Parse page_id:{n} links, and specific module links underscored_module:{n}- #
	public function parseLink($url) {
		if (preg_match('~^\s*page:(?P<id>\d+)\s*(?P<hash>#.*)?\s*$~', $url, $matches)) {
			$url = $this->getPageLink($matches['id']) . (!empty($matches['hash']) ? $matches['hash'] : null);
		} else if (preg_match('~^\s*(?P<model>[a-z_]*):(?P<id>\d+)\s*(?P<hash>#.*)?\s*$~', $url, $matches)) {
			$url = ClassRegistry::init(Inflector::classify($matches['model']))->getLink($matches['id']);
		} else {
			return $url;
		}
		return $this->parseLink($url);
	}

	# ~ Iterates trought sitemap and sets the path for each page - - - - - - - - - #
	public function setLinkPaths($tree = null, $level = 0) {
		setDefault($tree, $this->sitemap);

		# Set flattened sitemap
		if (empty($this->flatSitemap)) {
			$this->flatSitemap = Set::flatten($this->sitemap);
		}

		foreach ($tree as $i => $node) {

			# Work only on Link pages
			if ($node['Page']['type'] == Page::TYPE_LINK) {

				# Unset all children
				$tree[$i]['children'] = [];

				# Internal links
				if (preg_match('~page:(\d+)~', $node['Page']['url'], $url)) {
					foreach ($this->flatSitemap as $key => $value) {
						if (substr($key, -8) == '.Page.id' && $value == $url[1]) {
							$path = str_replace('.', "']['", substr($key, 0, -3));
							$path = "['{$path}']['path']";
							$tree[$i]['Page']['path'] = eval("return \$this->sitemap{$path};");
						}
					}

					# External links
				} else {
					$tree[$i]['Page']['path'] = $node['Page']['url'];
				}

			} else {
				$tree[$i]['children'] = $this->setLinkPaths($node['children'], $level + 1);
			}
		}

		# Update sitemap
		if (!$level) {
			$this->sitemap = $tree;
		}

		return $tree;
	}

	# ~ Returns the list of parents by level - - - - - - - - - - - - - - - - - - - #
	public function getLevel($level = 0, $sitemap = null, $i = 0) {
		setDefault($sitemap, Stash::read('Sitemap'));

		# Skip the first n levels
		if ($i < STRUCTURE_SKIP) {
			foreach ($sitemap as $item) {
				$found = $this->getLevel($level, $item['children'], $i);
				if ($found) {
					return $found;
				}
			}
		}

		# Iterate trough sitemap
		foreach ($sitemap as $slug => $item) {

			# If found return page
			if (isset($this->path[$level]) && $slug == $this->path[$level]) {
				return $item;

				# Go down the tree
			} else if (isset($this->path[$i + 1]) && $slug == $this->path[$i]) {
				return $this->getLevel($level, $item['children'], $i + 1);
			}
		}

		return null;
	}

	# ~ Get childred by path - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function getByPath($path, $sitemap = null, $level = 0) {
		if ($sitemap === null)
			$sitemap = $this->getSitemap();

		$node = explode('/', trim($path, '/ '));

		# Iterate trough sitemap
		foreach ($sitemap as $item) {

			# If found return page
			if ($item['Page']['path'] == $path) {
				return $item;

				# Go down
			} else if (isset($node[$level]) && $item['Page']['slug'] == $node[$level]) {
				return $this->getByPath($path, $item['children'], $level + 1);
			}
		}

		return null;
	}

	# ~ Transform categories to pages  - - - - - - - - - - - - - - - - - - - - - - #
	public function fromCategories($categories, $host) {

		$pages = [];
		foreach ((array) $categories as $category) {
			$id = $category['Category']['id'];
			$slug = $category['Category']['slug'];
			$title = $category['Category']['title'];

			# Page
			$pages[$slug]['Page'] = [
				'type'        => 'Category',
				'category'    => $id,
				'id'          => $host['Page']['id'],
				'parent_id'   => null,
				'is_default'  => false,
				'is_visible'  => true,
				'selected'    => '0',
				'title'       => $title,
				'title_short' => $title,
				'slug'        => $slug
			];

			# Apply recursivelly
			$pages[$slug]['children'] = $this->fromCategories($category['children'], $host);
		}

		return $pages;
	}

	# ~ Fetch page by supplied path	 - - - - - - - - - - - - - - - - - - - - - - - #
	function fetch($path = [], $recursion = 0) {
		# Read sitemap
		$pageId = null;
		$moduleId = null;
		$this->sitemap = $this->getSitemap();

		# If page id has been supplied
		if (is_numeric($path)) {
			$pageId = $path;

			# Else get page by path
		} else {

			# Convert URL (string) to path (array)
			if (is_string($path)) {
				$path = explode('/', trim($path));
			}

			# Somethimes URL decode is required
			foreach ($path as $i => $slug) {
				$path[$i] = urldecode($slug);
			}

			# Remove empty items
			$path = array_filter($path);

			# Load default page on empty path
			if (!$path) {
				$path = [ $this->field('slug', [], [ 'Page.is_default' => DESC ]) ];
			}

			# Skip structure page nodes
			$sitemap = [ 'children' => [] ];
			foreach ($this->sitemap as $slug => $data) {
				$sitemap['children'] = array_merge($sitemap['children'], $data['children']);
			}

			# Get the page
			$pointer = $sitemap;
			$moduleId = null;
			$checkModule = false;
			foreach ($path as $i => $slug) {

				if (isset($pointer['children'][$slug])) {
					$pointer = $pointer['children'][$slug];
					$pageId = $pointer['Page']['id'];

					if (!empty($pointer['Page']['module_id'])) {
						$moduleId = $pointer['Page']['module_id'];
					}

					# Crumb not found
				} else if (empty($moduleId)) {

					# Module page
					if (!empty($pointer['Page']) && $pointer['Page']['module_id'] !== null) {
						$moduleId = $pointer['Page']['module_id'];

						# Check top wrappers
					} else if (!empty($pointer['Page']['parent_id'])) {
						foreach ($this->sitemap as $sItem) {
							if (!empty($sItem['Page']) && $sItem['Page']['id'] == $pointer['Page']['parent_id'] && $sItem['Page']['module_id'] !== null) {
								$moduleId = $sItem['Page']['module_id'];
								$checkModule = true;
							}
						}
						# Error 404
					} else {
						$pageId = 1;
						break;
					}

				} else {
					$checkModule = true;
				}
			}

			# Module template
			if ($moduleId && $checkModule) {
				$page = $this->readModuleAsPage($moduleId, $slug);

				# If page not found, display 404
				if ($page === null) {
					$pageId = 1;

				} else if (!is_array($page)) {
					$path[$i] = $page;
					return '/' . implode('/', $path);

				} else {
					$slug = $page['Page']['slug'];
				}
			}

			if ($pageId == 1) {

				# Set 404 response code
				header($_SERVER["SERVER_PROTOCOL"] . " 404 Not Found");
				header("Status: 404 Not Found");

				# Try with slug logs
				$updatedSlug = ClassRegistry::init('Seo.SlugLog')->getPageSlug($slug, $pointer);
				if (!empty($updatedSlug)) {
					$link = $this->getPageLink($updatedSlug['page_id'], null, false, false);

					# Remove paths up to this one
					$pathEnd = array_slice($path, $i + 1);
					$pathStart = explode('/', $link);

					# Generate new path
					$path = array_merge($pathStart, $pathEnd);
					$path = array_filter($path);

					# Generate link and return as string
					$link = '/' . implode('/', $path);
					$link = Language::urlLocale($link);
					return $link;
				}
			}

			# Highlight the path to the page
			$this->path = $path;
			if (STRUCTURE_SKIP) {
				$structure = array_keys($this->sitemap);
				foreach ($structure as $key) {
					$marked = false;
					$this->sitemap[$key]['children'] = $this->highlightNodes($this->sitemap[$key]['children'], 'slug', $this->path, 'selected', $marked);
					$this->sitemap[$key]['Page']['selected'] = $marked;
				}
			} else {
				$this->sitemap = $this->highlightNodes($this->sitemap, 'slug', $this->path);
			}

			# Update the sitemap with highlighted nodes
			Stash::write('Sitemap', $this->sitemap);
		}

		# Try to read as static page
		if (empty($page) && $pageId) {

			# Read the page
			while (empty($page) && $pageId) {
				$page = $this->find('first', [
					'Read the page itself',
					'contain'    => [ 'Images', 'Image', 'ListImage', 'Documents', 'MetaImage' ],
					'raw'        => true,
					'conditions' => [
						'Page.id' => $pageId ]
				]);
				$pageId = (int) ($pageId !== 1);
			}

			# Put the data into page
			if (!empty($pointer)) {
				$page = Set::merge($page, $pointer);
			}

			# Read the layout
			$layout = $this->Layout->find('first', [
				'Get layout for the page',
				'recursive'  => -1,
				'fields'     => [
					'Layout.id', 'Layout.name', 'Layout.body',
					'Layout.stylesheet_set', 'Layout.javascript_set' ],
				'conditions' => [
					'Layout.id' => $page['Page']['layout_id'] ]
			]);
			$page['Layout'] = $layout ? $layout['Layout'] : null;

			# Handle linked pages
			if ($page && $page['Page']['type'] == Page::TYPE_LINK && preg_match('~^\s*page:(?P<id>\d+)\s*(?P<hash>#.*)\s*$~', $page['Page']['url'], $url)) {

				# Get linked page
				$alias = $this->fetch($url[1], $recursion + 1);

				# Update data
				$page['Page']['layout_id'] = $alias['Page']['layout_id'];
				$alias['Page'] = $page['Page'];
				$page = $alias;
			}
		}
		$page['Content'] = ClassRegistry::init('Content')->getContent('Page', $page['Page']['id']);
		if (empty($this->forceFields['Page']['content']))
			$page['Page']['content'] = $page['Content']['Body'];

		return $this->set($page);
	}

	# ~ Get the page from the module name and slug - - - - - - - - - - - - - - - - #
	public function readModuleAsPage($module, $slug, $pageId = null) {

		# Load module
		$this->Module->recursive = -1;
		$module = $this->Module->read([ 'name', 'id' ], $module);

		if (!$module) {
			throw new Exception('Module with id = ' . $module . ' not found.');
		}

		# Read the module
		$Model = ClassRegistry::init($module['Module']['name']);

		$data = $Model->find('first', [
			'Read the module details',
			'conditions' => [
				"{$module['Module']['name']}.slug" => $slug ]
		]);

		# Try with slug log if data empty
		if (empty($data)) {
			$slugData = ClassRegistry::init('Seo.SlugLog')->getModuleSlug($slug, $Model, $pageId);
			if (!empty($slugData)) {
				return $slugData['to_slug'];
			}
		}

		# Set the page up
		if ($data) {

			# If there is pageId (page as category navigation) return closest page, not module template page
			$conditions = empty($pageId)
				? [
					'Page.type'      => 'Module',
					'Page.module_id' => $module['Module']['id'] ]
				: [
					'Page.id' => $pageId ];

			$var = Inflector::variable($module['Module']['name']);
			$page = $this->find('first', [
				'Read the page for the module details',
				'conditions' => $conditions
			]);

			if (empty($page['Module']['name'])) {
				$page['Module'] = $module['Module'];
			}

			$page['Data'] = $data[$Model->alias];

			# Prevent searching for parent if pageId provided
			if (!empty($pageId)) {
				$parent = $page;
			} else {
				$parent = $this->find('first', $page['Page']['parent_id']);
			}
			$page['Page']['title'] = $data[$Model->alias][$Model->displayField];
			$page['Page']['lft'] = $parent['Page']['lft'];
			$page['Page']['rght'] = $parent['Page']['rght'];;

			if ($Model->hasComments) {
				if (!isset($data['Comments'])) {
					$commentsInstance = ClassRegistry::init("Comment");
					$comments = $commentsInstance->find('all', [
						'conditions' => [
							'Comment.model'       => $Model->alias,
							'Comment.foreign_key' => $data[$Model->alias]['id'],
							'Comment.is_active'   => true
						],
						'order'      => 'Comment.created DESC'
					]);
					$data['Comments'] = $comments;
				}
			}

			$page['Variables'][$var] = $page['Data'] = $data;

			# Pass module generated tags to page
			if (!empty($data[$Model->alias]['seo_generated_metatags'])) {
				$page['Page']['seo_generated_metatags'] = $data[$Model->alias]['seo_generated_metatags'];
			}

			return $page;
		}

		return null;
	}

}
