#define NX_SERVICE_ASSUME_NON_DOMAIN
#include <errno.h>
#include <string.h>

// Complete definition of struct timeout:
#include <sys/time.h>

#include <fcntl.h>

// For ioctls:
#include <sys/ioctl.h>
#include <net/if.h>
#include <net/if_media.h>

#include "service_guard.h"
#include "kernel/shmem.h"
#include "kernel/rwlock.h"
#include "sf/sessionmgr.h"
#include "services/bsd.h"
#include "runtime/hosversion.h"

typedef struct BsdSelectTimeval {
    struct timeval tv;
    bool is_null;
} BsdSelectTimeval;

__thread Result g_bsdResult;
__thread int g_bsdErrno;

static Service g_bsdSrv;
static Service g_bsdMonitor;
static SessionMgr g_bsdSessionMgr;
static u64 g_bsdClientPid = -1;

static TransferMemory g_bsdTmem;

typedef struct {
    u32 version;

    u32 tcp_tx_buf_size;
    u32 tcp_rx_buf_size;
    u32 tcp_tx_buf_max_size;
    u32 tcp_rx_buf_max_size;

    u32 udp_tx_buf_size;
    u32 udp_rx_buf_size;

    u32 sb_efficiency;
} BsdServiceConfig;

static const BsdInitConfig g_defaultBsdInitConfig = {
    .version = 1,

    .tmem_buffer            = NULL,
    .tmem_buffer_size       = 0,

    .tcp_tx_buf_size        = 0x8000,
    .tcp_rx_buf_size        = 0x10000,
    .tcp_tx_buf_max_size    = 0x40000,
    .tcp_rx_buf_max_size    = 0x40000,

    .udp_tx_buf_size = 0x2400,
    .udp_rx_buf_size = 0xA500,

    .sb_efficiency = 4,
};

NX_GENERATE_SERVICE_GUARD_PARAMS(bsd, (const BsdInitConfig *config, u32 num_sessions, u32 service_type), (config, num_sessions, service_type));

// This function computes the minimal size of the transfer memory to be passed to @ref bsdInitalize.
// Should the transfer memory be smaller than that, the BSD sockets service would only send
// ZeroWindow packets (for TCP), resulting in a transfer rate not exceeding 1 byte/s.
NX_CONSTEXPR size_t _bsdGetTransferMemSizeForConfig(const BsdInitConfig *config) {
    u32 tcp_tx_buf_max_size = config->tcp_tx_buf_max_size != 0 ? config->tcp_tx_buf_max_size : config->tcp_tx_buf_size;
    u32 tcp_rx_buf_max_size = config->tcp_rx_buf_max_size != 0 ? config->tcp_rx_buf_max_size : config->tcp_rx_buf_size;
    u32 sum = tcp_tx_buf_max_size + tcp_rx_buf_max_size + config->udp_tx_buf_size + config->udp_rx_buf_size;

    sum = (sum + 0xFFF) &~ 0xFFF; // page round-up
    return (size_t)(config->sb_efficiency * sum);
}

NX_CONSTEXPR BsdSelectTimeval _bsdCreateSelectTimeval(struct timeval *timeval) {
    BsdSelectTimeval ret = {};
    if (timeval)
        ret.tv = *timeval;
    else
        ret.is_null = true;
    return ret;
}

static Result _bsdRegisterClient(TransferMemory* tmem, const BsdServiceConfig *config, u64* pid_out) {
    const struct {
        BsdServiceConfig config;
        u64 pid_placeholder;
        u64 tmem_sz;
    } in = { *config, 0, tmem->size };

    Result rc = serviceDispatchInOut(&g_bsdSrv, 0, in, *pid_out,
        .in_send_pid = true,
        .in_num_handles = 1,
        .in_handles = { tmem->handle },
    );

    g_bsdResult = rc;
    g_bsdErrno = 0;
    return rc;
}

static Result _bsdStartMonitoring(u64 pid) {
    Result rc = serviceDispatchIn(&g_bsdMonitor, 1, pid, .in_send_pid = true);
    g_bsdResult = rc;
    g_bsdErrno = 0;
    return rc;
}

NX_INLINE int _bsdDispatchImpl(
    u32 request_id,
    const void* in_data, u32 in_data_size,
    void* out_data, u32 out_data_size,
    SfDispatchParams disp
)
{
    // Make a copy of the service struct, so that the compiler can assume that it won't be modified by function calls.
    Service srv = g_bsdSrv;

    void* in = serviceMakeRequest(&srv, request_id, disp.context,
        in_data_size, disp.in_send_pid,
        disp.buffer_attrs, disp.buffers,
        disp.in_num_objects, disp.in_objects,
        disp.in_num_handles, disp.in_handles);

    if (in_data_size)
        __builtin_memcpy(in, in_data, in_data_size);

    int slot = sessionmgrAttachClient(&g_bsdSessionMgr);
    Result rc = svcSendSyncRequest(sessionmgrGetClientSession(&g_bsdSessionMgr, slot));
    sessionmgrDetachClient(&g_bsdSessionMgr, slot);

    int ret = -1;
    int errno_ = -1;
    void* out_ptr = NULL;
    if (R_SUCCEEDED(rc)) {
        // This is only correct if extra outputs need 32-bit alignment or more
        // So far no BSD commands with extra outputs smaller than 32-bit have been observed
        struct {
            int ret;
            int errno_;
        } *out = NULL;

        rc = serviceParseResponse(&srv,
            sizeof(*out)+out_data_size, (void**)&out,
            disp.out_num_objects, disp.out_objects,
            disp.out_handle_attrs, disp.out_handles);

        if (R_SUCCEEDED(rc)) {
            ret = out->ret;
            errno_ = ret < 0 ? out->errno_ : 0;
            if (errno_ == 0)
                out_ptr = out+1;
        }
    }

    if (out_ptr && out_data && out_data_size)
        __builtin_memcpy(out_data, out_ptr, out_data_size);

    g_bsdResult = rc;
    g_bsdErrno = errno_;
    return ret;
}

#define _bsdDispatch(_rid,...) \
    _bsdDispatchImpl((_rid),NULL,0,NULL,0,(SfDispatchParams){ __VA_ARGS__ })

#define _bsdDispatchIn(_rid,_in,...) \
    _bsdDispatchImpl((_rid),&(_in),sizeof(_in),NULL,0,(SfDispatchParams){ __VA_ARGS__ })

#define _bsdDispatchOut(_rid,_out,...) \
    _bsdDispatchImpl((_rid),NULL,0,&(_out),sizeof(_out),(SfDispatchParams){ __VA_ARGS__ })

#define _bsdDispatchInOut(_rid,_in,_out,...) \
    _bsdDispatchImpl((_rid),&(_in),sizeof(_in),&(_out),sizeof(_out),(SfDispatchParams){ __VA_ARGS__ })

static int _bsdCmdInSockfdOutSockaddr(int sockfd, struct sockaddr *addr, socklen_t *addrlen, u32 cmd_id) {
    socklen_t maxaddrlen = addrlen ? *addrlen : 0;

    return _bsdDispatchInOut(cmd_id, sockfd, *addrlen,
        .buffer_attrs = { SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out },
        .buffers = { { addr, maxaddrlen } },
    );
}

static int _bsdCmdInSockfdSockaddr(int sockfd, const struct sockaddr *addr, socklen_t addrlen, u32 cmd_id) {
    return _bsdDispatchIn(cmd_id, sockfd,
        .buffer_attrs = { SfBufferAttr_HipcAutoSelect | SfBufferAttr_In },
        .buffers = { { addr, addrlen } },
    );
}

static int _bsdCmdInDomainTypeProtocol(int domain, int type, int protocol, u32 cmd_id) {
    const struct {
        int domain;
        int type;
        int protocol;
    } in = { domain, type, protocol };

    return _bsdDispatchIn(cmd_id, in);
}

const BsdInitConfig *bsdGetDefaultInitConfig(void) {
    return &g_defaultBsdInitConfig;
}

Result _bsdInitialize(const BsdInitConfig *config, u32 num_sessions, u32 service_type) {
    if (!config)
        config = &g_defaultBsdInitConfig;

    SmServiceName bsd_srv = {0};
    Result rc = MAKERESULT(Module_Libnx, LibnxError_BadInput);

    if (service_type & BIT(1)) {
        bsd_srv = smEncodeName("bsd:s");
        rc = smGetServiceWrapper(&g_bsdSrv, bsd_srv);
    }

    if (R_FAILED(rc) && (service_type & BIT(0))) {
        bsd_srv = smEncodeName("bsd:u");
        rc = smGetServiceWrapper(&g_bsdSrv, bsd_srv);
    }

    if (R_SUCCEEDED(rc))
        rc = smGetServiceWrapper(&g_bsdMonitor, bsd_srv);

    if (R_SUCCEEDED(rc)) {
        const size_t min_tmem_size = _bsdGetTransferMemSizeForConfig(config);

        if (config->tmem_buffer != NULL && config->tmem_buffer_size >= min_tmem_size)
            rc = tmemCreateFromMemory(&g_bsdTmem, config->tmem_buffer, config->tmem_buffer_size, 0);
        else
            rc = tmemCreate(&g_bsdTmem, min_tmem_size, 0);
    }

    if (R_SUCCEEDED(rc)){
        const BsdServiceConfig srv_config = {
            .version             = config->version,

            .tcp_tx_buf_size     = config->tcp_tx_buf_size,
            .tcp_rx_buf_size     = config->tcp_rx_buf_size,
            .tcp_tx_buf_max_size = config->tcp_tx_buf_max_size,
            .tcp_rx_buf_max_size = config->tcp_rx_buf_max_size,

            .udp_tx_buf_size     = config->udp_tx_buf_size,
            .udp_rx_buf_size     = config->udp_rx_buf_size,

            .sb_efficiency       = config->sb_efficiency,
        };
        rc = _bsdRegisterClient(&g_bsdTmem, &srv_config, &g_bsdClientPid);
    }

    if (R_SUCCEEDED(rc))
        rc = _bsdStartMonitoring(g_bsdClientPid);

    if (R_SUCCEEDED(rc))
        rc = sessionmgrCreate(&g_bsdSessionMgr, g_bsdSrv.session, num_sessions);

    return rc;
}

void _bsdCleanup(void) {
    g_bsdClientPid = 0;
    sessionmgrClose(&g_bsdSessionMgr);
    serviceClose(&g_bsdMonitor);
    serviceClose(&g_bsdSrv);
    tmemClose(&g_bsdTmem);
}

Service* bsdGetServiceSession(void) {
    return &g_bsdSrv;
}

int bsdSocket(int domain, int type, int protocol) {
    return _bsdCmdInDomainTypeProtocol(domain, type, protocol, 2);
}

int bsdSocketExempt(int domain, int type, int protocol) {
    return _bsdCmdInDomainTypeProtocol(domain, type, protocol, 3);
}

int bsdOpen(const char *pathname, int flags) {
    return _bsdDispatchIn(4, flags,
        .buffer_attrs = { SfBufferAttr_HipcAutoSelect | SfBufferAttr_In },
        .buffers = { { pathname, strlen(pathname) + 1 } },
    );
}

int bsdSelect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) {
    size_t readfds_size   = readfds   ? sizeof(fd_set) : 0;
    size_t writefds_size  = writefds  ? sizeof(fd_set) : 0;
    size_t exceptfds_size = exceptfds ? sizeof(fd_set) : 0;

    const struct {
        int nfds;
        BsdSelectTimeval timeout;
    } in = { nfds, _bsdCreateSelectTimeval(timeout) };

    return _bsdDispatchIn(5, in,
        .buffer_attrs = {
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_In,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_In,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_In,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out,
        },
        .buffers = {
            { readfds,   readfds_size   },
            { writefds,  writefds_size  },
            { exceptfds, exceptfds_size },
            { readfds,   readfds_size   },
            { writefds,  writefds_size  },
            { exceptfds, exceptfds_size },
        },
    );
}

int bsdPoll(struct pollfd *fds, nfds_t nfds, int timeout) {
    size_t fds_size = nfds * sizeof(struct pollfd);

    const struct {
        nfds_t nfds;
        int timeout;
    } in = { nfds, timeout };

    return _bsdDispatchIn(6, in,
        .buffer_attrs = {
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_In,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out,
        },
        .buffers = {
            { fds, fds_size },
            { fds, fds_size },
        },
    );
}

int bsdSysctl(const int *name, unsigned int namelen, void *oldp, size_t *oldlenp, const void *newp, size_t newlen) {
    size_t inlen = oldlenp ? *oldlenp : 0;

    return _bsdDispatchOut(7, *oldlenp,
        .buffer_attrs = {
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_In,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_In,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out,
        },
        .buffers = {
            { name, 4*namelen },
            { newp, newlen    },
            { oldp, inlen     },
        },
    );
}

ssize_t bsdRecv(int sockfd, void *buf, size_t len, int flags) {
    const struct {
        int sockfd;
        int flags;
    } in = { sockfd, flags };

    return _bsdDispatchIn(8, in,
        .buffer_attrs = { SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out },
        .buffers = { { buf, len } },
    );
}

ssize_t bsdRecvFrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen){
    socklen_t inaddrlen = addrlen ? *addrlen : 0;

    const struct {
        int sockfd;
        int flags;
    } in = { sockfd, flags };

    return _bsdDispatchInOut(9, in, *addrlen,
        .buffer_attrs = {
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out,
        },
        .buffers = {
            { buf,      len       },
            { src_addr, inaddrlen },
        },
    );
}

ssize_t bsdSend(int sockfd, const void* buf, size_t len, int flags) {
    const struct {
        int sockfd;
        int flags;
    } in = { sockfd, flags };

    return _bsdDispatchIn(10, in,
        .buffer_attrs = { SfBufferAttr_HipcAutoSelect | SfBufferAttr_In },
        .buffers = { { buf, len } },
    );
}

ssize_t bsdSendTo(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) {
    const struct {
        int sockfd;
        int flags;
    } in = { sockfd, flags };

    return _bsdDispatchIn(11, in,
        .buffer_attrs = {
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_In,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_In,
        },
        .buffers = {
            { buf,       len     },
            { dest_addr, addrlen },
        },
    );
}

int bsdAccept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
    return _bsdCmdInSockfdOutSockaddr(sockfd, addr, addrlen, 12);
}

int bsdBind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
    return _bsdCmdInSockfdSockaddr(sockfd, addr, addrlen, 13);
}

int bsdConnect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
    return _bsdCmdInSockfdSockaddr(sockfd, addr, addrlen, 14);
}

int bsdGetPeerName(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
    return _bsdCmdInSockfdOutSockaddr(sockfd, addr, addrlen, 15);
}

int bsdGetSockName(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
    return _bsdCmdInSockfdOutSockaddr(sockfd, addr, addrlen, 16);
}

int bsdGetSockOpt(int sockfd, int level, int optname, void *optval, socklen_t *optlen) {
    socklen_t inoptlen = optlen ? *optlen : 0;

    const struct {
        int sockfd;
        int level;
        int optname;
    } in = { sockfd, level, optname };

    return _bsdDispatchInOut(17, in, *optlen,
        .buffer_attrs = { SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out },
        .buffers = { { optval, inoptlen } },
    );
}

int bsdListen(int sockfd, int backlog) {
    struct {
        int sockfd;
        int backlog;
    } in = { sockfd, backlog };

    return _bsdDispatchIn(18, in);
}

int bsdIoctl(int fd, int request, void *data) {
    const void *in1 = NULL, *in2 = NULL, *in3 = NULL, *in4 = NULL;
    size_t in1sz = 0, in2sz = 0, in3sz = 0, in4sz = 0;

    void *out1 = NULL, *out2 = NULL, *out3 = NULL, *out4 = NULL;
    size_t out1sz = 0, out2sz = 0, out3sz = 0, out4sz = 0;

    int bufcount = 1;

    switch(request) {
        case SIOCGIFCONF: {
            struct ifconf *data_ = (struct ifconf *)data;
            in1 = out1 = data;
            in1sz = out1sz = sizeof(struct ifconf);
            in2 = out2 = data_->ifc_req;
            in2sz = out2sz = data_->ifc_len;
            bufcount = 2;
            break;
        }
        case SIOCGIFMEDIA:
        case SIOCGIFXMEDIA: {
            struct ifmediareq *data_ = (struct ifmediareq *)data;
            in1 = out1 = data;
            in1sz = out1sz = sizeof(struct ifmediareq);
            in2 = out2 = data_->ifm_ulist;
            in2sz = out2sz = 8 * data_->ifm_count;
            bufcount = 2;
            break;
        }
        // Generic ioctl
        default: {
            void *data_ = NULL;
            if(request & IOC_INOUT)
                data_ = data;
            if(request & IOC_IN) {
                in1 = data_;
                in1sz = IOCPARM_LEN(request);
            }
            if(request & IOC_OUT) {
                out1 = data_;
                out1sz = IOCPARM_LEN(request);
            }
            break;
        }
    }

    const struct {
        int fd;
        int request;
        int bufcount;
    } in = { fd, request, bufcount };

    return _bsdDispatchIn(19, in,
        .buffer_attrs = {
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_In,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_In,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_In,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_In,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out,
            SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out,
        },
        .buffers = {
            { in1,  in1sz  },
            { in2,  in2sz  },
            { in3,  in3sz  },
            { in4,  in4sz  },
            { out1, out1sz },
            { out2, out2sz },
            { out3, out3sz },
            { out4, out4sz },
        },
    );
}

int bsdFcntl(int fd, int cmd, int flags) {
    if(cmd != F_GETFL && cmd != F_SETFL) {
        g_bsdResult = 0;
        g_bsdErrno = EOPNOTSUPP;
        return -1;
    }

    if(cmd == F_GETFL)
        flags = 0;

    const struct {
        int fd;
        int cmd;
        int flags;
    } in = { fd, cmd, flags };

    return _bsdDispatchIn(20, in);
}

int bsdSetSockOpt(int sockfd, int level, int optname, const void *optval, socklen_t optlen) {
    const struct {
        int sockfd;
        int level;
        int optname;
    } in = { sockfd, level, optname };

    return _bsdDispatchIn(21, in,
        .buffer_attrs = { SfBufferAttr_HipcAutoSelect | SfBufferAttr_In },
        .buffers = { { optval, optlen } },
    );
}

int bsdShutdown(int sockfd, int how) {
    const struct {
        int sockfd;
        int how;
    } in = { sockfd, how };

    return _bsdDispatchIn(22, in);
}

int bsdShutdownAllSockets(int how) {
    return _bsdDispatchIn(23, how);
}

ssize_t bsdWrite(int fd, const void *buf, size_t count) {
    return _bsdDispatchIn(24, fd,
        .buffer_attrs = { SfBufferAttr_HipcAutoSelect | SfBufferAttr_In },
        .buffers = { { buf, count } },
    );
}

ssize_t bsdRead(int fd, void *buf, size_t count) {
    return _bsdDispatchIn(25, fd,
        .buffer_attrs = { SfBufferAttr_HipcAutoSelect | SfBufferAttr_Out },
        .buffers = { { buf, count } },
    );
}

int bsdClose(int fd) {
    return _bsdDispatchIn(26, fd);
}

int bsdDuplicateSocket(int sockfd) {
    const struct {
        int sockfd;
        u32 _padding;
        u64 reserved;
    } in = { sockfd, 0, 0 };

    return _bsdDispatchIn(27, in);
}

int bsdRecvMMsg(int sockfd, void *buf, size_t size, unsigned int vlen, int flags, struct timespec *timeout) {
    if (hosversionBefore(7,0,0)) // This cmd was added with [3.0.0+], but we'll only support the updated [7.0.0+] version of it.
        return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);

    const struct {
        int sockfd;
        int vlen;
        int flags;
        u32 _padding;
        struct timespec timeout;
    } in = { sockfd, vlen, flags, 0, *timeout };

    return _bsdDispatchIn(29, in,
        .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
        .buffers = { { buf, size } },
    );
}

int bsdSendMMsg(int sockfd, void *buf, size_t size, unsigned int vlen, int flags) {
    if (hosversionBefore(7,0,0)) // This cmd was added with [3.0.0+], but we'll only support the updated [7.0.0+] version of it.
        return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);

    const struct {
        int sockfd;
        int vlen;
        int flags;
    } in = { sockfd, vlen, flags };

    return _bsdDispatchIn(30, in,
        .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
        .buffers = { { buf, size } },
    );
}