<?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\NodeVisitor;

use Magento\Mray\CodeStructuralElement\Php\Reflection\Context;
use PhpParser\Node;
use PhpParser\NodeVisitor;
use function array_pop;

class ContextSwitcher implements NodeVisitor
{
    /** @var Context\Pointer */
    private $context;

    /** @var Node|null */
    private $contextDefiningNode = null;
    /** @var array */
    private $nodesStack = [];
    /** @var array */
    private $contextsStack = [];

    /**
     * @param Context\Pointer $context
     */
    public function __construct(Context\Pointer $context)
    {
        $this->context = $context;
    }

    /**
     * @inheritDoc
     */
    public function beforeTraverse(array $nodes)
    {
        return null;
    }

    /**
     * @inheritDoc
     */
    public function enterNode(Node $node)
    {
        if ($node instanceof Node\Stmt\Function_) {
            $this->enterContext($node, new Context\Function_($this->context->getGlobal()));
            return null;
        }
        if ($node instanceof Node\Stmt\ClassLike) {
            $this->enterContext($node, new Context\ClassLike($node, $this->context->getGlobal()));
            return null;
        }
        if ($node instanceof Node\Stmt\ClassMethod) {
            /** @var Context\ClassLike $context */
            $context = $this->context->value();
            $this->enterContext($node, new Context\ClassMethod($node, $context));
            return null;
        }
        if ($node instanceof Node\Expr\Closure) {
            $this->enterContext($node, new Context\Closure($this->context->value()));
            return null;
        }
        if ($node instanceof Node\Expr\ArrowFunction) {
            $this->enterContext($node, new Context\ArrowFunction($this->context->value()));
            return null;
        }

        return null;
    }

    /**
     * @inheritDoc
     */
    public function leaveNode(Node $node)
    {
        if ($node === $this->contextDefiningNode) {
            $this->leaveContext();
        }
        return null;
    }

    /**
     * @inheritDoc
     */
    public function afterTraverse(array $nodes)
    {
        return null;
    }

    /**
     * @param Node $node
     * @param Context $context
     */
    private function enterContext(Node $node, Context $context)
    {
        $contextToRestore = $this->context->switch($context);
        $this->contextsStack[] = $contextToRestore;

        $this->nodesStack[] = $this->contextDefiningNode;
        $this->contextDefiningNode = $node;
    }

    /**
     * Context is restored.
     */
    private function leaveContext()
    {
        $contextToRestore = array_pop($this->contextsStack);
        $this->context->switch($contextToRestore);

        $this->contextDefiningNode = array_pop($this->nodesStack);
    }
}
