<?php

namespace Dedoc\Scramble\Support\Type;

use WeakMap;

class TypeTraverser
{
    /** @var WeakMap<Type, true> */
    private WeakMap $traversedTypes;

    /**
     * @param  TypeVisitor[]  $visitors
     */
    public function __construct(
        private array $visitors = [],
    ) {
        $this->traversedTypes = new WeakMap;
    }

    public function traverse(Type $type): Type
    {
        if (isset($this->traversedTypes[$type])) {
            return $type;
        }

        $this->traversedTypes[$type] = true;

        $enterResult = $this->enterType($type);

        if ($enterResult === TypeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN) {
            return $type;
        }

        if ($enterResult instanceof Type) {
            $type = $enterResult;
        }

        if ($enterResult !== TypeVisitor::DONT_TRAVERSE_CHILDREN) {
            $propertiesWithNodes = $type->nodes();

            foreach ($propertiesWithNodes as $propertyWithNode) {
                $node = $type->$propertyWithNode;
                if (! is_array($node)) {
                    $type->$propertyWithNode = $this->traverse($node);
                } else {
                    foreach ($node as $index => $item) {
                        $type->$propertyWithNode[$index] = $this->traverse($item);
                    }
                }
            }
        }

        $leaveResult = $this->leaveType($type);
        if ($leaveResult instanceof Type) {
            $type = $leaveResult;
        }

        return $type;
    }

    private function enterType(Type $type): Type|int|null
    {
        $result = null;
        $resultType = $type;
        foreach ($this->visitors as $visitor) {
            $enterResult = $visitor->enter($resultType);

            if ($enterResult === TypeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN) {
                return $enterResult;
            }

            if ($enterResult instanceof Type) {
                $resultType = $enterResult;
            }

            $result = $enterResult;
        }

        return $resultType === $type ? $result : $resultType;
    }

    private function leaveType(Type $type): Type|int|null
    {
        $result = null;
        $resultType = $type;
        foreach ($this->visitors as $visitor) {
            $leaveResult = $visitor->leave($resultType);

            if ($leaveResult instanceof Type) {
                $resultType = $leaveResult;
            }

            $result = $leaveResult;
        }

        return $resultType === $type ? $result : $resultType;
    }
}
