package card

import (
	"image"
	"image/color"
	"math"

	"golang.org/x/image/vector"
)

// premul is a premultiplied color with components in 0..1.
type premul struct{ r, g, b, a float32 }

func pm(c color.NRGBA) premul {
	a := float32(c.A) / 255
	return premul{float32(c.R) / 255 * a, float32(c.G) / 255 * a, float32(c.B) / 255 * a, a}
}

// blend composites c scaled by coverage over the pixel at (x, y).
func blend(dst *image.RGBA, x, y int, c premul, cov float32) {
	if cov <= 0 || !(image.Point{x, y}.In(dst.Rect)) {
		return
	}
	if cov > 1 {
		cov = 1
	}
	i := dst.PixOffset(x, y)
	p := dst.Pix[i : i+4 : i+4]
	inv := 1 - c.a*cov
	p[0] = uint8(c.r*cov*255 + float32(p[0])*inv + 0.5)
	p[1] = uint8(c.g*cov*255 + float32(p[1])*inv + 0.5)
	p[2] = uint8(c.b*cov*255 + float32(p[2])*inv + 0.5)
	p[3] = uint8(c.a*cov*255 + float32(p[3])*inv + 0.5)
}

// rrect is a rounded rectangle in device px.
type rrect struct{ x, y, w, h, r float64 }

// dist is the signed distance from (px, py) to the rectangle's edge.
func (rr rrect) dist(px, py float64) float64 {
	hx, hy := rr.w/2, rr.h/2
	qx := math.Abs(px-(rr.x+hx)) - (hx - rr.r)
	qy := math.Abs(py-(rr.y+hy)) - (hy - rr.r)
	out := math.Hypot(math.Max(qx, 0), math.Max(qy, 0))
	return out + math.Min(math.Max(qx, qy), 0) - rr.r
}

func (rr rrect) bounds(pad float64) image.Rectangle {
	return image.Rect(int(math.Floor(rr.x-pad)), int(math.Floor(rr.y-pad)),
		int(math.Ceil(rr.x+rr.w+pad)), int(math.Ceil(rr.y+rr.h+pad)))
}

// inside returns the pixel columns [x0, x1) on row py whose centres lie at
// least inset px inside the edge, so callers can skip the distance math.
func (rr rrect) inside(py int, inset float64) (int, int) {
	hx, hy := rr.w/2-inset, rr.h/2-inset
	r := math.Max(rr.r-inset, 0)
	if hx <= 0 || hy <= 0 {
		return 0, 0
	}
	qy := math.Abs(float64(py)+0.5-(rr.y+rr.h/2)) - (hy - r)
	half := hx
	switch {
	case qy > r:
		return 0, 0
	case qy > 0:
		half = hx - r + math.Sqrt(r*r-qy*qy)
	}
	cx := rr.x + rr.w/2
	return int(math.Ceil(cx - half - 0.5)), int(math.Floor(cx + half - 0.5))
}

func fillRRect(dst *image.RGBA, rr rrect, c color.NRGBA) {
	col := pm(c)
	b := rr.bounds(1).Intersect(dst.Rect)
	for y := b.Min.Y; y < b.Max.Y; y++ {
		i0, i1 := rr.inside(y, 1)
		for x := b.Min.X; x < b.Max.X; x++ {
			if x >= i0 && x < i1 {
				blend(dst, x, y, col, 1)
				continue
			}
			blend(dst, x, y, col, float32(0.5-rr.dist(float64(x)+0.5, float64(y)+0.5)))
		}
	}
}

// strokeRRect draws a w-px line just inside the edge.
func strokeRRect(dst *image.RGBA, rr rrect, w float64, c color.NRGBA) {
	col := pm(c)
	b := rr.bounds(1).Intersect(dst.Rect)
	for y := b.Min.Y; y < b.Max.Y; y++ {
		i0, i1 := rr.inside(y, w+1)
		for x := b.Min.X; x < b.Max.X; x++ {
			if x >= i0 && x < i1 {
				x = i1 - 1
				continue
			}
			d := rr.dist(float64(x)+0.5, float64(y)+0.5)
			blend(dst, x, y, col, float32(clamp01(0.5-d)-clamp01(0.5-d-w)))
		}
	}
}

// shadow paints a soft drop shadow of rr, blurred over blur px, leaving out
// the pixels the card itself will cover.
func shadow(dst *image.RGBA, rr, card rrect, blur float64, c color.NRGBA) {
	col := pm(c)
	b := rr.bounds(blur).Intersect(dst.Rect)
	for y := b.Min.Y; y < b.Max.Y; y++ {
		i0, i1 := card.inside(y, 1)
		for x := b.Min.X; x < b.Max.X; x++ {
			if x >= i0 && x < i1 {
				x = i1 - 1
				continue
			}
			d := rr.dist(float64(x)+0.5, float64(y)+0.5)
			t := clamp01((d + blur*0.25) / blur)
			blend(dst, x, y, col, float32((1-t)*(1-t)*(1-t)))
		}
	}
}

func fillCircle(dst *image.RGBA, cx, cy, r float64, c color.NRGBA) {
	fillRRect(dst, rrect{cx - r, cy - r, 2 * r, 2 * r, r}, c)
}

// fillRect fills an axis-aligned rectangle with antialiased edges.
func fillRect(dst *image.RGBA, x, y, w, h float64, c color.NRGBA) {
	fillRRect(dst, rrect{x, y, w, h, 0}, c)
}

func clamp01(v float64) float64 { return math.Max(0, math.Min(1, v)) }

// speaker draws a small speaker with two sound waves centred on (cx, cy);
// s is the icon size in device px.
func speaker(dst *image.RGBA, cx, cy, s float64, c color.NRGBA) {
	b := image.Rect(int(cx-s), int(cy-s), int(cx+s), int(cy+s)).Intersect(dst.Rect)
	if b.Empty() {
		return
	}
	var z vector.Rasterizer
	z.Reset(b.Dx(), b.Dy())
	ox, oy := float32(cx)-float32(b.Min.X), float32(cy)-float32(b.Min.Y)
	u := float32(s) / 16
	pt := func(x, y float32) (float32, float32) { return ox + x*u, oy + y*u }
	z.MoveTo(pt(-7, -2.5))
	z.LineTo(pt(-4, -2.5))
	z.LineTo(pt(0, -6.5))
	z.LineTo(pt(0, 6.5))
	z.LineTo(pt(-4, 2.5))
	z.LineTo(pt(-7, 2.5))
	z.ClosePath()
	arc := func(r, th float32) {
		const a0, a1, n = -0.85, 0.85, 12
		for i := 0; i <= n; i++ {
			a := a0 + (a1-a0)*float32(i)/n
			x, y := pt(1+r*cos32(a), r*sin32(a))
			if i == 0 {
				z.MoveTo(x, y)
			} else {
				z.LineTo(x, y)
			}
		}
		for i := n; i >= 0; i-- {
			a := a0 + (a1-a0)*float32(i)/n
			z.LineTo(pt(1+(r-th)*cos32(a), (r-th)*sin32(a)))
		}
		z.ClosePath()
	}
	arc(5, 1.6)
	arc(8.5, 1.6)
	mask := image.NewAlpha(image.Rect(0, 0, b.Dx(), b.Dy()))
	z.Draw(mask, mask.Bounds(), image.Opaque, image.Point{})
	col := pm(c)
	for y := 0; y < b.Dy(); y++ {
		for x := 0; x < b.Dx(); x++ {
			if a := mask.AlphaAt(x, y).A; a > 0 {
				blend(dst, b.Min.X+x, b.Min.Y+y, col, float32(a)/255)
			}
		}
	}
}

// cross draws an × of half-size r and stroke width w centred at (cx, cy).
func cross(dst *image.RGBA, cx, cy, r, w float64, c color.NRGBA) {
	b := image.Rect(int(cx-r-w), int(cy-r-w), int(cx+r+w)+1, int(cy+r+w)+1).Intersect(dst.Rect)
	if b.Empty() {
		return
	}
	var z vector.Rasterizer
	z.Reset(b.Dx(), b.Dy())
	ox, oy := float32(cx)-float32(b.Min.X), float32(cy)-float32(b.Min.Y)
	R, h := float32(r), float32(w/math.Sqrt2/2)
	for _, sgn := range []float32{1, -1} {
		// a stroke from (-R, -sgn R) to (R, sgn R), h either side
		z.MoveTo(ox-R-h, oy-sgn*R+sgn*h)
		z.LineTo(ox-R+h, oy-sgn*R-sgn*h)
		z.LineTo(ox+R+h, oy+sgn*R-sgn*h)
		z.LineTo(ox+R-h, oy+sgn*R+sgn*h)
		z.ClosePath()
	}
	mask := image.NewAlpha(image.Rect(0, 0, b.Dx(), b.Dy()))
	z.Draw(mask, mask.Bounds(), image.Opaque, image.Point{})
	col := pm(c)
	for y := 0; y < b.Dy(); y++ {
		for x := 0; x < b.Dx(); x++ {
			if a := mask.AlphaAt(x, y).A; a > 0 {
				blend(dst, b.Min.X+x, b.Min.Y+y, col, float32(a)/255)
			}
		}
	}
}

func cos32(a float32) float32 { return float32(math.Cos(float64(a))) }
func sin32(a float32) float32 { return float32(math.Sin(float64(a))) }
