// Filesystem implementation
#include "romfs.h"
#include "c_string.h"
// #include "c_errno.h"
#include "romfiles.h"
#include "c_stdio.h"
// #include "c_stdlib.h"
#include "c_fcntl.h"

#include "platform.h"

#if defined( BUILD_ROMFS ) || defined( BUILD_WOFS )

#define TOTAL_MAX_FDS   8
// DO NOT CHANGE THE ROMFS ALIGNMENT.
// UNLESS YOU _LIKE_ TO WATCH THE WORLD BURN.
#define ROMFS_ALIGN     4

#define fsmin( x , y ) ( ( x ) < ( y ) ? ( x ) : ( y ) )

static FD fd_table[ TOTAL_MAX_FDS ];
static int romfs_num_fd;

#define WOFS_END_MARKER_CHAR  0xFF
#define WOFS_DEL_FIELD_SIZE   ( ROMFS_ALIGN )
#define WOFS_FILE_DELETED     0xAA

// Length of the 'file size' field for both ROMFS/WOFS
#define ROMFS_SIZE_LEN        4

static int ICACHE_FLASH_ATTR romfs_find_empty_fd(void)
{
  int i;
  
  for( i = 0; i < TOTAL_MAX_FDS; i ++ )
    if( fd_table[ i ].baseaddr == 0xFFFFFFFF &&
        fd_table[ i ].offset == 0xFFFFFFFF &&
        fd_table[ i ].size == 0xFFFFFFFF )
      return i;
  return -1;
}

static void ICACHE_FLASH_ATTR romfs_close_fd( int fd )
{
  if(fd<0 || fd>=TOTAL_MAX_FDS) 
    return;
  c_memset( fd_table + fd, 0xFF, sizeof( FD ) );
  fd_table[ fd ].flags = 0;
}

// Helper function: read a byte from the FS
static uint8_t ICACHE_FLASH_ATTR romfsh_read8( uint32_t addr, const FSDATA *pfs )
{
  uint8_t temp;
  if( pfs->flags & ROMFS_FS_FLAG_DIRECT )
    return pfs->pbase[ addr ];
  pfs->readf( &temp, addr, 1, pfs );
  return temp;
}

// Helper function: return 1 if PFS reffers to a WOFS, 0 otherwise
static int ICACHE_FLASH_ATTR romfsh_is_wofs( const FSDATA* pfs )
{
  return ( pfs->flags & ROMFS_FS_FLAG_WO ) != 0;
}

// Find the next file, returning FS_FILE_OK or FS_FILE_NOT_FOUND if there no file left.
static uint8_t ICACHE_FLASH_ATTR romfs_next_file( uint32_t *start, char* fname, size_t len, size_t *act_len, FSDATA *pfs )
{
  uint32_t i, j, n;
  uint32_t fsize;
  int is_deleted;
  
  // Look for the file
  i = *start;
  *act_len = 0;
  if( (i >= INTERNAL_FLASH_SIZE) || (romfsh_read8( i, pfs ) == WOFS_END_MARKER_CHAR ))    // end of file system
  {
    *start = (i >= INTERNAL_FLASH_SIZE)?(INTERNAL_FLASH_SIZE-1):i;
    return FS_FILE_NOT_FOUND;
  }
  // Read file name
  len = len>MAX_FNAME_LENGTH?MAX_FNAME_LENGTH:len;
  for( j = 0; j < len; j ++ )
  {
    fname[ j ] = romfsh_read8( i + j, pfs );
    if( fname[ j ] == 0 )
       break;
  }
  n = j; // save the file name length to n
  // ' i + j' now points at the '0' byte
  j = i + j + 1;
  // Round to a multiple of ROMFS_ALIGN
  j = ( j + ROMFS_ALIGN - 1 ) & ~( ROMFS_ALIGN - 1 );
  // WOFS has an additional WOFS_DEL_FIELD_SIZE bytes before the size as an indication for "file deleted"
  if( romfsh_is_wofs( pfs ) )
  {
    is_deleted = romfsh_read8( j, pfs ) == WOFS_FILE_DELETED;
    j += WOFS_DEL_FIELD_SIZE;
  }
  else
    is_deleted = 0;
  // And read the size
  fsize = romfsh_read8( j, pfs ) + ( romfsh_read8( j + 1, pfs ) << 8 );
  fsize += ( romfsh_read8( j + 2, pfs ) << 16 ) + ( romfsh_read8( j + 3, pfs ) << 24 );
  j += ROMFS_SIZE_LEN;
  if( !is_deleted )
  {
    // Found the valid file
    *act_len = n;
  }
  // Move to next file
  i = j + fsize;
  // On WOFS, all file names must begin at a multiple of ROMFS_ALIGN
  if( romfsh_is_wofs( pfs ) )
    i = ( i + ROMFS_ALIGN - 1 ) & ~( ROMFS_ALIGN - 1 );
  *start = i;   // modify the start address
  return FS_FILE_OK;
}

// Open the given file, returning one of FS_FILE_NOT_FOUND, FS_FILE_ALREADY_OPENED
// or FS_FILE_OK
static uint8_t ICACHE_FLASH_ATTR romfs_open_file( const char* fname, FD* pfd, FSDATA *pfs, uint32_t *plast, uint32_t *pnameaddr )
{
  uint32_t i, j, n;
  char fsname[ MAX_FNAME_LENGTH + 1 ];
  uint32_t fsize;
  int is_deleted;
  
  // Look for the file
  i = 0;
  while( 1 )
  {
    if( i >= INTERNAL_FLASH_SIZE ){
      *plast = INTERNAL_FLASH_SIZE - 1;   // point to last one
      return FS_FILE_NOT_FOUND;
    }
    if( romfsh_read8( i, pfs ) == WOFS_END_MARKER_CHAR )
    {
      *plast = i;
      return FS_FILE_NOT_FOUND;
    }
    // Read file name
    n = i;
    for( j = 0; j < MAX_FNAME_LENGTH; j ++ )
    {
      fsname[ j ] = romfsh_read8( i + j, pfs );
      if( fsname[ j ] == 0 )
         break;
    }
    // ' i + j' now points at the '0' byte
    j = i + j + 1;
    // Round to a multiple of ROMFS_ALIGN
    j = ( j + ROMFS_ALIGN - 1 ) & ~( ROMFS_ALIGN - 1 );
    // WOFS has an additional WOFS_DEL_FIELD_SIZE bytes before the size as an indication for "file deleted"
    if( romfsh_is_wofs( pfs ) )
    {
      is_deleted = romfsh_read8( j, pfs ) == WOFS_FILE_DELETED;
      j += WOFS_DEL_FIELD_SIZE;
    }
    else
      is_deleted = 0;
    // And read the size
    fsize = romfsh_read8( j, pfs ) + ( romfsh_read8( j + 1, pfs ) << 8 );
    fsize += ( romfsh_read8( j + 2, pfs ) << 16 ) + ( romfsh_read8( j + 3, pfs ) << 24 );
    j += ROMFS_SIZE_LEN;
    if( !c_strncasecmp( fname, fsname, MAX_FNAME_LENGTH ) && !is_deleted )
    {
      // Found the file
      pfd->baseaddr = j;
      pfd->offset = 0;
      pfd->size = fsize;
      if( pnameaddr )
        *pnameaddr = n;
      return FS_FILE_OK;
    }
    // Move to next file
    i = j + fsize;
    // On WOFS, all file names must begin at a multiple of ROMFS_ALIGN
    if( romfsh_is_wofs( pfs ) )
      i = ( i + ROMFS_ALIGN - 1 ) & ~( ROMFS_ALIGN - 1 );
  }
  *plast = 0;
  return FS_FILE_NOT_FOUND;
}

static int ICACHE_FLASH_ATTR romfs_open( const char *path, int flags, int mode, void *pdata )
{
  FD tempfs;
  int i;
  FSDATA *pfsdata = ( FSDATA* )pdata;
  int must_create = 0;
  int exists;
  uint8_t lflags = ROMFS_FILE_FLAG_READ;
  uint32_t firstfree, nameaddr;

  if( romfs_num_fd == TOTAL_MAX_FDS )
  {
    return -1;
  }
  // Does the file exist?
  exists = romfs_open_file( path, &tempfs, pfsdata, &firstfree, &nameaddr ) == FS_FILE_OK;
  // Now interpret "flags" to set file flags and to check if we should create the file
  if( flags & O_CREAT )
  {
    // If O_CREAT is specified with O_EXCL and the file already exists, return with error
    if( ( flags & O_EXCL ) && exists )
    {
      return -1;
    }
    // Otherwise create the file if it does not exist
    must_create = !exists;
  }
  if( ( flags & O_TRUNC ) && ( flags & ( O_WRONLY | O_RDWR ) ) && exists )
  {
    // The file exists, but it must be truncated
    // In the case of WOFS, this effectively means "create a new file"
    must_create = 1;
  }
  // ROMFS can't create files
  if( must_create && ( ( pfsdata->flags & ROMFS_FS_FLAG_WO ) == 0 ) )
  {
    return -1;
  }
  // Decode access mode
  if( flags & O_WRONLY )
    lflags = ROMFS_FILE_FLAG_WRITE;
  else if( flags & O_RDWR )
    lflags = ROMFS_FILE_FLAG_READ | ROMFS_FILE_FLAG_WRITE;
  if( flags & O_APPEND )
    lflags |= ROMFS_FILE_FLAG_APPEND;
  // If a write access is requested when the file must NOT be created, this
  // is an error
  if( ( lflags & ( ROMFS_FILE_FLAG_WRITE | ROMFS_FILE_FLAG_APPEND ) ) && !must_create )
  {
    return -1;
  }
  if( ( lflags & ( ROMFS_FILE_FLAG_WRITE | ROMFS_FILE_FLAG_APPEND ) ) && romfs_fs_is_flag_set( pfsdata, ROMFS_FS_FLAG_WRITING ) )
  {
    // At most one file can be opened in write mode at any given time on WOFS
    return -1;
  }
  // Do we need to create the file ?
  if( must_create )
  {
    if( exists )
    {
      // Invalidate the file first by changing WOFS_DEL_FIELD_SIZE bytes before
      // the file length to WOFS_FILE_DELETED
      uint8_t tempb[] = { WOFS_FILE_DELETED, 0xFF, 0xFF, 0xFF };
      pfsdata->writef( tempb, tempfs.baseaddr - ROMFS_SIZE_LEN - WOFS_DEL_FIELD_SIZE, WOFS_DEL_FIELD_SIZE, pfsdata );
    }
    // Find the last available position by asking romfs_open_file to look for a file
    // with an invalid name
    romfs_open_file( "\1", &tempfs, pfsdata, &firstfree, NULL );
    // Is there enough space on the FS for another file?
    if( pfsdata->max_size - firstfree + 1 < c_strlen( path ) + 1 + WOFS_MIN_NEEDED_SIZE + WOFS_DEL_FIELD_SIZE )
    {
      return -1;
    }

    // Make sure we can get a file descriptor before writing
    if( ( i = romfs_find_empty_fd() ) < 0 )
    {
      return -1;
    }

    // Write the name of the file
    pfsdata->writef( path, firstfree, c_strlen( path ) + 1, pfsdata );
    firstfree += c_strlen( path ) + 1; // skip over the name
    // Align to a multiple of ROMFS_ALIGN
    firstfree = ( firstfree + ROMFS_ALIGN - 1 ) & ~( ROMFS_ALIGN - 1 );
    firstfree += ROMFS_SIZE_LEN + WOFS_DEL_FIELD_SIZE; // skip over the size and the deleted flags area
    tempfs.baseaddr = firstfree;
    tempfs.offset = tempfs.size = 0;
    // Set the "writing" flag on the FS to indicate that there is a file opened in write mode
    romfs_fs_set_flag( pfsdata, ROMFS_FS_FLAG_WRITING );
  }
  else // File must exist (and was found in the previous 'romfs_open_file' call)
  {
    if( !exists )
    {
      return -1;
    }

    if( ( i = romfs_find_empty_fd() ) < 0 )
    {
      return -1;
    }
  }
  // Copy the descriptor information
  tempfs.flags = lflags;
  c_memcpy( fd_table + i, &tempfs, sizeof( FD ) );
  romfs_num_fd ++;
  return i;
}

static int ICACHE_FLASH_ATTR romfs_close( int fd, void *pdata )
{
  if(fd<0 || fd>=TOTAL_MAX_FDS) 
    return 0;
  FD* pfd = fd_table + fd;
  FSDATA *pfsdata = ( FSDATA* )pdata;
  uint8_t temp[ ROMFS_SIZE_LEN ];

  if( pfd->flags & ( ROMFS_FILE_FLAG_WRITE | ROMFS_FILE_FLAG_APPEND ) )
  {
    // Write back the size
    temp[ 0 ] = pfd->size & 0xFF;
    temp[ 1 ] = ( pfd->size >> 8 ) & 0xFF;
    temp[ 2 ] = ( pfd->size >> 16 ) & 0xFF;
    temp[ 3 ] = ( pfd->size >> 24 ) & 0xFF;
    pfsdata->writef( temp, pfd->baseaddr - ROMFS_SIZE_LEN, ROMFS_SIZE_LEN, pfsdata );
    // Clear the "writing" flag on the FS instance to allow other files to be opened
    // in write mode
    romfs_fs_clear_flag( pfsdata, ROMFS_FS_FLAG_WRITING );
  }
  romfs_close_fd( fd );
  romfs_num_fd --;
  return 0;
}

static _ssize_t ICACHE_FLASH_ATTR romfs_write( int fd, const void* ptr, size_t len, void *pdata )
{
  if(fd<0 || fd>=TOTAL_MAX_FDS) 
    return -1;
  if(len == 0)
    return 0;
  FD* pfd = fd_table + fd;
  FSDATA *pfsdata = ( FSDATA* )pdata;

  if( ( pfd->flags & ( ROMFS_FILE_FLAG_WRITE | ROMFS_FILE_FLAG_APPEND ) ) == 0 )
  {
    return -1;
  }
  // Append mode: set the file pointer to the end
  if( pfd->flags & ROMFS_FILE_FLAG_APPEND )
    pfd->offset = pfd->size;
  // Only write at the end of the file!
  if( pfd->offset != pfd->size )
    return 0;
  // Check if we have enough space left on the device. Always keep 1 byte for the final 0xFF
  // and ROMFS_ALIGN - 1 bytes for aligning the contents of the file data in the worst case
  // scenario (so ROMFS_ALIGN bytes in total)
  if( pfd->baseaddr + pfd->size + len > pfsdata->max_size - ROMFS_ALIGN )
    len = pfsdata->max_size - ( pfd->baseaddr + pfd->size ) - ROMFS_ALIGN;
  pfsdata->writef( ptr, pfd->offset + pfd->baseaddr, len, pfsdata );
  pfd->offset += len;
  pfd->size += len;
  return len;
}

static _ssize_t ICACHE_FLASH_ATTR romfs_read( int fd, void* ptr, size_t len, void *pdata )
{
  if(fd<0 || fd>=TOTAL_MAX_FDS) 
    return -1;
  if(len == 0)
    return 0;

  FD* pfd = fd_table + fd;
  long actlen = fsmin( len, pfd->size - pfd->offset );
  FSDATA *pfsdata = ( FSDATA* )pdata;

  if( ( pfd->flags & ROMFS_FILE_FLAG_READ ) == 0 )
  {
    return -1;
  }
  if( pfsdata->flags & ROMFS_FS_FLAG_DIRECT )
    c_memcpy( ptr, pfsdata->pbase + pfd->offset + pfd->baseaddr, actlen );
  else
    actlen = pfsdata->readf( ptr, pfd->offset + pfd->baseaddr, actlen, pfsdata );
  pfd->offset += actlen;
  return actlen;
}

// lseek
static int ICACHE_FLASH_ATTR romfs_lseek( int fd, int off, int whence, void *pdata )
{
  if(fd<0 || fd>=TOTAL_MAX_FDS) 
    return -1;

  FD* pfd = fd_table + fd;
  uint32_t newpos = 0;
  
  switch( whence )
  {
    case SEEK_SET:
      newpos = off;
      break;
      
    case SEEK_CUR:
      newpos = pfd->offset + off;
      break;
      
    case SEEK_END:
      newpos = pfd->size + off;
      break;
      
    default:
      return -1;
  }    
  if( newpos > pfd->size )
    return -1;
  pfd->offset = newpos;
  return newpos;
}

// ****************************************************************************
// WOFS functions and instance descriptor for real hardware

#if defined( BUILD_WOFS )
static uint32_t ICACHE_FLASH_ATTR sim_wofs_write( const void *from, uint32_t toaddr, uint32_t size, const void *pdata )
{
  const FSDATA *pfsdata = ( const FSDATA* )pdata;
  if(toaddr>=INTERNAL_FLASH_SIZE)
  {
    NODE_ERR("ERROR in flash op: wrong addr.\n");
    return 0;
  }
  toaddr += ( uint32_t )pfsdata->pbase;
  return platform_flash_write( from, toaddr, size );
}

static uint32_t ICACHE_FLASH_ATTR sim_wofs_read( void *to, uint32_t fromaddr, uint32_t size, const void *pdata )
{
  const FSDATA *pfsdata = ( const FSDATA* )pdata;
  if(fromaddr>=INTERNAL_FLASH_SIZE)
  {
    NODE_ERR("ERROR in flash op: wrong addr.\n");
    return 0;
  }
  fromaddr += ( uint32_t )pfsdata->pbase;
  return platform_flash_read( to, fromaddr, size );
}

// This must NOT be a const!
static FSDATA wofs_fsdata =
{
  NULL,
  ROMFS_FS_FLAG_WO,
  sim_wofs_read,
  sim_wofs_write,
  0
};

// WOFS formatting function
// Returns 1 if OK, 0 for error
int ICACHE_FLASH_ATTR wofs_format( void )
{
  uint32_t sect_first, sect_last;
  FD tempfd;
  platform_flash_get_first_free_block_address( &sect_first );
  // Get the first free address in WOFS. We use this address to compute the last block that we need to
  // erase, instead of simply erasing everything from sect_first to the last Flash page. 
  romfs_open_file( "\1", &tempfd, &wofs_fsdata, &sect_last, NULL );
  sect_last = platform_flash_get_sector_of_address( sect_last + ( uint32_t )wofs_fsdata.pbase );
  while( sect_first <= sect_last )
    if( platform_flash_erase_sector( sect_first ++ ) == PLATFORM_ERR )
      return 0;
  return 1;
}

int ICACHE_FLASH_ATTR wofs_open(const char *_name, int flags){
  return romfs_open( _name, flags, 0, &wofs_fsdata );
}

int ICACHE_FLASH_ATTR wofs_close( int fd ){
  return romfs_close( fd, &wofs_fsdata );
}

size_t ICACHE_FLASH_ATTR wofs_write( int fd, const void* ptr, size_t len ){
  return romfs_write( fd, ptr, len, &wofs_fsdata );
}

size_t ICACHE_FLASH_ATTR wofs_read( int fd, void* ptr, size_t len){
  return romfs_read( fd, ptr, len, &wofs_fsdata );
}

int ICACHE_FLASH_ATTR wofs_lseek( int fd, int off, int whence ){
  return romfs_lseek( fd, off, whence, &wofs_fsdata );
}

int ICACHE_FLASH_ATTR wofs_eof( int fd ){
  if(fd<0 || fd>=TOTAL_MAX_FDS) 
    return -1;
  FD* pfd = fd_table + fd;
  // NODE_DBG("off:%d, sz:%d\n",pfd->offset, pfd->size);
  return pfd->offset == pfd->size;
}

int ICACHE_FLASH_ATTR wofs_getc( int fd ){
  char c = EOF;
  if(!wofs_eof(fd)){
    romfs_read( fd, &c, 1, &wofs_fsdata );
  }
  // NODE_DBG("c: %d\n", c);
  return (int)c;
}

int ICACHE_FLASH_ATTR wofs_ungetc( int c, int fd ){
  return romfs_lseek( fd, -1, SEEK_CUR, &wofs_fsdata );
}

// Find the next file, returning FS_FILE_OK or FS_FILE_NOT_FOUND if there no file left.
uint8_t ICACHE_FLASH_ATTR wofs_next( uint32_t *start, char* fname, size_t len, size_t *act_len ){
  return romfs_next_file( start, fname, len, act_len, &wofs_fsdata );
}

#endif // #ifdef BUILD_WOFS

// Initialize both ROMFS and WOFS as needed
int ICACHE_FLASH_ATTR romfs_init( void )
{
  unsigned i;

  for( i = 0; i < TOTAL_MAX_FDS; i ++ )
  {
    c_memset( fd_table + i, 0xFF, sizeof( FD ) );
    fd_table[ i ].flags = 0;
  }
#if defined( BUILD_WOFS ) 
  // Get the start address and size of WOFS and register it
  wofs_fsdata.pbase = ( uint8_t* )platform_flash_get_first_free_block_address( NULL );
  wofs_fsdata.max_size = INTERNAL_FLASH_SIZE - ( ( uint32_t )wofs_fsdata.pbase - INTERNAL_FLASH_START_ADDRESS );
  NODE_DBG("wofs.pbase:%x,max:%x\n",wofs_fsdata.pbase,wofs_fsdata.max_size);
#endif // ifdef BUILD_WOFS
  return 0;
}

#else // #if defined( BUILD_ROMFS ) || defined( BUILD_WOFS )

int ICACHE_FLASH_ATTR romfs_init( void )
{
}

#endif // #if defined( BUILD_ROMFS ) || defined( BUILD_WOFS )

int ICACHE_FLASH_ATTR test_romfs()
{
  int fd;
  int i, size;
  
  fd = wofs_open("init.lua",O_RDONLY);
  NODE_DBG("open file fd:%d\n", fd);
  char r[128];
  NODE_DBG("read from file:\n");
  c_memset(r,0,128);
  size = wofs_read(fd,r,128);
  r[size]=0;
  NODE_DBG(r);
  NODE_DBG("\n");
  wofs_close(fd);

  fd = wofs_open("testm.lua",O_RDONLY);
  NODE_DBG("open file fd:%d\n", fd);
  NODE_DBG("read from file:\n");
  c_memset(r,0,128);
  size = wofs_read(fd,r,128);
  r[size]=0;
  NODE_DBG(r);
  NODE_DBG("\n");
  wofs_close(fd);

  return 0;
}
