// Hikari – WS2812 LED effects for XIAO ESP32C3
//
// Hardware:
//   D6 (GPIO21) – WS2812 data (20 LEDs, 4 rows × 5 columns)
//   D3 (GPIO5)  – Pushbutton, active-low (internal pull-up)
//
// Flash:
//   tinygo flash -target=xiao-esp32c3 -stack-size=8KB .

package main

import (
	"image/color"
	"machine"
	"time"

	"tinygo.org/x/drivers/ws2812"
)

// ─── constants ────────────────────────────────────────────────────────────────

const (
	numLEDs    = 20
	numRows    = 4
	numCols    = 5
	numEffects = 7
	frameDelay = 20 * time.Millisecond
	debounce   = 200 * time.Millisecond
)

// Pin assignments for XIAO ESP32C3
const (
	pinLED = machine.D6 // WS2812 data line
	pinBtn = machine.D3 // Button (pulled up, LOW when pressed)
)

// ─── globals ──────────────────────────────────────────────────────────────────

var (
	leds  [numLEDs]color.RGBA
	strip ws2812.Device

	// xorshift32 PRNG – seeded with a compile-time constant
	rngState uint32 = 0xCAFE1234
)

// ─── colour helpers ───────────────────────────────────────────────────────────

// wheel maps 0-255 → smooth full-hue spectrum (R→G→B→R)
func wheel(pos uint8) color.RGBA {
	switch {
	case pos < 85:
		return color.RGBA{R: pos * 3, G: 255 - pos*3, B: 0}
	case pos < 170:
		pos -= 85
		return color.RGBA{R: 255 - pos*3, G: 0, B: pos * 3}
	default:
		pos -= 170
		return color.RGBA{R: 0, G: pos * 3, B: 255 - pos*3}
	}
}

// dim scales a colour by brightness (0-255)
func dim(c color.RGBA, brightness uint8) color.RGBA {
	return color.RGBA{
		R: uint8(uint16(c.R) * uint16(brightness) >> 8),
		G: uint8(uint16(c.G) * uint16(brightness) >> 8),
		B: uint8(uint16(c.B) * uint16(brightness) >> 8),
	}
}

// idx converts grid coordinates to LED index (simple row-major)
func idx(row, col int) int { return row*numCols + col }

// clear sets all LEDs to off
func clear() {
	for i := range leds {
		leds[i] = color.RGBA{}
	}
}

// rand returns the next pseudo-random uint32
func rand() uint32 {
	rngState ^= rngState << 13
	rngState ^= rngState >> 17
	rngState ^= rngState << 5
	return rngState
}

// ─── effect 1 – Rainbow ───────────────────────────────────────────────────────
// All LEDs show the same hue; the hue cycles slowly through the full spectrum.

func effectRainbow(step uint32) {
	c := wheel(uint8(step >> 2)) // full cycle ≈ 20 s
	for i := range leds {
		leds[i] = c
	}
}

// ─── effect 2 – Row sweep ─────────────────────────────────────────────────────
// One row at a time is lit; the active row sweeps downward while the hue drifts.

func effectRowSweep(step uint32) {
	clear()
	row := int(step/40) % numRows
	c := wheel(uint8(step >> 1))
	for col := 0; col < numCols; col++ {
		leds[idx(row, col)] = c
	}
}

// ─── effect 3 – Column sweep ──────────────────────────────────────────────────
// One column at a time is lit; the active column sweeps rightward.

func effectColSweep(step uint32) {
	clear()
	col := int(step/40) % numCols
	c := wheel(uint8(step >> 1))
	for row := 0; row < numRows; row++ {
		leds[idx(row, col)] = c
	}
}

// ─── effect 4 – Stars ─────────────────────────────────────────────────────────
// Random LEDs glow in random colours then fade out, one by one.

type star struct {
	pos uint8 // LED index
	hue uint8 // colour on the wheel
	br  uint8 // current brightness (0-255)
	dir int8  // +1 rising, -1 falling
}

const (
	numStars = 8
	starStep = 7 // brightness change per frame (~35 frames to full glow)
)

var (
	starList [numStars]star
	starsOK  bool
)

func initStars() {
	for i := range starList {
		starList[i] = star{
			pos: uint8(rand() % numLEDs),
			hue: uint8(rand()),
			br:  uint8(i * (255 / numStars)), // stagger so they don't sync
			dir: 1,
		}
	}
	starsOK = true
}

func effectStars(_ uint32) {
	if !starsOK {
		initStars()
	}
	clear()
	for i := range starList {
		s := &starList[i]

		// Advance brightness
		nb := int16(s.br) + int16(s.dir)*starStep
		switch {
		case nb >= 255:
			s.br = 255
			s.dir = -1
		case nb <= 0:
			// Star died – pick a new random one
			s.br = 0
			s.dir = 1
			s.pos = uint8(rand() % numLEDs)
			s.hue = uint8(rand())
		default:
			s.br = uint8(nb)
		}

		leds[s.pos] = dim(wheel(s.hue), s.br)
	}
}

// ─── effect 5 – Breathing ─────────────────────────────────────────────────────
// All LEDs pulse in unison with a triangular brightness envelope; the hue
// drifts imperceptibly slowly.

func effectBreathing(step uint32) {
	t := uint8(step) // 0-255, period = 256 frames ≈ 5 s
	var br uint8
	if t < 128 {
		br = t * 2 // 0 → 254
	} else {
		br = (255 - t) * 2 // 254 → 0
	}
	hue := uint8(step >> 7) // hue changes once per breath cycle
	for i := range leds {
		leds[i] = dim(wheel(hue), br)
	}
}

// ─── effect 6 – Rainbow rows ──────────────────────────────────────────────────
// Each row shows a distinct hue (separated by 64/256 of the wheel); all hues
// advance together, making the whole grid slowly rotate through colours.

func effectRainbowRows(step uint32) {
	base := uint8(step >> 2) // same drift speed as effect 1
	for row := 0; row < numRows; row++ {
		c := wheel(base + uint8(row*64))
		for col := 0; col < numCols; col++ {
			leds[idx(row, col)] = c
		}
	}
}

// ─── effect 7 – Column wave ───────────────────────────────────────────────────
// A bright column chases across the grid with a fading two-column tail.

func effectWave(step uint32) {
	clear()
	head := int(step/8) % numCols // advances one column every 8 frames ≈ 160 ms
	hue := uint8(step >> 1)

	// Brightness per distance behind the head
	bri := [numCols]uint8{255, 110, 35, 0, 0}

	for col := 0; col < numCols; col++ {
		dist := (head - col + numCols) % numCols
		if dist >= numCols || bri[dist] == 0 {
			continue
		}
		c := dim(wheel(hue+uint8(col*12)), bri[dist])
		for row := 0; row < numRows; row++ {
			leds[idx(row, col)] = c
		}
	}
}

// ─── main ─────────────────────────────────────────────────────────────────────

func main() {
	// Initialise WS2812
	pinLED.Configure(machine.PinConfig{Mode: machine.PinOutput})
	strip = ws2812.New(pinLED)

	// Initialise button with internal pull-up
	pinBtn.Configure(machine.PinConfig{Mode: machine.PinInputPullup})

	var (
		effect    int
		step      uint32
		prevBtn   bool
		lastPress time.Time
	)

	for {
		// ── Button: detect falling edge with debounce ─────────────────────
		btnDown := !pinBtn.Get() // LOW = pressed (active-low)
		if btnDown && !prevBtn && time.Since(lastPress) > debounce {
			effect = (effect + 1) % numEffects
			lastPress = time.Now()
			starsOK = false // force stars to reinitialise next time
		}
		prevBtn = btnDown

		// ── Render current effect ─────────────────────────────────────────
		switch effect {
		case 0:
			effectRainbow(step)
		case 1:
			effectRowSweep(step)
		case 2:
			effectColSweep(step)
		case 3:
			effectStars(step)
		case 4:
			effectBreathing(step)
		case 5:
			effectRainbowRows(step)
		case 6:
			effectWave(step)
		}

		strip.WriteColors(leds[:])
		step++
		time.Sleep(frameDelay)
	}
}
