/* fs.c - SPI small file system. Taken from intbasic3.
 *  Copyright (C) 2022 by Jed Marti
 *   This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see https://www.gnu.org/licenses/.
 */

#include <C8051F380.h>
#include <stdint.h>

#include "lfxmit.h"
#include "serial.h"
#include "spi.h"
#include "fs.h"
#include "strings.h"

//#include "symtab.h"
//#include "parse.h"

#include "lex.h"



// The first 2 bytes of the extra 16 bytes in a sector. See spi.c
extern __xdata uint16_t nsector;

// Wear leveling value. This is in the upper 2 bytes of the FAT table.
__xdata uint16_t WLEVEL;

// Block use table. The FAT table - 1 bit for each block.
__xdata uint8_t ABLOCK[(MEMSIZE / (SECTORSIZE * SECTORSPERBLOCK)) / 8];

// The directory entry table and temporary sector.
__xdata uint8_t TSECTOR[SECTORSIZE];

// File control blocks. Two open files at once.
__xdata MFILE FT[2];

// Directory search.
__xdata uint8_t dirSearch;           // The directory number.
__xdata uint8_t dirSector;           // The sector we're searching in.


// Lexical interface.
extern __xdata char     STR[];
extern __xdata uint32_t N;

// 2 bytes we can use sometimes.
extern uint16_t DREG;



static void setBlockInUse(uint16_t blk)
// Mark the block blk as in use. Set the bit corresponding to the block to 1.
{
  ABLOCK[blk / 8] |= 1 << (((uint8_t) blk) & 0x07);
}



static void clearBlockInUse(uint16_t blk)
// Clear the block in use. Clear the bit corresponding to the block to 1.
{
  ABLOCK[blk / 8] &=  ~(1 << (((uint8_t) blk) & 0x07));
}



static uint8_t isBlockInUse(uint16_t blk)
// Returns 1 if block in use, 0 if not.
{
  if (ABLOCK[blk / 8] & (1 << (((uint8_t) blk) & 0x07))) return 1;
  return 0;
}


static uint16_t locateEmptyBlock(void)
//    Locate an empty block and return its index. 0 is an invalid block because
// the FS base is stored there. BBLOCK is used to wear level and is stored
// in the extra bytes of the first block.
{
  uint16_t i;
  for (i = 0; i < BLOCKSAVAIL; i++)
    if (!isBlockInUse((i + WLEVEL) & BLOCKMASK))
    {
      WLEVEL = (i + WLEVEL) & BLOCKMASK;
      return WLEVEL;
    }
  return 0;
}


static uint8_t locateFH(void)
//    Locate an empty file handle and return its index or 255 if none available.
{
  uint8_t i;

  for (i = 0; i < OPENFILES; ++i)
    if (FT[i].dir_entry == 0xFF) return i;
  return 0xFF;
}



void clearHandles(void)
//    Clear active files (don't close). This gets done during initialization and between
// program chains.
{
  FT[0].dir_entry = 0xFF;
  FT[1].dir_entry = 0xFF;
}  



void initFS(void)
// Called at startup. Reads the FAT table and sort of checks. Clears the open
// files.
{
  uint8_t i;
  putstring("  Initializing FS\n");

  // Read the full sector, then copy it to the FAT table - it will be smaller.
  spiReadSector(0, TSECTOR);
  for (i = 0; i < FATSIZE; ++i) ABLOCK[i] = TSECTOR[i];

  // Wear level is stored in the top two bytes.
  WLEVEL = nsector;

  // A crude check.
  if (!isBlockInUse(0)) putstring("  Flash not formatted\n");

  // No files in use.
  clearHandles();
  terpri();
}


void writeFAT(void)
// Write the FAT table and wear level value. Note that we may be writing more
// gunk from beyond ABLOCK here but who cares. (ABLOCK typically 128 bytes, not
// 256).
{
  nsector = WLEVEL;
  spiWriteSector(0, ABLOCK);
}



void formatFS(void)
// Format the file system then initialize it.
{
  uint8_t i;
  spiErase();

  // Set wear level block to restart allocation.
  WLEVEL = 1;
  
  // Clear the FAT table.
  for (i = 0; i < 128; ++i) ABLOCK[i] = 0;

  // Set blocks for directory and FAT. For this system, the directory can
  // have 24 entries and sits in the same block as the FAT.
  //  for (i = (1 + DIRENTRIES / (BLKSIZE / sizeof(DIR))) / SECTORSPERBLOCK - 1;
  //       i != 0;
  //       --i) setBlockInUse(i);
  setBlockInUse(0);

  // Write out the FAT.
  writeFAT();

  // Now initialize the blank file system.
  initFS();
}






uint8_t locateFreeDir(void)
//    Locate an unused directory entry. Return the directory entry number
// or 255 if all are in use. This features a lot of crap in the C version
// that can be made substantially smaller.
{
  uint8_t i, j;
  __xdata uint8_t *dirptr = 0;

  // Search all the directory entries.
  for (j = i = 0; i < DIRENTRIES; i++)
  {
    // The are 8 directory entries per sector. Read a new one if we've
    // finished with the previous or are just starting out. The first
    // directory sector is always 1.
    if ((i & 0x07) == 0)
    {
      ++j;
      spiReadSector(j, TSECTOR);
      dirptr = TSECTOR;
    }

    // The first byte will be 0xFF if the entry is empty. Return its index
    // which is always less than 255.
    if (*dirptr == 0xFF) return i;

    dirptr += sizeof(DIR);
  }
  return 0xFF;
}



DIR __xdata *dirNext(void)
//     Move to the next entry in the directory that isn't empty. 'dirSearch' is
// the entry number and 'dirSector' is the sector to read. Returns the address
// of the entry in TBLOCK so don't destroy TBLOCK whilst searching the
// directory.
{
  uint8_t __xdata *tptr = 0;

  // Set the pointer into TBLOCK.
  tptr = &TSECTOR[(dirSearch & 0x07) * 32];

 loop:
  // No more? This returns some sort of NULL.
  if (dirSearch == DIRENTRIES) return 0x0000;

  // If we're out of a block, read the next (or first).
  if ((dirSearch & 0x07) == 0)
  {
    ++dirSector;
    spiReadSector(dirSector, TSECTOR);
    tptr = TSECTOR;
  }

  // If we have an entry, return its address. Crank the index number for the
  // next search.
  if (*tptr != 0xFF)
  {
    ++dirSearch;
    return (DIR __xdata *) tptr;
  }

  ++dirSearch;
  tptr += sizeof(DIR);
  goto loop;
}



DIR __xdata *dirFirst(void)
//     Find the first entry in the directory and return a pointer to its 32
// byte entry or 0x0000 (NULL) if no entries.
{
  dirSearch = dirSector = 0;
  return dirNext();
}



static uint8_t locateFile(char __xdata *fname)
// Locate a file by name in the directory and return its index. The file name
// stored may be mixed case but the comparison is case insentive to accomodate
// Windows.
{
  DIR __xdata *d;
  
  d = dirFirst();
 loop:
  if (d == 0x0000) return 255;
  if (strEq((char __xdata *) d->fname, fname)) return dirSearch - 1;
  d = dirNext();
  goto loop;
}



__bit rmFS(char __xdata *fn)
// Remove a file from the directory. Returns 1 if not there and 0 if removed.
// Clean the directory entry out, follow the FAT links and remove from FAT,
// rewrite the FAT.
{
  uint8_t fno;
  DIR __xdata *d;
  uint16_t b;

  fno = locateFile(fn);
  if (fno == 255) return 1;
  // putstring(" fno = "); putbyte(fno); terpri();

  // Here's the DIR entry. Locate file has filled TSECTOR for us.
  d = (DIR __xdata *) &TSECTOR[(fno & DIRENTRIES_MASK) * sizeof(DIR)];
  b = d->blk;
  // putstring("  got b = "); putuint16(b); terpri();

  // Clear the directory entry and write it back out. The first directory
  // sector is always 1.
  d->fname[0] = 0xFF;
  spiWriteSector(1 + fno / DIRENTRIES_SECTOR, TSECTOR);
  // putstring("  wrote dir sector" ); putbyte(1 + fno / DIRENTRIES_SECTOR); terpri();
 

  // Clean up the FAT by looking at the last sector in each block.
 loop:
  // putstring("  clearing block "); putuint16(b); terpri();
  clearBlockInUse(b);
  spiReadSector((b * SECTORSPERBLOCK) + (SECTORSPERBLOCK - 1), TSECTOR);
  if (nsector == 0xFFFF || nsector == 0)
  {
    // putstring("  Writing FAT\n");
    writeFAT();
    return 0;
  }
  b = nsector;
  // putstring("    b = "); putuint16(b); terpri();
  goto loop;
}


uint8_t createFile(char __xdata *fname)
//     Create a file with the given name and return its file handle. Errors
// return 255.
{
  __xdata uint8_t       n;        // Directory entry number.
  __xdata uint8_t       fh;       // File handle to return.
  DIR   __xdata *d;       // New directory entry.
  MFILE __xdata *m;       // File control block address.
  __xdata uint16_t      eb;       // The block to start writing.

  // Remove the previous one if there.
  rmFS(fname);

  // Get a directory entry and a file handle. Return 255 if either fails.
  // The first call gets the free directory entry number into n and the
  // directory block into TSECTOR.
  if ((n = locateFreeDir()) == 255) return 255;
  
  if ((fh = locateFH()) == 255) return 255;

  // Get the directory entry.
  d = (DIR __xdata *) &TSECTOR[(n & DIRENTRIES_MASK) * sizeof(DIR)];

  // Now the file control block.
  m = (MFILE __xdata *) &FT[fh];
  m->dir_entry = n | WRITE_FILE,
    m->byte_ptr = 0,
    m->fsize = 0;

  // Where to put the first block. Punt if none left.
  if ((eb = locateEmptyBlock()) == 0) return 255;
  setBlockInUse(eb);
  m->next_blk = eb;

  // File name to directory entry and write out the new directory sector.
  strCpy(d->fname, fname);
  d->blk = eb,
    d->fsize = 0;
  spiWriteSector(1 + n / DIRENTRIES_SECTOR, TSECTOR);
  return fh;
}



uint8_t openFile(char __xdata *fname)
// Open an existing file for reading.
{
  uint8_t  fh;              // File handle to return.
  MFILE   __xdata *m;       // File control block address.
  DIR     __xdata *d;       // Directory entry address.
  uint8_t dn;               // Directory number.

  // If too many files open, then punt, otherwise get address of block.
  if ((fh = locateFH()) == 255) return 255;
  m = (MFILE __xdata *) &FT[fh];

  // Get the directory number of the file and quit if not found. Otherwise get
  // the directory adddress in TSECTOR.,
  if ((dn = locateFile(fname)) == 255) return 255;
  d = (DIR __xdata *) &TSECTOR[(dn  & DIRENTRIES_MASK) * sizeof(DIR)];

  // Fill in the file control block.
  m->dir_entry = dn,
    m->byte_ptr = 0,
    m->fsize = d->fsize,
    m->next_blk = d->blk;
  // putstring("First sector "); putuint16(d->blk * SECTORSPERBLOCK); terpri();
  return fh;
}



int8_t putchFile(uint8_t ch, uint8_t fh)
//     Write a character to an open file.
{
  MFILE __xdata *m;
  __xdata uint16_t      eb;
  
  // Address of table entry.
  if (fh >= OPENFILES)
  {
    putbyte(fh);
    putstring(" bad file handle\n");
    return -1;
  }
  m = (MFILE __xdata *) &FT[fh];

  // Verify open or not open for writing.
  if ((m->dir_entry == 0xFF) || ((m->dir_entry & WRITE_FILE) == 0))
  {
    putbyte(fh);
    putstring(" output file not open\n");
    return -1;
  }

  // Save the byte, crank index (0-1024) and file size.
  m->buffer[m->byte_ptr & SECTORMASK] = ch;
  ++m->fsize;
  ++m->byte_ptr;

  // Start a new sector after filling the previous one.
  if ((m->byte_ptr & SECTORMASK) == 0) 
  {
    // If this is the last sector in the block, then allocate the next block
    // and keep its address for the write.
    if (m->byte_ptr == SECTORSIZE * SECTORSPERBLOCK)
    {
      if ((eb = locateEmptyBlock()) == 0) return -1;
      setBlockInUse(eb);
      nsector = eb;
    }
  
    // Next sector is part of this block, leave blank.
    else
    {
      nsector = 0xFFFF;
      eb = m->next_blk;
    }

    // Write the sector and pointer (nsector) to next block out.
    spiWriteSector(m->next_blk * SECTORSPERBLOCK + m->byte_ptr / SECTORSIZE - 1,
                   m->buffer);
    m->next_blk = eb;

    // If SECTORSPERBLOCK sectors, then restart index for next block.
    if (m->byte_ptr == BLKSIZE) m->byte_ptr = 0;
  }
  return 0;
}



int16_t getchFile(uint8_t fh)
//     Return next byte from an open file or -1 if EOF reached. Error checks
// for file not open or bad file handle.
{
  MFILE __xdata *m;
  uint8_t       c;

  // Bad handle?
  if (fh >= OPENFILES)
  {
    putbyte(fh);
    putstring(" bad file handle\n");
    return -1;
  }

  // Table address.
  m = (MFILE __xdata *) &FT[fh];

  // File open for reading (the top bit can't be on or it's open for write or
  // not open at all)
  if (m->dir_entry & WRITE_FILE)
  {
    putbyte(fh);
    putstring(" input file not open\n");
    return -1;
  }

  // End of file? Return -1. m->fsize counts backward from size.
  if (m->fsize == 0) return -1;

  // Empty buffer?
  if ((m->byte_ptr & 0xFF) == 0)
  {

    // Last sector in block? Then next_blk will have the next block address.
    if (m->byte_ptr == BLKSIZE)
    {
      //      putstring("\nFirst sector ");
      //      putuint16(m->next_blk * SECTORSPERBLOCK);
      //      terpri();
      spiReadSector(m->next_blk * SECTORSPERBLOCK, m->buffer);
      m->byte_ptr = 0;
    }

    // If we need to read the last sector in the block do so. m->next_blk
    // points to the current block. 
    else if (m->byte_ptr == (BLKSIZE - SECTORSIZE))
    {
      //      putstring("\nLast sector ");
      //      putuint16(m->next_blk * SECTORSPERBLOCK + SECTORSPERBLOCK - 1);
      //      terpri();
      spiReadSector(m->next_blk * SECTORSPERBLOCK + SECTORSPERBLOCK - 1,
                    m->buffer);
      m->next_blk = nsector;
    }

    else
    {
      //      putstring("\nSector ");
      //      putuint16(m->next_blk * SECTORSPERBLOCK + m->byte_ptr / SECTORSIZE);
      //      terpri();
      spiReadSector(m->next_blk * SECTORSPERBLOCK + m->byte_ptr / SECTORSIZE,
                    m->buffer);
    }
  }
  c = m->buffer[m->byte_ptr & SECTORMASK];
  --(m->fsize);
  ++(m->byte_ptr);
  return c;
}



#define n DREG
int8_t closeFile(uint8_t fh)
// Close one of the two files that can be open.
{
  MFILE __xdata *m;
  DIR   __xdata *d;
  //  uint16_t n;

  m = (MFILE __xdata *) &FT[fh];

  // Quit if file not open\ - not really an error.
  if (m->dir_entry == 0xFF) return 0;

  // Write the last sector if we're writing.
  if (m->dir_entry & WRITE_FILE)
  {
    // If any bytes in buffer, then write last sector.
    nsector = 0xFFFF;
    if (m->byte_ptr & SECTORMASK)
    {
      //      putstring("last sector ");
      //      putuint16(m->next_blk * SECTORSPERBLOCK + m->byte_ptr / SECTORSIZE);
      //      terpri();
      spiWriteSector(m->next_blk * SECTORSPERBLOCK + m->byte_ptr / SECTORSIZE,
                     m->buffer);
    }

    // Update the directory. Get the directory sector number, read it, 
    n = 1 + (m->dir_entry & 0x7F) / DIRENTRIES_SECTOR;
    spiReadSector(n, TSECTOR);
    d = (DIR __xdata*) &TSECTOR[(m->dir_entry & DIRENTRIES_MASK) * sizeof(DIR)];
    d->fsize = m->fsize;
    spiWriteSector(n, TSECTOR);
    writeFAT();
  }

  // Close out the entry. Come here too if just reading a file.
  m->dir_entry = 0xFF;
  return 0;
}
#undef n



void listDir(void)
//     List the current directory contents - file size, then file name. Keep
// arranged in nice columns.
{
  DIR __xdata *d;

  d = dirFirst();
  if (d == 0x0000)
  {
    putstring("No files\n");
    return;
  }

 loop:
  putuint32(d->fsize);
  if (d->fsize < 10) putstring("       ");
  else if (d->fsize < 100) putstring("      ");
  else if (d->fsize < 1000) putstring("     ");
  else if (d->fsize < 10000) putstring("    ");
  else if (d->fsize < 100000) putstring("   ");
  else if (d->fsize < 1000000) putstring("  ");
  else putstring(" ");
  putstring(d->fname);
  terpri();
  d = dirNext();
  if (d == 0x0000) return;
  goto loop;
}


void deleteFile(void)
//     Delete the file name given.
{
  rmFS((char __xdata *) STR);
}



void readSector(void)
//    Read a sector and dump it in hex and ASCII. Also dump the two bytes
// in the sector extension.
{
  uint16_t i;
  uint16_t j;
  uint8_t  c;
  ntok();
  spiReadSector(N, TSECTOR);
  for (i = 0; i < 256; i += 16)
  {
    putstring("0x");
    puthexbyte(i);
    putstring(": ");
    for (j = 0; j < 16; ++j)
    {
      putch(' ');
      puthexbyte(TSECTOR[i + j]);
    }
    putstring(" ");
    for (j = 0; j < 16; ++j)
    {
      c = TSECTOR[i + j];
      if (c >= 32 && c < 128) putch(c); else putch(' ');
    }
    terpri();
  }
  putstring("****: ");
  puthexbyte(nsector >> 8);
  putch(' ');
  puthexbyte(nsector);
  terpri();
  return;
}


void writeSector(void)
{
  ntok();

  putstring("Writing "); putuint32(N); terpri();
  //  spiWriteSector(N, ABLOCK);
  writeFAT();
  putstring("Done\n");
}



// EOF
