#pragma once

/**
 * \file DataBuffer.h
 * \brief File containing the blitz::DataBuffer class
 */

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

/**
 * \class DataBuffer
 * \brief Class used to manipulate memory
 */
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(std::size_t initalCapacity);
	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);

	/**
	 * \brief Append data to the buffer
	 */
	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);
		std::memcpy(&m_Buffer[end_pos], &data, size);
	}

	/**
	 * \brief Append data to the buffer
	 */
	template <typename T>
	DataBuffer& operator<<(const T& data) {
		Append(data);
		return *this;
	}

	/**
	 * \brief Append a string to the buffer
	 * \warning Don't use it for binary data !
	 * \param str The string to append
	 */
	DataBuffer& operator<<(const std::string& str) {
		std::size_t strlen = str.length() + 1;	// including null character
		Resize(GetSize() + strlen);
		std::memcpy(m_Buffer.data() + GetSize() - strlen, str.data(), strlen);
		return *this;
	}

	/**
	 * \brief Append data to the buffer from another const buffer
	 * \param data The buffer to append
	 */
	DataBuffer& operator<<(const DataBuffer& data) {
		m_Buffer.insert(m_Buffer.end(), data.begin(), data.end());
		return *this;
	}

	/**
	 * \brief Read some data from the buffer and assign to desired variable
	 */
	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;
	}

	/**
	 * \brief Read some data from the buffer and assign to the new buffer
	 * \param data The buffer to assign
	 */
	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;
	}

	/**
	 * \brief Read a string from the buffer
	 * \param str The string to assign in the new buffer
	 * \warning Don't use it for binary data !
	 */
	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;
	}

	/**
	 * \brief Write some data to the buffer
	 * \param buffer The buffer to write
	 * \param amount The amount of data to write
	 */
	void WriteSome(const char* buffer, std::size_t amount) {
		std::size_t end_pos = m_Buffer.size();
		m_Buffer.resize(m_Buffer.size() + amount);
		std::memcpy(m_Buffer.data() + end_pos, buffer, amount);
	}

	/**
	 * \brief Write some data to the buffer
	 * \param buffer The buffer to write
	 * \param amount The amount of data to write
	 */
	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);
		std::memcpy(m_Buffer.data() + end_pos, buffer, amount);
	}

	/**
	 * \brief Read some data from the buffer
	 * \param buffer The buffer to Read
	 * \param amount The amount of data from the buffer
	 */
	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;
	}

	/**
	 * \brief Read some data from the buffer
	 * \param buffer The buffer to Read
	 * \param amount The amount of data from the buffer
	 */
	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;
	}

	/**
	 * \brief Read some data from the buffer
	 * \param buffer The buffer to Read
	 * \param amount The amount of data from the buffer
	 */
	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;
	}

	/**
	 * \brief Resize the buffer
	 * \param size The new size of the buffer
	 */
	void Resize(std::size_t size) {
		m_Buffer.resize(size);
	}

	/**
	 * \brief Reserve some space in the buffer
	 * \param amount The amount of space to reserve
	 */
	void Reserve(std::size_t amount) {
		m_Buffer.reserve(amount);
	}


	/**
	 * \brief Clear the buffer
	 */
	void Clear() {
		m_Buffer.clear();
		m_ReadOffset = 0;
	}

	/**
	 * \brief When the buffer has been read entirely
	 */
	bool IsFinished() const {
		return m_ReadOffset >= m_Buffer.size();
	}

	/**
	 * \brief Get the buffer data
	 */
	std::uint8_t* data() {
		return m_Buffer.data();
	}

	/**
	 * \brief Get the buffer data
	 */
	const std::uint8_t* data() const {
		return m_Buffer.data();
	}

	/**
	 * \brief Get the read offset
	 */
	std::size_t GetReadOffset() const {
		return m_ReadOffset;
	}

	/**
	 * \brief Set the read offset
	 * \param pos The new read offset
	 */
	void SetReadOffset(std::size_t pos);

	/**
	 * \brief Get the size of the buffer
	 */
	std::size_t GetSize() const;

	/**
	 * \brief Get the remaining size of the buffer
	 */
	std::size_t GetRemaining() const;

	/**
	 * \brief Read a file into the buffer
	 * \param fileName The name of the file to read
	 */
	bool ReadFile(const std::string& fileName);

	/**
	 * \brief Write a file into the buffer
	 * \param fileName The name of the file to write to
	 */
	bool WriteFile(const std::string& fileName) const;

	/**
	 * \brief Allocate the buffer on the heap
	 * \warning Don't forget to free the data !
	 */
	std::uint8_t* HeapAllocatedData() const {
		std::uint8_t* newBuffer = new std::uint8_t[GetSize()];
		std::memcpy(newBuffer, data(), GetSize());
		return newBuffer;
	}

	/**
	 * \brief Operator == to compare two DataBuffer
	 */
	bool operator==(const DataBuffer& other) const {
		return m_Buffer == other.m_Buffer;
	}

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

/**
 * \brief Operator << to write a DataBuffer to an ostream
 */
std::ostream& operator<<(std::ostream& os, const DataBuffer& buffer);

