#ifndef BYTE_STREAM_WRAPPER_H
#define BYTE_STREAM_WRAPPER_H

#include <cstdint>
#include <cstdlib>
#include <string>
#include <stdexcept>
#include <memory>

class ByteBufferException : public std::runtime_error {
public:
    ByteBufferException(std::string const &what) : std::runtime_error(what) {
    }
};

class ByteBuffer {
public:
    static ByteBuffer alloc(std::size_t capacity);

    ByteBuffer(const std::shared_ptr<uint8_t> bytes, size_t capacity);

    ByteBuffer(const std::shared_ptr<uint8_t> bytes, size_t capacity, size_t size);

    ByteBuffer(const std::shared_ptr<uint8_t> bytes, size_t capacity, size_t size, size_t zero);

    inline size_t getSize() const {
        return end - zero;
    }

    inline size_t getCapacity() const {
        return capacity;
    }

    inline size_t getCursor() const {
        return ptr - zero;
    }

    inline size_t getBytesLeft() const {
        return end - ptr;
    }

    inline ByteBuffer &setCursor(size_t newCursor) {
        ptr = (uint8_t *) &zero[newCursor];
        return *this;
    }

    inline void skip(size_t length) {
        ptr += length;
    }

    ByteBuffer &write8(uint8_t value);

    ByteBuffer &write16le(uint16_t value);

    /**
    * Appends the entire buffer. Make a view if you want to write a part of it.
    */
    ByteBuffer &write(const ByteBuffer &value);

    ByteBuffer &write(const uint8_t *bytes, size_t len);

    uint8_t get8(size_t index) const;

    uint8_t read8();

    uint16_t read16le();

    void copy(uint8_t *bytes, size_t length) const;

    /**
    * Creates a view from cursor to size.
    */
    ByteBuffer view() const;

    // TODO: should return const
    ByteBuffer view(size_t length) const;

    std::string toString() const;

private:
    ByteBuffer(const std::shared_ptr<uint8_t> bytes, size_t capacity, const uint8_t *zero, const uint8_t *end);

    ByteBuffer view(uint8_t *ptr, const uint8_t *end) const;

    void checkAndUpdateEnd(size_t count);

    void assertCanAccessRelative(size_t diff) const;

    void assertCanAccessIndex(uint8_t *p) const;

    const std::shared_ptr<uint8_t> bytes;
    const size_t capacity;
    const uint8_t *zero;
    const uint8_t *end;
    uint8_t *ptr;
};

#endif