<?php

/**
 * Process Extends Visitor
 *
 * @package Less
 * @subpackage visitor
 */
class Less_Visitor_processExtends extends Less_Visitor{

	public $allExtendsStack;

	/**
	 * @param Less_Tree_Ruleset $root
	 */
	public function run( $root ){
		$extendFinder = new Less_Visitor_extendFinder();
		$extendFinder->run( $root );
		if( !$extendFinder->foundExtends){
			return $root;
		}

		$root->allExtends = $this->doExtendChaining( $root->allExtends, $root->allExtends);

		$this->allExtendsStack = array();
		$this->allExtendsStack[] = &$root->allExtends;

		return $this->visitObj( $root );
	}

	private function doExtendChaining( $extendsList, $extendsListTarget, $iterationCount = 0){
		//
		// chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting
		// the selector we would do normally, but we are also adding an extend with the same target selector
		// this means this new extend can then go and alter other extends
		//
		// this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors
		// this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if
		// we look at each selector at a time, as is done in visitRuleset

		$extendsToAdd = array();


		//loop through comparing every extend with every target extend.
		// a target extend is the one on the ruleset we are looking at copy/edit/pasting in place
		// e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one
		// and the second is the target.
		// the seperation into two lists allows us to process a subset of chains with a bigger set, as is the
		// case when processing media queries
		for( $extendIndex = 0, $extendsList_len = count($extendsList); $extendIndex < $extendsList_len; $extendIndex++ ){
			for( $targetExtendIndex = 0; $targetExtendIndex < count($extendsListTarget); $targetExtendIndex++ ){

				$extend = $extendsList[$extendIndex];
				$targetExtend = $extendsListTarget[$targetExtendIndex];

				// look for circular references
				if( in_array($targetExtend->object_id, $extend->parent_ids,true) ){
					continue;
				}

				// find a match in the target extends self selector (the bit before :extend)
				$selectorPath = array( $targetExtend->selfSelectors[0] );
				$matches = $this->findMatch( $extend, $selectorPath);


				if( $matches ){

					// we found a match, so for each self selector..
					foreach($extend->selfSelectors as $selfSelector ){


						// process the extend as usual
						$newSelector = $this->extendSelector( $matches, $selectorPath, $selfSelector);

						// but now we create a new extend from it
						$newExtend = new Less_Tree_Extend( $targetExtend->selector, $targetExtend->option, 0);
						$newExtend->selfSelectors = $newSelector;

						// add the extend onto the list of extends for that selector
						end($newSelector)->extendList = array($newExtend);
						//$newSelector[ count($newSelector)-1]->extendList = array($newExtend);

						// record that we need to add it.
						$extendsToAdd[] = $newExtend;
						$newExtend->ruleset = $targetExtend->ruleset;

						//remember its parents for circular references
						$newExtend->parent_ids = array_merge($newExtend->parent_ids,$targetExtend->parent_ids,$extend->parent_ids);

						// only process the selector once.. if we have :extend(.a,.b) then multiple
						// extends will look at the same selector path, so when extending
						// we know that any others will be duplicates in terms of what is added to the css
						if( $targetExtend->firstExtendOnThisSelectorPath ){
							$newExtend->firstExtendOnThisSelectorPath = true;
							$targetExtend->ruleset->paths[] = $newSelector;
						}
					}
				}
			}
		}

		if( $extendsToAdd ){
			// try to detect circular references to stop a stack overflow.
			// may no longer be needed.			$this->extendChainCount++;
			if( $iterationCount > 100) {

				try{
					$selectorOne = $extendsToAdd[0]->selfSelectors[0]->toCSS();
					$selectorTwo = $extendsToAdd[0]->selector->toCSS();
				}catch(Exception $e){
					$selectorOne = "{unable to calculate}";
					$selectorTwo = "{unable to calculate}";
				}

				throw new Less_Exception_Parser("extend circular reference detected. One of the circular extends is currently:"+$selectorOne+":extend(" + $selectorTwo+")");
			}

			// now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e...
			$extendsToAdd = $this->doExtendChaining( $extendsToAdd, $extendsListTarget, $iterationCount+1);
		}

		return array_merge($extendsList, $extendsToAdd);
	}


	protected function visitRule( $ruleNode, &$visitDeeper ){
		$visitDeeper = false;
	}

	protected function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ){
		$visitDeeper = false;
	}

	protected function visitSelector( $selectorNode, &$visitDeeper ){
		$visitDeeper = false;
	}

	protected function visitRuleset($rulesetNode){


		if( $rulesetNode->root ){
			return;
		}

		$allExtends	= end($this->allExtendsStack);
		$paths_len = count($rulesetNode->paths);

		// look at each selector path in the ruleset, find any extend matches and then copy, find and replace
		foreach($allExtends as $allExtend){
			for($pathIndex = 0; $pathIndex < $paths_len; $pathIndex++ ){

				// extending extends happens initially, before the main pass
				if( isset($rulesetNode->extendOnEveryPath) && $rulesetNode->extendOnEveryPath ){
					continue;
				}

				$selectorPath = $rulesetNode->paths[$pathIndex];

				if( end($selectorPath)->extendList ){
					continue;
				}

				$this->ExtendMatch( $rulesetNode, $allExtend, $selectorPath);

			}
		}
	}


	private function ExtendMatch( $rulesetNode, $extend, $selectorPath ){
		$matches = $this->findMatch($extend, $selectorPath);

		if( $matches ){
			foreach($extend->selfSelectors as $selfSelector ){
				$rulesetNode->paths[] = $this->extendSelector($matches, $selectorPath, $selfSelector);
			}
		}
	}



	private function findMatch($extend, $haystackSelectorPath ){


		if( !$this->HasMatches($extend, $haystackSelectorPath) ){
			return false;
		}


		//
		// look through the haystack selector path to try and find the needle - extend.selector
		// returns an array of selector matches that can then be replaced
		//
		$needleElements = $extend->selector->elements;
		$potentialMatches = array();
		$potentialMatches_len = 0;
		$potentialMatch = null;
		$matches = array();



		// loop through the haystack elements
		$haystack_path_len = count($haystackSelectorPath);
		for($haystackSelectorIndex = 0; $haystackSelectorIndex < $haystack_path_len; $haystackSelectorIndex++ ){
			$hackstackSelector = $haystackSelectorPath[$haystackSelectorIndex];

			$haystack_elements_len = count($hackstackSelector->elements);
			for($hackstackElementIndex = 0; $hackstackElementIndex < $haystack_elements_len; $hackstackElementIndex++ ){

				$haystackElement = $hackstackSelector->elements[$hackstackElementIndex];

				// if we allow elements before our match we can add a potential match every time. otherwise only at the first element.
				if( $extend->allowBefore || ($haystackSelectorIndex === 0 && $hackstackElementIndex === 0) ){
					$potentialMatches[] = array('pathIndex'=> $haystackSelectorIndex, 'index'=> $hackstackElementIndex, 'matched'=> 0, 'initialCombinator'=> $haystackElement->combinator);
					$potentialMatches_len++;
				}

				for($i = 0; $i < $potentialMatches_len; $i++ ){

					$potentialMatch = &$potentialMatches[$i];
					$potentialMatch = $this->PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex );


					// if we are still valid and have finished, test whether we have elements after and whether these are allowed
					if( $potentialMatch && $potentialMatch['matched'] === $extend->selector->elements_len ){
						$potentialMatch['finished'] = true;

						if( !$extend->allowAfter && ($hackstackElementIndex+1 < $haystack_elements_len || $haystackSelectorIndex+1 < $haystack_path_len) ){
							$potentialMatch = null;
						}
					}

					// if null we remove, if not, we are still valid, so either push as a valid match or continue
					if( $potentialMatch ){
						if( $potentialMatch['finished'] ){
							$potentialMatch['length'] = $extend->selector->elements_len;
							$potentialMatch['endPathIndex'] = $haystackSelectorIndex;
							$potentialMatch['endPathElementIndex'] = $hackstackElementIndex + 1; // index after end of match
							$potentialMatches = array(); // we don't allow matches to overlap, so start matching again
							$potentialMatches_len = 0;
							$matches[] = $potentialMatch;
						}
						continue;
					}

					array_splice($potentialMatches, $i, 1);
					$potentialMatches_len--;
					$i--;
				}
			}
		}

		return $matches;
	}


	// Before going through all the nested loops, lets check to see if a match is possible
	// Reduces Bootstrap 3.1 compile time from ~6.5s to ~5.6s
	private function HasMatches($extend, $haystackSelectorPath){

		if( !$extend->selector->cacheable ){
			return true;
		}

		$first_el = $extend->selector->_oelements[0];

		foreach($haystackSelectorPath as $hackstackSelector){
			if( !$hackstackSelector->cacheable ){
				return true;
			}

			if( in_array($first_el, $hackstackSelector->_oelements) ){
				return true;
			}
		}

		return false;
	}


	/**
	 * @param integer $hackstackElementIndex
	 */
	private function PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex ){


		if( $potentialMatch['matched'] > 0 ){

			// selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't
			// then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to work out
			// what the resulting combinator will be
			$targetCombinator = $haystackElement->combinator;
			if( $targetCombinator === '' && $hackstackElementIndex === 0 ){
				$targetCombinator = ' ';
			}

			if( $needleElements[ $potentialMatch['matched'] ]->combinator !== $targetCombinator ){
				return null;
			}
		}

		// if we don't match, null our match to indicate failure
		if( !$this->isElementValuesEqual( $needleElements[$potentialMatch['matched'] ]->value, $haystackElement->value) ){
			return null;
		}

		$potentialMatch['finished'] = false;
		$potentialMatch['matched']++;

		return $potentialMatch;
	}


	private function isElementValuesEqual( $elementValue1, $elementValue2 ){

		if( $elementValue1 === $elementValue2 ){
			return true;
		}

		if( is_string($elementValue1) || is_string($elementValue2) ) {
			return false;
		}

		if( $elementValue1 instanceof Less_Tree_Attribute ){
			return $this->isAttributeValuesEqual( $elementValue1, $elementValue2 );
		}

		$elementValue1 = $elementValue1->value;
		if( $elementValue1 instanceof Less_Tree_Selector ){
			return $this->isSelectorValuesEqual( $elementValue1, $elementValue2 );
		}

		return false;
	}


	/**
	 * @param Less_Tree_Selector $elementValue1
	 */
	private function isSelectorValuesEqual( $elementValue1, $elementValue2 ){

		$elementValue2 = $elementValue2->value;
		if( !($elementValue2 instanceof Less_Tree_Selector) || $elementValue1->elements_len !== $elementValue2->elements_len ){
			return false;
		}

		for( $i = 0; $i < $elementValue1->elements_len; $i++ ){

			if( $elementValue1->elements[$i]->combinator !== $elementValue2->elements[$i]->combinator ){
				if( $i !== 0 || ($elementValue1->elements[$i]->combinator || ' ') !== ($elementValue2->elements[$i]->combinator || ' ') ){
					return false;
				}
			}

			if( !$this->isElementValuesEqual($elementValue1->elements[$i]->value, $elementValue2->elements[$i]->value) ){
				return false;
			}
		}

		return true;
	}


	/**
	 * @param Less_Tree_Attribute $elementValue1
	 */
	private function isAttributeValuesEqual( $elementValue1, $elementValue2 ){

		if( $elementValue1->op !== $elementValue2->op || $elementValue1->key !== $elementValue2->key ){
			return false;
		}

		if( !$elementValue1->value || !$elementValue2->value ){
			if( $elementValue1->value || $elementValue2->value ) {
				return false;
			}
			return true;
		}

		$elementValue1 = ($elementValue1->value->value ? $elementValue1->value->value : $elementValue1->value );
		$elementValue2 = ($elementValue2->value->value ? $elementValue2->value->value : $elementValue2->value );

		return $elementValue1 === $elementValue2;
	}


	private function extendSelector($matches, $selectorPath, $replacementSelector){

		//for a set of matches, replace each match with the replacement selector

		$currentSelectorPathIndex = 0;
		$currentSelectorPathElementIndex = 0;
		$path = array();
		$selectorPath_len = count($selectorPath);

		for($matchIndex = 0, $matches_len = count($matches); $matchIndex < $matches_len; $matchIndex++ ){


			$match = $matches[$matchIndex];
			$selector = $selectorPath[ $match['pathIndex'] ];

			$firstElement = new Less_Tree_Element(
				$match['initialCombinator'],
				$replacementSelector->elements[0]->value,
				$replacementSelector->elements[0]->index,
				$replacementSelector->elements[0]->currentFileInfo
			);

			if( $match['pathIndex'] > $currentSelectorPathIndex && $currentSelectorPathElementIndex > 0 ){
				$last_path = end($path);
				$last_path->elements = array_merge( $last_path->elements, array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex));
				$currentSelectorPathElementIndex = 0;
				$currentSelectorPathIndex++;
			}

			$newElements = array_merge(
				array_slice($selector->elements, $currentSelectorPathElementIndex, ($match['index'] - $currentSelectorPathElementIndex) ) // last parameter of array_slice is different than the last parameter of javascript's slice
				, array($firstElement)
				, array_slice($replacementSelector->elements,1)
				);

			if( $currentSelectorPathIndex === $match['pathIndex'] && $matchIndex > 0 ){
				$last_key = count($path)-1;
				$path[$last_key]->elements = array_merge($path[$last_key]->elements,$newElements);
			}else{
				$path = array_merge( $path, array_slice( $selectorPath, $currentSelectorPathIndex, $match['pathIndex'] ));
				$path[] = new Less_Tree_Selector( $newElements );
			}

			$currentSelectorPathIndex = $match['endPathIndex'];
			$currentSelectorPathElementIndex = $match['endPathElementIndex'];
			if( $currentSelectorPathElementIndex >= count($selectorPath[$currentSelectorPathIndex]->elements) ){
				$currentSelectorPathElementIndex = 0;
				$currentSelectorPathIndex++;
			}
		}

		if( $currentSelectorPathIndex < $selectorPath_len && $currentSelectorPathElementIndex > 0 ){
			$last_path = end($path);
			$last_path->elements = array_merge( $last_path->elements, array_slice($selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex));
			$currentSelectorPathIndex++;
		}

		$slice_len = $selectorPath_len - $currentSelectorPathIndex;
		$path = array_merge($path, array_slice($selectorPath, $currentSelectorPathIndex, $slice_len));

		return $path;
	}


	protected function visitMedia( $mediaNode ){
		$newAllExtends = array_merge( $mediaNode->allExtends, end($this->allExtendsStack) );
		$this->allExtendsStack[] = $this->doExtendChaining($newAllExtends, $mediaNode->allExtends);
	}

	protected function visitMediaOut(){
		array_pop( $this->allExtendsStack );
	}

	protected function visitDirective( $directiveNode ){
		$newAllExtends = array_merge( $directiveNode->allExtends, end($this->allExtendsStack) );
		$this->allExtendsStack[] = $this->doExtendChaining($newAllExtends, $directiveNode->allExtends);
	}

	protected function visitDirectiveOut(){
		array_pop($this->allExtendsStack);
	}

}