/*
 * 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 <algorithm>
#include <iterator>
#include <initializer_list>
#include <cstddef>
#include <vector>
#include <stdexcept>
#include <string>

namespace OpenVulkano
{
	/**
	 * Heap allocated array. For all these times you need a fixed size data structure, but don't know the size at compile time.
	 * @tparam T The type to be used
	 */
	template<typename T>
	class Array
	{
		size_t size;
		T* data;

		void ClearData()
		{
			if constexpr (!std::is_trivially_destructible_v<T>)
			{
				// Call the destructor on all the members in reverse order
				for (size_t i = size; i > 0;)
				{
					data[--i].~T();
				}
			}
		}

		static T* MakeBuffer(size_t count)
		{
			return static_cast<T*>(::operator new(sizeof(T) * count));
		}

	public:
		using iterator = T*;
		using const_iterator = const T*;
		using reverse_iterator = std::reverse_iterator<iterator>;
		using const_reverse_iterator = std::reverse_iterator<const_iterator>;

		Array() noexcept: size(0), data(nullptr)
		{}

		explicit Array(size_t size) : size(size), data(MakeBuffer(size))
		{
			for(size_t i = 0; i < size; i++)
			{
				new (&data[i]) T();
			}
		}

		Array(const std::initializer_list<T>& list) : size(list.size()), data(MakeBuffer(list.size()))
		{
			int i = 0;
			for(const T& e : list)
			{
				new (&data[i++]) T(e);
			}
		}

		Array(const Array<T>& other) : size(other.Size()), data(MakeBuffer(other.Size()))
		{
			for(size_t i = 0; i < size; i++)
			{
				new (&data[i]) T(other[i]);
			}
		}

		Array(Array<T>&& other) noexcept:
				size(other.size), data(other.data)
		{
			other.data = nullptr;
			other.size = 0;
		}

		explicit Array(const std::vector<T>& vector) : size(vector.size()), data(MakeBuffer(size))
		{
			int i = 0;
			for (const auto& e : vector)
			{
				new (&data[i++]) T(e);
			}
		}

		Array(size_t size, const T& defaultValue) : size(size), data(MakeBuffer(size))
		{
			Fill(defaultValue);
		}

		template<typename... ARGS>
		Array(size_t size, ARGS... args) : size(size), data(MakeBuffer(size))
		{
			for(size_t i = 0; i < size; i++)
			{
				new (&data[i]) T(args...);
			}
		}

		~Array() noexcept
		{
			ClearData();
			::operator delete(data);
		}

		Array& operator=(const Array<T>& rhs)
		{
			if (this == &rhs) return *this;
			Resize(rhs.Size());
			for(size_t i = 0; i < size; i++)
			{
				new (&data[i]) T(rhs[i]);
			}
			return *this;
		}

		Array& operator=(const std::initializer_list<T>& list)
		{
			Resize(list.size());
			int i = 0;
			for(const T& e : list)
			{
				new (&data[i++]) T(e);
			}
			return *this;
		}

		Array& operator=(Array<T>&& rhs) noexcept
		{
			if (this != &rhs)
			{
				Array(std::move(rhs)).Swap(*this);
			}
			return *this;
		}

		T& operator[](size_t n)
		{
			return data[n];
		}

		const T& operator[](size_t n) const
		{
			return data[n];
		}

		operator bool() const noexcept
		{
			return data;
		}

		iterator Begin() noexcept
		{
			return data;
		}

		const_iterator Begin() const noexcept
		{
			return data;
		}

		iterator begin() noexcept
		{
			return data;
		}

		const_iterator begin() const noexcept
		{
			return data;
		}

		iterator End() noexcept
		{
			return Begin() + Size();
		}

		const_iterator End() const noexcept
		{
			return Begin() + Size();
		}

		iterator end() noexcept
		{
			return Begin() + Size();
		}

		const_iterator end() const noexcept
		{
			return Begin() + Size();
		}

		reverse_iterator ReverseBegin() noexcept
		{
			return reverse_iterator(End());
		}

		const_reverse_iterator ReverseBegin() const noexcept
		{
			return const_reverse_iterator(End());
		}

		reverse_iterator ReverseEnd() noexcept
		{
			return reverse_iterator(Begin());
		}

		const_reverse_iterator ReverseEnd() const noexcept
		{
			return const_reverse_iterator(Begin());
		}

		const_iterator ConstBegin() const noexcept
		{
			return Begin();
		}

		const_iterator ConstEnd() const noexcept
		{
			return End();
		}

		const_reverse_iterator ConstReverseBegin() const noexcept
		{
			return ReverseBegin();
		}

		const_reverse_iterator ConstReverseEnd() const noexcept
		{
			return ReverseEnd();
		}

		[[nodiscard]] size_t Size() const noexcept
		{
			return size;
		}

		[[nodiscard]] size_t MaxSize() const noexcept
		{
			return size;
		}

		[[nodiscard]] bool Empty() const noexcept
		{
			return Size() == 0;
		}

		T& At(size_t n)
		{
			if (n >= Size()) throw std::out_of_range("Index " + std::to_string(n) + " is out of bounds.");
			return data[n];
		}

		const T& At(size_t n) const
		{
			return const_cast<Array*>(this)->At(n);
		}

		T& Front() noexcept
		{
			return *Begin();
		}

		const T& Front() const noexcept
		{
			return *ConstBegin();
		}

		T& Back() noexcept
		{
			return *(End() - 1);
		}

		const T& Back() const noexcept
		{
			return *(ConstEnd() - 1);
		}

		T* Data() noexcept
		{
			return data;
		}

		const T* Data() const noexcept
		{
			return data;
		}

		void Fill(const T& value) noexcept(noexcept(std::fill(Begin(), End(), value)))
		{
			std::fill(Begin(), End(), value);
		}

		void Swap(Array<T>& other) noexcept
		{
			std::swap(data, other.data);
			std::swap(size, other.size);
		}

	private:
		void Resize(size_t newSize)
		{
			ClearData();
			if (Size() != newSize)
			{
				::operator delete(data);
				data = MakeBuffer(newSize);
				size = newSize;
			}
		}
	};

	template<typename T>
	inline bool operator==(const Array<T>& lhs, const Array<T>& rhs)
	{
		return lhs.Size() == rhs.Size() && std::equal(lhs.begin(), lhs.end(), rhs.begin());
	}

	template<typename T>
	inline bool operator!=(const Array<T>& lhs, const Array<T>& rhs)
	{
		return !(lhs == rhs);
	}

	template<typename T>
	inline bool operator<(const Array<T>& lhs, const Array<T>& rhs)
	{
		return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), rhs.end());
	}

	template<typename T>
	inline bool operator>(const Array<T>& lhs, const Array<T>& rhs)
	{
		return rhs < lhs;
	}

	template<typename T>
	inline bool operator>=(const Array<T>& lhs, const Array<T>& rhs)
	{
		return !(lhs < rhs);
	}

	template<typename T>
	inline bool operator<=(const Array<T>& lhs, const Array<T>& rhs)
	{
		return !(rhs < lhs);
	}
}