#!/usr/bin/env python3
#
# Copyright 2021 NXP
# SPDX-License-Identifier: BSD-3-Clause
#
# TEST CODE of NXP USBSIO Library - SPI tests
#

import functools
import logging
import os
import sys
import unittest
from test import *

# global SPI test parameters
if TARGET == T_LPCLINK2:
    SPI_PORT = 0
    SPI_SSEL = (0, 15)
elif TARGET == T_MCULINK_PRO:
    SPI_PORT = 0
    SPI_SSEL = (0, 0) # SSEL is actually ignored by the MCU Link bridge.
elif TARGET == T_MCULINK_55S36:
    SPI_PORT = 0
    SPI_SSEL = (0, 0) # SSEL is actually ignored by the MCU Link bridge.

SPI_BUS_SPEED = 500000
SPI_ECHO_DATA = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
#SPI_ECHO_DATA = b"1234567890";


# SIOTEST protocol description:
#
#  RECEIVE:   >[XXXX][cmd]$
#     >      - start of frame character which resets our receiver state machine
#     [XXXX] - four hex bytes indicating length of [cmd]
#     [cmd]  - command and arguments as a text, multiple commands may be separated by ;
#     $      - command terminator to validate the [cmd] length matches XXXX
#  TRANSMIT:  <[YYYY][resp]$
#     <      - start of frame character which resets the remote receiver's state machine
#     [YYYY] - four hex bytes indicating length of [resp]
#     [resp] - response text (generated by commands processing)
#     $      - response terminator to validate the [resp] length matches YYYY

def use_spi(portNum):
    '''Decorator to automatically open the SIO library and SPI port'''
    def decorator(func):
        @functools.wraps(func)
        def wrapper(self, *args, **kwargs):
            ports = self.sio.GetNumPorts(vidpids=VIDPIDS)
            self.assertTrue(ports > 0)
            if ports>0:
                ok = self.sio.Open(0)
                self.assertTrue(ok)
                if ok:
                    self.spi = self.sio.SPI_Open(SPI_BUS_SPEED, portNum=portNum, preDelay=100)
                    if self.spi:
                        return func(self, *args, **kwargs)
            raise Exception("The 'use_spi(%d)' decorator has failed to open SPI port" % portNum)
        return wrapper
    return decorator

class TestSPI(TestBase):
    def test_SPI_OpenOptions(self):
        self.assertEqual(LIBUSBSIO._SPI_OpenOptions(0, dataSize=8, cpol=0, cpha=0, preDelay=0, postDelay=0),
                         LIBUSBSIO.SPI_CONFIG_OPTION_DATA_SIZE_8)
        self.assertEqual(LIBUSBSIO._SPI_OpenOptions(0, dataSize=16, cpol=0, cpha=0, preDelay=0, postDelay=0),
                         LIBUSBSIO.SPI_CONFIG_OPTION_DATA_SIZE_16)
        self.assertEqual(LIBUSBSIO._SPI_OpenOptions(0, dataSize=8, cpol=1, cpha=0, preDelay=0, postDelay=0),
                         LIBUSBSIO.SPI_CONFIG_OPTION_DATA_SIZE_8 | LIBUSBSIO.SPI_CONFIG_OPTION_POL_1)
        self.assertEqual(LIBUSBSIO._SPI_OpenOptions(0, dataSize=8, cpol=0, cpha=1, preDelay=0, postDelay=0),
                         LIBUSBSIO.SPI_CONFIG_OPTION_DATA_SIZE_8 | LIBUSBSIO.SPI_CONFIG_OPTION_PHA_1)
        self.assertEqual(LIBUSBSIO._SPI_OpenOptions(0, dataSize=8, cpol=1, cpha=1, preDelay=0, postDelay=0),
                         LIBUSBSIO.SPI_CONFIG_OPTION_DATA_SIZE_8 | LIBUSBSIO.SPI_CONFIG_OPTION_POL_1 | LIBUSBSIO.SPI_CONFIG_OPTION_PHA_1)
        self.assertEqual(LIBUSBSIO._SPI_OpenOptions(0, dataSize=8, cpol=0, cpha=0, preDelay=200, postDelay=0),
                         LIBUSBSIO.SPI_CONFIG_OPTION_DATA_SIZE_8 | LIBUSBSIO.SPI_CONFIG_OPTION_PRE_DELAY(200))
        self.assertEqual(LIBUSBSIO._SPI_OpenOptions(0, dataSize=8, cpol=0, cpha=0, preDelay=0, postDelay=200),
                         LIBUSBSIO.SPI_CONFIG_OPTION_DATA_SIZE_8 | LIBUSBSIO.SPI_CONFIG_OPTION_POST_DELAY(200))
        self.assertEqual(LIBUSBSIO._SPI_OpenOptions(0, dataSize=8, cpol=0, cpha=0, preDelay=100, postDelay=100),
                         LIBUSBSIO.SPI_CONFIG_OPTION_DATA_SIZE_8 | LIBUSBSIO.SPI_CONFIG_OPTION_PRE_DELAY(100) | LIBUSBSIO.SPI_CONFIG_OPTION_POST_DELAY(100))
        self.assertEqual(LIBUSBSIO._SPI_OpenOptions(0, dataSize=8, cpol=1, cpha=1, preDelay=100, postDelay=100),
                         LIBUSBSIO.SPI_CONFIG_OPTION_DATA_SIZE_8 | LIBUSBSIO.SPI_CONFIG_OPTION_POL_1 | LIBUSBSIO.SPI_CONFIG_OPTION_PHA_1 | LIBUSBSIO.SPI_CONFIG_OPTION_PRE_DELAY(100) | LIBUSBSIO.SPI_CONFIG_OPTION_POST_DELAY(100))

    @use_spi(SPI_PORT)
    def test_SPI_OpenClose(self):
        self.assertTrue(isinstance(self.spi, LIBUSBSIO.SPI))
        self.assertTrue(self.spi._h)
        self.assertEqual(self.spi.Close(), 0)
        self.assertFalse(self.spi._h)

    @use_spi(SPI_PORT)
    def test_SPI_IsAnyOpen(self):
        self.assertTrue(isinstance(self.spi, LIBUSBSIO.SPI))
        self.assertTrue(self.sio.IsAnyPortOpen())
        self.assertEqual(self.spi.Close(), 0)
        self.assertFalse(self.sio.IsAnyPortOpen())

    @use_spi(SPI_PORT)
    def test_SPI_Reset(self):
        self.assertEqual(self.spi.Reset(), 0)

    def siotest_spi_write(self, data:bytes):
        '''SPI write command to the siotest application at MCU side'''
        size = ">%04x" % len(data)
        tx = size.encode() + data + b'$'
        data, ret = self.spi.Transfer(SPI_SSEL[0], SPI_SSEL[1], tx)
        self.assertEqual(ret, len(tx), "SPI expected to write [%d]:'%s', but wrote %d" % (len(tx), tx, ret))
        return True, data if ret == len(tx) else False, None

    def siotest_spi_read(self):
        '''SPI read response from the siotest application at MCU side'''
        # read <YYYY length
        data, ret = self.spi.Transfer(SPI_SSEL[0], SPI_SSEL[1], None, 5)
        self.assertEqual(ret, 5, "SPI expected to read '<YYYY', but received [%d]:%s" % (ret, data))
        if(ret != 5):
            return False, None
        if(data[0:1] != b'<'):
            self.assertTrue(False, "Expected SOF byte '<' but received %s" % data[0:1])
            return False, None
        try:
            sz = int(data[1:], 16)
        except:
            self.assertTrue(False, "Expected SIZE could nor be parsed %s" % data[1:])
            return False, None

        # read the data and EOF($)
        data, ret = self.spi.Transfer(SPI_SSEL[0], SPI_SSEL[1], None, sz+1)
        self.assertEqual(ret, sz+1, "SPI expected to read data[%d]+EOF, but received [%d]:%s" % (sz, ret, data))
        if(ret == (sz+1)):
            if(data[-1:] != b'$'):
                self.assertTrue(False, "Response not terminated with EOF %s" % data)
                return False, None
        else:
            return False, None

        return True, data[:-1]

    def siotest_spi_echo(self, minlen, maxlen):
        pattern = SPI_ECHO_DATA * int(maxlen/(len(SPI_ECHO_DATA))+1)

        for sz in range(minlen, maxlen):
            ok = self.siotest_spi_write(b"echo " + pattern[:sz])
            self.assertTrue(ok, "Testing SPI transfer size = %d" % sz)

            if ok:
                # TODO: pause for target to prepare answer (not necessary, we're slow enough)
                ok, data = self.siotest_spi_read()
                self.assertTrue(ok, "Reading SPI echoed data")

                if(ok):
                    self.assertEqual(sz, len(data), "SPI test echo data length:%d matching expected:%d" % (len(data), sz))
                    self.assertEqual(data, pattern[:len(data)], "SPI test echo data content matching. received:%s" % data)

    @use_spi(SPI_PORT)
    def test_SPI_Transfer_Echo_single(self):
        self.siotest_spi_echo(1, 2)

    @use_spi(SPI_PORT)
    def test_SPI_Transfer_Echo_short(self):
        if(self.sio.GetMaxDataSize() > 200):
            self.siotest_spi_echo(64, 200)

    @use_spi(SPI_PORT)
    def test_SPI_Transfer_Echo_longer(self):
        if(self.sio.GetMaxDataSize() > 600):
            self.siotest_spi_echo(580, 600)

    @use_spi(SPI_PORT)
    def test_SPI_Transfer_Echo_long(self):
        if(self.sio.GetMaxDataSize() > 1000):
            self.siotest_spi_echo(999, self.sio.GetMaxDataSize() - 10)

    @unittest.skipUnless(RUN_SLOW_TESTS, "Takes long time")
    @use_spi(SPI_PORT)
    def test_SPI_Transfer_Echo_full(self):
        self.siotest_spi_echo(1, self.sio.GetMaxDataSize() - 10)
