<?php

namespace App\Spotlight\Source;

class Menu extends \App\Spotlight\SourceAbstract
{
	protected $lang = [];

	public function __construct()
	{
		parent::__construct();
		\App\Loader_Lang::getInstance()->loadAppFile("remoteData/spotlight/menu.lang.php");
	}

	/**
	 * {@inheritDoc}
	 * @see \App\Spotlight\SourceAbstract::determineRecords()
	 */
	protected function determineRecords($offset, $resultsPerPage):array
	{
		$return = [];

		$referenceAccessibleSections = $this->getReferenceAccessibleSections();

		// at this point, $referenceAccessibleSections contains all accessible sections for this user
		foreach ($referenceAccessibleSections as $refSectionData) {
			$refSection = $refSectionData['section'];

			$parents = $refSectionData['parents'];
			$parentsLabel = implode(" / ", array_map(function($pSection) {
				return $pSection->getLabel();
			}, ($parents?:[])));

			$displayLabel = trim($parentsLabel . ' / ' . $refSection->getLabel(), ' /');

			$similarity = $this->determineSimilarity(str_replace('/', ' ', $displayLabel));
			if ($similarity['matched']) {
				$return[]= $this->buildRecord($similarity, $refSection, $displayLabel);
			}
		}

		if (!$return) {
			foreach ($referenceAccessibleSections as $refSectionData) {
				$refSection = $refSectionData['section'];

				$parents = $refSectionData['parents'];
				$parentsLabel = implode(" / ", array_map(function($pSection) {
					return $pSection->getLabel();
				}, ($parents?:[])));

				$displayLabel = trim($parentsLabel . ' / ' . $refSection->getLabel(), ' /');

				$similarity = $this->determineSimilarityLoose(str_replace('/', ' ', $displayLabel));
				if ($similarity['matched']) {
					$return[]= $this->buildRecord($similarity, $refSection, $displayLabel);
				}
			}
		}

		return $return;
	}

	/**
	 * Get the list of accessible options for the current user
	 * Cache them for this user
	 */
	protected function getReferenceAccessibleSections()
	{
		$cacheKey = sprintf("%s:%s:%s", __METHOD__, \Entity\User::selectCurrent()->getId(), count(\App\UserAccess\CurrentUserAccessFast::getInstance()->getAccessibleSectionsList()));

		$referenceAccessibleSections = \Engine\Cache\General::getInstance('disk')->get($cacheKey);

		if (!$referenceAccessibleSections) {
			$referenceAccessibleSections = [];

			$menu = \App\Menu\Structure\Menu::getInstance();
			$accessibleSectionsTree = $menu->getStructure();

			$accessibleSectionsTree->each(function($sectionObj, $options) use (&$referenceAccessibleSections) {
				$referenceAccessibleSections[]= [
					'section' => $sectionObj,
					'parents' => $options['parents'],
				];
			}, []);
			$accessibleSections = 			\App\UserAccess\CurrentUserAccessFast::getInstance()->getAccessibleSectionsList();

			// make an intersection between accessible sections and reference sections
			foreach ($referenceAccessibleSections as $k => $refSectionData) {
				$refSection = $refSectionData['section'];
				$keep = false;

				// leave only sections accessible by the current user
				if (in_array($refSection->getId(), $accessibleSections)) {
					$keep = true;
				}

				// remove js sections (the logout button)
				if ($keep && $refSection->getJsAction()) {
					$keep = false;
				}

				// leave only menu items, removing groups for example
				if ($keep && $refSection->getType()!='menuItem') {
					$keep = false;
				}

				if (!$keep) {
					unset($referenceAccessibleSections[$k]);
				}
			}

			\Engine\Cache\General::getInstance('disk')->set($cacheKey, $referenceAccessibleSections, 60*60*8);
		}

		return $referenceAccessibleSections;
	}

	protected function buildRecord($similarity, $refSection, $displayLabel)
	{
		$lang = \App\Loader_Lang::getInstance()->getGlobal();
		if ($refSection->isNonMVC()) {	// old sections
			return new \App\Spotlight\Record(
				$displayLabel,
				$lang['RemoteData']['Spotlight']['Menu']['section'],
				sprintf("window.open('%s', '_blank');", APP_URL.$refSection->getPath()),
				$similarity['priority']
			);
		}
		else {	// MVC sections
			return new \App\Spotlight\Record(
				$displayLabel,
				$lang['RemoteData']['Spotlight']['Menu']['section'],
				sprintf("window.open('%s', '_blank');", \Engine\Mvc\Dispatcher::getInstance()->getAppUrl($refSection->getId())),
				$similarity['priority']
			);
		}
	}

	/**
	 * Determine the similarity between the search input and the current scanned label
	 * @return array
	 */
	protected function determineSimilarity($label)
	{
		// TODO: over-complicated; always returns priority = 0

		if (!$this->searchWords) {
			return [
				'priority' => 100,
				'matched' => false,
			];
		}

		$replacements = [
			'/ \/ /' 	=> 	' ',
			'/&amp;/' 	=> 	' ',
			'/&gt;/' 	=> 	' ',
			'/&lt;/' 	=> 	' ',
			'/[\s]+/' 	=> 	' ',
		];

		if (stripos(
			trim(preg_replace(array_keys($replacements), array_values($replacements), $label)),
			trim(preg_replace(array_keys($replacements), array_values($replacements), $this->searchString))
			)===0) {
			return [
				'priority' => 0,
				'matched' => true,
			];
		}

		$rx = sprintf("/(^|\W)%s.*/i", $this->extractor->getRegexpToMatchAnyOf($this->searchWords));
		$ret = $this->extractor->find($rx, $label);

		$score = 0;
		$matched = false;

		// we have a match
		if ($ret->getCount()) {
			$score+= 0.25;
			$matched = true;

			// score it higher based on the number of matched items
			$localScore = array_sum(array_map(function($sw) use ($label) {
				$rx = sprintf("/(^|\W)%s.*/i", $this->extractor->getRegexpToMatch($sw));
				return (int)$this->extractor->contains($rx, $label);
			}, $this->searchWords)) / count($this->searchWords);

			$matched = ($localScore>0.9);	// ignore weakly matched items

			$score+= 0.75 * $localScore;
		}

		return [
			'priority' => 100 - max(0, min(100, $score*100)),	// clamp to 0..100 and invert the score according to the priority semantics
			'matched' => $matched,
		];
	}

	/**
	 * Determine the similarity between the search input and the current scanned label
	 * @return array
	 */
	protected function determineSimilarityLoose($label)
	{
		// If no search words, return default values
		if (!$this->searchWords) {
			return [
				'priority' => 100,
				'matched' => false,
			];
		}

		$replacements = [
			'/ \/ /' 	=> 	' ',
			'/&amp;/' 	=> 	' ',
			'/&gt;/' 	=> 	' ',
			'/&lt;/' 	=> 	' ',
			'/[\s]+/' 	=> 	' ',
		];

		// Clean up the label and search string
		$cleanLabel = strtolower(trim(preg_replace(array_keys($replacements), array_values($replacements), $label)));
		$cleanSearch = strtolower(trim(preg_replace(array_keys($replacements), array_values($replacements), $this->searchString)));
		
		// Split the label into words for individual word matching
		$labelWords = preg_split('/\s+/', $cleanLabel);
		$matched = false;
		$bestScore = 100; // Lower is better
		
		// Check each search word against each label word using Levenshtein
		foreach ($this->searchWords as $searchWord) {
			$searchWord = strtolower($searchWord);
			$searchLen = strlen($searchWord);
			
			if ($searchLen < 3) continue; // Skip very short search terms
			
			$wordMatched = false;
			$wordBestScore = 100;
			
			foreach ($labelWords as $labelWord) {
				$labelLen = strlen($labelWord);
				
				// Skip if the length difference is too great
				if (abs($labelLen - $searchLen) > max(3, floor($searchLen / 2))) {
					continue;
				}
				
				// Calculate Levenshtein distance
				$distance = levenshtein($searchWord, $labelWord);
				
				// Normalize the distance based on the length of the strings
				$maxLen = max($searchLen, $labelLen);
				$normalizedScore = ($maxLen > 0) ? ($distance / $maxLen) * 100 : 100;
				
				// Consider it a match if the normalized score is below threshold
				if ($normalizedScore < 40) { // 40% difference threshold
					$wordMatched = true;
					$wordBestScore = min($wordBestScore, $normalizedScore);
				}
			}
			
			if ($wordMatched) {
				$matched = true;
				$bestScore = min($bestScore, $wordBestScore);
			}
		}
		
		// Also try matching against the whole label for multi-word searches
		if (!$matched && strlen($cleanSearch) > 3) {
			$distance = levenshtein($cleanSearch, $cleanLabel);
			$maxLen = max(strlen($cleanSearch), strlen($cleanLabel));
			$normalizedScore = ($maxLen > 0) ? ($distance / $maxLen) * 100 : 100;
			
			if ($normalizedScore < 50) { // Higher threshold for whole label match
				$matched = true;
				$bestScore = $normalizedScore;
			}
		}
		
		return [
			'priority' => $matched ? min(90, max(20, $bestScore)) : 100, // Scale priority between 20-90
			'matched' => $matched,
		];
	}
}
