# cPy / mPy - filesystem for Fram devices like 64 kb MB85RS64
# Copyright by Anthony Vogelaar, Perth Scotland
# rev 0.10  2025-01-28  initial release

import  struct
import  json

# =================== FramFs =======================
#Implemented:
# FramFs(fram)
#    FramFs.toFatRec(begin, end, id)    -->  b
#    FramFs.fromFatRec(rec)             -->  t
#    FramFs.erase()
#    FramFs.dump(start=0, end=None)
#    FramFs.write(id, obj)
#    FramFs.read(id)                    -->  obj
#    FramFs.delete(id)
#    FramFs.ls()                        -->  list
#    FramFs.printLs()
#    FramFs.defragment()

# Description for MB85RS64 and other sizes:
# the first 1024 byte are reserved as fat (file allocation table) for 64 files
# with a name id. Only the first 12 characters of id are stored in a 16 byte record.
# bytes 0.. 1 start address of file content
# bytes 2.. 3 first byte after file content
# bytes 4..15 file name
# The remaining room is for data. As the fram MB85RS64 has 8192 bytes, 7168 bytes is
#  available for data / files. When all 64 files are used the average available file size
# on the MB85RS64 is 7168 / 64 = 112 bytes
# a record is marked as deleted when it starts with 0xFFFF 

class FramFs:
    
    # convert begin, end and id into a 16_bytes fat record
    @staticmethod
    def toFatRec(begin, end, id):
        id = id[:10]
        id = b''.join([id.encode(), bytes(12-len(id))])
        return struct.pack('HH12s', begin, end, id)


    # convert a 16 bytes fat record to begin, end and id
    @staticmethod
    def fromFatRec(rec):
        begin, end, id = struct.unpack('HH12s', rec)
        id = id.decode().rstrip('\x00')
        return begin, end, id


    # fram is an instance implementin SPI communication or emulating a Fram device
    def __init__(self, fram):
        self.fram   = fram
        self.size   = fram.size
        
        
    # erase all content of fram to 0x00
    def erase(self):
        data = bytes(0x0100)						# erase in blocks of 256 bytes
        addr = 0
        while addr < self.size:
            self.fram.write(addr, data)
            addr += 0x0100


    # dump fram content to console, if end=None then dump all
    def dump(self, start=0, end=None):
        end = end if end else self.size
        start = 0x20 * (start // 0x20)
        end   = 0x20 * ((end + 0x1F)  // 0x20)
        while start < end:
            print(f'{start:04X} ', end='')
            print(self.fram.read(start, 0x20).hex(' '))
            start += 0x20
            
    
    # write utf8 string or obj (<= 255 bytes) to fram with ref id
    def write(self, id, obj):
        if not isinstance(obj, str):
            obj = json.dumps(obj)                       # exception if not json compatible
        start = self.size // 8                          # start address of data area
        for i in range(64):                             # find address of start free area
            fat_rec = self.fram.read(i*0x10, 0x10)
            begin, end, _id = self.fromFatRec(fat_rec)
            if begin == 0: break
            if begin != 0xFFFF: start = end
        else: raise ValueError('FRamFS write: fat full')
        l = self.fram.swrite(start, obj)
        fat_rec = self.toFatRec(start, start+l, id)
        self.fram.write(i*0x10, fat_rec)
        
        
    # read utf8 string or obj from fram with ref id
    def read(self, id):
        start, end, idx = self._getIdAddr(id)
        obj = self.fram.sread(start)
        try   : obj = json.loads(obj)                   # try to convert to obj
        except: pass
        return obj
    
        
    # delete fram string with ref id
    def delete(self, id):
        start, end, idx = self._getIdAddr(id)
        self.fram.write(idx << 4, b'\xFF\xFF' + bytes(14))
        
        
    # get (idx, id, begin, end) is (file_handle, file_name, begin, end) of fat
    def ls(self):
        res = []
        for i in range(64):
            fat_rec = self.fram.read(i*0x10, 0x10)
            begin, end, id = self.fromFatRec(fat_rec)
            if begin == 0: break
            if begin != 0xFFFF:
                res.append([i, id, begin, end])
        return res


    # print (idx, id, begin, end) is (file_handle, file_name, begin, end) of fat
    def printLs(self):
        for i in range(64):
            fat_rec = self.fram.read(i*0x10, 0x10)
            begin, end, id = self.fromFatRec(fat_rec)
            if begin == 0: return
            if begin != 0xFFFF:
                print(f'{i:3} {id:13} {begin:4} {end:4}')


    def defragment(self):
        dest_idx = 0
        next     = self.size // 8
        for idx in range(64):
            fat_rec = self.fram.read(0x10 * idx, 0x0010)
            start, end, id = self.fromFatRec(fat_rec)
            if start == 0x0000   : break
            if start == 0xFFFF   : continue
            if idx   == dest_idx : 
                next      = end; 
                dest_idx += 1; continue
            lng  = end - start
            data = self.fram.read(start, lng)
            self.fram.write(next, data)
            fat_rec = self.toFatRec(next, next+lng, id)
            self.fram.write(0x10 * dest_idx, fat_rec)
            next += lng; dest_idx += 1
        for i in range(dest_idx, 64):                   # clear remaining fat
            self.fram.write(0x10 * i, bytes(10))


    # local, get start and end address of id and index in fat
    def _getIdAddr(self, id):
        for i in range(64):
            fat_rec = self.fram.read(0x10 * i, 0x0010)
            start, end, _id = self.fromFatRec(fat_rec)
            if start == 0x0000:
                raise ValueError('FRamFS: id unknown')
            if start != 0xFFFF and id == _id:
                return start, end, i 
        else: raise ValueError('FRamFS: id unknown')

# ======================== End ==========================
