package gift

import (
	"image"
	"image/draw"
	"math"
)

type uweight struct {
	u      int
	weight float32
}

type uvweight struct {
	u      int
	v      int
	weight float32
}

func prepareConvolutionWeights(kernel []float32, normalize bool) (int, []uvweight) {
	size := int(math.Sqrt(float64(len(kernel))))
	if size%2 == 0 {
		size--
	}
	if size < 1 {
		return 0, []uvweight{}
	}
	center := size / 2

	weights := []uvweight{}
	for i := 0; i < size; i++ {
		for j := 0; j < size; j++ {
			k := j*size + i
			w := float32(0)
			if k < len(kernel) {
				w = kernel[k]
			}
			if w != 0 {
				weights = append(weights, uvweight{u: i - center, v: j - center, weight: w})
			}
		}
	}

	if !normalize {
		return size, weights
	}

	var sum, sumpositive float32
	for _, w := range weights {
		sum += w.weight
		if w.weight > 0 {
			sumpositive += w.weight
		}
	}

	var div float32
	if sum != 0 {
		div = sum
	} else if sumpositive != 0 {
		div = sumpositive
	} else {
		return size, weights
	}

	for i := 0; i < len(weights); i++ {
		weights[i].weight /= div
	}

	return size, weights
}

type convolutionFilter struct {
	kernel    []float32
	normalize bool
	alpha     bool
	abs       bool
	delta     float32
}

func (p *convolutionFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
	dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
	return
}

func (p *convolutionFilter) Draw(dst draw.Image, src image.Image, options *Options) {
	if options == nil {
		options = &defaultOptions
	}

	srcb := src.Bounds()
	dstb := dst.Bounds()

	if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
		return
	}

	ksize, weights := prepareConvolutionWeights(p.kernel, p.normalize)
	kcenter := ksize / 2

	if ksize < 1 {
		copyimage(dst, src, options)
		return
	}

	pixGetter := newPixelGetter(src)
	pixSetter := newPixelSetter(dst)

	parallelize(options.Workers, srcb.Min.Y, srcb.Max.Y, func(start, stop int) {
		// Init temporary rows.
		starty := start
		rows := make([][]pixel, ksize)
		for i := range ksize {
			rowy := starty + i - kcenter
			if rowy < srcb.Min.Y {
				rowy = srcb.Min.Y
			} else if rowy > srcb.Max.Y-1 {
				rowy = srcb.Max.Y - 1
			}
			row := make([]pixel, srcb.Dx())
			pixGetter.getPixelRow(rowy, &row)
			rows[i] = row
		}

		for y := start; y < stop; y++ {
			// Calculate dst row.
			for x := srcb.Min.X; x < srcb.Max.X; x++ {
				var r, g, b, a float32
				for _, w := range weights {
					wx := x + w.u
					if wx < srcb.Min.X {
						wx = srcb.Min.X
					} else if wx > srcb.Max.X-1 {
						wx = srcb.Max.X - 1
					}
					rowsx := wx - srcb.Min.X
					rowsy := kcenter + w.v

					px := rows[rowsy][rowsx]
					r += px.r * w.weight
					g += px.g * w.weight
					b += px.b * w.weight
					if p.alpha {
						a += px.a * w.weight
					}
				}
				if p.abs {
					r = absf32(r)
					g = absf32(g)
					b = absf32(b)
					if p.alpha {
						a = absf32(a)
					}
				}
				if p.delta != 0 {
					r += p.delta
					g += p.delta
					b += p.delta
					if p.alpha {
						a += p.delta
					}
				}
				if !p.alpha {
					a = rows[kcenter][x-srcb.Min.X].a
				}
				pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, pixel{r, g, b, a})
			}

			// Rotate temporary rows.
			if y < stop-1 {
				tmprow := rows[0]
				for i := 0; i < ksize-1; i++ {
					rows[i] = rows[i+1]
				}
				nextrowy := min(y+ksize/2+1, srcb.Max.Y-1)
				pixGetter.getPixelRow(nextrowy, &tmprow)
				rows[ksize-1] = tmprow
			}
		}
	})
}

// Convolution creates a filter that applies a square convolution kernel to an image.
// The length of the kernel slice must be the square of an odd kernel size (e.g. 9 for 3x3 kernel, 25 for 5x5 kernel).
// Excessive slice members will be ignored.
// If normalize parameter is true, the kernel will be normalized before applying the filter.
// If alpha parameter is true, the alpha component of color will be filtered too.
// If abs parameter is true, absolute values of color components will be taken after doing calculations.
// If delta parameter is not zero, this value will be added to the filtered pixels.
//
// Example:
//
//	// Apply the emboss filter to an image.
//	g := gift.New(
//		gift.Convolution(
//			[]float32{
//				-1, -1, 0,
//				-1, 1, 1,
//				0, 1, 1,
//			},
//			false, false, false, 0,
//		),
//	)
//	dst := image.NewRGBA(g.Bounds(src.Bounds()))
//	g.Draw(dst, src)
func Convolution(kernel []float32, normalize, alpha, abs bool, delta float32) Filter {
	return &convolutionFilter{
		kernel:    kernel,
		normalize: normalize,
		alpha:     alpha,
		abs:       abs,
		delta:     delta,
	}
}

// prepareConvolutionWeights1d prepares pixel weights using a convolution kernel.
// Weights equal to 0 are excluded.
func prepareConvolutionWeights1d(kernel []float32) (int, []uweight) {
	size := len(kernel)
	if size%2 == 0 {
		size--
	}
	if size < 1 {
		return 0, []uweight{}
	}
	center := size / 2
	weights := []uweight{}
	for i := 0; i < size; i++ {
		w := float32(0)
		if i < len(kernel) {
			w = kernel[i]
		}
		if w != 0 {
			weights = append(weights, uweight{i - center, w})
		}
	}
	return size, weights
}

// convolveLine convolves a single line of pixels according to the given weights.
func convolveLine(dstBuf []pixel, srcBuf []pixel, weights []uweight) {
	max := len(srcBuf) - 1
	if max < 0 {
		return
	}
	for dstu := range srcBuf {
		var r, g, b, a float32
		for _, w := range weights {
			k := dstu + w.u
			if k < 0 {
				k = 0
			} else if k > max {
				k = max
			}
			c := srcBuf[k]
			wa := c.a * w.weight
			r += c.r * wa
			g += c.g * wa
			b += c.b * wa
			a += wa
		}
		if a != 0 {
			r /= a
			g /= a
			b /= a
		}
		dstBuf[dstu] = pixel{r, g, b, a}
	}
}

// convolve1dv performs a fast vertical 1d convolution.
func convolve1dv(dst draw.Image, src image.Image, kernel []float32, options *Options) {
	srcb := src.Bounds()
	dstb := dst.Bounds()
	if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
		return
	}
	if len(kernel) < 1 {
		copyimage(dst, src, options)
		return
	}
	_, weights := prepareConvolutionWeights1d(kernel)
	pixGetter := newPixelGetter(src)
	pixSetter := newPixelSetter(dst)
	parallelize(options.Workers, srcb.Min.X, srcb.Max.X, func(start, stop int) {
		buf := getPixelBuf(srcb.Dy(), srcb.Dy())
		defer putPixelBuf(buf)

		for x := start; x < stop; x++ {
			pixGetter.getPixelColumn(x, &buf.src)
			convolveLine(buf.dst, buf.src, weights)
			pixSetter.setPixelColumn(dstb.Min.X+x-srcb.Min.X, buf.dst)
		}
	})
}

// convolve1dh performs afast horizontal 1d convolution.
func convolve1dh(dst draw.Image, src image.Image, kernel []float32, options *Options) {
	srcb := src.Bounds()
	dstb := dst.Bounds()
	if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
		return
	}
	if len(kernel) < 1 {
		copyimage(dst, src, options)
		return
	}
	_, weights := prepareConvolutionWeights1d(kernel)
	pixGetter := newPixelGetter(src)
	pixSetter := newPixelSetter(dst)
	parallelize(options.Workers, srcb.Min.Y, srcb.Max.Y, func(start, stop int) {
		buf := getPixelBuf(srcb.Dx(), srcb.Dx())
		defer putPixelBuf(buf)

		for y := start; y < stop; y++ {
			pixGetter.getPixelRow(y, &buf.src)
			convolveLine(buf.dst, buf.src, weights)
			pixSetter.setPixelRow(dstb.Min.Y+y-srcb.Min.Y, buf.dst)
		}
	})
}

func gaussianBlurKernel(x, sigma float32) float32 {
	return float32(math.Exp(-float64(x*x)/float64(2*sigma*sigma)) / (float64(sigma) * math.Sqrt(2*math.Pi)))
}

type gausssianBlurFilter struct {
	sigma float32
}

func (p *gausssianBlurFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
	dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
	return
}

func (p *gausssianBlurFilter) Draw(dst draw.Image, src image.Image, options *Options) {
	if options == nil {
		options = &defaultOptions
	}

	srcb := src.Bounds()
	if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
		return
	}

	if p.sigma <= 0 {
		copyimage(dst, src, options)
		return
	}

	radius := int(math.Ceil(float64(p.sigma * 3)))
	size := 2*radius + 1
	center := radius
	kernel := make([]float32, size)

	kernel[center] = gaussianBlurKernel(0, p.sigma)
	sum := kernel[center]

	for i := 1; i <= radius; i++ {
		f := gaussianBlurKernel(float32(i), p.sigma)
		kernel[center-i] = f
		kernel[center+i] = f
		sum += 2 * f
	}

	for i := range kernel {
		kernel[i] /= sum
	}

	tmp := createTempImage(srcb)
	convolve1dh(tmp, src, kernel, options)
	convolve1dv(dst, tmp, kernel, options)
}

// GaussianBlur creates a filter that applies a gaussian blur to an image.
// The sigma parameter must be positive and indicates how much the image will be blurred.
// Blur affected radius roughly equals 3 * sigma.
//
// Example:
//
//	g := gift.New(
//		gift.GaussianBlur(1.5),
//	)
//	dst := image.NewRGBA(g.Bounds(src.Bounds()))
//	g.Draw(dst, src)
func GaussianBlur(sigma float32) Filter {
	return &gausssianBlurFilter{
		sigma: sigma,
	}
}

type unsharpMaskFilter struct {
	sigma     float32
	amount    float32
	threshold float32
}

func (p *unsharpMaskFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
	dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
	return
}

func unsharp(orig, blurred, amount, threshold float32) float32 {
	dif := (orig - blurred) * amount
	if absf32(dif) > absf32(threshold) {
		return orig + dif
	}
	return orig
}

func (p *unsharpMaskFilter) Draw(dst draw.Image, src image.Image, options *Options) {
	if options == nil {
		options = &defaultOptions
	}

	srcb := src.Bounds()
	dstb := dst.Bounds()

	if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
		return
	}

	blurred := createTempImage(srcb)
	blur := GaussianBlur(p.sigma)
	blur.Draw(blurred, src, options)

	pixGetterOrig := newPixelGetter(src)
	pixGetterBlur := newPixelGetter(blurred)
	pixelSetter := newPixelSetter(dst)

	parallelize(options.Workers, srcb.Min.Y, srcb.Max.Y, func(start, stop int) {
		for y := start; y < stop; y++ {
			for x := srcb.Min.X; x < srcb.Max.X; x++ {
				pxOrig := pixGetterOrig.getPixel(x, y)
				pxBlur := pixGetterBlur.getPixel(x, y)

				r := unsharp(pxOrig.r, pxBlur.r, p.amount, p.threshold)
				g := unsharp(pxOrig.g, pxBlur.g, p.amount, p.threshold)
				b := unsharp(pxOrig.b, pxBlur.b, p.amount, p.threshold)
				a := unsharp(pxOrig.a, pxBlur.a, p.amount, p.threshold)

				pixelSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, pixel{r, g, b, a})
			}
		}
	})
}

// UnsharpMask creates a filter that sharpens an image.
// The sigma parameter is used in a gaussian function and affects the radius of effect.
// Sigma must be positive. Sharpen radius roughly equals 3 * sigma.
// The amount parameter controls how much darker and how much lighter the edge borders become. Typically between 0.5 and 1.5.
// The threshold parameter controls the minimum brightness change that will be sharpened. Typically between 0 and 0.05.
//
// Example:
//
//	g := gift.New(
//		gift.UnsharpMask(1, 1, 0),
//	)
//	dst := image.NewRGBA(g.Bounds(src.Bounds()))
//	g.Draw(dst, src)
func UnsharpMask(sigma, amount, threshold float32) Filter {
	return &unsharpMaskFilter{
		sigma:     sigma,
		amount:    amount,
		threshold: threshold,
	}
}

type meanFilter struct {
	ksize int
	disk  bool
}

func (p *meanFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
	dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
	return
}

func (p *meanFilter) Draw(dst draw.Image, src image.Image, options *Options) {
	if options == nil {
		options = &defaultOptions
	}

	srcb := src.Bounds()
	if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
		return
	}

	ksize := p.ksize
	if ksize%2 == 0 {
		ksize--
	}

	if ksize <= 1 {
		copyimage(dst, src, options)
		return
	}

	if p.disk {
		diskKernel := genDisk(p.ksize)
		f := Convolution(diskKernel, true, true, false, 0)
		f.Draw(dst, src, options)
	} else {
		kernel := make([]float32, ksize*ksize)
		for i := range kernel {
			kernel[i] = 1
		}
		f := Convolution(kernel, true, true, false, 0)
		f.Draw(dst, src, options)
	}
}

// Mean creates a local mean image filter.
// Takes an average across a neighborhood for each pixel.
// The ksize parameter is the kernel size. It must be an odd positive integer (for example: 3, 5, 7).
// If the disk parameter is true, a disk-shaped neighborhood will be used instead of a square neighborhood.
func Mean(ksize int, disk bool) Filter {
	return &meanFilter{
		ksize: ksize,
		disk:  disk,
	}
}

type hvConvolutionFilter struct {
	hkernel, vkernel []float32
}

func (p *hvConvolutionFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
	dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
	return
}

func (p *hvConvolutionFilter) Draw(dst draw.Image, src image.Image, options *Options) {
	if options == nil {
		options = &defaultOptions
	}

	srcb := src.Bounds()
	dstb := dst.Bounds()

	if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
		return
	}

	tmph := createTempImage(srcb)
	Convolution(p.hkernel, false, false, true, 0).Draw(tmph, src, options)
	pixGetterH := newPixelGetter(tmph)

	tmpv := createTempImage(srcb)
	Convolution(p.vkernel, false, false, true, 0).Draw(tmpv, src, options)
	pixGetterV := newPixelGetter(tmpv)

	pixSetter := newPixelSetter(dst)

	parallelize(options.Workers, srcb.Min.Y, srcb.Max.Y, func(start, stop int) {
		for y := start; y < stop; y++ {
			for x := srcb.Min.X; x < srcb.Max.X; x++ {
				pxh := pixGetterH.getPixel(x, y)
				pxv := pixGetterV.getPixel(x, y)
				r := sqrtf32(pxh.r*pxh.r + pxv.r*pxv.r)
				g := sqrtf32(pxh.g*pxh.g + pxv.g*pxv.g)
				b := sqrtf32(pxh.b*pxh.b + pxv.b*pxv.b)
				pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, pixel{r, g, b, pxh.a})
			}
		}
	})
}

// Sobel creates a filter that applies a sobel operator to an image.
func Sobel() Filter {
	return &hvConvolutionFilter{
		hkernel: []float32{-1, 0, 1, -2, 0, 2, -1, 0, 1},
		vkernel: []float32{-1, -2, -1, 0, 0, 0, 1, 2, 1},
	}
}
