package webui

import (
	"kjol/vdom"
	"kjol/wasmruntime"
)

// Dark mode.
//
// The kit is themed by TOKENS, not by a dark: variant on every class. A component says
// bg-surface, text-ink, border-line; the theme decides what those mean. Flipping the
// theme is therefore one class on <html> and a block of CSS variables — not four hundred
// class strings, each of which is a chance to forget one and leave a white card sitting
// in the middle of a dark page.
//
// Only genuinely COLOURED things (an alert's red tint, a validation message) carry dark:
// variants, because those are not surfaces or ink and there is no token that means "red,
// but for a dark background".
//
// The app must define the tokens and the variant in its stylesheet. See the example's
// css/app.css; the required set is listed in ThemeTokens below.

// ThemeTokens is the contract between the kit and an app's stylesheet: the CSS custom
// properties every component assumes exist, with light values in :root and dark values
// under .dark.
//
// It is a documentation constant, not code — but it is here, next to the components that
// depend on it, rather than in a README nobody opens when a card comes out the wrong
// colour.
const ThemeTokens = `
  --color-surface          page and card backgrounds
  --color-surface-muted    a subtle fill (table stripes, footers)
  --color-surface-raised   a hovered or filled row
  --color-surface-strong   the strongest neutral fill
  --color-line             ordinary borders and dividers
  --color-line-strong      an input's border — the one you must be able to see
  --color-ink              body text and headings
  --color-ink-soft         secondary text
  --color-ink-muted        labels, captions
  --color-ink-faint        placeholders, disabled text
  --color-accent           accent TEXT and icons (readable on the surface)
  --color-primary          accent FILLS, which carry white text
  --color-primary-hover
  --color-primary-subtle   a tinted panel
  --color-primary-border
`

// ThemeMode is what the user chose. System is the default: follow the OS until told
// otherwise, because a site that ignores the OS setting is a site that flashes white at
// someone who asked their whole computer not to.
type ThemeMode string

const (
	ThemeSystem ThemeMode = "system"
	ThemeLight  ThemeMode = "light"
	ThemeDark   ThemeMode = "dark"
)

// themeStorageKey is where the choice is kept. The bootstrap script in the document head
// reads the SAME key — see ThemeBootScript.
const themeStorageKey = "kjol-theme"

// Theme is the site-wide theme controller. Create one, call Init once after mount, and
// render its Toggle wherever the switch belongs.
type Theme struct {
	mode *vdom.Signal[ThemeMode]
	// dark is the RESOLVED answer: what "system" actually means right now.
	dark *vdom.Signal[bool]
	stop wasmruntime.Unsub
}

// NewTheme creates the controller. It reads no storage and touches no DOM — Init does
// that, once the document exists.
func NewTheme() *Theme {
	return &Theme{
		mode: vdom.NewSignal(ThemeSystem),
		dark: vdom.NewSignal(false),
	}
}

// Init adopts the stored choice and starts following the OS while the mode is "system".
// Call it once, after the app has mounted.
//
// It does not FLASH, because it is not what puts the class on <html> for the first
// paint: ThemeBootScript already did that, before any of this code existed. Init only
// takes over.
func (t *Theme) Init() {
	if s, ok := wasmruntime.StorageGet(themeStorageKey); ok {
		switch ThemeMode(s) {
		case ThemeLight:
			t.mode.Set(ThemeLight)
		case ThemeDark:
			t.mode.Set(ThemeDark)
		}
	}
	t.apply()

	// Follow the OS, but only while the user has not overridden it.
	t.stop = wasmruntime.OnMediaChange("(prefers-color-scheme: dark)", func(bool) {
		if t.mode.Get() == ThemeSystem {
			t.apply()
		}
	})
}

// Dispose stops following the OS.
func (t *Theme) Dispose() {
	if t.stop != nil {
		t.stop()
	}
}

// Mode is the user's choice; IsDark is what that currently resolves to.
func (t *Theme) Mode() ThemeMode { return t.mode.Get() }
func (t *Theme) IsDark() bool    { return t.dark.Get() }

// Set changes the theme. "system" forgets the choice entirely rather than storing the
// current resolution — otherwise "follow my OS" would quietly freeze at whatever the OS
// happened to say the day you chose it.
func (t *Theme) Set(m ThemeMode) {
	t.mode.Set(m)
	if m == ThemeSystem {
		wasmruntime.StorageRemove(themeStorageKey)
	} else {
		wasmruntime.StorageSet(themeStorageKey, string(m))
	}
	t.apply()
}

// Toggle flips between light and dark. It resolves "system" first, so the first click
// does what it looks like it will do — the opposite of what you are looking at.
func (t *Theme) Toggle() {
	if t.resolve() {
		t.Set(ThemeLight)
		return
	}
	t.Set(ThemeDark)
}

// resolve turns the mode into a yes or no.
func (t *Theme) resolve() bool {
	switch t.mode.Get() {
	case ThemeDark:
		return true
	case ThemeLight:
		return false
	default:
		return wasmruntime.PrefersDark()
	}
}

func (t *Theme) apply() {
	dark := t.resolve()
	t.dark.Set(dark)
	wasmruntime.SetRootClass("dark", dark)
}

// ThemeToggleProps configures the switch.
type ThemeToggleProps struct {
	Class string
	// Small renders an icon-only button.
	Small bool
}

// ThemeToggle is the switch: one button, showing the theme you would get by pressing it.
func (t *Theme) ThemeToggle(p ThemeToggleProps) *vdom.VNode {
	icon, label := "moon", "Dark"
	if t.IsDark() {
		icon, label = "sun", "Light"
	}

	cls := "inline-flex items-center gap-2 rounded-default border border-line px-2.5 py-1.5 text-sm text-ink-soft hover:bg-surface-raised hover:text-ink"
	if p.Small {
		cls = "inline-flex h-8 w-8 items-center justify-center rounded-default border border-line text-ink-soft hover:bg-surface-raised hover:text-ink"
	}

	mods := []vdom.Mod{
		vdom.Attr("type", "button"),
		vdom.Attr("class", cx(cls, p.Class)),
		vdom.Attr("aria-label", "Switch to "+label+" theme"),
		vdom.Attr("title", "Switch to "+label+" theme"),
		vdom.On(vdom.EVENT_CLICK, t.Toggle),
		IconInline(icon, 15, ""),
	}
	if !p.Small {
		mods = append(mods, vdom.Span(vdom.Text(label)))
	}
	return vdom.Button(mods...)
}

// ThemeBootScript is the inline script an app puts in its <head>, BEFORE any stylesheet
// or markup.
//
// It exists to prevent the flash. The server cannot read localStorage, so it cannot know
// which theme to render; if the class were applied by the WebAssembly after it loads,
// every dark-mode user would be shown a white page for as long as the binary takes to
// download, and then have it yanked out from under them. This runs first, synchronously,
// and the first paint is already correct.
//
// It is ten lines of JavaScript in a project that has none. That is the price of the
// browser giving the page no way to ask about localStorage before it paints, and it is
// worth paying — the alternative is a white flash on every single load.
const ThemeBootScript = `<script>(function(){try{
  var m = localStorage.getItem("kjol-theme");
  var dark = m === "dark" || (!m && matchMedia("(prefers-color-scheme: dark)").matches);
  if (dark) document.documentElement.classList.add("dark");
}catch(e){}})();</script>`
