<?php
/*
 * Copyright (c) 2012, webvariants GbR, http://www.webvariants.de
 *
 * This file is released under the terms of the MIT license. You can find the
 * complete text in the attached LICENSE file or online at:
 *
 * http://www.opensource.org/licenses/mit-license.php
 */

/**
 * @since  v4.0
 * @author Christoph
 */
class _WV24_Resolver {
	protected $storage;
	protected $cache;

	public function __construct(_WV24_Storage $storage, BabelCache_Interface $cache) {
		$this->storage = $storage;
		$this->cache   = $cache;
	}

	/**
	 * @return _WV24_URL
	 */
	public function resolveUrl($url) {
		$url = WV24_Util::cleanUrl($url);

		// try the raw URL itself
		$urlInstance = $this->findUrl($url);

		if ($urlInstance !== null) {
			$this->writeCache($url, $urlInstance);
			return $urlInstance;
		}

		// try '/index.html' and 'missing slash' magic
		$urlInstance = $this->testAlternatives($url);

		if ($urlInstance !== null) {
			$this->writeCache($url, $urlInstance);
			return $urlInstance;
		}

		// try all of our configured domains to see whether one matches
		$domains   = WV24_Util::getDefaultDomains(true);
		$reqDomain = parse_url($url, PHP_URL_HOST);

		foreach ($domains as $clang => $clangDomains) {
			foreach ($clangDomains as $clangDomain) {
				if ($reqDomain !== $clangDomain) {
					$test        = 'http://'.$clangDomain.mb_substr($url, 7+strlen($reqDomain));
					$urlInstance = $this->findUrl($test);

					if ($urlInstance !== null) {
						$this->writeCache($url, $urlInstance);
						return $urlInstance;
					}

					// try the '/index.html' and 'missing slash' magic as well
					$urlInstance = $this->testAlternatives($test);

					if ($urlInstance !== null) {
						$this->writeCache($url, $urlInstance);
						return $urlInstance;
					}
				}
			}
		}

		// well, we're out of options
		return null;
	}

	protected function testAlternatives($url) {
		// replace '/index.[html|php|...]' (some commonly used suffixes) with '/'
		$test        = preg_replace('#/index\.(html|html|php|phtml|xhtml)$#i', '/', $url);
		$urlInstance = $this->findUrl($test);

		if ($urlInstance !== null) {
			return $urlInstance;
		}

		// if the URL does not look like a file, maybe someone just forgot the trailing slash
		if (!preg_match('#\.(html|html|php|phtml|xhtml)$#', $url)) {
			$test        = $url.'/';
			$urlInstance = $this->findUrl($test);

			if ($urlInstance !== null) {
				return $urlInstance;
			}
		}

		return null;
	}

	public function findUrl($url, $returnAll = false) {
		// try cache
		$instance = $this->readCache($url);
		if ($instance !== null) return $instance;

		// find the URL in our storage
		$urls = $this->storage->findByUrl($url, true);
		if (empty($urls)) return null;

		// if only a single URL was found, cool, move on.
		if (count($urls) === 1) return reset($urls);

		// return all candidates if requested
		if ($returnAll) return $urls;

		// We got a conflict. Damn.
		$foreign = array();
		$own     = array();

		foreach ($urls as $url) {
			if ($url->getNamespace() !== WV24_RealURL2::URL_NAMESPACE) {
				$foreign[] = $url;
			}
			else {
				$own[] = $url;
			}
		}

		// Rule 1: non-realURL2 URLs (i.e. addOn URLs) have priority
		if (count($foreign) === 1) {
			return $foreign[0];
		}
		elseif (count($foreign) > 1) {
			usort($foreign, array($this, 'compareURLByPath'));
			return reset($foreign);
		}

		// Rule 2: sort by structure depth; deeper articles have priority
		return $this->resolveConflicts($own);
	}

	public function resolveConflicts(array $urls) {
		$posMap = array();
		$urlMap = array();
		$maxLen = 0;

		foreach ($urls as $url) {
			$article = sly_Util_Article::findById($url->getArticleID(), $url->getClang());
			if (!$article) continue;

			$positions = $this->computePositions($article);
			$ident     = $url->getIdent();

			$posMap[$ident] = $positions;
			$urlMap[$ident] = $url;

			if (count($positions) > $maxLen) {
				$maxLen = count($positions);
			}
		}

		// well, no valid URL, i.e. article, found
		if (count($posMap) === 0) {
			return null;
		}

		for ($level = 0; $level < $maxLen; ++$level) {
			$maxPos = 0;

			// get highest priority in this level
			foreach ($posMap as $posList) {
				if (isset($posList[$level]) && $posList[$level] > $maxPos) {
					$maxPos = $posList[$level];
				}
			}

			// remove urls with lower priotity or those that aren't on the current level anymores
			foreach ($posMap as $ident => $posList) {
				if (!isset($posList[$level]) || $posList[$level] < $maxPos) {
					unset($posMap[$ident]);
				}
			}

			if (count($posMap) === 1) {
				reset($posMap);
				$ident = key($posMap);

				return $urlMap[$ident];
			}
			elseif (count($posMap) === 0) {
				break;
			}
		}

		throw new Exception('Conflict resolution ended without a result URL. This should never happen.');
	}

	public function computePositions(sly_Model_Article $article) {
		$path      = WV24_Util::decodePath($article->getPath());
		$clang     = $article->getClang();
		$positions = array();

		foreach ($path as $id) {
			$positions[] = $this->getPosition($id, $clang);
		}

		// add own position
		$positions[] = $this->getPosition($article->getId(), $clang);

		return $positions;
	}

	protected function getPosition($id, $clang) {
		$cat      = sly_Util_Article::findById($id, $clang);
		$parentId = $cat->getParentId();

		// select all elements on the same level
		$db  = sly_DB_Persistence::getInstance();
		$pos = 1;

		$db->select('article', 'id', array('re_id' => $parentId, 'clang' => $clang), null, 'startpage DESC, catpos ASC, pos ASC');

		foreach ($db as $row) {
			if ($row['id'] == $id) {
				return $pos;
			}

			++$pos;
		}

		throw new Exception('Article '.$id.' does not live inside it\'s re_id '.$parentId.'. This should never happen.');
	}

	public function compareURLByPath(_WV24_URL $a, _WV24_URL $b) {
		// sort descending
		return strcmp($b->getPath(), $a->getPath());
	}

	protected function readCache($url) {
		return $this->cache->get('realurl2.volatile', md5($url));
	}

	protected function writeCache($url, _WV24_URL $urlInstance) {
		$this->cache->set('realurl2.volatile', md5($url), $urlInstance);
	}
}
