package gift

import (
	"image"
	"image/color"
	"image/draw"
)

type transformType int

const (
	ttRotate90 transformType = iota
	ttRotate180
	ttRotate270
	ttFlipHorizontal
	ttFlipVertical
	ttTranspose
	ttTransverse
)

type transformFilter struct {
	tt transformType
}

func (p *transformFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
	if p.tt == ttRotate90 || p.tt == ttRotate270 || p.tt == ttTranspose || p.tt == ttTransverse {
		dstBounds = image.Rect(0, 0, srcBounds.Dy(), srcBounds.Dx())
	} else {
		dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
	}
	return
}

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

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

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

	parallelize(options.Workers, srcb.Min.Y, srcb.Max.Y, func(start, stop int) {
		for srcy := start; srcy < stop; srcy++ {
			for srcx := srcb.Min.X; srcx < srcb.Max.X; srcx++ {
				var dstx, dsty int
				switch p.tt {
				case ttRotate90:
					dstx = dstb.Min.X + srcy - srcb.Min.Y
					dsty = dstb.Min.Y + srcb.Max.X - srcx - 1
				case ttRotate180:
					dstx = dstb.Min.X + srcb.Max.X - srcx - 1
					dsty = dstb.Min.Y + srcb.Max.Y - srcy - 1
				case ttRotate270:
					dstx = dstb.Min.X + srcb.Max.Y - srcy - 1
					dsty = dstb.Min.Y + srcx - srcb.Min.X
				case ttFlipHorizontal:
					dstx = dstb.Min.X + srcb.Max.X - srcx - 1
					dsty = dstb.Min.Y + srcy - srcb.Min.Y
				case ttFlipVertical:
					dstx = dstb.Min.X + srcx - srcb.Min.X
					dsty = dstb.Min.Y + srcb.Max.Y - srcy - 1
				case ttTranspose:
					dstx = dstb.Min.X + srcy - srcb.Min.Y
					dsty = dstb.Min.Y + srcx - srcb.Min.X
				case ttTransverse:
					dstx = dstb.Min.Y + srcb.Max.Y - srcy - 1
					dsty = dstb.Min.X + srcb.Max.X - srcx - 1
				}
				pixSetter.setPixel(dstx, dsty, pixGetter.getPixel(srcx, srcy))
			}
		}
	})
}

// Rotate90 creates a filter that rotates an image 90 degrees counter-clockwise.
func Rotate90() Filter {
	return &transformFilter{
		tt: ttRotate90,
	}
}

// Rotate180 creates a filter that rotates an image 180 degrees counter-clockwise.
func Rotate180() Filter {
	return &transformFilter{
		tt: ttRotate180,
	}
}

// Rotate270 creates a filter that rotates an image 270 degrees counter-clockwise.
func Rotate270() Filter {
	return &transformFilter{
		tt: ttRotate270,
	}
}

// FlipHorizontal creates a filter that flips an image horizontally.
func FlipHorizontal() Filter {
	return &transformFilter{
		tt: ttFlipHorizontal,
	}
}

// FlipVertical creates a filter that flips an image vertically.
func FlipVertical() Filter {
	return &transformFilter{
		tt: ttFlipVertical,
	}
}

// Transpose creates a filter that flips an image horizontally and rotates 90 degrees counter-clockwise.
func Transpose() Filter {
	return &transformFilter{
		tt: ttTranspose,
	}
}

// Transverse creates a filter that flips an image vertically and rotates 90 degrees counter-clockwise.
func Transverse() Filter {
	return &transformFilter{
		tt: ttTransverse,
	}
}

// Interpolation is an interpolation algorithm used for image transformation.
type Interpolation int

const (
	// NearestNeighborInterpolation is a nearest-neighbor interpolation algorithm.
	NearestNeighborInterpolation Interpolation = iota
	// LinearInterpolation is a bilinear interpolation algorithm.
	LinearInterpolation
	// CubicInterpolation is a bicubic interpolation algorithm.
	CubicInterpolation
)

func rotatePoint(x, y, asin, acos float32) (float32, float32) {
	newx := x*acos - y*asin
	newy := x*asin + y*acos
	return newx, newy
}

func calcRotatedSize(w, h int, angle float32) (int, int) {
	if w <= 0 || h <= 0 {
		return 0, 0
	}

	xoff := float32(w)/2 - 0.5
	yoff := float32(h)/2 - 0.5

	asin, acos := sincosf32(angle)
	x1, y1 := rotatePoint(0-xoff, 0-yoff, asin, acos)
	x2, y2 := rotatePoint(float32(w-1)-xoff, 0-yoff, asin, acos)
	x3, y3 := rotatePoint(float32(w-1)-xoff, float32(h-1)-yoff, asin, acos)
	x4, y4 := rotatePoint(0-xoff, float32(h-1)-yoff, asin, acos)

	minx := minf32(x1, minf32(x2, minf32(x3, x4)))
	maxx := maxf32(x1, maxf32(x2, maxf32(x3, x4)))
	miny := minf32(y1, minf32(y2, minf32(y3, y4)))
	maxy := maxf32(y1, maxf32(y2, maxf32(y3, y4)))

	neww := maxx - minx + 1
	if neww-floorf32(neww) > 0.01 {
		neww += 2
	}
	newh := maxy - miny + 1
	if newh-floorf32(newh) > 0.01 {
		newh += 2
	}
	return int(neww), int(newh)
}

type rotateFilter struct {
	angle         float32
	bgcolor       color.Color
	interpolation Interpolation
}

func (p *rotateFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
	w, h := calcRotatedSize(srcBounds.Dx(), srcBounds.Dy(), p.angle)
	dstBounds = image.Rect(0, 0, w, h)
	return
}

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

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

	w, h := calcRotatedSize(srcb.Dx(), srcb.Dy(), p.angle)
	if w <= 0 || h <= 0 {
		return
	}

	srcxoff := float32(srcb.Dx())/2 - 0.5
	srcyoff := float32(srcb.Dy())/2 - 0.5
	dstxoff := float32(w)/2 - 0.5
	dstyoff := float32(h)/2 - 0.5

	bgpx := pixelFromColor(p.bgcolor)
	asin, acos := sincosf32(p.angle)

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

	parallelize(options.Workers, 0, h, func(start, stop int) {
		for y := start; y < stop; y++ {
			for x := range w {

				xf, yf := rotatePoint(float32(x)-dstxoff, float32(y)-dstyoff, asin, acos)
				xf, yf = float32(srcb.Min.X)+xf+srcxoff, float32(srcb.Min.Y)+yf+srcyoff
				var px pixel

				switch p.interpolation {
				case CubicInterpolation:
					px = interpolateCubic(xf, yf, srcb, pixGetter, bgpx)
				case LinearInterpolation:
					px = interpolateLinear(xf, yf, srcb, pixGetter, bgpx)
				default:
					px = interpolateNearest(xf, yf, srcb, pixGetter, bgpx)
				}

				pixSetter.setPixel(dstb.Min.X+x, dstb.Min.Y+y, px)
			}
		}
	})
}

func interpolateCubic(xf, yf float32, bounds image.Rectangle, pixGetter *pixelGetter, bgpx pixel) pixel {
	var pxs [16]pixel
	var cfs [16]float32
	var px pixel

	x0, y0 := int(floorf32(xf)), int(floorf32(yf))
	if !image.Pt(x0, y0).In(image.Rect(bounds.Min.X-1, bounds.Min.Y-1, bounds.Max.X, bounds.Max.Y)) {
		return bgpx
	}
	xq, yq := xf-float32(x0), yf-float32(y0)

	for i := range 4 {
		for j := range 4 {
			pt := image.Pt(x0+j-1, y0+i-1)
			if pt.In(bounds) {
				pxs[i*4+j] = pixGetter.getPixel(pt.X, pt.Y)
			} else {
				pxs[i*4+j] = bgpx
			}
		}
	}

	const (
		k04 = 1 / 4.0
		k12 = 1 / 12.0
		k36 = 1 / 36.0
	)

	cfs[0] = k36 * xq * yq * (xq - 1) * (xq - 2) * (yq - 1) * (yq - 2)
	cfs[1] = -k12 * yq * (xq - 1) * (xq - 2) * (xq + 1) * (yq - 1) * (yq - 2)
	cfs[2] = k12 * xq * yq * (xq + 1) * (xq - 2) * (yq - 1) * (yq - 2)
	cfs[3] = -k36 * xq * yq * (xq - 1) * (xq + 1) * (yq - 1) * (yq - 2)
	cfs[4] = -k12 * xq * (xq - 1) * (xq - 2) * (yq - 1) * (yq - 2) * (yq + 1)
	cfs[5] = k04 * (xq - 1) * (xq - 2) * (xq + 1) * (yq - 1) * (yq - 2) * (yq + 1)
	cfs[6] = -k04 * xq * (xq + 1) * (xq - 2) * (yq - 1) * (yq - 2) * (yq + 1)
	cfs[7] = k12 * xq * (xq - 1) * (xq + 1) * (yq - 1) * (yq - 2) * (yq + 1)
	cfs[8] = k12 * xq * yq * (xq - 1) * (xq - 2) * (yq + 1) * (yq - 2)
	cfs[9] = -k04 * yq * (xq - 1) * (xq - 2) * (xq + 1) * (yq + 1) * (yq - 2)
	cfs[10] = k04 * xq * yq * (xq + 1) * (xq - 2) * (yq + 1) * (yq - 2)
	cfs[11] = -k12 * xq * yq * (xq - 1) * (xq + 1) * (yq + 1) * (yq - 2)
	cfs[12] = -k36 * xq * yq * (xq - 1) * (xq - 2) * (yq - 1) * (yq + 1)
	cfs[13] = k12 * yq * (xq - 1) * (xq - 2) * (xq + 1) * (yq - 1) * (yq + 1)
	cfs[14] = -k12 * xq * yq * (xq + 1) * (xq - 2) * (yq - 1) * (yq + 1)
	cfs[15] = k36 * xq * yq * (xq - 1) * (xq + 1) * (yq - 1) * (yq + 1)

	for i := range pxs {
		wa := pxs[i].a * cfs[i]
		px.r += pxs[i].r * wa
		px.g += pxs[i].g * wa
		px.b += pxs[i].b * wa
		px.a += wa
	}

	if px.a != 0 {
		px.r /= px.a
		px.g /= px.a
		px.b /= px.a
	}

	return px
}

func interpolateLinear(xf, yf float32, bounds image.Rectangle, pixGetter *pixelGetter, bgpx pixel) pixel {
	var pxs [4]pixel
	var cfs [4]float32
	var px pixel

	x0, y0 := int(floorf32(xf)), int(floorf32(yf))
	if !image.Pt(x0, y0).In(image.Rect(bounds.Min.X-1, bounds.Min.Y-1, bounds.Max.X, bounds.Max.Y)) {
		return bgpx
	}
	xq, yq := xf-float32(x0), yf-float32(y0)

	for i := range 2 {
		for j := range 2 {
			pt := image.Pt(x0+j, y0+i)
			if pt.In(bounds) {
				pxs[i*2+j] = pixGetter.getPixel(pt.X, pt.Y)
			} else {
				pxs[i*2+j] = bgpx
			}
		}
	}

	cfs[0] = (1 - xq) * (1 - yq)
	cfs[1] = xq * (1 - yq)
	cfs[2] = (1 - xq) * yq
	cfs[3] = xq * yq

	for i := range pxs {
		wa := pxs[i].a * cfs[i]
		px.r += pxs[i].r * wa
		px.g += pxs[i].g * wa
		px.b += pxs[i].b * wa
		px.a += wa
	}

	if px.a != 0 {
		px.r /= px.a
		px.g /= px.a
		px.b /= px.a
	}

	return px
}

func interpolateNearest(xf, yf float32, bounds image.Rectangle, pixGetter *pixelGetter, bgpx pixel) pixel {
	x0, y0 := int(floorf32(xf+0.5)), int(floorf32(yf+0.5))
	if image.Pt(x0, y0).In(bounds) {
		return pixGetter.getPixel(x0, y0)
	}
	return bgpx
}

// Rotate creates a filter that rotates an image by the given angle counter-clockwise.
// The angle parameter is the rotation angle in degrees.
// The backgroundColor parameter specifies the color of the uncovered zone after the rotation.
// The interpolation parameter specifies the interpolation method.
// Supported interpolation methods: NearestNeighborInterpolation, LinearInterpolation, CubicInterpolation.
//
// Example:
//
//	g := gift.New(
//		gift.Rotate(45, color.Black, gift.LinearInterpolation),
//	)
//	dst := image.NewRGBA(g.Bounds(src.Bounds()))
//	g.Draw(dst, src)
func Rotate(angle float32, backgroundColor color.Color, interpolation Interpolation) Filter {
	return &rotateFilter{
		angle:         angle,
		bgcolor:       backgroundColor,
		interpolation: interpolation,
	}
}

type cropFilter struct {
	rect image.Rectangle
}

func (p *cropFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
	b := srcBounds.Intersect(p.rect)
	return b.Sub(b.Min)
}

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

	srcb := src.Bounds().Intersect(p.rect)
	dstb := dst.Bounds()
	pixGetter := newPixelGetter(src)
	pixSetter := newPixelSetter(dst)

	parallelize(options.Workers, srcb.Min.Y, srcb.Max.Y, func(start, stop int) {
		for srcy := start; srcy < stop; srcy++ {
			for srcx := srcb.Min.X; srcx < srcb.Max.X; srcx++ {
				dstx := dstb.Min.X + srcx - srcb.Min.X
				dsty := dstb.Min.Y + srcy - srcb.Min.Y
				pixSetter.setPixel(dstx, dsty, pixGetter.getPixel(srcx, srcy))
			}
		}
	})
}

// Crop creates a filter that crops the specified rectangular region from an image.
//
// Example:
//
//	g := gift.New(
//		gift.Crop(image.Rect(100, 100, 200, 200)),
//	)
//	dst := image.NewRGBA(g.Bounds(src.Bounds()))
//	g.Draw(dst, src)
func Crop(rect image.Rectangle) Filter {
	return &cropFilter{
		rect: rect,
	}
}

// Anchor is the anchor point for image cropping.
type Anchor int

// Anchor point positions.
const (
	CenterAnchor Anchor = iota
	TopLeftAnchor
	TopAnchor
	TopRightAnchor
	LeftAnchor
	RightAnchor
	BottomLeftAnchor
	BottomAnchor
	BottomRightAnchor
)

func anchorPt(b image.Rectangle, w, h int, anchor Anchor) image.Point {
	var x, y int
	switch anchor {
	case TopLeftAnchor:
		x = b.Min.X
		y = b.Min.Y
	case TopAnchor:
		x = b.Min.X + (b.Dx()-w)/2
		y = b.Min.Y
	case TopRightAnchor:
		x = b.Max.X - w
		y = b.Min.Y
	case LeftAnchor:
		x = b.Min.X
		y = b.Min.Y + (b.Dy()-h)/2
	case RightAnchor:
		x = b.Max.X - w
		y = b.Min.Y + (b.Dy()-h)/2
	case BottomLeftAnchor:
		x = b.Min.X
		y = b.Max.Y - h
	case BottomAnchor:
		x = b.Min.X + (b.Dx()-w)/2
		y = b.Max.Y - h
	case BottomRightAnchor:
		x = b.Max.X - w
		y = b.Max.Y - h
	default:
		x = b.Min.X + (b.Dx()-w)/2
		y = b.Min.Y + (b.Dy()-h)/2
	}
	return image.Pt(x, y)
}

type cropToSizeFilter struct {
	w, h   int
	anchor Anchor
}

func (p *cropToSizeFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
	if p.w <= 0 || p.h <= 0 {
		return image.Rect(0, 0, 0, 0)
	}
	pt := anchorPt(srcBounds, p.w, p.h, p.anchor)
	r := image.Rect(0, 0, p.w, p.h).Add(pt)
	b := srcBounds.Intersect(r)
	return b.Sub(b.Min)
}

func (p *cropToSizeFilter) Draw(dst draw.Image, src image.Image, options *Options) {
	if p.w <= 0 || p.h <= 0 {
		return
	}
	pt := anchorPt(src.Bounds(), p.w, p.h, p.anchor)
	r := image.Rect(0, 0, p.w, p.h).Add(pt)
	b := src.Bounds().Intersect(r)
	Crop(b).Draw(dst, src, options)
}

// CropToSize creates a filter that crops an image to the specified size using the specified anchor point.
func CropToSize(width, height int, anchor Anchor) Filter {
	return &cropToSizeFilter{
		w:      width,
		h:      height,
		anchor: anchor,
	}
}
