<?php


namespace Mainto\RpcServer\Protocol\Common;


use Illuminate\Support\Facades\Log;
use Mainto\RpcServer\Protocol\Common\Types\BodyType;
use Mainto\RpcServer\Util\Binary;
use Mainto\RpcServer\Util\Bytes\Buffer;
use Mainto\RpcServer\Util\Bytes\Bytes;
use Mainto\RpcServer\Util\IO\IO;
use Mainto\RpcServer\Util\IO\Reader;
use Mainto\RpcServer\Util\IO\ReaderFrom;
use Mainto\RpcServer\Util\IO\Writer;
use Mainto\RpcServer\Util\IO\WriterTo;
use Mainto\RpcServer\Util\Reader\LimitReader;
use RuntimeException;

class Body implements WriterTo, ReaderFrom {
    /**
     * uint8
     * @var int
     */
    private int $bodyType;

    /**
     * uint32
     * @var string
     */
    private string $bodySize;


    private string $body = "";

    private Buffer $bodyReader;

    /**
     * Body constructor.
     */
    private function __construct () {
    }

    public static function newBodyFromReader (Reader $reader): Body {
        $object = new self();
        $object->readFrom($reader);

        return $object;
    }

    public function readFrom (Reader $reader): int {
        $read = $reader->read(5);
        $this->bodyType = Binary::strToUint8($read[0]);
        $this->bodySize = Binary::$littleEndian::strToUint32(substr($read, 1, 4));

        $buffer = new Buffer();
        IO::copy($buffer, IO::limitReader($reader, $this->bodySize));

        $this->bodyReader = $buffer;

        return 5;
    }

    public static function newRawBody (string $body): Body {
        return self::newBody(BodyType::RAW, $body);
    }

    public static function newJsonBody (string $body): Body {
        return self::newBody(BodyType::JSON, $body);
    }

    public static function newBody (int $bodyType, string $body) {
        $object = new self();
        $object->bodyType = $bodyType;
        $object->bodySize = strlen($body);
        $object->body = $body;

        $buffer = new Buffer();
        $buffer->writeString($body);

        $object->bodyReader = $buffer;

        return $object;
    }

    public function setBodyBytes (string $body) {
        $this->bodySize = strlen($body);
        $this->body = $body;

        $this->bodyReader->reset();
        $this->bodyReader->writeString($body);
    }

    public function reset () {
        $this->bodySize = 0;
        $this->body = "";
        $this->bodyReader->reset();
    }

    public function cacheBody () {
        $buffer = Bytes::newBuffer();

        IO::copy($buffer, $this->getBodyReader());

        $this->bodyReader = $buffer;

        return $this;
    }

    public function getBodyReader () {
        return IO::limitReader($this->bodyReader, $this->bodySize);
    }

    public function getBodyBytes (bool $cache = false): string {
        $buffer = Bytes::newBuffer();

        IO::copy($buffer, $this->getBodyReader());

        if ($cache) {
            $this->bodyReader = $buffer;
        }

        return $buffer->bytes();
    }

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

    public function getContent () {
        $bodyBytes = $this->getBodyBytes(true);
        switch ($this->bodyType) {
            case BodyType::JSON:
                if ($bodyBytes === 'false') {
                    return false;
                }

                $decode = json_decode($bodyBytes, true);
                if ($decode === false) {
                    Log::error("body decode fail");
                }

                return $decode;
            case BodyType::RAW:
                return $bodyBytes;
            default:
                throw new RuntimeException("body type not support");
        }
    }

    public function toRaw (): string {
        switch ($this->bodyType) {
            case BodyType::RAW:
                return $this->getBodyBytes(true);
            case BodyType::JSON:
            default:
                throw new RuntimeException("body type not support");
        }
    }

    private function encodeSize (): string {
        $makeBytes = Binary::makeBytes(5);
        $makeBytes[0] = Binary::intToUint8($this->bodyType);

        Binary::$littleEndian::putUint32($makeBytes, 1, $this->bodySize);

        return $makeBytes;
    }

    public function writeTo (Writer $w): int {
        return $w->write($this->encodeSize()) + IO::copy($w, $this->getBodyReader());
    }

    public function checkParams () {
        if ($this->bodySize > 10 * 1024 * 1024) {
            throw new RuntimeException("body package is too large");
        }
    }

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

    public function checkLimitReadOver (): bool {
        if ($this->bodyReader instanceof LimitReader) {
            return $this->bodyReader->n <= 0;
        }

        return true;
    }

    public function toArray (): array {
        $ret = [
            'type'        => $this->bodyType,
            'type string' => BodyType::Map[$this->bodyType],
            'size'        => $this->bodySize,
        ];

        if ($this->bodyType == BodyType::JSON || $this->bodyType == BodyType::RAW) {
            $ret['body'] = $this->getBodyBytes(true);
        }

        return $ret;
    }

    public function dumpToWriter (Writer $writer) {
        return json_encode($this->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    }
}