const std = #import("std");
const Allocator = std.Allocator;
const assert = std.assert;
const builtin = #import("builtin");
const current = builtin.os;
const link_libc = builtin.link_libc;

const linux = #import("os/linux.nat");
const macos = #import("os/macos.nat");
const windows = #import("os/windows.nat");

const c = std.c;

const system = switch (link_libc) {
    true => c,
    false => switch (current) {
        .linux => linux,
        .macos => c,
        .windows => windows,
    },
};

const unwrap_syscall = system.unwrap_syscall;

const exit = fn(exit_code: s32) noreturn {
    switch (current) {
        .linux, .macos => system.exit(exit_code),
        .windows => windows.ExitProcess(#cast(exit_code)),
    }
}

const max_file_operation_byte_count = switch (current) {
    .linux => 0x7ffff000,
    .macos => 0x7fffffff,
    else => #error("OS not supported"),
};

const FileDescriptor = struct{
    handle: system.FileDescriptor,

    const ReadError = error{
        failed,
    };

    const read = fn(file_descriptor: FileDescriptor, bytes: []u8) ReadError!usize {
        if (bytes.length > 0) {
            switch (current) {
                .linux, .macos => {
                    const len: usize = #min(max_file_operation_byte_count, bytes.length);
                    const syscall_result = system.read(file_descriptor.handle, bytes.pointer, bytes.length);
                    const byte_count = unwrap_syscall(syscall_result) catch |err| switch (err) {
                        else => return ReadError.failed,
                    };
                    return byte_count;
                },
                else => #error("OS not supported"),
            }
        } else {
            const result: usize = 0;
            return result;
        }
    }

    const read_all = fn(file_descriptor: FileDescriptor, bytes: []u8) ReadError!void {
        var bytes_read: usize = 0;

        while (bytes_read < bytes.length) {
            const iteration_read_byte_count = try file_descriptor.read(bytes = bytes[bytes_read..]);
            bytes_read += iteration_read_byte_count;
        }

        assert(bytes_read == bytes.length);
    }

    const WriteError = error{
        write_failed,
    };

    const write = fn (file_descriptor: FileDescriptor, bytes: []const u8) WriteError!usize {
        switch (current) {
            .linux => {
                const length: usize = #min(max_file_operation_byte_count, bytes.length);
                const syscall_result = system.write(file_descriptor.handle, bytes[0..length]);
                const byte_count = unwrap_syscall(syscall_result) catch |err| switch (err) {
                    else => return WriteError.write_failed,
                };
                return byte_count;
            },
            .macos => {
                const length: usize = #min(max_file_operation_byte_count, bytes.length);
                const syscall_result = system.write(file_descriptor.handle, bytes.pointer, length);
                const byte_count = unwrap_syscall(syscall_result) catch |err| switch (err) {
                    else => return WriteError.write_failed,
                };
                return byte_count;
            },
            else => #error("OS not supported"),
        }
    }

    const write_all = fn(file_descriptor: FileDescriptor, bytes: []const u8) WriteError!void {
        var bytes_written: usize = 0;

        while (bytes_written < bytes.length) {
            const iteration_written_byte_count = try file_descriptor.write(bytes = bytes[bytes_written..]);
            bytes_written += iteration_written_byte_count;
        }

        assert(bytes_written == bytes.length);
    }

    const get_size = fn (file_descriptor: FileDescriptor) GetAttributesError!u64 {
        const file_attributes = try file_descriptor.get_attributes();
        return file_attributes.size;
    }

    const GetAttributesError = error{
        failed,
    };

    const get_attributes = fn (file_descriptor: FileDescriptor) GetAttributesError!FileAttributes {
        switch (current) {
            .linux, .macos => {
                var stat_buffer: system.Stat = undefined;
                const raw_result = system.fstat(file_descriptor.handle, stat_buffer.&);
                const result = unwrap_syscall(raw_result) catch |err| switch (err) {
                    else => return GetAttributesError.failed,
                };

                const size: u64 = #cast(stat_buffer.size);

                const file_attributes = FileAttributes{
                    .size = size,
                };

                return file_attributes;
            },
            else => #error("OS not supported"),
        }
    }
};

const FileAttributes = struct{
    size: u64,
};

const StdFileDescriptor = enum {
    stdin = 0,
    stdout = 1,
    stderr = 2,

    const get = fn(descriptor: StdFileDescriptor) FileDescriptor{
        switch (current) {
            .linux, .macos => {
                return FileDescriptor{
                    .handle = #cast(descriptor),
                };
            },
            else => #error("OS not supported"),
        }
    }
};

const ProtectionFlags = bitfield(u32){
    read: bool,
    write: bool,
    execute: bool,
};

const MapFlags = bitfield(u32){
    reserve: bool,
    commit: bool,
};

const VirtualAllocateError = error{
    allocation_failed,
};

const allocate_virtual_memory = fn(address: ?[&]u8, length: usize, general_protection_flags: ProtectionFlags, general_map_flags: MapFlags) VirtualAllocateError![&]u8 {
    const protection_flags = system.get_protection_flags(flags = general_protection_flags);
    const map_flags = system.get_map_flags(flags = general_map_flags);
    const file_descriptor = -1;
    const offset = 0;

    switch (current) {
        .linux, .macos => {
            const syscall_result = system.mmap(address, length, protection_flags, map_flags, file_descriptor, offset);
            if (link_libc) {
                if (syscall_result != system.MAP_FAILED) {
                    const result_address: [&]u8 = #cast(syscall_result);
                    return result_address;
                } else {
                    // TODO:
                    unreachable;
                }
            } else {
                const result = unwrap_syscall(syscall_result) catch |err| switch (err) {
                    else => unreachable,
                };

                const pointer: [&]u8 = #cast(result);
                return pointer;
            }
        },
        else => #error("OS not supported"),
    }
}

const FreeError = error{
    free_failed,
};
const free_virtual_memory = fn(bytes: []const u8) FreeError!void {
    switch (current) {
        .linux => {
            const syscall_result = system.munmap(bytes);
            _ = unwrap_syscall(syscall_result) catch |err| switch (err) {
                else => unreachable,
            };
        },
        .macos => {
            const syscall_result = system.munmap(bytes.pointer, bytes.length);
            _ = unwrap_syscall(syscall_result) catch |err| switch (err) {
                else => unreachable,
            };
        },
        else => #error("OS not supported"),
    }
}

const ReadLinkError = error{
    failed,
};

const readlink = fn(file_path: [&:0]const u8, buffer: []u8) ReadLinkError![]u8 {
    switch (current) {
        .linux, .macos => {
            const syscall_result = system.readlink(file_path, buffer);

            const byte_count = unwrap_syscall(syscall_result) catch |err| switch (err) {
                else => unreachable,
            };

            const bytes = buffer[0..byte_count];
            return bytes;
        },
        else => #error("OS not supported"),
    }
}

const max_path_byte_count = switch (current) {
    .linux => 0x1000,
    .macos => 1024,
    else => #error("OS not supported"),
};

const CurrentExecutablePath = error{
    failed,
};
const current_executable_path = fn(buffer: [:0]u8) CurrentExecutablePath![]u8 {
    switch (current) {
        .linux => {
            const bytes = readlink("/proc/self/exe", buffer) catch |err| switch (err) {
                else => unreachable,
            };
            return bytes;
        },
        .macos => {
            var symlink_path_buffer: [max_path_byte_count:0]u8 = undefined;
            var symlink_path_len: u32 = symlink_path_buffer.length + 1;
            const ns_result = c._NSGetExecutablePath(symlink_path_buffer.&, symlink_path_len.&);
            if (ns_result == 0) {
                const symlink_path = symlink_path_buffer[0..symlink_path_len];
                if (c.realpath(symlink_path.pointer, buffer.pointer)) |result| {
                    var i: usize = 0;
                    while (i < buffer.length) {
                        if (result[i] == 0) {
                            break;
                        }
                        i += 1;
                    }
                    assert(i < buffer.length);

                    const r: []u8 = result[0..i];
                    return r;
                } else {
                    return CurrentExecutablePath.failed;
                }
            } else {
                return CurrentExecutablePath.failed;
            }
        },
        else => #error("OS not supported"),
    }
}

const Process = struct{
    const Id = system.ProcessId;
};

const DuplicateProcessError = error{
    system_resources,
    out_of_memory,
};

const duplicate_process = fn () DuplicateProcessError!Process.Id {
    switch (current) {
        .linux, .macos => {
            const syscall_result = system.fork();
            const result = unwrap_syscall(syscall_result) catch |err| return switch (err) {
                .AGAIN, .NOMEM => DuplicateProcessError.system_resources,
                else => unreachable,
            };
            const truncated: u32 = #cast(result);

            const process_id: Process.Id = #cast(truncated);
            return process_id;
        },
        else => #error("OS not supported"),
    }
}

const ExecveError = error{
    execve_failed,
};

const execute = fn(path: [&:0]const u8, argv: [&:null]const ?[&:0]const u8, env: [&:null]const ?[&:null]const u8) ExecveError!noreturn {
    switch (current) {
        .linux, .macos =>  {
            const syscall_result = system.execve(path, argv, env);
            _ = unwrap_syscall(syscall_result) catch |err| switch (err) {
                else => return ExecveError.execve_failed,
            };
            unreachable;
        },
        else => #error("OS not supported"),
    }
}

const EventFileDescriptorError = error{
};
const event_file_descriptor = fn(initial_value: u32, flags: u32) EventFileDescriptorError!FileDescriptor {
    switch (current) {
        .linux => {
            const syscall_result = linux.event_file_descriptor(count = initial_value, flags);
            const result = unwrap_syscall(syscall_result) catch |err| switch (err) {
                else => unreachable,
            };
            const file_descriptor: system.FileDescriptor = #cast(result);
            return file_descriptor;
        },
        else => #error("OS not supported"),
    }
}

const Dup2Error = error{
};
const dup2 = fn(old_file_descriptor: system.FileDescriptor, new_file_descriptor: system.FileDescriptor) Dup2Error!void {
    switch (current) {
        .linux => {
            const syscall_result = linux.dup2(old = old_file_descriptor, new = new_file_descriptor);
            _ = unwrap_syscall(syscall_result) catch |err| switch (err) {
                else => unreachable,
            };
        },
        else => #error("OS not supported"),
    }
}

const OpenError = error{
    failed,
};

const OpenFlags = bitfield(u32) {
    access_mode: AccessMode = .read_only,
};

const AccessMode = enum(u2) {
    read_only = 0,
    write_only = 1,
    read_write = 2,
};

const open = fn(path: [&:0]const u8, open_flags: OpenFlags) OpenError!FileDescriptor{
    switch (current) {
        .linux => {
            const flags = system.OpenFlags{
                .access_mode = switch (open_flags.access_mode) {
                    .read_only => .read_only,
                    .write_only => .write_only,
                    .read_write => .read_write,
                },
            };
            const syscall_result = system.open(path, flags, 0);
            const result = unwrap_syscall(syscall_result) catch |err| switch (err) {
                else => unreachable,
            };

            const r: u32 = #cast(result);

            const file_descriptor = FileDescriptor{
                .handle = #cast(r),
            };
            return file_descriptor;
        },
        .macos => {
            const flags = system.OpenFlags{
                .access_mode = switch (open_flags.access_mode) {
                    .read_only => .read_only,
                    .write_only => .write_only,
                    .read_write => .read_write,
                },
            };
            const syscall_result = system.open(path, flags);
            const result = unwrap_syscall(syscall_result) catch |err| switch (err) {
                else => unreachable,
            };

            const r: u32 = #cast(result);

            const file_descriptor = FileDescriptor{
                .handle = #cast(r),
            };
            return file_descriptor;
        },
        else => #error("OS not supported"),
    }
}

const CloseError = error{
};
const close = fn(file_descriptor: system.FileDescriptor) CloseError!void {
    switch (current) {
        .linux => {
            const syscall_result = system.close(file_descriptor);
            _ = unwrap_syscall(syscall_result) catch |err| switch (err) {
                else => unreachable,
            };
        },
        else => #error("OS not supported"),
    }
}

const Pipe2Error = error{
};
const pipe2 = fn(flags: u32) Pipe2Error![2]system.FileDescriptor{
    switch (current) {
        .linux => {
            var pipe: [2]system.FileDescriptor = undefined;
            const syscall_result = linux.pipe2(pipe.&, flags);
            _ = unwrap_syscall(syscall_result) catch |err| switch (err) {
                else => unreachable,
            };

            return pipe;
        },
        else => #error("OS not supported"),
    }
}

const PollFileDescriptor = system.PollFileDescriptor;

const poll = fn(file_descriptors: []PollFileDescriptor, timeout: s32) ?usize {
    switch (current) {
        .linux => {
            if (linux.unwrap_syscall(syscall_result = linux.poll(file_descriptors = file_descriptors.pointer, file_descriptor_count = file_descriptors.length, timeout = timeout))) |result| {
                return result;
            } else {
                return null;
            }
        },
        else => #error("OS not supported"),
    }
}

const termsig = fn(status: u32) u32 {
    return status & 0x7f;
}

const ifexited = fn(status: u32) bool {
    return termsig(status) == 0;
}

const exitstatus = fn(status: u32) u8 {
    const result: u8 = #cast((status & 0xff00) >> 8);
    return result;
}

const stopsig = fn(status: u32) u32 {
    return exitstatus(status);
}

const ifstopped = fn(status: u32) bool {
    const result: u16 = #cast(((status & 0xffff) * 0x10001) >> 8);
    return result > 0x7f00;
}

const ifsignaled = fn(status: u32) bool {
    return (status & 0xffff) - 1 < 0xff;
}

const WaitPidError = error{
    failed,
};
const waitpid = fn(pid: Process.Id, flags: u32) WaitPidError!u32 {
    switch (current) {
        .linux => {
            var status: u32 = undefined;
            while (true) {
                const syscall_result = linux.waitpid(pid, status = status.&, flags, resource_usage = 0);
                const signed_syscall_result: ssize = #cast(syscall_result);
                if (signed_syscall_result != -4) {
                    _ = unwrap_syscall(syscall_result) catch |err| switch (err) {
                        else => unreachable,
                    };
                    return status;
                }
            }
        },
        .macos => {
            var status: s32 = undefined;
            if (system.waitpid(pid, status.&, #cast(flags)) != -1) {
                const status_u: u32 = #cast(status);
                return status_u;
            } else {
                unreachable;
            }
        },
        else => #error("OS not supported"),
    }

}

const reserve = fn (size: u64) *!&any {
    switch (current) {
        .linux, .macos => {
            const syscall_result = system.mmap(null, size, .{
                .read = false,
                .write = false,
                .execute = false,
            }, .{
                .anonymous = true,
                .private = true,
                .shared = false,
                .fixed = false,
            }, -1, 0);

            switch (link_libc) {
                true => {
                    if (syscall_result != system.MAP_FAILED) {
                        const result: &any = #cast(syscall_result);
                        return result;
                    } else {
                        unreachable;
                    }
                },
                false => {
                    const result = unwrap_syscall(syscall_result) catch |err| switch (err) {
                        else => unreachable,
                    };
                    const pointer: &any = #cast(result);
                    return pointer;
                },
            }
        
        },
        else => #error("OS not supported"),
    }
}

const commit = fn (reserved_memory: &any, size: u64) *!void {
    switch (current) {
        .linux, .macos => {
            const syscall_result = system.mprotect(reserved_memory, size, .{
                .read = true,
                .write = true,
                .execute = false,
            });
        },
        else => #error("OS not supported"),
    }
}

const MemFdCreateError = error{
};

const memfd_create = fn(name: [&:0]const u8, flags: u32) MemFdCreateError!FileDescriptor{
    switch (current) {
        .linux => {
            const syscall_result = linux.memfd_create(path, flags);
            const result = unwrap_syscall(syscall_result) catch |err| switch (err) {
                else => unreachable,
            };

            const file_descriptor = FileDescriptor{
                .handle = #cast(result),
            };

            return file_descriptor;
        },
        else => #error("OS not supported"),
    }
}

const basename = fn (path: []const u8) ?[]const u8 {
    var i: usize = path.length;

    while (i > 0) {
        i -= 1;

        if (path[i] == '/') {
            i = i + 1;
            break;
        }
    }

    const p = path[i..];
    return p;
}

const IoChannelBehavior = enum{
    pipe,
    close,
    inherit,
    ignore,
};
