<?php

namespace Bitrix\Main\Entity;

class QueryChain
{
	/**
	 * @var QueryChainElement[]
	 */
	protected $chain;

	protected $size = 0;

	protected $definition;

	protected $alias;

	protected $custom_alias;

	/**
	 * @var QueryChainElement
	 */
	protected $last_element;

	public function __construct()
	{
		$this->chain = array();
	}

	public function addElement(QueryChainElement $element)
	{
		if (empty($this->chain) && !($element->getValue() instanceof Base))
		{
			throw new \Exception('The first element of chain should be Entity only.');
		}

		$this->chain[] = $element;
		$this->definition = null;
		$this->alias = null;

		$this->last_element = $element;
		$this->size++;
	}

	public function getFirstElement()
	{
		return $this->chain[0];
	}

	public function getLastElement()
	{
		return $this->last_element;
	}

	public function getAllElements()
	{
		return $this->chain;
	}

	public function removeLastElement()
	{
		$this->chain = array_slice($this->chain, 0, -1);
		$this->definition = null;
		$this->alias = null;

		$this->last_element = end($this->chain);
		$this->size--;
	}

	public function hasBackReference()
	{
		foreach ($this->chain as $element)
		{
			if ($element->isBackReference())
			{
				return true;
			}
		}

		return false;
	}

	public function getSize()
	{
		return $this->size;
	}

	public function getDefinition()
	{
		if (is_null($this->definition))
		{
			$this->definition = self::getDefinitionByChain($this);
		}

		return $this->definition;
	}

	public function getAlias()
	{
		if ($this->custom_alias !== null)
		{
			return $this->custom_alias;
		}

		if ($this->alias === null)
		{
			$this->alias = self::getAliasByChain($this);
		}

		return $this->alias;
	}

	public function setCustomAlias($alias)
	{
		$this->custom_alias = $alias;
	}

	public static function getChainByDefinition(Base $init_entity, $definition)
	{
		$chain = new QueryChain;
		$chain->addElement(new QueryChainElement($init_entity));

		$def_elements = explode('.', $definition);
		$def_elements_size = count($def_elements);

		$prev_entity  = $init_entity;

		$i = 0;

		foreach ($def_elements as &$def_element)
		{
			$is_first_elem = ($i == 0);
			$is_last_elem  = (++$i == $def_elements_size);

			$not_found = false;

			// all elements should be a Reference field or Entity
			// normal (scalar) field can only be the last element

			if ($prev_entity->hasField($def_element))
			{
				// field has been found at current entity
				$field = $prev_entity->getField($def_element);

				if ($field instanceof ReferenceField)
				{
					$prev_entity = $field->getRefEntity();
				}
				elseif ($field instanceof ExpressionField)
				{
					// expr can be in the middle too
				}
				elseif (!$is_last_elem)
				{
					throw new \Exception(sprintf(
						'Normal fields can be only the last in chain, `%s` %s is not the last.',
						$field->getName(), get_class($field)
					));
				}

				if ($is_last_elem && $field instanceof ExpressionField)
				{
					// we should have own copy of build_from_chains to set join aliases there
					$field = clone $field;
				}

				$chain->addElement(new QueryChainElement($field));
			}
			elseif ($prev_entity->hasUField($def_element))
			{
				// extend chain with utm/uts entity
				$ufield = $prev_entity->getUField($def_element);

				$u_entity = null;

				if ($ufield->isMultiple())
				{
					// add utm entity  user.utm:source_object (1:N)
					$utm_entity = Base::getInstance('Utm'.$prev_entity->getName());
					$u_entity = $utm_entity;

					$chain->addElement(new QueryChainElement(
						array($utm_entity, $utm_entity->getField('SOURCE_OBJECT')),
						array('ufield' => $ufield)
					));

					if ($ufield->getTypeId() == 'iblock_section'
						&& substr($ufield->getName(), -3) == '_BY'
						&& $prev_entity->hasUField(substr($ufield->getName(), 0, -3))
					)
					{
						// connect next entity
						$utm_fname = $ufield->getName();
						$prev_entity = Base::getInstance('Bitrix\Iblock\Section');
					}
					else
					{
						$utm_fname = $ufield->getValueFieldName();
					}

					$chain->addElement(new QueryChainElement(
						$utm_entity->getField($utm_fname),
						array('ufield' => $ufield)
					));
				}
				else
				{
					// uts table - single value
					// add uts entity user.uts (1:1)
					$uts_entity = Base::getInstance('Uts'.$prev_entity->getName());
					$u_entity = $uts_entity;

					$chain->addElement(new QueryChainElement(
						$prev_entity->getField('UTS_OBJECT')
					));

					// add `value` field
					$chain->addElement(new QueryChainElement(
						$uts_entity->getField($def_element)
					));
				}
			}
			elseif (Base::isExists($def_element)
				&& Base::getInstance($def_element)->getReferencesCountTo($prev_entity->getName()) == 1
			)
			{
				// def_element is another entity with only 1 reference to current entity
				// need to identify Reference field
				$ref_entity = Base::getInstance($def_element);
				$field = end($ref_entity->getReferencesTo($prev_entity->getName()));

				$prev_entity = $ref_entity;

				$chain->addElement(new QueryChainElement(
					array($ref_entity, $field)
				));
			}
			elseif ( ($pos_wh = strpos($def_element, ':')) > 0 )
			{
				$ref_entity_name = substr($def_element, 0, $pos_wh);

				if (strpos($ref_entity_name, '\\') === false)
				{
					// if reference has no namespace, then it'is in the namespace of previous entity
					$ref_entity_name = $prev_entity->getNamespace().'\\'.$ref_entity_name;
				}

				if (
					Base::isExists($ref_entity_name)
					&& Base::getInstance($ref_entity_name)->hasField($ref_field_name = substr($def_element, $pos_wh+1))
					&& Base::getInstance($ref_entity_name)->getField($ref_field_name)->getRefEntity()->getName() == $prev_entity->getName()
				)
				{
					// chain element is another entity with >1 references to current entity
					// def like NewsArticle:AUTHOR, NewsArticle:LAST_COMMENTER
					// NewsArticle - entity, AUTHOR and LAST_COMMENTER - Reference fields
					$chain->addElement(new QueryChainElement(array(
						Base::getInstance($ref_entity_name),
						Base::getInstance($ref_entity_name)->getField($ref_field_name)
					)));

					$prev_entity = Base::getInstance($ref_entity_name);
				}
				else
				{
					$not_found = true;
				}

			}
			elseif ($def_element == '*' && $is_last_elem)
			{
				continue;
			}
			else
			{
				// unknown chain
				$not_found = true;
			}

			if ($not_found)
			{
				throw new \Exception(sprintf(
					'Unknown field definition `%s` (%s) for %s Entity.',
					$def_element, $definition, $prev_entity->getName()
				), 100);
			}
		}

		return $chain;
	}

	public static function getDefinitionByChain(QueryChain $chain)
	{
		$def = array();

		// add members of chain except of init entity
		$elements = array_slice($chain->getAllElements(), 1);

		foreach ($elements  as $element)
		{
			if ($element->getValue() instanceof ExpressionField && $element !== end($elements))
			{
				// skip non-last expressions
				continue;
			}

			$def[] = $element->getDefinitionFragment();
		}

		return join('.', $def);
	}

	public static function getAliasByChain(QueryChain $chain)
	{
		$alias = array();

		$elements = $chain->getAllElements();

		// cut non-last expressions
		foreach ($elements as $k => $element)
		{
			if ($element->getValue() instanceof ExpressionField && $element !== end($elements))
			{
				unset($elements[$k]);
			}
		}

		// add prefix of init entity
		if (count($elements) > 2)
		{
			$alias[] = $chain->getFirstElement()->getAliasFragment();
		}

		// add other members of chain
		$elements = array_slice($elements, 1);

		foreach ($elements  as $element)
		{
			$fragment = $element->getAliasFragment();

			if (strlen($fragment))
			{
				$alias[] = $fragment;
			}
		}

		return join('_', $alias);
	}

	public static function getAliasByDefinition(Base $entity, $definition)
	{
		return self::getChainByDefinition($entity, $definition)->getAlias();
	}

	public function hasAggregation()
	{
		$elements = array_reverse($this->chain);

		foreach ($elements as $element)
		{
			/**
			 * @var $element QueryChainElement
			 */
			if ($element->getValue() instanceof ExpressionField && $element->getValue()->isAggregated())
			{
				return true;
			}
		}

		return false;
	}

	public function hasSubquery()
	{
		$elements = array_reverse($this->chain);

		foreach ($elements as $element)
		{
			/**
			 * @var $element QueryChainElement
			 */
			if ($element->getValue() instanceof ExpressionField && $element->getValue()->hasSubquery())
			{
				return true;
			}
		}

		return false;
	}

	public function isConstant()
	{
		return ($this->getLastElement()->getValue() instanceof ExpressionField
			&& $this->getLastElement()->getValue()->isConstant());
	}

	public function getSqlDefinition($with_alias = false)
	{
		$sql_def = $this->getLastElement()->getSqlDefinition();

		if ($with_alias)
		{
			global $DB;

			$sql_def .= ' AS ' . $DB->escL . $this->getAlias() . $DB->escR;
		}

		return $sql_def;
	}

	public function __clone()
	{
		$this->custom_alias = null;
	}

	public function dump()
	{
		$i = 0;
		foreach ($this->chain as $elem)
		{
			echo '  '.++$i.'. ';
			$elem->dump();
			echo PHP_EOL;
		}
	}
}