package video

import (
	"errors"
	"image"

	"golang.org/x/image/draw"
)

var (
	scalerTestAlgos = map[string]Scaler{
		"NearestNeighbor": ScalerNearestNeighbor,
	}
	scalerBenchAlgos = map[string]Scaler{
		"NearestNeighbor": ScalerNearestNeighbor,
		"ApproxBiLinear":  ScalerApproxBiLinear,
		"BiLinear":        ScalerBiLinear,
	}
)

// Scaler represents scaling algorithm
type Scaler draw.Scaler

// List of scaling algorithms
var (
	ScalerNearestNeighbor = Scaler(draw.NearestNeighbor)
	ScalerApproxBiLinear  = Scaler(draw.ApproxBiLinear)
	ScalerBiLinear        = Scaler(draw.BiLinear)
	ScalerCatmullRom      = Scaler(draw.CatmullRom)
)

var errUnsupportedImageType = errors.New("scaling: unsupported image type")

// Scale returns video scaling transform.
// Setting scaler=nil to use default scaler. (ScalerNearestNeighbor)
// Negative width or height value will keep the aspect ratio of incoming image.
//
// Note: computation cost to scale YCbCr format is 10 times higher than RGB
// due to the implementation in x/image/draw package.
func Scale(width, height int, scaler Scaler) TransformFunc {
	return func(r Reader) Reader {
		scalerCached := ScalerNearestNeighbor
		if scaler != nil {
			scalerCached = scaler
		}

		// cacheScaler makes cached version of Scaler if available.
		cacheScaler := func(dRect, sRect image.Rectangle) {}
		if kernel, ok := scaler.(interface {
			NewScaler(int, int, int, int) draw.Scaler
		}); ok {
			cacheScaler = func(dRect, sRect image.Rectangle) {
				scalerCached = kernel.NewScaler(dRect.Dx(), dRect.Dy(), sRect.Dx(), sRect.Dy())
			}
		}

		var rect image.Rectangle
		var imgScaled image.Image
		if width > 0 && height > 0 {
			rect = image.Rect(0, 0, width, height)
		} else if width <= 0 && height <= 0 {
			panic("Both width and height are negative!")
		}

		// updateRect updates output image size
		updateRect := func(r image.Rectangle) {}
		if height <= 0 {
			updateRect = func(r image.Rectangle) {
				h := r.Dy() * width / r.Dx()
				rect = image.Rect(0, 0, width, h)
			}
		} else if width <= 0 {
			updateRect = func(r image.Rectangle) {
				w := r.Dx() * height / r.Dy()
				rect = image.Rect(0, 0, w, height)
			}
		}

		// fixedRect returns Rectangle of chroma plane
		fixedRect := func(rect image.Rectangle, sr image.YCbCrSubsampleRatio) image.Rectangle {
			switch sr {
			case image.YCbCrSubsampleRatio444:
			case image.YCbCrSubsampleRatio422:
				rect.Max.X /= 2
			case image.YCbCrSubsampleRatio420:
				rect.Max.X /= 2
				rect.Max.Y /= 2
			}
			return rect
		}

		src := &rgbLikeYCbCr{y: &image.Gray{}, cb: &image.Gray{}, cr: &image.Gray{}}
		dst := &rgbLikeYCbCr{y: &image.Gray{}, cb: &image.Gray{}, cr: &image.Gray{}}

		// ycbcrRealloc reallocs image.YCbCr if needed
		ycbcrRealloc := func(i1 *image.YCbCr) {
			if imgScaled == nil || imgScaled.ColorModel() != i1.ColorModel() {
				updateRect(i1.Rect)
				cacheScaler(rect, i1.Rect)
				imgScaled = image.NewYCbCr(rect, i1.SubsampleRatio)
			}
			imgDst := imgScaled.(*image.YCbCr)
			if imgDst.Rect.Dx() != i1.Rect.Dx() || imgDst.Rect.Dy() != i1.Rect.Dy() {
				updateRect(i1.Rect)
				cacheScaler(rect, i1.Rect)
			}

			yDx := rect.Dx()
			yDy := rect.Dy()
			cRect := fixedRect(rect, i1.SubsampleRatio)
			cDx := cRect.Dx()
			cDy := cRect.Dx()
			yLen := yDx * yDy
			cLen := cDx * cDy
			if len(imgDst.Y) < yLen {
				if cap(imgDst.Y) < yLen {
					imgDst.Y = make([]uint8, yLen)
				}
				imgDst.Y = imgDst.Y[:yLen]
			}
			if len(imgDst.Cr) < cLen {
				if cap(imgDst.Cr) < cLen {
					imgDst.Cr = make([]uint8, cLen)
				}
				imgDst.Cr = imgDst.Cr[:cLen]
			}
			if len(imgDst.Cb) < cLen {
				if cap(imgDst.Cb) < cLen {
					imgDst.Cb = make([]uint8, cLen)
				}
				imgDst.Cb = imgDst.Cb[:cLen]
			}
			*dst.y = image.Gray{Pix: imgDst.Y, Stride: imgDst.YStride, Rect: rect}
			*dst.cb = image.Gray{Pix: imgDst.Cb, Stride: imgDst.CStride, Rect: cRect}
			*dst.cr = image.Gray{Pix: imgDst.Cr, Stride: imgDst.CStride, Rect: cRect}
		}
		// ycbcrRealloc reallocs image.RGBA if needed
		rgbaRealloc := func(i1 *image.RGBA) {
			if imgScaled == nil || imgScaled.ColorModel() != i1.ColorModel() {
				updateRect(i1.Rect)
				cacheScaler(rect, i1.Rect)
				imgScaled = image.NewRGBA(rect)
			}
			imgDst := imgScaled.(*image.RGBA)
			if imgDst.Rect.Dx() != i1.Rect.Dx() || imgDst.Rect.Dy() != i1.Rect.Dy() {
				updateRect(i1.Rect)
				cacheScaler(rect, i1.Rect)
			}

			l := 4 * rect.Dx() * rect.Dy()
			if len(imgDst.Pix) < l {
				if cap(imgDst.Pix) < l {
					imgDst.Pix = make([]uint8, l)
				}
				imgDst.Pix = imgDst.Pix[:l]
			}
		}

		return ReaderFunc(func() (image.Image, error) {
			img, err := r.Read()
			if err != nil {
				return nil, err
			}

			switch v := img.(type) {
			case *image.RGBA:
				rgbaRealloc(v)
				dst := imgScaled.(*image.RGBA)
				scalerCached.Scale(dst, rect, v, v.Rect, draw.Src, nil)

				cloned := *dst // clone metadata
				return &cloned, nil

			case *image.YCbCr:
				ycbcrRealloc(v)
				// Scale each plane
				*src.y = image.Gray{Pix: v.Y, Stride: v.YStride, Rect: v.Rect}
				*src.cb = image.Gray{
					Pix: v.Cb, Stride: v.CStride, Rect: fixedRect(v.Rect, v.SubsampleRatio),
				}
				*src.cr = image.Gray{
					Pix: v.Cr, Stride: v.CStride, Rect: fixedRect(v.Rect, v.SubsampleRatio),
				}
				scalerCached.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Src, nil)

				cloned := *(imgScaled.(*image.YCbCr)) // clone metadata
				return &cloned, nil

			default:
				return nil, errUnsupportedImageType
			}
		})
	}
}
