<?php

declare(strict_types=1);

namespace Psl\Network\Internal;

use Generator;
use Override;
use Psl\Channel;
use Psl\Network;
use Psl\Network\StreamServerInterface;
use Revolt\EventLoop;

use function error_get_last;
use function fclose;
use function is_resource;
use function stream_socket_accept;

abstract class AbstractStreamServer implements StreamServerInterface
{
    private const int DEFAULT_IDLE_CONNECTIONS = 256;

    /**
     * @var closed-resource|resource|null $impl
     */
    private mixed $impl;

    /**
     * @var string
     */
    private string $watcher;

    /**
     * @var Channel\ReceiverInterface<array{true, Socket}|array{false, Network\Exception\RuntimeException}>
     */
    private Channel\ReceiverInterface $receiver;

    /**
     * @param resource $impl
     * @param int<1, max> $idleConnections
     */
    protected function __construct(mixed $impl, int $idleConnections = self::DEFAULT_IDLE_CONNECTIONS)
    {
        $this->impl = $impl;

        /**
         * @var Channel\ReceiverInterface<array{true, Socket}|array{false, Network\Exception\RuntimeException}> $receiver
         * @var Channel\SenderInterface<array{true, Socket}|array{false, Network\Exception\RuntimeException}> $sender
         */
        [$receiver, $sender] = Channel\bounded($idleConnections);

        $this->receiver = $receiver;
        $this->watcher = EventLoop::onReadable(
            $impl,
            /**
             * @param resource $resource
             */
            static function (string $watcher, mixed $resource) use ($sender): void {
                try {
                    $sock = @stream_socket_accept($resource, timeout: 0.0);
                    if (false !== $sock) {
                        $sender->send([true, new Socket($sock)]);

                        return;
                    }

                    // @codeCoverageIgnoreStart
                    /** @var array{file: string, line: int, message: string, type: int} $err */
                    $err = error_get_last();
                    $sender->send([
                        false,
                        new Network\Exception\RuntimeException(
                            'Failed to accept incoming connection: ' . $err['message'],
                            $err['type'],
                        ),
                    ]);
                    // @codeCoverageIgnoreEnd
                } catch (Channel\Exception\ClosedChannelException) {
                    EventLoop::cancel($watcher);

                    return;
                }
            },
        );
    }

    /**
     * {@inheritDoc}
     */
    #[Override]
    public function nextConnection(): Network\StreamSocketInterface
    {
        try {
            [$success, $result] = $this->receiver->receive();
        } catch (Channel\Exception\ClosedChannelException) {
            throw new Network\Exception\AlreadyStoppedException('Server socket has already been stopped.');
        }

        if ($success) {
            /** @var Socket $result */
            return $result;
        }

        /** @var Network\Exception\RuntimeException $result */
        throw $result;
    }

    /**
     * @return Generator<null, Network\StreamSocketInterface, void, null>
     */
    #[Override]
    public function incoming(): Generator
    {
        try {
            while (true) {
                [$success, $result] = $this->receiver->receive();
                if ($success) {
                    /** @var Socket $result */
                    yield null => $result;
                    continue;
                }

                /** @var Network\Exception\RuntimeException $result */
                throw $result;
            }
        } catch (Channel\Exception\ClosedChannelException) {
            return;
        }
    }

    /**
     * {@inheritDoc}
     */
    #[\Override]
    public function getLocalAddress(): Network\Address
    {
        if (!is_resource($this->impl)) {
            throw new Network\Exception\AlreadyStoppedException('Server socket has already been stopped.');
        }

        return Network\Internal\get_sock_name($this->impl);
    }

    public function __destruct()
    {
        $this->close();
    }

    /**
     * {@inheritDoc}
     */
    #[\Override]
    public function close(): void
    {
        EventLoop::disable($this->watcher);
        if (null === $this->impl) {
            return;
        }

        $this->receiver->close();
        $resource = $this->impl;
        $this->impl = null;
        if (is_resource($resource)) {
            fclose($resource);
        }
    }

    /**
     * {@inheritDoc}
     */
    #[\Override]
    public function getStream(): mixed
    {
        /** @var resource */
        return $this->impl;
    }
}
