# SPDX-FileCopyrightText: 2022 Jeff Epler, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""Drive a 7-segment display entirely from the PIO peripheral

Each segment is driven with a 'breathing' waveform that runs at its own pace.
It also demonstrates the use of `asyncio` to perform multiple tasks.

This example is designed for a Raspberry Pi Pico and bare LED display. For
simplicity, it is wired without any current limiting resistors, instead relying
on a combination of the RP2040's pin drive strength and the 1/45 duty cycle to
limit LED current to an acceptable level, and longevity of the display was not
a priority.

Before integrating a variant of this example code in a project, evaluate
whether your design needs to add current-limiting resistors.

https://www.adafruit.com/product/4864
https://www.adafruit.com/product/865

Wiring:
 * Pico GP15 to LED matrix 1 (E SEG)
 * Pico GP14 to LED matrix 2 (D SEG)
 * Pico GP13 to LED matrix 3 (DP SEG)
 * Pico GP12 to LED matrix 4 (C SEG)
 * Pico GP11 to LED matrix 5 (G SEG)
 * Pico GP10 to LED matrix 6 (COM4)
 * Pico GP9 to LED matrix 7 (COLON COM)
 * Pico GP22 to LED matrix 8 (COLON SEG)
 * Pico GP21 to LED matrix 9 (B SEG)
 * Pico GP20 to LED matrix 10 (COM3)
 * Pico GP19 to LED matrix 11 (COM2)
 * Pico GP18 to LED matrix 12 (F SEG)
 * Pico GP17 to LED matrix 13 (A SEG)
 * Pico GP16 to LED matrix 14 (COM1)
"""

import array
import asyncio
import random

import board
import rp2pio
from ulab import numpy as np

import adafruit_pioasm

_pio_source = """
    mov pins, null     ; turn all pins off
    pull
    out pindirs, {n}   ; set the new direction
    pull
    out pins, {n}      ; set the new values
    pull
    out y, 32
delay:
    jmp y--, delay
    """

# Display Pins 1-7 are GP 15-9 [need to re-wire 9]
# Display Pins 8-12 are GP 22-16 [need to re-wire 22]
# GP#       Display#    Function
# 15 (+ 6)   1          E SEG
# 14 (+ 5)   2          D SEG
# 13 (+ 4)   3          DP SEG
# 12 (+ 3)   4          C SEG
# 11 (+ 2)   5          G SEG
# 10 (+ 1)   6          COM4
#  9 (+ 0)   7          COLON COM
# 22 (+13)   8          COLON SEG
# 21 (+12)   9          B SEG
# 20 (+11)  10          COM3
# 19 (+10)  11          COM2
# 18 (+ 9)  12          F SEG
# 17 (+ 8)  13          A SEG
# 16 (+ 7)  14          COM1

COM1_WT = 1 << 7
COM2_WT = 1 << 10
COM3_WT = 1 << 11
COM4_WT = 1 << 1
COMC_WT = 1 << 0

SEGA_WT = 1 << 8
SEGB_WT = 1 << 12
SEGC_WT = 1 << 3
SEGD_WT = 1 << 5
SEGE_WT = 1 << 6
SEGF_WT = 1 << 9
SEGG_WT = 1 << 2

SEGDP_WT = 1 << 4
SEGCOL_WT = 1 << 13

ALL_COM = COM1_WT | COM2_WT | COM3_WT | COM4_WT | COMC_WT

SEG_WT = [
    SEGA_WT,
    SEGB_WT,
    SEGC_WT,
    SEGD_WT,
    SEGE_WT,
    SEGF_WT,
    SEGG_WT,
    SEGDP_WT,
    SEGCOL_WT,
]
COM_WT = [COM1_WT, COM2_WT, COM3_WT, COM4_WT, COMC_WT]

DIGITS = [
    0b0111111,  # 0
    0b0000110,  # 1
    0b1011011,  # 2
    0b1001111,  # 3
    0b1100110,  # 4
    0b1101101,  # 5
    0b1111100,  # 6
    0b0000111,  # 7
    0b1111111,  # 8
    0b1101111,  # 9
]


def make_digit_wt(v):
    val = ALL_COM
    seg = DIGITS[v]
    for i in range(8):
        if seg & (1 << i):
            val |= SEG_WT[i]
    return val


class LedFader:
    def __init__(self, first_pin, pin_count, cathode_weights, anode_weights, levels=64):
        self._cathode_weights = cathode_weights
        self._anode_weights = anode_weights
        self._stream = array.array("L", [0, 0, 1]) * (1 + len(cathode_weights) * len(anode_weights))
        self._levels = levels
        self._max_count = levels * len(self)
        self._total = len(self)

        program = adafruit_pioasm.Program(_pio_source.format(n=pin_count))
        self._sm = rp2pio.StateMachine(
            program.assembled,
            frequency=125_000_000,
            first_out_pin=first_pin,
            out_pin_count=14,
            auto_pull=True,
            pull_threshold=14,
            **program.pio_kwargs,
        )
        print(f"Note: approximate refresh rate {self._sm.frequency / self._max_count:.0f}Hz")
        self._sm.background_write(loop=self._stream)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.deinit()

    def deinit(self):
        self._sm.deinit()

    def __setitem__(self, i, v):
        if not 0 <= v < self._levels:
            raise ValueError()

        c = i % len(self._cathode_weights)
        r = i // len(self._cathode_weights)
        if not v:
            self._total = self._total - self._stream[3 * i + 2] + 1
            self._stream[3 * i] = 0
            self._stream[3 * i + 1] = 0
            self._stream[3 * i + 2] = 1
        else:
            self._total = self._total - self._stream[3 * i + 2] + v
            self._stream[3 * i] = self._cathode_weights[c] | self._anode_weights[r]
            self._stream[3 * i + 1] = self._cathode_weights[c]
            self._stream[3 * i + 2] = v
        self._stream[3 * len(self) + 2] = self._max_count - self._total

    def __len__(self):
        return len(self._stream) // 3 - 1


class CyclicSignal:
    def __init__(self, data, phase=0):
        self._data = data
        self._phase = 0
        self.phase = phase
        self._scale = len(self._data) - 1

    @property
    def phase(self):
        return self._phase

    @phase.setter
    def phase(self, value):
        self._phase = value % 1

    @property
    def value(self):
        idxf = self._phase * len(self._data)
        idx = int(idxf)
        frac = idxf % 1
        idx1 = (idx + 1) % len(self._data)
        val = self._data[idx]
        val1 = self._data[idx1]
        return val + (val1 - val) * frac

    def advance(self, delta):
        self._phase = (self._phase + delta) % 1


sine = (np.sin(np.linspace(0, 2 * np.pi, 50, endpoint=False)) * 0.5 + 0.5) ** 2.2 * 64


async def segment_throbber(c, i):
    signal = CyclicSignal(sine, random.random())
    velocity = random.random() * 0.04 + 0.005

    while True:
        signal.advance(velocity)
        c[i] = int(signal.value)
        await asyncio.sleep(0)


async def main():
    with LedFader(
        board.GP9,
        14,
        (
            SEGA_WT,
            SEGB_WT,
            SEGC_WT,
            SEGD_WT,
            SEGE_WT,
            SEGF_WT,
            SEGG_WT,
            SEGDP_WT,
            SEGCOL_WT,
        ),
        (COM1_WT, COM2_WT, COM3_WT, COM4_WT, COMC_WT),
    ) as c:
        await asyncio.gather(*(segment_throbber(c, i) for i in range(len(c))))


asyncio.run(main())
