package canvas

import (
	"math"
	"runtime"
)

// LinearGradient is a gradient with any number of
// stops and any number of colors. The gradient will
// be drawn such that each point on the gradient
// will correspond to a straight line
type LinearGradient struct {
	gradient
}

// RadialGradient is a gradient with any number of
// stops and any number of colors. The gradient will
// be drawn such that each point on the gradient
// will correspond to a circle
type RadialGradient struct {
	gradient
	radFrom, radTo float64
}

type gradient struct {
	from, to vec
	stops    []gradientStop
	tex      uint32
	loaded   bool
	deleted  bool
}

type gradientStop struct {
	pos   float64
	color glColor
}

// NewLinearGradient creates a new linear gradient with
// the coordinates from where to where the gradient
// will apply on the canvas
func NewLinearGradient(x0, y0, x1, y1 float64) *LinearGradient {
	lg := &LinearGradient{gradient: gradient{from: vec{x0, y0}, to: vec{x1, y1}}}
	gli.GenTextures(1, &lg.tex)
	gli.ActiveTexture(gl_TEXTURE0)
	gli.BindTexture(gl_TEXTURE_2D, lg.tex)
	gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_LINEAR)
	gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_LINEAR)
	gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE)
	gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE)
	runtime.SetFinalizer(lg, func(lg *LinearGradient) {
		glChan <- func() {
			gli.DeleteTextures(1, &lg.tex)
		}
	})
	return lg
}

// NewRadialGradient creates a new linear gradient with
// the coordinates and the radii for two circles. The
// gradient will apply from the first to the second
// circle
func NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) *RadialGradient {
	rg := &RadialGradient{gradient: gradient{from: vec{x0, y0}, to: vec{x1, y1}}, radFrom: r0, radTo: r1}
	gli.GenTextures(1, &rg.tex)
	gli.ActiveTexture(gl_TEXTURE0)
	gli.BindTexture(gl_TEXTURE_2D, rg.tex)
	gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_LINEAR)
	gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_LINEAR)
	gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE)
	gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE)
	runtime.SetFinalizer(rg, func(rg *RadialGradient) {
		glChan <- func() {
			gli.DeleteTextures(1, &rg.tex)
		}
	})
	return rg
}

// Delete explicitly deletes the gradient
func (g *gradient) Delete() {
	gli.DeleteTextures(1, &g.tex)
	g.deleted = true
}

func (g *gradient) load() {
	if g.loaded {
		return
	}

	gli.ActiveTexture(gl_TEXTURE0)
	gli.BindTexture(gl_TEXTURE_2D, g.tex)
	var pixels [2048 * 4]byte
	pp := 0
	for i := 0; i < 2048; i++ {
		c := g.colorAt(float64(i) / 2047)
		pixels[pp] = byte(math.Floor(c.r*255 + 0.5))
		pixels[pp+1] = byte(math.Floor(c.g*255 + 0.5))
		pixels[pp+2] = byte(math.Floor(c.b*255 + 0.5))
		pixels[pp+3] = byte(math.Floor(c.a*255 + 0.5))
		pp += 4
	}
	gli.TexImage2D(gl_TEXTURE_2D, 0, gl_RGBA, 2048, 1, 0, gl_RGBA, gl_UNSIGNED_BYTE, gli.Ptr(&pixels[0]))
	g.loaded = true
}

func (g *gradient) colorAt(pos float64) glColor {
	if len(g.stops) == 0 {
		return glColor{}
	} else if len(g.stops) == 1 {
		return g.stops[0].color
	}
	beforeIdx, afterIdx := -1, -1
	for i, stop := range g.stops {
		if stop.pos > pos {
			afterIdx = i
			break
		}
		beforeIdx = i
	}
	if beforeIdx == -1 {
		return g.stops[0].color
	} else if afterIdx == -1 {
		return g.stops[len(g.stops)-1].color
	}
	before, after := g.stops[beforeIdx], g.stops[afterIdx]
	p := (pos - before.pos) / (after.pos - before.pos)
	var c glColor
	c.r = (after.color.r-before.color.r)*p + before.color.r
	c.g = (after.color.g-before.color.g)*p + before.color.g
	c.b = (after.color.b-before.color.b)*p + before.color.b
	c.a = (after.color.a-before.color.a)*p + before.color.a
	return c
}

// AddColorStop adds a color stop to the gradient. The stops
// don't have to be added in order, they are sorted into the
// right place
func (g *gradient) AddColorStop(pos float64, color ...interface{}) {
	c, _ := parseColor(color...)
	insert := len(g.stops)
	for i, stop := range g.stops {
		if stop.pos > pos {
			insert = i
			break
		}
	}
	g.stops = append(g.stops, gradientStop{})
	if insert < len(g.stops)-1 {
		copy(g.stops[insert+1:], g.stops[insert:len(g.stops)-1])
	}
	g.stops[insert] = gradientStop{pos: pos, color: c}
	g.loaded = false
}
