// Jonathan Frech, 2026-09-05. Go 1.26.1.
//
// .cf.web https://blog.jfrech.com/302/ 2026-09-05
package main

import (
	"bufio"
	"errors"
	"fmt"
	"image"
	"image/color"
	"image/png"
	"log"
	"math"
	"os"
	"path/filepath"
)

// image dimensions
const w, h = 3840, 2160

func RenderHSL() image.Image {
	m := image.NewRGBA(image.Rect(0, 0, w, h))
	for y := range h {
		for x := range w {
			m.Set(x, y, HSL{float64(x) * 360 / float64(w), 1, .5})
		}
	}
	return m
}

func RenderOklab() image.Image {
	m := image.NewRGBA(image.Rect(0, 0, w, h))
	for y := range h {
		for x := range w {
			m.Set(x, y, LCh{.75, .25, float64(x) * 2 * math.Pi / float64(w)})
		}
	}
	return m
}

//

type HSL struct {
	// 0.0 <= HueDegrees < 360.0, else interpreted modulo 360.
	// From 0.0 increasing, the colours are red, orange, yellow, green, light blue, blue, magenta, pink, red.
	HueDegrees float64

	// 0.0 <= Saturation <= 1.0, else clamped.
	//
	// Saturation == 0.0 is pale, Saturation == 1.0 is bright.
	Saturation float64

	// 0.0 <= Luminosity <= 1.0, else clamped.
	//
	// Luminosity == 0.0 is black, Luminosity == 0.5 is colorful, Luminosity == 1.0 is white.
	Luminosity float64
}

func (c HSL) canon() HSL {
	return HSL{
		math.Mod(math.Mod(c.HueDegrees, 360.0)+360.0, 360.0),
		max(0, min(1, c.Saturation)),
		max(0, min(1, c.Luminosity)),
	}
}

var _ color.Color = HSL{}

func (c HSL) RGBA() (r, g, b, a uint32) { return c.rgba().RGBA() }

func (hsl HSL) rgba() color.RGBA {
	hsl = hsl.canon()
	h, s, l := hsl.HueDegrees, hsl.Saturation, hsl.Luminosity

	c := (1 - math.Abs(2*l-1)) * s
	hh := h / 60.0
	x := c * (1 - math.Abs(math.Mod(hh, 2)-1))
	r, g, b := func() (float64, float64, float64) {
		switch {
		case 0 <= hh && hh < 1:
			return c, x, 0
		case 1 <= hh && hh < 2:
			return x, c, 0
		case 2 <= hh && hh < 3:
			return 0, c, x
		case 3 <= hh && hh < 4:
			return 0, x, c
		case 4 <= hh && hh < 5:
			return x, 0, c
		case 5 <= hh && hh < 6:
			return c, 0, x
		default:
			panic("unreachable")
		}
	}()
	m := l - c/2.0
	return color.RGBA{
		R: uint8((r + m) * 255.0),
		G: uint8((g + m) * 255.0),
		B: uint8((b + m) * 255.0),
		A: 255,
	}
}

//

// Polar form of a point in Oklab colour space: Lightness, chroma, hue in radians.
type LCh struct{ L, C, H float64 }

var _ color.Color = LCh{}

func (c LCh) RGBA() (r, g, b, a uint32) { return c.Lab().RGBA() }

func (c LCh) Lab() Lab {
	// .cf.web https://bottosson.github.io/posts/oklab/#the-oklab-color-space 2026-09-04
	return Lab{c.L, c.C * math.Cos(c.H), c.C * math.Sin(c.H)}
}

type Lab struct{ L, A, B float64 }

func (c Lab) RGBA() (r, g, b, a uint32) { return c.LinearSRGB().RGBA() }

var _ color.Color = Lab{}

func (c Lab) LinearSRGB() LinearSRGB {
	// .cf.web https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab 2026-09-04

	var (
		l_ float64 = c.L + 0.3963377774*c.A + 0.2158037573*c.B
		m_ float64 = c.L - 0.1055613458*c.A - 0.0638541728*c.B
		s_ float64 = c.L - 0.0894841775*c.A - 1.2914855480*c.B
	)

	var (
		l float64 = l_ * l_ * l_
		m float64 = m_ * m_ * m_
		s float64 = s_ * s_ * s_
	)

	return LinearSRGB{
		+4.0767416621*l - 3.3077115913*m + 0.2309699292*s,
		-1.2684380046*l + 2.6097574011*m - 0.3413193965*s,
		-0.0041960863*l - 0.7034186147*m + 1.7076147010*s,
	}
}

type LinearSRGB struct{ R, G, B float64 }

var _ color.Color = LinearSRGB{}

func (c LinearSRGB) RGBA() (r, g, b, a uint32) { return c.rgba().RGBA() }

func (c LinearSRGB) rgba() color.RGBA {
	// .cf.web https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F 2026-09-04
	f := func(x float64) float64 {
		switch {
		case x >= 0.0031308:
			return 1.055*math.Pow(x, 1/2.4) - 0.055
		default:
			return 12.92 * x
		}

	}

	return color.RGBA{
		uint8(max(0, min(255, math.RoundToEven(f(c.R)*0xff)))),
		uint8(max(0, min(255, math.RoundToEven(f(c.G)*0xff)))),
		uint8(max(0, min(255, math.RoundToEven(f(c.B)*0xff)))),
		0xff,
	}
}

//

func main() {
	if err := Main(); err != nil {
		log.Fatal(err)
	}
}

func Main() error {
	ostmpname, err := os.MkdirTemp("", "")
	if err != nil {
		return err
	}

	type NameRender struct {
		Name   string
		Render func() image.Image
	}
	for _, nr := range []NameRender{
		{"rainbow-hsl.png", RenderHSL},
		{"rainbow-oklab.png", RenderOklab},
	} {
		osname := filepath.Join(ostmpname, nr.Name)
		if err := encodePNG(osname, nr.Render()); err != nil {
			return err
		}
		_, err = fmt.Fprintf(os.Stdout, "%s\n", osname)
		if err != nil {
			return err
		}
	}

	return nil
}

func encodePNG(osname string, m image.Image) error {
	f, err := os.OpenFile(osname, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
	if err != nil {
		return err
	}
	defer f.Close()

	bw := bufio.NewWriter(f)
	return errors.Join(
		png.Encode(bw, m), bw.Flush(),
		f.Sync(), f.Close(),
	)
}
