const std = @import("../../std.zig");
const builtin = @import("builtin");
const net = @import("net.zig");

const os = std.os;
const fmt = std.fmt;
const mem = std.mem;
const time = std.time;
const meta = std.meta;
const native_os = builtin.os;
const native_endian = builtin.cpu.arch.endian();

const Buffer = std.x.os.Buffer;

const assert = std.debug.assert;

/// A generic, cross-platform socket abstraction.
pub const Socket = struct {
    /// A socket-address pair.
    pub const Connection = struct {
        socket: Socket,
        address: Socket.Address,

        /// Enclose a socket and address into a socket-address pair.
        pub fn from(socket: Socket, address: Socket.Address) Socket.Connection {
            return .{ .socket = socket, .address = address };
        }
    };

    /// A generic socket address abstraction. It is safe to directly access and modify
    /// the fields of a `Socket.Address`.
    pub const Address = union(enum) {
        pub const Native = struct {
            pub const requires_prepended_length = native_os.getVersionRange() == .semver;
            pub const Length = if (requires_prepended_length) u8 else [0]u8;

            pub const Family = if (requires_prepended_length) u8 else c_ushort;

            /// POSIX `sockaddr.storage`. The expected size and alignment is specified in IETF RFC 2553.
            pub const Storage = extern struct {
                pub const expected_size = os.sockaddr.SS_MAXSIZE;
                pub const expected_alignment = 8;

                pub const padding_size = expected_size -
                    mem.alignForward(@sizeOf(Address.Native.Length), expected_alignment) -
                    mem.alignForward(@sizeOf(Address.Native.Family), expected_alignment);

                len: Address.Native.Length align(expected_alignment) = undefined,
                family: Address.Native.Family align(expected_alignment) = undefined,
                padding: [padding_size]u8 align(expected_alignment) = undefined,

                comptime {
                    assert(@sizeOf(Storage) == Storage.expected_size);
                    assert(@alignOf(Storage) == Storage.expected_alignment);
                }
            };
        };

        ipv4: net.IPv4.Address,
        ipv6: net.IPv6.Address,

        /// Instantiate a new address with a IPv4 host and port.
        pub fn initIPv4(host: net.IPv4, port: u16) Socket.Address {
            return .{ .ipv4 = .{ .host = host, .port = port } };
        }

        /// Instantiate a new address with a IPv6 host and port.
        pub fn initIPv6(host: net.IPv6, port: u16) Socket.Address {
            return .{ .ipv6 = .{ .host = host, .port = port } };
        }

        /// Parses a `sockaddr` into a generic socket address.
        pub fn fromNative(address: *align(4) const os.sockaddr) Socket.Address {
            switch (address.family) {
                os.AF.INET => {
                    const info = @ptrCast(*const os.sockaddr.in, address);
                    const host = net.IPv4{ .octets = @bitCast([4]u8, info.addr) };
                    const port = mem.bigToNative(u16, info.port);
                    return Socket.Address.initIPv4(host, port);
                },
                os.AF.INET6 => {
                    const info = @ptrCast(*const os.sockaddr.in6, address);
                    const host = net.IPv6{ .octets = info.addr, .scope_id = info.scope_id };
                    const port = mem.bigToNative(u16, info.port);
                    return Socket.Address.initIPv6(host, port);
                },
                else => unreachable,
            }
        }

        /// Encodes a generic socket address into an extern union that may be reliably
        /// casted into a `sockaddr` which may be passed into socket syscalls.
        pub fn toNative(self: Socket.Address) extern union {
            ipv4: os.sockaddr.in,
            ipv6: os.sockaddr.in6,
        } {
            return switch (self) {
                .ipv4 => |address| .{
                    .ipv4 = .{
                        .addr = @bitCast(u32, address.host.octets),
                        .port = mem.nativeToBig(u16, address.port),
                    },
                },
                .ipv6 => |address| .{
                    .ipv6 = .{
                        .addr = address.host.octets,
                        .port = mem.nativeToBig(u16, address.port),
                        .scope_id = address.host.scope_id,
                        .flowinfo = 0,
                    },
                },
            };
        }

        /// Returns the number of bytes that make up the `sockaddr` equivalent to the address.
        pub fn getNativeSize(self: Socket.Address) u32 {
            return switch (self) {
                .ipv4 => @sizeOf(os.sockaddr.in),
                .ipv6 => @sizeOf(os.sockaddr.in6),
            };
        }

        /// Implements the `std.fmt.format` API.
        pub fn format(
            self: Socket.Address,
            comptime layout: []const u8,
            opts: fmt.FormatOptions,
            writer: anytype,
        ) !void {
            _ = opts;
            _ = layout;
            switch (self) {
                .ipv4 => |address| try fmt.format(writer, "{}:{}", .{ address.host, address.port }),
                .ipv6 => |address| try fmt.format(writer, "{}:{}", .{ address.host, address.port }),
            }
        }
    };

    /// POSIX `msghdr`. Denotes a destination address, set of buffers, control data, and flags. Ported
    /// directly from musl.
    pub const Message = if (native_os.isAtLeast(.windows, .vista) != null and native_os.isAtLeast(.windows, .vista).?)
        extern struct {
            name: usize = @ptrToInt(@as(?[*]u8, null)),
            name_len: c_int = 0,

            buffers: usize = undefined,
            buffers_len: c_ulong = undefined,

            control: Buffer = .{
                .ptr = @ptrToInt(@as(?[*]u8, null)),
                .len = 0,
            },
            flags: c_ulong = 0,

            pub usingnamespace MessageMixin(Message);
        }
    else if (native_os.tag == .windows)
        extern struct {
            name: usize = @ptrToInt(@as(?[*]u8, null)),
            name_len: c_int = 0,

            buffers: usize = undefined,
            buffers_len: u32 = undefined,

            control: Buffer = .{
                .ptr = @ptrToInt(@as(?[*]u8, null)),
                .len = 0,
            },
            flags: u32 = 0,

            pub usingnamespace MessageMixin(Message);
        }
    else if (@sizeOf(usize) > 4 and native_endian == .Big)
        extern struct {
            name: usize = @ptrToInt(@as(?[*]u8, null)),
            name_len: c_uint = 0,

            buffers: usize = undefined,
            _pad_1: c_int = 0,
            buffers_len: c_int = undefined,

            control: usize = @ptrToInt(@as(?[*]u8, null)),
            _pad_2: c_int = 0,
            control_len: c_uint = 0,

            flags: c_int = 0,

            pub usingnamespace MessageMixin(Message);
        }
    else if (@sizeOf(usize) > 4 and native_endian == .Little)
        extern struct {
            name: usize = @ptrToInt(@as(?[*]u8, null)),
            name_len: c_uint = 0,

            buffers: usize = undefined,
            buffers_len: c_int = undefined,
            _pad_1: c_int = 0,

            control: usize = @ptrToInt(@as(?[*]u8, null)),
            control_len: c_uint = 0,
            _pad_2: c_int = 0,

            flags: c_int = 0,

            pub usingnamespace MessageMixin(Message);
        }
    else
        extern struct {
            name: usize = @ptrToInt(@as(?[*]u8, null)),
            name_len: c_uint = 0,

            buffers: usize = undefined,
            buffers_len: c_int = undefined,

            control: usize = @ptrToInt(@as(?[*]u8, null)),
            control_len: c_uint = 0,

            flags: c_int = 0,

            pub usingnamespace MessageMixin(Message);
        };

    fn MessageMixin(comptime Self: type) type {
        return struct {
            pub fn fromBuffers(buffers: []const Buffer) Self {
                var self: Self = .{};
                self.setBuffers(buffers);
                return self;
            }

            pub fn setName(self: *Self, name: []const u8) void {
                self.name = @ptrToInt(name.ptr);
                self.name_len = @intCast(meta.fieldInfo(Self, .name_len).field_type, name.len);
            }

            pub fn setBuffers(self: *Self, buffers: []const Buffer) void {
                self.buffers = @ptrToInt(buffers.ptr);
                self.buffers_len = @intCast(meta.fieldInfo(Self, .buffers_len).field_type, buffers.len);
            }

            pub fn setControl(self: *Self, control: []const u8) void {
                if (native_os.tag == .windows) {
                    self.control = Buffer.from(control);
                } else {
                    self.control = @ptrToInt(control.ptr);
                    self.control_len = @intCast(meta.fieldInfo(Self, .control_len).field_type, control.len);
                }
            }

            pub fn setFlags(self: *Self, flags: u32) void {
                self.flags = @intCast(meta.fieldInfo(Self, .flags).field_type, flags);
            }

            pub fn getName(self: Self) []const u8 {
                return @intToPtr([*]const u8, self.name)[0..@intCast(usize, self.name_len)];
            }

            pub fn getBuffers(self: Self) []const Buffer {
                return @intToPtr([*]const Buffer, self.buffers)[0..@intCast(usize, self.buffers_len)];
            }

            pub fn getControl(self: Self) []const u8 {
                if (native_os.tag == .windows) {
                    return self.control.into();
                } else {
                    return @intToPtr([*]const u8, self.control)[0..@intCast(usize, self.control_len)];
                }
            }

            pub fn getFlags(self: Self) u32 {
                return @intCast(u32, self.flags);
            }
        };
    }

    /// POSIX `linger`, denoting the linger settings of a socket.
    ///
    /// Microsoft's documentation and glibc denote the fields to be unsigned
    /// short's on Windows, whereas glibc and musl denote the fields to be
    /// int's on every other platform.
    pub const Linger = extern struct {
        pub const Field = switch (native_os.tag) {
            .windows => c_ushort,
            else => c_int,
        };

        enabled: Field,
        timeout_seconds: Field,

        pub fn init(timeout_seconds: ?u16) Socket.Linger {
            return .{
                .enabled = @intCast(Socket.Linger.Field, @boolToInt(timeout_seconds != null)),
                .timeout_seconds = if (timeout_seconds) |seconds| @intCast(Socket.Linger.Field, seconds) else 0,
            };
        }
    };

    /// Possible set of flags to initialize a socket with.
    pub const InitFlags = enum {
        // Initialize a socket to be non-blocking.
        nonblocking,

        // Have a socket close itself on exec syscalls.
        close_on_exec,
    };

    /// The underlying handle of a socket.
    fd: os.socket_t,

    /// Enclose a socket abstraction over an existing socket file descriptor.
    pub fn from(fd: os.socket_t) Socket {
        return Socket{ .fd = fd };
    }

    /// Mix in socket syscalls depending on the platform we are compiling against.
    pub usingnamespace switch (native_os.tag) {
        .windows => @import("socket_windows.zig"),
        else => @import("socket_posix.zig"),
    }.Mixin(Socket);
};
