const std = @import("std");
const os = std.os;
const mem = std.mem;
const fmt = std.fmt;
const meta = std.meta;
const maxInt = std.math.maxInt;
const Allocator = mem.Allocator;
const ArrayList = std.ArrayList;
const Group = @This();

gid: u32,
name: []const u8,
members: []align(1) const []const u8,

// storage of name, members and members strings. for no particular reason.
// Everything (name, members, member strings) could be allocated separately
// (one extreme) and only once (another extreme). For some reason I chose the
// right extreme, but it doesn't have to be this way.
_buf: []const u8,

pub fn init(
    allocator: Allocator,
    gid: u32,
    name: []const u8,
    members: []align(1) const []const u8,
) error{OutOfMemory}!Group {
    const buf_len = @sizeOf([]const u8) * members.len + name.len + blk: {
        var sum: usize = 0;
        for (members) |member| sum += member.len;
        break :blk @intCast(u32, sum);
    };
    var buf = try allocator.alloc(u8, buf_len);
    errdefer allocator.free(buf);
    var ptr_end = @sizeOf([]const u8) * members.len;
    var members_ptr = mem.bytesAsSlice([]const u8, buf[0..ptr_end]);
    var offset: usize = ptr_end;
    for (members, members_ptr) |member, *members_ptr_i| {
        mem.copy(u8, buf[offset..], member);
        members_ptr_i.* = buf[offset .. offset + member.len];
        offset += member.len;
    }
    mem.copy(u8, buf[offset..], name);
    return Group{
        .gid = gid,
        .name = buf[offset .. offset + name.len],
        .members = members_ptr,
        ._buf = buf,
    };
}

// This could be made more efficient, but clone() is never in the hot path.
pub fn clone(self: *const Group, allocator: Allocator) error{OutOfMemory}!Group {
    return init(allocator, self.gid, self.name, self.members);
}

pub fn deinit(self: *Group, allocator: Allocator) void {
    allocator.free(self._buf);
    self.* = undefined;
}

pub const FromReaderError = error{ InvalidRecord, OutOfMemory } || os.ReadError;

pub fn fromReader(allocator: Allocator, reader: anytype) FromReaderError![]Group {
    var groups = ArrayList(Group).init(allocator);
    errdefer {
        for (groups.items) |*group| group.deinit(allocator);
        groups.deinit();
    }

    var member_ptrs = ArrayList([]const u8).init(allocator);
    defer member_ptrs.deinit();
    var line = ArrayList(u8).init(allocator);
    defer line.deinit();

    var i: usize = 0;
    while (true) : (i += 1) {
        const max = std.math.maxInt(u32);
        reader.readUntilDelimiterArrayList(&line, '\n', max) catch |err| switch (err) {
            error.EndOfStream => break,
            error.StreamTooLong => unreachable,
            else => |e| return e,
        };

        var it = mem.split(u8, line.items, ":");
        const name = it.next() orelse return error.InvalidRecord;
        _ = it.next() orelse return error.InvalidRecord; // password
        const gids = it.next() orelse return error.InvalidRecord;
        const members_commas = it.next() orelse return error.InvalidRecord;
        if (it.next() != null) return error.InvalidRecord;

        const gid = fmt.parseInt(u32, gids, 10) catch return error.InvalidRecord;

        var members_it = mem.split(u8, members_commas, ",");
        while (members_it.next()) |member|
            if (member.len != 0)
                try member_ptrs.append(member);

        const group = try init(allocator, gid, name, member_ptrs.items);
        try groups.append(group);
        member_ptrs.shrinkRetainingCapacity(0);
    }

    return groups.toOwnedSlice();
}

pub fn writeTo(self: *const Group, writer: anytype) os.WriteError!void {
    try writer.print("{s}:x:{d}:", .{ self.name, self.gid });
    if (self.members.len != 0) {
        try writer.writeAll(self.members[0]);
        for (self.members[1..]) |member|
            try writer.print(",{s}", .{member});
    }
    try writer.writeByte('\n');
}

// suggested buffer size in bytes if all strings were zero-terminated
// (for CGroup).
pub fn strlenZ(self: *const Group) usize {
    var count: usize = 0;
    for (self.members) |member|
        count += member.len + 1;
    count += ptr_size * (self.members.len + 1);
    count += self.name.len + 1;
    return count;
}

pub const CGroup = extern struct {
    gr_name: [*:0]const u8,
    gr_passwd: [*:0]const u8 = "x",
    gr_gid: u32,
    //gr_mem: [*:]align(1) const ?[*:0]const u8,
    gr_mem: [*:null]align(1) const ?[*:0]const u8,
};

// size of the pointer to a single member.
pub const ptr_size = @sizeOf(meta.Child(meta.fieldInfo(CGroup, .gr_mem).type));

const testing = std.testing;

test "Group clone" {
    // TODO: how to do this on stack?
    var member1 = mem.sliceTo(try testing.allocator.dupeZ(u8, "member1"), 0);
    defer testing.allocator.free(member1);

    var group = try init(testing.allocator, 1, "foo", &[_][]const u8{ member1, "member2" });
    defer group.deinit(testing.allocator);

    var cloned = try group.clone(testing.allocator);
    defer cloned.deinit(testing.allocator);

    group.gid = 2;
    group.name = "bar";
    member1[0] = 'x';
    try testing.expectEqual(cloned.gid, 1);
    try testing.expectEqualSlices(u8, cloned.name, "foo");
    try testing.expectEqualSlices(u8, cloned.members[0], "member1");
}
