package ui

import (
	"strings"
	"time"
)

// sandDriver runs a falling-sand cellular automaton on the dot grid. Each
// frame, new grains drop from the top — colored by which spectrum band
// triggered them — and existing grains fall straight down or, if blocked,
// slide diagonally onto piles. Bass adds a small "shake" by occasionally
// nudging a row sideways, which destabilises slopes and triggers little
// avalanches; loud passages keep the panel actively pouring.
type sandDriver struct {
	grid             []int8 // 0 = empty; 1 = green tier; 2 = yellow; 3 = red
	dotRows, dotCols int
	rng              uint64
	prevBass         float64 // for detecting bass transients that bump the bed

	// Explosion phase: when triggered, all grains become ballistic particles
	// for a few dozen frames. While particles exist, normal spawning, bumping,
	// and falling are suspended; the grid is re-derived from particle positions
	// each tick so the existing renderer needs no changes.
	particles    []sandParticle
	explosionTTL int
}

// sandParticle is one grain in mid-flight during the explosion sequence. Sub-
// dot positions and a real velocity per particle let us see the rise, peak,
// scatter, and fall across many frames instead of a single teleport.
type sandParticle struct {
	x, y   float64
	vx, vy float64
	tier   int8
}

func newSandDriver() visModeDriver {
	return &sandDriver{rng: 0x5A4D5A4D5A4D}
}

func (*sandDriver) AnalysisSpec(*Visualizer) VisAnalysisSpec {
	return spectrumAnalysisSpec(DefaultSpectrumBands)
}

func (d *sandDriver) ensure(rows, cols int) {
	if rows == d.dotRows && cols == d.dotCols && len(d.grid) == rows*cols {
		return
	}
	d.grid = make([]int8, rows*cols)
	d.dotRows = rows
	d.dotCols = cols
}

// rand01 returns a deterministic pseudo-random float in [0,1).
func (d *sandDriver) rand01() float64 {
	d.rng = d.rng*6364136223846793005 + 1442695040888963407
	return float64((d.rng>>33)%1000) / 1000.0
}

func (d *sandDriver) Tick(v *Visualizer, ctx VisTickContext) {
	defaultDriverTick(v, ctx, d.AnalysisSpec(v))
	if ctx.OverlayActive {
		return
	}
	dotRows := v.Rows * 4
	dotCols := PanelWidth * 2
	if dotRows < 4 || dotCols < 4 {
		return
	}
	d.ensure(dotRows, dotCols)

	bands := v.SmoothedBands()
	bass := bandAvg(bands, 0, max(1, len(bands)/3))
	bandCount := len(bands)

	// EXPLOSION PHASE: while particles are still in flight we suspend the
	// normal sand simulation entirely and just animate the burst. The grid is
	// re-derived from particle positions each tick so the renderer is
	// unchanged.
	if d.explosionTTL > 0 || len(d.particles) > 0 {
		d.tickExplosion()
		d.prevBass = bass
		return
	}

	// Spawn grains: each band emits at a column proportional to its index, with
	// a small spread so neighbouring grains don't stack into a single tower.
	if bandCount > 0 {
		for b := 0; b < bandCount; b++ {
			level := bands[b]
			if level < 0.10 {
				continue
			}
			// Probability of emitting this frame scales with band level.
			if d.rand01() > level*0.85 {
				continue
			}
			centre := (b*2 + 1) * dotCols / (2 * bandCount)
			spread := dotCols / (bandCount * 2)
			if spread < 1 {
				spread = 1
			}
			x := centre + int(d.rand01()*float64(2*spread)) - spread
			if x < 0 {
				x = 0
			}
			if x >= dotCols {
				x = dotCols - 1
			}
			// Tier mapping: low bands → red (hot bass), mid → yellow, high → green.
			var tier int8 = 1
			switch {
			case b < bandCount/3:
				tier = 3 // red
			case b < 2*bandCount/3:
				tier = 2 // yellow
			default:
				tier = 1 // green
			}
			if d.grid[0*dotCols+x] == 0 {
				d.grid[0*dotCols+x] = tier
			}
		}
	}

	// Bass-driven bumps. Three regimes layered together:
	//
	//   0. EXPLOSION — when the bed has accumulated past ~40% capacity, the
	//      next bass kick blows everything sky-high. Grains are launched far
	//      enough that most fly off the top of the panel and disappear; the
	//      simulation then starts over from an empty bed.
	//   1. TRANSIENT BUMP — rising-edge of bass, fires once per kick. This is
	//      the speaker-cone slap: violent vertical lift across the WHOLE bed,
	//      with grains thrown high and far. Closer to the bottom = more lift,
	//      but every grain has a chance to fly.
	//   2. SUSTAINED RUMBLE — when bass stays high, every frame jitters
	//      grains a small amount so the bed never settles still during a
	//      heavy bass passage. This is what makes the sand keep dancing on
	//      sustained kicks instead of just popping once and freezing.
	delta := bass - d.prevBass
	d.prevBass = bass

	// 0. Explosion check: fires before the normal bump branches so the
	// grid is cleared *instead* of being merely shaken when overfilled.
	if delta > 0.06 && bass > 0.15 {
		fill := 0
		for _, g := range d.grid {
			if g != 0 {
				fill++
			}
		}
		if float64(fill)/float64(len(d.grid)) > 0.30 {
			// Convert every grain into a ballistic particle and enter the
			// explosion phase. The simulation will animate the burst over
			// the next few dozen frames, then resume.
			d.startExplosion()
			return
		}
	}

	// 1. Transient bump.
	if delta > 0.06 && bass > 0.15 {
		strength := delta*3.5 + bass*0.8
		if strength > 1.4 {
			strength = 1.4
		}
		// Process top-down so a lifted grain isn't visited again this frame.
		for y := 0; y < dotRows; y++ {
			depthFrac := float64(y) / float64(max(1, dotRows-1)) // 0 at top, 1 at bottom
			// Probability close to 1 near the bottom on a strong kick — the bed
			// effectively detonates upward.
			liftProb := strength * (0.30 + 0.70*depthFrac)
			if liftProb > 0.95 {
				liftProb = 0.95
			}
			// Lift height scales with strength AND depth — a 1.0-strength kick
			// can throw a bottom grain ~10 dot rows (half the panel). Multiply
			// by ~7 to feel like a speaker cone, not a soft tap.
			liftMax := 2 + int(strength*7.0*(0.4+0.6*depthFrac))
			// Lateral spread also scales — sand sprays out, not just up.
			jitterRange := 1 + int(strength*5.0)
			for x := 0; x < dotCols; x++ {
				g := d.grid[y*dotCols+x]
				if g == 0 {
					continue
				}
				if d.rand01() > liftProb {
					continue
				}
				lift := 1 + int(d.rand01()*float64(liftMax))
				jitter := int(d.rand01()*float64(2*jitterRange+1)) - jitterRange
				ny := y - lift
				nx := x + jitter
				if ny < 0 {
					ny = 0
				}
				if nx < 0 {
					nx = 0
				}
				if nx >= dotCols {
					nx = dotCols - 1
				}
				if d.grid[ny*dotCols+nx] == 0 {
					d.grid[ny*dotCols+nx] = g
					d.grid[y*dotCols+x] = 0
				}
			}
		}
	}

	// 2. Sustained rumble — applies whenever bass is high, regardless of
	// transient. Smaller per-grain motion but applied every frame, so the bed
	// keeps churning during a held kick.
	if bass > 0.30 {
		// Strength climbs with how far above the threshold we are.
		rumble := (bass - 0.30) * 1.8
		if rumble > 0.6 {
			rumble = 0.6
		}
		// Only churn the bottom half — that's what's coupled to the speaker.
		minY := dotRows / 2
		for y := minY; y < dotRows; y++ {
			depthFrac := float64(y-minY) / float64(max(1, dotRows-1-minY))
			prob := rumble * (0.15 + 0.55*depthFrac)
			for x := 0; x < dotCols; x++ {
				g := d.grid[y*dotCols+x]
				if g == 0 {
					continue
				}
				if d.rand01() > prob {
					continue
				}
				lift := 1 + int(d.rand01()*2.0) // 1..2
				jitter := int(d.rand01()*5) - 2 // -2..+2
				ny := y - lift
				nx := x + jitter
				if ny < 0 {
					ny = 0
				}
				if nx < 0 {
					nx = 0
				}
				if nx >= dotCols {
					nx = dotCols - 1
				}
				if d.grid[ny*dotCols+nx] == 0 {
					d.grid[ny*dotCols+nx] = g
					d.grid[y*dotCols+x] = 0
				}
			}
		}
	}

	// Falling pass: bottom-up so a grain we just moved into y+1 isn't moved
	// twice this frame. Grains at the bottom row leave the grid.
	for y := dotRows - 2; y >= 0; y-- {
		// Alternate horizontal scan direction each frame so piles don't lean
		// permanently to one side from diagonal-left-first bias.
		leftFirst := (v.frame % 2) == 0
		startX, endX, stepX := 0, dotCols, 1
		if !leftFirst {
			startX, endX, stepX = dotCols-1, -1, -1
		}
		for x := startX; x != endX; x += stepX {
			g := d.grid[y*dotCols+x]
			if g == 0 {
				continue
			}
			// Try straight down.
			if d.grid[(y+1)*dotCols+x] == 0 {
				d.grid[(y+1)*dotCols+x] = g
				d.grid[y*dotCols+x] = 0
				continue
			}
			// Diagonal: pick left or right first based on parity for symmetry.
			diag1, diag2 := -1, 1
			if d.rand01() < 0.5 {
				diag1, diag2 = 1, -1
			}
			for _, dx := range [2]int{diag1, diag2} {
				nx := x + dx
				if nx < 0 || nx >= dotCols {
					continue
				}
				if d.grid[(y+1)*dotCols+nx] == 0 {
					d.grid[(y+1)*dotCols+nx] = g
					d.grid[y*dotCols+x] = 0
					break
				}
			}
		}
	}

	// Floor: grains in the very bottom row drift off-screen at a slow rate so
	// the grid doesn't fill up over time. Without this, a long-running session
	// gradually packs every cell.
	for x := 0; x < dotCols; x++ {
		if d.grid[(dotRows-1)*dotCols+x] != 0 && d.rand01() < 0.04 {
			d.grid[(dotRows-1)*dotCols+x] = 0
		}
	}
}

func (*sandDriver) TickInterval(_ *Visualizer, ctx VisTickContext) time.Duration {
	return defaultDriverTickInterval(ctx)
}

func (d *sandDriver) OnEnter(*Visualizer) {
	d.grid = nil
	d.dotRows = 0
	d.dotCols = 0
	d.prevBass = 0
	d.particles = nil
	d.explosionTTL = 0
}

func (*sandDriver) OnLeave(*Visualizer) {}

// startExplosion converts every grain on the grid into a ballistic particle
// with a random outward velocity, then enters the multi-frame explosion
// phase. Bottom grains carry slightly more upward energy (they're closer to
// the speaker cone), so the burst peaks naturally from below.
func (d *sandDriver) startExplosion() {
	dotRows, dotCols := d.dotRows, d.dotCols
	d.particles = d.particles[:0]
	for y := 0; y < dotRows; y++ {
		depthFrac := float64(y) / float64(max(1, dotRows-1)) // 0=top, 1=bottom
		for x := 0; x < dotCols; x++ {
			g := d.grid[y*dotCols+x]
			if g == 0 {
				continue
			}
			d.grid[y*dotCols+x] = 0
			// Vertical: -3..-9 dot/frame upward, biased so bottom grains fly
			// fastest. Lateral: ±4 dot/frame for a wide spray.
			vy := -(2.0 + d.rand01()*5.0 + depthFrac*2.0)
			vx := (d.rand01() - 0.5) * 8.0
			d.particles = append(d.particles, sandParticle{
				x:    float64(x),
				y:    float64(y),
				vx:   vx,
				vy:   vy,
				tier: g,
			})
		}
	}
	// Generous TTL — particles will mostly fall off earlier; the natural end
	// is when the particles list empties. TTL is the safety cap.
	d.explosionTTL = 80
}

// tickExplosion advances all in-flight particles one frame: gravity pulls
// them down, drag slows lateral motion, and any particle that leaves the
// panel through any edge is removed. The grid is fully rebuilt from the
// surviving particles so the renderer can stay unchanged.
func (d *sandDriver) tickExplosion() {
	const gravity = 0.50
	const drag = 0.985
	dotRows, dotCols := d.dotRows, d.dotCols

	for i := range d.grid {
		d.grid[i] = 0
	}

	live := d.particles[:0]
	for _, p := range d.particles {
		p.vy += gravity
		p.vx *= drag
		p.x += p.vx
		p.y += p.vy
		ix := int(p.x)
		iy := int(p.y)
		if iy < 0 || iy >= dotRows || ix < 0 || ix >= dotCols {
			// Off panel — particle is gone (continued ballistic flight beyond
			// our viewport doesn't matter visually).
			continue
		}
		d.grid[iy*dotCols+ix] = p.tier
		live = append(live, p)
	}
	d.particles = live

	if d.explosionTTL > 0 {
		d.explosionTTL--
	}
	if len(d.particles) == 0 {
		d.explosionTTL = 0
	}
}

func (d *sandDriver) Render(v *Visualizer) string {
	height := v.Rows
	dotRows := height * 4
	dotCols := PanelWidth * 2
	if dotRows < 4 || dotCols < 4 {
		return strings.Repeat("\n", max(0, height-1))
	}
	if d.dotRows != dotRows || d.dotCols != dotCols || len(d.grid) != dotRows*dotCols {
		d.ensure(dotRows, dotCols)
	}

	lines := make([]string, height)
	for row := 0; row < height; row++ {
		var sb, run strings.Builder
		tag := -1
		for col := 0; col < PanelWidth; col++ {
			var braille rune = '⠀'
			cellTag := -1
			for dr := 0; dr < 4; dr++ {
				for dc := 0; dc < 2; dc++ {
					y := row*4 + dr
					x := col*2 + dc
					g := d.grid[y*dotCols+x]
					if g == 0 {
						continue
					}
					// Tier: 1=green(0), 2=yellow(1), 3=red(2)
					t := int(g) - 1
					braille |= brailleBit[dr][dc]
					if t > cellTag {
						cellTag = t
					}
				}
			}
			if cellTag < 0 {
				cellTag = 0
			}
			if cellTag != tag {
				flushStyleRun(&sb, &run, tag)
				tag = cellTag
			}
			run.WriteRune(braille)
		}
		flushStyleRun(&sb, &run, tag)
		lines[row] = sb.String()
	}
	return strings.Join(lines, "\n")
}
