//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//

#include <lua.h>
#include <lauxlib.h>

#include <unistd.h>
#include "string.h"
#include "socket.h"

////////////////////////////////////////////////////////////////////////////////

static const char *SOCKET_ANY     = "__thrift_socket_any";
static const char *SOCKET_CONN    = "__thrift_socket_connected";

static const char *SOCKET_GENERIC = "__thrift_socket_generic";
static const char *SOCKET_CLIENT  = "__thrift_socket_client";
static const char *SOCKET_SERVER  = "__thrift_socket_server";

static const char *DEFAULT_HOST   = "localhost";

typedef struct __t_tcp {
  t_socket sock;
  int timeout; // Milliseconds
} t_tcp;
typedef t_tcp *p_tcp;

////////////////////////////////////////////////////////////////////////////////
// Util

static void throw_argerror(lua_State *L, int index, const char *expected) {
  char msg[256];
  sprintf(msg, "%s expected, got %s", expected, luaL_typename(L, index));
  luaL_argerror(L, index, msg);
}

static void *checkgroup(lua_State *L, int index, const char *groupname) {
  if (!lua_getmetatable(L, index)) {
    throw_argerror(L, index, groupname);
  }

  lua_pushstring(L, groupname);
  lua_rawget(L, -2);
  if (lua_isnil(L, -1)) {
    lua_pop(L, 2);
    throw_argerror(L, index, groupname);
  } else {
    lua_pop(L, 2);
    return lua_touserdata(L, index);
  }
  return NULL; // Not reachable
}

static void *checktype(lua_State *L, int index, const char *typename) {
  if (strcmp(typename, SOCKET_ANY) == 0 ||
      strcmp(typename, SOCKET_CONN) == 0) {
    return checkgroup(L, index, typename);
  } else {
    return luaL_checkudata(L, index, typename);
  }
}

static void settype(lua_State *L, int index, const char *typename) {
  luaL_getmetatable(L, typename);
  lua_setmetatable(L, index);
}

#define LUA_SUCCESS_RETURN(L) \
  lua_pushnumber(L, 1); \
  return 1

#define LUA_CHECK_RETURN(L, err) \
  if (err) { \
    lua_pushnil(L); \
    lua_pushstring(L, err); \
    return 2; \
  } \
  LUA_SUCCESS_RETURN(L)

////////////////////////////////////////////////////////////////////////////////

static int l_socket_create(lua_State *L);
static int l_socket_destroy(lua_State *L);
static int l_socket_settimeout(lua_State *L);
static int l_socket_getsockinfo(lua_State *L);

static int l_socket_accept(lua_State *L);
static int l_socket_listen(lua_State *L);

static int l_socket_create_and_connect(lua_State *L);
static int l_socket_connect(lua_State *L);
static int l_socket_send(lua_State *L);
static int l_socket_receive(lua_State *L);

////////////////////////////////////////////////////////////////////////////////

static const struct luaL_Reg methods_generic[] = {
  {"destroy",     l_socket_destroy},
  {"settimeout",  l_socket_settimeout},
  {"getsockinfo", l_socket_getsockinfo},
  {"listen",      l_socket_listen},
  {"connect",     l_socket_connect},
  {NULL, NULL}
};

static const struct luaL_Reg methods_server[] = {
  {"destroy",     l_socket_destroy},
  {"getsockinfo", l_socket_getsockinfo},
  {"accept",      l_socket_accept},
  {"send",        l_socket_send},
  {"receive",     l_socket_receive},
  {NULL, NULL}
};

static const struct luaL_Reg methods_client[] = {
  {"destroy",     l_socket_destroy},
  {"settimeout",  l_socket_settimeout},
  {"getsockinfo", l_socket_getsockinfo},
  {"send",        l_socket_send},
  {"receive",     l_socket_receive},
  {NULL, NULL}
};

static const struct luaL_Reg funcs_luasocket[] = {
  {"create",             l_socket_create},
  {"create_and_connect", l_socket_create_and_connect},
  {NULL, NULL}
};

////////////////////////////////////////////////////////////////////////////////

// Check/enforce inheritance
static void add_to_group(lua_State *L,
                  const char *metatablename,
                  const char *groupname) {
  luaL_getmetatable(L, metatablename); // mt
  lua_pushstring(L, groupname);        // mt, "name"
  lua_pushboolean(L, 1);               // mt, "name", true
  lua_rawset(L, -3);                   // mt
  lua_pop(L, 1);
}

static void set_methods(lua_State *L,
  const char *metatablename,
  const struct luaL_Reg *methods) {
  luaL_getmetatable(L, metatablename);   // mt
  // Create the __index table
  lua_pushstring(L, "__index");          // mt, "__index"
  lua_newtable(L);                       // mt, "__index", t
  for (; methods->name; methods++) {
    lua_pushstring(L, methods->name);    // mt, "__index", t, "name"
    lua_pushcfunction(L, methods->func); // mt, "__index", t, "name", func
    lua_rawset(L, -3);                   // mt, "__index", t
  }
  lua_rawset(L, -3);                     // mt
  lua_pop(L, 1);
}

int luaopen_libluasocket(lua_State *L) {
  luaL_newmetatable(L, SOCKET_GENERIC);
  luaL_newmetatable(L, SOCKET_CLIENT);
  luaL_newmetatable(L, SOCKET_SERVER);
  lua_pop(L, 3);
  add_to_group(L, SOCKET_GENERIC, SOCKET_ANY);
  add_to_group(L, SOCKET_CLIENT, SOCKET_ANY);
  add_to_group(L, SOCKET_SERVER, SOCKET_ANY);
  add_to_group(L, SOCKET_CLIENT, SOCKET_CONN);
  add_to_group(L, SOCKET_SERVER, SOCKET_CONN);
  set_methods(L, SOCKET_GENERIC, methods_generic);
  set_methods(L, SOCKET_CLIENT, methods_client);
  set_methods(L, SOCKET_SERVER, methods_server);
#if LUA_VERSION_NUM >= 502
    lua_newtable(L);
    luaL_setfuncs(L, funcs_luasocket, 0);
#else
  luaL_register(L, "luasocket", funcs_luasocket);
#endif
  return 1;
}

////////////////////////////////////////////////////////////////////////////////
// General

// sock,err create(bind_host, bind_port)
// sock,err create(bind_host) -> any port
// sock,err create() -> any port on localhost
static int l_socket_create(lua_State *L) {
  const char *err;
  t_socket sock;
  const char *addr = lua_tostring(L, 1);
  if (!addr) {
    addr = DEFAULT_HOST;
  }
  unsigned short port = lua_tonumber(L, 2);
  err = tcp_create(&sock);
  if (!err) {
    err = tcp_bind(&sock, addr, port); // bind on create
    if (err) {
      tcp_destroy(&sock);
    } else {
      p_tcp tcp = (p_tcp) lua_newuserdata(L, sizeof(t_tcp));
      settype(L, -2, SOCKET_GENERIC);
      socket_setnonblocking(&sock);
      tcp->sock = sock;
      tcp->timeout = 0;
      return 1; // Return userdata
    }
  }
  LUA_CHECK_RETURN(L, err);
}

// destroy()
static int l_socket_destroy(lua_State *L) {
  p_tcp tcp = (p_tcp) checktype(L, 1, SOCKET_ANY);
  const char *err = tcp_destroy(&tcp->sock);
  LUA_CHECK_RETURN(L, err);
}

// send(socket, data)
static int l_socket_send(lua_State *L) {
  p_tcp self = (p_tcp) checktype(L, 1, SOCKET_CONN);
  p_tcp tcp = (p_tcp) checktype(L, 2, SOCKET_CONN);
  size_t len;
  const char *data = luaL_checklstring(L, 3, &len);
  const char *err =
    tcp_send(&tcp->sock, data, len, tcp->timeout);
  LUA_CHECK_RETURN(L, err);
}

#define LUA_READ_STEP 8192
static int l_socket_receive(lua_State *L) {
  p_tcp self = (p_tcp) checktype(L, 1, SOCKET_CONN);
  p_tcp handle = (p_tcp) checktype(L, 2, SOCKET_CONN);
  size_t len = luaL_checknumber(L, 3);
  char buf[LUA_READ_STEP];
  const char *err = NULL;
  int received;
  size_t got = 0, step = 0;
  luaL_Buffer b;

  luaL_buffinit(L, &b);
  do {
    step = (LUA_READ_STEP < len - got ? LUA_READ_STEP : len - got);
    err = tcp_raw_receive(&handle->sock, buf, step, self->timeout, &received);
    if (err == NULL) {
      luaL_addlstring(&b, buf, received);
      got += received;
    }
  } while (err == NULL && got < len);

  if (err) {
    lua_pushnil(L);
    lua_pushstring(L, err);
    return 2;
  }
  luaL_pushresult(&b);
  return 1;
}

// settimeout(timeout)
static int l_socket_settimeout(lua_State *L) {
  p_tcp self = (p_tcp) checktype(L, 1, SOCKET_ANY);
  int timeout = luaL_checknumber(L, 2);
  self->timeout = timeout;
  LUA_SUCCESS_RETURN(L);
}

// table getsockinfo()
static int l_socket_getsockinfo(lua_State *L) {
  char buf[256];
  short port = 0;
  p_tcp tcp = (p_tcp) checktype(L, 1, SOCKET_ANY);
  if (socket_get_info(&tcp->sock, &port, buf, 256) == SUCCESS) {
    lua_newtable(L);                    // t
    lua_pushstring(L, "host");          // t, "host"
    lua_pushstring(L, buf);             // t, "host", buf
    lua_rawset(L, -3);                  // t
    lua_pushstring(L, "port");          // t, "port"
    lua_pushnumber(L, port);            // t, "port", port
    lua_rawset(L, -3);                  // t
    return 1;
  }
  return 0;
}

////////////////////////////////////////////////////////////////////////////////
// Server

// accept()
static int l_socket_accept(lua_State *L) {
  const char *err;
  p_tcp self = (p_tcp) checktype(L, 1, SOCKET_SERVER);
  t_socket sock;
  err = tcp_accept(&self->sock, &sock, self->timeout);
  if (!err) { // Success
    // Create a reference to the client
    p_tcp client = (p_tcp) lua_newuserdata(L, sizeof(t_tcp));
    settype(L, 2, SOCKET_CLIENT);
    socket_setnonblocking(&sock);
    client->sock = sock;
    client->timeout = self->timeout;
    return 1;
  }
  LUA_CHECK_RETURN(L, err);
}

static int l_socket_listen(lua_State *L) {
  const char* err;
  p_tcp tcp = (p_tcp) checktype(L, 1, SOCKET_GENERIC);
  int backlog = 10;
  err = tcp_listen(&tcp->sock, backlog);
  if (!err) {
    // Set the current as a server
    settype(L, 1, SOCKET_SERVER); // Now a server
  }
  LUA_CHECK_RETURN(L, err);
}

////////////////////////////////////////////////////////////////////////////////
// Client

// create_and_connect(host, port, timeout)
extern double __gettime();
static int l_socket_create_and_connect(lua_State *L) {
  const char* err = NULL;
  double end;
  t_socket sock;
  const char *host = luaL_checkstring(L, 1);
  unsigned short port = luaL_checknumber(L, 2);
  int timeout = luaL_checknumber(L, 3);

  // Create and connect loop for timeout milliseconds
  end = __gettime() + timeout/1000;
  do {
    // Create and connect the socket
    err = tcp_create_and_connect(&sock, host, port, timeout);
    if (err) {
      tcp_destroy(&sock);
      usleep(100000); // sleep for 100ms
    } else {
      p_tcp tcp = (p_tcp) lua_newuserdata(L, sizeof(t_tcp));
      settype(L, -2, SOCKET_CLIENT);
      socket_setnonblocking(&sock);
      tcp->sock = sock;
      tcp->timeout = timeout;
      return 1; // Return userdata
    }
  } while (err && __gettime() < end);

  LUA_CHECK_RETURN(L, err);
}

// connect(host, port)
static int l_socket_connect(lua_State *L) {
  const char *err;
  p_tcp tcp = (p_tcp) checktype(L, 1, SOCKET_GENERIC);
  const char *host = luaL_checkstring(L, 2);
  unsigned short port = luaL_checknumber(L, 3);
  err = tcp_connect(&tcp->sock, host, port, tcp->timeout);
  if (!err) {
    settype(L, 1, SOCKET_CLIENT); // Now a client
  }
  LUA_CHECK_RETURN(L, err);
}
