
// imagery provides functions to read, edit and export images

package imagery

import "seekia/internal/encoding"
import "seekia/internal/helpers"
import "seekia/internal/appValues"
import "seekia/internal/localFilesystem"

import "github.com/disintegration/gift"
import "github.com/srwiley/oksvg"
import "github.com/srwiley/rasterx"

import goJpeg "image/jpeg"
import goWebp "golang.org/x/image/webp"
import goPng "image/png"

import goFilepath "path/filepath"
import "image"
import "image/color"
import "image/draw"
import "strings"
import "bytes"
import "errors"


// Image can be either jpeg/jpg, webp, or png
//Outputs:
//	-bool: File found
//	-bool: Able to read image
//	-image.Image: golang image
//	-error
func ReadImageFile(filepath string) (bool, bool, image.Image, error){

	exists, fileBytes, err := localFilesystem.GetFileContents(filepath)
	if (err != nil) { return false, false, nil, err }
	if (exists == false){
		return false, false, nil, nil
	}

	filename := goFilepath.Base(filepath)
	filenameLowercase := strings.ToLower(filename)

	isJPEG := strings.HasSuffix(filenameLowercase, "jpeg")
	isJPG := strings.HasSuffix(filenameLowercase, "jpg")
	if (isJPEG == true || isJPG == true){
		imageObject, err := ConvertJPEGImageFileBytesToGolangImage(fileBytes)
		if (err == nil) {
			return true, true, imageObject, nil
		}
	}

	isPNG := strings.HasSuffix(filenameLowercase, "png")
	if (isPNG == true){

		imageObject, err := ConvertPNGImageFileBytesToGolangImage(fileBytes)
		if (err == nil) {
			return true, true, imageObject, nil
		}
	}

	isWEBP := strings.HasSuffix(filenameLowercase, "webp")
	if (isWEBP == true){

		imageObject, err := ConvertWEBPImageFileBytesToGolangImage(fileBytes)
		if (err == nil){
			return true, true, imageObject, nil
		}
	}

	// File extention is unknown, or it does not correspond to the image file's true format
	// We will try every method we haven't already tried.

	if (isJPEG == false && isJPG == false){

		imageObject, err := ConvertJPEGImageFileBytesToGolangImage(fileBytes)
		if (err == nil) {
			return true, true, imageObject, nil
		}
	}
	if (isPNG == false){

		imageObject, err := ConvertPNGImageFileBytesToGolangImage(fileBytes)
		if (err == nil) {
			return true, true, imageObject, nil
		}
	}
	if (isWEBP == false){
		imageObject, err := ConvertWEBPImageFileBytesToGolangImage(fileBytes)
		if (err == nil){
			return true, true, imageObject, nil
		}
	}

	return true, false, nil, nil
}

func ConvertJPEGImageFileBytesToGolangImage(input []byte)(image.Image, error){

	bytesBuffer := bytes.NewBuffer(input)

	imageObject, err := goJpeg.Decode(bytesBuffer)
	if (err != nil) { return nil, err }

	return imageObject, nil
}

func ConvertPNGImageFileBytesToGolangImage(input []byte)(image.Image, error){

	bytesBuffer := bytes.NewBuffer(input)

	imageObject, err := goPng.Decode(bytesBuffer)
	if (err != nil) { return nil, err }

	return imageObject, nil
}

func ConvertWEBPImageFileBytesToGolangImage(input []byte)(image.Image, error){

	bytesBuffer := bytes.NewBuffer(input)

	imageObject, err := goWebp.Decode(bytesBuffer)
	if (err != nil) { return nil, err }

	return imageObject, nil
}

// This function only works for square images
func ConvertSVGImageFileBytesToGolangImage(inputBytes []byte)(image.Image, error){

	//TODO: Use a different svg package, because oksvg cannot render many complex openmoji icons

	fileReader := bytes.NewReader(inputBytes)

	svgIconObject, err := oksvg.ReadIconStream(fileReader, oksvg.StrictErrorMode)
	if (err != nil) { return nil, err }

	svgWidth := int(svgIconObject.ViewBox.W)
	svgHeight := int(svgIconObject.ViewBox.H)

	svgIconObject.SetTarget(0, 0, 400, 400)

	imageObject := image.NewRGBA(image.Rect(0, 0, 400, 400))

	scannerGV := rasterx.NewScannerGV(svgWidth, svgHeight, imageObject, imageObject.Bounds())

	raster := rasterx.NewDasher(400, 400, scannerGV)

	svgIconObject.Draw(raster, 1)

	return imageObject, nil
}

// This function creates a 1 pixel image of a provided color
func GetColorSquare(colorCode string)(image.Image, error){

	colorObject, err := GetColorObjectFromColorCode(colorCode)
	if (err != nil){
		return nil, errors.New("GetColorSquare called with invalid color code: " + colorCode)
	}

	imageRectangle := image.Rect(0, 0, 1, 1)
	imageObject := image.NewRGBA(imageRectangle)
	imageObject.Set(0, 0, colorObject)

	return imageObject, nil
}

// Example color codes: 
//	-Black: "000000"
//	-White: "ffffff"
//	-Blue: "0000ff"
//	-Red: "ff0000"
func GetColorObjectFromColorCode(colorCode string)(color.Color, error){

	colorCodeBytes, err := encoding.DecodeHexStringToBytes(colorCode)
	if (err != nil) {
		return nil, errors.New("GetColorObjectFromColorCode called with invalid color code: " + colorCode)
	}

	if (len(colorCodeBytes) != 3){
		return nil, errors.New("GetColorObjectFromColorCode called with invalid color code: " + colorCode)
	}

	colorRed := colorCodeBytes[0]
	colorGreen := colorCodeBytes[1]
	colorBlue := colorCodeBytes[2]

	// Color Alpha (opacity)
	// It is always ff, which represents 100% opacity
	colorAlpha := uint8(0xff)

	colorObject := color.RGBA{colorRed, colorGreen, colorBlue, colorAlpha}

	return colorObject, nil
}

// This converts a profile/message JPEG base64 image string to a viewable, cropped image
// The cropping adds transparent bars so it will conform to a reasonable ratio
// The images are encoded into profiles/messages without the bars to save space
func ConvertJPEGBase64StringToCroppedDownsizedImageObject(base64Input string)(image.Image, error){

	imageObject, err := ConvertJPEGBase64StringToImageObject(base64Input)
	if (err != nil) { return nil, err }

	croppedImage, err := cropGolangImageToMaximumRatio(imageObject)
	if (err != nil) { return nil, err }

	maximumSideLength := appValues.GetStandardImageMaximumSideLength()

	downsizedImage, err := DownsizeGolangImage(croppedImage, maximumSideLength)
	if (err != nil){ return nil, err }

	return downsizedImage, nil
}

func ConvertJPEGBase64StringToImageObject(base64Input string)(image.Image, error){

	imageBytes, err := encoding.DecodeBase64StringToBytes(base64Input)
	if (err != nil) { return nil, err }

	imageObject, err := ConvertJPEGImageFileBytesToGolangImage(imageBytes)
	if (err != nil) { return nil, err }

	return imageObject, nil
}

func GetImageWidthAndHeightPixels(inputImage image.Image)(int, int, error){

	if (inputImage == nil) {
		return 0, 0, errors.New("GetImageWidthAndHeightPixels called with nil image.")
	}

	minX := inputImage.Bounds().Min.X
	maxX := inputImage.Bounds().Max.X
	minY := inputImage.Bounds().Min.Y
	maxY := inputImage.Bounds().Max.Y

	width := maxX-minX
	height := maxY-minY

	if (width <= 0 || height <= 0) {
		return 0, 0, errors.New("Failed to derive image width and height.")
	}

	return width, height, nil
}

// This will downscale an image to the maximum side length
// If the image is already small enough, it will do nothing
// It preserves aspect ratio
func DownsizeGolangImage(inputImage image.Image, maximumSideLength int)(image.Image, error){

	inputImageWidth, inputImageHeight, err := GetImageWidthAndHeightPixels(inputImage)
	if (err != nil) { return nil, err }

	if (inputImageWidth <= maximumSideLength && inputImageHeight <= maximumSideLength) {
		return inputImage, nil
	}

	resizedImage, err := ResizeGolangImage(inputImage, maximumSideLength)
	if (err != nil) { return nil, err }

	return resizedImage, nil
}

//This function can upscale or downscale an image. It preserves aspect ratio.
func ResizeGolangImage(inputImage image.Image, maximumSideLength int) (image.Image, error){

	if (inputImage == nil) {
		return nil, errors.New("ResizeGolangImage called with nil image.")
	}

	imageWidth, imageHeight, err := GetImageWidthAndHeightPixels(inputImage)
	if (err != nil) { return nil, err }

	if (imageWidth == 0 || imageHeight == 0){
		return nil, errors.New("ResizeGolangImage called with input image of zero width and length.")
	}
	if (maximumSideLength <= 0){
		return nil, errors.New("ResizeGolangImage called with invalid maximumSideLength")
	}

	//Outputs:
	//	-int: New width
	//	-int: New height
	//	-error
	getResizedImageWidthAndHeight := func()(int, int, error){

		if (imageWidth == imageHeight){
			return maximumSideLength, maximumSideLength, nil
		}

		// oldWidth/oldHeight = newWidth/newHeight
		// We set either newWidth or newHeight to maximumSideLength
		// Then we solve for either newHeight or newWidth

		if (imageWidth > imageHeight) {

			newHeight := float64(maximumSideLength) * (float64(imageHeight)/float64(imageWidth))

			newHeightInt, err := helpers.FloorFloat64ToInt(newHeight)
			if (err != nil) { return 0, 0, err }

			return maximumSideLength, newHeightInt, nil
		}

		newWidth := float64(maximumSideLength) * (float64(imageWidth)/float64(imageHeight))

		newWidthInt, err := helpers.FloorFloat64ToInt(newWidth)
		if (err != nil) { return 0, 0, err }

		return newWidthInt, maximumSideLength, nil
	}

	newWidth, newHeight, err := getResizedImageWidthAndHeight()
	if (err != nil) { return nil, err }

	rectangle := image.Rect(0, 0, newWidth, newHeight)
	resizedImage := image.NewRGBA(rectangle)

	giftResizeFilterList := gift.New(gift.Resize(newWidth, newHeight, gift.LanczosResampling))
	giftResizeFilterList.Draw(resizedImage, inputImage)

	return resizedImage, nil
}


// This function crops image to maximum allowed aspect ratio.
// It adds transparent space to top/bottom or left/right of image.
// (Smaller Side)/(Larger side) must be > 0.7
func cropGolangImageToMaximumRatio(inputImage image.Image)(image.Image, error){

	if (inputImage == nil) {
		return nil, errors.New("cropGolangImageToMaximumRatio called with nil image.")
	}

	widthPixels, heightPixels, err := GetImageWidthAndHeightPixels(inputImage)
	if (err != nil) { return nil, err }

	shorterSide := min(widthPixels, heightPixels)
	longerSide := max(widthPixels, heightPixels)

	currentImageRatio := float64(shorterSide)/float64(longerSide)

	if (currentImageRatio > 0.7) {

		//Image does not exceed maximum allowed ratio, image does not need cropping.
		return inputImage, nil
	}

	// We must increase the length of the smaller side so that the image has a ratio of .7
	// smaller/larger = .7
	// smaller = (.7)*(larger)

	getNewWidthAndHeight := func()(int, int, error){

		minimumRatio := float64(0.7)

		if (widthPixels > heightPixels){
			newHeightFloat := float64(widthPixels) * minimumRatio

			newHeight, err := helpers.CeilFloat64ToInt(newHeightFloat)
			if (err != nil) { return 0, 0, err }

			return widthPixels, newHeight, nil
		}

		newWidthFloat := float64(heightPixels) * minimumRatio

		newWidth, err := helpers.CeilFloat64ToInt(newWidthFloat)
		if (err != nil) { return 0, 0, err }

		return newWidth, heightPixels, nil
	}

	newWidthPixels, newHeightPixels, err := getNewWidthAndHeight()
	if (err != nil) { return nil, err }	

	newLongerSideLength := max(newWidthPixels, newHeightPixels)

	resizedImage, err := ResizeGolangImage(inputImage, newLongerSideLength)
	if (err != nil) { return nil, err }

	resizedImageWidth, resizedImageHeight, err := GetImageWidthAndHeightPixels(resizedImage)
	if (err != nil) { return nil, err }

	croppedImageRectangle := image.Rect(0, 0, newWidthPixels, newHeightPixels)
	croppedImage := image.NewRGBA(croppedImageRectangle)

	// We find the coordinates of the top left corner of the image we are placing within our new image

	xAxisPlacementPoint := -((newWidthPixels - resizedImageWidth)/2)
	yAxisPlacementPoint := -((newHeightPixels - resizedImageHeight)/2)

	croppedImagePlacementPoint := image.Point{
		X: xAxisPlacementPoint,
		Y: yAxisPlacementPoint,
	}

	draw.Draw(croppedImage, croppedImageRectangle, resizedImage, croppedImagePlacementPoint, draw.Over)

	return croppedImage, nil
}

// Performs downsizing and compression to maximum standard size
func ConvertImageObjectToStandardJPEGBase64String(inputImage image.Image)(string, error){

	if (inputImage == nil) {
		return "", errors.New("ConvertImageObjectToStandardJPEGBase64String called with nil image.")
	}

	maximumSideLength := appValues.GetStandardImageMaximumSideLength()

	sideLength := maximumSideLength

	for sideLength > 1 {

		resizedImage, err := ResizeGolangImage(inputImage, sideLength)
		if (err != nil) { return "", err }

		imageMaximumBytes := appValues.GetStandardImageMaximumBytes()

		imageQuality := int(100)

		for {

			bytesBuffer := new(bytes.Buffer)

			jpegOptions := &goJpeg.Options{
				Quality: imageQuality,
			}

			err := goJpeg.Encode(bytesBuffer, resizedImage, jpegOptions)
			if (err != nil) { return "", err }

			if (&bytesBuffer == nil) {
				return "", errors.New("Nil image buffer after goJpeg.Encode()")
			}

			imageBytes := bytesBuffer.Bytes()

			if (len(imageBytes) == 0){
				return "", errors.New("Empty image buffer after goJpeg.Encode()")
			}

			imageSize := len(imageBytes)

			if (imageSize < imageMaximumBytes){

				imageBase64String := encoding.EncodeBytesToBase64String(imageBytes)

				return imageBase64String, nil
			}

			if (imageQuality > 10){

				imageQuality -= 10

			} else if (imageQuality > 4){

				imageQuality -= 1

			} else {
				// imageQuality is <= 4
				// We cannot compress the image while retaining good quality at these dimensions
				// We will downsize the image by 100 pixels and try again
				sideLength -= 100
				break
			}
		}
	}

	return "", errors.New("Failed to create standard JPEG image.")
}


func PixelateGolangImage(inputImage image.Image, amount0to100 int)(image.Image, error){

	if (inputImage == nil) {
		return nil, errors.New("PixelateGolangImage called with nil image.")
	}
	if (amount0to100 < 0 || amount0to100 > 100) {
		return nil, errors.New("PixelateGolangImage called with input not between 0 and 100.")
	}

	if (amount0to100 == 0) {
		return inputImage, nil
	}

	widthPixels, heightPixels, err := GetImageWidthAndHeightPixels(inputImage)
	if (err != nil) { return nil, err }

	longerSideLength := max(widthPixels, heightPixels)

	pixelationAmountInt, err := helpers.ScaleIntProportionally(true, amount0to100, 0, 100, 0, longerSideLength/5)
	if (err != nil) { return nil, err }

	rectangle := image.Rect(0, 0, widthPixels, heightPixels)
	pixelatedImage := image.NewRGBA(rectangle)

	giftPixelateFilterList := gift.New(gift.Pixelate(pixelationAmountInt))
	giftPixelateFilterList.Draw(pixelatedImage, inputImage)

	return pixelatedImage, nil
}

// This will verify an image conforms to Seekia's image size and ratio requirements
// All images in profiles and messages must conform to these requirements
//Outputs:
//	-bool: Image is valid
//	-error
func VerifyStandardImageBytes(inputBytes []byte)(bool, error){

	imageMaximumBytes := appValues.GetStandardImageMaximumBytes()

	if (len(inputBytes) > imageMaximumBytes){
		return false, nil
	}

	imageObject, err := ConvertJPEGImageFileBytesToGolangImage(inputBytes)
	if (err != nil) {
		return false, nil
	}

	imageWidth, imageHeight, err := GetImageWidthAndHeightPixels(imageObject)
	if (err != nil) {
		return false, errors.New("ConvertJPEGImageFileBytesToGolangImage returning image with invalid width and height: " + err.Error())
	}

	maximumAllowedSideLength := appValues.GetStandardImageMaximumSideLength()

	if (imageWidth > maximumAllowedSideLength || imageHeight > maximumAllowedSideLength){
		return false, nil
	}

	return true, nil
}



