// Copyright ©2016 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package cmpimg compares the raw representation of images taking into account
// idiosyncracies related to their underlying format (SVG, PDF, PNG, ...).
package cmpimg // import "gonum.org/v1/plot/cmpimg"

import (
	"bytes"
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"math"
	"reflect"
	"strings"

	"rsc.io/pdf"

	_ "image/jpeg"
	_ "image/png"

	_ "golang.org/x/image/tiff"
)

// Equal takes the raw representation of two images, raw1 and raw2,
// together with the underlying image type ("eps", "jpeg", "jpg", "pdf", "png", "svg", "tiff"),
// and returns whether the two images are equal or not.
//
// Equal may return an error if the decoding of the raw image somehow failed.
func Equal(typ string, raw1, raw2 []byte) (bool, error) {
	return EqualApprox(typ, raw1, raw2, 0)
}

// EqualApprox takes the raw representation of two images, raw1 and raw2,
// together with the underlying image type ("eps", "jpeg", "jpg", "pdf", "png", "svg", "tiff"),
// a normalized delta parameter to describe how close the matching should be
// performed (delta=0: perfect match, delta=1, loose match)
// and returns whether the two images are equal or not.
//
// EqualApprox may return an error if the decoding of the raw image somehow failed.
// EqualApprox only uses the normalized delta parameter for "jpeg", "jpg", "png",
// and "tiff" images. It ignores that parameter for other document types.
func EqualApprox(typ string, raw1, raw2 []byte, delta float64) (bool, error) {
	switch {
	case delta < 0:
		delta = 0
	case delta > 1:
		delta = 1
	}

	switch typ {
	case "svg", "tex":
		return bytes.Equal(raw1, raw2), nil

	case "eps":
		lines1, lines2 := strings.Split(string(raw1), "\n"), strings.Split(string(raw2), "\n")
		if len(lines1) != len(lines2) {
			return false, nil
		}
		for i, line1 := range lines1 {
			if strings.Contains(line1, "CreationDate") {
				continue
			}
			if line1 != lines2[i] {
				return false, nil
			}
		}
		return true, nil

	case "pdf":
		pdf1, err := pdf.NewReader(bytes.NewReader(raw1), int64(len(raw1)))
		if err != nil {
			return false, err
		}

		pdf2, err := pdf.NewReader(bytes.NewReader(raw2), int64(len(raw2)))
		if err != nil {
			return false, err
		}

		return cmpPdf(pdf1, pdf2), nil

	case "jpeg", "jpg", "png", "tiff":
		v1, _, err := image.Decode(bytes.NewReader(raw1))
		if err != nil {
			return false, err
		}
		v2, _, err := image.Decode(bytes.NewReader(raw2))
		if err != nil {
			return false, err
		}
		return cmpImg(v1, v2, delta), nil

	default:
		return false, fmt.Errorf("cmpimg: unknown image type %q", typ)
	}
}

func cmpPdf(pdf1, pdf2 *pdf.Reader) bool {
	n1 := pdf1.NumPage()
	n2 := pdf2.NumPage()
	if n1 != n2 {
		return false
	}

	for i := 1; i <= n1; i++ {
		p1 := pdf1.Page(i).Content()
		p2 := pdf2.Page(i).Content()
		if !reflect.DeepEqual(p1, p2) {
			return false
		}
	}

	t1 := pdf1.Trailer().String()
	t2 := pdf2.Trailer().String()
	return t1 == t2
}

func cmpImg(v1, v2 image.Image, delta float64) bool {
	img1, ok := v1.(*image.RGBA)
	if !ok {
		img1 = newRGBAFrom(v1)
	}

	img2, ok := v2.(*image.RGBA)
	if !ok {
		img2 = newRGBAFrom(v2)
	}

	if len(img1.Pix) != len(img2.Pix) {
		return false
	}

	max := delta * delta
	bnd := img1.Bounds()
	for x := bnd.Min.X; x < bnd.Max.X; x++ {
		for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
			c1 := img1.RGBAAt(x, y)
			c2 := img2.RGBAAt(x, y)
			if !yiqEqApprox(c1, c2, max) {
				return false
			}
		}
	}

	return true
}

// yiqEqApprox compares the colors of 2 pixels, in the NTSC YIQ color space,
// as described in:
//
//	Measuring perceived color difference using YIQ NTSC
//	transmission color space in mobile applications.
//	Yuriy Kotsarenko, Fernando Ramos.
//
// An electronic version is available at:
//
// - http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf
func yiqEqApprox(c1, c2 color.RGBA, d2 float64) bool {
	const max = 35215.0 // difference between 2 maximally different pixels.

	var (
		r1 = float64(c1.R)
		g1 = float64(c1.G)
		b1 = float64(c1.B)

		r2 = float64(c2.R)
		g2 = float64(c2.G)
		b2 = float64(c2.B)

		y1 = r1*0.29889531 + g1*0.58662247 + b1*0.11448223
		i1 = r1*0.59597799 - g1*0.27417610 - b1*0.32180189
		q1 = r1*0.21147017 - g1*0.52261711 + b1*0.31114694

		y2 = r2*0.29889531 + g2*0.58662247 + b2*0.11448223
		i2 = r2*0.59597799 - g2*0.27417610 - b2*0.32180189
		q2 = r2*0.21147017 - g2*0.52261711 + b2*0.31114694

		y = y1 - y2
		i = i1 - i2
		q = q1 - q2

		diff = 0.5053*y*y + 0.299*i*i + 0.1957*q*q
	)
	return diff <= max*d2
}

func newRGBAFrom(src image.Image) *image.RGBA {
	var (
		bnds = src.Bounds()
		dst  = image.NewRGBA(bnds)
	)
	draw.Draw(dst, bnds, src, image.Point{}, draw.Src)
	return dst
}

// Diff calculates an intensity-scaled difference between images a and b
// and places the result in dst, returning the intersection of a, b and
// dst. It is the responsibility of the caller to construct dst so that
// it will overlap with a and b. For the purposes of Diff, alpha is not
// considered.
//
// Diff is not intended to be used for quantitative analysis of the
// difference between the input images, but rather to highlight differences
// between them for testing purposes, so the calculation is rather naive.
func Diff(dst draw.Image, a, b image.Image) image.Rectangle {
	rect := dst.Bounds().Intersect(a.Bounds()).Intersect(b.Bounds())

	// Determine greyscale dynamic range.
	min := uint16(math.MaxUint16)
	max := uint16(0)
	for x := rect.Min.X; x < rect.Max.X; x++ {
		for y := rect.Min.Y; y < rect.Max.Y; y++ {
			p := diffColor{a.At(x, y), b.At(x, y)}
			g := color.Gray16Model.Convert(p).(color.Gray16)
			if g.Y < min {
				min = g.Y
			}
			if g.Y > max {
				max = g.Y
			}
		}
	}

	// Render intensity-scaled difference.
	for x := rect.Min.X; x < rect.Max.X; x++ {
		for y := rect.Min.Y; y < rect.Max.Y; y++ {
			dst.Set(x, y, scaledColor{
				min: uint32(min), max: uint32(max),
				c: diffColor{a.At(x, y), b.At(x, y)},
			})
		}
	}

	return rect
}

type diffColor struct {
	a, b color.Color
}

func (c diffColor) RGBA() (r, g, b, a uint32) {
	ra, ga, ba, _ := c.a.RGBA()
	rb, gb, bb, _ := c.b.RGBA()
	return diff(ra, rb), diff(ga, gb), diff(ba, bb), math.MaxUint16
}

func diff(a, b uint32) uint32 {
	if a < b {
		return b - a
	}
	return a - b
}

type scaledColor struct {
	min, max uint32
	c        color.Color
}

func (c scaledColor) RGBA() (r, g, b, a uint32) {
	if c.max == c.min {
		return 0, 0, 0, 0
	}
	f := uint32(math.MaxUint16) / (c.max - c.min)
	r, g, b, _ = c.c.RGBA()
	r -= c.min
	r *= f
	g -= c.min
	g *= f
	b -= c.min
	b *= f
	return r, g, b, math.MaxUint16
}
