#!/usr/bin/env python

# Copyright (c) 2015-2016 Freescale Semiconductor, Inc.
# Copyright 2016-2023 NXP
# Copyright 2022 ACRIOS Systems s.r.o.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

import struct
import socket
import threading
from socket import SHUT_RDWR

from .crc16 import Crc16
from .client import RequestError
import time

try:
    import serial
    SerialReady = True
except ImportError:
    SerialReady = False

try:
    from rpmsg.sysfs import RpmsgEndpoint
    RpmsgEndpointReady = True
except ImportError:
    RpmsgEndpointReady = False

try:
    from libusbsio import *
    LIBUSBSIOReady = True
except ImportError:
    LIBUSBSIOReady = False


class Transport(object):
    """ Base transport class.
    """

    def __init__(self):
        pass

    def send(self, message):
        raise NotImplementedError()

    def receive(self):
        raise NotImplementedError()


class FramedTransport(Transport):
    HEADER_LEN = 6

    def __init__(self):
        super(FramedTransport, self).__init__()
        self._sendLock = threading.Lock()
        self._receiveLock = threading.Lock()
        self._Crc16 = Crc16()

    @property
    def crc_16(self):
        return self._Crc16

    @crc_16.setter
    def crc_16(self, crcStart):
        if type(crcStart) is not int:
            raise RequestError("invalid CRC, not a number")
        self._Crc16 = Crc16(crcStart)

    def send(self, message):
        try:
            self._sendLock.acquire()

            crcBody = self._Crc16.computeCRC16(message)
            messageLength = len(message)
            crcHeader = self._Crc16.computeCRC16(bytearray(struct.pack('<H', messageLength))) + self._Crc16.computeCRC16(bytearray(struct.pack('<H', crcBody)))
            crcHeader &= 0xFFFF # 2bytes

            header = bytearray(struct.pack('<HHH', crcHeader, messageLength, crcBody))
            assert len(header) == self.HEADER_LEN
            self._base_send(header + message)
        finally:
            self._sendLock.release()

    def receive(self):
        try:
            self._receiveLock.acquire()

            # Read fixed size header containing the message length.
            headerData = self._base_receive(self.HEADER_LEN)
            crcHeader, messageLength, crcBody = struct.unpack('<HHH', headerData)

            computedCrc = self._Crc16.computeCRC16(bytearray(struct.pack('<H', messageLength))) + self._Crc16.computeCRC16(bytearray(struct.pack('<H', crcBody)))
            computedCrc &= 0xFFFF # 2bytes
            if computedCrc != crcHeader:
                raise RequestError("invalid header CRC")

            # Now we know the length, read the rest of the message.
            data = self._base_receive(messageLength)
            computedCrc = self._Crc16.computeCRC16(data)
            if computedCrc != crcBody:
                raise RequestError("invalid message CRC")

            return data
        finally:
            self._receiveLock.release()

    def _base_send(self, data):
        raise NotImplementedError()

    def _base_receive(self):
        raise NotImplementedError()


class SerialTransport(FramedTransport):
    def __init__(self, url, baudrate, **kwargs):
        super(SerialTransport, self).__init__()
        if not SerialReady:
            raise ImportError(
                "Please, install pySerial module (sudo pip3 install pyserial).")
        self._url = url
        self._serial = serial.serial_for_url(
            url, baudrate=baudrate, **kwargs)  # 8N1 by default

    def close(self):
        self._serial.close()

    def _base_send(self, data):
        self._serial.write(data)

    def _base_receive(self, count):
        return self._serial.read(count)


class ConnectionClosed(Exception):
    pass


class TCPTransport(FramedTransport):
    def __init__(self, host, port, isServer):
        super(TCPTransport, self).__init__()
        self._host = host
        self._port = port
        self._isServer = isServer
        self._sock = None
        self._socket_lock = threading.Lock()

        if self._isServer:
            self._serverThread = threading.Thread(target=self._serve)
            self._serverThread.daemon = True
            self._serverThread.start()
            self._serverSockEventStart = threading.Event()
        else:
            self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self._sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
            self._sock.connect((self._host, self._port))

    def _serve(self):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.setblocking(1)
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
        s.bind((self._host, self._port))
        s.listen(5)

        while True:
            self._sock, _ = s.accept()
            self._serverSockEventStart.set()

    def close(self):
        if self._isServer:
            self._serverSockEventStart.clear()

        with self._socket_lock:
            if self._sock is not None:
                try:
                    self._sock.shutdown(SHUT_RDWR)
                    self._sock.close()
                except OSError:
                    # May be raised by the OS if the socket was closed externally,
                    # thus invalidating the file descriptor.
                    pass
                self._sock = None

    def _base_send(self, message):
        if self._isServer:
            self._serverSockEventStart.wait()
        if self._sock:
            self._sock.sendall(message)

    def _base_receive(self, count):
        if self._isServer:
            self._serverSockEventStart.wait()
        if self._sock:
            remaining = count
            result = bytearray()
            while remaining:
                data = self._sock.recv(remaining)
                if len(data) == 0:
                    self.close()
                    raise ConnectionClosed()
                result += data
                remaining -= len(data)
            return result


class RpmsgTransport(Transport):
    def __init__(self, ept_addr_local=None, ept_addr_remote=None, channel_name=None):
        if not RpmsgEndpointReady:
            raise ImportError(
                "Please, install RPMsg from: https://github.com/EmbeddedRPC/erpc-imx-demos/tree/master/middleware/rpmsg-python")

        if ept_addr_local is None:
            ept_addr_local = RpmsgEndpoint.LOCAL_DEFAULT_ADDRESS
        if ept_addr_remote is None:
            ept_addr_remote = RpmsgEndpoint.REMOTE_DEFAULT_ADDRESS
        if channel_name is None:
            channel_name = RpmsgEndpoint.rpmsg_openamp_channel

        self.ept_addr_remote = ept_addr_remote
        self.ept = RpmsgEndpoint(
            channel_name,
            ept_addr_local,
            RpmsgEndpoint.Types.DATAGRAM)

    def send(self, message):
        self.ept.send(message, self.ept_addr_remote)

    def receive(self):
        while True:
            ret = self.ept.recv(-1)
            if len(ret[1]) != 0:
                return ret[1]
            else:
                time.sleep(0.001)


class LIBUSBSIOSPITransport(FramedTransport):
    def __init__(self, baudrate=None, cs_gpio_port=None, cs_gpio_pin=None, devidx=None):
        super(LIBUSBSIOSPITransport, self).__init__()

        if not LIBUSBSIOReady:
            raise ImportError("Please, install LIBUSBSIO module")

        if baudrate is None:
            baudrate = 500000

        if devidx is None:
            devidx = 0

        self._baudrate = baudrate
        self._cs_gpio_port = cs_gpio_port
        self._cs_gpio_pin = cs_gpio_pin
        self._devidx = devidx
        self._gpioport = 0
        self._gpiopin = 0
        self._gpiomode = 0

        # Load DLL from default directory without any debugging prints
        self.sio = LIBUSBSIO()

        # Get number of LIBUSBSIO devices
        res = self.sio.GetNumPorts(vidpids=[LIBUSBSIO.VIDPID_LPCLINK2])
        if res != 0:
            self._gpioport = 1
            self._gpiopin = 2
            self._gpiomode = 1
        else:
            res = self.sio.GetNumPorts(vidpids=[LIBUSBSIO.VIDPID_MCULINK])
            if res != 0:
                self._gpioport = 0
                self._gpiopin = 4
                self._gpiomode = 0x100
            else:
                print('No LIBUSBSIO devices found \r\n')
                return
        print('Total LIBUSBSIO devices: %d \r\n' % res)

        # Open device at given index
        self._hSIOPort = self.sio.Open(int(self._devidx))

        # Get the device version
        s = self.sio.GetVersion()
        str1 = ""
        print('Device version: %s \r\n ' % str1.join(str(s)))

        # Get number of available SPI ports
        num_spi_ports = self.sio.GetNumSPIPorts()
        print('Number of SPI ports available: %d \r\n' % num_spi_ports)

        # Get max number of bytes supported for I2C/SPI transfers
        max_num_bytes = self.sio.GetMaxDataSize()
        print('Max number of bytes supported for I2C/SPI transfers: %d \r\n' %
              max_num_bytes)

        # Call SPI_Open and store the _hSPIPort handler
        self._hSPIPort = self.sio.SPI_Open(
            int(self._baudrate), portNum=0, dataSize=8, preDelay=0)

        # Configure GPIO pin for SPI master-slave signalling
        res = self.sio.GPIO_ConfigIOPin(
            self._gpioport, self._gpiopin, self._gpiomode)
        print('GPIO_ConfigIOPin res: %d \r\n' % res)
        res = self.sio.GPIO_SetPortInDir(self._gpioport, self._gpiopin)
        print('GPIO_SetPortInDir res: %d \r\n' % res)
        res = self.sio.GPIO_GetPin(self._gpioport, self._gpiopin)
        print('GPIO_GetPin res: %d \r\n' % res)

    def close(self):
        res = self._hSPIPort.Close()
        self._hSPIPort = None
        res = self.sio.Close()
        self._hSIOPort = None

    def _base_send(self, message):
        # Wait for SPI master-slave signalling GPIO pin to be in low state
        res = self.sio.GPIO_GetPin(self._gpioport, self._gpiopin)
        while (1 == res):
            res = self.sio.GPIO_GetPin(self._gpioport, self._gpiopin)
        # Send the header first
        data, rxbytesnumber = self._hSPIPort.Transfer(
            0, 15, message[:self.HEADER_LEN], self.HEADER_LEN, 0)
        if rxbytesnumber > 0:
            #print('SPI received %d number of bytes' % rxbytesnumber)
            # Send the payload/data
            data, rxbytesnumber = self._hSPIPort.Transfer(
                0, 15, bytes(message[self.HEADER_LEN:]), len(message) - self.HEADER_LEN, 0)
        else:
            print('SPI transfer error: %d' % rxbytesnumber)

    def _base_receive(self, count):
        # Wait for SPI master-slave signalling GPIO pin to be in low state
        res = self.sio.GPIO_GetPin(self._gpioport, self._gpiopin)
        while (1 == res):
            res = self.sio.GPIO_GetPin(self._gpioport, self._gpiopin)
        data, rxbytesnumber = self._hSPIPort.Transfer(
            0, 15, range(count), count, 0)
        if rxbytesnumber > 0:
            #print('SPI received %d number of bytes' % rxbytesnumber)
            return bytes(data[:count])
        else:
            print('SPI transfer error: %d' % rxbytesnumber)
            res = self._hSPIPort.Reset()
            return b"\00" * count


class LIBUSBSIOI2CTransport(FramedTransport):
    def __init__(self, baudrate=None, devidx=None):
        super(LIBUSBSIOI2CTransport, self).__init__()

        if not LIBUSBSIOReady:
            raise ImportError("Please, install LIBUSBSIO module")

        if baudrate is None:
            baudrate = 100000

        if devidx is None:
            devidx = 0

        self._baudrate = baudrate
        self._devidx = devidx
        self._gpioport = 0
        self._gpiopin = 0
        self._gpiomode = 0

        # Load DLL from default directory without any debugging prints
        self.sio = LIBUSBSIO()

        # Get number of LIBUSBSIO devices
        res = self.sio.GetNumPorts(vidpids=[LIBUSBSIO.VIDPID_LPCLINK2])
        if res != 0:
            self._gpioport = 1
            self._gpiopin = 2
            self._gpiomode = 1
        else:
            res = self.sio.GetNumPorts(vidpids=[LIBUSBSIO.VIDPID_MCULINK])
            if res != 0:
                self._gpioport = 1
                self._gpiopin = 3
                self._gpiomode = 0x100
            else:
                print('No LIBUSBSIO devices found \r\n')
                return
        print('Total LIBUSBSIO devices: %d \r\n' % res)

        # Open device at given index
        self._hSIOPort = self.sio.Open(int(self._devidx))

        # Get the device version
        s = self.sio.GetVersion()
        str1 = ""
        print('Device version: %s \r\n ' % str1.join(str(s)))

        # Get number of available I2C ports
        num_spi_ports = self.sio.GetNumI2CPorts()
        print('Number of I2C ports available: %d \r\n' % num_spi_ports)

        # Get max number of bytes supported for SPI/I2C transfers
        max_num_bytes = self.sio.GetMaxDataSize()
        print('Max number of bytes supported for SPI/I2C transfers: %d \r\n' %
              max_num_bytes)

        # Call I2C_Open and store the _hI2CPort handler
        self._hI2CPort = self.sio.I2C_Open(int(self._baudrate), 0, 0)

        # Configure GPIO pin for I2C master-slave signalling
        res = self.sio.GPIO_ConfigIOPin(
            self._gpioport, self._gpiopin, self._gpiomode)
        print('GPIO_ConfigIOPin res: %d \r\n' % res)
        res = self.sio.GPIO_SetPortInDir(self._gpioport, self._gpiopin)
        print('GPIO_SetPortInDir res: %d \r\n' % res)
        res = self.sio.GPIO_GetPin(self._gpioport, self._gpiopin)
        print('GPIO_GetPin res: %d \r\n' % res)

    def close(self):
        res = self._hI2CPort.Close()
        self._hI2CPort = None
        res = self.sio.Close()
        self._hSIOPort = None

    def _base_send(self, message):
        # Wait for I2C master-slave signalling GPIO pin to be in low state
        res = self.sio.GPIO_GetPin(self._gpioport, self._gpiopin)
        while (1 == res):
            res = self.sio.GPIO_GetPin(self._gpioport, self._gpiopin)
        # Send the header first
        data, rxbytesnumber = self._hI2CPort.FastXfer(
            0x7E, message[:self.HEADER_LEN], self.HEADER_LEN, 0, False, True)
        if rxbytesnumber > 0:
            #print('I2C received %d number of bytes' % rxbytesnumber)
            # Send the payload/data
            data, rxbytesnumber = self._hI2CPort.FastXfer(
                0x7E, bytes(message[self.HEADER_LEN:]), len(message) - self.HEADER_LEN, 0, False, True)
        else:
            print('I2C transfer error: %d' % rxbytesnumber)

    def _base_receive(self, count):
        # Wait for I2C master-slave signalling GPIO pin to be in low state
        res = self.sio.GPIO_GetPin(self._gpioport, self._gpiopin)
        while (1 == res):
            res = self.sio.GPIO_GetPin(self._gpioport, self._gpiopin)
        # Issue the I2C_Transfer API
        data, rxbytesnumber = self._hI2CPort.FastXfer(0x7E, 0, 0, count, False, True)
        if rxbytesnumber > 0:
            #print('I2C received %d number of bytes' % rxbytesnumber)
            return bytes(data[:count])
        else:
            #print('I2C transfer error: %d' % rxbytesnumber)
            res = self._hI2CPort.Reset()
            return b"\00" * count
