#pragma once

#include <psemek/util/exception.hpp>

#include <memory>
#include <cstddef>
#include <algorithm>
#include <format>
#include <initializer_list>
#include <concepts>

namespace psemek::util
{

	struct array_index_out_of_bounds
		: util::exception
	{
		array_index_out_of_bounds(std::size_t index, std::size_t size, util::stacktrace stacktrace = {})
			: util::exception(std::format("Array index ({}) out of bounds ({})", index, size), std::move(stacktrace))
		{}
	};

	// Like std::vector, but without auto-reallocation and implicit copying
	template <typename T>
	struct array
	{
		using value_type = T;
		using reference_type = T &;
		using pointer_type = T *;
		using iterator = T *;
		using const_iterator = T const *;
		using size_type = std::size_t;

		array() = default;

		explicit array(std::size_t size);
		array(std::size_t size, T const & value);

		template <std::input_iterator Iterator>
		array(Iterator begin, Iterator end);

		array(std::initializer_list<T> const & list);

		array(array const & other) = delete;
		array(array && other);

		array & operator = (array const & other) = delete;
		array & operator = (array && other);

		std::size_t size() const;
		bool empty();

		T * data();
		T const * data() const;

		void resize(std::size_t size);
		void resize(std::size_t size, T const & value);
		void clear();

		void fill(T const & value);

		T & operator[] (std::size_t index);
		T const & operator[] (std::size_t index) const;

		T & at(std::size_t index);
		T const & at(std::size_t index) const;

		T * begin();
		T * end();

		T const * begin() const;
		T const * end() const;

		T const * cbegin() const;
		T const * cend() const;

		array<T> copy() const;

	private:
		std::unique_ptr<T[]> data_ = nullptr;
		std::size_t size_ = 0;

		static std::unique_ptr<T[]> allocate(std::size_t size);
		void check_index(std::size_t index);
	};

	template <typename T>
	array<T>::array(std::size_t size)
		: data_(allocate(size))
		, size_(size)
	{}

	template <typename T>
	array<T>::array(std::size_t size, T const & value)
		: data_(allocate(size))
		, size_(size)
	{
		fill(value);
	}

	template <typename T>
	template <std::input_iterator  Iterator>
	array<T>::array(Iterator begin, Iterator end)
	{
		size_ = std::distance(begin, end);
		data_ = allocate(size_);
		std::copy(begin, end, data_.get());
	}

	template <typename T>
	array<T>::array(std::initializer_list<T> const & list)
		: data_(allocate(list.size()))
		, size_(list.size())
	{
		std::copy(list.begin(), list.end(), begin());
	}

	template <typename T>
	array<T>::array(array && other)
		: data_(std::move(other.data_))
		, size_(other.size_)
	{
		other.size_ = 0;
	}

	template <typename T>
	array<T> & array<T>::operator = (array<T> && other)
	{
		data_ = std::move(other.data_);
		size_ = other.size_;
		return *this;
	}

	template <typename T>
	std::size_t array<T>::size() const
	{
		return size_;
	}

	template <typename T>
	bool array<T>::empty()
	{
		return size_ == 0;
	}

	template <typename T>
	T * array<T>::data()
	{
		return data_.get();
	}

	template <typename T>
	T const * array<T>::data() const
	{
		return data_.get();
	}

	template <typename T>
	void array<T>::resize(std::size_t size)
	{
		if (size == size_) return;

		auto new_data = allocate(size);
		std::copy_n(data_.get(), std::min(size, size_), new_data.get());

		data_ = std::move(new_data);
		size_ = size;
	}

	template <typename T>
	void array<T>::resize(std::size_t size, T const & value)
	{
		if (size == size_) return;

		auto min_size = std::min(size, size_);

		auto new_data = allocate(size);
		std::copy_n(data_.get(), min_size, new_data.get());
		std::fill_n(new_data.get() + min_size, size - min_size, value);

		data_ = std::move(new_data);
		size_ = size;
	}

	template <typename T>
	void array<T>::clear()
	{
		data_.reset();
		size_ = 0;
	}

	template <typename T>
	void array<T>::fill(T const & value)
	{
		std::fill(begin(), end(), value);
	}

	template <typename T>
	T & array<T>::operator[] (std::size_t index)
	{
		return data_[index];
	}

	template <typename T>
	T const & array<T>::operator[] (std::size_t index) const
	{
		return data_[index];
	}

	template <typename T>
	T & array<T>::at(std::size_t index)
	{
		check_index(index);
		return data_[index];
	}

	template <typename T>
	T const & array<T>::at(std::size_t index) const
	{
		check_index(index);
		return data_[index];
	}

	template <typename T>
	T * array<T>::begin()
	{
		return data_.get();
	}

	template <typename T>
	T * array<T>::end()
	{
		return data_.get() + size_;
	}

	template <typename T>
	T const * array<T>::begin() const
	{
		return data_.get();
	}

	template <typename T>
	T const * array<T>::end() const
	{
		return data_.get() + size_;
	}

	template <typename T>
	T const * array<T>::cbegin() const
	{
		return begin();
	}

	template <typename T>
	T const * array<T>::cend() const
	{
		return end();
	}

	template <typename T>
	array<T> array<T>::copy() const
	{
		return array<T>(begin(), end());
	}

	template <typename T>
	std::unique_ptr<T[]> array<T>::allocate(std::size_t size)
	{
		return size == 0 ? nullptr : std::make_unique_for_overwrite<T[]>(size);
	}

	template <typename T>
	void array<T>::check_index(std::size_t index)
	{
		if (index >= size_)
			throw array_index_out_of_bounds(index, size_);
	}

}
