package main

import (
	"fmt"
	"image/color"
	"math"
	"os"
	"time"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/ebitenutil"
	"github.com/hajimehoshi/ebiten/v2/text"
	"github.com/hajimehoshi/ebiten/v2/vector"
	"golang.org/x/image/font/basicfont"
)

const (
	screenW = 900
	screenH = 480
	fps     = 60

	vuRiseTime  = 0.15
	vuFallTime  = 0.25
	peakHoldSec = 2.5

	mFaceW  = 360
	mFaceH  = 260
	mGap    = 50
	mStartX = (screenW - mFaceW*2 - mGap) / 2
	mStartY = 80

	scaleStartDeg = -35.0
	scaleEndDeg   = 35.0
	needleLen     = 140.0
	pivotOffX     = 180.0
	pivotOffY     = 225.0

	dbMin = -20.0
	dbMax = 3.0
)

func dbToDeg(db float64) float64 {
	// Clamp the dB value so the needle physically cannot drop out of the screen
	if db < dbMin {
		db = dbMin
	}
	if db > dbMax {
		db = dbMax
	}
	return scaleStartDeg + (db-dbMin)/(dbMax-dbMin)*(scaleEndDeg-scaleStartDeg)
}

func volToDB(v float64) float64 {
	if v <= 0 {
		return dbMin
	}
	// The incoming volume from PipeWire is 0 to 100, representing -60 dBFS to 0 dBFS
	dbFS := (v/100.0)*60.0 - 60.0

	// Calibrate the VU meter: -12 dBFS equals 0 VU (creates headroom so it doesn't constantly peak)
	vu := dbFS + 12.0

	return vu
}

type vuState struct {
	level  float64
	peak   float64
	peakAt time.Time
}

func newVU() *vuState {
	return &vuState{level: dbMin, peak: dbMin, peakAt: time.Now()}
}

func (v *vuState) tick(target float64, dt float64) {
	coeff := 1 - math.Exp(-dt/vuRiseTime)
	if target < v.level {
		coeff = 1 - math.Exp(-dt/vuFallTime)
	}
	v.level += (target - v.level) * coeff

	if v.level > v.peak {
		v.peak = v.level
		v.peakAt = time.Now()
	} else if time.Since(v.peakAt).Seconds() > peakHoldSec {
		v.peak -= peakDecayDBs * dt
		if v.peak < v.level {
			v.peak = v.level
		}
	}
}

const peakDecayDBs = 15.0

func (v *vuState) peaked() bool {
	// Trigger the peak LED if we exceed +1 VU
	return v.peak > 1.0 && time.Since(v.peakAt).Seconds() < peakHoldSec
}

type Game struct {
	ds   *DataSource
	l, r *vuState
	prev time.Time
}

func NewGame() *Game {
	return &Game{
		ds:   NewDataSource(),
		l:    newVU(),
		r:    newVU(),
		prev: time.Now(),
	}
}

func (g *Game) Update() error {
	now := time.Now()
	dt := now.Sub(g.prev).Seconds()
	if dt > 0.1 {
		dt = 0.1
	}
	g.prev = now
	lv, rv := g.ds.GetLevels()
	g.l.tick(volToDB(lv), dt)
	g.r.tick(volToDB(rv), dt)
	return nil
}

func (g *Game) Draw(s *ebiten.Image) {
	s.Fill(color.RGBA{25, 25, 30, 255})

	drawBox(s, mStartX-16, mStartY-16, mFaceW*2+mGap+32, mFaceH+48, 10, color.RGBA{50, 48, 45, 255})
	drawBox(s, mStartX-10, mStartY-10, mFaceW*2+mGap+20, mFaceH+36, 8, color.RGBA{35, 33, 30, 255})

	g.drawFace(s, mStartX, mStartY, "LEFT", g.l)
	g.drawFace(s, mStartX+mFaceW+mGap, mStartY, "RIGHT", g.r)

	f := basicfont.Face7x13
	text.Draw(s, "VU  METER", f, screenW/2-30, 45, color.RGBA{180, 170, 150, 255})
}

func (g *Game) drawFace(s *ebiten.Image, ox, oy int, ch string, m *vuState) {
	cream := color.RGBA{248, 243, 230, 255}
	drawBox(s, ox, oy, mFaceW, mFaceH, 8, cream)
	boxOutline(s, ox+5, oy+5, mFaceW-10, mFaceH-10, 6, color.RGBA{80, 70, 55, 255}, 1)

	px := float64(ox) + pivotOffX
	py := float64(oy) + pivotOffY

	g.drawArcAndLabels(s, px, py, ox, oy, m)

	needleDeg := dbToDeg(m.level)
	drawNeedle(s, px, py, needleDeg, needleLen, color.RGBA{25, 25, 25, 255}, 2.5)

	drawFilledCircle(s, int(px), int(py), 9, color.RGBA{70, 65, 55, 255})
	drawFilledCircle(s, int(px), int(py), 6, color.RGBA{110, 105, 95, 255})
	drawFilledCircle(s, int(px), int(py), 3, color.RGBA{150, 145, 135, 255})

	f := basicfont.Face7x13
	text.Draw(s, ch, f, ox+mFaceW/2-8, oy+mFaceH-12, color.RGBA{100, 90, 70, 255})
	text.Draw(s, "VU", f, ox+mFaceW/2-6, oy+22, color.RGBA{140, 60, 50, 255})
}

func drawPeakLED(s *ebiten.Image, ox, oy, faceW int, active bool) {
	ledX := ox + faceW - 28
	ledY := oy + 30

	drawFilledCircle(s, ledX, ledY, 9, color.RGBA{40, 35, 30, 255})
	drawFilledCircle(s, ledX, ledY, 7, color.RGBA{30, 25, 20, 255})

	if active {
		drawFilledCircle(s, ledX, ledY, 12, color.RGBA{255, 60, 20, 50})
		drawFilledCircle(s, ledX, ledY, 10, color.RGBA{255, 50, 15, 70})
		drawFilledCircle(s, ledX, ledY, 6, color.RGBA{255, 45, 15, 255})
		drawFilledCircle(s, ledX-1, ledY-1, 3, color.RGBA{255, 140, 100, 200})
	} else {
		drawFilledCircle(s, ledX, ledY, 6, color.RGBA{80, 20, 15, 255})
		drawFilledCircle(s, ledX-1, ledY-1, 2, color.RGBA{100, 30, 20, 150})
	}

	f := basicfont.Face7x13
	text.Draw(s, "PEAK", f, ledX-14, ledY+18, color.RGBA{100, 90, 70, 255})
}

func (g *Game) drawArcAndLabels(s *ebiten.Image, px, py float64, ox, oy int, m *vuState) {
	f := basicfont.Face7x13

	arcCol := color.RGBA{160, 150, 130, 255}
	for a := scaleStartDeg; a <= scaleEndDeg; a += 0.3 {
		x, y := polar(px, py, a, needleLen+18)
		s.Set(int(x), int(y), arcCol)
	}

	type mark struct {
		db    float64
		label string
		major bool
	}
	marks := []mark{
		{-20, "-20", true},
		{-10, "-10", true},
		{-7, "-7", false},
		{-5, "-5", false},
		{-3, "-3", false},
		{-2, "-2", false},
		{-1, "-1", false},
		{0, "0", true},
		{1, "+1", false},
		{2, "+2", false},
		{3, "+3", true},
	}

	for _, m := range marks {
		deg := dbToDeg(m.db)
		inner := needleLen + 10
		outer := needleLen + 22
		if m.major {
			outer = needleLen + 28
		}

		x1, y1 := polar(px, py, deg, inner)
		x2, y2 := polar(px, py, deg, outer)
		tickCol := color.RGBA{70, 60, 45, 255}
		if m.db == 0 {
			tickCol = color.RGBA{210, 45, 35, 255}
		}
		drawLine(s, x1, y1, x2, y2, tickCol, 1.5) // Increased width slightly for antialiasing

		if m.major {
			lx, ly := polar(px, py, deg, needleLen+40)
			lblCol := color.RGBA{70, 60, 45, 255}
			if m.db == 0 {
				lblCol = color.RGBA{210, 45, 35, 255}
			}
			text.Draw(s, m.label, f, int(lx)-len(m.label)*3, int(ly)+4, lblCol)
		}
	}

	for db := -20.0; db <= 3.0; db += 1.0 {
		isMajor := false
		for _, m := range marks {
			if m.db == db && m.major {
				isMajor = true
			}
		}
		if !isMajor {
			x1, y1 := polar(px, py, dbToDeg(db), needleLen+12)
			x2, y2 := polar(px, py, dbToDeg(db), needleLen+17)
			drawLine(s, x1, y1, x2, y2, color.RGBA{160, 150, 130, 255}, 1)
		}
	}

	for a := dbToDeg(0); a <= scaleEndDeg; a += 0.4 {
		for r := needleLen + 10; r < needleLen+15; r++ {
			x, y := polar(px, py, a, float64(r))
			s.Set(int(x), int(y), color.RGBA{220, 80, 70, 60})
		}
	}

	text.Draw(s, "dB", f, int(px)+50, int(py)-15, color.RGBA{120, 110, 90, 255})

	drawPeakLED(s, ox, oy, mFaceW, m.peaked())
}

func polar(px, py, deg, r float64) (float64, float64) {
	rad := (deg - 90) * math.Pi / 180
	return px + r*math.Cos(rad), py + r*math.Sin(rad)
}

// Uses vector drawing with anti-aliasing (true flag)
func drawNeedle(s *ebiten.Image, px, py, deg, length float64, col color.Color, w float64) {
	ex, ey := polar(px, py, deg, length)
	vector.StrokeLine(s, float32(px), float32(py), float32(ex), float32(ey), float32(w), col, true)
}

// Uses vector drawing with anti-aliasing
func drawLine(s *ebiten.Image, x1, y1, x2, y2 float64, col color.Color, w float64) {
	vector.StrokeLine(s, float32(x1), float32(y1), float32(x2), float32(y2), float32(w), col, true)
}

// Replaced pixel-by-pixel rendering with anti-aliased vector rendering
func drawFilledCircle(s *ebiten.Image, cx, cy, r int, col color.Color) {
	vector.DrawFilledCircle(s, float32(cx), float32(cy), float32(r), col, true)
}

func drawBox(s *ebiten.Image, x, y, w, h, r int, col color.Color) {
	ebitenutil.DrawRect(s, float64(x+r), float64(y), float64(w-2*r), float64(h), col)
	ebitenutil.DrawRect(s, float64(x), float64(y+r), float64(w), float64(h-2*r), col)
	for dy := 0; dy < r; dy++ {
		for dx := 0; dx < r; dx++ {
			if dx*dx+dy*dy <= r*r {
				s.Set(x+r-dx, y+r-dy, col)
				s.Set(x+w-r+dx, y+r-dy, col)
				s.Set(x+r-dx, y+h-r+dy, col)
				s.Set(x+w-r+dx, y+h-r+dy, col)
			}
		}
	}
}

func boxOutline(s *ebiten.Image, x, y, w, h, r int, col color.Color, t int) {
	for i := 0; i < t; i++ {
		ebitenutil.DrawRect(s, float64(x+r), float64(y+i), float64(w-2*r), 1, col)
		ebitenutil.DrawRect(s, float64(x+r), float64(y+h-1-i), float64(w-2*r), 1, col)
		ebitenutil.DrawRect(s, float64(x+i), float64(y+r), 1, float64(h-2*r), col)
		ebitenutil.DrawRect(s, float64(x+w-1-i), float64(y+r), 1, float64(h-2*r), col)
	}
	for dy := 0; dy < r; dy++ {
		for dx := 0; dx < r; dx++ {
			d := dx*dx + dy*dy
			if d >= (r-t)*(r-t) && d <= r*r {
				s.Set(x+r-dx, y+r-dy, col)
				s.Set(x+w-r+dx, y+r-dy, col)
				s.Set(x+r-dx, y+h-r+dy, col)
				s.Set(x+w-r+dx, y+h-r+dy, col)
			}
		}
	}
}

func maxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func (g *Game) Layout(ow, oh int) (int, int) { return screenW, screenH }

func main() {
	g := NewGame()
	g.ds.Start()
	defer g.ds.Stop()

	ebiten.SetWindowSize(screenW, screenH)
	ebiten.SetWindowTitle("VU Meter")
	ebiten.SetTPS(fps)

	if err := ebiten.RunGame(g); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
