<?php

declare(strict_types=1);

namespace danog\MadelineProto\Stream\Ogg;

use Amp\Emitter;
use danog\MadelineProto\Exception;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Stream\BufferedStreamInterface;
use danog\MadelineProto\Stream\BufferInterface;
use RuntimeException;

use function count;

/**
 * Async OGG stream reader.
 *
 * @author Charles-Édouard Coste <contact@ccoste.fr>
 * @author Daniil Gentili <daniil@daniil.it>
 */
final class Ogg
{
    private const CAPTURE_PATTERN = "\x4f\x67\x67\x53"; // ASCII encoded "OggS" string
    private const BOS = 2;
    private const EOS = 4;

    const STATE_READ_HEADER = 0;
    const STATE_READ_COMMENT = 1;
    const STATE_STREAMING = 3;
    const STATE_END = 4;

    /**
     * Required frame duration in microseconds.
     */
    private int $frameDuration = 60000;
    /**
     * Current total frame duration in microseconds.
     */
    private int $currentDuration = 0;

    /**
     * Current OPUS payload.
     */
    private string $opusPayload = '';

    /**
     * OGG Stream count.
     */
    private int $streamCount;

    /**
     * Buffered stream interface.
     */
    private BufferInterface $stream;

    /**
     * Pack format.
     */
    private string $packFormat;

    /**
     * OPUS packet emitter.
     * @param iterable<string>
     */
    private iterable $emitter;

    private function __construct()
    {
    }
    /**
     * Constructor.
     *
     * @param BufferedStreamInterface $stream        The stream
     * @param int                     $frameDuration Required frame duration, microseconds
     */
    public static function init(BufferedStreamInterface $stream, int $frameDuration): self
    {
        $self = new self;
        $self->frameDuration = $frameDuration;
        $self->stream = $stream->getReadBuffer($l);
        $self->emitter = $self->read();
        $pack_format = [
            'stream_structure_version' => 'C',
            'header_type_flag'         => 'C',
            'granule_position'         => 'P',
            'bitstream_serial_number'  => 'V',
            'page_sequence_number'     => 'V',
            'CRC_checksum'             => 'V',
            'number_page_segments'     => 'C',
        ];

        $self->packFormat = \implode(
            '/',
            \array_map(
                fn (string $v, string $k): string => $v.$k,
                $pack_format,
                \array_keys($pack_format),
            ),
        );

        return $self;
    }

    /**
     * Read OPUS length.
     */
    private function readLen(string $content, int &$offset): int
    {
        $len = \ord($content[$offset++]);
        if ($len > 251) {
            $len += \ord($content[$offset++]) << 2;
        }
        return $len;
    }
    /**
     * OPUS state machine.
     *
     * @psalm-suppress InvalidArrayOffset
     */
    private function opusStateMachine(string $content): \Generator
    {
        $curStream = 0;
        $offset = 0;
        $len = \strlen($content);
        while ($offset < $len) {
            $selfDelimited = $curStream++ < $this->streamCount - 1;
            $sizes = [];

            $preOffset = $offset;

            $toc = \ord($content[$offset++]);
            $stereo = $toc & 4;
            $conf = $toc >> 3;
            $c = $toc & 3;

            if ($conf < 12) {
                $frameDuration = $conf % 4;
                if ($frameDuration === 0) {
                    $frameDuration = 10000;
                } else {
                    $frameDuration *= 20000;
                }
            } elseif ($conf < 16) {
                $frameDuration = 2**($conf % 2) * 10000;
            } else {
                $frameDuration = 2**($conf % 4) * 2500;
            }

            $paddingLen = 0;
            if ($c === 0) {
                // Exactly 1 frame
                $sizes []= $selfDelimited
                    ? $this->readLen($content, $offset)
                    : $len - $offset;
            } elseif ($c === 1) {
                // Exactly 2 frames, equal size
                $size = $selfDelimited
                    ? $this->readLen($content, $offset)
                    : ($len - $offset)/2;
                $sizes []= $size;
                $sizes []= $size;
            } elseif ($c === 2) {
                // Exactly 2 frames, different size
                $size = $this->readLen($content, $offset);
                $sizes []= $size;
                $sizes []= $selfDelimited
                    ? $this->readLen($content, $offset)
                    : $len - ($offset + $size);
            } else {
                // Arbitrary number of frames
                $ch = \ord($content[$offset++]);
                $len--;
                $count = $ch & 0x3F;
                $vbr = $ch & 0x80;
                $padding = $ch & 0x40;
                if ($padding) {
                    $paddingLen = $padding = \ord($content[$offset++]);
                    while ($padding === 255) {
                        $padding = \ord($content[$offset++]);
                        $paddingLen += $padding - 1;
                    }
                }
                if ($vbr) {
                    if (!$selfDelimited) {
                        $count -= 1;
                    }
                    for ($x = 0; $x < $count; $x++) {
                        $sizes[]= $this->readLen($content, $offset);
                    }
                    if (!$selfDelimited) {
                        $sizes []= ($len - ($offset + $padding));
                    }
                } else { // CBR
                    $size = $selfDelimited
                        ? $this->readLen($content, $offset)
                        : ($len - ($offset + $padding)) / $count;
                    \array_push($sizes, ...\array_fill(0, $count, $size));
                }
            }

            $totalDuration = \count($sizes) * $frameDuration;
            if (!$selfDelimited && $totalDuration + $this->currentDuration <= $this->frameDuration) {
                $this->currentDuration += $totalDuration;
                $sum = \array_sum($sizes);
                /** @psalm-suppress InvalidArgument */
                $this->opusPayload .= \substr($content, $preOffset, ($offset - $preOffset) + $sum + $paddingLen);
                if ($this->currentDuration === $this->frameDuration) {
                    yield $this->opusPayload;
                    $this->opusPayload = '';
                    $this->currentDuration = 0;
                }
                $offset += $sum;
                $offset += $paddingLen;
                continue;
            }

            foreach ($sizes as $size) {
                $this->opusPayload .= \chr($toc & ~3);
                $this->opusPayload .= \substr($content, $offset, $size);
                $offset += $size;
                $this->currentDuration += $frameDuration;
                if ($this->currentDuration >= $this->frameDuration) {
                    if ($this->currentDuration > $this->frameDuration) {
                        Logger::log("Emitting packet with duration {$this->currentDuration} but need {$this->frameDuration}, please reconvert the OGG file with a proper frame size.", Logger::WARNING);
                    }
                    yield $this->opusPayload;
                    $this->opusPayload = '';
                    $this->currentDuration = 0;
                }
            }
            $offset += $paddingLen;
        }
    }

    /**
     * Read frames.
     *
     * @return iterable<string>
     */
    private function read(): iterable
    {
        $state = self::STATE_READ_HEADER;
        $content = '';

        while (true) {
            $init = $this->stream->bufferRead(4+23);
            if (empty($init)) {
                return false; // EOF
            }
            if (\substr($init, 0, 4) !== self::CAPTURE_PATTERN) {
                throw new Exception('Bad capture pattern');
            }

            /*$headers = \unpack(
                $this->packFormat,
                \substr($init, 4)
            );

            if ($headers['stream_structure_version'] != 0x00) {
                throw new Exception("Bad stream version");
            }
            $granule_diff = $headers['granule_position'] - $granule;
            $granule = $headers['granule_position'];

            $continuation = (bool) ($headers['header_type_flag'] & 0x01);
            $firstPage = (bool) ($headers['header_type_flag'] & 0x02);
            $lastPage = (bool) ($headers['header_type_flag'] & 0x04);
            */

            $segments = \unpack(
                'C*',
                $this->stream->bufferRead(\ord($init[26])),
            );

            //$serial = $headers['bitstream_serial_number'];
            /*if ($headers['header_type_flag'] & Ogg::BOS) {
                $this->emit('ogg:stream:start', [$serial]);
            } elseif ($headers['header_type_flag'] & Ogg::EOS) {
                $this->emit('ogg:stream:end', [$serial]);
            } else {
                $this->emit('ogg:stream:continue', [$serial]);
            }*/
            $sizeAccumulated = 0;
            foreach ($segments as $segment_size) {
                $sizeAccumulated += $segment_size;
                if ($segment_size < 255) {
                    $content .= $this->stream->bufferRead($sizeAccumulated);
                    if ($state === self::STATE_STREAMING) {
                        yield from $this->opusStateMachine($content);
                    } elseif ($state === self::STATE_READ_HEADER) {
                        if (\substr($content, 0, 8) !== 'OpusHead') {
                            throw new RuntimeException('This is not an OPUS stream!');
                        }
                        $opus_head = \unpack('Cversion/Cchannel_count/vpre_skip/Vsample_rate/voutput_gain/Cchannel_mapping_family/', \substr($content, 8));
                        if ($opus_head['channel_mapping_family']) {
                            $opus_head['channel_mapping'] = \unpack('Cstream_count/Ccoupled_count/C*channel_mapping', \substr($content, 19));
                        } else {
                            $opus_head['channel_mapping'] = [
                                'stream_count' => 1,
                                'coupled_count' => $opus_head['channel_count'] - 1,
                                'channel_mapping' => [0],
                            ];
                            if ($opus_head['channel_count'] === 2) {
                                $opus_head['channel_mapping']['channel_mapping'][] = 1;
                            }
                        }
                        $this->streamCount = $opus_head['channel_mapping']['stream_count'];
                        $state = self::STATE_READ_COMMENT;
                    } elseif ($state === self::STATE_READ_COMMENT) {
                        $vendor_string_length = \unpack('V', \substr($content, 8, 4))[1];
                        $result = [];
                        $result['vendor_string'] = \substr($content, 12, $vendor_string_length);
                        $comment_count = \unpack('V', \substr($content, 12+$vendor_string_length, 4))[1];
                        $offset = 16+$vendor_string_length;
                        for ($x = 0; $x < $comment_count; $x++) {
                            $length = \unpack('V', \substr($content, $offset, 4))[1];
                            $result['comments'][$x] = \substr($content, $offset += 4, $length);
                            $offset += $length;
                        }
                        $state = self::STATE_STREAMING;
                    }
                    $content = '';
                    $sizeAccumulated = 0;
                }
            }
        }
    }

    /**
     * Get OPUS packet emitter.
     *
     * @return iterable<string>
     */
    public function getEmitter(): iterable
    {
        return $this->emitter;
    }
}
