#pragma once

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

namespace blitz {

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

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

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

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

	template <typename T>
	void Append(const 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<<(const T& data) {
		Append(data);
		return *this;
	}

	DataBuffer& operator<<(const std::string& str) {
		// including null character
		std::size_t strlen = str.length() + 1;
		Resize(GetSize() + strlen);
		std::memcpy(m_Buffer.data() + GetSize() - strlen, str.data(), strlen);
		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 = *(reinterpret_cast<T*>(&m_Buffer[m_ReadOffset]));
		m_ReadOffset += sizeof(T);
		return *this;
	}

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

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

	void WriteSome(const char* buffer, std::size_t amount) {
		std::size_t end_pos = m_Buffer.size();
		m_Buffer.resize(m_Buffer.size() + amount);
		memcpy(m_Buffer.data() + end_pos, buffer, amount);
	}

	void WriteSome(const std::uint8_t* buffer, std::size_t amount) {
		std::size_t end_pos = m_Buffer.size();
		m_Buffer.resize(m_Buffer.size() + amount);
		memcpy(m_Buffer.data() + end_pos, buffer, amount);
	}

	void ReadSome(char* buffer, std::size_t amount) {
		assert(m_ReadOffset + amount <= GetSize());
		std::copy_n(m_Buffer.begin() + static_cast<difference_type>(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() + static_cast<difference_type>(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() + static_cast<difference_type>(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() + static_cast<difference_type>(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::size_t GetSize() const;
	std::string ToString() const;
	std::size_t GetRemaining() const;

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

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

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

}  // namespace blitz
