const Limited = @This();

const std = @import("../../std.zig");
const Reader = std.Io.Reader;
const Writer = std.Io.Writer;
const Limit = std.Io.Limit;

unlimited: *Reader,
remaining: Limit,
interface: Reader,

pub fn init(reader: *Reader, limit: Limit, buffer: []u8) Limited {
    return .{
        .unlimited = reader,
        .remaining = limit,
        .interface = .{
            .vtable = &.{
                .stream = stream,
                .discard = discard,
            },
            .buffer = buffer,
            .seek = 0,
            .end = 0,
        },
    };
}

fn stream(r: *Reader, w: *Writer, limit: Limit) Reader.StreamError!usize {
    const l: *Limited = @fieldParentPtr("interface", r);
    if (l.remaining == .nothing) return error.EndOfStream;
    const combined_limit = limit.min(l.remaining);
    const n = try l.unlimited.stream(w, combined_limit);
    l.remaining = l.remaining.subtract(n).?;
    return n;
}

test stream {
    var orig_buf: [10]u8 = undefined;
    @memcpy(&orig_buf, "test bytes");
    var fixed: std.Io.Reader = .fixed(&orig_buf);

    var limit_buf: [1]u8 = undefined;
    var limited: std.Io.Reader.Limited = .init(&fixed, @fromBackingInt(@intCast(4)), &limit_buf);

    var result_buf: [10]u8 = undefined;
    var fixed_writer: std.Io.Writer = .fixed(&result_buf);
    const streamed = try limited.interface.stream(&fixed_writer, @fromBackingInt(@intCast(7)));

    try std.testing.expect(streamed == 4);
    try std.testing.expectEqualStrings("test", result_buf[0..streamed]);
}

fn discard(r: *Reader, limit: Limit) Reader.Error!usize {
    const l: *Limited = @fieldParentPtr("interface", r);
    if (l.remaining == .nothing) return error.EndOfStream;
    const combined_limit = limit.min(l.remaining);
    const n = try l.unlimited.discard(combined_limit);
    l.remaining = l.remaining.subtract(n).?;
    return n;
}

test "end of stream, read, hit limit exactly" {
    var f: Reader = .fixed("i'm dying");
    var l = f.limited(.limited(4), &.{});
    const r = &l.interface;

    var buf: [2]u8 = undefined;
    try r.readSliceAll(&buf);
    try r.readSliceAll(&buf);
    try std.testing.expectError(error.EndOfStream, l.interface.readSliceAll(&buf));
}

test "end of stream, read, hit limit after partial read" {
    var f: Reader = .fixed("i'm dying");
    var l = f.limited(.limited(5), &.{});
    const r = &l.interface;

    var buf: [2]u8 = undefined;
    try r.readSliceAll(&buf);
    try r.readSliceAll(&buf);
    try std.testing.expectError(error.EndOfStream, l.interface.readSliceAll(&buf));
}

test "end of stream, discard, hit limit exactly" {
    var f: Reader = .fixed("i'm dying");
    var l = f.limited(.limited(4), &.{});
    const r = &l.interface;

    try r.discardAll(2);
    try r.discardAll(2);
    try std.testing.expectError(error.EndOfStream, l.interface.discardAll(2));
}

test "end of stream, discard, hit limit after partial read" {
    var f: Reader = .fixed("i'm dying");
    var l = f.limited(.limited(5), &.{});
    const r = &l.interface;

    try r.discardAll(2);
    try r.discardAll(2);
    try std.testing.expectError(error.EndOfStream, l.interface.discardAll(2));
}
