<?php

class SitemapGenerator extends SeoAppModel {

	const SITEINDEX_LIMIT = 10000;

	var $useTable = false;

	var $currentMap = 0;
	var $skipLanguages = ['es'];

	private $paths = [];
	private $stashedPages = [];

	# ~ Overriden constructor- - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function __construct($id = false, $table = null, $ds = null) {
		parent::__construct($id, $table, $ds);

		# Define base url
		$this->baseUrl = rtrim(FRONT_BASE_URL, '/');

		# Get all languages
		$this->languages = Configure::read('Config.Languages');

		foreach ($this->languages as $language => $languageData) {
			if (in_array($language, $this->skipLanguages)) {
				unset($this->languages[$language]);
			}
		}

		$this->SitemapItem = ClassRegistry::init('Seo.SitemapItem');
	}

	# ~ Build sitemap- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function buildSitemap() {
		$this->SitemapItem->invalidateSitemap();

		# Get page items
		$pageItems = $this->getPages();

		# Get module items
		$moduleItems = $this->getModules();

		# Join all items
		$allItems = array_merge($pageItems, $moduleItems);

		# Store in database
		$this->storeInDatabase($allItems);
	}

	# ~ Get page items - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function getPages() {
		$this->Page = ClassRegistry::init('Page');

		$sitemapItems = [];
		# Get pages for all languages
		foreach ($this->languages as $language => $languageInfo) {
			# Set locale
			$this->locale = $language;
			$this->Page->setVirtual($language);

			# Get pages
			$pages = $this->Page->getSitemap();
			$extracted = $this->extractPageItems($pages);
			$sitemapItems = array_merge($sitemapItems, $extracted);

		}
		return $sitemapItems;
	}

	# ~ Get module items - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function getModules() {
		$sitemapItems = [];

		# Get modules with has_details
		$modules = ClassRegistry::init('Module')->find('all', [
			'recursive' => -1,
		]);

		# Get all items for each language for each module
		foreach ($this->languages as $language => $languageData) {
			foreach ($modules as $module) {
				$this->locale = $language;
				$sitemapItems = array_merge($sitemapItems, $this->extractModuleItems($module));
			}
		}

		return $sitemapItems;
	}

	# ~ Extract page items - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function extractPageItems($pages) {
		$items = [];
		foreach ($pages as $page) {
			$this->stashedPages[$this->locale][$page['Page']['id']] = [
				'path'             => $page['Page']['path'],
				'change_frequency' => $page['Page']['change_frequency'],
				'priority'         => $page['Page']['priority'],
				'parent_id'        => $page['Page']['parent_id']
			];

			$items[] = $this->parseSitemapItem([
				'loc'        => $page['Page']['path'],
				'lastmod'    => !empty($page['Page']['seo_last_mod']) ? $page['Page']['seo_last_mod'] : $page['Page']['modified'],
				'changefreq' => $this->getPageProperty($page['Page']['id'], 'change_frequency'),
				'priority'   => $this->getPageProperty($page['Page']['id'], 'priority'),
			]);

			if (!empty($page['children'])) {
				$items = array_merge($items, $this->extractPageItems($page['children']));
			}
		}
		return $items;
	}

	# ~ Extract module items - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function extractModuleItems($module) {
		$moduleInstance = ClassRegistry::init($module['Module']['name']);

		// Check if module should be included in sitemap
		if (!$moduleInstance->includeInSitemap()) {
			return [];
		}

		# If there is extractSitemapItems method in model, use it
		if (method_exists($moduleInstance, 'extractSitemapItems')) {
			$sitemapItems = $moduleInstance->extractSitemapItems();
			return $this->parseSitemapItems($sitemapItems);
		}

		# If module doesn't have details, prevent url extraction
		if (!$module['Module']['has_details']) {
			return [];
		}

		//		# If there is no page_id in schema and no overriden method, do nothing
		//		if(!$moduleInstance->schema('page_id') && empty($module['Module']['page_id'])) {
		//			return array();
		//		}

		# Set locale
		$moduleInstance->setVirtual($this->locale);

		# Get all module items
		$moduleItems = $moduleInstance->find('all', [
			'recursive' => -1,
			'locale'    => $this->locale,
			'fields'    => $moduleInstance->schema('page_id') ?
				[ 'id', 'page_id', $moduleInstance->displayField, 'slug', 'modified' ] :
				[ 'id', $moduleInstance->displayField, 'slug', 'modified' ]
		]);

		# Check if module has priority and frequency fields
		$hasPriorityField = (bool) $moduleInstance->schema('sitemap_priority');
		$hasFrequencyField = (bool) $moduleInstance->schema('sitemap_change_frequency');

		$items = [];
		foreach ($moduleItems as $moduleItem) {
			# Pack item
			$items[] = $this->extractModuleItem($moduleItem, $moduleInstance, $module, $hasPriorityField, $hasFrequencyField);
		}

		return $items;

	}

	# ~ Extract sitemap fields for one module item - - - - - - - - - - - - - - - - #
	private function extractModuleItem($moduleItem, $moduleInstance, $module, $hasPriorityField, $hasFrequencyField) {

		# Get item page id and location
		$locationsVar = $this->extractModuleItemLocation(
			$moduleItem,
			$moduleInstance,
			$module
		);
		extract($locationsVar);

		# Create sitemap item
		$sitemapItem = [
			'loc'     => $itemLocation,
			'lastmod' => $moduleItem[$moduleInstance->alias]['modified']
		];

		# Extract module item priority and change_freq vars
		$sitemapItem = $this->extractModuleItemVars(
			$sitemapItem,
			$moduleItem,
			$moduleInstance,
			$itemPageId,
			$hasPriorityField,
			$hasFrequencyField
		);

		return $this->parseSitemapItem($sitemapItem);
	}

	# ~ Extract module item location - - - - - - - - - - - - - - - - - - - - - - - #
	private function extractModuleItemLocation($moduleItem, $moduleInstance, $module) {
		$itemPageId = null;
		$itemLocation = '';

		$itemLocation = $moduleInstance->getLink($moduleItem);

		if (empty($itemLocation)) {
			# Generate link
			if (isset($moduleItem[$moduleInstance->alias]['page_id'])) {
				$itemLocation = sprintf(
					'%s/%s',
					$this->stashedPages[$this->locale][$moduleItem[$moduleInstance->alias]['page_id']]['path'],
					$moduleItem[$moduleInstance->alias]['slug']
				);
				$itemPageId = $moduleItem[$moduleInstance->alias]['page_id'];
			} else {
				$itemLocation = sprintf(
					'%s/%s',
					$this->stashedPages[$this->locale][$module['Module']['page_id']]['path'],
					$moduleItem[$moduleInstance->alias]['slug']
				);
				$itemPageId = $module['Module']['page_id'];
			}
		}

		return compact('itemLocation', 'itemPageId');

	}

	# ~ Extract priority and change frequency for module item- - - - - - - - - - - #
	private function extractModuleItemVars($sitemapItem, $moduleItem, $moduleInstance, $itemPageId, $hasPriorityField, $hasFrequencyField) {
		# Handle change frequency and priority
		if ($hasPriorityField) {
			$sitemapItem['priority'] =
				!empty($moduleItem[$moduleInstance->alias]['sitemap_priority']) ?
					$moduleItem[$moduleInstance->alias]['sitemap_priority'] :
					null;
		}

		if ($hasFrequencyField) {
			$sitemapItem['changefreq'] =
				!empty($moduleItem[$moduleInstance->alias]['sitemap_frequency']) ?
					$moduleItem[$moduleInstance->alias]['sitemap_frequency'] :
					'monthly';
		}

		if (empty($sitemapItem['priority'])) {
			$sitemapItem['priority'] = $this->getPageProperty($itemPageId, 'priority');
		}

		if (empty($sitemapItem['change_frequency'])) {
			$sitemapItem['changefreq'] = $this->getPageProperty($itemPageId, 'change_frequency');
		}

		return $sitemapItem;
	}

	# ~ Get page property from cache - - - - - - - - - - - - - - - - - - - - - - - #
	private function getPageProperty($pageId, $property) {
		if (empty($this->stashedPages) || empty($this->locale))
			return;

		if (!empty($this->stashedPages[$this->locale][$pageId][$property])) {
			return $this->stashedPages[$this->locale][$pageId][$property];
		}

		if (!empty($this->stashedPages[$this->locale][$pageId]['parent_id'])) {
			return $this->getPageProperty($this->stashedPages[$this->locale][$pageId]['parent_id'], $property);
		}

		if ($property == 'change_frequency') {
			return 'monthly';
		} else if ($property == 'priority') {
			return '0.5';
		}

		return null;
	}

	# ~ Handle location- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function sitemapLoc($path) {
		return $this->baseUrl . $path;
	}

	# ~ Convert date to W3C dateformat - - - - - - - - - - - - - - - - - - - - - - #
	private function sitemapLastmod($date) {
		return date('c', strtotime($date));
	}

	# ~ Manipulate with freq value if necessary- - - - - - - - - - - - - - - - - - #
	private function sitemapChangefreq($value) {
		return $value;
	}

	# ~ Manipulate with priority - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function sitemapPriority($value) {
		return str_replace(' - default', '', $value);
	}

	# ~ Generate sitemap xml - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function generateSitemap($second = false) {
		if ($this->currentMap > 0) {
			$items = $this->SitemapItem->getAll([
				'limit'  => self::SITEINDEX_LIMIT,
				'offset' => ($this->currentMap - 1) * self::SITEINDEX_LIMIT
			]);

			if (empty($items))
				return;
		} else {
			$items = $this->SitemapItem->getAll();
		}

		# If empty items
		if (empty($items)) {
			# Try once again to build sitemap
			if (!$second) {
				$this->buildSitemap();
				return $this->generateSitemap(true);
			}
			return;
		}

		# Check if sitemap of siteindex
		if (count($items) > self::SITEINDEX_LIMIT) {
			return $this->generateSiteIndex($items);
		}

		@date_default_timezone_set("GMT");
		# Open new document
		$this->writer = new XMLWriter();
		$this->writer->openMemory();
		$this->writer->startDocument('1.0');
		$this->writer->setIndent(4);

		# Start urlset element
		$this->writer->startElement('urlset');
		$this->writer->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');

		# Write items
		foreach ($items as $item) {
			$item = reset($item);
			$this->writeSitemapItem($item);
		}

		# End urlset element
		$this->writer->endElement();

		$sitemapXML = $this->writer->flush(true);
		return $sitemapXML;
	}

	# ~ Generate site index- - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function generateSiteIndex($items) {
		@date_default_timezone_set("GMT");
		# Open new document
		$this->writer = new XMLWriter();
		$this->writer->openMemory();
		$this->writer->startDocument('1.0');
		$this->writer->setIndent(4);

		# Start urlset element
		$this->writer->startElement('sitemapindex');
		$this->writer->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
		$this->writer->writeAttribute('xsi:schemaLocation', 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/siteindex.xsd');
		$this->writer->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');

		# Get number of sitemaps
		$sitemapsCount = ceil(count($items) / self::SITEINDEX_LIMIT);

		# Get chunks
		$lastModItems = Set::extract('/SitemapItem/modtime', $items);
		$lastModChunks = array_chunk($lastModItems, self::SITEINDEX_LIMIT);
		# Write siteindex
		for ($i = 0; $i < $sitemapsCount; $i++) {
			$this->writer->startElement("url");
			$this->writer->writeElement('loc', $this->sitemapLoc('/sitemap.xml?map=' . ($i + 1)));

			# Get last modified from chunk
			$lastModChunk = $lastModChunks[$i];
			arsort($lastModChunk);
			$lastModChunk = array_values($lastModChunk);

			# Write last modified
			$this->writer->writeElement(
				'lastmod',
				$this->sitemapLastmod(date('Y-m-d H:i:s', $lastModChunk[0]))
			);

			$this->writer->endElement();
		}

		# End urlset element
		$this->writer->endElement();

		return $this->writer->flush(true);
	}

	# ~ Refresh sitemap- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function invalidateSitemap() {
		$this->SitemapItem->invalidateSitemap();
	}

	# ~ Write sitemap item - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function writeSitemapItem($url) {
		# Unset unnecesary items
		if (isset($url['modtime'])) {
			unset($url['modtime']);
		}

		# Write element
		$this->writer->startElement("url");
		foreach ($url as $key => $value) {
			$this->writer->writeElement($key, $value);
		}
		$this->writer->endElement();

		return $this->writer;
	}

	# ~ Echo sitemap - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function echoSitemap($map = 0) {
		$this->currentMap = (int) $map;
		$sitemap = $this->generateSitemap();
		header('Content-type: text/xml');
		header('Pragma: public');
		header('Cache-control: private');
		header('Expires: -1');
		echo $sitemap;
		exit(0);
	}

	# ~ Parse one sitemap item - - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function parseSitemapItem($sitemapItem) {
		$sitemapItem['modtime'] = strtotime($sitemapItem['lastmod']);
		foreach ($sitemapItem as $key => $value) {
			$methodName = 'sitemap' . ucfirst($key);
			if (method_exists($this, $methodName)) {
				$sitemapItem[$key] = $this->$methodName($value);
			}
		}
		return $sitemapItem;
	}

	# ~ Parse array of sitemap items - - - - - - - - - - - - - - - - - - - - - - - #
	private function parseSitemapItems($sitemapItems) {
		foreach ($sitemapItems as $key => $sitemapItem) {
			$sitemapItems[$key] = $this->parseSitemapItem($sitemapItem);
		}
		return $sitemapItems;
	}

	# ~ Store siteap in database - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function storeInDatabase($items) {
		return $this->SitemapItem->storeItems($items);
	}
}
