"""Support for controlling raspihats boards."""
import logging
import threading
import time

from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType

_LOGGER = logging.getLogger(__name__)

DOMAIN = "raspihats"

CONF_I2C_HATS = "i2c_hats"
CONF_BOARD = "board"
CONF_CHANNELS = "channels"
CONF_INDEX = "index"
CONF_INVERT_LOGIC = "invert_logic"
CONF_INITIAL_STATE = "initial_state"

I2C_HAT_NAMES = [
    "Di16",
    "Rly10",
    "Di6Rly6",
    "DI16ac",
    "DQ10rly",
    "DQ16oc",
    "DI6acDQ6rly",
]

I2C_HATS_MANAGER = "I2CH_MNG"


def setup(hass: HomeAssistant, config: ConfigType) -> bool:
    """Set up the raspihats component."""
    _LOGGER.warning(
        "The Raspihats pHAT integration is deprecated and will be removed "
        "in Home Assistant Core 2022.4; this integration is removed under "
        "Architectural Decision Record 0019, more information can be found here: "
        "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md"
    )

    hass.data[DOMAIN][I2C_HATS_MANAGER] = I2CHatsManager()

    def start_i2c_hats_keep_alive(event):
        """Start I2C-HATs keep alive."""
        hass.data[DOMAIN][I2C_HATS_MANAGER].start_keep_alive()

    def stop_i2c_hats_keep_alive(event):
        """Stop I2C-HATs keep alive."""
        hass.data[DOMAIN][I2C_HATS_MANAGER].stop_keep_alive()

    hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_i2c_hats_keep_alive)
    hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_i2c_hats_keep_alive)
    return True


def log_message(source, *parts):
    """Build log message."""
    message = source.__class__.__name__
    for part in parts:
        message += f": {part!s}"
    return message


class I2CHatsException(Exception):
    """I2C-HATs exception."""


class I2CHatsDIScanner:
    """Scan Digital Inputs and fire callbacks."""

    _DIGITAL_INPUTS = "di"
    _OLD_VALUE = "old_value"
    _CALLBACKS = "callbacks"

    def setup(self, i2c_hat):
        """Set up the I2C-HAT instance for digital inputs scanner."""
        if hasattr(i2c_hat, self._DIGITAL_INPUTS):
            digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS)
            old_value = None
            # Add old value attribute
            setattr(digital_inputs, self._OLD_VALUE, old_value)
            # Add callbacks dict attribute {channel: callback}
            setattr(digital_inputs, self._CALLBACKS, {})

    def register_callback(self, i2c_hat, channel, callback):
        """Register edge callback."""
        if hasattr(i2c_hat, self._DIGITAL_INPUTS):
            digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS)
            callbacks = getattr(digital_inputs, self._CALLBACKS)
            callbacks[channel] = callback
            setattr(digital_inputs, self._CALLBACKS, callbacks)

    def scan(self, i2c_hat):
        """Scan I2C-HATs digital inputs and fire callbacks."""
        if hasattr(i2c_hat, self._DIGITAL_INPUTS):
            digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS)
            callbacks = getattr(digital_inputs, self._CALLBACKS)
            old_value = getattr(digital_inputs, self._OLD_VALUE)
            value = digital_inputs.value  # i2c data transfer
            if old_value is not None and value != old_value:
                for channel in range(0, len(digital_inputs.channels)):
                    state = (value >> channel) & 0x01
                    old_state = (old_value >> channel) & 0x01
                    if state != old_state:
                        callback = callbacks.get(channel)
                        if callback is not None:
                            callback(state)
            setattr(digital_inputs, self._OLD_VALUE, value)


class I2CHatsManager(threading.Thread):
    """Manages all I2C-HATs instances."""

    _EXCEPTION = "exception"
    _CALLBACKS = "callbacks"

    def __init__(self):
        """Init I2C-HATs Manager."""
        threading.Thread.__init__(self)
        self._lock = threading.Lock()
        self._i2c_hats = {}
        self._run = False
        self._di_scanner = I2CHatsDIScanner()

    def register_board(self, board, address):
        """Register I2C-HAT."""
        with self._lock:
            if (i2c_hat := self._i2c_hats.get(address)) is None:
                # This is a Pi module and can't be installed in CI without
                # breaking the build.
                # pylint: disable=import-outside-toplevel,import-error
                import raspihats.i2c_hats as module

                constructor = getattr(module, board)
                i2c_hat = constructor(address)
                setattr(i2c_hat, self._CALLBACKS, {})

                # Setting exception attribute will trigger online callbacks
                # when keep alive thread starts.
                setattr(i2c_hat, self._EXCEPTION, None)

                self._di_scanner.setup(i2c_hat)
                self._i2c_hats[address] = i2c_hat
                status_word = i2c_hat.status  # read status_word to reset bits
                _LOGGER.info(log_message(self, i2c_hat, "registered", status_word))

    def run(self):
        """Keep alive for I2C-HATs."""
        # This is a Pi module and can't be installed in CI without
        # breaking the build.
        # pylint: disable=import-outside-toplevel,import-error
        from raspihats.i2c_hats import ResponseException

        _LOGGER.info(log_message(self, "starting"))

        while self._run:
            with self._lock:
                for i2c_hat in list(self._i2c_hats.values()):
                    try:
                        self._di_scanner.scan(i2c_hat)
                        self._read_status(i2c_hat)

                        if hasattr(i2c_hat, self._EXCEPTION):
                            if getattr(i2c_hat, self._EXCEPTION) is not None:
                                _LOGGER.warning(
                                    log_message(self, i2c_hat, "online again")
                                )
                            delattr(i2c_hat, self._EXCEPTION)
                            # trigger online callbacks
                            callbacks = getattr(i2c_hat, self._CALLBACKS)
                            for callback in list(callbacks.values()):
                                callback()
                    except ResponseException as ex:
                        if not hasattr(i2c_hat, self._EXCEPTION):
                            _LOGGER.error(log_message(self, i2c_hat, ex))
                        setattr(i2c_hat, self._EXCEPTION, ex)
            time.sleep(0.05)
        _LOGGER.info(log_message(self, "exiting"))

    def _read_status(self, i2c_hat):
        """Read I2C-HATs status."""
        status_word = i2c_hat.status
        if status_word.value != 0x00:
            _LOGGER.error(log_message(self, i2c_hat, status_word))

    def start_keep_alive(self):
        """Start keep alive mechanism."""
        self._run = True
        threading.Thread.start(self)

    def stop_keep_alive(self):
        """Stop keep alive mechanism."""
        self._run = False
        self.join()

    def register_di_callback(self, address, channel, callback):
        """Register I2C-HAT digital input edge callback."""
        with self._lock:
            i2c_hat = self._i2c_hats[address]
            self._di_scanner.register_callback(i2c_hat, channel, callback)

    def register_online_callback(self, address, channel, callback):
        """Register I2C-HAT online callback."""
        with self._lock:
            i2c_hat = self._i2c_hats[address]
            callbacks = getattr(i2c_hat, self._CALLBACKS)
            callbacks[channel] = callback
            setattr(i2c_hat, self._CALLBACKS, callbacks)

    def read_di(self, address, channel):
        """Read a value from a I2C-HAT digital input."""
        # This is a Pi module and can't be installed in CI without
        # breaking the build.
        # pylint: disable=import-outside-toplevel,import-error
        from raspihats.i2c_hats import ResponseException

        with self._lock:
            i2c_hat = self._i2c_hats[address]
            try:
                value = i2c_hat.di.value
                return (value >> channel) & 0x01
            except ResponseException as ex:
                raise I2CHatsException(str(ex)) from ex

    def write_dq(self, address, channel, value):
        """Write a value to a I2C-HAT digital output."""
        # This is a Pi module and can't be installed in CI without
        # breaking the build.
        # pylint: disable=import-outside-toplevel,import-error
        from raspihats.i2c_hats import ResponseException

        with self._lock:
            i2c_hat = self._i2c_hats[address]
            try:
                i2c_hat.dq.channels[channel] = value
            except ResponseException as ex:
                raise I2CHatsException(str(ex)) from ex

    def read_dq(self, address, channel):
        """Read a value from a I2C-HAT digital output."""
        # This is a Pi module and can't be installed in CI without
        # breaking the build.
        # pylint: disable=import-outside-toplevel,import-error
        from raspihats.i2c_hats import ResponseException

        with self._lock:
            i2c_hat = self._i2c_hats[address]
            try:
                return i2c_hat.dq.channels[channel]
            except ResponseException as ex:
                raise I2CHatsException(str(ex)) from ex
