# -*- coding: utf-8 -*-
# Copyright (C) 2022 Paul Grebeniuk <paul@coolautomation.com>

# This library is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation; either version 2.1 of the License, or (at your
# option) any later version.

# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
# for more details.

# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

"""
    sub20.Device
    =====================
    Device class implementation of :mod:`sub20`.
    .. moduleauthor::  Paul Grebeniuk  <paul@coolautomation.com>
"""

import sys
from ctypes import create_string_buffer, c_int

from sub20.ctypeslib.libsub import SIGNATURES, sub_version
from sub20.ctypeslib.utils import load_ctypes_library
from sub20._errors import SubDeviceError, SubNotFoundError, SubNotOpenedError

# Enum

""" RS232/RS485"""
RS_RX_ENABLE = 0x80
RS_TX_ENABLE = 0x40

# Character Size
RS_CHAR_5 = 0x00
RS_CHAR_6 = 0x02
RS_CHAR_7 = 0x04
RS_CHAR_8 = 0x06
RS_CHAR_9 = 0x07

# Parity
RS_PARITY_NONE = 0x00
RS_PARITY_EVEN = 0x20
RS_PARITY_ODD = 0x30

# Stop Bits
RS_STOP_1 = 0x00
RS_STOP_2 = 0x08

# Timing Flags
RS_RX_BEFORE_TX = 0x01
RS_RX_AFTER_TX = 0x02

FIFO_SELECT_UART = 0x02
FIFO_CLEAR = 0x100
FIFO_READ_FULL = 0x200

I2C_GCE = 0x01
I2C_DISABLE = 0x02

MAX_BUF_SZ = 64
MAX_FREQ = 444444


class SUBDevice(object):

    def __init__(self):
        libname = 'sub20.dll' if sys.platform == "win32" \
            else 'libsub.so'

        self._libsub = load_ctypes_library(libname, SIGNATURES)
        self.sub_errno = c_int.in_dll(self._libsub, "sub_errno")
        self.sub_i2c_status = c_int.in_dll(self._libsub, "sub_i2c_status")
        self._device = None

    def open(self):
        self._device = self._libsub.sub_open(None)
        if not self._device:
            raise SubNotFoundError()

    def sub_get_serial_number(self):
        """
        Get serial number of SUB-20 device
        :returns: The serial number string
        :rtype: str
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """

        if not self._device:
            raise SubNotOpenedError()
        rx_buf_sz = MAX_BUF_SZ
        rx_buf = create_string_buffer(rx_buf_sz)
        if self._libsub.sub_get_serial_number(self._device, rx_buf, rx_buf_sz)<0:
            raise SubDeviceError(self.exc_str())
        return rx_buf.value.decode('UTF-8')

    def sub_get_product_id(self):
        """
       Get product ID of SUB-20 device
       :returns: The product ID string
       :rtype: str
       :raises SubNotOpenedError:  if the device is not opened
       :raises SubDeviceError: if the wrapped function returned the error code

       """
        if not self._device:
            raise SubNotOpenedError()
        rx_buf_sz = MAX_BUF_SZ
        rx_buf = create_string_buffer(rx_buf_sz)
        if self._libsub.sub_get_product_id(self._device, rx_buf, rx_buf_sz)<0:
            raise SubDeviceError(self.exc_str())
        return rx_buf.value.decode('UTF-8')

    def sub_get_version(self):
        """
      Get version of the sub20 library, driver and SUB-20 firmware
      :returns: The pointer to the sub20 inner version structure
      :rtype: c_void_p
      :raises SubNotOpenedError:  if the device is not opened

      """
        if not self._device:
            raise SubNotOpenedError()
        return sub_version.from_address(self._libsub.sub_get_version(self._device))

    def sub_get_version_dict(self):
        """
        Get the version in the python dictionary
        :returns: The pointer to the sub20 inner version structure
        :rtype: dict

        """
        version = self.sub_get_version()
        return {"dll": f'{version.dll.major}.{version.dll.minor}.'
                       f'{version.dll.micro}.{version.dll.nano}',
                "driver": f'{version.driver.major}.{version.driver.minor}.'
                          f'{version.driver.micro}.{version.driver.nano}',
                "sub_device": f'{version.sub_device.major}.{version.sub_device.minor}.'
                              f'{version.sub_device.micro}.{version.sub_device.nano}'
                }

    def sub_reset(self):
        if not self._device:
            raise SubNotOpenedError()
        if self._libsub.sub_reset(self._device):
            raise SubDeviceError(self.exc_str())

    def sub_eep_read(self, addr, sz):
        """
        Read sz bytes from internal EEPROM starting from the given address
        :param addr: The start address
        :type addr: int
        :param sz: Read size in bytes
        :type sz: int
        :returns: buffer
        :rtype: bytes
        :raises SubNotOpenedError: if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()

        rx_buf = create_string_buffer(sz)
        if self._libsub.sub_eep_read(self._device, addr, rx_buf, sz):
            raise SubDeviceError(self.exc_str())

        return rx_buf

    def sub_eep_write(self, addr, data):
        """
        Write data to internal EEPROM starting from the given address. If more than 64 data bytes
        are transmitted to the EEPROM, the data address will “roll over” and previous data will be overwritten. The
        address “roll over” during write is from the last byte of the current page to the first byte of the same page.

        :param addr: The start address
        :type addr: int
        :param data:
        :type data: bytes
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        tx_buf_sz = len(data)
        tx_buf = create_string_buffer(data)
        if self._libsub.sub_eep_write(self._device, addr, tx_buf, tx_buf_sz):
            raise SubDeviceError(self.exc_str())

    def sub_i2c_freq(self, freq=0):
        """
        Set and get SUB-20 I2C master clock frequency.
        :param freq: a frequency. if freq == 0 it's a get function otherwise it's a set function
        :type freq: int
        :returns: a current frequency if freq == 0 otherwise it just returns freq value
        :rtype: int
        :raises SubNotOpenedError:  if the device is not opened
        :raises ValueError:  freq > 444444
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()

        if freq > MAX_FREQ:
            raise ValueError("Maximal frequency exceeded!")

        _freq = c_int(freq)
        if self._libsub.sub_i2c_freq(self._device, _freq):
            raise SubDeviceError(self.exc_str())

        return _freq.value

    def sub_i2c_config(self, sa, flags):
        """
        Configure SUB-20 I2C module.

        :param sa: slave address for SUB-20 in I2C slave mode
        :type sa: int
        :param flags: I2C_GCE | I2C_DISABLE
        :type flags: int
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """

        if not self._device:
            raise SubNotOpenedError()

        if self._libsub.sub_i2c_freq(self._device, sa, flags):
            raise SubDeviceError(self.exc_str())

    def sub_i2c_start(self):
        if not self._device:
            raise SubNotOpenedError()

        if self._libsub.sub_i2c_start(self._device):
            raise SubDeviceError(self.exc_str())

    def sub_i2c_stop(self):
        if not self._device:
            raise SubNotOpenedError()

        if self._libsub.sub_i2c_stop(self._device):
            raise SubDeviceError(self.exc_str())

    def sub_i2c_scan(self):
        """
        Scan I2C bus looking for connected slave devices.

        :returns: List of available i2c addresses
        :rtype: list
        :raises SubNotOpenedError:  if the device is not opened

        """
        if not self._device:
            raise SubNotOpenedError()
        addresses = []
        counter = c_int(1)

        """ Create buffer for slave addresses - 7 bit number representing I2C slave device """
        rx_buf = create_string_buffer(128)
        rc = self._libsub.sub_i2c_scan(self._device, counter, rx_buf)
        if not rc:
            for ii in range(0, counter.value):
                addresses.append(ord(rx_buf[ii]))
        return addresses

    def sub_i2c_read(self, sa, ma, ma_sz, rx_sz):
        """
        I2C master read transaction.

        :param sa: slave address for SUB-20 in I2C slave mode
        :type sa: int
        :param ma: memory address
        :type ma: int
        :param ma_sz: memory address size bytes
        :type ma: int
        :param rx_sz: read data size bytes
        :type rx_sz: int
        :returns: rx buffer
        :rtype: Array[c_char]
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()

        rx_buf = create_string_buffer(rx_sz)
        _rc = self._libsub.sub_i2c_read(self._device, sa, ma, ma_sz, rx_buf, rx_sz)
        if _rc:
            raise SubDeviceError(self.exc_str())
        return rx_buf

    def sub_i2c_write(self, sa, ma, ma_sz, data):
        """
        I2C master write transaction.

        :param sa: slave address for SUB-20 in I2C slave mode
        :type sa: int
        :param ma: memory address
        :type ma: int
        :param ma_sz: memory address size
        :type ma_sz: int
        :param data: buffer for data to be written
        :type data: bytes
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        tx_buf_sz = len(data)
        tx_buf = create_string_buffer(data)
        if self._libsub.sub_i2c_write(self._device, sa, ma, ma_sz, tx_buf, tx_buf_sz):
            raise SubDeviceError(self.exc_str())

    def sub_i2c_transfer(self, sa, data, rx_buf_sz):
        """
        I2C master write transaction followed by master read transaction.

        :param sa: slave address for SUB-20 in I2C slave mode
        :type sa: int
        :param data: memory address
        :type data: bytes
        :param rx_buf_sz: rx buffer size in bytes
        :type rx_buf_sz: int
        :returns: rx buffer
        :rtype: Array[c_char]
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        rx_buf = create_string_buffer(rx_buf_sz)
        tx_buf_sz = len(data)
        tx_buf = create_string_buffer(data)

        if self._libsub.sub_i2c_transfer(self._device, sa, tx_buf, tx_buf_sz, rx_buf, rx_buf_sz):
            raise SubDeviceError(self.exc_str())

        return rx_buf

    def sub_gpio_config(self, set_par, mask):
        """
        Configure GPIO state (direction) as input or output.
        :param set_par: Bits 0-31 of set parameter correspond to GPIO0-GPIO31 configuration bits. If GPIOn configuration bit is "1" then GPIOn direction is output, otherwise it is input.
        :param mask: Bit in set parameter will take effect only if corresponding mask bit is "1". With mask=0 function will only read current GPIO configuration.
        :returns: current GPIO configuration state
        :rtype: int32
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        get_par = c_int(1)
        _rc = self._libsub.sub_gpio_config(self._device, set_par, get_par, mask)
        if _rc:
            raise SubDeviceError(self.exc_str())
        return get_par.value

    def sub_gpio_read(self):
        """
        Read GPIO input status
        :returns: received GPIO input status
        :rtype: int32
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        get_par = c_int(1)
        _rc = self._libsub.sub_gpio_read(self._device, get_par)
        if _rc:
            raise SubDeviceError(self.exc_str())
        return get_par.value

    def sub_gpio_write(self, set_par, mask):
        """
        Set GPIO output status

        :param set_par: Bits 0-31 of set parameter correspond to GPIO0-GPIO31 output statuses.
        :param mask: Bit in set parameter will take effect only if corresponding mask bit is "1". With mask=0 function will only read current GPIO output status.
        :returns: received GPIO output status.
        :rtype: int32
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        get_par = c_int(1)
        _rc = self._libsub.sub_gpio_write(self._device, set_par, get_par, mask)
        if _rc:
            raise SubDeviceError(self.exc_str())
        return get_par.value

    def sub_gpiob_config(self, set_par, mask):

        """
        Configure GPIOB state (direction) as input or output.

        :param set_par: Bits 0-31 of set parameter correspond to GPIO0-GPIO31 configuration bits. If GPIOn configuration bit is "1" then GPIOn direction is output, otherwise it is input.
        :param mask: Bit in set parameter will take effect only if corresponding mask bit is "1". With mask=0 function will only read current GPIO configuration.
        :returns: current GPIO configuration state
        :rtype: int32
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        get_par = c_int(1)
        _rc = self._libsub.sub_gpiob_config(self._device, set_par, get_par, mask)
        if _rc:
            raise SubDeviceError(self.exc_str())
        return get_par.value

    def sub_gpiob_read(self):
        """
        Read GPIO input status

        :returns: received GPIO input status
        :rtype: int32
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        get_par = c_int(1)
        _rc = self._libsub.sub_gpiob_read(self._device, get_par)
        if _rc:
            raise SubDeviceError(self.exc_str())
        return get_par.value

    def sub_gpiob_write(self, set_par, mask):
        """
        Set GPIO output status

        :param set_par: Bits 0-31 of set parameter correspond to GPIO0-GPIO31 output statuses.
        :param mask: Bit in set parameter will take effect only if corresponding mask bit is "1". With mask=0 function will only read current GPIO output status.
        :returns: received GPIO output status.
        :rtype: int32
        :raises SubNotOpenedError: if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        get_par = c_int(1)
        _rc = self._libsub.sub_gpiob_write(self._device, set_par, get_par, mask)
        if _rc:
            raise SubDeviceError(self.exc_str())
        return get_par.value

    def sub_rs_set_config(self, config, baud):
        """
        Configure SUB-20 UART.

        :param config: UART configuration
        :param baud: Desired baud rate. Maximum baud rate is 2Mbps.
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        _rc = self._libsub.sub_rs_set_config(self._device, config, baud)
        if _rc:
            raise SubDeviceError(self.exc_str())

    def sub_rs_get_config(self):
        """
        Read current SUB-20 UART configuration.

        :returns: current UART configuration, current UART baud rate
        :rtype: int, int
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        config = c_int(8)
        baud = c_int(8)
        _rc = self._libsub.sub_rs_get_config(self._device, config, baud)
        if _rc:
            raise SubDeviceError(self.exc_str())
        return config.value, baud.value

    def sub_rs_timing(self, flags, tx_space_us, rx_msg_us, rx_byte_us):
        """
        Configure UART transfer timing and order of transmit and receive operations.

        :param flags: handling flags - RS_RX_BEFORE_TX | RS_RX_AFTER_TX | 0
        :param tx_space_us: delay in µs between subsequent byte transmit
        :param rx_msg_us: message reception timeout in µs
        :param rx_byte_us: byte to byte reception timeout in µs
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        _rc = self._libsub.sub_rs_timing(self._device, flags, tx_space_us, rx_msg_us, rx_byte_us)
        if _rc:
            raise SubDeviceError(self.exc_str())

    def sub_rs_xfer(self, data, rx_sz=MAX_BUF_SZ):
        """
        Transmit and/or receive message(s) via UART
        :param data: buffer with data to be transmitted
        :param rx_sz: maximal number of bytes to receive (can be 0 if reception is not required)
        :returns: Actually recieved bytes
        :rtype: bytes
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code
        """
        if not self._device:
            raise SubNotOpenedError()
        rx_buf = create_string_buffer(rx_sz)
        _rc = self._libsub.sub_rs_xfer(self._device, data, len(data), rx_buf, rx_sz)
        if _rc == -1:
            raise SubDeviceError(self.exc_str())
        return rx_buf[0:_rc]

    def sub_fifo_config(self, config):
        """
        FIFO configuration setup
        :param config: FIFO configuration flags
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        _rc = self._libsub.sub_fifo_config(self._device, config)
        if _rc:
            raise SubDeviceError(self.exc_str())

    def sub_fifo_read(self, sz, to_ms):
        """
        Read from IN FIFO into buffer
        :param sz: read buffer size. You should not exceed 64 b
        :param to_ms: read timeout
        :returns: Actually received bytes
        :rtype: bytes
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        rx_buf = create_string_buffer(sz)
        _rc = self._libsub.sub_fifo_read(self._device, rx_buf, sz, to_ms)
        if _rc < 0:
            raise SubDeviceError(self.exc_str())
        return rx_buf[0:_rc]

    def sub_fifo_write(self, data_buf, sz, to_ms):
        """
        Transfer sz bytes from buffer into OUT FIFO

        :param data_buf: source buffer
        :param sz: buffer size
        :param to_ms: timeout in milliseconds
        :returns: written bytes
        :rtype: int
        :raises SubNotOpenedError:  if the device is not opened
        :raises SubDeviceError: if the wrapped function returned the error code

        """
        if not self._device:
            raise SubNotOpenedError()
        _rc = self._libsub.sub_fifo_write(self._device, data_buf, sz, to_ms)
        if _rc < 0:
            raise SubDeviceError(self.exc_str())
        return _rc

    def exc_str(self):
        return f"{self.strerror(self.sub_errno.value)}[{self.sub_errno.value}]"

    def strerror(self, errno):
        return self._libsub.sub_strerror(errno).decode('UTF-8')

    def close(self):
        if self._device:
            self._libsub.sub_close(self._device)
