package imaging

import (
	"image"
	"image/color"
	"math"
	"runtime"
	"testing"

	"github.com/kovidgoyal/imaging/nrgb"
)

var (
	testdataBranchesJPG     = mustOpen("testdata/branches.jpg")
	testdataBranchesPNG     = mustOpen("testdata/branches.png")
	testdataFlowersSmallPNG = mustOpen("testdata/flowers_small.png")
)

func mustOpen(filename string) image.Image {
	img, err := Open(filename)
	if err != nil {
		panic(err)
	}
	return img
}

func TestParallel(t *testing.T) {
	for _, n := range []int{0, 1, 10, 100, 1000} {
		for _, p := range []int{1, 2, 4, 8, 16, 100} {
			if !testParallelN(n, p) {
				t.Fatalf("test [parallel %d %d] failed", n, p)
			}
		}
	}
}

func testParallelN(n, procs int) bool {
	data := make([]bool, n)
	before := runtime.GOMAXPROCS(0)
	runtime.GOMAXPROCS(procs)
	if err := run_in_parallel_over_range(0, func(start, limit int) {
		for i := start; i < limit; i++ {
			data[i] = true
		}
	}, 0, n); err != nil {
		panic(err)
	}
	runtime.GOMAXPROCS(before)
	for i := range n {
		if !data[i] {
			return false
		}
	}
	return true
}

func TestParallelMaxProcs(t *testing.T) {
	for _, n := range []int{0, 1, 10, 100, 1000} {
		for _, p := range []int{1, 2, 4, 8, 16, 100} {
			if !testParallelMaxProcsN(n, p) {
				t.Fatalf("test [parallel max procs %d %d] failed", n, p)
			}
		}
	}
}

func testParallelMaxProcsN(n, procs int) bool {
	data := make([]bool, n)
	SetMaxProcs(procs)
	if err := run_in_parallel_over_range(0, func(start, limit int) {
		for i := start; i < limit; i++ {
			data[i] = true
		}
	}, 0, n); err != nil {
		panic(err)
	}
	SetMaxProcs(0)
	for i := range n {
		if !data[i] {
			return false
		}
	}
	return true
}

func TestSetMaxProcs(t *testing.T) {
	for _, p := range []int{-1, 0, 10} {
		SetMaxProcs(p)
		if int(max_procs.Load()) != p {
			t.Fatalf("test [set max procs %d] failed", p)
		}
	}

	SetMaxProcs(0)
}

func TestClamp(t *testing.T) {
	testCases := []struct {
		f float64
		u uint8
	}{
		{0, 0},
		{255, 255},
		{128, 128},
		{0.49, 0},
		{0.50, 1},
		{254.9, 255},
		{254.0, 254},
		{256, 255},
		{2500, 255},
		{-10, 0},
		{127.6, 128},
	}

	for _, tc := range testCases {
		if clamp(tc.f) != tc.u {
			t.Fatalf("test [clamp %v %v] failed: %v", tc.f, tc.u, clamp(tc.f))
		}
	}
}

func compareNRGBA(img1 image.Image, img2 *image.NRGBA, delta int) bool {
	if !img1.Bounds().Eq(img2.Bounds()) {
		return false
	}
	return compareBytes(AsNRGBA(img1).Pix, img2.Pix, delta)
}

func compareNRGB(img1 image.Image, img2 *nrgb.Image, delta int) bool {
	if !img1.Bounds().Eq(img2.Bounds()) {
		return false
	}
	return compareBytes(AsNRGB(img1).Pix, img2.Pix, delta)
}

func delta16(a, b uint16) int { return absint(int(a) - int(b)) }

func compare_colors(ac, bc color.Color, delta int) bool {
	a := color.NRGBA64Model.Convert(ac).(color.NRGBA64)
	b := color.NRGBA64Model.Convert(bc).(color.NRGBA64)
	return !(delta16(a.R, b.R) > delta || delta16(a.G, b.G) > delta || delta16(a.B, b.B) > delta || delta16(a.A, b.A) > delta)
}

func compareImages(img1, img2 image.Image, delta int) bool {
	if !img1.Bounds().Eq(img2.Bounds()) {
		return false
	}
	for y := img1.Bounds().Min.Y; y < img1.Bounds().Max.Y; y++ {
		for x := img1.Bounds().Min.X; x < img1.Bounds().Max.X; x++ {
			ac, bc := img1.At(x, y), img2.At(x, y)
			if !compare_colors(ac, bc, delta) {
				return false
			}
		}
	}
	return true
}

func compareBytes(a, b []uint8, delta int) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range len(a) {
		if absint(int(a[i])-int(b[i])) > delta {
			return false
		}
	}
	return true
}

// compareNRGBAGolden is a special version of compareNRGBA used in golden tests.
// All the golden images are generated on amd64 architecture. Due to differences
// in floating-point rounding on different architectures, we need to add some
// level of tolerance when comparing images on architectures other than amd64.
// See https://golang.org/ref/spec#Floating_point_operators for information on
// fused multiply and add (FMA) instruction.
func compareNRGBAGolden(img1, img2 *image.NRGBA) bool {
	delta := 0
	if runtime.GOARCH != "amd64" {
		delta = 1
	}
	return compareNRGBA(img1, img2, delta)
}

func compareNRGBGolden(img1, img2 *nrgb.Image) bool {
	delta := 0
	if runtime.GOARCH != "amd64" {
		delta = 1
	}
	return compareNRGB(img1, img2, delta)
}

func compareFloat64(a, b, delta float64) bool {
	return math.Abs(a-b) <= delta
}

var rgbHSLTestCases = []struct {
	r, g, b uint8
	h, s, l float64
}{
	{
		r: 255,
		g: 0,
		b: 0,
		h: 0.000,
		s: 1.000,
		l: 0.500,
	},
	{
		r: 191,
		g: 191,
		b: 0,
		h: 0.167,
		s: 1.000,
		l: 0.375,
	},
	{
		r: 0,
		g: 128,
		b: 0,
		h: 0.333,
		s: 1.000,
		l: 0.251,
	},
	{
		r: 128,
		g: 255,
		b: 255,
		h: 0.500,
		s: 1.000,
		l: 0.751,
	},
	{
		r: 128,
		g: 128,
		b: 255,
		h: 0.667,
		s: 1.000,
		l: 0.751,
	},
	{
		r: 191,
		g: 64,
		b: 191,
		h: 0.833,
		s: 0.498,
		l: 0.500,
	},
	{
		r: 160,
		g: 164,
		b: 36,
		h: 0.172,
		s: 0.640,
		l: 0.392,
	},
	{
		r: 65,
		g: 27,
		b: 234,
		h: 0.697,
		s: 0.831,
		l: 0.512,
	},
	{
		r: 30,
		g: 172,
		b: 65,
		h: 0.374,
		s: 0.703,
		l: 0.396,
	},
	{
		r: 240,
		g: 200,
		b: 14,
		h: 0.137,
		s: 0.890,
		l: 0.498,
	},
	{
		r: 180,
		g: 48,
		b: 229,
		h: 0.788,
		s: 0.777,
		l: 0.543,
	},
	{
		r: 237,
		g: 119,
		b: 81,
		h: 0.040,
		s: 0.813,
		l: 0.624,
	},
	{
		r: 254,
		g: 248,
		b: 136,
		h: 0.158,
		s: 0.983,
		l: 0.765,
	},
	{
		r: 25,
		g: 203,
		b: 151,
		h: 0.451,
		s: 0.781,
		l: 0.447,
	},
	{
		r: 54,
		g: 38,
		b: 152,
		h: 0.690,
		s: 0.600,
		l: 0.373,
	},
	{
		r: 126,
		g: 126,
		b: 184,
		h: 0.667,
		s: 0.290,
		l: 0.608,
	},
}

func TestRGBToHSL(t *testing.T) {
	for _, tc := range rgbHSLTestCases {
		t.Run("", func(t *testing.T) {
			h, s, l := rgbToHSL(tc.r, tc.g, tc.b)
			if !compareFloat64(h, tc.h, 0.001) || !compareFloat64(s, tc.s, 0.001) || !compareFloat64(l, tc.l, 0.001) {
				t.Fatalf("(%d, %d, %d): got (%.3f, %.3f, %.3f) want (%.3f, %.3f, %.3f)", tc.r, tc.g, tc.b, h, s, l, tc.h, tc.s, tc.l)
			}
		})
	}
}

func TestHSLToRGB(t *testing.T) {
	for _, tc := range rgbHSLTestCases {
		t.Run("", func(t *testing.T) {
			r, g, b := hslToRGB(tc.h, tc.s, tc.l)
			if r != tc.r || g != tc.g || b != tc.b {
				t.Fatalf("(%.3f, %.3f, %.3f): got (%d, %d, %d) want (%d, %d, %d)", tc.h, tc.s, tc.l, r, g, b, tc.r, tc.g, tc.b)
			}
		})
	}
}
