/*
 * Copyright (C) 2018 Freie Universität Berlin
 *
 * This file is subject to the terms and conditions of the GNU Lesser
 * General Public License v2.1. See the file LICENSE in the top level
 * directory for more details.
 */

/**
 * @ingroup     lua
 * @{
 *
 * @file
 * @brief       Basic UDP sockets.
 *
 * Lua bindings for UDP sock.
 *
 * @author      Juan Carrano <j.carrano@fu-berlin.de>
 *
 * @}
 */

#define LUA_LIB

#include "lprefix.h"

#include "net/sock/udp.h"
#include "net/sock/util.h"

#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"

/* MetaTable names */
#define SOCK_UDP_TNAME "sock_udp"

enum EP_PARSE_RESULT { EP_NULL, EP_PARSED, EP_ERROR};

/**
 * Push a nil and a string to the lua stack.
 */
static void _nil_and_str(lua_State *L, const char *s)
{
    lua_pushnil(L);
    lua_pushstring(L, s);
}

/**
 * Get a 16 bit number from a table.
 *
 * If the table entry is nil, it does nothing and returns EP_NULL.
 *
 * @return  EP_NULL, EP_PARSED  on success. EP_ERROR on error.
 */
static enum EP_PARSE_RESULT _field2int(lua_State *L, int index, const char *key,
                                       uint16_t *result)
{
    if (lua_getfield (L, index, key) != LUA_TNIL) {
        int isnum;
        lua_Number pn = lua_tonumberx(L, -1, &isnum);

        if (!isnum) {
            _nil_and_str(L, "Cannot convert object to number");
            return EP_ERROR;
        } else if (pn >=0 && pn <= UINT16_MAX){
            *result = pn;
            lua_pop(L, 1);
            return EP_PARSED;
        } else {
            _nil_and_str(L, "Number off-range (must be 16 bit)");
            return EP_ERROR;
        }
    }

    return EP_NULL;
}

/**
 * Convert a string or a table into a sock_udp_ep_t.
 *
 * @param   index   Index of the string or table in the stack.
 *
 * @return  EP_NULL   If the endpoint should be null
 * @return  EP_PARSED   If the endpoint was parsed and the result is in *ep
 * @return  EP_ERROR    If there was an error parsing the endpoint. A nil and
 *                      a message will be pushed to the stack.
 */
static enum EP_PARSE_RESULT _parse_udp_endpoint(lua_State *L, int index, sock_udp_ep_t *ep)
{
    switch (lua_type(L, index)) {
        case LUA_TNONE: /* falls through */
        case LUA_TNIL:
            return EP_NULL;
        case LUA_TSTRING:
            {
                const char *s = lua_tolstring(L, index, NULL);
                if (sock_udp_str2ep(ep, s) == 0) {
                    return EP_PARSED;
                } else {
                    _nil_and_str(L, "Address/port badly formatted");
                    return EP_ERROR;
                }
            }
            break;
        default: /* table-like */
            ep->port = 0;
            ep->netif = SOCK_ADDR_ANY_NETIF;
            ep->family = AF_UNSPEC;

            if (_field2int(L, index, "port", &ep->port) == EP_ERROR) {
                return EP_ERROR;
            }

            if (_field2int(L, index, "netif", &ep->netif) == EP_ERROR) {
                return EP_ERROR;
            }

            if (lua_getfield (L, index, "address")  != LUA_TNIL) {
                size_t slen;
                const char *addr = lua_tolstring (L, -1, &slen);

                ep->family = AF_INET6;

                if (!slen
                    || ipv6_addr_from_str((ipv6_addr_t*)&ep->addr.ipv6, addr) == NULL) {
                    _nil_and_str(L, "Address badly formatted");
                    return EP_ERROR;
                }

                lua_pop(L, 1);
            }
            return EP_PARSED;
    }
}

/**
 * Create a new UDP socket.
 *
 * @param   local   Local endpoint (as table or string, can be nil).
 * @param   remote  Remote endpoint (as table or string, can be nil).
 * @param   flags   Additional parameters after the remote endpoint will be
 *                  interpreted as flags. Not implemented yet.
 * @return  UDP socket object (full userdata with custom metatable)
 */
static int udp_new(lua_State *L)
{
    uint16_t flags = 0;
    int retval;
    sock_udp_ep_t local, remote;
    sock_udp_ep_t *plocal, *premote;

    switch (_parse_udp_endpoint(L, 1, &local)) {
        default:
        case EP_NULL:
            plocal = NULL;
            break;
        case EP_PARSED:
            plocal = &local;
            break;
        case EP_ERROR:
            return 2; /* 2 return values, a nil and a message */
    }

    switch (_parse_udp_endpoint(L, 2, &remote)) {
        default:
        case EP_NULL:
            premote = NULL;
            break;
        case EP_PARSED:
            premote = &remote;
            break;
        case EP_ERROR:
            return 2; /* 2 return values, a nil and a message */
    }

    sock_udp_t *s = lua_newuserdata(L, sizeof(*s));

    retval = sock_udp_create(s, plocal, premote, flags);

    if (retval != 0) {
        lua_pushnil(L);
    }
    switch (retval) {
        case -EINVAL:
            lua_pushliteral(L, "Invalid endpoints");
            break;
        case -EAFNOSUPPORT:
            lua_pushliteral(L, "Socket type not supported");
            break;
        case -EADDRINUSE:
            lua_pushliteral(L, "Address in use");
            break;
        default:
            lua_pushliteral(L, "Unknown error");
            break;
        case 0:
            break;
    }

    if (retval != 0) {
        return 2;
    }

    luaL_setmetatable(L, SOCK_UDP_TNAME);

    return 1;
}

/**
 * Receive data from a UDP socket.
 *
 * @param   sock
 * @param   n              Receive up to n bytes.
 * @param   timeout_ms     Use 0 to return immediately, -1 for no timeout.
 * @param   remote (optional) remote end point.
 *
 * @return  Received data as string, or nil+error message.
 */
static int udp_recv(lua_State *L)
{
    /* s cannot be NULL */
    sock_udp_t *s = luaL_checkudata(L, 1, SOCK_UDP_TNAME);
    int n = luaL_checkinteger(L, 2);
    int timeout = luaL_checkinteger(L, 3);
    sock_udp_ep_t remote, *premote;

    switch (_parse_udp_endpoint(L, 4, &remote)) {
        default:
        case EP_NULL:
            premote = NULL;
            break;
        case EP_PARSED:
            premote = &remote;
            break;
        case EP_ERROR:
            return 2; /* 2 return values, a nil and a message */
    }

    /* This is wasteful: we are allocating an array and then copying the contents
     * to a smaller array instead of resizing, but AFAIK it is not possible to
     * preallocate and then shrink a string in lua.
     */
    void *buf = lua_newuserdata(L, n);
    ssize_t nrecv = sock_udp_recv(s, buf, n, timeout, premote);

    if (nrecv < 0) {
        lua_pop(L, 1);
        lua_pushnil(L);
        lua_pushliteral(L, "error, i'm to lazy to report properly");
        return 2;
    } else {
        lua_pushlstring(L, buf, nrecv);
        lua_replace(L, -2); /*this is to get rid of the userdata */
        return 1;
    }
}

/**
 * Send data through a udp socket.
 *
 * @param   sock
 * @param   data    String containing the data to be sent.
 * @param   remote  (optional) remote end point.
 *
 * @return  number of bytes sent.
 */
static int udp_send(lua_State *L)
{
    sock_udp_t *s = luaL_checkudata(L, 1, SOCK_UDP_TNAME);
    size_t len;
    ssize_t sent;
    const char *data = luaL_checklstring(L, 2, &len);
    sock_udp_ep_t remote, *premote;

    switch (_parse_udp_endpoint(L, 3, &remote)) {
        default:
        case EP_NULL:
            premote = NULL;
            break;
        case EP_PARSED:
            premote = &remote;
            break;
        case EP_ERROR:
            return 2; /* 2 return values, a nil and a message */
    }

    sent = sock_udp_send(s, data, len, premote);

    if (sent < 0) {
        lua_pop(L, 1);
        lua_pushnil(L);
        lua_pushliteral(L, "error, i'm to lazy to report properly");
        return 2;
    } else {
        lua_pushinteger(L, sent);
        return 1;
    }
}

/**
 * Close a UDP socket.
 */
static int udp_close(lua_State *L)
{
    /* s cannot be NULL */
    sock_udp_t *s = luaL_checkudata(L, 1, SOCK_UDP_TNAME);

    sock_udp_close(s);

    return 0;
}

static const luaL_Reg udp_methods[] = {
  {"close", udp_close},
  {"recv", udp_recv},
  {"send", udp_send},
  {NULL, NULL}
};

static const luaL_Reg funcs[] = {
  {"udp", udp_new},
  /* placeholders */
  {"REUSE_EP", NULL},
  {NULL, NULL}
};

/**
 * Load the library.
 *
 * @return      Lua table.
 */
int luaopen_socket(lua_State *L)
{
    if (luaL_newmetatable(L, SOCK_UDP_TNAME)) {
        luaL_newlib(L, udp_methods);
        lua_setfield(L, -2, "__index");
    }

    luaL_newlib(L, funcs);

    lua_pushinteger(L, SOCK_FLAGS_REUSE_EP);
    lua_setfield(L, -2, "REUSE_EP");

    return 1;
}