# Source code for gpiozero.pins.lgpio

    # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins
    #
    # Copyright (c) 2021 Dave Jones <dave@waveform.org.uk>
    #
    # SPDX-License-Identifier: BSD-3-Clause

    from __future__ import (
        unicode_literals,
        absolute_import,
        print_function,
        division,
        )
    str = type('')

    import os

    import lgpio

    from . import SPI
    from .pi import spi_port_device
    from .local import LocalPiFactory, LocalPiPin
    from ..mixins import SharedMixin
    from ..exc import (
        PinInvalidFunction,
        PinSetInput,
        PinFixedPull,
        PinInvalidPull,
        PinInvalidBounce,
        PinInvalidState,
        SPIBadArgs,
        SPIInvalidClockMode,
        PinPWMFixedValue,
        DeviceClosed
    )

    class LGPIOFactory(LocalPiFactory):
        """
        Extends :class:`~gpiozero.pins.local.LocalPiFactory`. Uses the `lgpio`_
        library to interface to the local computer's GPIO pins. The lgpio library
        simply talks to Linux gpiochip devices; it is not specific to the Raspberry
        Pi although this class is currently constructed under the assumption that
        it is running on a Raspberry Pi.

        You can construct lgpio pins manually like so::

            from gpiozero.pins.lgpio import LGPIOFactory
            from gpiozero import LED

            factory = LGPIOFactory(chip=0)
            led = LED(12, pin_factory=factory)

        The *chip* parameter to the factory constructor specifies which gpiochip
        device to attempt to open. It defaults to 0 and thus doesn't normally need
        to be specified (the example above only includes it for completeness).

        The lgpio library relies on access to the :file:`/dev/gpiochip*` devices.
        If you run into issues, please check that your user has read/write access
        to the specific gpiochip device you are attempting to open (0 by default).

        .. _lgpio: http://abyz.me.uk/lg/py_lgpio.html
        """
        def __init__(self, chip=0):
            super(LGPIOFactory, self).__init__()
            self._handle = lgpio.gpiochip_open(chip)
            self._chip = chip
            self.pin_class = LGPIOPin

        def close(self):
            super(LGPIOFactory, self).close()
            if self._handle is not None:
                lgpio.gpiochip_close(self._handle)
                self._handle = None

        @property
        def chip(self):
            return self._chip

        def _get_spi_class(self, shared, hardware):
            # support via lgpio instead of spidev
            if hardware:
                return [LGPIOHardwareSPI, LGPIOHardwareSPIShared][shared]
            return super(LGPIOFactory, self)._get_spi_class(shared, hardware=False)

    class LGPIOPin(LocalPiPin):
        """
        Extends :class:`~gpiozero.pins.local.LocalPiPin`. Pin implementation for
        the `lgpio`_ library. See :class:`LGPIOFactory` for more information.

        .. _lgpio: http://abyz.me.uk/lg/py_lgpio.html
        """
        GPIO_IS_KERNEL         = 1 << 0
        GPIO_IS_OUT            = 1 << 1
        GPIO_IS_ACTIVE_LOW     = 1 << 2
        GPIO_IS_OPEN_DRAIN     = 1 << 3
        GPIO_IS_OPEN_SOURCE    = 1 << 4
        GPIO_IS_BIAS_PULL_UP   = 1 << 5
        GPIO_IS_BIAS_PULL_DOWN = 1 << 6
        GPIO_IS_BIAS_DISABLE   = 1 << 7
        GPIO_IS_LG_INPUT       = 1 << 8
        GPIO_IS_LG_OUTPUT      = 1 << 9
        GPIO_IS_LG_ALERT       = 1 << 10
        GPIO_IS_LG_GROUP       = 1 << 11

        GPIO_LINE_FLAGS_MASK = (
            GPIO_IS_ACTIVE_LOW | GPIO_IS_OPEN_DRAIN | GPIO_IS_OPEN_SOURCE |
            GPIO_IS_BIAS_PULL_UP | GPIO_IS_BIAS_PULL_DOWN | GPIO_IS_BIAS_DISABLE)

        GPIO_EDGES = {
            'both':    lgpio.BOTH_EDGES,
            'rising':  lgpio.RISING_EDGE,
            'falling': lgpio.FALLING_EDGE,
            }

        GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()}

        def __init__(self, factory, number):
            super(LGPIOPin, self).__init__(factory, number)
            self._pwm = None
            self._bounce = None
            self._callback = None
            self._edges = lgpio.BOTH_EDGES
            lgpio.gpio_claim_input(
                self.factory._handle, self.number, lgpio.SET_BIAS_DISABLE)

        def close(self):
            if self.factory._handle is not None:
                # Closing is really just "resetting" the function of the pin;
                # we let the factory close deal with actually freeing stuff
                lgpio.gpio_claim_input(
                    self.factory._handle, self.number, lgpio.SET_BIAS_DISABLE)

        def _get_function(self):
            mode = lgpio.gpio_get_mode(self.factory._handle, self.number)
            return ['input', 'output'][bool(mode & self.GPIO_IS_OUT)]

        def _set_function(self, value):
            if self._callback is not None:
                self._callback.cancel()
                self._callback = None
            try:
                {
                    'input': lgpio.gpio_claim_input,
                    'output': lgpio.gpio_claim_output,
                }[value](self.factory._handle, self.number)
            except KeyError:
                raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self))

        def _get_state(self):
            if self._pwm:
                return self._pwm[1] / 100
            else:
                return bool(lgpio.gpio_read(self.factory._handle, self.number))

        def _set_state(self, value):
            if self._pwm:
                freq, duty = self._pwm
                self._pwm = (freq, int(value * 100))
                try:
                    lgpio.tx_pwm(self.factory._handle, self.number, *self._pwm)
                except lgpio.error:
                    raise PinInvalidState('invalid state "%s" for pin %r' % (value, self))
            elif self.function == 'input':
                raise PinSetInput('cannot set state of pin %r' % self)
            else:
                lgpio.gpio_write(self.factory._handle, self.number, bool(value))

        def _get_pull(self):
            mode = lgpio.gpio_get_mode(self.factory._handle, self.number)
            if mode & self.GPIO_IS_BIAS_PULL_UP:
                return 'up'
            elif mode & self.GPIO_IS_BIAS_PULL_DOWN:
                return 'down'
            else:
                return 'floating'

        def _set_pull(self, value):
            if self.function != 'input':
                raise PinFixedPull('cannot set pull on non-input pin %r' % self)
            if value != 'up' and self.factory.pi_info.pulled_up(repr(self)):
                raise PinFixedPull('%r has a physical pull-up resistor' % self)
            try:
                flags = {
                    'up': lgpio.SET_BIAS_PULL_UP,
                    'down': lgpio.SET_BIAS_PULL_DOWN,
                    'floating': lgpio.SET_BIAS_DISABLE,
                }[value]
            except KeyError:
                raise PinInvalidPull('invalid pull "%s" for pin %r' % (value, self))
            else:
                # Simply calling gpio_claim_input is insufficient to change the
                # line flags on a pin; it needs to be freed and re-claimed
                lgpio.gpio_free(self.factory._handle, self.number)
                lgpio.gpio_claim_input(self.factory._handle, self.number, flags)

        def _get_frequency(self):
            if self._pwm:
                freq, duty = self._pwm
                return freq
            else:
                return None

        def _set_frequency(self, value):
            if not self._pwm and value is not None and value > 0:
                if self.function != 'output':
                    raise PinPWMFixedValue('cannot start PWM on pin %r' % self)
                lgpio.tx_pwm(self.factory._handle, self.number, value, 0)
                self._pwm = (value, 0)
            elif self._pwm and value is not None and value > 0:
                freq, duty = self._pwm
                lgpio.tx_pwm(self.factory._handle, self.number, value, duty)
                self._pwm = (value, duty)
            elif self._pwm and (value is None or value == 0):
                lgpio.tx_pwm(self.factory._handle, self.number, 0, 0)
                self._pwm = None

        def _get_bounce(self):
            return None if not self._bounce else self._bounce / 1000000

        def _set_bounce(self, value):
            if value is None:
                value = 0
            elif value < 0:
                raise PinInvalidBounce('bounce must be 0 or greater')
            value = int(value * 1000000)
            lgpio.gpio_set_debounce_micros(self.factory._handle, self.number, value)
            self._bounce = value

        def _get_edges(self):
            return self.GPIO_EDGES_NAMES[self._edges]

        def _set_edges(self, value):
            f = self.when_changed
            self.when_changed = None
            try:
                self._edges = self.GPIO_EDGES[value]
            finally:
                self.when_changed = f

        def _call_when_changed(self, chip, gpio, level, ticks):
            super(LGPIOPin, self)._call_when_changed(ticks / 1000000000, level)

        def _enable_event_detect(self):
            lgpio.gpio_claim_alert(
                self.factory._handle, self.number, self._edges,
                lgpio.gpio_get_mode(self.factory._handle, self.number) &
                self.GPIO_LINE_FLAGS_MASK)
            self._callback = lgpio.callback(
                self.factory._handle, self.number, self._edges,
                self._call_when_changed)

        def _disable_event_detect(self):
            if self._callback is not None:
                self._callback.cancel()
                self._callback = None
            lgpio.gpio_claim_input(
                self.factory._handle, self.number,
                lgpio.gpio_get_mode(self.factory._handle, self.number) &
                self.GPIO_LINE_FLAGS_MASK)

    class LGPIOHardwareSPI(SPI):
        """
        Hardware SPI implementation for the `lgpio`_ library. Uses the ``spi_*``
        functions from the lgpio API.

        .. _lgpio: http://abyz.me.uk/lg/py_lgpio.html
        """
        def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory):
            port, device = spi_port_device(
                clock_pin, mosi_pin, miso_pin, select_pin)
            self._port = port
            self._device = device
            self._baud = 500000
            self._spi_flags = 0
            self._handle = None
            super(LGPIOHardwareSPI, self).__init__(pin_factory=pin_factory)
            to_reserve = {clock_pin, select_pin}
            if mosi_pin is not None:
                to_reserve.add(mosi_pin)
            if miso_pin is not None:
                to_reserve.add(miso_pin)
            self.pin_factory.reserve_pins(self, *to_reserve)
            self._handle = lgpio.spi_open(port, device, self._baud, self._spi_flags)

        def _conflicts_with(self, other):
            return not (
                isinstance(other, LGPIOHardwareSPI) and
                (self._port, self._device) != (other._port, other._device)
                )

        def close(self):
            if not self.closed:
                lgpio.spi_close(self._handle)
            self._handle = None
            self.pin_factory.release_all(self)
            super(LGPIOHardwareSPI, self).close()

        @property
        def closed(self):
            return self._handle is None

        def __repr__(self):
            try:
                self._check_open()
                return 'SPI(port=%d, device=%d)' % (self._port, self._device)
            except DeviceClosed:
                return 'SPI(closed)'

        def _get_clock_mode(self):
            return self._spi_flags

        def _set_clock_mode(self, value):
            self._check_open()
            if not 0 <= value < 4:
                raise SPIInvalidClockMode("%d is not a valid SPI clock mode" % value)
            lgpio.spi_close(self._handle)
            self._spi_flags = value
            self._handle = lgpio.spi_open(
                self._port, self._device, self._baud, self._spi_flags)

        def _get_rate(self):
            return self._baud

        def _set_rate(self, value):
            self._check_open()
            value = int(value)
            lgpio.spi_close(self._handle)
            self._baud = value
            self._handle = lgpio.spi_open(
                self._port, self._device, self._baud, self._spi_flags)

        def read(self, n):
            self._check_open()
            count, data = lgpio.spi_read(self._handle, n)
            if count < 0:
                raise IOError('SPI transfer error %d' % count)
            return [int(b) for b in data]

        def write(self, data):
            self._check_open()
            count = lgpio.spi_write(self._handle, data)
            if count < 0:
                raise IOError('SPI transfer error %d' % count)
            return len(data)

        def transfer(self, data):
            self._check_open()
            count, data = lgpio.spi_xfer(self._handle, data)
            if count < 0:
                raise IOError('SPI transfer error %d' % count)
            return [int(b) for b in data]

    class LGPIOHardwareSPIShared(SharedMixin, LGPIOHardwareSPI):
        @classmethod
        def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory):
            return (clock_pin, select_pin)