package gg

import (
	"image/color"
	"math"
	"sort"
)

type stop struct {
	pos   float64
	color color.Color
}

type stops []stop

// Len satisfies the Sort interface.
func (s stops) Len() int {
	return len(s)
}

// Less satisfies the Sort interface.
func (s stops) Less(i, j int) bool {
	return s[i].pos < s[j].pos
}

// Swap satisfies the Sort interface.
func (s stops) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

type Gradient interface {
	Pattern
	AddColorStop(offset float64, color color.Color)
}

// Linear Gradient
type linearGradient struct {
	x0, y0, x1, y1 float64
	stops          stops
}

func (g *linearGradient) ColorAt(x, y int) color.Color {
	if len(g.stops) == 0 {
		return color.Transparent
	}

	fx, fy := float64(x), float64(y)
	x0, y0, x1, y1 := g.x0, g.y0, g.x1, g.y1
	dx, dy := x1-x0, y1-y0

	// Horizontal
	if dy == 0 && dx != 0 {
		return getColor((fx-x0)/dx, g.stops)
	}

	// Vertical
	if dx == 0 && dy != 0 {
		return getColor((fy-y0)/dy, g.stops)
	}

	// Dot product
	s0 := dx*(fx-x0) + dy*(fy-y0)
	if s0 < 0 {
		return g.stops[0].color
	}
	// Calculate distance to (x0,y0) alone (x0,y0)->(x1,y1)
	mag := math.Hypot(dx, dy)
	u := ((fx-x0)*-dy + (fy-y0)*dx) / (mag * mag)
	x2, y2 := x0+u*-dy, y0+u*dx
	d := math.Hypot(fx-x2, fy-y2) / mag
	return getColor(d, g.stops)
}

func (g *linearGradient) AddColorStop(offset float64, color color.Color) {
	g.stops = append(g.stops, stop{pos: offset, color: color})
	sort.Sort(g.stops)
}

func NewLinearGradient(x0, y0, x1, y1 float64) Gradient {
	g := &linearGradient{
		x0: x0, y0: y0,
		x1: x1, y1: y1,
	}
	return g
}

// Radial Gradient
type circle struct {
	x, y, r float64
}

type radialGradient struct {
	c0, c1, cd circle
	a, inva    float64
	mindr      float64
	stops      stops
}

func dot3(x0, y0, z0, x1, y1, z1 float64) float64 {
	return x0*x1 + y0*y1 + z0*z1
}

func (g *radialGradient) ColorAt(x, y int) color.Color {
	if len(g.stops) == 0 {
		return color.Transparent
	}

	// copy from pixman's pixman-radial-gradient.c

	dx, dy := float64(x)+0.5-g.c0.x, float64(y)+0.5-g.c0.y
	b := dot3(dx, dy, g.c0.r, g.cd.x, g.cd.y, g.cd.r)
	c := dot3(dx, dy, -g.c0.r, dx, dy, g.c0.r)

	if g.a == 0 {
		if b == 0 {
			return color.Transparent
		}
		t := 0.5 * c / b
		if t*g.cd.r >= g.mindr {
			return getColor(t, g.stops)
		}
		return color.Transparent
	}

	discr := dot3(b, g.a, 0, b, -c, 0)
	if discr >= 0 {
		sqrtdiscr := math.Sqrt(discr)
		t0 := (b + sqrtdiscr) * g.inva
		t1 := (b - sqrtdiscr) * g.inva

		if t0*g.cd.r >= g.mindr {
			return getColor(t0, g.stops)
		} else if t1*g.cd.r >= g.mindr {
			return getColor(t1, g.stops)
		}
	}

	return color.Transparent
}

func (g *radialGradient) AddColorStop(offset float64, color color.Color) {
	g.stops = append(g.stops, stop{pos: offset, color: color})
	sort.Sort(g.stops)
}

func NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) Gradient {
	c0 := circle{x0, y0, r0}
	c1 := circle{x1, y1, r1}
	cd := circle{x1 - x0, y1 - y0, r1 - r0}
	a := dot3(cd.x, cd.y, -cd.r, cd.x, cd.y, cd.r)
	var inva float64
	if a != 0 {
		inva = 1.0 / a
	}
	mindr := -c0.r
	g := &radialGradient{
		c0:    c0,
		c1:    c1,
		cd:    cd,
		a:     a,
		inva:  inva,
		mindr: mindr,
	}
	return g
}

// Conic Gradient
type conicGradient struct {
	cx, cy   float64
	rotation float64
	stops    stops
}

func (g *conicGradient) ColorAt(x, y int) color.Color {
	if len(g.stops) == 0 {
		return color.Transparent
	}
	a := math.Atan2(float64(y)-g.cy, float64(x)-g.cx)
	t := norm(a, -math.Pi, math.Pi) - g.rotation
	if t < 0 {
		t += 1
	}
	return getColor(t, g.stops)
}

func (g *conicGradient) AddColorStop(offset float64, color color.Color) {
	g.stops = append(g.stops, stop{pos: offset, color: color})
	sort.Sort(g.stops)
}

func NewConicGradient(cx, cy, deg float64) Gradient {
	g := &conicGradient{
		cx:       cx,
		cy:       cy,
		rotation: normalizeAngle(deg) / 360,
	}
	return g
}

func normalizeAngle(t float64) float64 {
	t = math.Mod(t, 360)
	if t < 0 {
		t += 360
	}
	return t
}

// Map value which is in range [a..b] to range [0..1]
func norm(value, a, b float64) float64 {
	return (value - a) * (1.0 / (b - a))
}

func getColor(pos float64, stops stops) color.Color {
	if pos <= 0.0 || len(stops) == 1 {
		return stops[0].color
	}

	last := stops[len(stops)-1]

	if pos >= last.pos {
		return last.color
	}

	for i, stop := range stops[1:] {
		if pos < stop.pos {
			pos = (pos - stops[i].pos) / (stop.pos - stops[i].pos)
			return colorLerp(stops[i].color, stop.color, pos)
		}
	}

	return last.color
}

func colorLerp(c0, c1 color.Color, t float64) color.Color {
	r0, g0, b0, a0 := c0.RGBA()
	r1, g1, b1, a1 := c1.RGBA()

	return color.RGBA{
		lerp(r0, r1, t),
		lerp(g0, g1, t),
		lerp(b0, b1, t),
		lerp(a0, a1, t),
	}
}

func lerp(a, b uint32, t float64) uint8 {
	return uint8(int32(float64(a)*(1.0-t)+float64(b)*t) >> 8)
}
