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

///
/// Potential errors that may occur while trying to push one or more elements into a stack of a
/// known maximum size.
///
/// [FinitePushError.Overflow] is returned if the stack does not have sufficient capacity to hold a
/// given set of elements.
///
pub const FinitePushError = error {
    Overflow,
};

///
/// Returns a fixed-size stack collection capable of holding a maximum of `capacity` elements of
/// type `Element`.
///
pub fn Fixed(comptime Element: type, comptime capacity: usize) type {
    return struct {
        filled: usize,
        buffer: [capacity]Element,

        const Self = @This();

        ///
        /// Wraps `self` and returns it in a [io.Writer] value.
        ///
        /// Note that this will raise a compilation error if [Element] is not `u8`.
        ///
        pub fn asWriter(self: *Self) io.Writer {
            if (Element != u8) @compileError("Cannot coerce fixed stack of type " ++
                @typeName(Element) ++ " into a Writer");

            return io.Writer.wrap(Self, self, struct {
                fn write(stack: *Self, buffer: []const u8) usize {
                    stack.pushAll(buffer) catch |err| switch (err) {
                        error.Overflow => return 0,
                    };

                    return buffer.len;
                }
            }.write);
        }

        ///
        /// Clears all elements from `self`.
        ///
        pub fn clear(self: *Self) void {
            self.filled = 0;
        }

        ///
        /// Counts and returns the number of pushed elements in `self`.
        ///
        pub fn count(self: Self) usize {
            return self.filled;
        }

        ///
        /// Creates and returns a [Self] value.
        ///
        pub fn init() Self {
            return Self{
                .filled = 0,
                .buffer = undefined,
            };
        }

        ///
        /// Attempts to pop the tail-end of `self`, returning the element value or `null` if the
        /// stack is empty.
        ///
        pub fn pop(self: *Self) ?Element {
            if (self.filled == 0) return null;

            self.filled -= 1;

            return self.buffer[self.filled];
        }

        ///
        /// Attempts to push `element` into `self`, returning [FinitePushError.Overflow] if the
        /// stack is full.
        ///
        pub fn push(self: *Self, element: Element) FinitePushError!void {
            if (self.filled == capacity) return error.Overflow;

            self.buffer[self.filled] = element;
            self.filled += 1;
        }

        ///
        /// Attempts to push all of `elements` into `self`, returning [FinitePushError.Overflow] if
        /// the stack does not have sufficient capacity to hold the new elements.
        ///
        pub fn pushAll(self: *Self, elements: []const u8) FinitePushError!void {
            const filled = (self.filled + elements.len);

            if (filled > capacity) return error.Overflow;

            std.mem.copy(u8, self.buffer[self.filled ..], elements);

            self.filled = filled;
        }
    };
}

pub fn Unmanaged(comptime Element: type) type {
    return struct {
        filled: usize = 0,
        buffer: []Element,

        const Self = @This();

        ///
        /// Wraps `self` and returns it in a [io.Writer] value.
        ///
        /// Note that this will raise a compilation error if [Element] is not `u8`.
        ///
        pub fn asWriter(self: *Self) io.Writer {
            if (Element != u8) @compileError("Cannot coerce fixed stack of type " ++
                @typeName(Element) ++ " into a Writer");

            return io.Writer.wrap(Self, self, struct {
                fn write(stack: *Self, buffer: []const u8) usize {
                    stack.pushAll(buffer) catch |err| switch (err) {
                        error.Overflow => return 0,
                    };

                    return buffer.len;
                }
            }.write);
        }

        ///
        /// Clears all elements from `self`.
        ///
        pub fn clear(self: *Self) void {
            self.filled = 0;
        }

        ///
        /// Counts and returns the number of pushed elements in `self`.
        ///
        pub fn count(self: Self) usize {
            return self.filled;
        }

        ///
        /// Creates and returns a [Self] value wrapping `buffer` as its writable memory buffer.
        ///
        pub fn init(buffer: []Element) Self {
            return Self{
                .filled = 0,
                .buffer = buffer,
            };
        }

        ///
        /// Attempts to pop the tail-end of `self`, returning the element value or `null` if the
        /// stack is empty.
        ///
        pub fn pop(self: *Self) ?Element {
            if (self.filled == 0) return null;

            self.filled -= 1;

            return self.buffer[self.filled];
        }

        ///
        /// Attempts to push `element` into `self`, returning [FinitePushError.Overflow] if the
        /// stack is full.
        ///
        pub fn push(self: *Self, element: Element) FinitePushError!void {
            if (self.filled == self.buffer.len) return error.Overflow;

            self.buffer[self.filled] = element;
            self.filled += 1;
        }

        ///
        /// Attempts to push all of `elements` into `self`, returning [FinitePushError.Overflow] if
        /// the stack does not have sufficient capacity to hold the new elements.
        ///
        pub fn pushAll(self: *Self, elements: []const u8) FinitePushError!void {
            const filled = (self.filled + elements.len);

            if (filled > self.buffer.len) return error.Overflow;

            std.mem.copy(u8, self.buffer[self.filled ..], elements);

            self.filled = filled;
        }
    };
}

test "fixed stack" {
    const testing = @import("std").testing;
    const expectError = testing.expectError;
    const expectEqual = testing.expectEqual;
    var stack = Fixed(u8, 4){};

    try expectEqual(stack.count(), 0);
    try expectEqual(stack.pop(), null);
    try stack.push(69);
    try expectEqual(stack.count(), 1);
    try expectEqual(stack.pop(), 69);
    try stack.pushAll(&.{42, 10, 95, 0});
    try expectEqual(stack.count(), 4);
    try expectError(FinitePushError.Overflow, stack.push(1));
    try expectError(FinitePushError.Overflow, stack.pushAll(&.{1, 11, 11}));

    stack.clear();

    try expectEqual(stack.count(), 0);

    const writer = stack.asWriter();

    try expectEqual(writer.write(&.{0, 0, 0, 0}), 4);
    try expectEqual(writer.writeByte(0), false);
}