<?php
/**
 * Copyright 2020 Adobe
 * All Rights Reserved.
 *
 * NOTICE: Adobe permits you to use, modify, and distribute this file in
 * accordance with the terms of the Adobe license agreement accompanying
 * it.
 */
declare(strict_types=1);

namespace Magento\Mray\CodeStructuralElement\Php\NodeRuntimeTypeResolver;

use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;
use function array_map;
use function array_merge;

class Ruleset
{
    /**
     * @return static
     */
    public static function default(): self
    {
        static $instance;
        if (!isset($instance)) {
            $instance = new self([
                new Rule(
                    [
                        Expr\Include_::class, // included file may return any value
                        Expr\Yield_::class, // depends on value passed to Generator::send() or Generator::next()
                        Expr\YieldFrom::class,
                        Expr\Eval_::class, // evaluated expression may return any value
                        Expr\Error::class, // parser cannot handle expression so we cannot resolve type as well
                    ],
                    new AlwaysMixed()
                ),
                new Rule(
                    [
                        Expr\Cast\Bool_::class,
                        Expr\BooleanNot::class,
                        Expr\BinaryOp\BooleanAnd::class,
                        Expr\BinaryOp\BooleanOr::class,
                        Expr\BinaryOp\LogicalAnd::class,
                        Expr\BinaryOp\LogicalOr::class,
                        Expr\BinaryOp\LogicalXor::class,
                        Expr\BinaryOp\Equal::class,
                        Expr\BinaryOp\Identical::class,
                        Expr\BinaryOp\NotEqual::class,
                        Expr\BinaryOp\NotIdentical::class,
                        Expr\BinaryOp\Smaller::class,
                        Expr\BinaryOp\Greater::class,
                        Expr\BinaryOp\SmallerOrEqual::class,
                        Expr\BinaryOp\GreaterOrEqual::class,
                        Expr\Instanceof_::class,
                        Expr\Isset_::class,
                        Expr\Empty_::class,
                    ],
                    new AlwaysBool()
                ),
                new Rule(
                    [
                        Scalar\String_::class,
                        Scalar\Encapsed::class,
                        Scalar\EncapsedStringPart::class,
                        Scalar\MagicConst\Class_::class,
                        Scalar\MagicConst\Dir::class,
                        Scalar\MagicConst\File::class,
                        Scalar\MagicConst\Function_::class,
                        Scalar\MagicConst\Method::class,
                        Scalar\MagicConst\Namespace_::class,
                        Scalar\MagicConst\Trait_::class,
                        Expr\Cast\String_::class,
                        Expr\BinaryOp\Concat::class,
                        Expr\AssignOp\Concat::class,
                    ],
                    new AlwaysString()
                ),
                new Rule(
                    [
                        Scalar\LNumber::class,
                        Scalar\MagicConst\Line::class,
                        Expr\Cast\Int_::class,
                        Expr\BinaryOp\Spaceship::class,
                        Expr\Print_::class,
                        Expr\BinaryOp\ShiftLeft::class,
                        Expr\BinaryOp\ShiftRight::class,
                        Expr\AssignOp\ShiftLeft::class,
                        Expr\AssignOp\ShiftRight::class,
                    ],
                    new AlwaysInt()
                ),
                new Rule(
                    [
                        Scalar\DNumber::class,
                        Expr\Cast\Double::class,
                    ],
                    new AlwaysFloat()
                ),
                new Rule(
                    [
                        Expr\Cast\Array_::class,
                        Expr\Array_::class,
                        Expr\List_::class,
                    ],
                    new AlwaysArray()
                ),
                new Rule(
                    [
                        Expr\Cast\Unset_::class,
                    ],
                    new AlwaysNull()
                ),
                new Rule(
                    [
                        Expr\Cast\Object_::class,
                    ],
                    new AlwaysObject()
                ),
                new Rule(
                    [
                        Expr\Closure::class,
                        Expr\ArrowFunction::class,
                    ],
                    new AlwaysCallable()
                ),
                new Rule(
                    [
                        Expr\BitwiseNot::class,
                        Expr\BinaryOp\BitwiseAnd::class,
                        Expr\BinaryOp\BitwiseOr::class,
                        Expr\BinaryOp\BitwiseXor::class,
                        Expr\AssignOp\BitwiseAnd::class,
                        Expr\AssignOp\BitwiseOr::class,
                        Expr\AssignOp\BitwiseXor::class,
                        // Shift Operators is exclusion from this logic and always return int
                    ],
                    new BitwiseOperators()
                ),
                new Rule(
                    [
                        Expr\UnaryPlus::class,
                        Expr\UnaryMinus::class,
                        Expr\BinaryOp\Plus::class,
                        Expr\BinaryOp\Minus::class,
                        Expr\BinaryOp\Mul::class,
                        Expr\BinaryOp\Div::class,
                        Expr\BinaryOp\Mod::class,
                        Expr\BinaryOp\Pow::class,
                        Expr\AssignOp\Plus::class,
                        Expr\AssignOp\Minus::class,
                        Expr\AssignOp\Mul::class,
                        Expr\AssignOp\Div::class,
                        Expr\AssignOp\Mod::class,
                        Expr\AssignOp\Pow::class,
                    ],
                    new ArithmeticOperators()
                ),
                new Rule(
                    [
                        Expr\Ternary::class,
                    ],
                    new TernaryOperator()
                ),
                new Rule(
                    [
                        Expr\BinaryOp\Coalesce::class,
                        Expr\AssignOp\Coalesce::class,
                    ],
                    new CoalesceOperators()
                ),
                new Rule(
                    [
                        Expr\PostInc::class,
                        Expr\PostDec::class,
                    ],
                    new PostCrementOperators()
                ),
                new Rule(
                    [
                        Expr\New_::class,
                    ],
                    new New_()
                ),
                new Rule(
                    [
                        Expr\Clone_::class,
                    ],
                    new Clone_()
                ),
                new Rule(
                    [
                        Expr\ArrayItem::class,
                    ],
                    new ArrayItem()
                ),
                new Rule(
                    [
                        Expr\Array_::class,
                    ],
                    new Array_()
                ),
                new Rule(
                    [
                        Expr\ArrayDimFetch::class,
                    ],
                    new ArrayDimFetch()
                ),
                new Rule(
                    [
                        Expr\PropertyFetch::class,
                        Expr\NullsafePropertyFetch::class,
                    ],
                    new PropertyFetch()
                ),
                new Rule(
                    [
                        Expr\StaticPropertyFetch::class,
                    ],
                    new StaticPropertyFetch()
                ),
                new Rule(
                    [
                        Expr\ClassConstFetch::class,
                    ],
                    new ClassConstFetch()
                ),
                new Rule(
                    [
                        Expr\ConstFetch::class,
                    ],
                    new ConstFetch()
                ),
                new Rule(
                    [
                        Expr\Assign::class,
                        Expr\AssignRef::class,
                        Expr\ErrorSuppress::class,
                    ],
                    new ExprPassThrough()
                ),
                new Rule(
                    [
                        Expr\PreDec::class,
                        Expr\PreInc::class,
                    ],
                    new PreCrementOperators()
                ),
                new Rule(
                    [
                        Expr\ShellExec::class,
                    ],
                    new ShellExec()
                ),
                new Rule(
                    [
                        Expr\Exit_::class, // ends execution, result cannot be used
                        Expr\ClosureUse::class, // part of closure syntax, cannot be assigned or used
                        Expr\Throw_::class, // ends execution of code block, result cannot be used in same scope
                    ],
                    new Void_()
                ),
                new Rule(
                    [
                        Expr\Variable::class,
                    ],
                    new Variable()
                ),
                new Rule(
                    [
                        Expr\MethodCall::class,
                        Expr\NullsafeMethodCall::class,
                        Expr\StaticCall::class,
                    ],
                    new MethodReturnType()
                ),
                new Rule(
                    [
                        Expr\FuncCall::class,
                    ],
                    new FuncCall()
                ),
                new Rule(
                    [
                        Expr\Match_::class,
                    ],
                    new MatchResolver()
                ),
            ]);
        }
        return $instance;
    }

    /** @var Rule[] */
    private $rules;

    /**
     * Ruleset constructor.
     * @param Rule[] $rules
     */
    public function __construct(array $rules)
    {
        $rules = array_map(function (Rule $rule) {
            return $rule;
        }, $rules);
        $this->rules = $rules;
    }

    /**
     * @param Ruleset $other
     * @return $this
     */
    public function merge(self $other): self
    {
        return new self(array_merge($this->rules, $other->rules));
    }

    /**
     * @param Rule $rule
     * @return $this
     */
    public function extend(Rule $rule): self
    {
        return $this->merge(new self([$rule]));
    }

    /**
     * Returns list of resolvers for known expression types.
     *
     * Keys are class names of corresponding expressions.
     *
     * @return Resolver[]
     */
    public function compile(): array
    {
        $resolversMap = [];
        foreach ($this->rules as $rule) {
            foreach ($rule->getTargetExpressions() as $exprType) {
                unset($resolversMap[$exprType]);
                $resolversMap[$exprType] = $rule->getResolver();
            }
        }
        return $resolversMap;
    }
}
