<?php

set_error_handler(function ($errno, $errstr, $errfile, $errline ) {
    throw new \ErrorException($errstr, $errno, 0, $errfile, $errline);
});

class Addition extends ArithmeticOperator
{
	public static $precedence = 2;
	public static $isLeftAssociative = true;

	public function operate(SimpleFraction $leftOperand, SimpleFraction $rightOperand)
	{
		if ($rightOperand->getNumerator() === 0) {
			return $leftOperand;
		}

		$commonDenumerator = $leftOperand->getDenumerator() * $rightOperand->getDenumerator();
		$resultNumerator = $leftOperand->getNumerator() * $rightOperand->getDenumerator();
		$resultNumerator += $leftOperand->getDenumerator() * $rightOperand->getNumerator();

		return new SimpleFraction($resultNumerator, $commonDenumerator);
	}
}

abstract class ArithmeticOperator
{
	public function getPrecedence()
	{
		return static::$precedence;
	}

	public function isLeftAssociative()
	{
		return !! static::$isLeftAssociative;
	}

	public function isRightAssociative()
	{
		return ! static::$isLeftAssociative;
	}

	abstract function operate(SimpleFraction $leftOperand, SimpleFraction $rightOperand);
}

class DivideByZeroException extends Exception
{
	public function __construct($numerator)
	{
		echo sprintf("Ошибка: (%s/0) делить на ноль нельзя", $numerator);
	}
}

class Division extends ArithmeticOperator
{
	public static $precedence = 3;
	public static $isLeftAssociative = true;

	public function operate(SimpleFraction $leftOperand, SimpleFraction $rightOperand)
	{
		if ($rightOperand->getNumerator() === 0) {
			throw new DivideByZeroException($leftOperand);
		}

		$multiplication = new Multiplication();
		return $multiplication->operate($leftOperand, $rightOperand->invert());
	}
}

class LeftParenthesis {}

class Lexer
{
	public function getLexemsFromInfix($infix)
	{
		if (!$this->isValidInfix($infix)) throw new InvalidArgumentException();

		preg_match_all('/(-?\d+(\.\d+)?|([\+\-\/\*\^\(\)]))/', $infix, $match);
		$lexems = $match[0];

		return $lexems;
	}

	private function isValidInfix($infix)
	{
		return true;
	}
}

class MathHelper
{
	public static function getLeastCommonMultiple($a, $b) {
		$max = max($a, $b);
		$min = min($a, $b);

		$lcm = $max;
		for ($i = 1; $lcm % $min != 0; $i++) { 
			$lcm = $max * $i;
		}

		return $lcm;
	}
}

class Multiplication extends ArithmeticOperator
{
	public static $precedence = 3;
	public static $isLeftAssociative = true;

	public function operate(SimpleFraction $leftOperand, SimpleFraction $rightOperand)
	{
		$newNumerator = $leftOperand->getNumerator() * $rightOperand->getNumerator();
		$newDenumerator = $leftOperand->getDenumerator() * $rightOperand->getDenumerator();

		return new SimpleFraction($newNumerator, $newDenumerator);
	}
}

class OperatorFactory
{
	public static function getOperatorBySign($sign)
	{
		switch ($sign) {
			case '+': return new Addition();
			case '-': return new Substraction();
			case '*': return new Multiplication();
			case '/': return new Division();
			case '^': return new Power();
			case '(': return new LeftParenthesis();
			case ')': return new RightParenthesis();
			default: throw new InvalidArgumentException();
		}
	}
}

class Power extends ArithmeticOperator
{
	public static $precedence = 4;
	public static $isLeftAssociative = false;

	public function operate(SimpleFraction $leftOperand, SimpleFraction $rightOperand)
	{
		$newNumerator = pow($leftOperand->getNumerator(), $rightOperand->getNumerator());
		$newDenumerator = pow($leftOperand->getDenumerator(), $rightOperand->getNumerator());

		return new SimpleFraction($newNumerator, $newDenumerator);
	}
}

class RightParenthesis {}

class RpnQueueEvaluator
{
	private $stack;

	public function __construct(SplStack $stack)
	{
		$this->stack = $stack;
	}

	public function evaluate(SplQueue $tokens)
	{
		foreach ($tokens as $token) {
			if ($token instanceof SimpleFraction) {
				$this->stack->push($token);
			} elseif ($token instanceof ArithmeticOperator) {
				$rightOperand = $this->stack->pop();
				$leftOperand  = $this->stack->pop();
				$fraction = $token->operate($leftOperand, $rightOperand);
				$this->stack->push($fraction);
			} else {
				throw new InvalidArgumentException();
			}
		}

		return $this->stack->top();
	}
}

class ShuntingYard
{
	private $operatorStack;
	private $outputQueue;

	public function __construct(SplStack $operatorStack, SplQueue $outputQueue)
	{
		$this->operatorStack = $operatorStack;
		$this->outputQueue = $outputQueue;
	}

	public function createRpnQueueFromTokens(array $tokens)
	{
		foreach ($tokens as $token) {
			if ($token instanceof SimpleFraction) {
				$this->outputQueue->enqueue($token);
			} elseif ($token instanceof ArithmeticOperator) {
				if ($this->operatorStack->isEmpty()) {
					$this->operatorStack->push($token);
					continue;
				}

				$operatorTop = $this->operatorStack->top();
				if (($operatorTop instanceof ArithmeticOperator)
					&& (($token->isLeftassociative() && $token->getPrecedence() <= $operatorTop->getPrecedence())
   					|| ($token->isRightAssociative() && $token->getPrecedence() < $operatorTop->getPrecedence()))
					) {
						$this->outputQueue->enqueue($this->operatorStack->pop());
				}

				$this->operatorStack->push($token);
			} elseif ($token instanceof LeftParenthesis) {
				$this->operatorStack->push($token);
			} elseif ($token instanceof RightParenthesis) {
				$operatorTop = $this->operatorStack->top();
				if (!$operatorTop instanceof LeftParenthesis) {
					$this->outputQueue->enqueue($this->operatorStack->pop());
				}
				$this->operatorStack->pop();
			}
		}

		while (!$this->operatorStack->isEmpty()) {
			$operatorTop = $this->operatorStack->pop();
			if ($operatorTop instanceof LeftParenthesis || $operatorTop instanceof RightParenthesis) {
				throw new Exception('Mismatched parentheses');
			}
			$this->outputQueue->enqueue($operatorTop);
		}

		return $this->outputQueue;
	}
}

class SimpleFraction
{
	private $numerator; 
	private $denumerator;

	public function __construct($numerator, $denumerator = 1)
	{
		$this->numerator = $numerator;
		$this->denumerator = $denumerator;
		$this->simplify();
	}

	public static function createFromDecimal($decimal)
	{
		preg_match_all('/^(-?\d+)(?:\.(\d+))?$/', (string) $decimal, $matches);

		if (!$matches) throw new InvalidArgumentException();
		$integerPart = $matches[1][0];
		$fractionalPart = empty($matches[2][0]) ? 0 : $matches[2][0];

		$numerator = $integerPart * 10 + $fractionalPart;
		$denumerator = 10;

		return new self($numerator, $denumerator);
	}

	public function getDenumerator()
	{
		return $this->denumerator;
	}

	public function getNumerator()
	{
		return $this->numerator;	
	}

	public function changeSign()
	{
		$this->numerator *= -1;

		return $this;
	}

	public function invert()
	{
		list($this->denumerator, $this->numerator) = [$this->numerator, $this->denumerator];

		return $this;
	}

	public function showAsDecimal()
	{
		$result = $this->numerator / $this->denumerator;

		if (is_int($result)) {
			$valueAsString = sprintf('%s', $result);
		} else {
			list($integerPart, $fractionalPart) = explode('.', (string) $result);
			$valueAsString = sprintf('%s.%s', $integerPart, $fractionalPart);
		}

		return $valueAsString;
	}

	public function __toString()
	{
		if ($this->denumerator == 10) {
			$valueAsString = sprintf('%s', $this->showAsDecimal());
		} elseif ($this->denumerator == 1 || $this->denumerator == -1) {
			$valueAsString = sprintf('%s', $this->numerator * $this->denumerator);
		} else {
			$valueAsString = sprintf('%s/%s', $this->numerator, $this->denumerator);
		}

		return $valueAsString;
	}

	private function simplify()
	{
		if ($this->numerator === 0) return;
		$lcm = MathHelper::getLeastCommonMultiple($this->numerator, $this->denumerator);
		$oldNumerator = $this->numerator;
		$this->numerator = $lcm / $this->denumerator;
		$this->denumerator = $lcm / $oldNumerator;
	}
}

class Substraction extends ArithmeticOperator
{
	public static $precedence = 2;
	public static $isLeftAssociative = true;

	public function operate(SimpleFraction $leftOperand, SimpleFraction $rightOperand)
	{
		$addition = new Addition();

		return $addition->operate($leftOperand, $rightOperand->changeSign());
	}
}

class Tokenizer
{
	public function tokenizeLexems(array $lexems)
	{
		$tokens = [];

		foreach ($lexems as $lexem) {
			if (is_numeric($lexem)) {
				$fraction = SimpleFraction::createFromDecimal($lexem);
				$tokens[] = $fraction;
			} elseif (in_array($lexem, ['+', '-', '/', '*', '^', '(', ')'])) {
				$operator = OperatorFactory::getOperatorBySign($lexem);
				$tokens[] = $operator;
			} else {
				throw new Exception(sprintf('Invalid lexem %s given', $lexem));
			}
		}

		return $tokens;
	}
}

$infix_correctAnswer = [
	'))))'	=> 'Ошибка',
	'- - 1' => '1',	
	'2 + 3' => '5',
	'4 - 3' => ' 1',
	'2 + (-3)' => '-1',
	'4 * 5' => '20',
	'6/4' => '3/2',
	'1.2 + 1/2' => '1.7',
	'1/(-3)   ' => '-1/3',
	'0.5 + 0.2' => '0.7',
	'3 ^ 2 ^ 2' => '81',
	'17654/342' => '8827/171',
	'2/3 ^ 2' =>  '2/9',
	'(2/3) ^ 2' => '4/9',
	'(2 + 3) / (2 - 2)' => 'Ошибка'
];

$lexer = new Lexer();
$tokenizer = new Tokenizer();
$shuntingYard = new ShuntingYard(new SplStack, new SplQueue);
$rpnQueueEvaluator = new RpnQueueEvaluator(new SplStack);

foreach ($infix_correctAnswer as $infix => $correctAnswer) {
	$lexems = $lexer->getLexemsFromInfix($infix);
	$tokens = $tokenizer->tokenizeLexems($lexems);
	$rpnQueue = $shuntingYard->createRpnQueueFromTokens($tokens);
	echo sprintf("\n%20s ожидается: %10s, получено ", $infix, $correctAnswer);
	$result = $rpnQueueEvaluator->evaluate($rpnQueue);
	echo sprintf('%10s', $result);
}
