<?php 
 
/* 
 * This file is part of Chevere. 
 * 
 * (c) Rodolfo Berrios <[email protected]> 
 * 
 * For the full copyright and license information, please view the LICENSE 
 * file that was distributed with this source code. 
 */ 
 
declare(strict_types=1); 
 
namespace Chevere\Workflow; 
 
use Chevere\DataStructure\Interfaces\VectorInterface; 
use Chevere\DataStructure\Map; 
use Chevere\DataStructure\Traits\MapTrait; 
use Chevere\DataStructure\Vector; 
use Chevere\Workflow\Interfaces\GraphInterface; 
use Chevere\Workflow\Interfaces\JobInterface; 
use InvalidArgumentException; 
use function Chevere\Message\message; 
 
final class Graph implements GraphInterface 
{ 
    /** 
     * @template-use MapTrait<VectorInterface<string>> 
     */ 
    use MapTrait; 
 
    /** 
     * @var VectorInterface<string> 
     */ 
    private VectorInterface $syncJobs; 
 
    public function __construct() 
    { 
        $this->map = new Map(); 
        $this->syncJobs = new Vector(); 
    } 
 
    public function withPut( 
        string $name, 
        JobInterface $job, 
    ): GraphInterface { 
        $vector = $job->dependencies(); 
        $this->assertNotSelfDependency($name, $vector); 
        $new = clone $this; 
        foreach ($vector as $dependency) { 
            if (! $new->has($dependency)) { 
                $new->map = $new->map 
                    ->withPut($dependency, new Vector()); 
            } 
        } 
        if ($new->map->has($name)) { 
            /** @var VectorInterface<string> $existing */ 
            $existing = $new->map->get($name); 
            $merge = array_merge($existing->toArray(), $vector->toArray()); 
            $vector = new Vector(...$merge); 
        } 
        $new->handleDependencyUpdate($name, $vector); 
        $new->map = $new->map->withPut($name, $vector); 
        if ($job->isSync()) { 
            $new->syncJobs = $new->syncJobs->withPush($name); 
        } 
 
        return $new; 
    } 
 
    public function has(string $job): bool 
    { 
        return $this->map->has($job); 
    } 
 
    public function get(string $job): VectorInterface 
    { 
        /** @var VectorInterface<string> */ 
        return $this->map->get($job); 
    } 
 
    public function hasDependencies(string $job, string ...$dependencies): bool 
    { 
        /** @var VectorInterface<string> $array */ 
        $array = $this->map->get($job); 
 
        return $array->contains(...$dependencies); 
    } 
 
    public function toArray(): array 
    { 
        $sort = []; 
        $previous = []; 
        $sync = []; 
        $toIndex = 0; 
        foreach ($this->getSortAsc() as $job => $dependencies) { 
            foreach ($dependencies as $dependency) { 
                if (in_array($dependency, $previous, true)) { 
                    $toIndex++; 
                    $previous = []; 
 
                    break; 
                } 
            } 
            $sort[$toIndex][] = $job; 
            $previous[] = $job; 
            if ($this->syncJobs->find($job) !== null) { 
                $sync[$job] = $toIndex; 
            } 
        } 
 
        return $this->getSortJobs($sort, $sync); 
    } 
 
    /** 
     * @return array<string, VectorInterface<string>> 
     */ 
    private function getSortAsc(): array 
    { 
        $array = $this->map->toArray(); 
        uasort($array, function (VectorInterface $a, VectorInterface $b) { 
            return match (true) { 
                $b->contains(...$a->toArray()) => -1, 
                // @infection-ignore-all 
                $a->contains(...$b->toArray()) => 1, 
                default => 0 
            }; 
        }); 
 
        /* @phpstan-ignore-next-line */ 
        return $array; 
    } 
 
    /** 
     * @param array<int, array<int, string>> $sort 
     * @param array<string, int> $sync 
     * @return array<int, array<int, string>> 
     */ 
    private function getSortJobs(array $sort, array $sync): array 
    { 
        if (count($this->syncJobs) === 0) { 
            return $sort; 
        } 
        $aux = 0; 
        $vector = new Vector(...$sort); 
        foreach ($sync as $job => $index) { 
            $auxIndex = $index + $aux; 
            /** @var array<int, string> $array */ 
            $array = $vector->get($auxIndex); 
            $key = array_search($job, $array, true); 
            unset($array[$key]); 
            $array = array_values($array); 
            $vector = $vector 
                ->withSet($auxIndex, $array) 
                ->withInsert($auxIndex, [$job]); 
            $aux++; 
        } 
        /** @var array<int, array<int, string>> */ 
        $array = $vector->toArray(); 
 
        return array_values( 
            array_filter($array) 
        ); 
    } 
 
    /** 
     * @param VectorInterface<string> $vector 
     */ 
    private function assertNotSelfDependency(string $job, VectorInterface $vector): void 
    { 
        if (! $vector->contains($job)) { 
            return; 
        } 
 
        throw new InvalidArgumentException( 
            (string) message( 
                'Cannot declare job **%job%** as a self-dependency', 
                job: $job 
            ) 
        ); 
    } 
 
    /** 
     * @param VectorInterface<string> $vector 
     */ 
    private function handleDependencyUpdate(string $job, VectorInterface $vector): void 
    { 
        /** @var string $dependency */ 
        foreach ($vector as $dependency) { 
            /** @var VectorInterface<string> $update */ 
            $update = $this->map->get($dependency); 
            $findJob = $update->find($job); 
            if ($findJob !== null) { 
                $update = $update->withRemove($findJob); 
            } 
            $this->map = $this->map->withPut($dependency, $update); 
        } 
    } 
} 
 
 |