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

/// This allocator is used in front of another allocator and logs to the provided writer
/// on every call to the allocator. Writer errors are ignored.
pub fn LogToWriterAllocator(comptime Writer: type) type {
    return struct {
        parent_allocator: Allocator,
        writer: Writer,

        const Self = @This();

        pub fn init(parent_allocator: Allocator, writer: Writer) Self {
            return Self{
                .parent_allocator = parent_allocator,
                .writer = writer,
            };
        }

        pub fn allocator(self: *Self) Allocator {
            return Allocator.init(self, alloc, resize, free);
        }

        fn alloc(
            self: *Self,
            len: usize,
            ptr_align: u29,
            len_align: u29,
            ra: usize,
        ) error{OutOfMemory}![]u8 {
            self.writer.print("alloc : {}", .{len}) catch {};
            const result = self.parent_allocator.rawAlloc(len, ptr_align, len_align, ra);
            if (result) |_| {
                self.writer.print(" success!\n", .{}) catch {};
            } else |_| {
                self.writer.print(" failure!\n", .{}) catch {};
            }
            return result;
        }

        fn resize(
            self: *Self,
            buf: []u8,
            buf_align: u29,
            new_len: usize,
            len_align: u29,
            ra: usize,
        ) ?usize {
            if (new_len <= buf.len) {
                self.writer.print("shrink: {} to {}\n", .{ buf.len, new_len }) catch {};
            } else {
                self.writer.print("expand: {} to {}", .{ buf.len, new_len }) catch {};
            }

            if (self.parent_allocator.rawResize(buf, buf_align, new_len, len_align, ra)) |resized_len| {
                if (new_len > buf.len) {
                    self.writer.print(" success!\n", .{}) catch {};
                }
                return resized_len;
            }

            std.debug.assert(new_len > buf.len);
            self.writer.print(" failure!\n", .{}) catch {};
            return null;
        }

        fn free(
            self: *Self,
            buf: []u8,
            buf_align: u29,
            ra: usize,
        ) void {
            self.writer.print("free  : {}\n", .{buf.len}) catch {};
            self.parent_allocator.rawFree(buf, buf_align, ra);
        }
    };
}

/// This allocator is used in front of another allocator and logs to the provided writer
/// on every call to the allocator. Writer errors are ignored.
pub fn logToWriterAllocator(
    parent_allocator: Allocator,
    writer: anytype,
) LogToWriterAllocator(@TypeOf(writer)) {
    return LogToWriterAllocator(@TypeOf(writer)).init(parent_allocator, writer);
}

test "LogToWriterAllocator" {
    var log_buf: [255]u8 = undefined;
    var fbs = std.io.fixedBufferStream(&log_buf);

    var allocator_buf: [10]u8 = undefined;
    var fixedBufferAllocator = std.mem.validationWrap(std.heap.FixedBufferAllocator.init(&allocator_buf));
    var allocator_state = logToWriterAllocator(fixedBufferAllocator.allocator(), fbs.writer());
    const allocator = allocator_state.allocator();

    var a = try allocator.alloc(u8, 10);
    a = allocator.shrink(a, 5);
    try std.testing.expect(a.len == 5);
    try std.testing.expect(allocator.resize(a, 20) == null);
    allocator.free(a);

    try std.testing.expectEqualSlices(u8,
        \\alloc : 10 success!
        \\shrink: 10 to 5
        \\expand: 5 to 20 failure!
        \\free  : 5
        \\
    , fbs.getWritten());
}
