# SPDX-FileCopyrightText: 2019 Bryan Siepert for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
`adafruit_lsm303_accel`
====================================================


CircuitPython driver for the accelerometer in LSM303 sensors.

* Author(s): Dave Astels, Bryan Siepert

Implementation Notes
--------------------

**Hardware:**

* Adafruit `Triple-axis Accelerometer+Magnetometer (Compass) Board - LSM303
  <https://www.adafruit.com/product/1120>`_ (Product ID: 1120)
* Adafruit `FLORA Accelerometer/Compass Sensor - LSM303 - v1.0
  <https://www.adafruit.com/product/1247>`_ (Product ID: 1247)

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
  https://circuitpython.org/downloads
* Adafruit's Bus Device library:
  https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
"""

import struct

from adafruit_bus_device.i2c_device import I2CDevice
from adafruit_register.i2c_bit import ROBit, RWBit
from adafruit_register.i2c_bits import RWBits
from adafruit_register.i2c_struct import UnaryStruct
from adafruit_register.i2c_struct_array import StructArray
from micropython import const

try:
    from typing import Optional, Tuple

    from busio import I2C
    from typing_extensions import Literal
except ImportError:
    pass

__version__ = "1.1.26"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_LSM303_Accel.git"

_ADDRESS_ACCEL = const(0x19)  # (0x32 >> 1)       // 0011001x
_ADDRESS_MAG = const(0x1E)  # (0x3C >> 1)       // 0011110x
_ID = const(0xD4)  # (0b11010100)

# Accelerometer registers

_REG_ACCEL_WHO_AM_I = const(0x0F)
_REG_ACCEL_CTRL_REG1_A = const(0x20)
_REG_ACCEL_CTRL_REG2_A = const(0x21)
_REG_ACCEL_CTRL_REG3_A = const(0x22)
_REG_ACCEL_CTRL_REG4_A = const(0x23)
_REG_ACCEL_CTRL_REG5_A = const(0x24)
_REG_ACCEL_CTRL_REG6_A = const(0x25)
_REG_ACCEL_REFERENCE_A = const(0x26)
_REG_ACCEL_STATUS_REG_A = const(0x27)
_REG_ACCEL_OUT_X_L_A = const(0x28)
_REG_ACCEL_OUT_X_H_A = const(0x29)
_REG_ACCEL_OUT_Y_L_A = const(0x2A)
_REG_ACCEL_OUT_Y_H_A = const(0x2B)
_REG_ACCEL_OUT_Z_L_A = const(0x2C)
_REG_ACCEL_OUT_Z_H_A = const(0x2D)
_REG_ACCEL_FIFO_CTRL_REG_A = const(0x2E)
_REG_ACCEL_FIFO_SRC_REG_A = const(0x2F)
_REG_ACCEL_INT1_CFG_A = const(0x30)
_REG_ACCEL_INT1_SOURCE_A = const(0x31)
_REG_ACCEL_INT1_THS_A = const(0x32)
_REG_ACCEL_INT1_DURATION_A = const(0x33)
_REG_ACCEL_INT2_CFG_A = const(0x34)
_REG_ACCEL_INT2_SOURCE_A = const(0x35)
_REG_ACCEL_INT2_THS_A = const(0x36)
_REG_ACCEL_INT2_DURATION_A = const(0x37)
_REG_ACCEL_CLICK_CFG_A = const(0x38)
_REG_ACCEL_CLICK_SRC_A = const(0x39)
_REG_ACCEL_CLICK_THS_A = const(0x3A)
_REG_ACCEL_TIME_LIMIT_A = const(0x3B)
_REG_ACCEL_TIME_LATENCY_A = const(0x3C)
_REG_ACCEL_TIME_WINDOW_A = const(0x3D)
_REG_ACCEL_ACT_THS_A = const(0x3E)
_REG_ACCEL_ACT_DUR_A = const(0x3F)
# note:: Tap related registers are called ``CLICK_`` in the datasheet
# Conversion constants
_LSM303ACCEL_MG_LSB = 16704.0  # magic!
_GRAVITY_STANDARD = 9.80665  # Earth's gravity in m/s^2
_SMOLLER_GRAVITY = 0.00980665


class Rate:
    """Options for `data_rate`"""

    RATE_SHUTDOWN = const(0)
    RATE_1_HZ = const(1)
    RATE_10_HZ = const(2)
    RATE_25_HZ = const(3)
    RATE_50_HZ = const(4)
    RATE_100_HZ = const(5)
    RATE_200_HZ = const(6)
    RATE_400_HZ = const(7)
    RATE_1620_HZ = const(8)
    RATE_1344_HZ = const(9)


class Mode:
    """Options for `mode`"""

    MODE_NORMAL = const(0)
    MODE_HIGH_RESOLUTION = const(1)
    MODE_LOW_POWER = const(2)


class Range:
    """Options for `range`"""

    RANGE_2G = const(0)
    RANGE_4G = const(1)
    RANGE_8G = const(2)
    RANGE_16G = const(3)


class LSM303_Accel:
    """Driver for the LSM303's accelerometer.

    :param ~busio.I2C i2c: The I2C bus the device is connected to.


    **Quickstart: Importing and using the device**

        Here is an example of using the :class:`LSM303_Accel` class.
        First you will need to import the libraries to use the sensor

        .. code-block:: python

            import board
            import adafruit_lsm303_accel

        Once this is done you can define your `board.I2C` object and define your sensor object

        .. code-block:: python

            i2c = board.I2C()  # uses board.SCL and board.SDA
            sensor = adafruit_lsm303_accel.LSM303_Accel(i2c)

        Now you have access to the :attr:`acceleration` attribute

        .. code-block:: python

            acc_x, acc_y, acc_z = sensor.acceleration


    """

    # Class-level buffer for reading and writing data with the sensor.
    # This reduces memory allocations but means the code is not re-entrant or
    # thread safe!
    _chip_id = UnaryStruct(_REG_ACCEL_WHO_AM_I, "B")
    _int2_int1_enable = RWBit(_REG_ACCEL_CTRL_REG6_A, 6)
    _int2_int2_enable = RWBit(_REG_ACCEL_CTRL_REG6_A, 5)
    _int1_latching = RWBit(_REG_ACCEL_CTRL_REG5_A, 3)
    _int2_latching = RWBit(_REG_ACCEL_CTRL_REG5_A, 1)
    _bdu = RWBit(_REG_ACCEL_CTRL_REG4_A, 7)

    _int2_activity_enable = RWBit(_REG_ACCEL_CTRL_REG6_A, 3)
    _int_pin_active_low = RWBit(_REG_ACCEL_CTRL_REG6_A, 1)

    _act_threshold = UnaryStruct(_REG_ACCEL_ACT_THS_A, "B")
    _act_duration = UnaryStruct(_REG_ACCEL_ACT_DUR_A, "B")

    _data_rate = RWBits(4, _REG_ACCEL_CTRL_REG1_A, 4)
    _enable_xyz = RWBits(3, _REG_ACCEL_CTRL_REG1_A, 0)
    _raw_accel_data = StructArray(_REG_ACCEL_OUT_X_L_A, "<h", 3)

    _low_power = RWBit(_REG_ACCEL_CTRL_REG1_A, 3)
    _high_resolution = RWBit(_REG_ACCEL_CTRL_REG4_A, 3)

    _range = RWBits(2, _REG_ACCEL_CTRL_REG4_A, 4)

    _int1_src = UnaryStruct(_REG_ACCEL_INT1_SOURCE_A, "B")
    _tap_src = UnaryStruct(_REG_ACCEL_CLICK_SRC_A, "B")

    _tap_interrupt_enable = RWBit(_REG_ACCEL_CTRL_REG3_A, 7, 1)
    _tap_config = UnaryStruct(_REG_ACCEL_CLICK_CFG_A, "B")
    _tap_interrupt_active = ROBit(_REG_ACCEL_CLICK_SRC_A, 6, 1)
    _tap_threshold = UnaryStruct(_REG_ACCEL_CLICK_THS_A, "B")
    _tap_time_limit = UnaryStruct(_REG_ACCEL_TIME_LIMIT_A, "B")
    _tap_time_latency = UnaryStruct(_REG_ACCEL_TIME_LATENCY_A, "B")
    _tap_time_window = UnaryStruct(_REG_ACCEL_TIME_WINDOW_A, "B")

    _BUFFER = bytearray(6)

    def __init__(self, i2c: I2C) -> None:
        self._accel_device = I2CDevice(i2c, _ADDRESS_ACCEL)
        self.i2c_device = self._accel_device
        self._data_rate = 2
        self._enable_xyz = 0b111
        self._int1_latching = True
        self._int2_latching = True
        self._bdu = True

        # self._write_register_byte(_REG_CTRL5, 0x80)
        # time.sleep(0.01)  # takes 5ms
        self._cached_mode = 0
        self._cached_range = 0

    def set_tap(
        self,
        tap: Literal[0, 1, 2],
        threshold: int,
        *,
        time_limit: int = 10,
        time_latency: int = 20,
        time_window: int = 255,
        tap_cfg: Optional[int] = None,
    ) -> None:
        """
        The tap detection parameters.

        :param int tap: 0 to disable tap detection, 1 to detect only single taps, and 2 to detect \
            only double taps.
        :param int threshold: A threshold for the tap detection.  The higher the value the less\
            sensitive the detection. This changes based on the accelerometer range.  Good values\
            are 5-10 for 16G, 10-20 for 8G, 20-40 for 4G, and 40-80 for 2G.
        :param int time_limit: TIME_LIMIT register value. Defaults to :const:`10`
        :param int time_latency: TIME_LATENCY register value. Defaults to :const:`20`
        :param int time_window: TIME_WINDOW register value. Defaults to :const:`255`
        :param int tap_cfg: CLICK_CFG register value. Defaults to `None`

        """

        if (tap < 0 or tap > 2) and tap_cfg is None:
            raise ValueError("Tap must be 0 (disabled), 1 (single tap), or 2 (double tap)!")
        if threshold > 127 or threshold < 0:
            raise ValueError("Threshold out of range (0-127)")

        if tap == 0 and tap_cfg is None:
            # Disable click interrupt.
            self._tap_interrupt_enable = False
            self._tap_config = 0
            return

        self._tap_interrupt_enable = True

        if tap_cfg is None:
            if tap == 1:
                tap_cfg = 0x15  # Turn on all axes & singletap.
            if tap == 2:
                tap_cfg = 0x2A  # Turn on all axes & doubletap.
        # Or, if a custom tap configuration register value specified, use it.
        self._tap_config = tap_cfg

        self._tap_threshold = threshold  # why and?
        self._tap_time_limit = time_limit
        self._tap_time_latency = time_latency
        self._tap_time_window = time_window

    @property
    def tapped(self) -> bool:
        """
        True if a tap was detected recently. Whether its a single tap or double tap is
        determined by the tap param on :meth:`set_tap`. :attr:`tapped` may be True over
        multiple reads even if only a single tap or single double tap occurred.
        """
        tap_src = self._tap_src
        return tap_src & 0b1000000 > 0

    @property
    def _raw_acceleration(self) -> Tuple[int, int, int]:
        self._read_bytes(self._accel_device, _REG_ACCEL_OUT_X_L_A | 0x80, 6, self._BUFFER)
        return struct.unpack_from("<hhh", self._BUFFER[0:6])

    @property
    def acceleration(self) -> Tuple[float, float, float]:
        """The measured accelerometer sensor values.
        A 3-tuple of X, Y, Z axis values in m/s^2 squared that are signed floats.
        """

        raw_accel_data = self._raw_acceleration

        x = self._scale_data(raw_accel_data[0])
        y = self._scale_data(raw_accel_data[1])
        z = self._scale_data(raw_accel_data[2])

        return (x, y, z)

    def _scale_data(self, raw_measurement: int) -> float:
        lsb, shift = self._lsb_shift()

        return (raw_measurement >> shift) * lsb * _SMOLLER_GRAVITY

    def _lsb_shift(self) -> Tuple[float, int]:
        # the bit depth of the data depends on the mode, and the lsb value
        # depends on the mode and range
        lsb = -1  # the default, normal mode @ 2G

        if self._cached_mode is Mode.MODE_HIGH_RESOLUTION:  # 12-bit
            shift = 4
            if self._cached_range is Range.RANGE_2G:
                lsb = 0.98
            elif self._cached_range is Range.RANGE_4G:
                lsb = 1.95
            elif self._cached_range is Range.RANGE_8G:
                lsb = 3.9
            elif self._cached_range is Range.RANGE_16G:
                lsb = 11.72
        elif self._cached_mode is Mode.MODE_NORMAL:  # 10-bit
            shift = 6
            if self._cached_range is Range.RANGE_2G:
                lsb = 3.9
            elif self._cached_range is Range.RANGE_4G:
                lsb = 7.82
            elif self._cached_range is Range.RANGE_8G:
                lsb = 15.63
            elif self._cached_range is Range.RANGE_16G:
                lsb = 46.9

        elif self._cached_mode is Mode.MODE_LOW_POWER:  # 8-bit
            shift = 8
            if self._cached_range is Range.RANGE_2G:
                lsb = 15.63
            elif self._cached_range is Range.RANGE_4G:
                lsb = 31.26
            elif self._cached_range is Range.RANGE_8G:
                lsb = 62.52
            elif self._cached_range is Range.RANGE_16G:
                lsb = 187.58

        if lsb is -1:
            raise AttributeError(
                "'impossible' range or mode detected: "
                f"range: {self._cached_range} mode: {self._cached_mode}"
            )
        return (lsb, shift)

    @property
    def data_rate(self) -> int:
        """Select the rate at which the sensor takes measurements. Must be a `Rate`"""
        return self._data_rate

    @data_rate.setter
    def data_rate(self, value: int) -> None:
        if value < 0 or value > 9:
            raise AttributeError("data_rate must be a `Rate`")

        self._data_rate = value

    @property
    def range(self) -> int:
        """Adjusts the range of values that the sensor can measure, from +- 2G to +-16G
        Note that larger ranges will be less accurate. Must be a `Range`"""
        return self._cached_range

    @range.setter
    def range(self, value: int) -> None:
        if value < 0 or value > 3:
            raise AttributeError("range must be a `Range`")
        self._range = value
        self._cached_range = value

    @property
    def mode(self) -> int:
        """Sets the power mode of the sensor. The mode must be a `Mode`. Note that the
        mode and range will both affect the accuracy of the sensor"""
        return self._cached_mode

    @mode.setter
    def mode(self, value: int) -> None:
        if value < 0 or value > 2:
            raise AttributeError("mode must be a `Mode`")
        self._high_resolution = value & 0b01
        self._low_power = (value & 0b10) >> 1
        self._cached_mode = value

    def _read_u8(self, device: I2CDevice, address: int) -> int:
        with device as i2c:
            self._BUFFER[0] = address & 0xFF
            i2c.write_then_readinto(self._BUFFER, self._BUFFER, out_end=1, in_end=1)
        return self._BUFFER[0]

    def _write_u8(self, device: I2CDevice, address: int, val: int) -> None:
        with device as i2c:
            self._BUFFER[0] = address & 0xFF
            self._BUFFER[1] = val & 0xFF
            i2c.write(self._BUFFER, end=2)

    @staticmethod
    def _read_bytes(device: I2CDevice, address: int, count: int, buf: bytearray) -> None:
        with device as i2c:
            buf[0] = address & 0xFF
            i2c.write_then_readinto(buf, buf, out_end=1, in_end=count)
