#ifndef MCLIB_COMMON_DATA_BUFFER_H_
#define MCLIB_COMMON_DATA_BUFFER_H_

#include <vector>
#include <algorithm>
#include <cstring>
#include <cassert>
#include <string>

namespace td {

class DataBuffer {
private:
    typedef std::vector<std::uint8_t> Data;
    Data m_Buffer;
    std::size_t m_ReadOffset = 0;

public:
    typedef Data::iterator iterator;
    typedef Data::const_iterator const_iterator;
    typedef Data::reference reference;
    typedef Data::const_reference const_reference;

    DataBuffer();
    DataBuffer(const DataBuffer& other);
    DataBuffer(const DataBuffer& other, std::size_t offset);
    DataBuffer(DataBuffer&& other);
    DataBuffer(const std::string& str);

    DataBuffer& operator=(const DataBuffer& other);
    DataBuffer& operator=(DataBuffer&& other);

    template <typename T>
    void Append(T data) {
        std::size_t size = sizeof(data);
        std::size_t end_pos = m_Buffer.size();
        m_Buffer.resize(m_Buffer.size() + size);
        memcpy(&m_Buffer[end_pos], &data, size);
    }

    template <typename T>
    DataBuffer& operator<<(T data) {
        // Switch to big endian
        //std::reverse((std::uint8_t*)&data, (std::uint8_t*)&data + sizeof(T));
        Append(data);
        return *this;
    }

    DataBuffer& operator<<(std::string str) {
        m_Buffer.insert(m_Buffer.end(), str.begin(), str.end());
        return *this;
    }

    DataBuffer& operator<<(DataBuffer& data) {
        m_Buffer.insert(m_Buffer.end(), data.begin(), data.end());
        return *this;
    }

    DataBuffer& operator<<(const DataBuffer& data) {
        m_Buffer.insert(m_Buffer.end(), data.begin(), data.end());
        return *this;
    }

    template <typename T>
    DataBuffer& operator>>(T& data) {
        assert(m_ReadOffset + sizeof(T) <= GetSize());
        data = *(T*)&m_Buffer[m_ReadOffset];
        //std::reverse((std::uint8_t*)&data, (std::uint8_t*)&data + sizeof(T));
        m_ReadOffset += sizeof(T);
        return *this;
    }

    DataBuffer& operator>>(DataBuffer& data) {
        data.Resize(GetSize() - m_ReadOffset);
        std::copy(m_Buffer.begin() + m_ReadOffset, m_Buffer.end(), data.begin());
        m_ReadOffset = m_Buffer.size();
        return *this;
    }

    DataBuffer& operator>>(std::string& str) {
        std::size_t stringSize = strlen((const char*)m_Buffer.data() + m_ReadOffset) + 1; // including null character
        str.resize(stringSize);
        std::copy(m_Buffer.begin() + m_ReadOffset, m_Buffer.begin() + m_ReadOffset + stringSize, str.begin());
        m_ReadOffset += stringSize;
        return *this;
    }

    void ReadSome(char* buffer, std::size_t amount) {
        assert(m_ReadOffset + amount <= GetSize());
        std::copy_n(m_Buffer.begin() + m_ReadOffset, amount, buffer);
        m_ReadOffset += amount;
    }

    void ReadSome(std::uint8_t* buffer, std::size_t amount) {
        assert(m_ReadOffset + amount <= GetSize());
        std::copy_n(m_Buffer.begin() + m_ReadOffset, amount, buffer);
        m_ReadOffset += amount;
    }

    void ReadSome(DataBuffer& buffer, std::size_t amount) {
        assert(m_ReadOffset + amount <= GetSize());
        buffer.Resize(amount);
        std::copy_n(m_Buffer.begin() + m_ReadOffset, amount, buffer.begin());
        m_ReadOffset += amount;
    }

    void ReadSome(std::string& buffer, std::size_t amount) {
        assert(m_ReadOffset + amount <= GetSize());
        buffer.resize(amount);
        std::copy_n(m_Buffer.begin() + m_ReadOffset, amount, buffer.begin());
        m_ReadOffset += amount;
    }

    void Resize(std::size_t size) {
        m_Buffer.resize(size);
    }

    void Reserve(std::size_t amount) {
        m_Buffer.reserve(amount);
    }

    void erase(iterator it) {
        m_Buffer.erase(it);
    }

    void Clear() {
        m_Buffer.clear();
        m_ReadOffset = 0;
    }

    bool IsFinished() const {
        return m_ReadOffset >= m_Buffer.size();
    }

    std::uint8_t* data() {
        return m_Buffer.data();
    }

    const std::uint8_t* data() const {
        return m_Buffer.data();
    }

    std::size_t GetReadOffset() const { return m_ReadOffset; }
    void  SetReadOffset(std::size_t pos);

    std::string  ToString() const;
    std::size_t  GetSize() const;
    bool  IsEmpty() const;
    std::size_t  GetRemaining() const;

    iterator  begin();
    iterator  end();
    const_iterator  begin() const;
    const_iterator  end() const;

    reference operator[](Data::size_type i) { return m_Buffer[i]; }
    const_reference operator[](Data::size_type i) const { return m_Buffer[i]; }

    bool ReadFile(const std::string& fileName);
    bool WriteFile(const std::string& fileName);
};

std::ostream& operator<<(std::ostream& os, const DataBuffer& buffer);

} // ns td

#endif
