// =====================================================================================================================
//  fennec, a free and open source game engine
//  Copyright © 2025 - 2026  Medusa Slockbower
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <https://www.gnu.org/licenses/>.
// =====================================================================================================================

///
/// \file fennec/memory/allocator.h
/// \brief This header contains structures and classes related to allocating blocks of memory
///
///
/// \details
/// \author Medusa Slockbower
///
/// \copyright Copyright © 2025 - 2026 Medusa Slockbower ([GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html))
///
///


#ifndef FENNEC_MEMORY_ALLOCATOR_H
#define FENNEC_MEMORY_ALLOCATOR_H

#include <fennec/memory/pointer_traits.h>
#include <fennec/memory/new.h>

#include <fennec/lang/conditional_types.h>
#include <fennec/lang/numeric_transforms.h>
#include <fennec/lang/types.h>
#include <fennec/lang/type_traits.h>

#include <fennec/math/ext/constants.h>
#include <fennec/math/common.h>


#ifdef FENNEC_COMPILER_GCC
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wchanges-meaning"
#endif


namespace fennec
{

///
/// \brief Helper structure for obtaining traits of an allocator class
/// \tparam Alloc The allocator class to analyze
template<class Alloc>
struct allocator_traits
{
private:
	// These help with using concepts in `detect_t`
	template<typename ClassT> using _pointer            = typename ClassT::pointer_t;
	template<typename ClassT> using _const_pointer      = typename ClassT::const_pointer_t;
	template<typename ClassT> using _void_pointer       = typename ClassT::void_pointer_t;
	template<typename ClassT> using _void_const_pointer = typename ClassT::void_const_pointer_t;

	// Propagation Patterns
	template<typename ClassT> using _propagate_on_containter_copy_assignment = typename ClassT::propagate_on_containter_copy_assignment;
	template<typename ClassT> using _propagate_on_containter_move_assignment = typename ClassT::propagate_on_containter_move_assignment;
	template<typename ClassT> using _propagate_on_containter_swap            = typename ClassT::propagate_on_containter_swap;

	template<typename ClassT> using _is_always_equal = typename ClassT::is_always_equal;

	template<typename AllocT, typename TypeT>
    struct _rebind : replace_first_element<AllocT, TypeT> {};

	template<typename AllocT, typename TypeT>
	requires requires { typename AllocT::template rebind<TypeT>::other; }
	struct _rebind<AllocT, TypeT> { using type = typename AllocT::template rebind<TypeT>::other; };

	// This detects AllocT::diff_t if present, otherwise uses the diff_t associated with PtrT
	// It works using SFINAE, 'typename = void' forces the second _diff to be evaluated first when
	// _diff is substituted using only two template arguments. That one uses 'type = typename AllocT::diff_t'
	// however, if it fails, the compiler moves on to the original definition. _size works in the same manner.
	template<typename AllocT, typename PtrT, typename = void>
	struct _diff { using type = typename pointer_traits<PtrT>::diff_t; };

	template<typename AllocT, typename PtrT>
	struct _diff<AllocT, PtrT, void_t<typename AllocT::diff_t>> { using type = typename AllocT::diff_t; };

	template<typename AllocT, typename DiffT, typename = void>
	struct _size : make_unsigned<DiffT> {};

	template<typename AllocT, typename DiffT>
	struct _size<AllocT, DiffT, void_t<typename AllocT::size_t>> { using type = typename AllocT::size_t; };

public:

	/// \brief Alias for the allocator type
	using alloc_t = Alloc;

	/// \brief Alias for the value type of the allocator
	using value_t = typename Alloc::value_t;

	/// \brief Alias for a pointer to the value type. Will use `Alloc::pointer_t` if present
	using pointer_t = detect_t<value_t*, _pointer, Alloc>;

	/// \brief Alias for a const pointer to the value type. Will use `Alloc::const_pointer_t` if present
	using const_pointer_t = detect_t<const value_t*, _const_pointer, Alloc>;

	/// \brief Alias for a pointer to void. Will use `Alloc::void_pointer_t` if present
	using void_pointer_t = detect_t<void*, _void_pointer, Alloc>;

	/// \brief Alias for a const pointer to void. Will use `Alloc::const_void_pointer_t` if present
	using const_void_pointer_t = detect_t<const void*, _void_const_pointer, Alloc>;

	/// \brief Alias for differences between pointers. Will use `Alloc::diff_t` if present
	using diff_t = typename _diff<Alloc, pointer_t>::type;

	/// \brief Alias for the size of allocations. Will use `Alloc::size_t` if present
	using size_t = typename _size<Alloc, pointer_t>::type;

	/// \brief Checks if this allocator type is always equal to another allocator of similar type
	using is_always_equal = detect_t<false_type, _is_always_equal, Alloc>;

	/// \brief Rebinds the allocator type to produce an element type of type \emph{TypeT}
	template<typename TypeT> using rebind = typename _rebind<Alloc, TypeT>::type;

	// TODO: allocator_traits static functions
};


///
/// \brief Allocator implementation, uses \emph{new} and \emph{delete} operators.
/// \tparam T The data type to allocate
template<typename T>
class allocator
{
public:
	///
	/// \brief Alias for the data type used for metaprogramming
	using value_t = T;

	///
	/// \brief Metaprogramming utility to rebind an allocator to a different data type
	template<typename R> using rebind = allocator<R>;

	///
	/// \brief Default Constructor
	constexpr allocator() = default;

	///
	/// \brief Default Destructor
	constexpr ~allocator() = default;

	///
	/// \brief Copy Constructor
	constexpr allocator(const allocator&) = default;

	///
	/// \brief Copy Assignment
	/// \returns A reference to \emph{this}
	constexpr allocator& operator=(const allocator&) = default;

	///
	/// \brief Equality operator
	/// \returns \emph{true}
	constexpr bool_t operator==(const allocator&) {
		return true;
	}

	///
	/// \brief Inequality operator
	/// \returns \emph{false}
	constexpr bool_t operator!=(const allocator&) {
		return false;
	}

	///
	/// \brief Equality operator for allocators of same type but with different data type
	/// \returns \emph{false}
	template<typename U> constexpr bool_t operator==(const allocator<U>&) {
		return false;
	}

	///
	/// \brief Inequality operator for allocators of same type but with different data type
	/// \returns \emph{true}
	template<typename U> constexpr bool_t operator!=(const allocator<U>&) {
		return true;
	}

	///
	/// \brief Allocate a block of memory large enough to hold \emph{n} elements of type \emph{T}
	/// \param n The number of elements
	/// \returns A pointer to the allocated block
	constexpr T* allocate(size_t n) {
		return static_cast<T*>(::operator new(n * sizeof(T)));
	}

	///
	/// \brief Allocate a block of memory large enough to hold \emph{n} elements of type \emph{T}
	/// \param n The number of elements
	/// \param align The alignment
	/// \returns A pointer to the allocated block
	constexpr T* allocate(size_t n, align_t align) {
		return static_cast<T*>(::operator new(n * sizeof(T), align));
	}

	///
	/// \brief Deallocate a block of memory with type \emph{T}
	/// \param ptr The block to release
	constexpr void deallocate(T* ptr) {
		return ::operator delete(ptr);
	}

	///
	/// \brief Deallocate a block of memory with type \emph{T}
	/// \param ptr The block to release
	/// \param align The alignment
	constexpr void deallocate(T* ptr, align_t align) {
		return ::operator delete(ptr, align);
	}
};


///
/// \brief Allocator implementation, uses \emph{new} and \emph{delete} operators.
/// \tparam T The data type to allocate
template<typename T>
class allocator<T[]>
{
public:
	///
	/// \brief Alias for the data type used for metaprogramming
	using value_t = T;

	///
	/// \brief Metaprogramming utility to rebind an allocator to a different data type
	template<typename R> using rebind = allocator<R>;

	///
	/// \brief Default Constructor
	constexpr allocator() = default;

	///
	/// \brief Default Destructor
	constexpr ~allocator() = default;

	///
	/// \brief Copy Constructor
	constexpr allocator(const allocator&) = default;

	///
	/// \brief Copy Assignment
	/// \returns A reference to \emph{this}
	constexpr allocator& operator=(const allocator&) = default;


	///
	/// \brief Equality operator
	/// \returns \emph{true}
	constexpr bool_t operator==(const allocator&) {
		return true;
	}

	///
	/// \brief Inequality operator
	/// \returns \emph{false}
	constexpr bool_t operator!=(const allocator&) {
		return false;
	}

	///
	/// \brief Equality operator for allocators of same type but with different data type
	/// \returns \emph{false}
	template<typename U> constexpr bool_t operator==(const allocator<U>&) {
		return false;
	}

	///
	/// \brief Inequality operator for allocators of same type but with different data type
	/// \returns \emph{true}
	template<typename U> constexpr bool_t operator!=(const allocator<U>&) {
		return true;
	}

	///
	/// \brief Allocate a block of memory large enough to hold \emph{n} elements of type \emph{T}
	/// \param n The number of elements
	/// \returns A pointer to the allocated block
	constexpr T* allocate(size_t n) {
		return static_cast<T*>(::operator new[](n * sizeof(T)));
	}

	///
	/// \brief Allocate a block of memory large enough to hold \emph{n} elements of type \emph{T}
	/// \param n The number of elements
	/// \param align The alignment
	/// \returns A pointer to the allocated block
	constexpr T* allocate(size_t n, align_t align) {
		return static_cast<T*>(::operator new[](n * sizeof(T), align));
	}

	///
	/// \brief Deallocate a block of memory with type \emph{T}
	/// \param ptr The block to release
	constexpr void deallocate(T* ptr) {
		return ::operator delete[](ptr);
	}

	///
	/// \brief Deallocate a block of memory with type \emph{T}
	/// \param ptr The block to release
	/// \param align The alignment
	constexpr void deallocate(T* ptr, align_t align) {
		return ::operator delete[](ptr, align);
	}
};


///
/// \brief Container to hold a memory allocation
/// \tparam T The data type of the allocation
///
/// \details This simply acts as a proxy for allocating memory. It does not call any constructors or
///          initialize any values as if they were the provided data type. Any operations present work
///          only on individual bytes.
template<typename T, class AllocT = allocator<T>>
struct allocation {

// Definitions =========================================================================================================
public:

	/// \name Definitions
	/// @{

	/// \brief alias for the allocator type
	using alloc_t = typename allocator_traits<AllocT>::template rebind<T>;

	/// \brief alias for the data type
	using value_t = T;

	/// \brief size type definition for ptr_traits
	using size_t = size_t;

	/// \brief diff type definition for ptr_traits
	using diff_t = ptrdiff_t;

	/// @}


// Constructors & Destructor ===========================================================================================
public:

	/// \name Constructors & Destructor
	/// @{

	///
	/// \brief Default Constructor, initializes internal data to \emph{null} and the capacity to \emph{0}
	constexpr allocation() noexcept
		: _data(nullptr), _capacity(0), _alignment(zero<align_t>()) {
	}

	///
	/// \brief Sized Constructor, initializes the allocation with a block of size `n * sizeof(T)` bytes
	/// \param n The number of elements of type \emph{T} to allocate for
	explicit constexpr allocation(size_t n) noexcept
		: _data(nullptr), _capacity(0), _alignment(zero<align_t>()) {
		allocate(n);
	}

	///
	/// \brief Buffer Copy Constructor, initializes the allocation with a block of size `n * sizeof(T)` bytes.
	///        Then, the contents of data are byte copied into the allocation.
	/// \param data the buffer to copy
	/// \param n the number of elements
	constexpr allocation(const T* data, size_t n)
		: allocation(n) {
		fennec::memmove(_data, data, n);
	}

	///
	/// \brief Sized Constructor, initializes the allocation with a block of size `n * sizeof(T)` bytes
	/// \param n The number of elements of type \emph{T} to allocate for
	/// \param align The alignment of the allocation
	constexpr allocation(size_t n, align_t align) noexcept
		: _data(nullptr)
		, _capacity(0)
		, _alignment(align) {
		allocate(n, align);
	}

	///
	/// \brief Buffer Copy Constructor, initializes the allocation with a block of size `n * sizeof(T)` bytes.
	///        Then, the contents of data are byte copied into the allocation.
	/// \param data the buffer to copy
	/// \param n the number of elements
	/// \param align The alignment of the allocation
	constexpr allocation(const T* data, size_t n, align_t align)
		: allocation(n, align) {
		fennec::memmove(static_cast<void*>(_data), data, n);
	}

	///
	/// \brief Allocator Constructor
	/// \param alloc The allocation object to copy.
	///
	/// \details This constructor should be used when the type \emph{AllocT} needs internal data.
	explicit constexpr allocation(const alloc_t& alloc) noexcept
		: _alloc(alloc)
		, _data(nullptr)
		, _capacity(0)
		, _alignment(zero<align_t>()){
	}

	///
	/// \brief Sized Allocator Constructor
	/// \param n The number of elements of type \emph{T} to allocate for
	/// \param alloc The allocation object to copy.
	///
	/// \details This constructor should be used when the type \emph{AllocT} needs internal data.
	constexpr allocation(size_t n, const alloc_t& alloc) noexcept
		: _alloc(alloc)
		, _data(nullptr)
		, _capacity(0)
		, _alignment(zero<align_t>()) {
		allocate(n);
	}

	///
	/// \brief Buffer Copy Allocator Constructor, initializes the allocation with a block of size `n * sizeof(T)` bytes.
	///        Then, the contents of data are copied into the allocation.
	/// \param data the buffer to copy
	/// \param n the number of elements
	/// \param alloc The allocation object to copy.
	///
	/// \details This constructor should be used when the type \emph{AllocT} needs internal data.
	constexpr allocation(const T* data, size_t n, const alloc_t& alloc)
		: allocation(n, alloc) {
		fennec::memmove(static_cast<void*>(_data), data, n);
	}

	///
	/// \brief Sized Allocator Constructor
	/// \param n The number of elements of type \emph{T} to allocate for
	/// \param align The alignment of the allocation
	/// \param alloc The allocation object to copy.
	///
	/// \details This constructor should be used when the type \emph{AllocT} needs internal data.
	constexpr allocation(size_t n, align_t align, const alloc_t& alloc) noexcept
		: _alloc(alloc)
		, _data(nullptr)
		, _capacity(0)
		, _alignment(zero<align_t>()) {
		allocate(n, align);
	}

	///
	/// \brief Buffer Copy Allocator Constructor, initializes the allocation with a block of size `n * sizeof(T)` bytes.
	///        Then, the contents of data are copied into the allocation.
	/// \param data the buffer to copy
	/// \param n the number of elements
	/// \param align The alignment of the allocation
	/// \param alloc The allocation object to copy.
	///
	/// \details This constructor should be used when the type \emph{AllocT} needs internal data.
	constexpr allocation(const T* data, size_t n, align_t align, const alloc_t& alloc)
		: allocation(n, align, alloc) {
		fennec::memmove(_data, data, n);
	}

	///
	/// \brief Copy Constructor, creates an allocation of equal size and performs a byte-wise copy
	/// \param alloc The allocation to copy
	constexpr allocation(const allocation& alloc) noexcept
		: _alloc(alloc._alloc)
		, _data(_alloc.allocate(alloc._capacity))
		, _capacity(alloc._capacity)
		, _alignment(alloc._alignment) {
		fennec::memmove(static_cast<void*>(_data), alloc._data, alloc._capacity * sizeof(T));
	}

	///
	/// \brief Move Constructor, moves the data in \emph{alloc} to the new object and cleans \emph{alloc} so that it
	///        can safely destruct
	/// \param alloc The allocation to move
	constexpr allocation(allocation&& alloc) noexcept
		: _alloc(alloc._alloc)
		, _data(alloc._data)
		, _capacity(alloc._capacity)
		, _alignment(alloc._alignment) {
		alloc._data = nullptr; alloc._capacity = 0;
	}


	///
	/// \brief Default Destructor, releases the memory block if still present
	constexpr ~allocation() noexcept {
		if (_data) {
			_alloc.deallocate(_data);
			_data = nullptr;
		}
	}

	/// @}


// Assignment ==========================================================================================================
public:

	/// \name Assignment
	/// @{

	///
	/// \brief Copy Assignment Operator
	/// \param alloc the allocation to copy
	/// \returns a reference to \emph{this}
	constexpr allocation& operator=(const allocation& alloc) {
		allocation::allocate(alloc.capacity(), alloc.alignment());
		fennec::memmove(_data, alloc, size());
		return *this;
	}

	///
	/// \brief Move Assignment Operator
	/// \param alloc the allocation to copy
	/// \returns a reference to \emph{this}
	constexpr allocation& operator=(allocation&& alloc) noexcept {

		// Copy contents
		fennec::swap(_alloc, alloc._alloc);
		fennec::swap(_data, alloc._data);
		fennec::swap(_capacity, alloc._capacity);
		fennec::swap(_alignment, alloc._alignment);

		return *this;
	}

	/// @}


// Properties ==========================================================================================================
public:

	/// \name Properties
	/// @{

	///
	/// \brief Getter for the byte size of the allocation.
	/// \returns the size of the allocation in bytes
	constexpr size_t size() const {
		return _capacity * sizeof(T);
	}

	///
	/// \brief Getter for the number of elements \emph{n} of type \emph{T} that the allocation can hold.
	/// \returns the size of the allocation in elements
	constexpr size_t capacity() const {
		return _capacity;
	}

	///
	/// \brief Getter for the alignment of the allocation.
	/// \returns the alignment of the allocation
	constexpr align_t alignment() const {
		return _alignment;
	}

	/// @}


// Modifiers ===========================================================================================================
public:

	/// \name Modifiers
	/// @{

	///
	/// \brief Allocate a block of memory for the allocation.
	/// \details If there is already an allocated block of memory, the previous allocation is released.
	///
	///
	/// \param n The number of elements of type \emph{T} to allocate for
	/// \param align The alignment to use
	constexpr void allocate(size_t n, align_t align = zero<align_t>()) noexcept {
		deallocate();

		if (align != zero<align_t>()) {
			_data = _alloc.allocate(_capacity = n, _alignment = align);
		} else {
			_data = _alloc.allocate(_capacity = n);
		}
	}

	///
	/// \brief Release the block of memory.
	constexpr void deallocate() noexcept {
		if (_data) {
			if (_alignment != zero<align_t>()) {
				_alloc.deallocate(_data, _alignment);
			} else {
				_alloc.deallocate(_data);
			}
		}

		_data = nullptr;
		_capacity = 0;
		_alignment = zero<align_t>();
	}

	///
	/// \brief Reallocate the block with a new size.
	///        Contents are copied to the new allocation.
	///
	/// \param n The number of elements of type \emph{T} to allocate for
	/// \param align The alignment to use
	constexpr void reallocate(size_t n, align_t align = zero<align_t>()) noexcept {
		if (_data == nullptr) {
			allocate(n, align);
			return;
		}

		value_t* old = _data;
		const size_t old_cap = _capacity;
		_data = nullptr;
		allocate(n, align);

		fennec::memmove(static_cast<void*>(_data), old, min(_capacity, old_cap) * sizeof(T));

		_alloc.deallocate(old);
		_capacity = n;
	}

	///
	/// \brief Clear the block of memory, setting all bytes to 0.
	constexpr void clear() noexcept {
		fennec::memset(static_cast<void*>(_data), 0, _capacity * sizeof(T));
	}

	/// @}


// Access ==============================================================================================================
public:

	/// \name Access
	/// @{

	///
	/// \param i The index to access
	/// \returns a reference to the value at position \emph{i} in the allocation
	constexpr value_t& operator[](size_t i) {
		assertd(i < capacity(), "Array Out of Bounds");
		return _data[i];
	}

	///
	/// \brief Array Access Operator
	/// \param i The index to access
	/// \returns a reference to the value at position \emph{i} in the allocation
	constexpr const value_t& operator[](size_t i) const {
		assertd(i < capacity(), "Array Out of Bounds");
		return _data[i];
	}

	///
	/// \returns The underlying pointer.
	constexpr operator value_t*() {
		return _data;
	}

	///
	/// \brief Dereference Operators
	/// \returns The underlying pointer.
	constexpr operator const value_t*() const {
		return _data;
	}

	///
	/// \brief Getter for the real pointer to the allocated block of memory
	/// \returns Pointer to the allocated memory.
	constexpr value_t* data() {
		return _data;
	}

	///
	/// \brief Getter for the real pointer to the allocated block of memory
	/// \returns Pointer to the allocated memory.
	constexpr const value_t* data() const {
		return _data;
	}

	///
	/// \returns A pointer to the start of the allocation.
	value_t* begin() {
		return _data;
	}

	///
	/// \brief C++ Iterator Begin
	/// \returns A pointer to the start of the allocation.
	const value_t* begin() const {
		return _data;
	}

	///
	/// \returns A pointer to the element one after the last.
	value_t* end() {
		return _data + capacity();
	}

	///
	/// \brief C++ Iterator End
	/// \returns A pointer to the element one after the last.
	const value_t* end() const {
		return _data + capacity();
	}

	/// @}

private:
	alloc_t     _alloc; // Allocator object
	value_t*     _data; // Handle for the memory block
	size_t   _capacity; // Capacity of the memory block in elements.
	align_t _alignment; // Alignment information
};

#ifdef FENNEC_COMPILER_GCC
#pragma GCC diagnostic pop
#endif

}

#endif // FENNEC_MEMORY_ALLOCATOR_H
