local ffi = require 'ffi'
local cast = ffi.cast
local C = ffi.C
local UV = nil

print("OS:", ffi.os)
if ffi.os == 'Windows' then
  UV = ffi.load('libuv')
  ffi.cdef [[
    typedef uint32_t     ULONG;
    typedef struct uv_buf_t {
      ULONG len;
      char* base;
    } uv_buf_t;
  ]]
end
if ffi.os == "Linux" then 
  UV = ffi.load('./bin/linux/x64/libuv.so')
  ffi.cdef [[
    typedef struct uv_buf_t {
      char* base;
      size_t len;
    } uv_buf_t;
  ]]
end

ffi.cdef [[
  uv_buf_t uv_buf_init(const char* base, unsigned int len);

  typedef enum {
    UV_EOF = -4095
  } uv_errno_t;

  typedef enum {
    UV_UNKNOWN_HANDLE = 0,
    UV_ASYNC,
    UV_CHECK,
    UV_FS_EVENT,
    UV_FS_POLL,
    UV_HANDLE,
    UV_IDLE,
    UV_NAMED_PIPE,
    UV_POLL,
    UV_PREPARE,
    UV_PROCESS,
    UV_STREAM,
    UV_TCP,
    UV_TIMER,
    UV_TTY,
    UV_UDP,
    UV_SIGNAL,
    UV_FILE,
    UV_HANDLE_TYPE_MAX
  } uv_handle_type;

  typedef enum {
    UV_UNKNOWN_REQ = 0,
    UV_REQ,
    UV_CONNECT,
    UV_WRITE,
    UV_SHUTDOWN,
    UV_UDP_SEND,
    UV_FS,
    UV_WORK,
    UV_GETADDRINFO,
    UV_GETNAMEINFO,
    UV_REQ_TYPE_MAX,
  } uv_req_type;

  size_t uv_loop_size(void);
  size_t uv_req_size(uv_req_type type);
  size_t uv_handle_size(uv_handle_type type);
]]

ffi.cdef(
  string.format(
    [[
    struct uv_loop_s {uint8_t _[%d];};
    struct uv_connect_s {uint8_t _[%d];};
    struct uv_write_s {uint8_t _[%d];};
    struct uv_shutdown_s {uint8_t _[%d];};
    struct uv_getaddrinfo_s {uint8_t _[%d];};
    struct uv_tcp_s {uint8_t _[%d];};
    struct uv_tty_s {uint8_t _[%d];};
    struct uv_pipe_s {uint8_t _[%d];};
    struct uv_timer_s {uint8_t _[%d];};
  ]],
    tonumber(UV.uv_loop_size()),
    tonumber(UV.uv_req_size(UV.UV_CONNECT)),
    tonumber(UV.uv_req_size(UV.UV_WRITE)),
    tonumber(UV.uv_req_size(UV.UV_SHUTDOWN)),
    tonumber(UV.uv_req_size(UV.UV_GETADDRINFO)),
    tonumber(UV.uv_handle_size(UV.UV_TCP)),
    tonumber(UV.uv_handle_size(UV.UV_TTY)),
    tonumber(UV.uv_handle_size(UV.UV_NAMED_PIPE)),
    tonumber(UV.uv_handle_size(UV.UV_TIMER))
  )
)

ffi.cdef [[
  typedef struct uv_loop_s uv_loop_t;
  typedef struct uv_req_s uv_req_t;
  typedef struct uv_write_s uv_write_t;
  typedef struct uv_connect_s uv_connect_t;
  typedef struct uv_shutdown_s uv_shutdown_t;
  typedef struct uv_getaddrinfo_s uv_getaddrinfo_t;
  typedef struct uv_handle_s uv_handle_t;
  typedef struct uv_stream_s uv_stream_t;
  typedef struct uv_tcp_s uv_tcp_t;
  typedef struct uv_tty_s uv_tty_t;
  typedef struct uv_pipe_s uv_pipe_t;
  typedef struct uv_timer_s uv_timer_t;

  typedef enum uv_run_mode_e {
    UV_RUN_DEFAULT = 0,
    UV_RUN_ONCE,
    UV_RUN_NOWAIT
  } uv_run_mode;

  int uv_ip4_addr(const char* ip, int port, struct sockaddr_in* addr);
  int uv_ip6_addr(const char* ip, int port, struct sockaddr_in6* addr);
  int uv_ip4_name(const struct sockaddr_in* src, char* dst, size_t size);
  int uv_ip6_name(const struct sockaddr_in6* src, char* dst, size_t size);
  int uv_inet_ntop(int af, const void* src, char* dst, size_t size);
  int uv_inet_pton(int af, const char* src, void* dst);

  const char* uv_err_name(int err);
  const char* uv_strerror(int err);
]]

local function makeCallback(type)
  local thread, isMain = coroutine.running()
  if not thread or isMain then return end
  local cb
  cb =
    cast(
    type,
    function(...)
      -- print('oncall', ...)
      cb:free()
      assert(coroutine.resume(thread, ...))
    end
  )
  return cb
end

local function uvGetError(status)
  return ffi.string(UV.uv_err_name(status)) .. ': ' .. ffi.string(UV.uv_strerror(status))
end

local function uvCheck(status)
  if status < 0 then
    error(uvGetError(status))
  else
    return status
  end
end

local Loop = {}

-------------------------------------------------------------------------------
-- File
-------------------------------------------------------------------------------
local File = {}

ffi.cdef [[

  typedef uint32_t     DWORD;

  typedef enum {
    UV_FS_UNKNOWN = -1,
    UV_FS_CUSTOM,
    UV_FS_OPEN,
    UV_FS_CLOSE,
    UV_FS_READ,
    UV_FS_WRITE,
    UV_FS_SENDFILE,
    UV_FS_STAT,
    UV_FS_LSTAT,
    UV_FS_FSTAT,
    UV_FS_FTRUNCATE,
    UV_FS_UTIME,
    UV_FS_FUTIME,
    UV_FS_ACCESS,
    UV_FS_CHMOD,
    UV_FS_FCHMOD,
    UV_FS_FSYNC,
    UV_FS_FDATASYNC,
    UV_FS_UNLINK,
    UV_FS_RMDIR,
    UV_FS_MKDIR,
    UV_FS_MKDTEMP,
    UV_FS_RENAME,
    UV_FS_SCANDIR,
    UV_FS_LINK,
    UV_FS_SYMLINK,
    UV_FS_READLINK,
    UV_FS_CHOWN,
    UV_FS_FCHOWN,
    UV_FS_REALPATH,
    UV_FS_COPYFILE,
    UV_FS_LCHOWN,
    UV_FS_OPENDIR,
    UV_FS_READDIR,
    UV_FS_CLOSEDIR,
    UV_FS_MKSTEMP
  } uv_fs_type;

  typedef struct {
    long tv_sec;
    long tv_nsec;
  } uv_timespec_t;

  struct uv__work {
    void (*work)(struct uv__work *w);
    void (*done)(struct uv__work *w, int status);
    struct uv_loop_s* loop;
    void* wq[2];
  };

  typedef struct {
    uint64_t st_dev;
    uint64_t st_mode;
    uint64_t st_nlink;
    uint64_t st_uid;
    uint64_t st_gid;
    uint64_t st_rdev;
    uint64_t st_ino;
    uint64_t st_size;
    uint64_t st_blksize;
    uint64_t st_blocks;
    uint64_t st_flags;
    uint64_t st_gen;
    uv_timespec_t st_atim;
    uv_timespec_t st_mtim;
    uv_timespec_t st_ctim;
    uv_timespec_t st_birthtim;
  } uv_stat_t;
  
  typedef enum {
    UV_DIRENT_UNKNOWN,
    UV_DIRENT_FILE,
    UV_DIRENT_DIR,
    UV_DIRENT_LINK,
    UV_DIRENT_FIFO,
    UV_DIRENT_SOCKET,
    UV_DIRENT_CHAR,
    UV_DIRENT_BLOCK
  } uv_dirent_type_t;

  typedef struct uv_dirent_s {
      const char* name;
      uv_dirent_type_t type;
  } uv_dirent_t;

  typedef struct uv_statfs_s {
    uint64_t f_type;
    uint64_t f_bsize;
    uint64_t f_blocks;
    uint64_t f_bfree;
    uint64_t f_bavail;
    uint64_t f_files;
    uint64_t f_ffree;
    uint64_t f_spare[4];
  } uv_statfs_t;

  typedef struct uv_dir_s {
    uv_dirent_t* dirents;
    size_t nentries;
  } uv_dir_t;  

  typedef struct uv_fs_s uv_fs_t;
  typedef void (*uv_fs_cb)(uv_fs_t* req);

  /* uv_fs_t is a subclass of uv_req_t. */
  typedef struct uv_fs_s {
    void* data;
    uv_req_type type;
    void* reserved[6];
    void* data;
    uv_req_type type;
    void* reserved[6];
    uv_loop_t* loop;
    uv_fs_cb cb;
    size_t result;
    void* ptr;
    const char* path;
    uv_stat_t statbuf;  /* Stores the result of uv_fs_stat() and uv_fs_fstat(). */
    struct uv__work work_req;
    int flags;               
    DWORD sys_errno_;        
    union {                  
      uint16_t* pathw;          
      int fd;                
    } file;                  
    union {                  
      struct {               
        int mode;            
        uint16_t* new_pathw;    
        int file_flags;      
        int fd_out;          
        unsigned int nbufs;  
        uv_buf_t* bufs;      
        int64_t offset;      
        uv_buf_t bufsml[4];  
      } info;                
      struct {               
        double atime;        
        double mtime;        
      } time;                
    } fs;    
  };

  int uv_fs_stat(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb);
  const char* uv_fs_get_path(const uv_fs_t* req);

  int uv_exepath(char* buffer, size_t* size);
  int uv_cwd(char* buffer, size_t* size);
]]

-------------------------------------------------------------------------------
-- Thread
-------------------------------------------------------------------------------

local Thread = {}


ffi.cdef [[

  typedef uint32_t HANDLE;

  typedef HANDLE uv_thread_t;
  typedef void (*uv_thread_cb)(void* arg);

  int uv_thread_create(uv_thread_t* tid, uv_thread_cb entry, void* arg);

  typedef enum {
    UV_THREAD_NO_FLAGS = 0x00,
    UV_THREAD_HAS_STACK_SIZE = 0x01
  } uv_thread_create_flags;
  
  struct uv_thread_options_s {
    unsigned int flags;
    size_t stack_size;
    /* More fields may be added at any time. */
  };
  
  typedef struct uv_thread_options_s uv_thread_options_t;
  
  int uv_thread_create_ex(uv_thread_t* tid, const uv_thread_options_t* params,
                           uv_thread_cb entry, void* arg);
  uv_thread_t uv_thread_self(void);
  int uv_thread_join(uv_thread_t *tid);
  int uv_thread_equal(const uv_thread_t* t1, const uv_thread_t* t2);
]]

-------------------------------------------------------------------------------
-- Req
-------------------------------------------------------------------------------

ffi.cdef [[
  int uv_cancel(uv_req_t* req);
  uv_req_type uv_req_get_type(const uv_req_t* req);
  const char* uv_req_type_name(uv_req_type type);
]]

local Req = {}

function Req:cancel()
  return uvCheck(UV.uv_cancel(cast('uv_req_t*', self)))
end

function Req:getType()
  local id = UV.uv_req_get_type(cast('uv_req_t*', self))
  return ffi.string(UV.uv_req_type_name(id))
end

-------------------------------------------------------------------------------
-- Connect
-------------------------------------------------------------------------------

local Connect = setmetatable({}, {__index = Req})
Connect.type = ffi.typeof 'uv_connect_t'
ffi.metatype(Connect.type, {__index = Connect})
function Connect.new()
  return Connect.type()
end

-------------------------------------------------------------------------------
-- Write
-------------------------------------------------------------------------------

local Write = setmetatable({}, {__index = Req})
Write.type = ffi.typeof 'uv_write_t'
ffi.metatype(Write.type, {__index = Write})
function Write.new()
  return Write.type()
end

-------------------------------------------------------------------------------
-- Shutdown
-------------------------------------------------------------------------------

local Shutdown = setmetatable({}, {__index = Req})
Shutdown.type = ffi.typeof 'uv_shutdown_t'
ffi.metatype(Shutdown.type, {__index = Shutdown})
function Shutdown.new()
  return Shutdown.type()
end

-------------------------------------------------------------------------------
-- Getaddrinfo
-------------------------------------------------------------------------------

ffi.cdef [[
  enum {
    AF_UNSPEC = 0,
    AF_INET = 2,
    AF_INET6 = 10
  };
  enum {
    SOCK_STREAM = 1,
    SOCK_DGRAM = 2
  };

  typedef int32_t socklen_t;

  struct addrinfo {
    int              ai_flags;
    int              ai_family;
    int              ai_socktype;
    int              ai_protocol;
    socklen_t        ai_addrlen;
    struct sockaddr *ai_addr;
    char            *ai_canonname;
    struct addrinfo *ai_next;
  };

  typedef void (*uv_getaddrinfo_cb)(uv_getaddrinfo_t* req, int status, struct addrinfo* res);

  int uv_getaddrinfo(
    uv_loop_t* loop, uv_getaddrinfo_t* req, uv_getaddrinfo_cb getaddrinfo_cb,
    const char* node, const char* service, const struct addrinfo* hints
  );
  void uv_freeaddrinfo(struct addrinfo* ai);
]]

local Getaddrinfo = setmetatable({}, {__index = Req})
Getaddrinfo.type = ffi.typeof 'uv_getaddrinfo_t'
ffi.metatype(Getaddrinfo.type, {__index = Getaddrinfo})
function Getaddrinfo.new()
  return Getaddrinfo.type()
end

local families = {
  inet = UV.AF_INET,
  inet6 = UV.AF_INET6,
  [UV.AF_INET] = 'inet',
  [UV.AF_INET6] = 'inet6'
}

local socktypes = {
  stream = UV.SOCK_STREAM,
  dgram = UV.SOCK_DGRAM,
  [UV.SOCK_STREAM] = 'stream',
  [UV.SOCK_DGRAM] = 'dgram'
}

function Loop:getaddrinfo(node, service, hints)
  local req = Getaddrinfo.new()
  p(req)
  local opts = ffi.new('struct addrinfo')
  if type(service) == 'number' then
    service = tostring(service)
  end
  if hints then
    if hints.family then
      opts.ai_family = assert(families[hints.family], 'Unknown family name')
    end
    if hints.socktype then
      opts.ai_socktype = assert(socktypes[hints.socktype], 'Unknown socktype name')
    end
  end
  p(node, service, hints)
  uvCheck(UV.uv_getaddrinfo(self, req, makeCallback 'uv_getaddrinfo_cb', node, service, opts))
  local _, status, res = coroutine.yield()
  uvCheck(status)
  local results = {}
  local i = 1
  while res ~= nil do
    local family = families[res.ai_family]
    local socktype = socktypes[res.ai_socktype]
    local entry = {
      family = family,
      socktype = socktype
    }
    results[i] = entry
    i = i + 1
    if family == 'inet' then
      local buf = ffi.new 'char[16]'
      local addr = cast('const struct sockaddr_in*', res.ai_addr)
      uvCheck(UV.uv_ip4_name(addr, buf, 16))
      entry.addr = ffi.string(buf)
      entry.port = C.ntohs(addr.sin_port)
    elseif family == 'inet6' then
      local buf = ffi.new 'char[46]'
      local addr = cast('const struct sockaddr_in6*', res.ai_addr)
      uvCheck(UV.uv_ip6_name(addr, buf, 46))
      entry.addr = ffi.string(buf)
      entry.port = C.ntohs(addr.sin6_port)
    end
    res = res.ai_next
  end
  return results
end

-------------------------------------------------------------------------------
-- Handle
-------------------------------------------------------------------------------

ffi.cdef [[
  typedef void (*uv_walk_cb)(uv_handle_t* handle, void* arg);
  typedef void (*uv_close_cb)(uv_handle_t* handle);
  typedef void (*uv_alloc_cb)(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf);

  struct sockaddr {
    unsigned short    sa_family;
    char              sa_data[14];
  };

  struct in_addr {
    unsigned long s_addr;
  };

  struct sockaddr_in {
    short            sin_family;
    unsigned short   sin_port;
    struct in_addr   sin_addr;
    char             sin_zero[8];
  };

  struct in6_addr {
    unsigned char   s6_addr[16];
  };

  struct sockaddr_in6 {
    uint16_t        sin6_family;
    uint16_t        sin6_port;
    uint32_t        sin6_flowinfo;
    struct in6_addr sin6_addr;
    uint32_t        sin6_scope_id;
  };

  typedef uint16_t sa_family_t;

  struct sockaddr_storage {
    sa_family_t  ss_family;
    char      __ss_pad1[6];
    int64_t   __ss_align;
    char      __ss_pad2[112];
  };

  uint32_t htonl(uint32_t hostlong);
  uint16_t htons(uint16_t hostshort);
  uint32_t ntohl(uint32_t netlong);
  uint16_t ntohs(uint16_t netshort);

  void *malloc(size_t size);
  void free(void *ptr);

  typedef int uv_file;

  uv_handle_type uv_guess_handle(uv_file file);

  int uv_is_active(const uv_handle_t* handle);
  int uv_is_closing(const uv_handle_t* handle);
  void uv_close(uv_handle_t* handle, uv_close_cb close_cb);
  void uv_ref(uv_handle_t* handle);
  void uv_unref(uv_handle_t* handle);
  int uv_has_ref(const uv_handle_t* handle);
  int uv_send_buffer_size(uv_handle_t* handle, int* value);
  int uv_recv_buffer_size(uv_handle_t* handle, int* value);
  uv_loop_t* uv_handle_get_loop(const uv_handle_t* handle);
  void* uv_handle_get_data(const uv_handle_t* handle);
  void* uv_handle_set_data(uv_handle_t* handle, void* data);
  uv_handle_type uv_handle_get_type(const uv_handle_t* handle);
  const char* uv_handle_type_name(uv_handle_type type);
]]

local function onAlloc(handle, suggestedSize, buf)
  local cached = cast('uv_buf_t*', UV.uv_handle_get_data(handle))
  if cached ~= nil then
    buf.base = cached.base
    buf.len = cached.len
  else
    local base = C.malloc(suggestedSize)
    buf.base = base
    buf.len = suggestedSize
    -- Store the data in handle->data as a uv_buf_t*
    local data = cast('uv_buf_t*', C.malloc(ffi.sizeof 'uv_buf_t'))
    data.base = base
    data.len = suggestedSize
    UV.uv_handle_set_data(handle, data)
  end
end

local allocCb = cast('uv_alloc_cb', onAlloc)

function Loop.guessHandle(fd)
  return ffi.string(UV.uv_handle_type_name(UV.uv_guess_handle(fd)))
end

local Handle = {}

function Handle:isActive()
  return UV.uv_is_active(cast('uv_handle_t*', self)) ~= 0
end

function Handle:isClosing()
  return UV.uv_is_closing(cast('uv_handle_t*', self)) ~= 0
end

function Handle:close()
  local handle = cast('uv_handle_t*', self)
  local _, main = coroutine.running()
  if main then
    UV.uv_close(handle, nil)
  else
    UV.uv_close(handle, makeCallback 'uv_close_cb')
    coroutine.yield()
  end
  local cached = cast('uv_buf_t*', UV.uv_handle_get_data(handle))
  if cached ~= nil then
    C.free(cached.base)
    C.free(cached)
  end
end

function Handle:ref()
  return UV.uv_ref(cast('uv_handle_t*', self))
end

function Handle:unref()
  return UV.uv_unref(cast('uv_handle_t*', self))
end

function Handle:hasRef()
  return UV.uv_has_ref(cast('uv_handle_t*', self)) ~= 0
end

function Handle:setSendBufferSize(value)
  uvCheck(UV.uv_send_buffer_size(cast('uv_handle_t*', self), value))
end

function Handle:getSendBufferSize()
  local out = ffi.new('int[1]')
  uvCheck(UV.uv_send_buffer_size(cast('uv_handle_t*', self), out))
  return out[0]
end

function Handle:setRecvBufferSize(value)
  uvCheck(UV.uv_recv_buffer_size(cast('uv_handle_t*', self), value))
end

function Handle:getRecvBufferSize()
  local out = ffi.new('int[1]')
  uvCheck(UV.uv_recv_buffer_size(cast('uv_handle_t*', self), out))
  return out[0]
end

function Handle:getLoop()
  return UV.uv_handle_get_loop(cast('uv_handle_t*', self))
end

function Handle:getType()
  local id = UV.uv_handle_get_type(cast('uv_handle_t*', self))
  return ffi.string(UV.uv_handle_type_name(id))
end

-------------------------------------------------------------------------------
-- Stream
-------------------------------------------------------------------------------

ffi.cdef [[
 
  typedef struct sws_handle_req_t{

      struct uv_tcp_s handle;
      int id;
      struct sws_parse_req_t parse_req;
  } sws_handle_req_t;

  typedef void (*uv_read_cb)(uv_stream_t* stream, int64_t nread, const uv_buf_t* buf);
  typedef void (*uv_write_cb)(uv_write_t* req, int status);
  typedef void (*uv_connect_cb)(uv_connect_t* req, int status);
  typedef void (*uv_shutdown_cb)(uv_shutdown_t* req, int status);
  typedef void (*uv_connection_cb)(uv_stream_t* server, int status);

  int uv_shutdown(uv_shutdown_t* req, uv_stream_t* handle, uv_shutdown_cb cb);
  int uv_listen(uv_stream_t* stream, int backlog, uv_connection_cb cb);
  int uv_accept(uv_stream_t* server, uv_stream_t* client);
  int uv_read_start(uv_stream_t* stream, uv_alloc_cb alloc_cb, uv_read_cb read_cb);
  int uv_read_stop(uv_stream_t*);
  int uv_write(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, uv_write_cb cb);
  int uv_write2(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, 
                uv_stream_t* send_handle, uv_write_cb cb);
  int uv_try_write(uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs);
  int uv_is_readable(const uv_stream_t* handle);
  int uv_is_writable(const uv_stream_t* handle);
  int uv_stream_set_blocking(uv_stream_t* handle, int blocking);
  size_t uv_stream_get_write_queue_size(const uv_stream_t* stream);
]]

local Stream = setmetatable({}, {__index = Handle})

function Stream:shutdown()
  local req = Shutdown.new()
  uvCheck(UV.uv_shutdown(req, cast('uv_stream_t*', self), makeCallback 'uv_shutdown_cb'))
  local _, status = coroutine.yield()
  uvCheck(status)
end

function Stream:listen(backlog, onConnection)
  local cb = cast('uv_connection_cb', onConnection)
  uvCheck(UV.uv_listen(cast('uv_stream_t*', self), backlog, cb))
  return cb
end

function Stream:accept(client)
  uvCheck(UV.uv_accept(cast('uv_stream_t*', self), cast('uv_stream_t*', client)))
end

function Stream:readStart(onRead)
  local function onEvent(_, status, buf)
    print('onRead', _, status, buf)
    if status == 0 then
      return
    end
    if status == UV.UV_EOF then
      onRead(nil)
    elseif status < 0 then
      return onRead(uvGetError(status))
    else
      onRead(nil, ffi.string(buf.base, status))
    end
  end
  local cb = cast('uv_read_cb', onEvent)
  uvCheck(UV.uv_read_start(cast('uv_stream_t*', self), allocCb, cb))
end

function Stream:readStop()
  uvCheck(UV.uv_read_stop(cast('uv_stream_t*', self)))
end

function Stream:write(data)
  local req = Write.new()
  local bufs = ffi.new('uv_buf_t[1]')
  bufs[0].base = cast('char*', data)
  bufs[0].len = #data
  local cb = makeCallback 'uv_write_cb'
  uvCheck(UV.uv_write(req, cast('uv_stream_t*', self), bufs, 1, cb))
  if cb then
    local _, status = coroutine.yield()
    uvCheck(status)
  end
end

function Stream:write2(handle)
  local req = Write.new()
  local cb = makeCallback 'uv_write_cb'
  uvCheck(UV.uv_write(req, cast('uv_stream_t*', self), nil, 0, cast('uv_stream_t*', handle), cb))
  local _, status = coroutine.yield()
  uvCheck(status)
end

function Stream:tryWrite(data)
  local bufs = ffi.new('uv_buf_t[1]')
  bufs[0].base = cast('char*', data)
  bufs[0].len = #data
  return uvCheck(UV.uv_try_write(cast('uv_stream_t*', self), bufs, 1))
end

function Stream:isReadable()
  return UV.uv_is_readable(cast('uv_stream_t*', self)) ~= 0
end

function Stream:isWritable()
  return UV.uv_is_writable(cast('uv_stream_t*', self)) ~= 0
end

function Stream:setBlocking(blocking)
  uvCheck(UV.uv_set_blocking(cast('uv_stream_t*', self), blocking))
end

function Stream:getWriteQueueSize()
  return tonumber(UV.uv_stream_get_write_queue_size(cast('uv_stream_t*', self)))
end

-------------------------------------------------------------------------------
-- Tcp
-------------------------------------------------------------------------------

ffi.cdef [[
  int uv_tcp_init(uv_loop_t* loop, uv_tcp_t* handle);
  int uv_tcp_bind(uv_tcp_t* handle, const struct sockaddr* addr, unsigned int flags);
  int uv_tcp_connect(uv_connect_t* req, uv_tcp_t* handle, const struct sockaddr_in* addr, uv_connect_cb cb);
]]

local Tcp = setmetatable({}, {__index = Stream})
Tcp.type = ffi.typeof 'uv_tcp_t'

function Loop:newTcp()
  local tcp = Tcp.type()
  uvCheck(UV.uv_tcp_init(self, tcp))
  return tcp
end

function Tcp:getsockname()
  -- TODO: Improve output
  return UV.uv_tcp_getsockname(self)
end

function Tcp:getpeername()
  -- TODO: Improve output
  return UV.uv_tcp_getpeername(self)
end

function Tcp:connect(host, port)
  local req = Connect.new()
  local addr = ffi.new("struct sockaddr_in [1]")
  UV.uv_ip4_addr(host, port, cast("struct sockaddr_in *", addr))
  uvCheck(UV.uv_tcp_connect(req, self, addr, makeCallback 'uv_connect_cb'))
  local _, status = coroutine.yield()
  uvCheck(status)
end

ffi.metatype(Tcp.type, {__index = Tcp})

-------------------------------------------------------------------------------
-- Tty
-------------------------------------------------------------------------------

ffi.cdef [[
  typedef enum {
    /* Initial/normal terminal mode */
    UV_TTY_MODE_NORMAL,
    /* Raw input mode (On Windows, ENABLE_WINDOW_INPUT is also enabled) */
    UV_TTY_MODE_RAW,
    /* Binary-safe I/O mode for IPC (Unix-only) */
    UV_TTY_MODE_IO
  } uv_tty_mode_t;

  int uv_tty_init(uv_loop_t* loop, uv_tty_t* handle, uv_file fd, int unused);
  int uv_tty_set_mode(uv_tty_t* handle, uv_tty_mode_t mode);
  int uv_tty_reset_mode(void);
  int uv_tty_get_winsize(uv_tty_t* handle, int* width, int* height);
]]

local Tty = setmetatable({}, {__index = Stream})
Tty.type = ffi.typeof 'uv_tty_t'

function Loop:newTty(fd)
  local tty = Tty.type()
  uvCheck(UV.uv_tty_init(self, tty, fd, 0))
  return tty
end

function Tty:setMode(mode)
  uvCheck(
    UV.uv_tty_set_mode(self, assert(UV['UV_TTY_MODE_' .. string.upper(mode)], 'Unknown tty mode'))
  )
end

function Tty:resetMode()
  uvCheck(UV.uv_tty_reset_mode())
end

function Tty:getWinsize()
  local width = ffi.new('int[1]')
  local height = ffi.new('int[1]')
  uvCheck(UV.uv_tty_get_winsize(self, width, height))
  return tonumber(width[0]), tonumber(height[0])
end

ffi.metatype(Tty.type, {__index = Tty})

-------------------------------------------------------------------------------
-- Pipe
-------------------------------------------------------------------------------

ffi.cdef [[
  int uv_pipe_init(uv_loop_t* loop, uv_pipe_t* handle, int ipc);
  int uv_pipe_open(uv_pipe_t* handle, uv_file file);
  int uv_pipe_bind(uv_pipe_t* handle, const char* name);
  void uv_pipe_connect(uv_connect_t* req, uv_pipe_t* handle, const char* name, uv_connect_cb cb);
  int uv_pipe_getsockname(const uv_pipe_t* handle, char* buffer, size_t* size);
  int uv_pipe_getpeername(const uv_pipe_t* handle, char* buffer, size_t* size);
  void uv_pipe_pending_instances(uv_pipe_t* handle, int count);
  int uv_pipe_pending_count(uv_pipe_t* handle);
  uv_handle_type uv_pipe_pending_type(uv_pipe_t* handle);
  int uv_pipe_chmod(uv_pipe_t* handle, int flags);
]]

local Pipe = setmetatable({}, {__index = Stream})
Pipe.type = ffi.typeof 'uv_pipe_t'

function Loop:newPipe(ipc)
  local pipe = Pipe.type()
  uvCheck(UV.uv_pipe_init(self, pipe, ipc and 1 or 0))
  return pipe
end

function Pipe:open(fd)
  uvCheck(UV.uv_pipe_open(self, fd))
end

function Pipe:bind(path)
  uvCheck(UV.uv_pipe_bind(self, path))
end

-- TODO: add more pipe functions

ffi.metatype(Pipe.type, {__index = Pipe})

-------------------------------------------------------------------------------
-- Timer
-------------------------------------------------------------------------------

ffi.cdef [[
  typedef void (*uv_timer_cb)(uv_timer_t* handle);

  int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle);
  int uv_timer_start(uv_timer_t* handle, uv_timer_cb cb, uint64_t timeout, uint64_t repeat);
  int uv_timer_stop(uv_timer_t* handle);
  int uv_timer_again(uv_timer_t* handle);
  void uv_timer_set_repeat(uv_timer_t* handle, uint64_t repeat);
  uint64_t uv_timer_get_repeat(const uv_timer_t* handle);
]]

local Timer = setmetatable({}, {__index = Handle})
Timer.Type = ffi.typeof 'uv_timer_t'

function Loop:newTimer()
  local timer = Timer.Type()
  uvCheck(UV.uv_timer_init(self, timer))
  return timer
end

function Timer:sleep(timeout)
  uvCheck(UV.uv_timer_start(self, makeCallback 'uv_timer_cb', timeout, 0))
  coroutine.yield()
  uvCheck(UV.uv_timer_stop(self))
end

function Timer:start(callback, timeout, rep)
  local cb = cast('uv_timer_cb', timeout)
  uvCheck(UV.uv_timer_start(self, callback, cb, rep))
  return cb
end

function Timer:stop()
  uvCheck(UV.uv_timer_stop(self))
end

function Timer:again()
  uvCheck(UV.uv_timer_again(self))
end

function Timer:setRepeat(rep)
  uvCheck(UV.uv_timer_set_repeat(self, rep))
end

function Timer:getRepeat()
  return UV.uv_timer_get_repeat(self)
end

local function onGc(handle)
  if not handle:isClosing() then
    -- We can't safely close handles here because onClose happens after the
    -- struct is freed by lua.
    -- Instead abort the process so the programmer can fix it early.
    handle = cast('uv_' .. Handle.getType(handle) .. '_t*', handle)
    error('Unclosed ' .. tostring(handle) .. ' got garbage collected')
  end
end

ffi.metatype(Timer.Type, {__index = Timer, __gc = onGc})

-------------------------------------------------------------------------------
-- Loop
-------------------------------------------------------------------------------

ffi.cdef [[
  uv_loop_t* uv_default_loop();
  int uv_loop_init(uv_loop_t* loop);
  int uv_loop_close(uv_loop_t* loop);
  int uv_loop_alive(const uv_loop_t* loop);
  void uv_stop(uv_loop_t* loop);
  uint64_t uv_now(const uv_loop_t* loop);
  void uv_update_time(uv_loop_t* loop);
  void uv_walk(uv_loop_t* loop, uv_walk_cb walk_cb, void* arg);
  int uv_run(uv_loop_t* loop, uv_run_mode mode);
]]

local LoopType = ffi.typeof 'uv_loop_t'

function Loop.new()
  local loop = LoopType()
  uvCheck(UV.uv_loop_init(loop))
  return loop
end

function Loop:close()
  uvCheck(UV.uv_loop_close(self))
end

function Loop:alive()
  return UV.uv_loop_alive(self) ~= 0
end

function Loop:stop()
  return UV.uv_loop_stop(self)
end

function Loop:now()
  return UV.uv_loop_now(self)
end

function Loop:updateTime()
  return UV.uv_update_time(self)
end

function Loop:walk(callback)
  local function onHandle(handle)
    callback(cast('uv_' .. Handle.getType(handle) .. '_t*', handle))
  end
  local cb = cast('uv_walk_cb', onHandle)
  UV.uv_walk(self, cb, nil)
  cb:free()
end

function Loop:run(mode)
  mode = assert(UV['UV_RUN_' .. string.upper(mode)], 'Unknown run mode')
  return tonumber(uvCheck(UV.uv_run(self, mode)))
end

function Loop:bind(server, host, port)
  local addr = ffi.new("struct sockaddr_in [1]")
  local res = UV.uv_ip4_addr(host, port, cast("struct sockaddr_in *", addr))
  uvCheck(UV.uv_tcp_bind(server, cast("struct sockaddr *", addr), 0 ))
end

function Loop:listen(tcp, backlog, onConnect)
  tcp:listen( backlog, onConnect )
end


ffi.metatype(LoopType, {__index = Loop})

-------------------------------------------------------------------------------

Loop.Req = Req
Loop.Connect = Connect
Loop.Write = Write
Loop.Shutdown = Shutdown
Loop.Handle = Handle
Loop.Stream = Stream
Loop.Tcp = Tcp
Loop.Timer = Timer
Loop.uv = UV

return UV.uv_default_loop()
