// Module for interfacing with file system

#include "module.h"
#include "lauxlib.h"
#include "lmem.h"
#include "platform.h"

#include "c_types.h"
#include "vfs.h"
#include "c_string.h"

#include <alloca.h>

#define FILE_READ_CHUNK 1024

// use this time/date in absence of a timestamp
#define FILE_TIMEDEF_YEAR 1970
#define FILE_TIMEDEF_MON 01
#define FILE_TIMEDEF_DAY 01
#define FILE_TIMEDEF_HOUR 00
#define FILE_TIMEDEF_MIN 00
#define FILE_TIMEDEF_SEC 00

static int file_fd = 0;
static int file_fd_ref = LUA_NOREF;
static int rtc_cb_ref = LUA_NOREF;

typedef struct _file_fd_ud {
  int fd;
} file_fd_ud;

static void table2tm( lua_State *L, vfs_time *tm )
{
  int idx = lua_gettop( L );

  // extract items from table
  lua_getfield( L, idx, "year" );
  lua_getfield( L, idx, "mon" );
  lua_getfield( L, idx, "day" );
  lua_getfield( L, idx, "hour" );
  lua_getfield( L, idx, "min" );
  lua_getfield( L, idx, "sec" );

  tm->year = luaL_optint( L, ++idx, FILE_TIMEDEF_YEAR );
  tm->mon  = luaL_optint( L, ++idx, FILE_TIMEDEF_MON );
  tm->day  = luaL_optint( L, ++idx, FILE_TIMEDEF_DAY );
  tm->hour = luaL_optint( L, ++idx, FILE_TIMEDEF_HOUR );
  tm->min  = luaL_optint( L, ++idx, FILE_TIMEDEF_MIN );
  tm->sec  = luaL_optint( L, ++idx, FILE_TIMEDEF_SEC );

  // remove items from stack
  lua_pop( L, 6 );
}

static sint32_t file_rtc_cb( vfs_time *tm )
{
  sint32_t res = VFS_RES_ERR;

  if (rtc_cb_ref != LUA_NOREF) {
    lua_State *L = lua_getstate();

    lua_rawgeti( L, LUA_REGISTRYINDEX, rtc_cb_ref );
    lua_call( L, 0, 1 );

    if (lua_type( L, lua_gettop( L ) ) == LUA_TTABLE) {
      table2tm( L, tm );
      res = VFS_RES_OK;
    }

    // pop item returned by callback
    lua_pop( L, 1 );
  }

  return res;
}

// Lua: on()
static int file_on(lua_State *L)
{
  enum events{
    ON_RTC = 0
  };
  const char *const eventnames[] = {"rtc", NULL};

  int event = luaL_checkoption(L, 1, "rtc", eventnames);

  switch (event) {
  case ON_RTC:
    luaL_unref(L, LUA_REGISTRYINDEX, rtc_cb_ref);

    if ((lua_type(L, 2) == LUA_TFUNCTION) ||
        (lua_type(L, 2) == LUA_TLIGHTFUNCTION)) {
      lua_pushvalue(L, 2);  // copy argument (func) to the top of stack
      rtc_cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
      vfs_register_rtc_cb(file_rtc_cb);
    } else {
      rtc_cb_ref = LUA_NOREF;
      vfs_register_rtc_cb(NULL);
    }
    break;
  default:
    break;
  }

  return 0;
}

// Lua: close()
static int file_close( lua_State* L )
{
  int need_pop = FALSE;
  file_fd_ud *ud;

  if (lua_type( L, 1 ) != LUA_TUSERDATA) {
    // fall back to last opened file
    if (file_fd_ref != LUA_NOREF) {
      lua_rawgeti( L, LUA_REGISTRYINDEX, file_fd_ref );
      // top of stack is now default file descriptor
      ud = (file_fd_ud *)luaL_checkudata(L, -1, "file.obj");
      lua_pop( L, 1 );
    } else {
      // no default file currently opened
      return 0;
    }
  } else {
    ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj");
  }

  // unref default file descriptor
  luaL_unref( L, LUA_REGISTRYINDEX, file_fd_ref );
  file_fd_ref = LUA_NOREF;

  if(ud->fd){
      vfs_close(ud->fd);
      // mark as closed
      ud->fd = 0;
  }
  return 0;  
}

static int file_obj_free( lua_State *L )
{
  file_fd_ud *ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj");
  if (ud->fd) {
    // close file if it's still open
    vfs_close(ud->fd);
    ud->fd = 0;
  }

  return 0;
}

// Lua: format()
static int file_format( lua_State* L )
{
  size_t len;
  file_close(L);
  if( !vfs_format() )
  {
    NODE_ERR( "\n*** ERROR ***: unable to format. FS might be compromised.\n" );
    NODE_ERR( "It is advised to re-flash the NodeMCU image.\n" );
    luaL_error(L, "Failed to format file system");
  }
  else{
    NODE_ERR( "format done.\n" );
  }
  return 0;
}

static int file_fscfg (lua_State *L)
{
  uint32_t phys_addr, phys_size;

  vfs_fscfg("/FLASH", &phys_addr, &phys_size);

  lua_pushinteger (L, phys_addr);
  lua_pushinteger (L, phys_size);
  return 2;
}

// Lua: open(filename, mode)
static int file_open( lua_State* L )
{
  size_t len;

  // unref last file descriptor to allow gc'ing if not kept by user script
  luaL_unref( L, LUA_REGISTRYINDEX, file_fd_ref );
  file_fd_ref = LUA_NOREF;

  const char *fname = luaL_checklstring( L, 1, &len );
  const char *basename = vfs_basename( fname );
  luaL_argcheck(L, c_strlen(basename) <= FS_OBJ_NAME_LEN && c_strlen(fname) == len, 1, "filename invalid");

  const char *mode = luaL_optstring(L, 2, "r");

  file_fd = vfs_open(fname, mode);

  if(!file_fd){
    lua_pushnil(L);
  } else {
    file_fd_ud *ud = (file_fd_ud *) lua_newuserdata( L, sizeof( file_fd_ud ) );
    ud->fd = file_fd;
    luaL_getmetatable( L, "file.obj" );
    lua_setmetatable( L, -2 );

    // store reference to opened file
    lua_pushvalue( L, -1 );
    file_fd_ref = luaL_ref( L, LUA_REGISTRYINDEX );
  }
  return 1; 
}

// Lua: list()
static int file_list( lua_State* L )
{
  vfs_dir  *dir;
  vfs_item *item;

  if (dir = vfs_opendir("")) {
    lua_newtable( L );
    while (item = vfs_readdir(dir)) {
      lua_pushinteger(L, vfs_item_size(item));
      lua_setfield(L, -2, vfs_item_name(item));
      vfs_closeitem(item);
    }
    vfs_closedir(dir);
    return 1;
  }
  return 0;
}

static int get_file_obj( lua_State *L, int *argpos )
{
  if (lua_type( L, 1 ) == LUA_TUSERDATA) {
    file_fd_ud *ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj");
    *argpos = 2;
    return ud->fd;
  } else {
    *argpos = 1;
    return file_fd;
  }
}

#define GET_FILE_OBJ int argpos; \
  int fd = get_file_obj( L, &argpos );

static int file_seek (lua_State *L)
{
  GET_FILE_OBJ;

  static const int mode[] = {VFS_SEEK_SET, VFS_SEEK_CUR, VFS_SEEK_END};
  static const char *const modenames[] = {"set", "cur", "end", NULL};
  if(!fd)
    return luaL_error(L, "open a file first");
  int op = luaL_checkoption(L, argpos, "cur", modenames);
  long offset = luaL_optlong(L, ++argpos, 0);
  op = vfs_lseek(fd, offset, mode[op]);
  if (op < 0)
    lua_pushnil(L);  /* error */
  else
    lua_pushinteger(L, vfs_tell(fd));
  return 1;
}

// Lua: exists(filename)
static int file_exists( lua_State* L )
{
  size_t len;
  const char *fname = luaL_checklstring( L, 1, &len );    
  const char *basename = vfs_basename( fname );
  luaL_argcheck(L, c_strlen(basename) <= FS_OBJ_NAME_LEN && c_strlen(fname) == len, 1, "filename invalid");

  vfs_item *stat = vfs_stat((char *)fname);

  lua_pushboolean(L, stat ? 1 : 0);

  if (stat) vfs_closeitem(stat);

  return 1;
}

// Lua: remove(filename)
static int file_remove( lua_State* L )
{
  size_t len;
  const char *fname = luaL_checklstring( L, 1, &len );    
  const char *basename = vfs_basename( fname );
  luaL_argcheck(L, c_strlen(basename) <= FS_OBJ_NAME_LEN && c_strlen(fname) == len, 1, "filename invalid");
  vfs_remove((char *)fname);
  return 0;
}

// Lua: flush()
static int file_flush( lua_State* L )
{
  GET_FILE_OBJ;

  if(!fd)
    return luaL_error(L, "open a file first");
  if(vfs_flush(fd) == 0)
    lua_pushboolean(L, 1);
  else
    lua_pushnil(L);
  return 1;
}

// Lua: rename("oldname", "newname")
static int file_rename( lua_State* L )
{
  size_t len;

  const char *oldname = luaL_checklstring( L, 1, &len );
  const char *basename = vfs_basename( oldname );
  luaL_argcheck(L, c_strlen(basename) <= FS_OBJ_NAME_LEN && c_strlen(oldname) == len, 1, "filename invalid");
  
  const char *newname = luaL_checklstring( L, 2, &len );  
  basename = vfs_basename( newname );
  luaL_argcheck(L, c_strlen(basename) <= FS_OBJ_NAME_LEN && c_strlen(newname) == len, 2, "filename invalid");

  if(0 <= vfs_rename( oldname, newname )){
    lua_pushboolean(L, 1);
  } else {
    lua_pushboolean(L, 0);
  }
  return 1;
}

// Lua: stat(filename)
static int file_stat( lua_State* L )
{
  size_t len;
  const char *fname = luaL_checklstring( L, 1, &len );    
  luaL_argcheck( L, c_strlen(fname) <= FS_OBJ_NAME_LEN && c_strlen(fname) == len, 1, "filename invalid" );

  vfs_item *stat = vfs_stat( (char *)fname );

  if (!stat) {
    lua_pushnil( L );
    return 1;
  }

  lua_createtable( L, 0, 7 );

  lua_pushinteger( L, vfs_item_size( stat ) );
  lua_setfield( L, -2, "size" );

  lua_pushstring( L, vfs_item_name( stat ) );
  lua_setfield( L, -2, "name" );

  lua_pushboolean( L, vfs_item_is_dir( stat ) );
  lua_setfield( L, -2, "is_dir" );

  lua_pushboolean( L, vfs_item_is_rdonly( stat ) );
  lua_setfield( L, -2, "is_rdonly" );

  lua_pushboolean( L, vfs_item_is_hidden( stat ) );
  lua_setfield( L, -2, "is_hidden" );

  lua_pushboolean( L, vfs_item_is_sys( stat ) );
  lua_setfield( L, -2, "is_sys" );

  lua_pushboolean( L, vfs_item_is_arch( stat ) );
  lua_setfield( L, -2, "is_arch" );

  // time stamp as sub-table
  vfs_time tm;
  int got_time = VFS_RES_OK == vfs_item_time( stat, &tm ) ? TRUE : FALSE;
  
  lua_createtable( L, 0, 6 );

  lua_pushinteger( L, got_time ? tm.year : FILE_TIMEDEF_YEAR );
  lua_setfield( L, -2, "year" );

  lua_pushinteger( L, got_time ? tm.mon : FILE_TIMEDEF_MON );
  lua_setfield( L, -2, "mon" );

  lua_pushinteger( L, got_time ? tm.day : FILE_TIMEDEF_DAY );
  lua_setfield( L, -2, "day" );

  lua_pushinteger( L, got_time ? tm.hour : FILE_TIMEDEF_HOUR );
  lua_setfield( L, -2, "hour" );

  lua_pushinteger( L, got_time ? tm.min : FILE_TIMEDEF_MIN );
  lua_setfield( L, -2, "min" );

  lua_pushinteger( L, got_time ? tm.sec : FILE_TIMEDEF_SEC );
  lua_setfield( L, -2, "sec" );

  lua_setfield( L, -2, "time" );

  return 1;
}

// g_read()
static int file_g_read( lua_State* L, int n, int16_t end_char, int fd )
{
  static char *heap_mem = NULL;
  // free leftover memory
  if (heap_mem) {
    luaM_free(L, heap_mem);
    heap_mem = NULL;
  }

  if(n <= 0)
    n = FILE_READ_CHUNK;

  if(end_char < 0 || end_char >255)
    end_char = EOF;


  if(!fd)
    return luaL_error(L, "open a file first");

  char *p;
  int i;

  if (n > LUAL_BUFFERSIZE) {
    // get buffer from heap
    p = heap_mem = luaM_malloc(L, n);
  } else {
    // small chunks go onto the stack
    p = alloca(n);
  }

  n = vfs_read(fd, p, n);
  // bypass search if no end character provided
  if (n > 0 && end_char != EOF) {
    for (i = 0; i < n; ++i)
      if (p[i] == end_char)
      {
        ++i;
        break;
      }
  } else {
    i = n;
  }

  if (i == 0 || n == VFS_RES_ERR) {
    if (heap_mem) {
      luaM_free(L, heap_mem);
      heap_mem = NULL;
    }
    return 0;
  }

  vfs_lseek(fd, -(n - i), VFS_SEEK_CUR);
  lua_pushlstring(L, p, i);
  if (heap_mem) {
    luaM_free(L, heap_mem);
    heap_mem = NULL;
  }
  return 1;
}

// Lua: read()
// file.read() will read all byte in file
// file.read(10) will read 10 byte from file, or EOF is reached.
// file.read('q') will read until 'q' or EOF is reached. 
static int file_read( lua_State* L )
{
  unsigned need_len = FILE_READ_CHUNK;
  int16_t end_char = EOF;
  size_t el;

  GET_FILE_OBJ;

  if( lua_type( L, argpos ) == LUA_TNUMBER )
  {
    need_len = ( unsigned )luaL_checkinteger( L, argpos );
  }
  else if(lua_isstring(L, argpos))
  {
    const char *end = luaL_checklstring( L, argpos, &el );
    if(el!=1){
      return luaL_error( L, "wrong arg range" );
    }
    end_char = (int16_t)end[0];
  }

  return file_g_read(L, need_len, end_char, fd);
}

// Lua: readline()
static int file_readline( lua_State* L )
{
  GET_FILE_OBJ;

  return file_g_read(L, LUAL_BUFFERSIZE, '\n', fd);
}

// Lua: write("string")
static int file_write( lua_State* L )
{
  GET_FILE_OBJ;

  if(!fd)
    return luaL_error(L, "open a file first");
  size_t l, rl;
  const char *s = luaL_checklstring(L, argpos, &l);
  rl = vfs_write(fd, s, l);
  if(rl==l)
    lua_pushboolean(L, 1);
  else
    lua_pushnil(L);
  return 1;
}

// Lua: writeline("string")
static int file_writeline( lua_State* L )
{
  GET_FILE_OBJ;

  if(!fd)
    return luaL_error(L, "open a file first");
  size_t l, rl;
  const char *s = luaL_checklstring(L, argpos, &l);
  rl = vfs_write(fd, s, l);
  if(rl==l){
    rl = vfs_write(fd, "\n", 1);
    if(rl==1)
      lua_pushboolean(L, 1);
    else
      lua_pushnil(L);
  }
  else{
    lua_pushnil(L);
  }
  return 1;
}

// Lua: fsinfo()
static int file_fsinfo( lua_State* L )
{
  u32_t total, used;
  if (vfs_fsinfo("", &total, &used)) {
    return luaL_error(L, "file system failed");
  }
  NODE_DBG("total: %d, used:%d\n", total, used);
  if(total>0x7FFFFFFF || used>0x7FFFFFFF || used > total)
  {
    return luaL_error(L, "file system error");
  }
  lua_pushinteger(L, total-used);
  lua_pushinteger(L, used);
  lua_pushinteger(L, total);
  return 3;
}

typedef struct {
  vfs_vol *vol;
} volume_type;

// Lua: vol = file.mount("/SD0")
static int file_mount( lua_State *L )
{
  const char *ldrv = luaL_checkstring( L, 1 );
  int num = luaL_optint( L, 2, -1 );
  volume_type *vol = (volume_type *)lua_newuserdata( L, sizeof( volume_type ) );

  if (vol->vol = vfs_mount( ldrv, num )) {
    /* set its metatable */
    luaL_getmetatable(L, "vfs.vol");
    lua_setmetatable(L, -2);
    return 1;
  } else {
    // remove created userdata
    lua_pop( L, 1 );
    return 0;
  }
}

// Lua: success = file.chdir("/SD0/")
static int file_chdir( lua_State *L )
{
  const char *path = luaL_checkstring( L, 1 );

  lua_pushboolean( L, 0 <= vfs_chdir( path ) );
  return 1;
}

static int file_vol_umount( lua_State *L )
{
  volume_type *vol = luaL_checkudata( L, 1, "file.vol" );
  luaL_argcheck( L, vol, 1, "volume expected" );

  lua_pushboolean( L, 0 <= vfs_umount( vol->vol ) );

  // invalidate vfs descriptor, it has been free'd anyway
  vol->vol = NULL;
  return 1;
}


static const LUA_REG_TYPE file_obj_map[] =
{
  { LSTRKEY( "close" ),     LFUNCVAL( file_close ) },
  { LSTRKEY( "read" ),      LFUNCVAL( file_read ) },
  { LSTRKEY( "readline" ),  LFUNCVAL( file_readline ) },
  { LSTRKEY( "write" ),     LFUNCVAL( file_write ) },
  { LSTRKEY( "writeline" ), LFUNCVAL( file_writeline ) },
  { LSTRKEY( "seek" ),      LFUNCVAL( file_seek ) },
  { LSTRKEY( "flush" ),     LFUNCVAL( file_flush ) },
  { LSTRKEY( "__gc" ),      LFUNCVAL( file_obj_free ) },
  { LSTRKEY( "__index" ),   LROVAL( file_obj_map ) },
  { LNILKEY, LNILVAL }
};

static const LUA_REG_TYPE file_vol_map[] =
{
  { LSTRKEY( "umount" ),   LFUNCVAL( file_vol_umount )},
  //{ LSTRKEY( "getfree" ),  LFUNCVAL( file_vol_getfree )},
  //{ LSTRKEY( "getlabel" ), LFUNCVAL( file_vol_getlabel )},
  //{ LSTRKEY( "__gc" ),     LFUNCVAL( file_vol_free ) },
  { LSTRKEY( "__index" ),  LROVAL( file_vol_map ) },
  { LNILKEY, LNILVAL }
};

// Module function map
static const LUA_REG_TYPE file_map[] = {
  { LSTRKEY( "list" ),      LFUNCVAL( file_list ) },
  { LSTRKEY( "open" ),      LFUNCVAL( file_open ) },
  { LSTRKEY( "close" ),     LFUNCVAL( file_close ) },
  { LSTRKEY( "write" ),     LFUNCVAL( file_write ) },
  { LSTRKEY( "writeline" ), LFUNCVAL( file_writeline ) },
  { LSTRKEY( "read" ),      LFUNCVAL( file_read ) },
  { LSTRKEY( "readline" ),  LFUNCVAL( file_readline ) },
#ifdef BUILD_SPIFFS
  { LSTRKEY( "format" ),    LFUNCVAL( file_format ) },
  { LSTRKEY( "fscfg" ),     LFUNCVAL( file_fscfg ) },
#endif
  { LSTRKEY( "remove" ),    LFUNCVAL( file_remove ) },
  { LSTRKEY( "seek" ),      LFUNCVAL( file_seek ) },
  { LSTRKEY( "flush" ),     LFUNCVAL( file_flush ) },
  { LSTRKEY( "rename" ),    LFUNCVAL( file_rename ) },
  { LSTRKEY( "exists" ),    LFUNCVAL( file_exists ) },  
  { LSTRKEY( "fsinfo" ),    LFUNCVAL( file_fsinfo ) },
  { LSTRKEY( "on" ),        LFUNCVAL( file_on ) },
  { LSTRKEY( "stat" ),      LFUNCVAL( file_stat ) },
#ifdef BUILD_FATFS
  { LSTRKEY( "mount" ),     LFUNCVAL( file_mount ) },
  { LSTRKEY( "chdir" ),     LFUNCVAL( file_chdir ) },
#endif
  { LNILKEY, LNILVAL }
};

int luaopen_file( lua_State *L ) {
  luaL_rometatable( L, "file.vol",  (void *)file_vol_map );
  luaL_rometatable( L, "file.obj",  (void *)file_obj_map );
  return 0;
}

NODEMCU_MODULE(FILE, "file", file_map, luaopen_file);
