// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Process-wide active theme state.
//!
//! The active document is published once and read by every per-slot
//! accessor in [`crate::theme`]. First access triggers a disk load of the
//! `default` theme; if that fails, an embedded B/W document takes over
//! and the [`is_fallback_active`] flag flips on so the draw layer can paint
//! a warning banner.

use std::sync::{ Arc, RwLock };

use super::assets;
use super::document::ThemeDocument;
use super::fallback;
use super::prefs::ThemeMode;

#[ derive( Clone ) ]
pub ( crate ) struct ActiveState
{
	/// Currently loaded theme document. The single source of truth —
	/// every consumer-facing accessor in this module reads through it
	/// against the active [`ThemeMode`].
	pub ( crate ) document:    Arc<ThemeDocument>,
	pub ( crate ) mode:        ThemeMode,
	/// `true` when the active document was produced by
	/// [`fallback::document`] because `ThemeDocument::find("default")`
	/// failed. The draw path stamps every frame with a red banner in
	/// this state so the user sees the missing-theme signal without
	/// the process having to abort.
	pub ( crate ) is_fallback: bool,
}

// `RwLock::new` and `Option::None` are both const, so this needs no lazy init.
static ACTIVE: RwLock<Option<ActiveState>> = RwLock::new( None );

/// Read the active state, loading the `default` theme from disk on
/// first access. If the `default` theme cannot be found in any search
/// path, the crate falls back to an in-memory B/W
/// [`fallback::document`], marks the state with `is_fallback = true`
/// (so the draw path can paint the warning banner), and logs a line
/// to stderr pointing at the install instructions. The process never
/// panics on first-access — making ltk embeddable inside programs
/// that want to handle missing theme gracefully.
pub ( crate ) fn ensure_active() -> ActiveState
{
	{
		let guard = ACTIVE.read().expect( "theme: ACTIVE poisoned" );
		if let Some( s ) = guard.as_ref()
		{
			return s.clone();
		}
	}
	let mut guard = ACTIVE.write().expect( "theme: ACTIVE poisoned" );
	if guard.is_none()
	{
		let ( doc, is_fallback ) = match ThemeDocument::find( "default" )
		{
			Ok( d )  => ( d, false ),
			Err( e ) =>
			{
				eprintln!
				(
					"[ltk] default theme not found ({e}); using embedded B/W \
					 fallback. Install the `ltk-theme-default` Debian package \
					 (Provides: ltk-theme) or set `LTK_THEMES_DIR` to a \
					 directory containing `default/theme.json` to get the \
					 real theme back."
				);
				( fallback::document(), true )
			}
		};
		*guard = Some( ActiveState
		{
			document: Arc::new( doc ),
			mode:     ThemeMode::Light,
			is_fallback,
		});
	}
	guard.as_ref().expect( "just installed" ).clone()
}

/// Install `doc` as the active theme. The current mode is preserved
/// (defaulting to [`ThemeMode::Light`] if nothing was set yet). Also
/// clears the fallback flag — an explicit install supersedes the
/// embedded B/W document and the warning banner stops painting from
/// the next frame on.
pub fn set_active_document( doc: ThemeDocument )
{
	let mut guard = ACTIVE.write().expect( "theme: ACTIVE poisoned" );
	let mode = guard.as_ref().map( |s| s.mode ).unwrap_or( ThemeMode::Light );
	*guard = Some( ActiveState
	{
		document:    Arc::new( doc ),
		mode,
		is_fallback: false,
	});
	// Drop any rasterised icons cached against the previous theme's
	// paths. Cache keys embed absolute paths so old entries are never
	// *wrong*, just dead memory; clearing keeps the working set small.
	assets::clear_svg_cache();
}

/// Switch the active variant. The document is left untouched — if nothing
/// has been loaded yet, the default theme is loaded first.
pub fn set_active_mode( mode: ThemeMode )
{
	// Ensure the default theme is loaded before mutating the mode so we
	// never publish an `ActiveState` with a missing document.
	let _ = ensure_active();
	let mut guard = ACTIVE.write().expect( "theme: ACTIVE poisoned" );
	if let Some( s ) = guard.as_mut() { s.mode = mode; }
}

/// The id of the active theme.
pub fn active_theme_id() -> String
{
	ensure_active().document.id.clone()
}

/// The active variant (light or dark).
pub fn active_mode() -> ThemeMode
{
	ensure_active().mode
}

/// The currently loaded theme document. Use this for slot-typed lookups
/// when the per-slot helpers ([`crate::theme::color`], [`crate::theme::surface()`], …) are not
/// expressive enough — e.g. iterating `mode.slots.entries`.
pub fn active_document() -> Arc<ThemeDocument>
{
	ensure_active().document
}

/// `true` when the active theme was produced by the embedded B/W
/// fallback because `ThemeDocument::find("default")` failed at
/// first-access. Flipped back to `false` the moment a consumer calls
/// [`set_active_document`] with any document (even another call to
/// the same fallback — the point is "this state came from an
/// explicit install, not from the missing-theme code path").
///
/// The draw layer reads this to decide whether to stamp each surface
/// with the red "install `ltk-theme-default`" banner; apps can read
/// it too, e.g. to log a diagnostic or surface a first-run
/// installation helper.
pub fn is_fallback_active() -> bool
{
	ensure_active().is_fallback
}
