const std = @import("std");
const builtin = @import("builtin");

const testing = std.testing;
const Allocator = std.mem.Allocator;

// ustar tar implementation
pub const Header = extern struct {
    name: [100]u8,
    mode: [7:0]u8,
    uid: [7:0]u8,
    gid: [7:0]u8,
    size: [11:0]u8,
    mtime: [11:0]u8,
    checksum: [7:0]u8,
    typeflag: FileType,
    linkname: [100]u8,
    magic: [5:0]u8,
    version: [2]u8,
    uname: [31:0]u8,
    gname: [31:0]u8,
    devmajor: [7:0]u8,
    devminor: [7:0]u8,
    prefix: [155]u8,
    pad: [12]u8,

    const Self = @This();

    const FileType = enum(u8) {
        regular = '0',
        hard_link = '1',
        symbolic_link = '2',
        character = '3',
        block = '4',
        directory = '5',
        fifo = '6',
        reserved = '7',
        pax_global = 'g',
        extended = 'x',
        _,
    };

    fn init() Self {
        var ret = std.mem.zeroes(Self);
        ret.magic = [_:0]u8{ 'u', 's', 't', 'a', 'r' };
        ret.version = [_:0]u8{ '0', '0' };
        return ret;
    }

    fn setPath(self: *Self, path: []const u8) !void {
        if (path.len > 100) {
            var i: usize = 0;
            while (i < path.len and path.len - i > 100) {
                while (path[i] != '/') : (i += 1) {}
            }

            _ = try std.fmt.bufPrint(&self.prefix, "{s}", .{path[0..i]});
            _ = try std.fmt.bufPrint(&self.name, "{s}", .{path[i + 1 ..]});
        } else {
            _ = try std.fmt.bufPrint(&self.name, "{s}", .{path});
        }
    }

    fn setSize(self: *Self, size: u64) !void {
        _ = try std.fmt.bufPrint(&self.size, "{o:0>11}", .{size});
    }

    fn setMtime(self: *Self, mtime: u32) !void {
        _ = try std.fmt.bufPrint(&self.mtime, "{o:0>11}", .{mtime});
    }

    fn setMode(self: *Self, filetype: FileType, perm: u9) !void {
        switch (filetype) {
            .regular => _ = try std.fmt.bufPrint(&self.mode, "0100{o:0>3}", .{perm}),
            .directory => _ = try std.fmt.bufPrint(&self.mode, "0040{o:0>3}", .{perm}),
            else => return error.Unsupported,
        }
    }

    fn setUid(self: *Self, uid: u32) !void {
        _ = try std.fmt.bufPrint(&self.uid, "{o:0>7}", .{uid});
    }

    fn setGid(self: *Self, gid: u32) !void {
        _ = try std.fmt.bufPrint(&self.gid, "{o:0>7}", .{gid});
    }

    fn updateChecksum(self: *Self) !void {
        const offset = @offsetOf(Self, "checksum");
        var checksum: usize = 0;
        for (std.mem.asBytes(self)) |val, i| {
            checksum += if (i >= offset and i < offset + @sizeOf(@TypeOf(self.checksum)))
                ' '
            else
                val;
        }

        _ = try std.fmt.bufPrint(&self.checksum, "{o:0>7}", .{checksum});
    }

    fn fromStat(stat: std.fs.File.Stat, path: []const u8) !Header {
        if (std.mem.indexOf(u8, path, "\\") != null) return error.NeedPosixPath;
        if (std.fs.path.isAbsolute(path)) return error.NeedRelPath;

        var ret = Self.init();
        ret.typeflag = switch (stat.kind) {
            .File => .regular,
            .Directory => .directory,
            else => return error.UnsupportedType,
        };

        try ret.setPath(path);
        try ret.setSize(stat.size);
        try ret.setMtime(@truncate(u32, @bitCast(u128, @divTrunc(stat.mtime, std.time.ns_per_s))));
        try ret.setMode(ret.typeflag, @truncate(u9, stat.mode));

        try ret.setUid(0);
        try ret.setGid(0);

        std.mem.copy(u8, &ret.uname, "root");
        std.mem.copy(u8, &ret.gname, "root");

        try ret.updateChecksum();
        return ret;
    }

    pub fn isBlank(self: *const Header) bool {
        const block = std.mem.asBytes(self);
        return for (block) |elem| {
            if (elem != 0) break false;
        } else true;
    }
};

test "Header size" {
    try testing.expectEqual(512, @sizeOf(Header));
}

pub fn instantiate(
    allocator: Allocator,
    dir: std.fs.Dir,
    reader: anytype,
    skip_depth: usize,
) !void {
    var count: usize = 0;
    while (true) {
        const header = reader.readStruct(Header) catch |err| {
            return if (err == error.EndOfStream)
                if (count < 2) error.AbrubtEnd else break
            else
                err;
        };

        if (header.isBlank()) {
            count += 1;
            continue;
        } else if (count > 0) {
            return error.Format;
        }

        var size = try std.fmt.parseUnsigned(usize, &header.size, 8);
        const block_size = ((size + 511) / 512) * 512;
        var components = std.ArrayList([]const u8).init(allocator);
        defer components.deinit();

        var path_it = std.mem.tokenize(u8, &header.prefix, "/\x00");
        if (header.prefix[0] != 0) {
            while (path_it.next()) |component| {
                try components.append(component);
            }
        }

        path_it = std.mem.tokenize(u8, &header.name, "/\x00");
        while (path_it.next()) |component| {
            try components.append(component);
        }

        const tmp_path = try std.fs.path.join(allocator, components.items);
        defer allocator.free(tmp_path);

        if (skip_depth >= components.items.len) {
            try reader.skipBytes(block_size, .{});
            continue;
        }

        var i: usize = 0;
        while (i < skip_depth) : (i += 1) {
            _ = components.orderedRemove(0);
        }

        const file_path = try std.fs.path.join(allocator, components.items);
        defer allocator.free(file_path);

        switch (header.typeflag) {
            .directory => try dir.makePath(file_path),
            .pax_global => try reader.skipBytes(512, .{}),
            .regular => {
                const file = try dir.createFile(file_path, .{ .read = true, .truncate = true });
                defer file.close();
                const skip_size = block_size - size;

                var buf: [std.mem.page_size]u8 = undefined;
                while (size > 0) {
                    const buffered = try reader.read(buf[0..std.math.min(size, 512)]);
                    try file.writeAll(buf[0..buffered]);
                    size -= buffered;
                }

                try reader.skipBytes(skip_size, .{});
            },
            else => {},
        }
    }
}

pub fn builder(allocator: Allocator, writer: anytype) Builder(@TypeOf(writer)) {
    return Builder(@TypeOf(writer)).init(allocator, writer);
}

pub fn Builder(comptime Writer: type) type {
    return struct {
        writer: Writer,
        arena: std.heap.ArenaAllocator,
        directories: std.StringHashMap(void),

        const Self = @This();

        pub fn init(allocator: Allocator, writer: Writer) Self {
            return Self{
                .arena = std.heap.ArenaAllocator.init(allocator),
                .writer = writer,
                .directories = std.StringHashMap(void).init(allocator),
            };
        }

        pub fn deinit(self: *Self) void {
            self.directories.deinit();
            self.arena.deinit();
        }

        pub fn finish(self: *Self) !void {
            try self.writer.writeByteNTimes(0, 1024);
        }

        fn maybeAddDirectories(
            self: *Self,
            path: []const u8,
        ) !void {
            var i: usize = 0;
            while (i < path.len) : (i += 1) {
                while (path[i] != '/' and i < path.len) i += 1;
                if (i >= path.len) break;
                const dirpath = try self.arena.allocator().dupe(u8, path[0..i]);
                if (self.directories.contains(dirpath)) continue else try self.directories.put(dirpath, {});

                const stat = std.fs.File.Stat{
                    .inode = undefined,
                    .size = 0,
                    .mode = switch (builtin.os.tag) {
                        .windows => 0,
                        else => 0o755,
                    },
                    .kind = .Directory,
                    .atime = undefined,
                    .mtime = std.time.nanoTimestamp(),
                    .ctime = undefined,
                };
                const allocator = self.arena.child_allocator;
                const posix_dirpath = try std.mem.replaceOwned(u8, allocator, dirpath, std.fs.path.sep_str_windows, std.fs.path.sep_str_posix);
                defer allocator.free(posix_dirpath);

                const header = try Header.fromStat(stat, posix_dirpath);
                try self.writer.writeAll(std.mem.asBytes(&header));
            }
        }

        /// prefix is a path to prepend subpath with
        pub fn addFile(
            self: *Self,
            root: std.fs.Dir,
            prefix: ?[]const u8,
            subpath: []const u8,
        ) !void {
            const allocator = self.arena.child_allocator;
            const path = if (prefix) |prefix_path|
                try std.fs.path.join(allocator, &[_][]const u8{ prefix_path, subpath })
            else
                subpath;
            defer if (prefix != null) allocator.free(path);

            const posix_path = try std.mem.replaceOwned(u8, allocator, path, std.fs.path.sep_str_windows, std.fs.path.sep_str_posix);
            defer allocator.free(posix_path);

            if (std.fs.path.dirname(posix_path)) |dirname|
                try self.maybeAddDirectories(posix_path[0 .. dirname.len + 1]);
            const subfile = try root.openFile(subpath, .{ .mode = .read_write });
            defer subfile.close();

            const stat = try subfile.stat();
            const header = try Header.fromStat(stat, posix_path);
            var buf: [std.mem.page_size]u8 = undefined;

            try self.writer.writeAll(std.mem.asBytes(&header));
            var counter = std.io.countingWriter(self.writer);

            while (true) {
                const n = try subfile.reader().read(&buf);
                if (n == 0) break;

                try counter.writer().writeAll(buf[0..n]);
            }

            const padding = blk: {
                const mod = counter.bytes_written % 512;
                break :blk if (mod > 0) 512 - mod else 0;
            };
            try self.writer.writeByteNTimes(0, @intCast(usize, padding));
        }

        /// add slice of bytes as file `path`
        pub fn addSlice(self: *Self, slice: []const u8, path: []const u8) !void {
            const allocator = self.arena.child_allocator;
            const posix_path = try std.mem.replaceOwned(u8, allocator, path, std.fs.path.sep_str_windows, std.fs.path.sep_str_posix);
            defer allocator.free(posix_path);

            const stat = std.fs.File.Stat{
                .inode = undefined,
                .size = slice.len,
                .mode = switch (builtin.os.tag) {
                    .windows => 0,
                    else => 0o644,
                },
                .kind = .File,
                .atime = undefined,
                .mtime = std.time.nanoTimestamp(),
                .ctime = undefined,
            };

            var header = try Header.fromStat(stat, posix_path);
            const padding = blk: {
                const mod = slice.len % 512;
                break :blk if (mod > 0) 512 - mod else 0;
            };
            try self.writer.writeAll(std.mem.asBytes(&header));
            try self.writer.writeAll(slice);
            try self.writer.writeByteNTimes(0, padding);
        }
    };
}

pub const PaxHeaderMap = struct {
    text: []const u8,
    map: std.StringHashMap([]const u8),

    const Self = @This();

    pub fn init(allocator: Allocator, reader: anytype) !Self {
        // TODO: header verification
        const header = try reader.readStruct(Header);
        if (header.typeflag != .pax_global) return error.NotPaxGlobalHeader;

        const size = try std.fmt.parseInt(usize, &header.size, 8);
        const text = try allocator.alloc(u8, size);
        errdefer allocator.free(text);

        var i: usize = 0;
        while (i < size) : (i = try reader.read(text[i..])) {}

        var map = std.StringHashMap([]const u8).init(allocator);
        errdefer map.deinit();

        var it = std.mem.tokenize(u8, text, "\n");
        while (it.next()) |line| {
            const begin = (std.mem.indexOf(u8, line, " ") orelse return error.BadMapEntry) + 1;
            const eql = std.mem.indexOf(u8, line[begin..], "=") orelse return error.BadMapEntry;
            try map.put(line[begin .. begin + eql], line[begin + eql + 1 ..]);
        }

        return Self{
            .text = text,
            .map = map,
        };
    }

    pub fn get(self: Self, key: []const u8) ?[]const u8 {
        return self.map.get(key);
    }

    pub fn deinit(self: *Self) void {
        self.map.allocator.free(self.text);
        self.map.deinit();
    }
};

pub fn fileExtractor(path: []const u8, reader: anytype) FileExtractor(@TypeOf(reader)) {
    return FileExtractor(@TypeOf(reader)).init(path, reader);
}

pub fn FileExtractor(comptime ReaderType: type) type {
    return struct {
        path: []const u8,
        internal: ReaderType,
        len: ?usize,

        const Self = @This();

        pub fn init(path: []const u8, internal: ReaderType) Self {
            return Self{
                .path = path,
                .internal = internal,
                .len = null,
            };
        }

        pub const Error = ReaderType.Error || error{ FileNotFound, EndOfStream } || std.fmt.ParseIntError;
        pub const Reader = std.io.Reader(*Self, Error, read);

        pub fn read(self: *Self, buf: []u8) Error!usize {
            if (self.len == null) {
                while (true) {
                    const header = try self.internal.readStruct(Header);
                    for (std.mem.asBytes(&header)) |c| {
                        if (c != 0) break;
                    } else return error.FileNotFound;
                    const size = try std.fmt.parseInt(usize, &header.size, 8);
                    const name = header.name[0 .. std.mem.indexOf(u8, &header.name, "\x00") orelse header.name.len];
                    if (std.mem.eql(u8, name, self.path)) {
                        self.len = size;
                        break;
                    } else if (size > 0) {
                        try self.internal.skipBytes(size + (512 - (size % 512)), .{});
                    }
                }
            }

            const n = try self.internal.read(buf[0..std.math.min(self.len.?, buf.len)]);
            self.len.? -= n;
            return n;
        }

        pub fn reader(self: *Self) Reader {
            return .{ .context = self };
        }
    };
}
