const std = @import("std");
const builtin = @import("builtin");
const os = std.os;
const mem = std.mem;
const math = std.math;
const fs = std.fs;
const assert = std.debug.assert;
const Allocator = mem.Allocator;
const wasi = std.os.wasi;
const fd_t = wasi.fd_t;
const prestat_t = wasi.prestat_t;
pub const PreopenTypeTag = enum {
    Dir,
};
pub const PreopenType = union(PreopenTypeTag) {
    
    Dir: []const u8,
    const Self = @This();
    pub fn eql(self: Self, other: PreopenType) bool {
        if (std.meta.activeTag(self) != std.meta.activeTag(other)) return false;
        switch (self) {
            PreopenTypeTag.Dir => |this_path| return mem.eql(u8, this_path, other.Dir),
        }
    }
    
    
    
    
    pub fn getRelativePath(self: Self, other: PreopenType) ?[]const u8 {
        if (std.meta.activeTag(self) != std.meta.activeTag(other)) return null;
        switch (self) {
            PreopenTypeTag.Dir => |self_path| {
                const other_path = other.Dir;
                if (mem.indexOfDiff(u8, self_path, other_path)) |index| {
                    if (index < self_path.len) return null;
                }
                const rel_path = other_path[self_path.len..];
                if (rel_path.len == 0) {
                    return rel_path;
                } else if (rel_path[0] == '/') {
                    return rel_path[1..];
                } else {
                    if (self_path[self_path.len - 1] != '/') return null;
                    return rel_path;
                }
            },
        }
    }
    pub fn format(self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype) !void {
        _ = fmt;
        _ = options;
        try out_stream.print("PreopenType{{ ", .{});
        switch (self) {
            PreopenType.Dir => |path| try out_stream.print(".Dir = '{}'", .{std.zig.fmtId(path)}),
        }
        return out_stream.print(" }}", .{});
    }
};
pub const Preopen = struct {
    
    fd: fd_t,
    
    @"type": PreopenType,
    
    pub fn new(fd: fd_t, preopen_type: PreopenType) Preopen {
        return Preopen{
            .fd = fd,
            .@"type" = preopen_type,
        };
    }
};
pub const PreopenUri = struct {
    
    base: Preopen,
    
    relative_path: []const u8,
};
pub const PreopenList = struct {
    const InnerList = std.ArrayList(Preopen);
    
    buffer: InnerList,
    const Self = @This();
    pub const Error = error{ OutOfMemory, Overflow } || os.UnexpectedError;
    
    pub fn init(allocator: Allocator) Self {
        return Self{ .buffer = InnerList.init(allocator) };
    }
    
    pub fn deinit(pm: Self) void {
        for (pm.buffer.items) |preopen| {
            switch (preopen.@"type") {
                PreopenType.Dir => |path| pm.buffer.allocator.free(path),
            }
        }
        pm.buffer.deinit();
    }
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    pub fn populate(self: *Self, cwd_root: ?[]const u8) Error!void {
        if (cwd_root) |root| assert(fs.path.isAbsolute(root));
        
        for (self.toOwnedSlice()) |preopen| {
            switch (preopen.@"type") {
                PreopenType.Dir => |path| self.buffer.allocator.free(path),
            }
        }
        errdefer self.deinit();
        var fd: fd_t = 3; 
        var path_buf: [fs.MAX_PATH_BYTES]u8 = undefined;
        while (true) {
            var buf: prestat_t = undefined;
            switch (wasi.fd_prestat_get(fd, &buf)) {
                .SUCCESS => {},
                .OPNOTSUPP => {
                    
                    fd = try math.add(fd_t, fd, 1);
                    continue;
                },
                .BADF => {
                    
                    break;
                },
                else => |err| return os.unexpectedErrno(err),
            }
            const preopen_len = buf.u.dir.pr_name_len;
            mem.set(u8, path_buf[0..preopen_len], 0);
            switch (wasi.fd_prestat_dir_name(fd, &path_buf, preopen_len)) {
                .SUCCESS => {},
                else => |err| return os.unexpectedErrno(err),
            }
            
            
            const raw_path = if (path_buf[preopen_len - 1] == 0) blk: {
                break :blk path_buf[0 .. preopen_len - 1];
            } else path_buf[0..preopen_len];
            
            
            const path = if (cwd_root) |cwd| blk: {
                const resolve_paths: []const []const u8 = if (raw_path[0] == '.') &.{ cwd, raw_path } else &.{ "/", raw_path };
                break :blk fs.path.resolve(self.buffer.allocator, resolve_paths) catch |err| switch (err) {
                    error.CurrentWorkingDirectoryUnlinked => unreachable, 
                    else => |e| return e,
                };
            } else blk: {
                
                break :blk try self.buffer.allocator.dupe(u8, raw_path);
            };
            errdefer self.buffer.allocator.free(path);
            const preopen = Preopen.new(fd, .{ .Dir = path });
            try self.buffer.append(preopen);
            fd = try math.add(fd_t, fd, 1);
        }
    }
    
    
    
    
    pub fn findContaining(self: Self, preopen_type: PreopenType) ?PreopenUri {
        var best_match: ?PreopenUri = null;
        for (self.buffer.items) |preopen| {
            if (preopen.@"type".getRelativePath(preopen_type)) |rel_path| {
                if (best_match == null or rel_path.len <= best_match.?.relative_path.len) {
                    best_match = PreopenUri{
                        .base = preopen,
                        .relative_path = if (rel_path.len == 0) "." else rel_path,
                    };
                }
            }
        }
        return best_match;
    }
    
    
    pub fn findByFd(self: Self, fd: fd_t) ?Preopen {
        for (self.buffer.items) |preopen| {
            if (preopen.fd == fd) {
                return preopen;
            }
        }
        return null;
    }
    
    
    pub fn find(self: Self, preopen_type: PreopenType) ?*const Preopen {
        for (self.buffer.items) |*preopen| {
            if (preopen.@"type".eql(preopen_type)) {
                return preopen;
            }
        }
        return null;
    }
    
    pub fn asSlice(self: Self) []const Preopen {
        return self.buffer.items;
    }
    
    pub fn toOwnedSlice(self: *Self) []Preopen {
        return self.buffer.toOwnedSlice();
    }
};
test "extracting WASI preopens" {
    if (builtin.os.tag != .wasi or builtin.link_libc) return error.SkipZigTest;
    var preopens = PreopenList.init(std.testing.allocator);
    defer preopens.deinit();
    try preopens.populate(null);
    const preopen = preopens.find(PreopenType{ .Dir = "." }) orelse unreachable;
    try std.testing.expect(preopen.@"type".eql(PreopenType{ .Dir = "." }));
    const po_type1 = PreopenType{ .Dir = "/" };
    try std.testing.expect(std.mem.eql(u8, po_type1.getRelativePath(.{ .Dir = "/" }).?, ""));
    try std.testing.expect(std.mem.eql(u8, po_type1.getRelativePath(.{ .Dir = "/test/foobar" }).?, "test/foobar"));
    const po_type2 = PreopenType{ .Dir = "/test/foo" };
    try std.testing.expect(po_type2.getRelativePath(.{ .Dir = "/test/foobar" }) == null);
    const po_type3 = PreopenType{ .Dir = "/test" };
    try std.testing.expect(std.mem.eql(u8, po_type3.getRelativePath(.{ .Dir = "/test" }).?, ""));
    try std.testing.expect(std.mem.eql(u8, po_type3.getRelativePath(.{ .Dir = "/test/" }).?, ""));
    try std.testing.expect(std.mem.eql(u8, po_type3.getRelativePath(.{ .Dir = "/test/foo/bar" }).?, "foo/bar"));
}