package imaging

import (
	"image"
)

// ConvolveOptions are convolution parameters.
type ConvolveOptions struct {
	// If Normalize is true the kernel is normalized before convolution.
	Normalize bool

	// If Abs is true the absolute value of each color channel is taken after convolution.
	Abs bool

	// Bias is added to each color channel value after convolution.
	Bias int
}

// Convolve3x3 convolves the image with the specified 3x3 convolution kernel.
// Default parameters are used if a nil *ConvolveOptions is passed.
func Convolve3x3(img image.Image, kernel [9]float64, options *ConvolveOptions) *image.NRGBA {
	return convolve(img, kernel[:], options)
}

// Convolve5x5 convolves the image with the specified 5x5 convolution kernel.
// Default parameters are used if a nil *ConvolveOptions is passed.
func Convolve5x5(img image.Image, kernel [25]float64, options *ConvolveOptions) *image.NRGBA {
	return convolve(img, kernel[:], options)
}

func convolve(img image.Image, kernel []float64, options *ConvolveOptions) *image.NRGBA {
	src := toNRGBA(img)
	w := src.Bounds().Max.X
	h := src.Bounds().Max.Y
	dst := image.NewNRGBA(image.Rect(0, 0, w, h))

	if w < 1 || h < 1 {
		return dst
	}

	if options == nil {
		options = &ConvolveOptions{}
	}

	if options.Normalize {
		normalizeKernel(kernel)
	}

	type coef struct {
		x, y int
		k    float64
	}
	var coefs []coef
	var m int

	switch len(kernel) {
	case 9:
		m = 1
	case 25:
		m = 2
	}

	i := 0
	for y := -m; y <= m; y++ {
		for x := -m; x <= m; x++ {
			if kernel[i] != 0 {
				coefs = append(coefs, coef{x: x, y: y, k: kernel[i]})
			}
			i++
		}
	}

	parallel(0, h, func(ys <-chan int) {
		for y := range ys {
			for x := 0; x < w; x++ {
				var r, g, b float64
				for _, c := range coefs {
					ix := x + c.x
					if ix < 0 {
						ix = 0
					} else if ix >= w {
						ix = w - 1
					}

					iy := y + c.y
					if iy < 0 {
						iy = 0
					} else if iy >= h {
						iy = h - 1
					}

					off := iy*src.Stride + ix*4
					s := src.Pix[off : off+3 : off+3]
					r += float64(s[0]) * c.k
					g += float64(s[1]) * c.k
					b += float64(s[2]) * c.k
				}

				if options.Abs {
					if r < 0 {
						r = -r
					}
					if g < 0 {
						g = -g
					}
					if b < 0 {
						b = -b
					}
				}

				if options.Bias != 0 {
					r += float64(options.Bias)
					g += float64(options.Bias)
					b += float64(options.Bias)
				}

				srcOff := y*src.Stride + x*4
				dstOff := y*dst.Stride + x*4
				d := dst.Pix[dstOff : dstOff+4 : dstOff+4]
				d[0] = clamp(r)
				d[1] = clamp(g)
				d[2] = clamp(b)
				d[3] = src.Pix[srcOff+3]
			}
		}
	})

	return dst
}

func normalizeKernel(kernel []float64) {
	var sum, sumpos float64
	for i := range kernel {
		sum += kernel[i]
		if kernel[i] > 0 {
			sumpos += kernel[i]
		}
	}
	if sum != 0 {
		for i := range kernel {
			kernel[i] /= sum
		}
	} else if sumpos != 0 {
		for i := range kernel {
			kernel[i] /= sumpos
		}
	}
}
