// 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 moreland

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

	"golang.org/x/exp/rand"

	"gonum.org/v1/plot/palette"
)

// bitTolerance is the precision of a uint8 when
// expressed as a uint32. This tolerance is used in tests
// where precision can be lost when coverting between
// 8-bit and 32-bit values.
const bitTolerance = 1.0 / 256.0 * 65535.0

func TestInterpolateMSHDiverging(t *testing.T) {
	type test struct {
		start, end                       msh
		convergeM, scalar, convergePoint float64
		result                           msh
	}
	tests := []test{
		{
			start:         msh{M: 80, S: 1.08, H: -1.1},
			end:           msh{M: 80, S: 1.08, H: 0.5},
			convergeM:     88,
			convergePoint: 0.5,
			scalar:        0.125,
			result:        msh{M: 82, S: 0.81, H: -1.2402896406131008},
		},
		{
			start:         msh{M: 80, S: 1.08, H: -1.1},
			end:           msh{M: 80, S: 1.08, H: 0.5},
			convergeM:     88,
			convergePoint: 0.5,
			scalar:        0.5,
			result:        msh{M: 88, S: 0, H: 0},
		},
		{
			start:         msh{M: 80, S: 1.08, H: -1.1},
			end:           msh{M: 80, S: 1.08, H: 0.5},
			convergeM:     88,
			convergePoint: 0.5,
			scalar:        0.75,
			result:        msh{M: 84, S: 0.54, H: 0.7805792812262012},
		},
		{
			start:         msh{M: 80, S: 1.08, H: -1.1},
			end:           msh{M: 80, S: 1.08, H: 0.5},
			convergeM:     88,
			convergePoint: 0.75,
			scalar:        0.7499999999999999,
			result:        msh{M: 88, S: 1.1990408665951691e-16, H: -1.6611585624524023},
		},
		{
			start:         msh{M: 80, S: 1.08, H: -1.1},
			end:           msh{M: 80, S: 1.08, H: 0.5},
			convergeM:     88,
			convergePoint: 0.75,
			scalar:        0.75,
			result:        msh{M: 88, S: 0, H: 0},
		},
		{
			start:         msh{M: 80, S: 1.08, H: -1.1},
			end:           msh{M: 80, S: 1.08, H: 0.5},
			convergeM:     88,
			convergePoint: 0.75,
			scalar:        0.7500000000000001,
			result:        msh{M: 88, S: 2.3980817331903383e-16, H: 1.0611585624524023},
		},
	}
	for i, test := range tests {
		p := newSmoothDiverging(test.start, test.end, test.convergeM)
		p.SetMin(0)
		p.SetMax(1)
		result := p.(*smoothDiverging).interpolateMSHDiverging(test.scalar, test.convergePoint)
		if result != test.result {
			t.Errorf("test %d: expected %v; got %v", i, test.result, result)
		}
	}
}

func TestSmoothBlueRed(t *testing.T) {
	p := SmoothBlueRed()
	wantP := []color.NRGBA{
		{59, 76, 192, 255},
		{68, 90, 204, 255},
		{77, 104, 215, 255},
		{87, 117, 225, 255},
		{98, 130, 234, 255},
		{108, 142, 241, 255},
		{119, 154, 247, 255},
		{130, 165, 251, 255},
		{141, 176, 254, 255},
		{152, 185, 255, 255},
		{163, 194, 255, 255},
		{174, 201, 253, 255},
		{184, 208, 249, 255},
		{194, 213, 244, 255},
		{204, 217, 238, 255},
		{213, 219, 230, 255},
		{221, 221, 221, 255},
		{229, 216, 209, 255},
		{236, 211, 197, 255},
		{241, 204, 185, 255},
		{245, 196, 173, 255},
		{247, 187, 160, 255},
		{247, 177, 148, 255},
		{247, 166, 135, 255},
		{244, 154, 123, 255},
		{241, 141, 111, 255},
		{236, 127, 99, 255},
		{229, 112, 88, 255},
		{222, 96, 77, 255},
		{213, 80, 66, 255},
		{203, 62, 56, 255},
		{192, 40, 47, 255},
		{180, 4, 38, 255},
	}
	c := p.Palette(33)
	if len(c.Colors()) != len(wantP) {
		t.Errorf("length doesn't match: %d != %d", len(c.Colors()), len(wantP))
	}
	for i, c := range c.Colors() {
		w := wantP[i]
		if !similar(w, c, bitTolerance) {
			t.Errorf("%d: want %+v but have %+v", i, w, c)
		}
	}
}

func TestSmoothCoolWarm(t *testing.T) {
	type test struct {
		start [3]float64
		f     func(int) palette.Palette
		end   [3]float64
	}
	tests := []test{
		{[3]float64{0.230, 0.299, 0.754}, SmoothBlueRed().Palette, [3]float64{0.706, 0.016, 0.150}},
		{[3]float64{0.436, 0.308, 0.631}, SmoothPurpleOrange().Palette, [3]float64{0.759, 0.334, 0.046}},
		{[3]float64{0.085, 0.532, 0.201}, SmoothGreenPurple().Palette, [3]float64{0.436, 0.308, 0.631}},
		{[3]float64{0.217, 0.525, 0.910}, SmoothBlueTan().Palette, [3]float64{0.677, 0.492, 0.093}},
		{[3]float64{0.085, 0.532, 0.201}, SmoothGreenRed().Palette, [3]float64{0.758, 0.214, 0.233}},
	}
	midPoint := [3]float64{0.865, 0.865, 0.865}

	for i, test := range tests {
		c := test.f(3).Colors()
		testRGB(t, i, "start", c[0], test.start)
		testRGB(t, i, "mid", c[1], midPoint)
		testRGB(t, i, "end", c[2], test.end)
	}
}

func fracToByte(v float64) uint8 {
	return uint8(v*255 + 0.5)
}

func testRGB(t *testing.T, i int, label string, c1 color.Color, c2 [3]float64) {
	c3 := color.NRGBA{
		R: fracToByte(c2[0]),
		G: fracToByte(c2[1]),
		B: fracToByte(c2[2]),
		A: 255,
	}
	if !similar(c1, c3, bitTolerance) {
		t.Errorf("%d %s: want %+v but have %+v", i, label, c1, c3)
	}
}

func TestSmoothDiverging_At(t *testing.T) {

	start := msh{M: 80, S: 1.08, H: -1.1}
	end := msh{M: 80, S: 1.08, H: 0.5}
	p := newSmoothDiverging(start, end, 88)
	p.SetMax(2)
	p.SetMin(-1)
	scalar := -1 + 3*0.125
	rgb, err := p.At(scalar)
	if err != nil {
		t.Error(err)
	}
	// The expected output values are from
	// http://www.kennethmoreland.com/color-maps/DivergingColorMapWorkshop.xls
	want := color.NRGBA{R: 98, G: 130, B: 234, A: 255}
	if !similar(want, rgb, bitTolerance) {
		t.Errorf("have %+v, want %+v", rgb, want)
	}
}

func BenchmarkSmoothDiverging_At(b *testing.B) {
	p := SmoothBlueRed()
	p.SetMax(1.0000000001)
	rand.Seed(1)
	for i := 0; i < b.N; i++ {
		if _, err := p.At(rand.Float64()); err != nil {
			b.Fatal(err)
		}
	}
}

func TestSmoothDiverging_Range(t *testing.T) {
	p := SmoothBlueRed()
	if _, err := p.At(0); !strings.Contains(err.Error(), "max == min") {
		t.Errorf("should have 'max == min' error")
	}
	p.SetMax(1)
	vals := []float64{-1, 0, 1, 2, math.Inf(1), math.Inf(-1), math.NaN()}
	errs := []error{palette.ErrUnderflow, nil, nil, palette.ErrOverflow,
		palette.ErrOverflow, palette.ErrUnderflow, palette.ErrNaN}
	for i, v := range vals {
		_, err := p.At(v)
		wantErr := errs[i]
		if wantErr == nil && err != nil {
			t.Errorf("val %g: want no error but have %v", v, err)
		} else if wantErr != nil && err == nil {
			t.Errorf("val %g: want error but have no error", v)
		} else if wantErr != err {
			t.Errorf("val %g: want error %v but have %v", v, wantErr, err)
		}
	}
	p.SetMin(2)
	if _, err := p.At(0); !strings.Contains(err.Error(), "< min") {
		t.Errorf("should have 'max < min' error")
	}
}

// similar compares whether the fields of a and b are within the
// specified tolerance of each other.
func similar(a, b color.Color, tolerance float64) bool {
	aR, aG, aB, aA := a.RGBA()
	bR, bG, bB, bA := b.RGBA()
	if math.Abs(float64(bR)-float64(aR)) > tolerance ||
		math.Abs(float64(bG)-float64(aG)) > tolerance ||
		math.Abs(float64(bB)-float64(aB)) > tolerance ||
		math.Abs(float64(bA)-float64(aA)) > tolerance {
		return false
	}
	return true
}
