//! Parser for transfer-encoding: chunked.

const ChunkParser = @This();
const std = @import("std");

state: State,
chunk_len: u64,

pub const init: ChunkParser = .{
    .state = .head_size,
    .chunk_len = 0,
};

pub const State = enum {
    head_size,
    head_ext,
    head_r,
    data,
    data_suffix,
    data_suffix_r,
    invalid,
};

/// Returns the number of bytes consumed by the chunk size. This is always
/// less than or equal to `bytes.len`.
///
/// After this function returns, `chunk_len` will contain the parsed chunk size
/// in bytes when `state` is `data`. Alternately, `state` may become `invalid`,
/// indicating a syntax error in the input stream.
///
/// If the amount returned is less than `bytes.len`, the parser is in the
/// `chunk_data` state and the first byte of the chunk is at `bytes[result]`.
///
/// Asserts `state` is neither `data` nor `invalid`.
pub fn feed(p: *ChunkParser, bytes: []const u8) usize {
    for (bytes, 0..) |c, i| switch (p.state) {
        .data_suffix => switch (c) {
            '\r' => p.state = .data_suffix_r,
            '\n' => p.state = .head_size,
            else => {
                p.state = .invalid;
                return i;
            },
        },
        .data_suffix_r => switch (c) {
            '\n' => p.state = .head_size,
            else => {
                p.state = .invalid;
                return i;
            },
        },
        .head_size => {
            const digit = switch (c) {
                '0'...'9' => |b| b - '0',
                'A'...'Z' => |b| b - 'A' + 10,
                'a'...'z' => |b| b - 'a' + 10,
                '\r' => {
                    p.state = .head_r;
                    continue;
                },
                '\n' => {
                    p.state = .data;
                    return i + 1;
                },
                else => {
                    p.state = .head_ext;
                    continue;
                },
            };

            const new_len = p.chunk_len *% 16 +% digit;
            if (new_len <= p.chunk_len and p.chunk_len != 0) {
                p.state = .invalid;
                return i;
            }

            p.chunk_len = new_len;
        },
        .head_ext => switch (c) {
            '\r' => p.state = .head_r,
            '\n' => {
                p.state = .data;
                return i + 1;
            },
            else => continue,
        },
        .head_r => switch (c) {
            '\n' => {
                p.state = .data;
                return i + 1;
            },
            else => {
                p.state = .invalid;
                return i;
            },
        },
        .data => unreachable,
        .invalid => unreachable,
    };
    return bytes.len;
}

test feed {
    const testing = std.testing;

    const data = "Ff\r\nf0f000 ; ext\n0\r\nffffffffffffffffffffffffffffffffffffffff\r\n";

    var p = init;
    const first = p.feed(data[0..]);
    try testing.expectEqual(@as(u32, 4), first);
    try testing.expectEqual(@as(u64, 0xff), p.chunk_len);
    try testing.expectEqual(.data, p.state);

    p = init;
    const second = p.feed(data[first..]);
    try testing.expectEqual(@as(u32, 13), second);
    try testing.expectEqual(@as(u64, 0xf0f000), p.chunk_len);
    try testing.expectEqual(.data, p.state);

    p = init;
    const third = p.feed(data[first + second ..]);
    try testing.expectEqual(@as(u32, 3), third);
    try testing.expectEqual(@as(u64, 0), p.chunk_len);
    try testing.expectEqual(.data, p.state);

    p = init;
    const fourth = p.feed(data[first + second + third ..]);
    try testing.expectEqual(@as(u32, 16), fourth);
    try testing.expectEqual(@as(u64, 0xffffffffffffffff), p.chunk_len);
    try testing.expectEqual(.invalid, p.state);
}
