<?php

declare(strict_types=1);

namespace App\Framework\Async;

use Fiber;
use SplQueue;
use Throwable;

/**
 * Go-Style Channel für asynchrone Producer-Consumer Patterns
 *
 * Ermöglicht sichere Kommunikation zwischen Fibers mit automatischer
 * Blockierung bei vollen/leeren Channels.
 */
final class AsyncChannel
{
    /** @var SplQueue<mixed> */
    private SplQueue $buffer;

    /** @var array<Fiber> Wartende Producer */
    private array $waitingProducers = [];

    /** @var array<Fiber> Wartende Consumer */
    private array $waitingConsumers = [];

    private bool $closed = false;

    public function __construct(
        private readonly int $capacity = 0 // 0 = unbuffered (synchronous)
    ) {
        $this->buffer = new SplQueue();
    }

    /**
     * Sendet einen Wert in den Channel
     *
     * Blockiert, wenn der Channel voll ist (buffered) oder kein Consumer wartet (unbuffered)
     *
     * @throws ChannelClosedException|Throwable wenn Channel geschlossen ist
     */
    public function send(mixed $value): void
    {
        if ($this->closed) {
            throw new ChannelClosedException('Cannot send on closed channel');
        }

        // Buffered channel: Warte bis Platz frei
        if ($this->capacity > 0) {
            while ($this->buffer->count() >= $this->capacity && !$this->closed) {
                $this->waitingProducers[] = Fiber::getCurrent();
                Fiber::suspend();
            }

            if ($this->closed) {
                throw new ChannelClosedException('Channel was closed while waiting');
            }

            $this->buffer->enqueue($value);

            // Wecke wartende Consumer auf
            $this->resumeWaitingConsumers();
        } else {
            // Unbuffered channel: Direkter Transfer
            if (empty($this->waitingConsumers)) {
                // Kein Consumer wartet, Producer muss warten
                $this->buffer->enqueue($value);
                $this->waitingProducers[] = Fiber::getCurrent();
                Fiber::suspend();
            } else {
                // Consumer wartet, direkter Transfer
                $consumer = array_shift($this->waitingConsumers);
                $this->buffer->enqueue($value);
                if ($consumer instanceof Fiber && $consumer->isSuspended()) {
                    $consumer->resume();
                }
            }
        }
    }

    /**
     * Empfängt einen Wert vom Channel
     *
     * Blockiert wenn Channel leer ist
     *
     * @throws ChannelClosedException wenn Channel geschlossen und leer ist
     */
    public function receive(): mixed
    {
        // Buffered channel: Warte auf Daten
        if ($this->capacity > 0) {
            while ($this->buffer->isEmpty() && !$this->closed) {
                $this->waitingConsumers[] = Fiber::getCurrent();
                Fiber::suspend();
            }

            if ($this->buffer->isEmpty() && $this->closed) {
                throw new ChannelClosedException('Channel is closed and empty');
            }

            $value = $this->buffer->dequeue();

            // Wecke wartende Producer auf
            $this->resumeWaitingProducers();

            return $value;
        } else {
            // Unbuffered channel: Warte auf Producer
            while ($this->buffer->isEmpty() && !$this->closed) {
                $this->waitingConsumers[] = Fiber::getCurrent();
                Fiber::suspend();
            }

            if ($this->buffer->isEmpty() && $this->closed) {
                throw new ChannelClosedException('Channel is closed and empty');
            }

            $value = $this->buffer->dequeue();

            // Wecke wartende Producer auf
            $this->resumeWaitingProducers();

            return $value;
        }
    }

    /**
     * Versucht einen Wert zu empfangen ohne zu blockieren
     *
     * @return array{success: bool, value: mixed}
     */
    public function tryReceive(): array
    {
        if ($this->buffer->isEmpty()) {
            return ['success' => false, 'value' => null];
        }

        $value = $this->buffer->dequeue();
        $this->resumeWaitingProducers();

        return ['success' => true, 'value' => $value];
    }

    /**
     * Versucht einen Wert zu senden ohne zu blockieren
     */
    public function trySend(mixed $value): bool
    {
        if ($this->closed) {
            throw new ChannelClosedException('Cannot send on closed channel');
        }

        if ($this->capacity > 0 && $this->buffer->count() >= $this->capacity) {
            return false; // Channel voll
        }

        $this->buffer->enqueue($value);
        $this->resumeWaitingConsumers();

        return true;
    }

    /**
     * Schließt den Channel
     *
     * Weitere send() Operationen werden fehlschlagen
     * Wartende Fibers werden aufgeweckt
     */
    public function close(): void
    {
        $this->closed = true;

        // Wecke alle wartenden Fibers auf
        $this->resumeWaitingProducers();
        $this->resumeWaitingConsumers();
    }

    /**
     * Prüft ob Channel geschlossen ist
     */
    public function isClosed(): bool
    {
        return $this->closed;
    }

    /**
     * Gibt die aktuelle Anzahl der Elemente im Buffer zurück
     */
    public function count(): int
    {
        return $this->buffer->count();
    }

    /**
     * Gibt Channel-Statistiken zurück
     */
    public function getStats(): array
    {
        return [
            'capacity' => $this->capacity,
            'buffered_items' => $this->buffer->count(),
            'waiting_producers' => count($this->waitingProducers),
            'waiting_consumers' => count($this->waitingConsumers),
            'is_closed' => $this->closed,
        ];
    }

    private function resumeWaitingConsumers(): void
    {
        while (!empty($this->waitingConsumers) && !$this->buffer->isEmpty()) {
            $consumer = array_shift($this->waitingConsumers);
            if ($consumer instanceof Fiber && $consumer->isSuspended()) {
                $consumer->resume();
            }
        }
    }

    private function resumeWaitingProducers(): void
    {
        if ($this->capacity > 0) {
            // Buffered: Wecke auf wenn Platz frei
            while (!empty($this->waitingProducers) && $this->buffer->count() < $this->capacity) {
                $producer = array_shift($this->waitingProducers);
                if ($producer instanceof Fiber && $producer->isSuspended()) {
                    $producer->resume();
                }
            }
        } else {
            // Unbuffered: Wecke erste wartende Producer auf
            if (!empty($this->waitingProducers)) {
                $producer = array_shift($this->waitingProducers);
                if ($producer instanceof Fiber && $producer->isSuspended()) {
                    $producer->resume();
                }
            }
        }
    }
}
