<?php

declare(strict_types=1);

namespace Phan\Analysis;

use ast\Node;
use Phan\AST\Visitor\KindVisitorImplementation;

/**
 * This checks if $inner is unconditionally reachable from the passed in node.
 *
 * This returns false if the node is not even a descendent node.
 *
 * @see BlockExitStatusChecker
 * @internal - This is specialized for AST_ARG_LIST right now.
 */
final class ReachabilityChecker extends KindVisitorImplementation
{
    /** @var Node the node we're checking for reachability. */
    private $inner;

    public function __construct(Node $inner)
    {
        $this->inner = $inner;
    }

    public function visitArgList(Node $node): ?bool
    {
        if ($node === $this->inner) {
            return true;
        }
        return $this->visit($node);
    }

    /**
     * If we don't know how to analyze a node type (or left it out), assume it always proceeds
     * @return ?bool - The status bitmask corresponding to always proceeding
     */
    public function visit(Node $node): ?bool
    {
        foreach ($node->children as $child) {
            if (!($child instanceof Node)) {
                continue;
            }
            $result = $this->__invoke($child);
            if ($result !== null) {
                return $result;
            }
        }
        return null;
    }

    /**
     * @unused-param $node
     * @return ?bool this gives up on analyzing catch lists
     */
    public function visitCatchList(Node $node): ?bool
    {
        return null;
    }

    /**
     * @return ?bool this gives up on analyzing switches, except for the condition
     */
    public function visitSwitch(Node $node): ?bool
    {
        $cond = $node->children['cond'];
        if ($cond instanceof Node) {
            return $this->__invoke($cond);
        }
        return null;
    }

    /**
     * @return ?bool this gives up on analyzing matches, except for the condition
     */
    public function visitMatch(Node $node): ?bool
    {
        $cond = $node->children['cond'];
        if ($cond instanceof Node) {
            return $this->__invoke($cond);
        }
        return null;
    }

    /**
     * @return ?bool this gives up on analyzing for loops, except for the initializer and condition
     */
    public function visitFor(Node $node): ?bool
    {
        $init = $node->children['init'];
        if ($init instanceof Node) {
            $result = $this->__invoke($init);
            if ($result !== null) {
                return $result;
            }
        }
        $cond = $node->children['cond'];
        if ($cond instanceof Node) {
            return $this->__invoke($cond);
        }
        return null;
    }

    /**
     * @return ?bool this gives up on analyzing loops, except for the condition
     */
    public function visitWhile(Node $node): ?bool
    {
        $cond = $node->children['cond'];
        if ($cond instanceof Node) {
            return $this->__invoke($cond);
        }
        return null;
    }

    /**
     * @return ?bool this gives up on analyzing loops, except for the condition
     */
    public function visitForeach(Node $node): ?bool
    {
        $expr = $node->children['expr'];
        if ($expr instanceof Node) {
            return $this->__invoke($expr);
        }
        return null;
    }

    /**
     * @unused-param $node
     * @override
     */
    public function visitBreak(Node $node): ?bool
    {
        return false;
    }

    /**
     * @unused-param $node
     * @override
     */
    public function visitContinue(Node $node): ?bool
    {
        return false;
    }

    public function visitReturn(Node $node): bool
    {
        $expr = $node->children['expr'];
        if (!($expr instanceof Node)) {
            return false;
        }
        return $this->__invoke($expr) ?? false;
    }

    /**
     * @unused-param $node
     * @override
     */
    public function visitClosure(Node $node): ?bool
    {
        return null;
    }

    /**
     * @unused-param $node
     * @override
     */
    public function visitArrowFunc(Node $node): ?bool
    {
        return null;
    }

    /**
     * @unused-param $node
     * @override
     */
    public function visitFuncDecl(Node $node): ?bool
    {
        return null;
    }

    /**
     * @unused-param $node
     * @override
     */
    public function visitClass(Node $node): ?bool
    {
        return null;
    }

    public function visitThrow(Node $node): bool
    {
        $expr = $node->children['expr'];
        if (!($expr instanceof Node)) {
            return false;
        }
        return $this->__invoke($expr) ?? false;
    }

    public function visitExit(Node $node): bool
    {
        $expr = $node->children['expr'];
        if (!($expr instanceof Node)) {
            return false;
        }
        return $this->__invoke($expr) ?? false;
    }

    /**
     * @return ?bool the first result seen for any statement, or null.
     */
    public function visitStmtList(Node $node): ?bool
    {
        foreach ($node->children as $child) {
            if (!($child instanceof Node)) {
                continue;
            }
            $result = $this->__invoke($child);
            if ($result !== null) {
                return $result;
            }
            $status = (new BlockExitStatusChecker())->__invoke($child);
            if ($status !== BlockExitStatusChecker::STATUS_PROCEED) {
                if ($status & BlockExitStatusChecker::STATUS_THROW_OR_RETURN_BITMASK) {
                    return false;
                }
                continue;
            }
        }
        return null;
    }

    /**
     * Analyzes a node with kind \ast\AST_IF
     * @return ?bool the result seen for an if statement (if $node contains $this->inner or causes this to give up), or null
     * @override
     */
    public function visitIf(Node $node): ?bool
    {
        foreach ($node->children as $i => $child) {
            // TODO could check first if element (not important)
            // @phan-suppress-next-line PhanPartialTypeMismatchArgument
            $result = $this->visitIfElem($child);
            if ($result !== null) {
                return $result && $i === 0;
            }
        }
        return null;
    }

    /**
     * Analyzes a node with kind \ast\AST_IF_ELEM
     * @return ?bool the result seen for an if statement element (if $node contains $this->inner or causes this to give up), or null
     */
    public function visitIfElem(Node $node): ?bool
    {
        $cond = $node->children['cond'] ?? null;
        if ($cond instanceof Node) {
            $result = $this->__invoke($cond);
            if ($result !== null) {
                return $result;
            }
        }
        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable this is never null
        $result = $this->__invoke($node->children['stmts']);
        if ($result !== null) {
            // This is a conditional; it's not guaranteed to work
            return false;
        }
        return null;
    }

    /**
     * Analyzes a node with kind \ast\AST_CONDITIONAL
     * @return ?bool the result seen for a conditional
     */
    public function visitConditional(Node $node): ?bool
    {
        $cond = $node->children['cond'];
        if ($cond instanceof Node) {
            $result = $this->__invoke($cond);
            if ($result !== null) {
                return $result;
            }
        }
        foreach (['true', 'false'] as $sub_node_name) {
            $value = $node->children[$sub_node_name];
            if (!($value instanceof Node)) {
                continue;
            }
            $result = $this->__invoke($value);
            if ($result !== null) {
                // This is a conditional; it's not guaranteed to work
                return false;
            }
        }
        return null;
    }

    /**
     * Returns true if there are no break/return/throw/etc statements
     * within the method that would prevent $inner (a descendant node of $node)
     * to be reached from the start of evaluating the statements in $node.
     *
     * This does not attempt to check if any statements in $node might indirectly throw.
     */
    public static function willUnconditionallyBeReached(Node $node, Node $inner): bool
    {
        return (new self($inner))->__invoke($node) ?? false;
    }
}
