from abc import ABC, abstractmethod
import os
import re
from pathlib import Path

import gpiod
from gpiod.chip import LineSettings
from gpiod.line import Direction, Value
from more_itertools import first_true

from ekfsm.core.components import SysTree
from ekfsm.exceptions import GPIOError
from ekfsm.log import ekfsm_logger

from .generic import Device
from ..core.probe import ProbeableDevice

gpio_pat = re.compile(r"gpiochip\d+")


def get_gpio_major_minor(path: Path) -> tuple[int, int]:
    for d in path.iterdir():
        if gpio_pat.match(d.name):
            dev = d / "dev"
            if dev.exists():
                content = dev.read_text().strip()
                major, minor = map(int, content.split(":"))
                return major, minor

    raise GPIOError(
        GPIOError.ErrorType.NO_MAJOR_MINOR,
        f"No minor/major number found for GPIO device at {path}",
    )


def find_gpio_dev_with_major_minor(major: int, minor: int) -> Path | None:
    for dev in Path("/dev").iterdir():
        if gpio_pat.match(dev.name):
            stat_info = dev.stat()

            cmaj = os.major(stat_info.st_rdev)
            cmin = os.minor(stat_info.st_rdev)

            if cmaj == major and cmin == minor:
                return dev

    raise GPIOError(
        GPIOError.ErrorType.NO_MATCHING_DEVICE,
        f"Failed to find GPIO device with major {major} and minor {minor}",
    )


class GPIO(Device):

    def __init__(
        self,
        name: str,
        parent: SysTree | None = None,
        *args,
        **kwargs,
    ):
        super().__init__(
            name,
            parent,
            None,
            *args,
            **kwargs,
        )
        self.logger = ekfsm_logger("GPIODevice" + name)
        major, minor = self._find_gpio_dev(parent, *args, **kwargs)
        self.gpio = find_gpio_dev_with_major_minor(major, minor)

        assert self.gpio is not None
        match = re.search(r"\d+", self.gpio.name)
        if match:
            self.number: int = int(match.group().strip())
        else:
            raise GPIOError(
                GPIOError.ErrorType.NO_MATCHING_DEVICE, "Failed to find matching device"
            )

        self.init_dev()

    def _find_gpio_dev(
        self,
        parent: SysTree | None = None,
        *args,
        **kwargs,
    ) -> tuple[int, int]:
        self.addr = self.get_i2c_chip_addr()
        self.logger.debug(f"GPIO: {self.addr}")
        self.sysfs_device = self.get_i2c_sysfs_device(self.addr)
        return get_gpio_major_minor(self.sysfs_device.path)

    def init_dev(self):
        if self.gpio:
            try:
                self.dev = gpiod.Chip(str(self.gpio))
                self.initialized = True
            except FileNotFoundError:
                raise FileNotFoundError(f"{self.gpio} does not exist")

    def num_lines(self) -> int:
        """
        Get number of GPIO lines available on the device.
        """
        return self.dev.get_info().num_lines

    def set_pin(self, pin: int, value: bool) -> None:
        """
        Set the value of a GPIO pin.

        Parameters
        ----------
        pin : int
            The pin number.
        value : bool
            The value to set.
        """
        v = Value.ACTIVE if value else Value.INACTIVE
        with self.dev.request_lines(
            consumer="set-pin",
            config={pin: gpiod.LineSettings()},
        ) as request:
            request.set_value(pin, v)

    def get_pin(self, pin: int) -> bool:
        """
        Get the value of a GPIO pin.

        Parameters
        ----------
        pin : int
            The pin number.

        Returns
        -------
        bool
            The value of the pin.
        """
        with self.dev.request_lines(
            consumer="get-pin",
            config={pin: gpiod.LineSettings()},
        ) as req:
            value = req.get_value(pin)
            return value == Value.ACTIVE

    def get_lines(self, lines: list[int]):
        if (
            invalid_pin := first_true(
                lines,
                pred=lambda line: line < 0 or line >= self.num_lines(),
                default=None,
            )
        ) is not None:
            raise GPIOError(
                GPIOError.ErrorType.INVALID_PIN, f"GPIO {invalid_pin} is invalid."
            )

    def set_direction(self, pin: int, direction: bool) -> None:
        """
        Set the direction of a GPIO pin.

        Parameters
        ----------
        pin : int
            The pin number.
        direction : bool
            The direction to set. True for output, False for input.
        """
        dir = Direction.OUTPUT if direction else Direction.INPUT
        self.dev.request_lines(
            consumer="set-direction",
            config={pin: LineSettings(direction=dir)},
        )

    def __str__(self) -> str:
        return (
            f"GPIO - Number: {self.number}; "
            f"sysfs_path: {self.sysfs_device.path if self.sysfs_device else ''} "
            f"(dev: {self.gpio if self.gpio else 'No matching device found'})"
        )


class GPIOExpander(GPIO):
    def __init__(
        self,
        name: str,
        parent: SysTree | None,
        *args,
        **kwargs,
    ):
        super().__init__(name, parent, None, *args, **kwargs)

    def __str__(self) -> str:
        return (
            f"GPIOExpander - Number: {self.number}; "
            f"sysfs_path: {self.sysfs_device.path if self.sysfs_device else ''}"
        )


class EKFIdentificationIOExpander(GPIOExpander, ProbeableDevice):
    def __init__(
        self,
        name: str,
        parent: SysTree | None,
        *args,
        **kwargs,
    ):
        super().__init__(name, parent, None, *args, **kwargs)

    def probe(self, *args, **kwargs) -> bool:
        from ekfsm.core import HwModule

        assert isinstance(self.hw_module, HwModule)
        id, _ = self.read_board_id_rev()
        self.logger.debug(f"Probing EKFIdentificationIOExpander: {id}")

        return self.hw_module.id == id

    def read_board_id_rev(self) -> tuple[int, int]:
        for pin in range(6, 8):
            self.set_direction(pin, True)
            self.set_pin(pin, False)
        for pin in range(0, 6):
            self.set_direction(pin, False)

        v_gnd = self.read_id_gpio_inputs()
        self.set_pin(7, True)
        v_7h = self.read_id_gpio_inputs()
        self.set_pin(6, True)
        v_6h = self.read_id_gpio_inputs()

        readings = [v_gnd, v_7h, v_6h]
        bit_sums = []
        for bit in range(5, -1, -1):
            s = 0
            for reading in readings:
                s += (reading >> bit) & 1
            bit_sums.append(s)

        return (
            sum(val * (4**i) for i, val in enumerate(reversed(bit_sums[2:]))),
            self._get_board_rev(bit_sums[:2]),  # board_rev
        )

    def revision(self) -> str:
        id, rev = self.read_board_id_rev()
        return str(rev)

    def read_id_gpio_inputs(self) -> int:
        value = 0
        for pin in range(6):
            if self.get_pin(pin):
                value |= 1 << pin

        return value

    @staticmethod
    def _get_board_rev(bits: list[int]) -> int:
        """Convert 2-bit sum values to board revision number."""
        rev_map = {
            (0, 0): 0,
            (0, 1): 1,
            (0, 2): 2,
            (1, 0): 3,
            (1, 1): 4,
            (1, 2): 5,
            (2, 0): 6,
            (2, 1): 7,
            (2, 2): 8,
        }
        return rev_map.get((bits[0], bits[1]), -1)

    def __str__(self) -> str:
        return (
            f"EKFIdentificationIOExpander - Number: {self.number}; "
            f'sysfs_path: {self.sysfs_device.path if self.sysfs_device else ""}'
        )


class SimGpio(ABC):
    @abstractmethod
    def num_lines(self) -> int:
        pass

    @abstractmethod
    def set_pin(self, pin: int, value: bool) -> None:
        pass

    @abstractmethod
    def get_pin(self, pin: int) -> bool:
        pass

    @abstractmethod
    def set_direction(self, pin: int, direction: bool) -> None:
        pass


class EKFIdSimGpio(SimGpio):
    def __init__(self, coding_gnd, coding_vcc, coding_6, coding_7) -> None:
        self._coding_gnd = coding_gnd
        self._coding_vcc = coding_vcc
        self._coding_6 = coding_6
        self._coding_7 = coding_7
        self._dir = 0
        self._out = 0
        self._in = 0

    def num_lines(self) -> int:
        return 8

    def set_pin(self, pin: int, value: bool) -> None:
        mask = 1 << pin
        if value not in [0, 1]:
            raise RuntimeError("value must be 0 or 1")
        if self._dir & mask:
            self._out = (self._out & ~mask) | (value << pin)
        else:
            raise RuntimeError("pin not set as output")

    def get_pin(self, pin: int) -> bool:
        mask = 1 << pin
        if self._coding_gnd & mask:
            return False
        if self._coding_vcc & mask:
            return True
        if self._coding_6 & mask:
            return True if self._out & (1 << 6) else False
        if self._coding_7 & mask:
            return True if self._out & (1 << 7) else False
        return False

    def set_direction(self, pin: int, direction: bool) -> None:
        if direction == 1 and (pin != 6 and pin != 7):
            raise RuntimeError("only pins 6 and 7 supported as output")
        mask = 1 << pin
        if direction:
            self._dir |= mask
        else:
            self._dir &= ~mask
