<?php


namespace Mainto\RpcServer\Protocol\Request;


use Illuminate\Contracts\Support\Jsonable;
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 RequestReader implements Jsonable {
    private Reader $reader;

    private int $magicNum;
    private bool $hasBody;
    private int $type;
    private bool $isStream;

    private string $traceId;
    private string $callClassName;
    private string $callMethodName;
    private bool $readBody;
    private bool $readExt = false;
    private ?Body $body = null;
    private ?Extend $ext = null;

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

    public static function ReadRequestFrom (Reader $reader): RequestReader {
        $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);
            }

            if ($this->ext instanceof QueryExtend) {
                $this->ext->readQueryFrom($this->reader);
            }
        }

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

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

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

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

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

        $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->traceId = substr($headerByte, 15, 16);

        $classLen = Binary::strToUint8($headerByte[31]);
        $this->callClassName = substr($headerByte, 32, $classLen);

        $methodLen = Binary::strToUint8($headerByte[32 + $classLen]);

        $this->callMethodName = substr($headerByte, 32 + $classLen + 1, $methodLen);

        return $size + 2;
    }

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

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

        $this->readBody = true;

        return $this->body;
    }

    public function getHeaderExtend(): ?HeaderExtend{
        if ($this->magicNum === 0) {
            return null;
        }

        if ($this->ext instanceof HeaderExtend) {
            return $this->ext;
        }

        return null;
    }

    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 string
     */
    public function getTraceId (): string {
        return $this->traceId;
    }

    /**
     * @return string
     */
    public function getCallClassName (): string {
        return $this->callClassName;
    }

    /**
     * @return string
     */
    public function getCallMethodName (): string {
        return $this->callMethodName;
    }

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

    /**
     * @return Reader
     */
    public function getReader (): Reader {
        return $this->reader;
    }

    public function toJson ($options = 0) {
        return json_encode([
            "type"           => BaseType::Map[$this->type],
            "traceId"        => $this->getTraceId(),
            "callClassName"  => $this->getCallClassName(),
            "callMethodName" => $this->getCallMethodName(),
            "body"           => optional($this->getBody())->toJson($options),
            //            "ext"           => $this->getExtend()->toJson($options),
        ], $options);
    }

    public function toRequest (): Request {
        $request = new Request();
        $request->setTraceId($this->getTraceId());
        $request->setBody($this->getBody());
        $request->setExtend($this->getExtend());
        $request->setCallClassAndMethod($this->getCallClassName(), $this->getCallMethodName());
        $request->setType($this->type);

        return $request;
    }
}