<?php


namespace Mainto\RpcServer\Protocol\Response;


use Mainto\RpcServer\Protocol\Common\BaseType;
use Mainto\RpcServer\Protocol\Common\Body;
use Mainto\RpcServer\Protocol\Common\Version;
use Mainto\RpcServer\Util\Binary;
use Mainto\RpcServer\Util\Bytes\Bytes;
use Mainto\RpcServer\Util\IO\IOUtil;
use Mainto\RpcServer\Util\IO\Reader;
use RuntimeException;

class ResponseReader {
    private Reader $reader;

    private bool $hasBody = false;
    private bool $readBody = false;
    private bool $readExt = false;

    private int $magicNum;

    private int $type;
    private bool $isStream;
    private int $runtime;

    private ?Extend $ext = null;
    private ?Body $body = null;

    private function __construct (Reader $reader) {
        $this->reader = $reader;
    }

    public static function ReadResponseFrom (Reader $reader) {
        $r = new self($reader);

        $r->init();

        return $r;
    }

    private function init () {
        $_ = $this->readerHeader();

        if ($this->magicNum !== 0) {
            $this->ext = ExtendFactory::getManagedRequestExtendByMagicNum($this->magicNum);
            if ($this->ext instanceof HeaderExtend) {
                $this->ext->readHeaderFrom($this->reader);
            }
        }

        $this->readBody = !$this->hasBody;

        if ($this->hasBody) {
            $this->body = Body::newBodyFromReader($this->reader);
        }
    }

    private function readerHeader () {
        $headerSize = $this->readFull(2);

        $size = Binary::$littleEndian::strToUint16($headerSize) - 2;

        if ($size > 542) { // max size 544 - 2
            throw new RuntimeException("ErrHeaderTooLarge");
        }

        $headerByte = $this->readFull($size);

        if (!Bytes::equal(substr($headerByte, 0, 2), Version::ProtocolVersion)) {
            throw new RuntimeException("ErrProtocolVersion");
        }

        $this->magicNum = Binary::$littleEndian::strToUint16(substr($headerByte, 2, 2));
        $this->hasBody = $headerByte[4] === "\x01";

        $this->type = Binary::$littleEndian::strToUint16(substr($headerByte, 13, 2)) & BaseType::TypeMask;

        $this->isStream = ($this->type & BaseType::TypeMask) !== "\x00";

        $this->runtime = Binary::$littleEndian::strToUint32(substr($headerByte, 15, 4));

        return $size + 2;
    }

    private function readFull ($size) {
        return IOUtil::readFullSize($this->reader, $size);
    }

    public function getBody () :?Body{
        if (!$this->hasBody) {
            return null;
        }

        $this->readBody = true;

        return $this->body;
    }

    public function getExtend (): ?Extend {
        if ($this->magicNum === 0) {
            return null;
        }
        if (!$this->readBody) {
            throw new RuntimeException("ErrMustReadBodyFirst");
        } else {
            if ($this->hasBody && !$this->body->checkLimitReadOver()) {
                throw new RuntimeException("ErrMustReadBodyFirst");
            }
        }
        if (!$this->readExt) {
            $this->readExt = true;

            if ($this->ext instanceof BodyExtend) {
                $this->ext->readBodyFrom($this->reader);
            }
        }

        return $this->ext;
    }

    /**
     * @return int
     */
    public function getType (): int {
        return $this->type;
    }

    public function toResponse(): Response {
        $response = new Response();
        $response->setType($this->type);
        $response->setRuntime($this->runtime);
        $response->setBody(optional($this->getBody())->cacheBody());
        $response->setExtend($this->getExtend());

        return $response;
    }

    /**
     * @return int
     */
    public function getRuntime (): int {
        return $this->runtime;
    }
}