//! Crustace user configuration.
//!
//! Lives at `~/.config/eydos/crustace.toml`. Five top-level sections:
//!
//! ```toml
//! [shell]                  # lock / screen-off timeouts, theme preference
//! [display]                # mobile / desktop / auto
//! [dock]                   # favorites bar, shared across pages and device classes
//! [mobile]                 # homescreen pages authored against a portrait grid
//! [desktop]                # homescreen single page for desktop sessions
//! ```
//!
//! `[mobile]` stores `pages = [ { items = [ ... ] }, ... ]`; `[desktop]`
//! stores a flat `items = [ ... ]` since by design the desktop homescreen is
//! a single page.
//!
//! Items carry `type = "app"` today. `type = "widget"` is reserved for the
//! upcoming ltk widget API; unknown types are skipped with a warning rather
//! than failing the load, so third-party widget types can roll out without
//! breaking existing user configs.

use anyhow::Result;
use ltk::ThemePreference;
use serde::{ Deserialize, Serialize };
use std::fs;
use std::path::PathBuf;

// =============================================================================
// ROOT
// =============================================================================

#[derive( Debug, Clone, Default, Serialize, Deserialize )]
pub struct Config
{
	#[serde( default )]
	pub shell:   ShellSection,
	#[serde( default )]
	pub display: DisplayConfig,
	#[serde( default )]
	pub dock:    DockConfig,
	#[serde( default )]
	pub mobile:  MobileConfig,
	#[serde( default )]
	pub desktop: DesktopConfig,
}

// =============================================================================
// [shell]
// =============================================================================

#[derive( Debug, Clone, Serialize, Deserialize )]
pub struct ShellSection
{
	#[serde( default = "default_lock_timeout" )]
	pub lock_timeout_secs:      u64,
	#[serde( default = "default_screen_off_timeout" )]
	pub screen_off_timeout_secs: u64,
	#[serde( default )]
	pub theme: ThemePreference,
	/// Fraction of the screen width the user must drag horizontally before a
	/// mobile page swipe commits. Below this, the drag snaps back to the
	/// current page; at or above it, the homescreen pages to the left or
	/// right. Clamped to a sane band at load time so `0.0` / `10.0` entries
	/// don't break the gesture.
	#[serde( default = "default_swipe_threshold" )]
	pub swipe_threshold: f32,
	/// Maximum time the launch splash stays on screen waiting for the
	/// app's first toplevel. Acts as a safety net: a crashed or
	/// blocked launch would otherwise leave the splash up indefinitely.
	#[serde( default = "default_splash_timeout" )]
	pub splash_timeout_secs: u64,
}

impl Default for ShellSection
{
	fn default() -> Self
	{
		Self {
			lock_timeout_secs:       default_lock_timeout(),
			screen_off_timeout_secs: default_screen_off_timeout(),
			theme:                   ThemePreference::Auto,
			swipe_threshold:         default_swipe_threshold(),
			splash_timeout_secs:     default_splash_timeout(),
		}
	}
}

fn default_lock_timeout()       -> u64 { 30 }
fn default_screen_off_timeout() -> u64 { 60 }
fn default_swipe_threshold()    -> f32 { 0.5 }
fn default_splash_timeout()     -> u64 { 15 }

// =============================================================================
// [display]
// =============================================================================

#[derive( Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default )]
#[serde( rename_all = "lowercase" )]
pub enum DisplayMode
{
	Mobile,
	Desktop,
	#[default]
	Auto,
}

impl DisplayMode
{
	/// Resolve to a concrete layout. `Auto` follows forge's decision (`FORGE_MOBILE`, inherited from the compositor) and falls back to orientation when running outside forge.
	pub fn use_desktop( self, is_landscape: bool ) -> bool
	{
		match self
		{
			DisplayMode::Mobile  => false,
			DisplayMode::Desktop => true,
			DisplayMode::Auto    => match forge_mobile()
			{
				Some( mobile ) => !mobile,
				None           => is_landscape,
			},
		}
	}
}

fn forge_mobile() -> Option<bool>
{
	static CACHE: std::sync::OnceLock<Option<bool>> = std::sync::OnceLock::new();
	*CACHE.get_or_init( || match std::env::var( "FORGE_MOBILE" ).ok()?.as_str()
	{
		"1" => Some( true ),
		"0" => Some( false ),
		_   => None,
	} )
}

#[derive( Debug, Clone, Default, Serialize, Deserialize )]
pub struct DisplayConfig
{
	#[serde( default )]
	pub mode: DisplayMode,
}

// =============================================================================
// [dock]
// =============================================================================

#[derive( Debug, Clone, Serialize, Deserialize )]
pub struct DockConfig
{
	#[serde( default )]
	pub items: Vec<DockItem>,
	/// Pin the desktop dock as a layer-shell `Top` overlay with
	/// reserved exclusive zone so it stays visible above app windows.
	/// When `false` the dock lives inside the homescreen surface
	/// (legacy behaviour) and disappears as soon as an app opens.
	/// Mobile is unaffected — the column-aligned dock is part of the
	/// homescreen grid by design.
	#[serde( default = "default_dock_always_visible" )]
	pub always_visible: bool,
}

impl Default for DockConfig
{
	fn default() -> Self
	{
		Self { items: Vec::new(), always_visible: default_dock_always_visible() }
	}
}

fn default_dock_always_visible() -> bool { true }

/// An entry in the favorites bar. Dock items are laid out linearly so they
/// do not carry grid coordinates — in contrast to [`GridItem`].
#[derive( Debug, Clone, Serialize, Deserialize )]
#[serde( tag = "type", rename_all = "lowercase" )]
pub enum DockItem
{
	/// An installed app, identified by the `.desktop` filename with its
	/// extension (e.g. `firefox.desktop`, `org.gnome.Console.desktop`). The
	/// extension is kept so the launcher can dispatch across app types (a
	/// future Android APK entry would be `com.whatsapp.WhatsApp.apk`).
	App { app_id: String },
}

// =============================================================================
// [mobile] / [desktop]
// =============================================================================

#[derive( Debug, Clone, Default, Serialize, Deserialize )]
pub struct MobileConfig
{
	#[serde( default )]
	pub pages: Vec<Page>,
}

#[derive( Debug, Clone, Default, Serialize, Deserialize )]
pub struct DesktopConfig
{
	#[serde( default )]
	pub items: Vec<GridItem>,
}

#[derive( Debug, Clone, Default, Serialize, Deserialize )]
pub struct Page
{
	#[serde( default )]
	pub items: Vec<GridItem>,
}

/// An entry placed on a homescreen grid. Carries the cell it occupies
/// (and, in a future revision, `size` for widgets that span multiple cells).
#[derive( Debug, Clone, Serialize, Deserialize )]
#[serde( tag = "type", rename_all = "lowercase" )]
pub enum GridItem
{
	/// An installed app pinned at grid cell `[col, row]`. See [`DockItem::App`]
	/// for the `app_id` format.
	App { app_id: String, cell: [u32; 2] },
}

// =============================================================================
// Load / save
// =============================================================================

impl Config
{
	pub fn config_path() -> PathBuf
	{
		dirs::config_dir()
			.unwrap_or_else( || PathBuf::from( "/etc" ) )
			.join( "eydos" )
			.join( "crustace.toml" )
	}

	pub fn load() -> Result<Self>
	{
		let path = Self::config_path();
		if path.exists()
		{
			let content = fs::read_to_string( &path )?;
			let config: Config = toml::from_str( &content )?;
			Ok( config )
		} else {
			let config = Config::seeded();
			config.save()?;
			Ok( config )
		}
	}

	pub fn save( &self ) -> Result<()>
	{
		let path = Self::config_path();
		if let Some( parent ) = path.parent()
		{
			fs::create_dir_all( parent )?;
		}
		let content = toml::to_string_pretty( self )?;
		fs::write( path, content )?;
		Ok( () )
	}

	/// Bootstrap config written the first time Crustace runs for a user that
	/// has no existing `crustace.toml`. Seeds the dock with a small set of
	/// common favorites; the homescreen grid starts empty so the user can
	/// populate it by dragging from the launcher. Entries whose `app_id`
	/// does not resolve on the target system are silently skipped at render
	/// time.
	fn seeded() -> Self
	{
		Self {
			shell:   ShellSection::default(),
			display: DisplayConfig::default(),
			dock:    DockConfig {
				items: vec![
					DockItem::App { app_id: "firefox-esr.desktop".into()          },
					DockItem::App { app_id: "org.gnome.Console.desktop".into()    },
					DockItem::App { app_id: "org.gnome.Nautilus.desktop".into()   },
					DockItem::App { app_id: "signal-desktop.desktop".into()       },
				],
				always_visible: default_dock_always_visible(),
			},
			mobile:  MobileConfig  {
				pages: vec![ Page::default() ],
			},
			desktop: DesktopConfig { items: vec![] },
		}
	}
}
