// Largely inspired by the descriptions in http://lab.medialab.sciences-po.fr/iwanthue/
// but written from scratch.

package colorful

import (
	"fmt"
	"math"
	"math/rand"
)

// The algorithm works in L*a*b* color space and converts to RGB in the end.
// L* in [0..1], a* and b* in [-1..1]
type lab_t struct {
	L, A, B float64
}

type SoftPaletteSettings struct {
	// A function which can be used to restrict the allowed color-space.
	CheckColor func(l, a, b float64) bool

	// The higher, the better quality but the slower. Usually two figures.
	Iterations int

	// Use up to 160000 or 8000 samples of the L*a*b* space (and thus calls to CheckColor).
	// Set this to true only if your CheckColor shapes the Lab space weirdly.
	ManySamples bool
}

// Yeah, windows-stype Foo, FooEx, screw you golang...
// Uses K-means to cluster the color-space and return the means of the clusters
// as a new palette of distinctive colors. Falls back to K-medoid if the mean
// happens to fall outside of the color-space, which can only happen if you
// specify a CheckColor function.
func SoftPaletteEx(colorsCount int, settings SoftPaletteSettings) ([]Color, error) {

	// Checks whether it's a valid RGB and also fulfills the potentially provided constraint.
	check := func(col lab_t) bool {
		c := Lab(col.L, col.A, col.B)
		return c.IsValid() && (settings.CheckColor == nil || settings.CheckColor(col.L, col.A, col.B))
	}

	// Sample the color space. These will be the points k-means is run on.
	dl := 0.05
	dab := 0.1
	if settings.ManySamples {
		dl = 0.01
		dab = 0.05
	}

	samples := make([]lab_t, 0, int(1.0/dl*2.0/dab*2.0/dab))
	for l := 0.0; l <= 1.0; l += dl {
		for a := -1.0; a <= 1.0; a += dab {
			for b := -1.0; b <= 1.0; b += dab {
				if check(lab_t{l, a, b}) {
					samples = append(samples, lab_t{l, a, b})
				}
			}
		}
	}

	// That would cause some infinite loops down there...
	if len(samples) < colorsCount {
		return nil, fmt.Errorf("palettegen: more colors requested (%v) than samples available (%v). Your requested color count may be wrong, you might want to use many samples or your constraint function makes the valid color space too small.", colorsCount, len(samples))
	} else if len(samples) == colorsCount {
		return labs2cols(samples), nil // Oops?
	}

	// We take the initial means out of the samples, so they are in fact medoids.
	// This helps us avoid infinite loops or arbitrary cutoffs with too restrictive constraints.
	means := make([]lab_t, colorsCount)
	for i := 0; i < colorsCount; i++ {
		for means[i] = samples[rand.Intn(len(samples))]; in(means, i, means[i]); means[i] = samples[rand.Intn(len(samples))] {
		}
	}

	clusters := make([]int, len(samples))
	samples_used := make([]bool, len(samples))

	// The actual k-means/medoid iterations
	for i := 0; i < settings.Iterations; i++ {
		// Reassing the samples to clusters, i.e. to their closest mean.
		// By the way, also check if any sample is used as a medoid and if so, mark that.
		for isample, sample := range samples {
			samples_used[isample] = false
			mindist := math.Inf(+1)
			for imean, mean := range means {
				dist := lab_dist(sample, mean)
				if dist < mindist {
					mindist = dist
					clusters[isample] = imean
				}

				// Mark samples which are used as a medoid.
				if lab_eq(sample, mean) {
					samples_used[isample] = true
				}
			}
		}

		// Compute new means according to the samples.
		for imean := range means {
			// The new mean is the average of all samples belonging to it..
			nsamples := 0
			newmean := lab_t{0.0, 0.0, 0.0}
			for isample, sample := range samples {
				if clusters[isample] == imean {
					nsamples++
					newmean.L += sample.L
					newmean.A += sample.A
					newmean.B += sample.B
				}
			}
			if nsamples > 0 {
				newmean.L /= float64(nsamples)
				newmean.A /= float64(nsamples)
				newmean.B /= float64(nsamples)
			} else {
				// That mean doesn't have any samples? Get a new mean from the sample list!
				var inewmean int
				for inewmean = rand.Intn(len(samples_used)); samples_used[inewmean]; inewmean = rand.Intn(len(samples_used)) {
				}
				newmean = samples[inewmean]
				samples_used[inewmean] = true
			}

			// But now we still need to check whether the new mean is an allowed color.
			if nsamples > 0 && check(newmean) {
				// It does, life's good (TM)
				means[imean] = newmean
			} else {
				// New mean isn't an allowed color or doesn't have any samples!
				// Switch to medoid mode and pick the closest (unused) sample.
				// This should always find something thanks to len(samples) >= colorsCount
				mindist := math.Inf(+1)
				for isample, sample := range samples {
					if !samples_used[isample] {
						dist := lab_dist(sample, newmean)
						if dist < mindist {
							mindist = dist
							newmean = sample
						}
					}
				}
			}
		}
	}
	return labs2cols(means), nil
}

// A wrapper which uses common parameters.
func SoftPalette(colorsCount int) ([]Color, error) {
	return SoftPaletteEx(colorsCount, SoftPaletteSettings{nil, 50, false})
}

func in(haystack []lab_t, upto int, needle lab_t) bool {
	for i := 0; i < upto && i < len(haystack); i++ {
		if haystack[i] == needle {
			return true
		}
	}
	return false
}

const LAB_DELTA = 1e-6

func lab_eq(lab1, lab2 lab_t) bool {
	return math.Abs(lab1.L-lab2.L) < LAB_DELTA &&
		math.Abs(lab1.A-lab2.A) < LAB_DELTA &&
		math.Abs(lab1.B-lab2.B) < LAB_DELTA
}

// That's faster than using colorful's DistanceLab since we would have to
// convert back and forth for that. Here is no conversion.
func lab_dist(lab1, lab2 lab_t) float64 {
	return math.Sqrt(sq(lab1.L-lab2.L) + sq(lab1.A-lab2.A) + sq(lab1.B-lab2.B))
}

func labs2cols(labs []lab_t) (cols []Color) {
	cols = make([]Color, len(labs))
	for k, v := range labs {
		cols[k] = Lab(v.L, v.A, v.B)
	}
	return cols
}
