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

class SeoGenerator {

	const TAG_LENGTH_LIMIT = 190;

	# Candidates for title.
	var $titleCandidates = array('title', 'name');
	# Candidates for description
	var $descriptionCandidates = array('description', 'text', 'details', 'full_text');
	# Candidates for keywords
	var $keywordsCandidates = array('description', 'text', 'details', 'full_text');
	# Candidates for meta image
	var $imageCandidates = array('MetaImage', 'Image', 'MainImage', 'CoverImage', 'Photo', 'Logo');
	# Candidates for url
	var $urlCandidates = array('url', 'link');
	# Cached metatags
	var $cachedMetatags = array();
	# Cached languages
	var $languages = array();


# ~ Cache metatags from database - - - - - - - - - - - - - - - - - - - - - - - #
	public function __construct() {
		# Initialize model
		$this->MetaTag = ClassRegistry::init('Seo.MetaTag');

		# Cache all metatags
		$metatags = $this->MetaTag->find('all');
		$this->cachedMetatags = array(
			'by_name' => $this->MetaTag->getAll($metatags),
			'by_id' => $this->MetaTag->formatList($metatags, 'id')
		);

		# Cache languages
		$this->languages = Configure::read('Config.Languages');
		$this->currentLanguage = Configure::read('Config.Language');
	}


# ~ Generate metatags after save - - - - - - - - - - - - - - - - - - - - - - - #
	public function generateAfterSave($Model, $additionalTags) {
		# Generate main tags (title, description, keywords...)
		$mainTags = $this->generateMainTags($Model);

		# Get generated child tags
		$generatedTags = $this->generateChildTags($mainTags);

		# Get altered metatags
		$metatags = $this->alterAfterSave($Model, $mainTags + $generatedTags + $additionalTags);

		# Save updated metatags
		return $Model->saveField(
			'seo_generated_metatags',
			serialize($metatags),
			array(
				'callbacks' => false,
				'validate' => false
			)
		);
	}


# ~ Generate metatags at runtime - - - - - - - - - - - - - - - - - - - - - - - #
	public function generateAtRuntime($page, $allTags, $request) {
		# Generate url tags
		foreach($this->cachedMetatags['by_id'] as $id => $tagDescription) {
			if(in_array($tagDescription['name'], $this->urlCandidates)) {
				if(isset($allTags[$id])) continue;
				$allTags[$id] = array(
					$request->params['language'] => FRONT_BASE_URL . $request->here
				);
			}
		}

		# Override cannonical url if there are query or named params
		if(!empty($request->query) || !empty($request->params['named'])) {
			$baseUrl = cleanUrl($request);
			$allTags = $this->addTag($allTags, 'rel:canonical', FRONT_BASE_URL . $baseUrl, $this->currentLanguage);
		}

		# Alter tags
		return $this->alterAtRuntime($page, $allTags, $request);
	}


# ~ Function to override in subclasses - - - - - - - - - - - - - - - - - - - - #
	protected function alterAfterSave($Model, $currentTags) {
		return $currentTags;
	}


# ~ Generate main tags (title, description, keywords, image) - - - - - - - - - #
	protected function generateMainTags($Model) {
		# Define main meta fields
		$metatags = array();
		$modelMetaFields = array('meta_title', 'meta_keyword', 'meta_description');
		$mainMetaFields = array('title', 'keywords', 'description', 'image');

		if(empty($Model->data[$Model->alias]['meta'])) {
			foreach($modelMetaFields as $metaField) {
				$tagName = str_replace('meta_', '', $metaField);

				# !!NOTE: This should be fixed!!!
				if($tagName == 'keyword') $tagName = 'keywords';

				$tagName = 'meta:' . $tagName;

				# If one language
				if(count($this->languages) == 1) {
					if(!empty($Model->data[$Model->alias][$metaField])) {
						#$tagValue = CakeText::truncate($Model->data[$Model->alias][$metaField], self::TAG_LENGTH_LIMIT);
						$tagValue = $Model->data[$Model->alias][$metaField];
						$metatags = $this->addTag($metatags, $tagName, $tagValue);
					}
				# More than one language
				} else {
					foreach($this->languages as $locale => $language) {
						if(!empty($Model->data[$Model->alias][$metaField . '__'][$locale])) {
						#$tagValue = CakeText::truncate($Model->data[$Model->alias][$metaField . '__'][$locale], self::TAG_LENGTH_LIMIT);
						$tagValue = $Model->data[$Model->alias][$metaField . '__'][$locale];
							$metatags = $this->addTag($metatags, $tagName, $tagValue, $locale);
						}
					}
				}
			}
		}

		# Check missing main tags and generate them
		foreach($mainMetaFields as $field) {
			if(!$this->hasTag($metatags, 'meta:' . $field)) {
				$func = 'generateMeta' . ucfirst($field);
				$value = $this->$func($Model);
				if(!empty($value)) {
					$metatags = $this->addTag($metatags, 'meta:' . $field, $value);
				}
			}
		}

		return $metatags;
	}


# ~ Check if metatags array contains tag with provided name- - - - - - - - - - #
	protected function hasTag($metatags, $tagName) {
		if(!isset($this->cachedMetatags['by_name'][$tagName])) {
			throw new Exception(sprintf('Metatag %s doesnt exists in predefined list', $tagName));
		}

		$metatagId = $this->cachedMetatags['by_name'][$tagName]['id'];

		return isset($metatags[$metatagId]);
	}


# ~ Add new tag to list- - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	protected function addTag($metatags, $tagName, $tagValue, $locale = null) {
		$locale = $this->getLocale($locale);

		if(!isset($this->cachedMetatags['by_name'][$tagName])) {
			throw new Exception(sprintf('Metatag %s doesnt exists in predefined list', $tagName));
		}

		# Add new tag to list
		$metatagDescription = $this->cachedMetatags['by_name'][$tagName];
		if(is_array($tagValue)) {
			$metatags[$metatagDescription['id']] = $tagValue;
		} else {
			$metatags[$metatagDescription['id']][$locale] = $tagValue;
		}

		return $metatags;
	}


# ~ Remove existing tag from list- - - - - - - - - - - - - - - - - - - - - - - #
	protected function removeTag($metatags, $tagName, $locale = null) {
		if(!isset($this->cachedMetatags['by_name'][$tagName])) {
			throw new Exception(sprintf('Metatag %s doesnt exists in predefined list', $tagName));
		}

		# Add new tag to list
		$metatagDescription = $this->cachedMetatags['by_name'][$tagName];

		if(empty($locale)) {
			unset($metatags[$metatagDescription['id']]);
		} else {
			unset($metatags[$metatagDescription['id']][$locale]);
		}
		return $metatags;
	}


# ~ Function to override in subclasses - - - - - - - - - - - - - - - - - - - - #
	protected function alterAtRuntime($page, $allTags, $request) {
		return $allTags;
	}


# ~ Generate meta title- - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	protected function generateMetaTitle($Model) {
		# Get meta title from display field
		return $this->getPolyglotValue($Model->data[$Model->alias], $Model->displayField);
	}


# ~ Generate meta keywords - - - - - - - - - - - - - - - - - - - - - - - - - - #
	protected function generateMetaKeywords($Model) {

		# Go through keywords candidates and find one
		foreach($this->keywordsCandidates as $candidate) {

			# If non existent, skip
			if(!isset($Model->data[$Model->alias][$candidate])) continue;

			# Get values
			$value = $this->getPolyglotValue($Model->data[$Model->alias], $candidate);

			# Filter if polygloted array
			if(is_array($value)) {
				$value = array_filter($value);
			}

			# If not empty value, we found it
			if(!empty($value)) {
				break;
			}
		}

		# If nothing found, return empty string
		if(empty($value)) {
			return "";
		}

		# Handle polygloted
		if(is_array($value)) {
			foreach($value as $locale => $val) {
				$value[$locale] = $this->extractKeywords($val);
			}
			return $value;
		# Handle normal field
		} else {
			return $this->extractKeywords($value);
		}
	}


# ~ Generate meta description- - - - - - - - - - - - - - - - - - - - - - - - - #
	protected function generateMetaDescription($Model) {

		# Go through description candidates and find one
		foreach($this->descriptionCandidates as $candidate) {

			# If non existent, skip
			if(!isset($Model->data[$Model->alias][$candidate])) continue;

			# Get values
			$value = $this->getPolyglotValue($Model->data[$Model->alias], $candidate);

			# Filter if polygloted array
			if(is_array($value)) {
				$value = array_filter($value);
			}

			# If not empty value, we found it
			if(!empty($value)) {
				break;
			}
		}

		# If nothing found, return empty string
		if(empty($value)) {
			return "";
		}

		# Handle polygloted
		if(is_array($value)) {
			foreach($value as $locale => $val) {
				$val = html2text($val);
				$value[$locale] = CakeText::truncate($val, self::TAG_LENGTH_LIMIT);
			}
			return $value;
		# Handle normal field
		} else {
			$value = html2text($value);
			return CakeText::truncate($value, self::TAG_LENGTH_LIMIT);
		}
	}


# ~ Generate meta image- - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	protected function generateMetaImage($Model) {
		foreach($this->imageCandidates as $imageCandidate) {
			if(isset($Model->data[$imageCandidate]['file']) && !empty($Model->data[$imageCandidate]['file'])) {
				$locales = array_keys($this->languages);
				return array_fill_keys($locales, FRONT_BASE_URL . $Model->data[$imageCandidate]['file']);
			}
		}
		return null;
	}


# ~ Generate child tags (Tags dependent on other tags) - - - - - - - - - - - - #
	public function generateChildTags($mainTags, $cachedMetatags = array()) {
		$tags = array();
		$metatags = array();

		if(empty($cachedMetatags)) {
			$cachedMetatags = $this->cachedMetatags['by_id'];
		}

		if($cachedMetatags) {

			# Get main tags in format 'name' => 'id'
			foreach($cachedMetatags as $id => $metatagDescription) {
				if(isset($mainTags[$metatagDescription['inherits']])) {
					$metatags[$id] = $mainTags[$metatagDescription['inherits']];
				}
			}
		}
		return $metatags;
	}


# ~ Get locale function- - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function getLocale($locale) {
		if(empty($locale)) {
			$locale = Configure::read('Config.language');
		}
		return $locale;
	}


# ~ Get polyglot value - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	private function getPolyglotValue($data, $field) {
		if(isset($data[$field . '__'])) {
			$value = array();
			foreach($this->languages as $locale => $language) {
				$value[$locale] = $data[$field . '__'][$locale];
			}
			return $value;
		} else {
			return $data[$field];
		}
	}


# ~ Extract keywords from text - - - - - - - - - - - - - - - - - - - - - - - - #
	private function extractKeywords($text, $locale = null) {
		$locale = $this->getLocale($locale);

		# Strip HTML tags
		$text = html2text($text);

		# Get all words
		$words = str_word_count($text, 1);

		# !!TODO: Make dictionary for ignored words, per locale


		# Count words
		$wordCount = array();
		foreach($words as $word) {
			# Ignore short words (this should be replaced with ignore dictionary)
			if(strlen($word) < 4) continue;

			if(!isset($wordCount[$word])) {
				$wordCount[$word] = 1;
				continue;
			}
			$wordCount[$word]++;
		}

		# Sort by occurrence
		arsort($wordCount);

		# Get first 30 words
		$selectWords = min(count($wordCount), 30);

		# Return string
		$words = array_slice($wordCount, 0, $selectWords);
		$words = array_keys($words);
		return implode(', ', $words);
	}
}
?>
