<?php


namespace Mainto\RpcServer\Protocol\Request;


use Mainto\RpcServer\Exceptions\RpcRuntimeException;
use Mainto\RpcServer\Protocol\Common\BaseType;
use Mainto\RpcServer\Protocol\Common\Body;
use Mainto\RpcServer\Protocol\Common\Types\BodyType;
use Mainto\RpcServer\Protocol\Common\Version;
use Mainto\RpcServer\Protocol\Request\Extend\RequestExtendHeader;
use Mainto\RpcServer\Protocol\Request\Extend\RequestExtendUrlInvoke;
use Mainto\RpcServer\Util\Binary;
use Mainto\RpcServer\Util\IO\IO;
use Mainto\RpcServer\Util\IO\Writer;
use Mainto\RpcServer\Util\IO\WriterTo;

class Request implements WriterTo {
    private string $traceId = "";
    private string $callClassName = "";
    private string $callMethodName = "";
    private int $type = 0;
    private ?Extend $ext = null;
    private ?Body $body = null;
    private bool $isStream = false;

    /**
     * @param string $traceId
     */
    public function setTraceId (string $traceId): void {
        $this->traceId = $traceId;
    }

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

    private function encodeHeaderLen () {
        if (strlen($this->callClassName) > 255 || strlen($this->callMethodName) > 255) {
            throw new RpcRuntimeException("header too large.");
        }

        $size = 0;
        $size += 2;  // head size
        $size += 2;  // version
        $size += 2;  // extend magic number
        $size += 1;  // has body
        $size += 8;  // 预留
        $size += 2;  // stream | empty  | main type | second type
        $size += 16; // request trace id

        $size += 1; // class size
        $size += strlen($this->callClassName);
        $size += 1; // method size
        $size += strlen($this->callMethodName);

        // uint16
        return $size;
    }

    private function encodeHeader () {
        $size = $this->encodeHeaderLen();
        $buf = Binary::makeBytes($size);
        Binary::$littleEndian::putUint16($buf, 0, $size);

        $buf[2] = Version::ProtocolVersion[0];
        $buf[3] = Version::ProtocolVersion[1];
        if ($this->ext !== null) {
            Binary::$littleEndian::putUint16($buf, 4, $this->ext->magicNum());
        }

        if ($this->body !== null) {
            $buf[6] = "\x01";
        }

        // type

        if ($this->isStream) {
            $this->type = $this->type | BaseType::StreamOpen | $this->type;
        }

        Binary::$littleEndian::putUint16($buf, 15, $this->type);

        Binary::copy($buf, 17, 33, $this->traceId);

        $classLen = strlen($this->callClassName);
        Binary::putUint8($buf, 33, $classLen);
        Binary::copy($buf, 34, 34 + $classLen, $this->callClassName);

        $methodLen = strlen($this->callMethodName);
        Binary::putUint8($buf, 34 + $classLen, $methodLen);
        Binary::copy($buf, 34 + $classLen + 1, 34 + $classLen + 1 + $methodLen, $this->callMethodName);

        return $buf;
    }

    public function encode () {
        return IO::IOWriterToBytes($this);
    }

    public function writeTo (Writer $w): int {
        $b = $this->encodeHeader();
        $totalWrite = $w->write($b);

        if ($this->ext !== null) {
            if ($this->ext instanceof HeaderExtend) {
                $totalWrite += $this->ext->writeHeaderTo($w);
            }

            if ($this->ext instanceof QueryExtend) {
                $totalWrite += $this->ext->writeQueryTo($w);
            }
        }

        if ($this->body !== null) {
            $totalWrite += $this->body->writeTo($w);
        }

        if ($this->ext !== null && $this->ext instanceof BodyExtend) {
            $totalWrite += $this->ext->writeBodyTo($w);
        }

        return $totalWrite;
    }

    /**
     * @param string $class
     * @param string $method
     */
    public function setCallClassAndMethod (string $class, string $method): void {
        $this->callClassName = $class;
        $this->callMethodName = $method;
    }

    public function setTypeWithExtend (int $getType, ?Extend $extent) {
        $this->type = $getType;
        $this->ext = $extent;
    }

    public function setExtend (?Extend $extent) {
        $this->ext = $extent;
    }

    public function setBody (?Body $body) {
        $this->body = $body;
    }

    /**
     * @return Body|null
     */
    public function getBody (): ?Body {
        return $this->body;
    }

    /**
     * @return bool
     */
    public function hasBody (): bool {
        return $this->body !== null;
    }

    public function checkParams () {
        if (strlen($this->callClassName) > 255 || strlen($this->callMethodName) > 255) {
            throw new RpcRuntimeException("header too large.");
        }

        if ($this->body !== null) {
            $this->body->checkParams();
        }

        if ($this->ext !== null) {
            $this->ext->checkParams();
        }
    }

    public function setType (int $type) {
        $this->type = $type;
    }

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

    public function toArray (): array {
        $ret = [
            'header' => [
                "type"         => $this->type,
                "type string"  => BaseType::Map[$this->type],
                "version"      => bin2hex(Version::ProtocolVersion),
                "magic number" => optional($this->ext)->magicNum(),
                "is stream"    => var_export($this->isStream, true),
                "trance id"    => $this->traceId,
                "call"         => "{$this->callClassName}::{$this->callMethodName}",
            ],
        ];

        if ($this->hasBody()) $ret['body'] = $this->getBody()->toArray();
        if ($this->hasExt()) $ret['ext'] = $this->getExt()->toArray();

        return $ret;
    }

    public function dump (): string {
        return json_encode($this->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    }

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

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

    /**
     * @return Extend|null
     */
    public function getExt (): ?Extend {
        return $this->ext;
    }

    /**
     * @return bool
     */
    public function hasExt (): bool {
        return $this->ext !== null;
    }

    public function getAllParams (): array {
        $params = [];

        if ($this->hasExt()) {
            $ext = $this->getExt();
            if ($ext instanceof RequestExtendUrlInvoke) {
                foreach ($ext->getAllQuery() as $key => $value) {
                    $params[$key] = count($value) === 1 ? $value[0] : $value;
                }
            }
        }

        if ($this->hasBody()) {
            if ($this->getBody()->getBodyType() == BodyType::RAW) {
                // raw mode use text as key
                $params += ['text' => $this->getBody()->getContent()];
            } else {
                $params += (array)$this->getBody()->getContent();
            }
        }

        return $params;
    }

    public function getAllHeaders (): array {
        $headers = [];
        if ($this->hasExt()) {
            $ext = $this->getExt();
            if ($ext instanceof RequestExtendHeader) {
                foreach ($ext->getAllHeader() as $key => $value) {
                    $headers[$key] = count($value) === 1 ? $value[0] : $value;
                }
            }
        }

        return $headers;
    }
}