<?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\Usage\ClassConstFetch;
use Magento\Mray\CodeStructuralElement\Php\Usage\ClassExtended;
use Magento\Mray\CodeStructuralElement\Php\Usage\InterfaceExtended;
use Magento\Mray\CodeStructuralElement\Php\Usage\InterfaceImplemented;
use Magento\Mray\CodeStructuralElement\Php\Usage\MethodCall;
use Magento\Mray\CodeStructuralElement\Php\Usage\InheritedMember;
use Magento\Mray\CodeStructuralElement\Php\Usage\MethodExtended;
use Magento\Mray\CodeStructuralElement\Php\Usage\NameDirectReference;
use Magento\Mray\CodeStructuralElement\Php\Usage\Probe;
use Magento\Mray\CodeStructuralElement\Php\Usage\PropertyAssign;
use Magento\Mray\CodeStructuralElement\Php\Usage\PropertyFetch;
use Magento\Mray\CodeStructuralElement\Php\Usage\UsageRegistry;
use PhpParser\Node;
use PhpParser\NodeVisitor;
use function array_merge;
use function array_pop;
use function count;

class UsageDiscovery implements NodeVisitor
{
    /** @var UsageRegistry  */
    private $usageRegistry;
    /** @var array  */
    private $globalLocation;

    /** @var Probe[] */
    private $probes;

    /** @var array  */
    private $stack = [];

    /**
     * @param UsageRegistry $usageRegistry
     * @param array $globalLocation
     */
    public function __construct(
        UsageRegistry $usageRegistry,
        array $globalLocation = []
    ) {
        $this->usageRegistry = $usageRegistry;
        $this->globalLocation = $globalLocation;
        $this->probes = [
            new NameDirectReference(),
            new ClassExtended(),
            new InterfaceImplemented(),
            new InterfaceExtended(),
            new MethodCall(),
            new PropertyFetch(),
            new PropertyAssign(),
            new ClassConstFetch(),
            new InheritedMember(),
            new MethodExtended()
        ];
    }

    /**
     * @inheritDoc
     */
    public function beforeTraverse(array $nodes)
    {
        $this->stack = [];
        return null;
    }

    /**
     * @inheritDoc
     */
    public function enterNode(Node $node)
    {
        if ($node instanceof Node\Stmt\Class_) {
            $this->stack[] = ['class' => $this->name($node->namespacedName ?? null)];
        } elseif ($node instanceof Node\Stmt\Interface_) {
            $this->stack[] = ['interface' => $this->name($node->namespacedName ?? null)];
        } elseif ($node instanceof Node\Stmt\ClassMethod) {
            $this->stack[] = array_merge(
                $this->stack[count($this->stack) - 1] ?? [],
                ['method' => $this->name($node->name ?? null)]
            );
        } elseif ($node instanceof Node\Stmt\PropertyProperty) {
            $this->stack[] = array_merge(
                $this->stack[count($this->stack) - 1] ?? [],
                ['property' => $this->name($node->name ?? null)]
            );
        } elseif ($node instanceof Node\Stmt\Function_) {
            $this->stack[] = ['function' => $this->name($node->namespacedName ?? null)];
        } elseif ($node instanceof Node\Stmt\Trait_) {
            $this->stack[] = ['trait' => $this->name($node->namespacedName ?? null)];
        } elseif ($node instanceof Node\Const_) {
            $this->stack[] = array_merge(
                $this->stack[count($this->stack) - 1] ?? [],
                ['const' => $this->name($node->name ?? null)]
            );
        }

        return null;
    }

    /**
     * @param mixed $name
     * @return string
     */
    private function name($name)
    {
        if ($name === null) {
            return '@anonymous';
        }
        if ($name instanceof Node\Name || $name instanceof Node\Identifier) {
            return (string)$name;
        }
        return '@unknown';
    }

    /**
     * @inheritDoc
     */
    public function leaveNode(Node $node)
    {
        foreach ($this->probes as $probe) {
            foreach ($probe->check($node) as $usageCase) {
                $usageCase->setDeclarationContext($this->stack[count($this->stack) - 1] ?? []);
                $usageCase->setLocationContext($this->globalLocation);
                $this->usageRegistry->register($usageCase);
            }
        }

        if ($node instanceof Node\Stmt\Class_ ||
            $node instanceof Node\Stmt\ClassMethod ||
            $node instanceof Node\Stmt\ClassConst ||
            $node instanceof Node\Stmt\Function_ ||
            $node instanceof Node\Stmt\Const_ ||
            $node instanceof Node\Stmt\Trait_
        ) {
            array_pop($this->stack);
        }
    }

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