const debug = @import("./debug.zig");

const io = @import("./io.zig");

const math = @import("./math.zig");

const std = @import("std");

///
/// Addressable mapping of integers described by `index_int` to values of type `Value`.
///
/// Slab maps are similar to slot maps in that they have O(1) insertion and removal, however, use a flat table layout
/// instead of parallel arrays. This reduces memory usage in some cases and can be useful for data that does not need to
/// be quickly iterated over, as values ordering is not guaranteed.
///
/// *Note* `index_int` values may be as big or as small as desired per the use-case of the consumer, however, integers
/// smaller than `usize` may result in the map reporting it is out of memory due to exhausting the addressable space
/// provided by the integer.
///
pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type {
	return struct {
		free_index: Index = 0,
		count: Index = 0,
		table: []Entry = &.{},

		///
		/// Table entry which may either store an inserted value or an index to the next free entry in the table.
		///
		const Entry = union (enum) {
			free_index: Index,
			value: Value,
		};

		///
		/// Used for indexing into the slab map.
		///
		const Index = math.Int(index_int);

		///
		/// Slab map type.
		///
		const Self = @This();

		///
		/// Overwrites the value referenced by `index` in `self`.
		///
		pub fn assign(self: *Self, index: Index, value: Value) void {
			const entry = &self.table[index];

			debug.assert(entry.* == .value);

			entry.value = value;
		}

		///
		/// Fetches the value referenced by `index` in `self`, returning it.
		///
		pub fn fetch(self: *Self, index: Index) Value {
			const entry = &self.table[index];

			debug.assert(entry.* == .value);

			return entry.value;
		}

		///
		/// Deinitializes `self` and sets it to an invalid state, freeing all memory allocated by `allocator`.
		///
		/// *Note* if the `table` field of `self` is an allocated slice, `allocator` must reference the same allocation
		/// strategy as the one originally used to allocate the current table.
		///
		pub fn deinit(self: *Self, allocator: io.Allocator) void {
			if (self.table.len == 0) {
				return;
			}

			io.deallocate(allocator, self.table);

			self.table = &.{};
			self.count = 0;
			self.free_index = 0;
		}

		///
		/// Attempts to grow the internal buffer of `self` by `growth_amount` using `allocator`.
		///
		/// The function returns [io.AllocatorError] if `allocator` could not commit the memory required to grow the
		/// table by `growth_amount`, leaving `self` in the same state that it was in prior to starting the grow.
		///
		/// Growing ahead of multiple insertion operations is useful when the upper bound of insertions is well-
		/// understood, as it can reduce the number of allocations required per insertion.
		///
		/// *Note* if the `table` field of `self` is an allocated slice, `allocator` must reference the same allocation
		/// strategy as the one originally used to allocate the current table.
		///
		pub fn grow(self: *Self, allocator: io.Allocator, growth_amount: usize) io.AllocationError!void {
			const grown_capacity = self.table.len + growth_amount;
			const entries = try io.allocate_many(allocator, grown_capacity, Entry);

			errdefer io.deallocate(allocator, entries);

			if (self.table.len != 0) {
				for (0 .. self.table.len) |index| {
					entries[index] = self.table[index];
				}

				for (self.table.len .. entries.len) |index| {
					entries[index] = .{.free_index = 0};
				}

				io.deallocate(allocator, self.table);
			}

			self.table = entries;
		}

		///
		/// Attempts to insert `value` into `self` as a new entry using `allocator` as the allocation strategy,
		/// returning an index value representing a reference to the inserted value that may be queried through `self`
		/// after.
		///
		/// The function returns [io.AllocationError] if `allocator` could not commit the memory required to grow the
		/// internal buffer of `self` when necessary.
		///
		/// *Note* if the `table` field of `self` is an allocated slice, `allocator` must reference the same allocation
		/// strategy as the one originally used to allocate the current table.
		///
		pub fn insert(self: *Self, allocator: io.Allocator, value: Value) io.AllocationError!Index {
			if (self.count == self.table.len) {
				try self.grow(allocator, math.max(1, self.count));
			}

			if (self.free_index == self.count) {
				const entry_index = self.count;
				const entry = &self.table[entry_index];

				entry.* = .{.value = value};

				self.count += 1;
				self.free_index += 1;

				return entry_index;
			}

			const entry_index = self.free_index;
			const entry = &self.table[self.free_index];

			debug.assert(entry.* == .free_index);

			self.free_index = entry.free_index;
			entry.* = .{.value = value};

			return entry_index;
		}

		///
		/// Removes the value referenced by `index` from `self`.
		///
		pub fn remove(self: *Self, index: Index) void {
			const entry = &self.table[index];

			debug.assert(entry.* == .value);

			entry.* = .{.free_index = self.free_index};
			self.free_index = index;
		}
	};
}