<?php declare(strict_types=1);

namespace GraphQL\Executor\Promise\Adapter;

use Amp\DeferredFuture;
use Amp\Future;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;

use function Amp\async;
use function Amp\Future\await;

/**
 * Allows integration with amphp/amp v3 (fiber-based futures).
 *
 * @see https://amphp.org/amp
 */
class AmpFutureAdapter implements PromiseAdapter
{
    public function isThenable($value): bool
    {
        return $value instanceof Future;
    }

    /** @throws InvariantViolation */
    public function convertThenable($thenable): Promise
    {
        return new Promise($thenable, $this);
    }

    /** @throws InvariantViolation */
    public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
    {
        $future = $promise->adoptedPromise;
        assert($future instanceof Future);

        $next = async(static function () use ($future, $onFulfilled, $onRejected) {
            try {
                $value = $future->await();
            } catch (\Throwable $reason) {
                if ($onRejected === null) {
                    throw $reason;
                }

                return static::unwrapResult($onRejected($reason));
            }

            if ($onFulfilled === null) {
                return $value;
            }

            return static::unwrapResult($onFulfilled($value));
        });

        return new Promise($next, $this);
    }

    /** @throws InvariantViolation */
    public function create(callable $resolver): Promise
    {
        $deferred = new DeferredFuture();

        try {
            $resolver(
                static function ($value) use ($deferred): void {
                    static::resolveDeferred($deferred, $value);
                },
                static function (\Throwable $exception) use ($deferred): void {
                    $deferred->error($exception);
                }
            );
        } catch (\Throwable $exception) {
            $deferred->error($exception);
        }

        return new Promise($deferred->getFuture(), $this);
    }

    /**
     * @throws \Error
     * @throws InvariantViolation
     */
    public function createFulfilled($value = null): Promise
    {
        if ($value instanceof Promise) {
            return $value;
        }

        if ($value instanceof Future) {
            return new Promise($value, $this);
        }

        return new Promise(Future::complete($value), $this);
    }

    /** @throws InvariantViolation */
    public function createRejected(\Throwable $reason): Promise
    {
        return new Promise(Future::error($reason), $this);
    }

    /**
     * @throws \Error
     * @throws InvariantViolation
     */
    public function all(iterable $promisesOrValues): Promise
    {
        $items = is_array($promisesOrValues)
            ? $promisesOrValues
            : iterator_to_array($promisesOrValues);

        /** @var array<Future<mixed>> $futures */
        $futures = [];

        foreach ($items as $key => $item) {
            if ($item instanceof Promise) {
                $item = $item->adoptedPromise;
            }

            if ($item instanceof Future) {
                $futures[$key] = $item;
            }
        }

        $combined = async(static function () use ($items, $futures): array {
            if ($futures === []) {
                return $items;
            }

            $resolved = await($futures);

            return array_replace($items, $resolved);
        });

        return new Promise($combined, $this);
    }

    /**
     * @param DeferredFuture<mixed> $deferred
     * @param mixed $value
     */
    protected static function resolveDeferred(DeferredFuture $deferred, $value): void
    {
        if ($value instanceof Promise) {
            $value = $value->adoptedPromise;
        }

        if ($value instanceof Future) {
            async(static function () use ($deferred, $value): void {
                try {
                    $deferred->complete($value->await());
                } catch (\Throwable $exception) {
                    $deferred->error($exception);
                }
            });

            return;
        }

        $deferred->complete($value);
    }

    /**
     * @param mixed $value
     *
     * @return mixed
     */
    protected static function unwrapResult($value)
    {
        if ($value instanceof Promise) {
            $value = $value->adoptedPromise;
        }

        if ($value instanceof Future) {
            return $value->await();
        }

        return $value;
    }
}
