<?php

/**
 * Výchozí zpracovatel hodnot.
 *
 * @category   Dalten
 * @package    Config
 * @subpackage Processer
 */
class Dalten_Config_Processer_DefaultProcesser implements Dalten_Config_Processer_ProcesserInterface
{
	/**
	 * Číselníky.
	 *
	 * @var Serenity_Config_Config
	 */
	private $_codebooks;

	/**
	 * Funkce.
	 *
	 * @var object
	 */
	private $_functions;

	/**
	 * Pole návzů funkcí, které obsluhuje třída s funkcemi.
	 *
	 * @var array
	 */
	private $_supportedMethods = array();

	/**
	 * Konstruktor.
	 *
	 * @param Serenity_Config_Config|Array $codebooks Číselníky.
	 * @param object                       $functions Funkce.
	 */
	public function __construct($codebooks, $functions = null)
	{
		if (is_array($codebooks)) {
			$codebooks = new ArrayObject($codebooks);
		}
		$this->_codebooks = $codebooks;
		$this->_functions = ($functions === null) ? new Dalten_Config_Processer_DefaultFunctions : $functions;
		$functionsReflection = new ReflectionClass($this->_functions);
		/** @var $reflectionMethod \ReflectionMethod */
		foreach ($functionsReflection->getMethods(ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
			$methodName = $reflectionMethod->getName();
			if (strpos($methodName, '__') === 0) {
				continue; // Odstraníme magické metody
			}
			$this->_supportedMethods[] = $methodName;
		}
	}

	/**
	 * Dosadí hodnoty proměnných včetně speciálních.
	 *
	 * Nedosazuje hodnoty do funkcí (ale ve funkcích se volá znovu na každém argumentu a až tam tedy dojde
	 * k doplnění).
	 *
	 * @param string $pattern               Vzorec.
	 * @param mixed  $value                 Hodnota položky, která se zrovna zpracovává.
	 * @param array  $values                Pole hodnot položek.
	 * @param bool   $isConditionExpression Jedná se o vnitřek podmínky?
	 *
	 * @return string Výsledná hodnota.
	 */
	protected function _replaceVariables($pattern, $value, array $values, $isConditionExpression = false)
	{
		$result = (string) $pattern;

		if (preg_match_all('/@[_0-9A-Z]+/i', $result, $matches, PREG_OFFSET_CAPTURE)) {
			$data = array();
			$quotes = array_pad(array(), count($this->_supportedMethods), '/');
			$allowedMethods = implode('|', array_map('preg_quote', $this->_supportedMethods, $quotes));
			$preMatchRegexp = '/' . $allowedMethods . '<[^>]+$/s';
			$patterns = array();
			foreach ($matches[0] as $match) {
				list($key, $offset) = $match;
				$preMatchPart = substr($pattern, 0, $offset);
				$openFunction = preg_match($preMatchRegexp, $preMatchPart);
				if ($openFunction) {
					continue; // Argumenty funkcí nahradíme až ve funkcích
				}
				switch ($key) {
					case '@null':
						$data[] = $isConditionExpression ? 'null' : null;
						break;

					case '@this':
						if ($isConditionExpression) {
							$data[] = isset($value) ? '"' . preg_replace('/^"(.*)"$/', '\1', $value) . '"' : 'null';
						} else {
							$data[] = $value;
						}
						break;

					default:
						$propertyName = substr($key, 1);

						if (isset($values[$propertyName])) {
							if ($isConditionExpression) {
								$data[] = '"' . preg_replace('/^"(.*)"$/', '\1', $values[$propertyName]) . '"';
							} else {
								$data[] = $values[$propertyName];
							}
						} else {
							$data[] = $isConditionExpression ? 'null' : null;
						}
						break;
				}
				$patterns[] = $key;
			}
			$result = str_replace($patterns, $data, $result);

			// Pokud už pattern byl s uvozovkami - třeba '"@this"',
			// tak odstraníme duplikované uvozovky na začátku a na konci
            $result = preg_replace('/^""(.*)""$/', '"$1"', $result);
		}

		return $result;
	}

	/**
	 * Dosadí číselníkové hodnoty.
	 *
	 * @param string $pattern Vzorec.
	 * @param mixed  $value   Hodnota položky, která se zrovna zpracovává.
	 *
	 * @return string Výsledná hodnota.
	 */
	protected function _replaceCodebooks($pattern, $value)
	{
		return $pattern;
	}

	/**
	 * Pomocná metoda, vracející číselníkovou hodnotu a nebo null.
	 *
	 * @param string $section  Název sekce číselníku.
	 * @param string $codebook Název číselníku.
	 * @param string $value    Hodnota z číselníku.
	 *
	 * @return mixed Číselníková hodnota a nebo null.
	 */
	protected function _getCodebookValue($section, $codebook, $value)
	{
		if (isset($this->_codebooks->$section->$codebook->$value)) {
			return $this->_codebooks->$section->$codebook->$value;
		}

		return null;
	}

	/**
	 * Zavolá funkce a dosadí výsledky.
	 *
	 * @param string $pattern Vzorec.
	 * @param string $key     Název položky, která se má sestavit.
	 * @param array  $values  Pole hodnot položek.
	 *
	 * @return string Výsledná hodnota.
	 */
	protected function _callFunctions($pattern, $key, array $values)
	{
		$result = (string) $pattern;

		if (preg_match('/([_0-9A-Z]+)<(.*)>/i', $pattern, $matches)) {
			$args = $this->_callFunctions($matches[2], $key, $values);

			$callback = array($this->_functions, $matches[1]);
			if (!is_callable($callback)) {
				return '';
			}

			$argArray = preg_split('/\s*,\s*/', $args);
			foreach ($argArray as $index => $argument) {
				$argArray[$index] = $this->_replaceVariables(
					$argument, isset($values[$key]) ? $values[$key] : null, $values
				);
			}
			$value = call_user_func_array($callback, $argArray);

			$result = str_replace($matches[0], $value, $result);
		}

		return $result;
	}

	/**
	 * Vyhodnotí ternární výrazy.
	 *
	 * @param string $pattern Vzorec.
	 * @param string $key     Název položky, která se má sestavit.
	 * @param array  $values  Pole hodnot položek.
	 *
	 * @return string Výsledná hodnota.
	 */
	protected function _evalConditions($pattern, $key, array $values)
	{
		$result = (string) $pattern;

		$value = (string) isset($values[$key])
			? $values[$key]
			: null;

		if (preg_match_all('/([^?]+)\?([^:]+):(.+)/', $result, $matches)) {
			$data = array();
			foreach ($matches[1] as $i => $condition) {
				$condition = trim($condition);
				$condition = $this->_callFunctions($condition, $key, $values);
				$condition = $this->_replaceVariables($condition, $value, $values, true);
				if (!$condition || preg_match('~^[\s0"]*$~s', $condition)) {
				    $condition = 'false';
                }
                $evaluatedCondition = trim(((bool) @eval("return ($condition);")) ? $matches[2][$i] : $matches[3][$i]);
                $data[] = $this->_replaceVariables(
					$evaluatedCondition, isset($values[$key]) ? $values[$key] : null, $values
				);
			}
			$result = str_replace($matches[0], $data, $result);
		}

		return $result;
	}

	/**
	 * Parsuje předaný vzorec a podle něho sestaví výslednou hodnotu.
	 *
	 * @param string $pattern Vzorec.
	 * @param string $key     Název položky, která se má sestavit.
	 * @param array  $values  Pole hodnot položek.
	 *
	 * @return string Výsledná hodnota.
	 */
	protected function _parse($pattern, $key, array $values)
	{
		$key = (string) $key;
		$value = (string) isset($values[$key])
			? $values[$key]
			: null;

		$result = $this->_evalConditions($pattern, $key, $values);
		$result = $this->_replaceVariables($result, $value, $values);

		return $this->_callFunctions($result, $key, $values);
	}

	/**
	 * Přetypuje položku na daný typ.
	 *
	 * @param mixed  $value Hodnota položky.
	 * @param string $type  Typ položky.
	 *
	 * @return mixed Přetypovaná hodnota.
	 */
	protected function _setType($value, $type)
	{
		$type = trim((string) $type);

		if (substr($type, 0, 5) == 'array') {
			if (is_string($value)) {
				$length = strlen($value);
				$array = array();
				for ($i = 0; $i < $length; ++$i) {
					if ($value[$i]) {
						$array[] = $i;
					}
				}

				$value = $array;
			} elseif (!is_array($value)) {
				$value = (array) $value;
			}

			$value = $this->_setArrayType($value, $this->_getArrayType($type));
			$type = 'array';
		}

		settype($value, $type);

		return $value;
	}

	/**
	 * Přetypuje prvky pole na zadaný typ.
	 *
	 * @param array  $array Pole, jehož prvky přetypovat.
	 * @param string $type  Typ prvků pole.
	 *
	 * @return array Pole s přetypovanými prvky.
	 */
	protected function _setArrayType(array $array, $type)
	{
		$type = trim((string) $type);
		foreach ($array as &$value) {
			settype($value, $type);
		}

		return $array;
	}

	/**
	 * Zjistí, jakého typu mají být prvky pole.
	 *
	 * @param string $type Celý typ pole.
	 *
	 * @return string Typ prvků pole.
	 */
	protected function _getArrayType($type)
	{
		$parts = preg_split('/[\s]+of[\s]+/i', trim((string) $type));

		return (!empty($parts[1])) ? $parts[1] : 'string';
	}

	/**
	 * Filtruje a konvertuje předané pole hodnot.
	 *
	 * @param array  $values Pole hodnot.
	 * @param object $config Konfigurace.
	 *
	 * @return array Upravené pole hodnot.
	 */
	public function processValues(array $values, $config)
	{
		$result = array();
		foreach ($config as $key => $item) {
			// Sestavení hodnoty položky.
			if (isset($item->value)) {
				$value = $this->_parse($item->value, $key, $values);
			} elseif (isset($values[$key])) {
				$value = $values[$key];
			} else {
				continue;
			}

			// Přidáme do values i konvertovanou hodnotu, protože ta se hodí do unsetu.
			// Ten musí mít její hodnotu aby prošel.
			$values['_convertedValue'] = $value;

			// Nastavení datového typu položky.
			if (isset($item->type)) {
				$value = $this->_setType($value, $item->type);
			}

			$parsedUnset = array();
			// Sestavení hodnot unsetu.
			if (isset($item->unset)) {
				foreach ((array) $item->unset as $unset) {
					$parsedUnset[] = $this->_parse($unset, $key, $values);
				}
			}

			$data = $value;
			$temp = array();
			// Unset a konverze hodnot.
			foreach ((array) $data as $index => $value) {
				if (!isset($item->unset) || !in_array($value, $parsedUnset)) {
					if (isset($item->convert->$value)) {
						$value = $this->_parse($item->convert->$value, $key, $values);
						if (isset($item->type)) {
							$type = (substr($item->type, 0, 5) == 'array')
								? $this->_getArrayType($item->type)
								: $item->type;

							settype($value, $type);
						}
					}

					$temp[$index] = $value;
				}
			}

			if (is_array($data)) {
				$value = $temp;
			} elseif (empty($temp)) {
				continue;
			} else {
				$value = current($temp);
			}

			// novou hodnoty pridame do seznamu $values, abysme s ni mohli dale operovat v konfiguracnich direktivach
			// tj. zkovertovanou hodnotu lze dale pouzivat pres volani @column
			foreach ((array) $item->column as $column) {
				if (!isset($values[$column])) {
					$values[$column] = $value;
				}
			}

			if (isset($item->table) && isset($item->column)) {
				$table = $this->_parse($item->table, $key, $values);
				foreach ((array) $item->column as $column) {
					$column = $this->_parse($column, $key, $values);
					$result[$table][$column] = $value;
				}
			} elseif (isset($item->column)) {
				foreach ((array) $item->column as $column) {
					$column = $this->_parse($column, $key, $values);
					$result[$column] = $value;
				}
			} else {
				$result[] = $value;
			}
		}

		return $result;
	}
}
