// 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 (
	"fmt"
	"image/color"
	"math"

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

// luminance is a color palette that interpolates
// between control colors in a way that ensures a linear relationship
// between the luminance of a color and the value it represents.
type luminance struct {
	// colors are the control colors to be interpolated among.
	// The colors must be monotonically increasing in luminance.
	colors []cieLAB

	// scalars are the scalar control points associated with
	// each item in colors (above). They are monotonically
	// increasing values between zero and one that correspond
	// to the luminance of a given control color in relation
	// to the minimum and maximum luminance among all control
	// colors.
	scalars []float64

	// alpha represents the opacity of the returned
	// colors in the range (0,1). It is set to 1 by default.
	alpha float64

	// min and max are the minimum and maximum values of the range of scalars
	// that can be mapped to colors using this ColorMap.
	min, max float64
}

// NewLuminance creates a new Luminance ColorMap from the given controlColors.
// luminance is a color palette that interpolates
// between control colors in a way that ensures a linear relationship
// between the luminance of a color and the value it represents.
// If the luminance of the controls is not monotonically increasing, an
// error will be returned.
func NewLuminance(controls []color.Color) (palette.ColorMap, error) {
	l := luminance{
		colors:  make([]cieLAB, len(controls)),
		scalars: make([]float64, len(controls)),
		alpha:   1,
	}
	max := math.Inf(-1)
	min := math.Inf(1)
	for i, c := range controls {
		lab := colorTosRGBA(c).cieLAB()
		l.colors[i] = lab
		max = math.Max(max, lab.L)
		min = math.Min(min, lab.L)
		if i > 0 && lab.L <= l.colors[i-1].L {
			return nil, fmt.Errorf("moreland: luminance of color %d (%g) is not "+
				"greater than that of color %d (%g)", i, lab.L, i-1, l.colors[i-1].L)
		}
	}
	// Normalize scalar values to the range (0,1).
	rnge := max - min
	for i, c := range l.colors {
		l.scalars[i] = (c.L - min) / rnge
	}
	// Sometimes the first and last scalars do not end up
	// being exactly zero and one owing to the imperfect
	// precision of floating point operations.
	// Here we set them to exactly zero and one to avoid
	// the possibility of the At() function returning
	// an out-of-range error for values that actually
	// should be in the range.
	l.scalars[0] = 0
	l.scalars[len(l.scalars)-1] = 1
	return &l, nil
}

// At implements the palette.ColorMap interface for a luminance value.
func (l *luminance) At(v float64) (color.Color, error) {
	if err := checkRange(l.min, l.max, v); err != nil {
		return nil, err
	}
	scalar := (v - l.min) / (l.max - l.min)
	if !inUnitRange(scalar) {
		return nil, fmt.Errorf("moreland: interpolation value (%g) out of range [%g,%g]", scalar, l.min, l.max)
	}
	i := searchFloat64s(l.scalars, scalar)
	if i == 0 {
		return l.colors[i].cieXYZ().rgb().sRGBA(l.alpha), nil
	}
	c1 := l.colors[i-1]
	c2 := l.colors[i]
	frac := (scalar - l.scalars[i-1]) / (l.scalars[i] - l.scalars[i-1])
	o := cieLAB{
		L: frac*(c2.L-c1.L) + c1.L,
		A: frac*(c2.A-c1.A) + c1.A,
		B: frac*(c2.B-c1.B) + c1.B,
	}.cieXYZ().rgb().sRGBA(l.alpha)
	o.clamp()
	return o, nil
}

func checkRange(min, max, val float64) error {
	if max == min {
		return fmt.Errorf("moreland: color map max == min == %g", max)
	}
	if min > max {
		return fmt.Errorf("moreland: color map max (%g) < min (%g)", max, min)
	}
	if val < min {
		return palette.ErrUnderflow
	}
	if val > max {
		return palette.ErrOverflow
	}
	if math.IsNaN(val) {
		return palette.ErrNaN
	}
	return nil
}

// searchFloat64s acts the same as sort.SearchFloat64s, except
// it uses a simple search algorithm instead of binary search.
func searchFloat64s(vals []float64, val float64) int {
	for j, v := range vals {
		if val <= v {
			return j
		}
	}
	return len(vals)
}

// SetMax implements the palette.ColorMap interface for a luminance value.
func (l *luminance) SetMax(v float64) {
	l.max = v
}

// SetMin implements the palette.ColorMap interface for a luminance value.
func (l *luminance) SetMin(v float64) {
	l.min = v
}

// Max implements the palette.ColorMap interface for a luminance value.
func (l *luminance) Max() float64 {
	return l.max
}

// Min implements the palette.ColorMap interface for a luminance value.
func (l *luminance) Min() float64 {
	return l.min
}

// SetAlpha sets the opacity value of this color map. Zero is transparent
// and one is completely opaque.
// The function will panic is alpha is not between zero and one.
func (l *luminance) SetAlpha(alpha float64) {
	if !inUnitRange(alpha) {
		panic(fmt.Errorf("moreland: invalid alpha: %g", alpha))
	}
	l.alpha = alpha
}

// Alpha returns the opacity value of this color map.
func (l *luminance) Alpha() float64 {
	return l.alpha
}

// Palette returns a value that fulfills the palette.Palette interface,
// where n is the number of desired colors.
func (l luminance) Palette(n int) palette.Palette {
	if l.Max() == 0 && l.Min() == 0 {
		l.SetMin(0)
		l.SetMax(1)
	}
	delta := (l.max - l.min) / float64(n-1)
	var v float64
	c := make([]color.Color, n)
	for i := 0; i < n; i++ {
		v = l.min + delta*float64(i)
		var err error
		c[i], err = l.At(v)
		if err != nil {
			panic(err)
		}
	}
	return plte(c)
}

// plte fulfils the palette.Palette interface.
type plte []color.Color

// Colors fulfils the palette.Palette interface.
func (p plte) Colors() []color.Color {
	return p
}

// BlackBody is a Luminance-class ColorMap based on the colors of black body radiation.
// Although the colors are inspired by the wavelengths of light from
// black body radiation, the actual colors used are designed to be
// perceptually uniform. Colors of the desired brightness and hue are chosen,
// and then the colors are adjusted such that the luminance is perceptually
// linear (according to the CIE LAB color space).
func BlackBody() palette.ColorMap {
	return &luminance{
		colors: []cieLAB{
			{L: 0, A: 0, B: 0},
			{L: 39.112572747719774, A: 55.92470934659227, B: 37.65159714510402},
			{L: 58.45705480680232, A: 43.34389690857626, B: 65.95409116544081},
			{L: 84.13253643355525, A: -6.459770854468639, B: 82.41994470228775},
			{L: 100, A: 0, B: 0}},
		scalars: []float64{0, 0.39112572747719776, 0.5845705480680232, 0.8413253643355525, 1},
		alpha:   1,
	}
}

// ExtendedBlackBody is a Luminance-class ColorMap based on the colors of black body radiation
// with some blue and purple hues thrown in at the lower end to add some "color."
// The color map is similar to the default colors used in gnuplot. Colors of
// the desired brightness and hue are chosen, and then the colors are adjusted
// such that the luminance is perceptually linear (according to the CIE LAB
// color space).
func ExtendedBlackBody() palette.ColorMap {
	return &luminance{
		colors: []cieLAB{
			{L: 0, A: 0, B: 0},
			{L: 21.873483862751876, A: 50.19882295659109, B: -74.66982659778306},
			{L: 34.506542513775905, A: 75.41302687474061, B: -88.73807072507786},
			{L: 47.02980511087303, A: 70.93217189227919, B: 33.59880053746508},
			{L: 65.17482203230537, A: 49.14591409658836, B: 56.86480950937553},
			{L: 84.13253643355525, A: -6.459770854468639, B: 82.41994470228775},
			{L: 100, A: 0, B: 0},
		},
		scalars: []float64{0, 0.21873483862751875, 0.34506542513775906, 0.4702980511087303,
			0.6517482203230537, 0.8413253643355525, 1},
		alpha: 1,
	}
}

// Kindlmann is a Luminance-class ColorMap that uses the colors
// first proposed in a paper
// by Kindlmann, Reinhard, and Creem. The map is basically the rainbow
// color map with the luminance adjusted such that it monotonically
// changes, making it much more perceptually viable.
//
// Citation:
// Gordon Kindlmann, Erik Reinhard, and Sarah Creem. 2002. Face-based
// luminance matching for perceptual colormap generation. In Proceedings
// of the conference on Visualization '02 (VIS '02). IEEE Computer Society,
// Washington, DC, USA, 299-306.
func Kindlmann() palette.ColorMap {
	return &luminance{
		colors: []cieLAB{
			{L: 0, A: 0, B: 0},
			{L: 10.479520542426698, A: 34.05557958902206, B: -34.21934877170809},
			{L: 21.03011379005111, A: 52.30473571100955, B: -61.852601228346536},
			{L: 31.03098927978494, A: 23.814976212074402, B: -57.73419358300511},
			{L: 40.21480513626115, A: -24.858012706049536, B: -7.322176588219942},
			{L: 52.73108089333358, A: -19.064976357731634, B: -25.558178073848147},
			{L: 60.007326812392634, A: -61.75624590074585, B: 56.43522875191319},
			{L: 69.81578343076002, A: -58.33353084882392, B: 68.37457857626646},
			{L: 79.55703752324776, A: -22.50477758899383, B: 78.57946686200843},
			{L: 89.818961593653, A: 7.586705160677109, B: 15.375961528833981},
			{L: 100, A: 0, B: 0},
		},
		scalars: []float64{0, 0.10479520542426699, 0.2103011379005111, 0.3103098927978494,
			0.4021480513626115, 0.5273108089333358, 0.6000732681239264, 0.6981578343076003,
			0.7955703752324775, 0.89818961593653, 1},
		alpha: 1,
	}
}

// ExtendedKindlmann is a Luminance-class ColorMap uses the colors from
// Kindlmann but also
// adds more hues by doing a more than 360 degree loop around the hues.
// This works because the endpoints have low saturation and very
// different brightness.
func ExtendedKindlmann() palette.ColorMap {
	return &luminance{
		colors: []cieLAB{
			{L: 0, A: 0, B: 0},
			{L: 13.371291966477482, A: 40.39368469479174, B: -47.73239449160565},
			{L: 25.072421338587574, A: -18.01441053740843, B: -5.313556572210176},
			{L: 37.411516363056116, A: -43.058336774976055, B: 39.30203907343062},
			{L: 49.75026355291354, A: -15.774050138318895, B: 53.507917567416094},
			{L: 61.643756252245225, A: 52.67703578954919, B: 43.82595336046358},
			{L: 74.93187540089825, A: 50.92061741619164, B: -30.235411697966242},
			{L: 87.64732748562544, A: 14.355163639545697, B: -17.471161313826332},
			{L: 100, A: 0, B: 0},
		},
		scalars: []float64{0, 0.13371291966477483, 0.25072421338587575, 0.37411516363056113,
			0.4975026355291354, 0.6164375625224523, 0.7493187540089825, 0.8764732748562544, 1},
		alpha: 1,
	}
}
