# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# files.py - MicroSD and related functions.
#
import pyb, ckcc, os, sys, utime, glob
from uerrno import ENOENT

async def needs_microsd():
    # Standard msg shown if no SD card detected when we need one.
    from ux import ux_show_story
    return await ux_show_story("Please insert a MicroSD card before attempting this operation.")

def is_dir(fname):
    if os.stat(fname)[0] & 0x4000:
        return True
    return False

def _try_microsd():
    # Power up, mount the SD card, return False if we can't for some reason.
    # - we know card is there already, and mux set appropriately
    #
    sd = pyb.SDCard()

    if ckcc.is_simulator():
        return True

    try:
        # already mounted and ready?
        st = os.statvfs('/sd')
        return True
    except OSError:
        pass

    try:
        sd.power(1)
        os.mount(sd, '/sd', readonly=0, mkfs=0)
        st = os.statvfs('/sd')

        return True

    except OSError as exc:
        # corrupt or unformated SD card (or something)
        #sys.print_exception(exc)
        return False

def wipe_flash_filesystem(do_rebuild=True):
    # erase and re-format the flash filesystem (/flash/**)
    import ckcc, pyb
    from glob import dis

    dis.fullscreen('Erasing...')
    os.umount('/flash')

    # from extmod/vfs.h
    BP_IOCTL_SEC_COUNT = (4)
    BP_IOCTL_SEC_SIZE  = (5)

    # block-level erase
    fl = pyb.Flash(start=0)         # start=0 does magic things
    bsize = fl.ioctl(BP_IOCTL_SEC_SIZE, 0)
    assert bsize == 512
    bcount = fl.ioctl(BP_IOCTL_SEC_COUNT, 0)

    blk = bytearray(bsize)
    ckcc.rng_bytes(blk)

    for n in range(bcount):
        fl.writeblocks(n, blk)
        dis.progress_sofar(n, bcount)
        
    if not do_rebuild:
        return

    # rebuild and mount /flash
    dis.fullscreen('Rebuilding...')

    # no need to erase, we just put new FS on top
    import mk4
    mk4.make_flash_fs()

    # re-store current settings
    from glob import settings
    settings.save()

def wipe_microsd_card():
    # Erase and re-format SD card. Not secure erase, because that is too slow.
    import ckcc, pyb
    from glob import dis
    from version import num_sd_slots

    if not CardSlot.is_inserted():
        return
    
    try:
        # just in case
        os.umount('/sd')
    except:
        pass

    if num_sd_slots == 2:
        # pick slot with a card or default A if both installed
        slot_b = (CardSlot.sd_detect2() == 0)
        if CardSlot.sd_detect() == 0:
            slot_b = False
        CardSlot.mux(1 if slot_b else 0)      # top slot = A
        active_led = CardSlot.active_led2 if slot_b else CardSlot.active_led1
    else:
        active_led = CardSlot.active_led
        slot_b = False

    try:
        active_led.on()

        sd = pyb.SDCard()
        assert sd

        # power cycle so card details (like size) are re-read from current card
        sd.power(0)
        sd.power(1)

        dis.fullscreen('Formatting...')
        cutoff = 1024       # arbitrary
        blk = bytearray(512)
        ckcc.rng_bytes(blk)

        for bnum in range(cutoff):
            sd.writeblocks(bnum, blk)
            dis.progress_bar_show(bnum/cutoff)

        # remount, with newfs option -- this does the formating (very quick)
        os.mount(sd, '/sd', readonly=0, mkfs=1)

        # done, cleanup
        os.umount('/sd')
    finally:
        active_led.off()

        # important: turn off power
        sd = pyb.SDCard()
        sd.power(0)

        if slot_b:
            # optional?
            CardSlot.mux(0)      # top slot = A

def dfu_parse(fd):
    # do just a little parsing of DFU headers, to find start/length of main binary
    # - not trying to support anything but what ../stm32/Makefile will generate
    # - see external/micropython/tools/pydfu.py for details
    # - works sequentially only
    import struct
    from ucollections import namedtuple

    fd.seek(0)

    def consume(xfd, tname, fmt, names):
        # Parses the struct defined by `fmt` from `data`, stores the parsed fields
        # into a named tuple using `names`. Returns the named tuple.
        size = struct.calcsize(fmt)
        here = xfd.read(size)
        ty = namedtuple(tname, names.split())
        values = struct.unpack(fmt, here)
        return ty(*values)

    dfu_prefix = consume(fd, 'DFU', '<5sBIB', 'signature version size targets')

    #print('dfu: ' + repr(dfu_prefix))

    assert dfu_prefix.signature == b'DfuSe', "Not a DFU file (bad magic)"

    for idx in range(dfu_prefix.targets):

        prefix = consume(fd, 'Target', '<6sBI255s2I', 
                                   'signature altsetting named name size elements')

        #print("target%d: %r" % (idx, prefix))

        for ei in range(prefix.elements):
            # Decode target prefix
            #   <   little endian
            #   I   uint32_t    element address
            #   I   uint32_t    element size
            elem = consume(fd, 'Element', '<2I', 'addr size')

            #print("target%d: %r" % (ei, elem))

            # assume bootloader at least 32k, and targeting flash.
            assert elem.addr >= 0x8008000, "Bad address?"

            return fd.tell(), elem.size


class CardMissingError(RuntimeError):
    pass

class CardSlot:
    # Manage access to the SDCard h/w resources
    last_change = None
    active_led = None

    @classmethod
    def setup(cls):
        # Watch the SD card-detect signal line... but very noisy
        # - this is called a few seconds after system startup

        from pyb import ExtInt
        from machine import Pin
        from version import num_sd_slots

        def card_change(_):
            # Careful: these can come fast and furious!
            cls.last_change = utime.ticks_ms()

        cls.last_change = utime.ticks_ms()

        if num_sd_slots == 2:
            # Q has luxurious dual slots
            cls.mux = Pin('SD_MUX', Pin.OUT, value=0)
            cls.sd_detect2 = Pin('SD_DETECT2', Pin.IN, pull=Pin.PULL_UP)
            cls.irq2 = ExtInt(cls.sd_detect2, ExtInt.IRQ_RISING_FALLING, Pin.PULL_UP, card_change)

            cls.active_led2 = Pin('SD_ACTIVE2', Pin.OUT)
            cls.active_led1 = Pin('SD_ACTIVE', Pin.OUT)
            cls.active_led = cls.active_led1
        else:
            cls.mux = None
            cls.active_led = Pin('SD_ACTIVE', Pin.OUT)

        cls.sd_detect = Pin('SD_DETECT', Pin.IN, pull=Pin.PULL_UP)
        cls.irq = ExtInt(cls.sd_detect, ExtInt.IRQ_RISING_FALLING, Pin.PULL_UP, card_change)

    @classmethod
    def is_inserted(cls):
        # Sense is inverted on Mk4, and true on Q.
        if cls.mux:
            return (cls.sd_detect() == 0) or (cls.sd_detect2() == 0)
        else:
            return cls.sd_detect() == 1

    @classmethod
    def both_inserted(cls):
        # Predicate: Is there a card in both slots? If only one slot exists, return None
        if cls.mux:
            return (cls.sd_detect() == 0) and (cls.sd_detect2() == 0)
        # return None (implicit)

    def __init__(self, force_vdisk=False, readonly=False, slot_b=None):
        self.mountpt = None
        self.force_vdisk = force_vdisk
        self.readonly = readonly
        self.wrote_files = set()
        if self.mux:
            use_b_slot = False  # default A if both installed, or none
            sa, sb = self.sd_detect() == 0, self.sd_detect2() == 0
            if slot_b and sb:
                # user choose B and B is present - proceed
                use_b_slot = True
            elif not slot_b and not sa and sb:
                # user didn't choose B, but A is not present and B is
                # write to B
                use_b_slot = True

            self.mux(1 if use_b_slot else 0)  # top slot = A
            self.active_led = self.active_led2 if use_b_slot else self.active_led1

    def __enter__(self):
        # maybe use our virtual disk in preference to SD Card
        if glob.VD and (self.force_vdisk or not self.is_inserted()):
            self.mountpt = glob.VD.mount(self.readonly)
            return self

        if not self.is_inserted():
            # bugfix on Q: #618
            raise CardMissingError

        # Get ready!
        self.active_led.on()

        # busy wait for card pin to debounce/settle
        while 1:
            since = utime.ticks_diff(utime.ticks_ms(), self.last_change)
            if since > 50:
                break
            utime.sleep_ms(5)

        # attempt to use micro SD
        ok = _try_microsd()

        if not ok:
            self._recover()

            raise CardMissingError

        self.mountpt = self.get_sd_root()       # probably /sd

        return self

    def __exit__(self, *a):
        if self.mountpt == self.get_sd_root():
            self._recover()
        elif glob.VD:
            glob.VD.unmount(self.wrote_files, self.readonly)

        self.mountpt = None

        # just in case?
        if self.mux:
            self.mux(0)

        return False

    def open(self, fname, mode='r', **kw):
        # open a file for read/write
        # - track new files for virtdisk case
        if 'w' in mode:
            assert not self.readonly
            self.wrote_files.add(fname)

        return open(fname, mode, **kw)
        
    def _recover(self):
        # done using the microSD -- unpower it
        self.active_led.off()

        try:
            assert self.mountpt == '/sd'
            os.umount('/sd')
        except: pass

        # turn off power to slot
        sd = pyb.SDCard()
        sd.power(0)

    def get_sd_root(self):
        # get the path to the SD card
        if ckcc.is_simulator():
            return ckcc.get_sim_root_dirs()[1]
        else:
            return '/sd'

    def get_paths(self):
        # (full) paths to check on the card
        #root = self.get_sd_root()
        #return [root]
        return [self.mountpt]

    def is_dir(self, fname):
        return is_dir(self.abs_path(fname))

    def abs_path(self, fname):
        return self.mountpt + "/" + fname

    def get_id_hash(self):
        # hash over card config and serial # details
        # - stupidly it's over the repr of a functions' result
        import ngu

        info = pyb.SDCard().info()
        assert info 

        if len(info) == 3:
            # expected in v4
            csd_cid = pyb.SDCard().ident()
            info = tuple(list(info) + list(csd_cid))

        return ngu.hash.sha256s(repr(info))

    @staticmethod
    def exists(fname):
        try:
            os.stat(fname)
        except OSError as e:
            if e.args[0] == ENOENT:
                return False
        return True

    def pick_filename(self, pattern, path=None, overwrite=False):
        # given foo.txt, return a full path to filesystem, AND
        # a nice shortened version of the filename for display to user
        # - assuming we will write to it, so cannot exist
        # - return None,None if no SD card or can't mount, etc.
        # - no UI here please
        import ure

        assert self.mountpt      # else: we got used out of context mgr

        # put it back where we found it
        path = path or (self.mountpt + '/')

        assert '/' not in pattern
        assert '.' in pattern

        basename, ext = pattern.rsplit('.', 1)
        ext = '.' + ext

        # try w/o any number first
        fname = path + basename + ext

        if overwrite or not self.exists(fname):
            return fname, basename + ext

        # look for existing numbered files, even if some are deleted, and pick next
        # highest filename
        highest = 1
        pat = ure.compile(basename + r'-(\d+)' + ext)

        for fn in os.listdir(path):
            m = pat.match(fn)
            if not m: continue
            highest = max(highest, int(m.group(1)))
                
        fname = path + basename + ('-%d'% (highest+1)) + ext

        return fname, fname[len(path):]

    def securely_blank_file(self, full_path):
        # input PSBT file no longer required; so delete it
        # - blank with zeros
        # - rename to garbage (to hide filename after undelete)
        # - delete 
        # - ok if file missing already (card maybe have been swapped)
        #
        # NOTE: we know the FAT filesystem code is simple, see 
        #       ../external/micropython/extmod/vfs_fat.[ch]

        self.wrote_files.discard(full_path)

        path, basename = full_path.rsplit('/', 1)

        try:
            blk = bytes(64)

            with open(full_path, 'r+b') as fd:
                size = fd.seek(0, 2)
                fd.seek(0)

                # blank it
                for i in range((size // len(blk)) + 1):
                    fd.write(blk)

                assert fd.seek(0, 1) >= size

            # probably pointless, but why not:
            os.sync()

        except OSError as exc:
            # missing file is okay
            if exc.args[0] == ENOENT: return
            raise

        # rename it and delete
        new_name = path + '/' + ('x'*len(basename))
        os.rename(full_path, new_name)
        os.remove(new_name)

# EOF
