package imageext

import (
	"bytes"
	"fmt"
	"github.com/disintegration/imaging"
	"gogs.mikescher.com/BlackForestBytes/goext/exerr"
	"gogs.mikescher.com/BlackForestBytes/goext/mathext"
	"image"
	"image/color"
	"image/draw"
	"image/jpeg"
	"image/png"
	"io"
	"math"
)

type ImageFit string //@enum:type

const (
	ImageFitStretch            ImageFit = "STRETCH"
	ImageFitCover              ImageFit = "COVER"
	ImageFitContainCenter      ImageFit = "CONTAIN_CENTER"
	ImageFitContainTopLeft     ImageFit = "CONTAIN_TOPLEFT"
	ImageFitContainTopRight    ImageFit = "CONTAIN_TOPRIGHT"
	ImageFitContainBottomLeft  ImageFit = "CONTAIN_BOTTOMLEFT"
	ImageFitContainBottomRight ImageFit = "CONTAIN_BOTTOMRIGHT"
)

type ImageCrop struct { // all crop values are percentages!

	CropX      float64 `bson:"cropX"      json:"cropX"`
	CropY      float64 `bson:"cropY"      json:"cropY"`
	CropWidth  float64 `bson:"cropWidth"  json:"cropWidth"`
	CropHeight float64 `bson:"cropHeight" json:"cropHeight"`
}

type ImageCompresson string //@enum:type

const (
	CompressionPNGNone  ImageCompresson = "PNG_NONE"
	CompressionPNGSpeed ImageCompresson = "PNG_SPEED"
	CompressionPNGBest  ImageCompresson = "PNG_BEST"
	CompressionJPEG100  ImageCompresson = "JPEG_100"
	CompressionJPEG90   ImageCompresson = "JPEG_090"
	CompressionJPEG80   ImageCompresson = "JPEG_080"
	CompressionJPEG70   ImageCompresson = "JPEG_070"
	CompressionJPEG60   ImageCompresson = "JPEG_060"
	CompressionJPEG50   ImageCompresson = "JPEG_050"
	CompressionJPEG25   ImageCompresson = "JPEG_025"
	CompressionJPEG10   ImageCompresson = "JPEG_010"
	CompressionJPEG1    ImageCompresson = "JPEG_001"
)

func CropImage(img image.Image, px float64, py float64, pw float64, ph float64) (image.Image, error) {

	type subImager interface {
		SubImage(r image.Rectangle) image.Image
	}

	x := int(float64(img.Bounds().Dx()) * px)
	y := int(float64(img.Bounds().Dy()) * py)
	w := int(float64(img.Bounds().Dx()) * pw)
	h := int(float64(img.Bounds().Dy()) * ph)

	if simg, ok := img.(subImager); ok {

		return simg.SubImage(image.Rect(x, y, x+w, y+h)), nil

	} else {

		bfr1 := bytes.Buffer{}
		err := png.Encode(&bfr1, img)
		if err != nil {
			return nil, exerr.Wrap(err, "").Build()
		}
		imgPNG, err := png.Decode(&bfr1)
		if err != nil {
			return nil, exerr.Wrap(err, "").Build()
		}

		return imgPNG.(subImager).SubImage(image.Rect(x, y, w+w, y+h)), nil
	}
}

func EncodeImage(img image.Image, compression ImageCompresson) (bytes.Buffer, string, error) {
	var err error

	bfr := bytes.Buffer{}

	switch compression {
	case CompressionPNGNone:
		enc := &png.Encoder{CompressionLevel: png.NoCompression}
		err = enc.Encode(&bfr, img)
		if err != nil {
			return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
		}
		return bfr, "image/png", nil
	case CompressionPNGSpeed:
		enc := &png.Encoder{CompressionLevel: png.BestSpeed}
		err = enc.Encode(&bfr, img)
		if err != nil {
			return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
		}
		return bfr, "image/png", nil
	case CompressionPNGBest:
		enc := &png.Encoder{CompressionLevel: png.BestCompression}
		err = enc.Encode(&bfr, img)
		if err != nil {
			return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
		}
		return bfr, "image/png", nil
	case CompressionJPEG100:
		err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 100})
		if err != nil {
			return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
		}
		return bfr, "image/jpeg", nil
	case CompressionJPEG90:
		err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 90})
		if err != nil {
			return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
		}
		return bfr, "image/jpeg", nil
	case CompressionJPEG80:
		err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 80})
		if err != nil {
			return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
		}
		return bfr, "image/jpeg", nil
	case CompressionJPEG70:
		err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 70})
		if err != nil {
			return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
		}
		return bfr, "image/jpeg", nil
	case CompressionJPEG60:
		err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 60})
		if err != nil {
			return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
		}
		return bfr, "image/jpeg", nil
	case CompressionJPEG50:
		err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 50})
		if err != nil {
			return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
		}
		return bfr, "image/jpeg", nil
	case CompressionJPEG25:
		err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 25})
		if err != nil {
			return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
		}
		return bfr, "image/jpeg", nil
	case CompressionJPEG10:
		err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 10})
		if err != nil {
			return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
		}
		return bfr, "image/jpeg", nil
	case CompressionJPEG1:
		err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 1})
		if err != nil {
			return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
		}
		return bfr, "image/jpeg", nil
	default:
		return bytes.Buffer{}, "", exerr.New(exerr.TypeInternal, "unknown compression method: "+compression.String()).Build()
	}
}

func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fillColor color.Color) (image.Image, PercentageRectangle, error) {

	iw := img.Bounds().Size().X
	ih := img.Bounds().Size().Y

	// [iw, ih]   is the size of the image
	// [bbw, bbh] is the target bounding box,
	//             - it specifies the target ratio
	//             - and the maximal target resolution

	facW := float64(iw) / bbw
	facH := float64(ih) / bbh

	// facW is the ratio between iw and bbw
	//  - it is the factor by which the bounding box must be multiplied to reach the image size (in the x-axis)
	//
	// (same is true for facH, but for the height and y-axis)

	if fit == ImageFitCover {

		// image-fit:cover completely fills the target-bounding-box, it potentially cuts parts of the image away

		// we use the smaller (!) value of facW and facH, because we want to have the smallest possible destination rect (due to file size)
		// and because the image is made to completely fill the bounding-box, the smaller factor (= teh dimension the image is stretched more) is relevant

		// but we cap `fac` at 1 (can be larger than 1)
		// a value >1 would mean the final image resolution is biger than the bounding box, which we do not want.

		// if the initial image (iw, ih) is already bigger than the bounding box (bbw, bbh), facW and facH are always >1 and fac will be 1
		// which means we will simply use the bounding box as destination rect (and scale the image down)

		fac := mathext.Clamp(mathext.Min(facW, facH), 0.0, 1.0)

		// we scale the bounding box by fac (both dimension the same amount, to keep the bounding-box ratio)

		w := int(math.Round(bbw * fac))
		h := int(math.Round(bbh * fac))

		img = imaging.Fill(img, w, h, imaging.Center, imaging.Lanczos)

		newImg := image.NewRGBA(image.Rect(0, 0, w, h))

		draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src)
		draw.Draw(newImg, newImg.Bounds(), img, image.Pt(0, 0), draw.Over)

		return newImg, PercentageRectangle{0, 0, 1, 1}, nil
	}

	if fit == ImageFitContainCenter || fit == ImageFitContainTopLeft || fit == ImageFitContainTopRight || fit == ImageFitContainBottomLeft || fit == ImageFitContainBottomRight {

		// image-fit:contain fills the target-bounding-box with the image, there is potentially empty-space, it potentially cuts parts of the image away

		// we use the bigger (!) value of facW and facH,
		// because the image is made to fit the bounding-box, the bigger factor (= the dimension the image is stretched less) is relevant

		// but we cap `fac` at 1 (can be larger than 1)
		// a value >1 would mean the final image resolution is biger than the bounding box, which we do not want.

		// if the initial image (iw, ih) is already bigger than the bounding box (bbw, bbh), facW and facH are always >1 and fac will be 1
		// which means we will simply use the bounding box as destination rect (and scale the image down)

		facOut := mathext.Clamp(mathext.Max(facW, facH), 0.0, 1.0)

		// we scale the bounding box by fac (both dimension the same amount, to keep the bounding-box ratio)

		// [ow|oh] ==> size of output image (same ratio as bounding box [bbw|bbh])

		ow := int(math.Round(bbw * facOut))
		oh := int(math.Round(bbh * facOut))

		facScale := mathext.Min(float64(ow)/float64(iw), float64(oh)/float64(ih))

		// [dw|dh] ==> size of destination rect (where to draw source in output image) (same ratio as input image [iw|ih])

		dw := int(math.Round(float64(iw) * facScale))
		dh := int(math.Round(float64(ih) * facScale))

		img = imaging.Resize(img, dw, dh, imaging.Lanczos)

		var destBounds image.Rectangle
		if fit == ImageFitContainCenter {
			destBounds = image.Rect((ow-dw)/2, (oh-dh)/2, (ow-dw)/2+dw, (oh-dh)/2+dh)
		} else if fit == ImageFitContainTopLeft {
			destBounds = image.Rect(0, 0, dw, dh)
		} else if fit == ImageFitContainTopRight {
			destBounds = image.Rect(ow-dw, 0, ow, dh)
		} else if fit == ImageFitContainBottomLeft {
			destBounds = image.Rect(0, oh-dh, dw, oh)
		} else if fit == ImageFitContainBottomRight {
			destBounds = image.Rect(ow-dw, oh-dh, ow, oh)
		}

		newImg := image.NewRGBA(image.Rect(0, 0, ow, oh))

		draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src)
		draw.Draw(newImg, destBounds, img, image.Pt(0, 0), draw.Over)

		return newImg, calcRelativeRect(destBounds, newImg.Bounds()), nil
	}

	if fit == ImageFitStretch {

		// image-fit:stretch simply stretches the image to the bounding box

		// we use the bigger value of [facW;facH], to (potentially) scale the bounding box down before applying it
		// theoretically we could directly use [bbw, bbh] in the call to imaging.Resize,
		// but if the image is (a lot) smaller than the bouding box it is useful to scale it down to reduce final pdf filesize

		// we also cap fac at 1, because we never want the final rect to be bigger than the inputted bounding box (see comments at start of method)

		fac := mathext.Clamp(mathext.Max(facW, facH), 0.0, 1.0)

		// we scale the bounding box by fac (both dimension the same amount, to keep the bounding-box ratio)

		w := int(math.Round(bbw * fac))
		h := int(math.Round(bbh * fac))

		img = imaging.Resize(img, w, h, imaging.Lanczos)

		newImg := image.NewRGBA(image.Rect(0, 0, w, h))

		draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src)
		draw.Draw(newImg, newImg.Bounds(), img, image.Pt(0, 0), draw.Over)

		return newImg, PercentageRectangle{0, 0, 1, 1}, nil
	}

	return nil, PercentageRectangle{}, exerr.New(exerr.TypeInternal, fmt.Sprintf("unknown image-fit: '%s'", fit)).Build()
}

func VerifyAndDecodeImage(data io.Reader, mime string) (image.Image, error) {

	if mime == "image/jpeg" {
		img, err := jpeg.Decode(data)
		if err != nil {
			return nil, exerr.Wrap(err, "failed to decode blob as jpeg").WithType(exerr.TypeInvalidImage).Build()
		}
		return img, nil
	}

	if mime == "image/png" {
		img, err := png.Decode(data)
		if err != nil {
			return nil, exerr.Wrap(err, "failed to decode blob as png").WithType(exerr.TypeInvalidImage).Build()
		}
		return img, nil
	}

	return nil, exerr.New(exerr.TypeInvalidMimeType, fmt.Sprintf("unknown/invalid image mimetype: '%s'", mime)).Build()
}