# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# selftest.py - Interactive Selftest code
#
import ckcc
from uasyncio import sleep_ms
from glob import dis, settings
from ux import ux_wait_keydown, ux_clear_keys, ux_poll_key
from ux import ux_show_story
from callgate import get_is_bricked, get_genuine, clear_genuine
from utils import problem_file_line
import version
from charcodes import KEY_ENTER, KEY_CANCEL, KEY_QR

try:
    from display import FontLarge
except ImportError:
    FontLarge = None

async def wait_ok():
    k = await ux_wait_keydown('xy' + KEY_ENTER + KEY_CANCEL)
    if k not in 'y' + KEY_ENTER:
        raise RuntimeError('Canceled')

def label_test(line1, line2=''):
    dis.clear()
    if version.has_qwerty:
        dis.text(None, 1, line1)
        dis.text(None, 3, line2)
    else:
        dis.text(None, 10, line1)
        dis.text(None, 34, line2, font=FontLarge)
    dis.show()

async def test_numpad():
    # do an interactive self test

    keys = list('123456789x0y')

    for ch in keys:
        dis.clear()
        dis.text(0,0, "Numpad Test. Press:")
        dis.text(None,24, ch if ch != 'y' else 'OK', FontLarge)
        dis.show()

        k = await ux_wait_keydown(ch + 'x')
        if k == 'x' and ch != 'x':
            raise RuntimeError("numpad test aborted")
        assert k == ch

async def test_gpu():
    import gpu_binary

    try:
        dis.gpu.take_spi()
        dis.clear()
        dis.text(0,-1, "GPU Test okay?")
        dis.show()

        assert dis.gpu.get_version() == gpu_binary.VERSION
        dis.bootrom_takeover()
        dis.gpu.show_test_pattern()

        await wait_ok()
    finally:
        # not required?
        dis.real_clear()

async def test_keyboard():
    # for Q1 - just some of them: one in each row and column

    keys = list('1wdvgy7k.p '+KEY_QR)
    
    for ch in keys:
        dis.clear()
        dis.text(None,2, "Keyboard Test. Press:")
        dis.text(None,4, 'SPACE' if ch == ' ' else ch.upper())
        dis.show()

        k = await ux_wait_keydown(ch + KEY_CANCEL)
        if k == KEY_CANCEL:
            raise RuntimeError("kbd test aborted")
        assert k == ch

async def test_qr_scanner():
    # QR Scanner module: assume pretested, just testing connection really
    from glob import SCAN

    # Can take 2+ seconds to setup the QR scanner (from powerup), and
    # if batteries are right, we can hit that timeframe in factory test case.
    for i in range(25):
        if SCAN.setup_done:
            break
        await sleep_ms(100)

    assert SCAN and SCAN.version, 'missing'
    assert SCAN.version.startswith('V2.'), 'QR: ' + SCAN.version

async def test_battery():
    from battery import get_batt_level

    try:
        from machine import ADC
        # expect 11500 or so, and certainly not 65535
        assert max(ADC(ADC.CORE_VREF).read_u16() for i in range(10)) < 60000, 'VREF fix not done'
    except ImportError:
        # simulator
        pass

    dis.clear()
    dis.text(None, 1, "Battery Test")
    dis.text(None, 3, "Connect 3.3v reference cells.")

    while 1:
        lvl = get_batt_level()

        if lvl and 3.2 <= lvl <= 3.4:
            # pass
            return 

        if lvl:
            dis.text(None, -1, "VIN Sense reads: %.1f volts" % lvl, dark=True)
        else:
            dis.text(None, -1, "Remove USB plug.", dark=True)
        dis.show()

        if settings.get('tested', False):
            # auto-pass this test if we've been thru here before; customer may
            # not have magic batteries and so on.
            break

        # show for 100ms or until they press key
        ch = await ux_wait_keydown(KEY_CANCEL+'s', 100)
        if not ch: continue

        if ch == 's':
            # undocumented: 'skip'
            return
        elif ch == KEY_CANCEL:
            raise RuntimeError("Battery test aborted")
        else:
            raise ValueError(ch)

def set_genuine():
    # PIN must be blank for this to work
    # - or logged in already as main
    from pincodes import pa

    if not pa.is_successful():
        # assume blank pin during factory selftest
        pa.setup(b'')

        if not pa.is_successful():
            pa.login()
            assert pa.is_successful()       # "PIN not blank?"

    # do verify step
    dis.bootrom_takeover()
    pa.greenlight_firmware()

    dis.show()

async def test_secure_element():

    assert not get_is_bricked()         # bricked already

    if ckcc.is_simulator(): return

    for ph in range(5):
        gg = get_genuine()

        dis.clear()

        if version.has_qwerty or version.mk_num == 5:
            dis.text(0, 0, "^^-- Green?      " if gg else "   ^^-- Red?")
        else:
            if gg:
                dis.text(-1, 8, "Green ON? -->")
            else:
                dis.text(-1,50, "Red ON? -->")

        dis.show()
        await wait_ok()

        if ph and gg:
            # stop once it's on and we've tested both states
            return

        # attempt to switch to other state
        if gg:
            clear_genuine()
        else:
            # very slow!
            dis.fullscreen("Wait...")
            set_genuine()
            ux_clear_keys()
            dis.busy_bar(False)

        ng = get_genuine()
        assert ng != gg     # "Could not invert LED"
            
async def test_sd_active():
    # Mark 2+: SD Card active light.
    # Q1: dual slots
    from machine import Pin

    for num in range(version.num_sd_slots):

        led = Pin('SD_ACTIVE' if not num else 'SD_ACTIVE2', Pin.OUT)

        for ph in range(2):
            gg = not ph
            led.value(gg)

            dis.clear()
            if version.has_qwerty:
                if num == 0:
                    dis.text(0, 2, "<-- SD A is %s?  " % ('ON' if gg else 'off'))
                else:
                    dis.text(0, 7, "<-- SD B is %s?  " % ('ON' if gg else 'off'))
            else:
                if gg:
                    dis.text(0,16, "<-- Green ON?")
                else:
                    dis.text(0,16, "<-- Green off?")
            dis.show()

            await wait_ok()

async def test_usb_light():
    # Mk4's new USB activity light (right by connector)
    from machine import Pin
    p = Pin('USB_ACTIVE', Pin.OUT)

    try:
        p.value(1)
        label_test("USB light is on?")

        await wait_ok()
    finally:
        p.value(0)

async def test_nfc_light():
    if not version.has_qwerty:
        return

    from machine import Pin
    p = Pin('NFC_ACTIVE', Pin.OUT)

    try:
        p.value(1)
        dis.clear()
        dis.text(-1, -1, "NFC light green? --->")
        dis.show()

        await wait_ok()
    finally:
        p.value(0)

async def test_nfc():
    # Mk4: NFC chip and field
    if not version.has_nfc: return
    from nfc import NFCHandler
    await NFCHandler.selftest()
    
async def test_psram():
    from glob import PSRAM
    from ustruct import pack
    import ngu

    label_test('PSRAM Test')

    test_len = PSRAM.length * 2
    chk = bytearray(32)
    spots = []
    for pos in range(0, PSRAM.length, 4000 * 17):
        if pos >= PSRAM.length: break
        rnd = ngu.hash.sha256s(pack('I', pos))

        PSRAM.write(pos, rnd)
        PSRAM.read(pos, chk)
        assert chk == rnd, "bad @ 0x%x" % pos
        dis.progress_bar_show(pos / test_len)
        spots.append(pos)

    for pos in spots:
        rnd = ngu.hash.sha256s(pack('I', pos))
        PSRAM.read(pos, chk)
        assert chk == rnd, "RB bad @ 0x%x" % pos
        dis.progress_bar_show((PSRAM.length + pos) / test_len)

async def test_oled():
    # all on/off tests
    for ph in (1, 0):
        dis.clear()
        dis.dis.fill(ph)
        dis.text(None,2, "Selftest", invert=ph)
        dis.text(None,30, "All on?" if ph else 'All off?', invert=ph, font=FontLarge)
        dis.show()

        await wait_ok()

async def test_lcd():
    # Very basic
    try:
        for nm, col in [('RED', 0xf800), ('GREEN', 0x07e0), ('BLUE', 0x001f)]:
            dis.real_clear()
            dis.dis.fill_screen(col)
            dis.text(1,1, "Selftest")
            dis.text(None,3, "All pixels are %s?" % nm)
            dis.show()

            await wait_ok()
    finally:
        dis.real_clear()
        dis.draw_status(full=1)
        dis.clear()

async def test_microsd():
    if ckcc.is_simulator(): return
    from version import num_sd_slots
    from files import CardSlot
    import os

    def _is_inserted(slot_num):
        if num_sd_slots > 1:
            if slot_num == 0:
                return CardSlot.sd_detect() == 0
            elif slot_num == 1:
                return CardSlot.sd_detect2() == 0
            else:
                assert False
        else:
            return CardSlot.is_inserted()

    async def wait_til_state(num, want):
        title = 'MicroSD Card'
        if num_sd_slots > 1:
            title += ' ' + chr(65+num)
        label_test(title +':', 'Remove' if _is_inserted(num) else 'Insert')

        while 1:
            if want == _is_inserted(num): return
            await sleep_ms(100)
            if ux_poll_key():
                raise RuntimeError("MicroSD test aborted")

    for slot_num in range(num_sd_slots):
        # test presence switch
        for ph in range(7):
            await wait_til_state(slot_num, not _is_inserted(slot_num))

            if ph >= 2 and _is_inserted(slot_num):
                # debounce
                await sleep_ms(100)
                if _is_inserted(slot_num): break
                if ux_poll_key():
                    raise RuntimeError("MicroSD test aborted")

        label_test('MicroSD Card:', 'Testing')

        # card inserted
        assert _is_inserted(slot_num)     #, "SD not present?"

        with CardSlot(slot_b=slot_num) as card:

            fn, _ = card.pick_filename('test-delme.txt')

            with open(fn, 'wt') as fd:
                fd.write("Hello")
            with open(fn, 'rt') as fd:
                assert fd.read() == "Hello"

            os.unlink(fn)

        # force removal, so cards don't get stuck in finished units
        await wait_til_state(slot_num, False)


async def start_selftest():
    try:
        if version.has_battery:
            await test_battery()

        if version.has_qr:
            await test_qr_scanner()

        if not version.has_qwerty:
            await test_oled()
        else:
            await test_lcd()
            await test_gpu()

        await test_psram()
        await test_nfc_light()
        await test_nfc()

        if version.has_qwerty:
            await test_keyboard()
        else:
            await test_numpad()

        await test_secure_element()
        await test_sd_active()
        await test_usb_light()
        await test_microsd()

        # add more tests here

        settings.set('tested', True)
        await ux_show_story("Selftest complete", 'PASS')

    except (RuntimeError, AssertionError) as e:
        e = str(e) or problem_file_line(e)
        await ux_show_story("Test failed:\n" + str(e), 'FAIL')
    
# EOF
