// Package oklab implements the Oklab color space, as described at https://bottosson.github.io/posts/oklab/
package oklab

// L: 0.000000–1.000000
// A: -0.233888–0.276216
// B: -0.311528–0.198570

// L: 0.000000–1.000000
// C: 0.000000–0.322491
// H: -3.141592–3.141592

import (
	"image/color"
	"math"
)

type Oklab struct {
	L float64 // Perceived lightness
	A float64 // How green/red the color is
	B float64 // How blue/yellow the color is
}

type Oklch struct {
	L float64 // Perceived lightness
	C float64 // Chroma
	H float64 // Hue
}

var OklabModel = color.ModelFunc(oklabModel)
var OklchModel = color.ModelFunc(oklchModel)

// See image.Color.
func (c Oklab) RGBA() (uint32, uint32, uint32, uint32) {
	r, g, b := c.SRGB()
	r, g, b = clampf(r), clampf(g), clampf(b)
	return (uint32(0x1fffe*r) + 1) >> 1, (uint32(0x1fffe*g) + 1) >> 1, (uint32(0x1fffe*b) + 1) >> 1, 0xffff
}

// Convert to linear sRGB.
// See https://bottosson.github.io/posts/oklab/
func (c Oklab) LinearSRGB() (float64, float64, float64) {
	l_ := c.L + 0.3963377774*c.A + 0.2158037573*c.B
	m_ := c.L - 0.1055613458*c.A - 0.0638541728*c.B
	s_ := c.L - 0.0894841775*c.A - 1.2914855480*c.B

	l := l_ * l_ * l_
	m := m_ * m_ * m_
	s := s_ * s_ * s_

	r := 4.0767416621*l - 3.3077115913*m + 0.2309699292*s
	g := -1.2684380046*l + 2.6097574011*m - 0.3413193965*s
	b := -0.0041960863*l - 0.7034186147*m + 1.7076147010*s

	return r, g, b
}

// Convert to sRGB.
// See https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F
func (c Oklab) SRGB() (float64, float64, float64) {
	r, g, b := c.LinearSRGB()
	return linearSrgbToSrgb(r), linearSrgbToSrgb(g), linearSrgbToSrgb(b)
}

// Convert to LCh, which is Oklab in polar.
func (c Oklab) Oklch() Oklch {
	return Oklch{
		L: c.L,
		C: math.Sqrt(c.A*c.A + c.B*c.B),
		H: math.Atan2(c.B, c.A),
	}
}

// See image.Color.
func (c Oklch) RGBA() (uint32, uint32, uint32, uint32) {
	return c.Oklab().RGBA()
}

// Convert to Oklab.
func (c Oklch) Oklab() Oklab {
	return Oklab{
		L: c.L,
		A: c.C * math.Cos(c.H),
		B: c.C * math.Sin(c.H),
	}
}

func oklabModel(c color.Color) color.Color {
	r8, g8, b8, a8 := c.RGBA()
	r := float64(r8) / float64(a8)
	g := float64(g8) / float64(a8)
	b := float64(b8) / float64(a8)

	r, g, b = srgbToLinearSrgb(r), srgbToLinearSrgb(g), srgbToLinearSrgb(b)

	l := 0.4122214708*r + 0.5363325363*g + 0.0514459929*b
	m := 0.2119034982*r + 0.6806995451*g + 0.1073969566*b
	s := 0.0883024619*r + 0.2817188376*g + 0.6299787005*b

	l_ := math.Cbrt(l)
	m_ := math.Cbrt(m)
	s_ := math.Cbrt(s)

	return Oklab{
		L: 0.2104542553*l_ + 0.7936177850*m_ - 0.0040720468*s_,
		A: 1.9779984951*l_ - 2.4285922050*m_ + 0.4505937099*s_,
		B: 0.0259040371*l_ + 0.7827717662*m_ - 0.8086757660*s_,
	}
}

func oklchModel(c color.Color) color.Color {
	return oklabModel(c).(Oklab).Oklch()
}

// Convert a linear sRGB color component to sRGB.
// See https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F
func linearSrgbToSrgb(x float64) float64 {
	if x >= 0.0031308 {
		return 1.055*math.Pow(x, 1.0/2.4) - 0.055
	}
	return 12.92 * x
}

// Convert an sRGB color component to linear sRGB.
// See https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F
func srgbToLinearSrgb(x float64) float64 {
	if x >= 0.04045 {
		return math.Pow((x+0.055)/(1+0.055), 2.4)
	}
	return x / 12.92
}

func clampf(x float64) float64 {
	if x < 0 {
		return 0
	} else if x > 1 {
		return 1
	}
	return x
}