<?php
/**
 * The Forminator_Calculator_Parser class.
 *
 * @package Forminator
 */

/**
 * The parsers has one important method: parse()
 * It takes an array of tokens as input and
 * returns an array of nodes as output.
 * These nodes are the syntax tree of the term.
 */
class Forminator_Calculator_Parser {

	/**
	 * The symbol container with all possible symbols
	 *
	 * @var Forminator_Calculator_Symbol_Loader
	 */
	protected $symbol_loader;

	/**
	 * Parser constructor.
	 *
	 * @param Forminator_Calculator_Symbol_Loader $symbol_loader Forminator_Calculator_Symbol_Loader.
	 */
	public function __construct( $symbol_loader ) {
		$this->symbol_loader = $symbol_loader;
	}

	/**
	 * Parses an array with tokens. Returns an array of nodes.
	 * These nodes define a syntax tree.
	 *
	 * @param Forminator_Calculator_Parser_Token[] $tokens Forminator_Calculator_Parser_Token.
	 *
	 * @return Forminator_Calculator_Parser_Node_Container
	 */
	public function parse( $tokens ) {
		$symbol_nodes = $this->detect_symbols( $tokens );

		$nodes = $this->create_tree_by_brackets( $symbol_nodes );

		$nodes = $this->transform_tree_by_functions( $nodes );

		$this->check_grammar( $nodes );

		// Wrap the nodes in an array node.
		$root_node = new Forminator_Calculator_Parser_Node_Container( $nodes );

		return $root_node;
	}

	/**
	 * Creates a flat array of symbol nodes from tokens.
	 *
	 * @param Forminator_Calculator_Parser_Token[] $tokens Forminator_Calculator_Parser_Token.
	 *
	 * @return Forminator_Calculator_Parser_Node_Symbol[]
	 * @throws Forminator_Calculator_Exception When there is an Calculator error.
	 */
	protected function detect_symbols( $tokens ) {
		$symbol_nodes = array();

		$expecting_opening_bracket = false; // True if we expect an opening bracket (after a function name).
		$open_bracket_counter      = 0;

		foreach ( $tokens as $token ) {
			$type = $token->type;

			if ( Forminator_Calculator_Parser_Token::TYPE_WORD === $type ) {
				$identifier = $token->value;
				$symbol     = $this->symbol_loader->find( $identifier );

				if ( null === $symbol ) {
					throw new Forminator_Calculator_Exception( 'Error: Detected unknown or invalid string identifier: ' . $identifier . '.' ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
				}
			} elseif ( Forminator_Calculator_Parser_Token::TYPE_NUMBER === $type ) {
				// Notice: Numbers do not have an identifier.
				$symbol_numbers = $this->symbol_loader->find_sub_types( 'Forminator_Calculator_Symbol_Number' );
				if ( empty( $symbol_numbers ) || ! is_array( $symbol_numbers ) ) {
					throw new Forminator_Calculator_Exception( 'Error: Unavailable number symbol processor.' );// @codeCoverageIgnore.
				}

				$symbol = $symbol_numbers[0];
			} else { // Type Token::TYPE_CHARACTER:.
				$identifier = $token->value;
				$symbol     = $this->symbol_loader->find( $identifier );

				if ( null === $symbol ) {
					throw new Forminator_Calculator_Exception( 'Error: Detected unknown or invalid string identifier: ' . $identifier . '.' ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
				}

				if ( $symbol instanceof Forminator_Calculator_Symbol_Opening_Bracket ) {
					++$open_bracket_counter;
				}
				if ( $symbol instanceof Forminator_Calculator_Symbol_Closing_Bracket ) {
					--$open_bracket_counter;

					// Make sure there are not too many closing brackets.
					if ( $open_bracket_counter < 0 ) {
						throw new Forminator_Calculator_Exception( 'Error: Found closing bracket that does not have an opening bracket.' );
					}
				}
			}

			// Make sure a function is not followed by a symbol that is not of type opening bracket.
			if ( $expecting_opening_bracket ) {
				if ( ! $symbol instanceof Forminator_Calculator_Symbol_Opening_Bracket ) {
					throw new Forminator_Calculator_Exception( 'Error: Expected opening bracket (after a function) but got something else.' );
				}

				$expecting_opening_bracket = false;
			} elseif ( $symbol instanceof Forminator_Calculator_Symbol_Function_Abstract ) {
					$expecting_opening_bracket = true;
			}

			$symbol_node = new Forminator_Calculator_Parser_Node_Symbol( $token, $symbol );

			$symbol_nodes[] = $symbol_node;
		}

		// Make sure the term does not end with the name of a function but without an opening bracket.
		if ( $expecting_opening_bracket ) {
			throw new Forminator_Calculator_Exception( 'Error: Expected opening bracket (after a function) but reached the end of the term' );
		}

		// Make sure there are not too many opening brackets.
		if ( $open_bracket_counter > 0 ) {
			throw new Forminator_Calculator_Exception( 'Error: There is at least one opening bracket that does not have a closing bracket' );
		}

		return $symbol_nodes;
	}

	/**
	 * Expects a flat array of symbol nodes and (if possible) transforms
	 * it to a tree of nodes. Cares for brackets.
	 * Attention: Expects valid brackets!
	 * Check the brackets before you call this method.
	 *
	 * @param Forminator_Calculator_Parser_Node_Symbol[] $symbol_nodes Forminator_Calculator_Parser_Node_Symbol.
	 *
	 * @return Forminator_Calculator_Parser_Node_Abstract[]
	 * @throws Forminator_Calculator_Exception When there is an Calculator error.
	 */
	protected function create_tree_by_brackets( $symbol_nodes ) {
		$tree                 = array();
		$nodes_in_brackets    = array(); // AbstractSymbol nodes inside level-0-brackets.
		$open_bracket_counter = 0;

		foreach ( $symbol_nodes as $index => $symbol_node ) {
			if ( ! $symbol_node instanceof Forminator_Calculator_Parser_Node_Symbol ) {
				throw new Forminator_Calculator_Exception( 'Error: Expected symbol node, but got "' . esc_html( gettype( $symbol_node ) ) . '"' );// @codeCoverageIgnore.
			}

			if ( $symbol_node->get_symbol() instanceof Forminator_Calculator_Symbol_Opening_Bracket ) {
				++$open_bracket_counter;

				if ( $open_bracket_counter > 1 ) {
					$nodes_in_brackets[] = $symbol_node;
				}
			} elseif ( $symbol_node->get_symbol() instanceof Forminator_Calculator_Symbol_Closing_Bracket ) {
				--$open_bracket_counter;

				// Found a closing bracket on level 0.
				if ( 0 === $open_bracket_counter ) {
					$sub_tree = $this->create_tree_by_brackets( $nodes_in_brackets );

					// Subtree can be empty for example if the term looks like this: "()" or "functioname()".
					// But this is okay, we need to allow this so we can call functions without a parameter.
					$tree[]            = new Forminator_Calculator_Parser_Node_Container( $sub_tree );
					$nodes_in_brackets = array();
				} else {
					$nodes_in_brackets[] = $symbol_node;
				}
			} elseif ( 0 === $open_bracket_counter ) {
					$tree[] = $symbol_node;
			} else {
				$nodes_in_brackets[] = $symbol_node;
			}
		}

		return $tree;
	}

	/**
	 * Replaces [a SymbolNode that has a symbol of type AbstractFunction,
	 * followed by a node of type ContainerNode] by a FunctionNode.
	 * Expects the $nodes not including any function nodes (yet).
	 *
	 * @param Forminator_Calculator_Parser_Node_Abstract[] $nodes Forminator_Calculator_Parser_Node_Abstract.
	 *
	 * @return Forminator_Calculator_Parser_Node_Abstract[]
	 * @throws Forminator_Calculator_Exception When there is an Calculator error.
	 */
	protected function transform_tree_by_functions( $nodes ) {
		$transformed_nodes = array();

		$function_symbol_node = null;

		foreach ( $nodes as $node ) {
			if ( $node instanceof Forminator_Calculator_Parser_Node_Container ) {
				/**
				 * Forminator_Calculator_Parser_Node_Container
				 *
				 * @var Forminator_Calculator_Parser_Node_Container $node */
				$transformed_child_nodes = $this->transform_tree_by_functions( $node->get_child_nodes() );

				if ( null !== $function_symbol_node ) {
					$function_node        = new Forminator_Calculator_Parser_Node_Function( $transformed_child_nodes, $function_symbol_node );
					$transformed_nodes[]  = $function_node;
					$function_symbol_node = null;
				} else {

					// not a function.
					$node->set_child_nodes( $transformed_child_nodes );
					$transformed_nodes[] = $node;
				}
			} elseif ( $node instanceof Forminator_Calculator_Parser_Node_Symbol ) {
				/**
				 * Forminator_Calculator_Parser_Node_Symbol
				 *
				 * @var Forminator_Calculator_Parser_Node_Symbol $node */
				$symbol = $node->get_symbol();
				if ( $symbol instanceof Forminator_Calculator_Symbol_Function_Abstract ) {
					$function_symbol_node = $node;
				} else {
					$transformed_nodes[] = $node;
				}
			} else {
				throw new Forminator_Calculator_Exception( 'Error: Expected array node or symbol node, got "' . esc_html( gettype( $node ) ) . '"' );
			}
		}

		return $transformed_nodes;
	}

	/**
	 * Ensures the tree follows the grammar rules for terms
	 *
	 * @param array $nodes Nodes.
	 *
	 * @return void
	 * @throws Forminator_Calculator_Exception When there is an Calculator error.
	 */
	protected function check_grammar( $nodes ) {
		// TODO Make sure that separators are only in the child nodes of the array node of a function node.
		// (If this happens the calculator will throw an exception).

		foreach ( $nodes as $index => $node ) {
			if ( $node instanceof Forminator_Calculator_Parser_Node_Symbol ) {
				/**
				 * Forminator_Calculator_Parser_Node_Symbol
				 *
				 * @var $node Forminator_Calculator_Parser_Node_Symbol */

				$symbol = $node->get_symbol();

				if ( $symbol instanceof Forminator_Calculator_Symbol_Operator_Abstract ) {
					/**
					 * Forminator_Calculator_Symbol_Operator_Abstract
					 *
					 * @var $symbol Forminator_Calculator_Symbol_Operator_Abstract */

					$pos_of_right_operand = $index + 1;

					// Make sure the operator is positioned left of a (potential) operand (=prefix notation).
					// Example term: "-1".
					if ( $pos_of_right_operand >= count( $nodes ) ) {
						throw new Forminator_Calculator_Exception( 'Error: Found operator that does not stand before an operand.' );
					}

					$pos_of_left_operand = $index - 1;

					$left_operand = null;

					// Operator is unary if positioned at the beginning of a term.
					if ( $pos_of_left_operand >= 0 ) {
						$left_operand = $nodes[ $pos_of_left_operand ];

						if ( $left_operand instanceof Forminator_Calculator_Parser_Node_Symbol ) {
							/**
							 * Forminator_Calculator_Parser_Node_Symbol
							 *
							 * @var $left_operand Forminator_Calculator_Parser_Node_Symbol */
							if ( $left_operand->get_symbol() instanceof Forminator_Calculator_Symbol_Operator_Abstract  // example 1`+-`5 : + = operator, - = unary.
								|| $left_operand->get_symbol() instanceof Forminator_Calculator_Symbol_Separator // example func(1`,-`5) ,= separator, - = unary.
							) {
								// Operator is unary if positioned right to another operator.
								$left_operand = null;
							}
						}
					}

					// If null, the operator is unary.
					if ( null === $left_operand ) {
						if ( ! $symbol->get_operates_unary() ) {
							throw new Forminator_Calculator_Exception( 'Error: Found operator in unary notation that is not unary.' );
						}

						// Remember that this node represents a unary operator.
						$node->set_is_unary_operator( true );
					} elseif ( ! $symbol->get_operates_binary() ) {
							throw new Forminator_Calculator_Exception( 'Error: Found operator in binary notation that is not binary.' );
					}
				}
			} else {
				/**
				 * Forminator_Calculator_Parser_Node_Container
				 *
				 * @var $node Forminator_Calculator_Parser_Node_Container */
				$this->check_grammar( $node->get_child_nodes() );
			}
		}
	}
}