# SPDX-FileCopyrightText: Copyright (c) 2023 JG for Cedar Grove Maker Studios
#
# SPDX-License-Identifier: MIT
"""
`cedargrove_ad9833`
================================================================================

A CircuitPython driver for the AD9833 Programmable Waveform Generator.

* Author(s): JG

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

**Hardware:**

* Cedar Grove Studios AD9833 Precision Waveform Generator FeatherWing
* Cedar Grove Studios AD9833 ADSR Precision Waveform Generator FeatherWing

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
  https://circuitpython.org/downloads

"""

import digitalio
from adafruit_bus_device.spi_device import SPIDevice

__version__ = "2.1.3"
__repo__ = "https://github.com/CedarGroveStudios/CircuitPython_AD9833.git"


# pylint: disable=too-many-instance-attributes
class AD9833:
    """The driver class for the AD9833 Programmable Waveform Generator.

    The AD9833 is a programmable waveform generator that produces sine, square,
    and triangular waveform output from 0 MHz to 12.5MHz with 28-bit frequency
    resolution. The CircuitPython class sets the waveform generator's frequency,
    phase, and wave shape properties as well as providing methods for
    resetting, starting, pausing, and stopping the generator."""

    # pylint: disable=too-many-arguments
    def __init__(
        self,
        wave_freq=440,
        wave_phase=0,
        wave_type="sine",
        spi=None,
        select=None,
        m_clock=25000000,
    ):
        """Initialize SPI bus interconnect and create the SPIDevice instance.
        During initialization, the generator is reset and placed in the pause
        state.

        :param float wave_freq: The floating point waveform frequency in Hz
        ranging from 0.09 to 12.5MHz (one-half the master clock frequency).
        Defaults to 440. Resolution is approximately 0.09Hz.
        :param int wave_phase: The waveform phase offset in 2π Rad // 4096.
          Defaults to 0.
        :param str wave_type: The "sine", "triangle", or "square" wave shape.
          Defaults to "sine".
        :param busio.SPI spi: The `busio.SPI` definition. Defaults to `None`.
        :param board select: The chip select pin designation. Defaults to `None`.
        :param int m_clock: Master clock frequency in Hz. Defaults to 25MHz.
        """

        self._spi = spi  # Define SPI bus; 5Mhz clock frequency
        self._cs = digitalio.DigitalInOut(select)
        self._device = SPIDevice(
            self._spi, self._cs, baudrate=5000000, polarity=1, phase=0
        )

        self._wave_freq = wave_freq
        self._wave_phase = wave_phase
        self._wave_type = wave_type
        self._m_clock = m_clock  # Master clock frequency

        # Initiate register pointers
        self._freq_reg = 0  # FREQ0
        self._phase_reg = 0  # PHASE0

        # Reset and pause the device
        self._pause = True
        self._reset = True
        self._update_control_register()

        # Set the master clock frequency
        self._m_clock = m_clock

    @property
    def wave_freq(self):
        """The frequency output of the wave generator. The wave_freq value can
        differ slightly from the wave generator’s output frequency due to the
        internal conversion needed for loading the DAC counter register."""
        return self._wave_freq

    @wave_freq.setter
    def wave_freq(self, new_wave_freq=440):
        """Set the wave generator output frequency.
        :param float new_wave_freq: The waveform frequency in Hz. Defaults to 440."""
        self._wave_freq = new_wave_freq
        self._wave_freq = max(self._wave_freq, 0)
        self._wave_freq = min(self._wave_freq, self._m_clock // 2)
        self._update_freq_register(self._wave_freq)

    @property
    def raw_wave_freq(self):
        """The frequency output of the wave generator as derived from the
        DAC counter register (FREQREG) value. The raw frequency is based on the
        25Mhz master clock and the 28-bit DAC counter register. The raw wave
        value can differ slightly from wave_freq due to the internal conversion
        needed for loading the DAC counter register."""
        freq_word = int(round(float(self._wave_freq * pow(2, 28)) / self._m_clock))
        return freq_word * (self._m_clock / pow(2, 28))

    @property
    def wave_phase(self):
        """The wave generator integer output phase value."""
        return self._wave_phase

    @wave_phase.setter
    def wave_phase(self, new_wave_phase=0):
        """Set the wave generator output phase value.
        :param int new_wave_phase: The waveform phase offset. Defaults to 0."""
        self._wave_phase = int(new_wave_phase)
        self._wave_phase = max(self._wave_phase, 0)
        self._wave_phase = min(self._wave_phase, 4095)
        self._update_phase_register(self._wave_phase)

    @property
    def wave_type(self):
        """The wave generator waveform type value."""
        return self._wave_type

    @wave_type.setter
    def wave_type(self, new_wave_type="sine"):
        """Set the wave generator waveform type.
        :param str new_wave_type: The waveform type. Defaults to 'sine'."""
        self._wave_type = new_wave_type
        if self._wave_type not in ("triangle", "square", "sine"):
            # Default to sine if type isn't valid
            self._wave_type = "sine"
        self._update_control_register()

    def pause(self):
        """Pause the wave generator and freeze the output at the latest voltage
        level by stopping the internal clock."""
        self._pause = True  # Set the pause bit
        self._update_control_register()

    def start(self):
        """Start the wave generator with current register contents, register
        selection and wave mode setting."""
        self._pause = False  # Clear the clock disable bit
        self._update_control_register()

    def stop(self):
        """Stop the wave generator and reset the output to the midpoint
        voltage level."""
        self._pause = True
        self._reset = True
        self._update_control_register()

    def reset(self):
        """Stop and reset the waveform generator. Pause the master clock.
        Update all registers with default values. Set `sine` wave mode."""
        self._reset = True
        self._pause = True
        self._freq_reg = 0
        self._phase_reg = 0
        self._wave_type = "sine"
        self._update_control_register()

        # While paused, zero the frequency and phase registers
        self._update_freq_register(new_freq=0, register=0)
        self._update_freq_register(new_freq=0, register=1)
        self._update_phase_register(new_phase=0, register=0)
        self._update_phase_register(new_phase=0, register=1)

    def _send_data(self, data):
        """Send a 16-bit word through SPI bus as two 8-bit bytes.
        :param int data: The 16-bit data value to write to the SPI device."""
        data &= 0xFFFF
        tx_msb = data >> 8
        tx_lsb = data & 0xFF

        with self._device:
            self._spi.write(bytes([tx_msb, tx_lsb]))

    def _update_control_register(self):
        """Construct the control register contents per existing local parameters
        then send the new control register word to the waveform generator."""
        if self._reset:
            # Immediately reset before updating register
            self._send_data(0x2100)
            self._reset = False
            # return

        # Set default control register mask value (sine mode, disable reset)
        control_reg = 0x2000

        if self._pause:
            # Disable master clock bit
            control_reg |= 0x0080

        control_reg |= (self._freq_reg & 0x01) << 11  # Frequency register select bit
        control_reg |= (self._phase_reg & 0x01) << 10  # Phase register select bit

        if self._wave_type == "triangle":
            # Set triangle mode
            control_reg |= 0x0002

        if self._wave_type == "square":
            # Set square mode
            control_reg |= 0x0028

        self._send_data(control_reg)

    def _update_freq_register(self, new_freq, register=None):
        """Load inactive register with new frequency value then set the
        register active in order to avoid partial frequency changes. Writes to
        specified register if not `None`.

        :param int new_freq: The new frequency value.
        :param union(int, None) register: The register for the new value; FREG0
        or FREG1. Selects the non-active register if register is `None`."""
        self._wave_freq = new_freq

        if register is None:
            # Automatically toggle to use the inactive register
            self._freq_reg = int(not self._freq_reg)
        else:
            self._freq_reg = register

        freq_word = int(round(float(self._wave_freq * pow(2, 28)) / self._m_clock))

        # Split frequency word into two 14-bit parts; MSB and LSB
        freq_msb = (freq_word & 0xFFFC000) >> 14
        freq_lsb = freq_word & 0x3FFF

        if self._freq_reg == 0:
            # bit-or freq register 0 select (DB15 = 0, DB14 = 1)
            freq_lsb |= 0x4000
            freq_msb |= 0x4000
        else:
            # bit-or freq register 1 select (DB15 = 1, DB14 = 0)
            freq_lsb |= 0x8000
            freq_msb |= 0x8000

        self._send_data(freq_lsb)  # Load new LSB into inactive register
        self._send_data(freq_msb)  # Load new MSB into inactive register

        self._update_control_register()

    def _update_phase_register(self, new_phase, register=None):
        """Load inactive register with new phase value then set the
        register active in order to avoid partial phase changes. Writes to
        specified register if not `None`.

        :param int new_phase: The new phase value.
        :param union(int, None) register: The register for the new value; PHASE0
        or PHASE1. Selects the non-active register if register is `None`.
        """
        self._wave_phase = new_phase

        if register is None:
            # Automatically toggle to use the inactive register
            self._phase_reg = int(not self._phase_reg)
        else:
            self._phase_reg = register

        if self._phase_reg == 0:
            # bit-or phase register 0 select (DB15=1, DB14=1, DB13=0)
            self._wave_phase |= 0xC000
        else:
            # bit-or phase register 1 select (DB15=1, DB14=1, DB13=1)
            self._wave_phase |= 0xE000

        self._send_data(self._wave_phase)

        self._update_control_register()
