# SPDX-FileCopyrightText: 2016 Philip R. Moyer  for Adafruit Industries
# SPDX-FileCopyrightText: 2016 Radomir Dopieralski for Adafruit Industries
# SPDX-FileCopyrightText: 2021 Daniel Flanagan
#
# SPDX-License-Identifier: MIT

"""
`adafruit_max7219.max7219` - MAX7219 LED Matrix/Digit Display Driver
========================================================================
CircuitPython library to support MAX7219 LED Matrix/Digit Display Driver.
This library supports the use of the MAX7219-based display in CircuitPython,
either an 8x8 matrix or a 8 digit 7-segment numeric display.

See Also
=========
* matrices.Maxtrix8x8 is a class support an 8x8 led matrix display
* matrices.CustomMatrix is a class support a custom sized constellation of 8x8 led matrix displays
* bcddigits.BCDDigits is a class that support the 8 digit 7-segment display

Beware that most CircuitPython compatible hardware are 3.3v logic level! Make
sure that the input pin is 5v tolerant.

* Author(s): Michael McWethy

Implementation Notes
--------------------
**Hardware:**

* Adafruit `MAX7219CNG LED Matrix/Digit Display Driver -
  MAX7219 <https://www.adafruit.com/product/453>`_ (Product ID: 453)

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the ESP8622 and M0-based boards:
  https://github.com/adafruit/circuitpython/releases

* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice

**Notes:**
#.  Datasheet: https://cdn-shop.adafruit.com/datasheets/MAX7219.pdf
"""
# MicroPython MAX7219 driver, SPI interfaces
import digitalio
from adafruit_bus_device import spi_device
from micropython import const
import adafruit_framebuf as framebuf

try:
    # Used only for typing
    import typing  # pylint: disable=unused-import
    import busio
except ImportError:
    pass

__version__ = "1.5.6"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MAX7219.git"

# register definitions
_DIGIT0 = const(1)
_INTENSITY = const(10)


class MAX7219:
    """
    MAX7219 - driver for displays based on max7219 chip_select

    :param int width: the number of pixels wide
    :param int height: the number of pixels high
    :param ~busio.SPI spi: an spi busio or spi bitbangio object
    :param ~digitalio.DigitalInOut chip_select: digital in/out to use as chip select signal
    :param int baudrate: for SPIDevice baudrate (default 8000000)
    :param int polarity: for SPIDevice polarity (default 0)
    :param int phase: for SPIDevice phase (default 0)
    """

    def __init__(
        self,
        width: int,
        height: int,
        spi: busio.SPI,
        cs: digitalio.DigitalInOut,
        *,
        baudrate: int = 8000000,
        polarity: int = 0,
        phase: int = 0
    ):

        self._chip_select = cs
        self._chip_select.direction = digitalio.Direction.OUTPUT

        self._spi_device = spi_device.SPIDevice(
            spi, cs, baudrate=baudrate, polarity=polarity, phase=phase
        )

        self._buffer = bytearray((height // 8) * width)
        self.framebuf = framebuf.FrameBuffer1(self._buffer, width, height)

        self.width = width
        self.height = height

        self.init_display()

    def init_display(self) -> None:
        """Must be implemented by derived class (``matrices``, ``bcddigits``)"""

    def brightness(self, value: int) -> None:
        """
        Controls the brightness of the display.

        :param int value: 0->15 dimmest to brightest
        """
        if not 0 <= value <= 15:
            raise ValueError("Brightness out of range")
        self.write_cmd(_INTENSITY, value)

    def show(self) -> None:
        """
        Updates the display.
        """
        for ypos in range(8):
            self.write_cmd(_DIGIT0 + ypos, self._buffer[ypos])

    def fill(self, bit_value: int) -> None:
        """
        Fill the display buffer.

        :param int bit_value: value > 0 set the buffer bit, else clears the buffer bit
        """
        self.framebuf.fill(bit_value)

    def pixel(self, xpos: int, ypos: int, bit_value: int = None) -> None:
        """
        Set one buffer bit

        :param int xpos: x position to set bit
        :param int ypos: y position to set bit
        :param int bit_value: value > 0 sets the buffer bit, else clears the buffer bit
        """
        bit_value = 0x01 if bit_value else 0x00
        self.framebuf.pixel(xpos, ypos, bit_value)

    def scroll(self, delta_x: int, delta_y: int) -> None:
        """
        Srcolls the display using delta_x,delta_y.

        :param int delta_x: positions to scroll in the x direction
        :param int delta_y: positions to scroll in the y direction
        """
        self.framebuf.scroll(delta_x, delta_y)

    def write_cmd(self, cmd: int, data: int) -> None:
        """
        Writes a command to spi device.

        :param int cmd: register address to write data to
        :param int data: data to be written to commanded register
        """
        # print('cmd {} data {}'.format(cmd,data))
        self._chip_select.value = False
        with self._spi_device as my_spi_device:
            my_spi_device.write(bytearray([cmd, data]))


class ChainableMAX7219(MAX7219):
    """
    Daisy Chainable MAX7219 - driver for cascading displays based on max7219 chip_select

    :param int width: the number of pixels wide
    :param int height: the number of pixels high
    :param ~busio.SPI spi: an spi busio or spi bitbangio object
    :param ~digitalio.DigitalInOut chip_select: digital in/out to use as chip select signal
    :param int baudrate: for SPIDevice baudrate (default 8000000)
    :param int polarity: for SPIDevice polarity (default 0)
    :param int phase: for SPIDevice phase (default 0)
    """

    def __init__(
        self,
        width: int,
        height: int,
        spi: busio.SPI,
        cs: digitalio.DigitalInOut,
        *,
        baudrate: int = 8000000,
        polarity: int = 0,
        phase: int = 0
    ):
        self.chain_length = (height // 8) * (width // 8)

        super().__init__(
            width, height, spi, cs, baudrate=baudrate, polarity=polarity, phase=phase
        )
        self._buffer = bytearray(self.chain_length * 8)
        self.framebuf = framebuf.FrameBuffer1(self._buffer, self.chain_length * 8, 8)

    def write_cmd(self, cmd: int, data: int) -> None:
        """
        Writes a command to spi device.

        :param int cmd: register address to write data to
        :param int data: data to be written to commanded register
        """
        # print('cmd {} data {}'.format(cmd,data))
        self._chip_select.value = False
        with self._spi_device as my_spi_device:
            for _ in range(self.chain_length):
                my_spi_device.write(bytearray([cmd, data]))

    def show(self) -> None:
        """
        Updates the display.
        """
        for ypos in range(8):
            self._chip_select.value = False
            with self._spi_device as my_spi_device:
                for chip in range(self.chain_length):
                    my_spi_device.write(
                        bytearray(
                            [
                                _DIGIT0 + ypos,
                                self._buffer[ypos * self.chain_length + chip],
                            ]
                        )
                    )
