<?php

declare(strict_types=1);

namespace OCA\Talk\Vendor\CuyZ\Valinor\Mapper\Object\Factory;

use OCA\Talk\Vendor\CuyZ\Valinor\Definition\ClassDefinition;
use OCA\Talk\Vendor\CuyZ\Valinor\Mapper\Object\Exception\ObjectBuildersCollision;
use OCA\Talk\Vendor\CuyZ\Valinor\Mapper\Object\ObjectBuilder;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\ScalarType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Type;
use OCA\Talk\Vendor\CuyZ\Valinor\Utility\TypeHelper;

use function array_merge;
use function count;
use function usort;

/** @internal */
final class SortingObjectBuilderFactory implements ObjectBuilderFactory
{
    public function __construct(private ObjectBuilderFactory $delegate) {}

    /**
     * Will properly sort the object builders, based on the number of arguments
     * they need, and on the types of the arguments.
     *
     * Builders with most parameters will be prioritized, and in the case of
     * parameters' name collision, the builder with the most specific types will
     * be prioritized.
     *
     * Type priority is as follows:
     * 1. Non-scalar type
     * 2. Integer type
     * 3. Float type
     * 4. String type
     * 5. Boolean type
     */
    public function for(ClassDefinition $class): array
    {
        $builders = $this->delegate->for($class);

        $sortedByArgumentsNumber = [];
        $sortedByPriority = [];

        foreach ($builders as $builder) {
            $sortedByArgumentsNumber[$builder->describeArguments()->count()][] = $builder;
        }

        krsort($sortedByArgumentsNumber);

        foreach ($sortedByArgumentsNumber as $sortedBuilders) {
            usort($sortedBuilders, $this->sortObjectBuilders(...));

            $sortedByPriority = array_merge($sortedByPriority, $sortedBuilders);
        }

        return $sortedByPriority;
    }

    private function sortObjectBuilders(ObjectBuilder $builderA, ObjectBuilder $builderB): int
    {
        $argumentsA = $builderA->describeArguments()->toArray();
        $argumentsB = $builderB->describeArguments()->toArray();

        $sharedArguments = array_keys(array_intersect_key($argumentsA, $argumentsB));

        $winner = null;

        foreach ($sharedArguments as $name) {
            $typeA = $argumentsA[$name]->type();
            $typeB = $argumentsB[$name]->type();

            $score = $this->sortTypes($typeA, $typeB);

            if ($score === 0) {
                continue;
            }

            $newWinner = $score === 1 ? $builderB : $builderA;

            if ($winner && $winner !== $newWinner) {
                throw new ObjectBuildersCollision($builderA, $builderB);
            }

            $winner = $newWinner;
        }

        if ($winner === null && count($sharedArguments) === count($argumentsA)) {
            throw new ObjectBuildersCollision($builderA, $builderB);
        }

        // @infection-ignore-all / Incrementing or decrementing sorting value makes no sense, so we ignore it.
        return $winner === $builderA ? -1 : 1;
    }

    private function sortTypes(Type $typeA, Type $typeB): int
    {
        if ($typeA instanceof ScalarType && $typeB instanceof ScalarType) {
            return TypeHelper::typePriority($typeB) <=> TypeHelper::typePriority($typeA);
        }

        if (! $typeA instanceof ScalarType) {
            // @infection-ignore-all / Decrementing sorting value makes no sense, so we ignore it.
            return -1;
        }

        return 1;
    }
}
