# Part of Odoo. See LICENSE file for full copyright and licensing details.

from typing import NamedTuple
from contextlib import contextmanager
import logging
import serial
from threading import Lock
import time

from odoo.addons.iot_drivers.driver import Driver

_logger = logging.getLogger(__name__)


class SerialProtocol(NamedTuple):
    name: any
    baudrate: any
    bytesize: any
    stopbits: any
    parity: any
    timeout: any
    writeTimeout: any
    measureRegexp: any
    statusRegexp: any
    commandTerminator: any
    commandDelay: any
    measureDelay: any
    newMeasureDelay: any
    measureCommand: any
    emptyAnswerValid: any


@contextmanager
def serial_connection(path, protocol, is_probing=False):
    """Opens a serial connection to a device and closes it automatically after use.

    :param path: path to the device
    :type path: string
    :param protocol: an object containing the serial protocol to connect to a device
    :type protocol: namedtuple
    :param is_probing: a flag thet if set to `True` makes the timeouts longer, defaults to False
    :type is_probing: bool, optional
    """

    PROBING_TIMEOUT = 1
    port_config = {
        'baudrate': protocol.baudrate,
        'bytesize': protocol.bytesize,
        'stopbits': protocol.stopbits,
        'parity': protocol.parity,
        'timeout': PROBING_TIMEOUT if is_probing else protocol.timeout,               # longer timeouts for probing
        'writeTimeout': PROBING_TIMEOUT if is_probing else protocol.writeTimeout      # longer timeouts for probing
    }
    connection = serial.Serial(path, **port_config)
    yield connection
    connection.close()


class SerialDriver(Driver):
    """Abstract base class for serial drivers."""

    _protocol = None
    connection_type = 'serial'

    def __init__(self, identifier: str, device: dict):
        """ Attributes initialization method for `SerialDriver`.

        :param device: path to the device
        """

        super().__init__(identifier, device)
        self.device_connection = 'serial'
        self._connection = None
        self._device_lock = Lock()
        self._set_name()

    def _set_name(self):
        """Tries to build the device's name based on its type and
        protocol name but falls back on a default name if that doesn't work."""

        try:
            name = ('%s serial %s' % (self._protocol.name, self.device_type)).title()
        except Exception:  # noqa: BLE001
            name = 'Unknown Serial Device'
        self.device_name = name

    def _take_measure(self):
        """Reads the device's value, and pushes that value to the frontend."""
        pass

    def action(self, data):
        """Override to add lock to the serial connection during
        action execution.
        """
        if not self._connection or not self._connection.is_open:
            with serial_connection(self.device_identifier, self._protocol) as connection:
                self._connection = connection

        with self._device_lock:
            res = super().action(data)
            time.sleep(self._protocol.commandDelay)
            return res

    def run(self):
        """Continuously gets new measures from the device."""

        try:
            with serial_connection(self.device_identifier, self._protocol) as connection:
                self._connection = connection
                while not self._stopped.is_set():
                    self._take_measure()
                    time.sleep(self._protocol.newMeasureDelay)
        except Exception:  # noqa: BLE001
            _logger.exception('Error while reading %s', self.device_name)
            self.data["status"] = "error"
