// ErrCtx is a way to pass friendly error messages to the user.
// Preallocates memory. Discards messages that do not fit.

const std = @import("std");
const mem = std.mem;
const math = std.math;
const BoundedArray = std.BoundedArray;

const assert = std.debug.assert;
const capacity = 1 << 16; // 64K
const ErrCtx = @This();

buf: BoundedArray(u8, capacity) =
    BoundedArray(u8, capacity).init(0) catch unreachable,
overflow: bool = false,
dirty: bool = false,

pub fn write(self: *ErrCtx, bytes: []const u8) error{}!usize {
    self.dirty = true;
    const can_add = capacity - self.buf.len;
    if (can_add == 0) return bytes.len;

    self.overflow = bytes.len > can_add;
    self.buf.appendSliceAssumeCapacity(bytes[0..@min(bytes.len, can_add)]);
    // not adding the final zero is ok, because it will
    // be ignored in the iterator anyway.
    _ = self.buf.append(0) catch null;

    return bytes.len;
}

const Writer = std.io.Writer(*ErrCtx, error{}, write);

// writer is private, because fmt.print(...) can do multiple write(...) calls
// for a single fmt.print(...). It is too easy to mis-use; therefore use the
// wrappers below.
fn writer(self: *ErrCtx) Writer {
    return Writer{ .context = self };
}

pub fn print(self: *ErrCtx, comptime format: []const u8, args: anytype) void {
    var buf: [capacity]u8 = undefined;
    var buf_stream = std.io.fixedBufferStream(&buf);
    buf_stream.writer().print(format, args) catch |err| switch (err) {
        error.NoSpaceLeft => {},
    };

    _ = self.write(buf[0..buf_stream.pos]) catch unreachable;
}

pub fn returnf(
    self: *ErrCtx,
    comptime format: []const u8,
    args: anytype,
    comptime ret: anytype,
) @TypeOf(ret) {
    self.print(format, args);
    return ret;
}

pub fn wrap(self: *ErrCtx, comptime msg: []const u8) *ErrCtx {
    self.writer().writeAll(msg) catch unreachable;
    return self;
}

pub fn wrapf(self: *ErrCtx, comptime format: []const u8, args: anytype) *ErrCtx {
    self.print(format, args);
    return self;
}

pub fn iterator(self: *const ErrCtx) mem.SplitIterator(u8, .scalar) {
    const slice = self.buf.constSlice();
    const last_byte = if (slice[slice.len - 1] == 0) slice.len - 1 else slice.len;
    return mem.splitScalar(u8, slice[0..last_byte], 0);
}

pub fn rev(self: *const ErrCtx) mem.SplitBackwardsIterator(u8, .scalar) {
    const slice = self.buf.constSlice();
    if (slice.len == 0) {
        return mem.SplitBackwardsIterator(u8, .scalar){
            .buffer = slice,
            .index = null,
            .delimiter = 0,
        };
    }

    const last_byte = if (slice[slice.len - 1] == 0) (slice.len - 1) else slice.len;
    return mem.splitBackwardsScalar(u8, slice[0..last_byte], 0);
}

pub fn unwrap(self: *const ErrCtx) BoundedArray(u8, capacity * 2) {
    var result = BoundedArray(u8, capacity * 2).init(0) catch unreachable;
    var wr = result.writer();

    var it = self.rev();
    if (it.next()) |msg| {
        wr.print("{s}", .{msg}) catch unreachable;
    } else return result;

    while (it.next()) |msg|
        wr.print(": {s}", .{msg}) catch unreachable;
    return result;
}

const testing = std.testing;

test "ErrCtx basics" {
    var ctx = ErrCtx{};
    var wr = ctx.writer();

    try wr.writeAll("0" ** 10);
    try wr.writeAll("1" ** 10);
    try wr.writeAll("3" ** capacity);
    try wr.writeAll("foo");

    var it = ctx.iterator();
    try testing.expectEqualSlices(u8, it.next().?, "0" ** 10);
    try testing.expectEqualSlices(u8, it.next().?, "1" ** 10);

    const long = it.next().?;
    try testing.expectEqual(@as(usize, capacity - 2 - 20), long.len);
    try testing.expectEqual(it.next(), null);
    try testing.expect(ctx.overflow);

    var it_rev = ctx.rev();
    try testing.expectEqualSlices(u8, it_rev.next().?, "3" ** (capacity - 22));
    try testing.expectEqualSlices(u8, it_rev.next().?, "1" ** 10);
    try testing.expectEqualSlices(u8, it_rev.next().?, "0" ** 10);
}

test "ErrCtx almost overflow" {
    var ctx = ErrCtx{};
    var wr = ctx.writer();

    try wr.writeAll("0" ** (capacity - 3));
    try wr.writeAll("11");

    var it = ctx.iterator();
    try testing.expectEqualSlices(u8, it.next().?, "0" ** (capacity - 3));
    try testing.expectEqualSlices(u8, it.next().?, "11");
    try testing.expectEqual(it.next(), null);

    var it_rev = ctx.rev();
    try testing.expectEqualSlices(u8, it_rev.next().?, "11");
    try testing.expectEqualSlices(u8, it_rev.next().?, "0" ** (capacity - 3));
    try testing.expectEqual(it.next(), null);
}

test "ErrCtx rev" {
    var ctx = ErrCtx{};
    ctx.print("yadda {s}", .{"xx"});

    var it = ctx.rev();
    try testing.expectEqualStrings("yadda xx", it.next().?);
    try testing.expectEqual(it.next(), null);
}
