<?php

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace Apache\Avro\Datum;

// @todo Implement JSON encoding, as is required by the Avro spec.
use Apache\Avro\Avro;
use Apache\Avro\AvroException;
use Apache\Avro\AvroGMP;
use Apache\Avro\AvroIO;

/**
 * Decodes and reads Avro data from an AvroIO object encoded using
 * Avro binary encoding.
 *
 * @package Avro
 */
class AvroIOBinaryDecoder
{
    /**
     * @var AvroIO
     */
    private $io;

    /**
     * @param AvroIO $io object from which to read.
     */
    public function __construct($io)
    {
        Avro::checkPlatform();
        $this->io = $io;
    }

    /**
     * @returns null
     */
    public function readNull()
    {
        return null;
    }

    /**
     * @returns boolean
     */
    public function readBoolean()
    {
        return (bool) (1 == ord($this->nextByte()));
    }

    /**
     * @returns string the next byte from $this->io.
     * @throws AvroException if the next byte cannot be read.
     */
    private function nextByte()
    {
        return $this->read(1);
    }

    /**
     * @param int $len count of bytes to read
     * @returns string
     */
    public function read($len)
    {
        return $this->io->read($len);
    }

    /**
     * @returns int
     */
    public function readInt()
    {
        return (int) $this->readLong();
    }

    /**
     * @returns string|int
     */
    public function readLong()
    {
        $byte = ord($this->nextByte());
        $bytes = array($byte);
        while (0 != ($byte & 0x80)) {
            $byte = ord($this->nextByte());
            $bytes [] = $byte;
        }

        if (Avro::usesGmp()) {
            return AvroGMP::decodeLongFromArray($bytes);
        }

        return self::decodeLongFromArray($bytes);
    }

    /**
     * @param int[] array of byte ascii values
     * @returns long decoded value
     * @internal Requires 64-bit platform
     */
    public static function decodeLongFromArray($bytes)
    {
        $b = array_shift($bytes);
        $n = $b & 0x7f;
        $shift = 7;
        while (0 != ($b & 0x80)) {
            $b = array_shift($bytes);
            $n |= (($b & 0x7f) << $shift);
            $shift += 7;
        }
        return ($n >> 1) ^ (($n >> 63) << 63) ^ -($n & 1);
    }

    /**
     * @returns float
     */
    public function readFloat()
    {
        return self::intBitsToFloat($this->read(4));
    }

    /**
     * Performs decoding of the binary string to a float value.
     *
     * XXX: This is <b>not</b> endian-aware! See comments in
     * {@link AvroIOBinaryEncoder::floatToIntBits()} for details.
     *
     * @param string $bits
     * @returns float
     */
    public static function intBitsToFloat($bits)
    {
        $float = unpack('g', $bits);
        return (float) $float[1];
    }

    /**
     * @returns double
     */
    public function readDouble()
    {
        return self::longBitsToDouble($this->read(8));
    }

    /**
     * Performs decoding of the binary string to a double value.
     *
     * XXX: This is <b>not</b> endian-aware! See comments in
     * {@link AvroIOBinaryEncoder::floatToIntBits()} for details.
     *
     * @param string $bits
     * @returns float
     */
    public static function longBitsToDouble($bits)
    {
        $double = unpack('e', $bits);
        return (double) $double[1];
    }

    /**
     * A string is encoded as a long followed by that many bytes
     * of UTF-8 encoded character data.
     * @returns string
     */
    public function readString()
    {
        return $this->readBytes();
    }

    /**
     * @returns string
     */
    public function readBytes()
    {
        return $this->read($this->readLong());
    }

    public function skipNull()
    {
        return null;
    }

    public function skipBoolean()
    {
        return $this->skip(1);
    }

    /**
     * @param int $len count of bytes to skip
     * @uses AvroIO::seek()
     */
    public function skip($len)
    {
        $this->seek($len, AvroIO::SEEK_CUR);
    }

    /**
     * @param int $offset
     * @param int $whence
     * @returns boolean true upon success
     * @uses AvroIO::seek()
     */
    private function seek($offset, $whence)
    {
        return $this->io->seek($offset, $whence);
    }

    public function skipInt()
    {
        return $this->skipLong();
    }

    public function skipLong()
    {
        $b = ord($this->nextByte());
        while (0 != ($b & 0x80)) {
            $b = ord($this->nextByte());
        }
    }

    public function skipFloat()
    {
        return $this->skip(4);
    }

    public function skipDouble()
    {
        return $this->skip(8);
    }

    public function skipString()
    {
        return $this->skipBytes();
    }

    public function skipBytes()
    {
        return $this->skip($this->readLong());
    }

    public function skipFixed($writers_schema, AvroIOBinaryDecoder $decoder)
    {
        $decoder->skip($writers_schema->size());
    }

    public function skipEnum($writers_schema, AvroIOBinaryDecoder $decoder)
    {
        $decoder->skipInt();
    }

    public function skipUnion($writers_schema, AvroIOBinaryDecoder $decoder)
    {
        $index = $decoder->readLong();
        AvroIODatumReader::skipData($writers_schema->schemaByIndex($index), $decoder);
    }

    public function skipRecord($writers_schema, AvroIOBinaryDecoder $decoder)
    {
        foreach ($writers_schema->fields() as $f) {
            AvroIODatumReader::skipData($f->type(), $decoder);
        }
    }

    public function skipArray($writers_schema, AvroIOBinaryDecoder $decoder)
    {
        $block_count = $decoder->readLong();
        while (0 !== $block_count) {
            if ($block_count < 0) {
                $decoder->skip($this->readLong());
            }
            for ($i = 0; $i < $block_count; $i++) {
                AvroIODatumReader::skipData($writers_schema->items(), $decoder);
            }
            $block_count = $decoder->readLong();
        }
    }

    public function skipMap($writers_schema, AvroIOBinaryDecoder $decoder)
    {
        $block_count = $decoder->readLong();
        while (0 !== $block_count) {
            if ($block_count < 0) {
                $decoder->skip($this->readLong());
            }
            for ($i = 0; $i < $block_count; $i++) {
                $decoder->skipString();
                AvroIODatumReader::skipData($writers_schema->values(), $decoder);
            }
            $block_count = $decoder->readLong();
        }
    }

    /**
     * @returns int position of pointer in AvroIO instance
     * @uses AvroIO::tell()
     */
    private function tell()
    {
        return $this->io->tell();
    }
}
