const std = @import("../std.zig");
const Allocator = std.mem.Allocator;
const Alignment = std.mem.Alignment;
const MemoryPool = std.heap.MemoryPool;

/// A memory pool that can allocate objects of a single type very quickly.
/// Use this when you need to allocate a lot of objects of the same type,
/// because it outperforms general purpose allocators.
/// Allocated items are aligned to `alignment`-byte addresses or `@alignOf(Item)`
/// if `alignment` is `null`.
/// Functions that potentially allocate memory accept an `Allocator` parameter.
pub fn Aligned(comptime Item: type, comptime alignment: Alignment) type {
    return Extra(Item, .{ .alignment = alignment });
}

pub const Options = struct {
    /// The alignment of the memory pool items. Use `null` for natural alignment.
    alignment: ?Alignment = null,

    /// If `true`, the memory pool can allocate additional items after a initial setup.
    /// If `false`, the memory pool will not allocate further after a call to `initPreheated`.
    growable: bool = true,
};

/// A memory pool that can allocate objects of a single type very quickly.
/// Use this when you need to allocate a lot of objects of the same type,
/// because it outperforms general purpose allocators.
/// Functions that potentially allocate memory accept an `Allocator` parameter.
pub fn Extra(comptime Item: type, comptime pool_options: Options) type {
    if (pool_options.alignment) |a| {
        if (a.compare(.eq, .of(Item))) {
            var new_options = pool_options;
            new_options.alignment = null;
            return Extra(Item, new_options);
        }
    }
    return struct {
        const Pool = @This();

        arena_state: std.heap.ArenaAllocator.State,
        free_list: std.SinglyLinkedList,

        /// Size of the memory pool items. This is not necessarily the same
        /// as `@sizeOf(Item)` as the pool also uses the items for internal means.
        pub const item_size = @max(@sizeOf(Node), @sizeOf(Item));

        /// Alignment of the memory pool items. This is not necessarily the same
        /// as `@alignOf(Item)` as the pool also uses the items for internal means.
        pub const item_alignment: Alignment = .max(pool_options.alignment orelse .of(Item), .of(Node));

        const Node = std.SinglyLinkedList.Node;
        const ItemPtr = *align(item_alignment.toByteUnits()) Item;

        /// A MemoryPool containing no elements.
        pub const empty: Pool = .{
            .arena_state = .{},
            .free_list = .{},
        };

        /// Creates a new memory pool and pre-allocates `num` items.
        /// This allows up to `num` active allocations before an
        /// `OutOfMemory` error might happen when calling `create()`.
        pub fn initCapacity(allocator: Allocator, num: usize) Allocator.Error!Pool {
            var pool: Pool = .empty;
            errdefer pool.deinit(allocator);
            try pool.addCapacity(allocator, num);
            return pool;
        }

        /// Destroys the memory pool and frees all allocated memory.
        pub fn deinit(pool: *Pool, allocator: Allocator) void {
            pool.arena_state.promote(allocator).deinit();
            pool.* = undefined;
        }

        /// Pre-allocates `num` items and adds them to the memory pool.
        /// This allows at least `num` active allocations before an
        /// `OutOfMemory` error might happen when calling `create()`.
        pub fn addCapacity(pool: *Pool, allocator: Allocator, num: usize) Allocator.Error!void {
            var i: usize = 0;
            while (i < num) : (i += 1) {
                const memory = try pool.allocNew(allocator);
                pool.free_list.prepend(@ptrCast(memory));
            }
        }

        pub const ResetMode = std.heap.ArenaAllocator.ResetMode;

        /// Resets the memory pool and destroys all allocated items.
        /// This can be used to batch-destroy all objects without invalidating the memory pool.
        ///
        /// The function will return whether the reset operation was successful or not.
        /// If the reallocation  failed `false` is returned. The pool will still be fully
        /// functional in that case, all memory is released. Future allocations just might
        /// be slower.
        ///
        /// NOTE: If `mode` is `free_all`, the function will always return `true`.
        pub fn reset(pool: *Pool, allocator: Allocator, mode: ResetMode) bool {
            // TODO: Potentially store all allocated objects in a list as well, allowing to
            // just move them into the free list instead of actually releasing the memory.

            var arena = pool.arena_state.promote(allocator);
            defer pool.arena_state = arena.state;

            const reset_successful = arena.reset(mode);
            pool.free_list = .{};

            return reset_successful;
        }

        /// Creates a new item and adds it to the memory pool.
        /// `allocator` may be `undefined` if pool is not `growable`.
        pub fn create(pool: *Pool, allocator: Allocator) Allocator.Error!ItemPtr {
            const ptr: ItemPtr = if (pool.free_list.popFirst()) |node|
                @ptrCast(@alignCast(node))
            else if (pool_options.growable)
                @ptrCast(try pool.allocNew(allocator))
            else
                return error.OutOfMemory;

            ptr.* = undefined;
            return ptr;
        }

        /// Destroys a previously created item.
        /// Only pass items to `ptr` that were previously created with `create()` of the same memory pool!
        pub fn destroy(pool: *Pool, ptr: ItemPtr) void {
            ptr.* = undefined;
            pool.free_list.prepend(@ptrCast(ptr));
        }

        fn allocNew(pool: *Pool, allocator: Allocator) Allocator.Error!*align(item_alignment.toByteUnits()) [item_size]u8 {
            var arena = pool.arena_state.promote(allocator);
            defer pool.arena_state = arena.state;
            const memory = try arena.allocator().alignedAlloc(u8, item_alignment, item_size);
            return memory[0..item_size];
        }
    };
}

test "basic" {
    const a = std.testing.allocator;

    var pool: MemoryPool(u32) = .empty;
    defer pool.deinit(a);

    const p1 = try pool.create(a);
    const p2 = try pool.create(a);
    const p3 = try pool.create(a);

    // Assert uniqueness
    try std.testing.expect(p1 != p2);
    try std.testing.expect(p1 != p3);
    try std.testing.expect(p2 != p3);

    pool.destroy(p2);
    const p4 = try pool.create(a);

    // Assert memory reuse
    try std.testing.expect(p2 == p4);
}

test "initCapacity (success)" {
    const a = std.testing.allocator;

    var pool: MemoryPool(u32) = try .initCapacity(a, 4);
    defer pool.deinit(a);

    _ = try pool.create(a);
    _ = try pool.create(a);
    _ = try pool.create(a);
}

test "initCapacity (failure)" {
    const failer = std.testing.failing_allocator;
    try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initCapacity(failer, 5));
}

test "growable" {
    const a = std.testing.allocator;

    var pool: Extra(u32, .{ .growable = false }) = try .initCapacity(a, 4);
    defer pool.deinit(a);

    _ = try pool.create(a);
    _ = try pool.create(a);
    _ = try pool.create(a);
    _ = try pool.create(a);

    try std.testing.expectError(error.OutOfMemory, pool.create(a));
}

test "greater than pointer default alignment" {
    const Foo = struct {
        data: u64 align(16),
    };
    const a = std.testing.allocator;

    var pool: MemoryPool(Foo) = .empty;
    defer pool.deinit(a);

    const foo: *Foo = try pool.create(a);
    pool.destroy(foo);
}

test "greater than pointer manual alignment" {
    const Foo = struct {
        data: u64,
    };
    const a = std.testing.allocator;

    var pool: Aligned(Foo, .@"16") = .empty;
    defer pool.deinit(a);

    const foo: *align(16) Foo = try pool.create(a);
    pool.destroy(foo);
}
