/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#pragma once

#include <memory>
#include <ostream>
#include <istream>

namespace OpenVulkano
{
	template<typename FRONT_TYPE, typename BACK_TYPE, typename = std::enable_if<!std::is_same_v<FRONT_TYPE, BACK_TYPE>>>
	class SharedDualTypeBuffer
	{
	protected:
		uint64_t m_size;
		uint32_t m_countFront;
		uint32_t m_countBack;
		void* m_buffer;
		BACK_TYPE* m_end;
		bool m_ownsMemory;

		static constexpr BACK_TYPE* CalculateEnd(void* buffer, size_t size)
		{
			return reinterpret_cast<BACK_TYPE*>(static_cast<uint8_t*>(buffer) + size);
		}

	public:
		SharedDualTypeBuffer(std::istream& stream, void* buffer = nullptr)
				: m_ownsMemory(!buffer)
		{
			stream.read(reinterpret_cast<char*>(&m_size), sizeof(uint64_t) + 2 * sizeof(uint32_t));
			m_buffer = buffer ? buffer : ::operator new(m_size);
			m_end = CalculateEnd(m_buffer, m_size);
			stream.read(static_cast<char*>(m_buffer), m_countFront * sizeof(FRONT_TYPE));
			stream.read(reinterpret_cast<char*>(GetBackBuffer()), m_countBack * sizeof(BACK_TYPE));
		}

		SharedDualTypeBuffer(size_t size, void* buffer = nullptr) : m_size(size), m_countFront(0), m_countBack(0)
				, m_buffer(buffer ? buffer : ::operator new(size)), m_end(CalculateEnd(m_buffer, size)), m_ownsMemory(!buffer)
		{}

		SharedDualTypeBuffer(SharedDualTypeBuffer&& buffer) noexcept
			: m_size(buffer.m_size), m_countFront(buffer.m_countFront)
			, m_countBack(buffer.m_countBack), m_buffer(buffer.m_buffer)
			, m_end(buffer.m_end), m_ownsMemory(buffer.m_ownsMemory)
		{
			buffer.m_size = 0;
			buffer.m_countFront = 0;
			buffer.m_countBack = 0;
			buffer.m_buffer = nullptr;
			buffer.m_end = nullptr;
			buffer.m_ownsMemory = false;
		}

		SharedDualTypeBuffer(const SharedDualTypeBuffer& buffer)
				: SharedDualTypeBuffer(buffer.m_size)
		{
			memcpy(m_buffer, buffer.m_buffer, buffer.m_size);
		}

		virtual ~SharedDualTypeBuffer()
		{
			Clear();
			if (m_ownsMemory) ::operator delete(m_buffer);
		}

		void Clear()
		{
			for(uint32_t i = 0; i < GetFrontCount(); i++)
			{
				GetFrontBuffer()[i].~FRONT_TYPE();
			}
			for (uint32_t i = 0; i < GetBackCount(); i++)
			{
				GetBackBuffer()[i].~BACK_TYPE();
			}
			m_countFront = 0;
			m_countBack = 0;
		}

		void ToStream(std::ostream& stream) const
		{
			stream.write(reinterpret_cast<const char*>(&m_size), sizeof(uint64_t) + 2 * sizeof(uint32_t));
			stream.write(static_cast<const char*>(m_buffer), m_countFront * sizeof(FRONT_TYPE));
			stream.write(reinterpret_cast<const char*>(GetBackBuffer()), m_countBack * sizeof(BACK_TYPE));
		}

		SharedDualTypeBuffer& operator =(SharedDualTypeBuffer&& buffer) noexcept
		{
			if (m_ownsMemory) ::operator delete(m_buffer);

			m_size = buffer.m_size;
			m_countFront = buffer.m_countFront;
			m_countBack = buffer.m_countBack;
			m_buffer = buffer.m_buffer;
			m_ownsMemory = buffer.m_ownsMemory;

			buffer.m_size = 0;
			buffer.m_countFront = 0;
			buffer.m_countBack = 0;
			buffer.m_buffer = nullptr;
			buffer.m_ownsMemory = false;
		}

		[[nodiscard]] size_t GetSizeFree() const
		{
			return m_size - (sizeof(BACK_TYPE) * m_countBack + sizeof(FRONT_TYPE) * m_countFront);
		}

		[[nodiscard]] size_t GetSize() const
		{
			return m_size;
		}

		[[nodiscard]] FRONT_TYPE* GetFrontBuffer()
		{
			return static_cast<FRONT_TYPE*>(m_buffer);
		}

		[[nodiscard]] const FRONT_TYPE* GetFrontBuffer() const
		{
			return static_cast<const FRONT_TYPE*>(m_buffer);
		}

		[[nodiscard]] BACK_TYPE* GetBackBuffer()
		{
			return m_end - GetBackCount();
		}

		[[nodiscard]] const BACK_TYPE* GetBackBuffer() const
		{
			return const_cast<SharedDualTypeBuffer*>(this)->GetBackBuffer();
		}

		[[nodiscard]] uint32_t GetFrontCount() const
		{
			return m_countFront;
		}

		[[nodiscard]] uint32_t GetBackCount() const
		{
			return m_countBack;
		}

		FRONT_TYPE* InsertFront(const FRONT_TYPE& value)
		{
			if (GetSizeFree() < sizeof(FRONT_TYPE)) return nullptr;
			GetFrontBuffer()[m_countFront] = value;
			m_countFront++;
			return GetFrontBuffer()[m_countFront - 1];
		}

		FRONT_TYPE* InsertFront(FRONT_TYPE&& value)
		{
			if (GetSizeFree() < sizeof(FRONT_TYPE)) return nullptr;
			GetFrontBuffer()[m_countFront] = std::move(value);
			m_countFront++;
			return GetFrontBuffer()[m_countFront - 1];
		}

		template<typename ...ARGS>
		FRONT_TYPE* EmplaceFront(ARGS&&... args)
		{
			if (GetSizeFree() < sizeof(FRONT_TYPE)) return nullptr;
			FRONT_TYPE* result = new (&GetFrontBuffer()[m_countFront]) FRONT_TYPE(std::forward<ARGS>(args)...);
			m_countFront++;
			return result;
		}

		BACK_TYPE* InsertBack(const BACK_TYPE& value)
		{
			if (GetSizeFree() < sizeof(BACK_TYPE)) return nullptr;
			m_countBack++;
			GetBackBuffer()[0] = value;
			return GetBackBuffer();
		}

		BACK_TYPE* InsertBack(BACK_TYPE&& value)
		{
			if (GetSizeFree() < sizeof(BACK_TYPE)) return nullptr;
			m_countBack++;
			GetBackBuffer()[0] = std::move(value);
			return GetBackBuffer();
		}

		template<typename ...ARGS>
		BACK_TYPE* EmplaceBack(ARGS&&... args)
		{
			if (GetSizeFree() < sizeof(BACK_TYPE)) return nullptr;
			m_countBack++;
			return new (&GetBackBuffer()[0]) BACK_TYPE(std::forward<ARGS>(args)...);
		}

		FRONT_TYPE* Insert(const FRONT_TYPE& value) { return InsertFront(value); }
		FRONT_TYPE* Insert(FRONT_TYPE&& value) { return InsertFront(value); }

		BACK_TYPE* Insert(const BACK_TYPE& value) { return InsertBack(value); }
		BACK_TYPE* Insert(BACK_TYPE&& value) { return InsertBack(value); }

		FRONT_TYPE& GetFront(size_t index)
		{
			return GetFrontBuffer()[index];
		}

		const FRONT_TYPE& GetFront(size_t index) const
		{
			return GetFrontBuffer()[index];
		}

		BACK_TYPE& GetBack(size_t index)
		{
			return m_end[-index - 1];
		}

		const BACK_TYPE& GetBack(size_t index) const
		{
			return const_cast<SharedDualTypeBuffer*>(this)->GetBack(index);
		}

		// Iterators
		FRONT_TYPE* BeginFront() { return GetFrontBuffer(); }
		FRONT_TYPE* EndFront() { return GetFrontBuffer() + GetSize(); }
	};
}
