# glow.property
# run on MicroPython on RP2040 MCU with 4x LED 8x8 matrix

from machine import Pin  # type: ignore
import neopixel  # type: ignore
from time import sleep_ms  # type: ignore
from random import randint as RI, uniform, choice

COMPRESSED_GLYPHS_FILE = "glyphV3.bin"

# Pin configuration and NeoPixel setup
NEO_PIN = Pin(14, Pin.OUT)
NUM_PIXELS = 256  # Total number of pixels in the NeoPixel matrix
NEO = neopixel.NeoPixel(NEO_PIN, NUM_PIXELS)

GRID_SIZE = 16

# Glyph and file configuration
BYTES_PER_GLYPH = NUM_PIXELS // 8  # 32 bytes per glyph

# RGB value ranges
MIN_RGB_VALUE = 4
MAX_RGB_VALUE = 32
FADE_STEP_SIZE = 2

# Parameters for random RGB generation
DIM_FACTOR = 0.5  # Factor to dim a channel (e.g., 0.5 = 50% dim)
BOOST_FACTOR = 1.5  # Factor to boost a channel (e.g., 1.5 = 150% boost)
SATURATION_BLEND_MIN = 0.1  # Minimum blend factor for desaturation
SATURATION_BLEND_MAX = 2.0  # Maximum blend factor for desaturation

# random rotations
PROB_0 = 0.5
PROB_90 = 0.25
PROB_180 = 0.15
PROB_270 = 0.1

MIN_HOLD_TIME = 2500
MAX_HOLD_TIME = 6000

def translate_pixel_index(index):
    """Translate the pixel index for the 16x16 panel with 4 8x8 matrices."""
    matrix_size = 8  # Each matrix is 8x8
    panel_width = 16  # The panel is 16 columns wide (2 matrices wide)

    # Calculate panel row and column
    panel_row = index // panel_width  # Determine the row within the 16x16 panel
    panel_col = index % panel_width  # Determine the column within the 16x16 panel

    # Determine the correct matrix and offset based on wiring
    if panel_row < matrix_size:  # Top matrices (rows 0–7)
        if panel_col < matrix_size:  # Columns 0–7 (top-left matrix)
            translated_index = panel_row * matrix_size + panel_col
        else:  # Columns 8–15 (top-right matrix)
            translated_index = panel_row * matrix_size + (panel_col - matrix_size) + 64
    else:  # Bottom matrices (rows 8–15)
        if panel_col < matrix_size:  # Columns 0–7 (bottom-left matrix)
            translated_index = (panel_row - matrix_size) * matrix_size + panel_col + 192
        else:  # Columns 8–15 (bottom-right matrix)
            translated_index = (
                (panel_row - matrix_size) * matrix_size
                + (panel_col - matrix_size)
                + 128
            )

    return translated_index

def count_glyphs_in_file(file_path):
    """Calculate the number of glyphs in the binary file."""
    with open(file_path, "rb") as f:
        f.seek(0, 2)  # Move to the end of the file
        file_size = f.tell()
    return file_size // BYTES_PER_GLYPH

NUM_GLYPHS = count_glyphs_in_file(COMPRESSED_GLYPHS_FILE)
print(f"Number of glyphs in {COMPRESSED_GLYPHS_FILE}: {NUM_GLYPHS}")

def read_glyph(file_path, index):
    """Read and decompress a single glyph from the binary file."""
    with open(file_path, "rb") as f:
        f.seek(index * BYTES_PER_GLYPH)  # Seek to the glyph's position
        glyph_bytes = f.read(BYTES_PER_GLYPH)

    # Convert bytes to binary (pixel data)
    glyph_code = [(byte >> (7 - i)) & 1 for byte in glyph_bytes for i in range(8)]
    return glyph_code

def rotate_glyph(glyph_code, rotation):
    """Rotate the glyph code by 0, 90, 180, or 270 degrees."""
    rotated = [0] * NUM_PIXELS
    for i, val in enumerate(glyph_code):
        row, col = divmod(i, GRID_SIZE)
        if rotation == 90:
            new_row, new_col = col, GRID_SIZE - 1 - row
        elif rotation == 180:
            new_row, new_col = GRID_SIZE - 1 - row, GRID_SIZE - 1 - col
        elif rotation == 270:
            new_row, new_col = GRID_SIZE - 1 - col, row
        else:  # 0 degrees
            new_row, new_col = row, col
        rotated[new_row * GRID_SIZE + new_col] = val
    return rotated

def clamp(value, min_value, max_value):
    """Ensure the value stays within the specified range."""
    return max(min_value, min(value, max_value))

def generate_random_rgb():
    """Generate a random RGB value with dimming, boosting, and desaturation."""
    # Start with random values in the full range
    r = RI(MIN_RGB_VALUE, MAX_RGB_VALUE)
    g = RI(MIN_RGB_VALUE, MAX_RGB_VALUE)
    b = RI(MIN_RGB_VALUE, MAX_RGB_VALUE)
    
    # Adjust saturation to allow pastel colors
    reduce_saturation = uniform(SATURATION_BLEND_MIN, SATURATION_BLEND_MAX)  # Blend factor for desaturation
    gray = int((r + g + b) / 3 * reduce_saturation)
    r = clamp(int(r * reduce_saturation + gray * (1 - reduce_saturation)), MIN_RGB_VALUE, MAX_RGB_VALUE)
    g = clamp(int(g * reduce_saturation + gray * (1 - reduce_saturation)), MIN_RGB_VALUE, MAX_RGB_VALUE)
    b = clamp(int(b * reduce_saturation + gray * (1 - reduce_saturation)), MIN_RGB_VALUE, MAX_RGB_VALUE)

    # Randomly adjust one channel (dim or boost)
    channel_to_modify = choice(['dim', 'boost', 'none'])  # Allow dimming, boosting, or leaving unchanged
    modified_channel = RI(1, 3)
    if channel_to_modify == 'dim':
        if modified_channel == 1:
            r = clamp(int(r * DIM_FACTOR), MIN_RGB_VALUE, MAX_RGB_VALUE)
        elif modified_channel == 2:
            g = clamp(int(g * DIM_FACTOR), MIN_RGB_VALUE, MAX_RGB_VALUE)
        elif modified_channel == 3:
            b = clamp(int(b * DIM_FACTOR), MIN_RGB_VALUE, MAX_RGB_VALUE)
    elif channel_to_modify == 'boost':
        if modified_channel == 1:
            r = clamp(int(r * BOOST_FACTOR), MIN_RGB_VALUE, MAX_RGB_VALUE)
        elif modified_channel == 2:
            g = clamp(int(g * BOOST_FACTOR), MIN_RGB_VALUE, MAX_RGB_VALUE)
        elif modified_channel == 3:
            b = clamp(int(b * BOOST_FACTOR), MIN_RGB_VALUE, MAX_RGB_VALUE)

    return r, g, b

def fade_to_new_glyph(target_colors, fade_step_size=1):
    """Gradually transition NeoPixel colors to new target values."""
    completed_pixels = 0
    while completed_pixels < NUM_PIXELS * 3:
        completed_pixels = 0
        for i in range(NUM_PIXELS):
            translated_index = translate_pixel_index(i)
            current_color = NEO[translated_index]
            target_color = target_colors[i]

            new_color = []
            for current, target in zip(current_color, target_color):
                if current < target:
                    # Step up, but don't overshoot the target
                    next_value = min(current + fade_step_size, target)
                elif current > target:
                    # Step down, but don't overshoot the target
                    next_value = max(current - fade_step_size, target)
                else:
                    # Already at target
                    next_value = target
                    completed_pixels += 1  # Increment when the channel matches

                new_color.append(next_value)

            NEO[translated_index] = tuple(new_color)
        NEO.write()

def display_random_glyph():
    """Load and display a random glyph from the file."""
    glyph_code = read_glyph(COMPRESSED_GLYPHS_FILE, RI(0, NUM_GLYPHS - 1))

    # Randomly rotate the glyph
    rotation_angles = [0, 90, 180, 270]
    rotation_probabilities = [PROB_0, PROB_90, PROB_180, PROB_270]

    # Generate a cumulative probability distribution
    cumulative_probabilities = [sum(rotation_probabilities[:i+1]) for i in range(len(rotation_probabilities))]

    # Generate a random value between 0 and 1
    rand_value = uniform(0, 1)

    # Select the rotation angle based on the random value
    for angle, cumulative in zip(rotation_angles, cumulative_probabilities):
        if rand_value <= cumulative:
            rotation = angle
            break
    
    glyph_code = rotate_glyph(glyph_code, rotation)

    target_colors = [
        generate_random_rgb() if glyph_code[i] else (0, 0, 0) for i in range(NUM_PIXELS)
    ]
    fade_to_new_glyph(target_colors, FADE_STEP_SIZE)

# Initialize NeoPixels with a dim glow
NEO.fill((3, 3, 3))
NEO.write()

# Main loop to display random glyphs
while True:
    display_random_glyph()
    sleep_ms(RI(MIN_HOLD_TIME, MAX_HOLD_TIME))
