//! Shell-level state container.
//!
//! Owns the [`Crustace`] struct — the single root that every subsystem
//! (paging, drag-and-drop, overlays, the ltk `App` trait impl) borrows
//! from. Kept separate from `app.rs` so the trait impl can stay focused
//! on plumbing Messages through the shell while the struct definition
//! sits next to its constructor and its bookkeeping helpers.

use std::sync::Arc;

use crate::appinfo::AppInfo;
use crate::config::Config;
use crate::dock_reveal::DockReveal;
use crate::drag::DraggingItem;
use crate::editmenu::EditMenu;
use crate::ipc::ForgeIpc;
use crate::launcher::icon_cache::IconCache;
use crate::launcher::AppLauncher;
use crate::mpris;
use crate::osd::Osd;
use crate::paging::PageAnim;
use crate::powermenu::PowerMenu;
use crate::quicksettings::QuickSettings;
use crate::running::RunningApps;
use crate::splash::SplashState;
use crate::system::dbus_service::SystemPoller;
use crate::system::SystemState;

use std::cell::RefCell;

pub struct Crustace
{
	// Components — topbar is now drawn by forge directly.
	pub( crate ) launcher: AppLauncher,
	pub( crate ) icon_cache: RefCell<IconCache>,

	// System state
	pub( crate ) system_state: SystemState,
	pub( crate ) system_poller: SystemPoller,

	// Quick settings panel (slide-down animation, toggle grid, sub-pickers).
	pub( crate ) quick_settings: QuickSettings,

	pub( crate ) dock_reveal: DockReveal,

	// Live toplevel inventory — what apps are currently running, keyed
	// by the matching `.desktop` `app_id`. Fed by `App::on_toplevel_event`
	// from the `ext-foreign-toplevel-list-v1` binding in ltk, consumed
	// by the dock to draw the running-app indicator dot under pinned
	// icons and to append "temporary" slots on the right for running
	// apps that have no pinned entry.
	pub( crate ) running_apps: RunningApps,

	/// Pending launch splashes. Populated by `Message::LaunchApp` /
	/// `Message::LauncherSearchSubmit` and dismissed when the
	/// compositor reports the matching toplevel.
	pub( crate ) splash: SplashState,

	/// App ids awaiting an `xdg-activation-v1` token before spawning.
	/// Drained by `App::take_activation_requests`; each comes back as
	/// `Message::ActivationTokenReady` once the compositor issues the token.
	pub( crate ) pending_token_requests: Vec<String>,

	// Calendar popup (shown when topbar clock is clicked).
	pub( crate ) calendar_visible: bool,
	pub( crate ) calendar_view_year:  i32,
	pub( crate ) calendar_view_month: u8,

	// Power menu
	pub( crate ) power_menu: PowerMenu,

	// Homescreen
	pub( crate ) homescreen: crate::homescreen::Homescreen,
	pub( crate ) screen_width:  u32,
	pub( crate ) screen_height: u32,
	pub( crate ) display_scale: u32,

	// IPC
	pub( crate ) forge_ipc: Option<ForgeIpc>,

	// OSD toast overlay
	pub( crate ) osd: Osd,

	// Media player
	pub( crate ) media_state:   Option<mpris::MediaState>,
	pub( crate ) mpris_poller:  mpris::MprisPoller,

	// Theme: keep the user's ThemePreference so `Auto` can flip Light/Dark
	// on the fly as the clock crosses the sunrise / sunset threshold. The
	// cached `current_mode` avoids writing through the ACTIVE lock every
	// tick — we only call `ltk::set_active_mode` when the resolved mode
	// actually changes.
	pub( crate ) theme_preference: ltk::ThemePreference,
	pub( crate ) current_mode:     ltk::ThemeMode,

	// Full user configuration — drives the dock, the homescreen grid layout,
	// display mode, etc. Homescreen reads this on every `view()` instead of
	// owning its own copy so edit-mode writes (phase 3) are visible
	// immediately without a struct-wide sync step.
	pub( crate ) config: Config,

	// Mobile homescreen paging state.
	/// Index into `config.mobile.pages` of the page currently centred on
	/// screen. Ignored by the desktop homescreen, which is single-page.
	pub( crate ) current_page: usize,
	/// Horizontal offset (logical pixels) applied to every rendered page.
	/// Positive values slide pages to the right (i.e. the user is
	/// revealing the previous page). Reset to zero at rest.
	pub( crate ) drag_dx:      f32,
	/// Settle tween in flight; `None` at rest.
	pub( crate ) page_anim:    Option<PageAnim>,
	/// `true` while a finger page-drag (and its settle) owns the slide: the
	/// pages ride sliding subsurfaces and the main paints only wallpaper +
	/// dock + indicator. Cleared at settle end. Page-indicator jumps leave it
	/// `false` and keep the old on-main render.
	pub( crate ) pages_sub_active: bool,
	/// Frames keeping the interactive grid painted on the main while the page
	/// subsurfaces raster and map, so the handoff at drag start does not flash
	/// an iconless wallpaper. Mirror of the launcher's `reveal_handoff`.
	pub( crate ) pages_enter_handoff: u8,
	/// Frames keeping the page subsurfaces shown after the settle ends, while
	/// the main re-rasters the grid back, so dropping them does not flash the
	/// frozen grid-less main for a frame.
	pub( crate ) pages_exit_handoff: u8,
	/// `current_page` the page subsurfaces were last emitted for. A page that
	/// is in the new window but was NOT in this one is freshly scrolled in and
	/// builds its full tree; pages already visible emit a placeholder and keep
	/// their cached texture. `None` at drag start forces a full build of the
	/// window. `Cell` because `build_subsurfaces` runs behind `&self`.
	pub( crate ) pages_last_center: std::cell::Cell<Option<usize>>,
	/// Wall-clock of the last applied page-drag sample. The Librem5 touchscreen
	/// emits motion far faster than the compositor presents; without coalescing,
	/// each sample repositions (and commits) the page subsurfaces, flooding the
	/// wayland socket until a flush blocks and the drag freezes. Samples within
	/// the vsync window are dropped so the reposition commits stay near 60 Hz.
	pub( crate ) pages_drag_last_applied: Option<std::time::Instant>,
	/// Active drag state (any origin: launcher / dock / grid); `None` when
	/// not dragging.
	pub( crate ) dragging_item: Option<DraggingItem>,
	/// Cursor position (logical px) at the first motion sample after the
	/// long-press fired. `None` if the finger has not moved yet. Used
	/// together with [`crate::drag::DRAG_THRESHOLD_SQ`] to distinguish a
	/// plain hold (menu stays, drop discarded) from a genuine drag.
	pub( crate ) drag_origin_cursor: Option<( f32, f32 )>,
	/// Becomes `true` once the cursor has travelled past the drag
	/// threshold since the long-press. Cleared on release. Drives the
	/// drop-vs-discard decision in `on_drop` and dismisses the edit menu
	/// the moment the user starts actually dragging.
	pub( crate ) drag_moved: bool,

	// Edit mode — shell-level state; the edit_menu overlay is opened by
	// long-press on a homescreen icon and owns the selected item's
	// metadata. `edit_mode` stays on after a successful drop / removal so
	// the user can keep rearranging; it clears when they cancel or tap
	// outside the menu.
	pub( crate ) edit_mode: bool,
	pub( crate ) edit_menu: Option<EditMenu>,
	pub( crate ) app_info:  Option<AppInfo>,

	pub( crate ) loginmanager_sock_path: Option<String>,
}

impl Crustace
{
	pub fn new( config: Config ) -> Self
	{
		// Decode the homescreen wallpaper exactly once. The launcher uses a
		// separate (lockscreen) bundle so it's not part of this Arc; the
		// homescreen is the only consumer of `wallpaper` today, but the
		// `Arc` wrapping is preserved so swapping a fresh bundle in
		// `apply_theme_mode` doesn't perturb the type expected by
		// `Homescreen::set_wallpaper`.
		let wallpaper = Arc::new( crate::assets::decode_wallpaper_bundle( 0, 0 ) );

		let launcher = AppLauncher::new( &config );

		// Shared icon cache for app icons (homescreen, launcher).
		// QS pre-decodes all its bundled chrome glyphs in
		// `QuickSettings::new` and no longer needs it.
		let icon_cache          = IconCache::new();
		let mut quick_settings  = QuickSettings::new();

		use chrono::Timelike as _;
		let theme_preference = config.shell.theme;
		let current_mode     = theme_preference.resolve( chrono::Local::now().hour() );

		// Seed the QS Dark pill from the resolved mode so the first pull-
		// down reflects the current theme instead of always showing the
		// toggle in the "off" position when launched via `crustace --dark`
		// or `config.shell.theme = "dark"`.
		quick_settings.toggle_state.dark_mode = current_mode == ltk::ThemeMode::Dark;

		// Forge renders its own chrome (topbar clock / battery / icons, SSD
		// title bars) in a separate process and therefore has its own copy
		// of ltk's `active_mode`. Push the mode we just resolved to forge
		// via IPC so both processes paint against the same variant on the
		// first frame — without this, forge falls back to gsettings'
		// `color-scheme`, which ignores the `--dark` / `--light` CLI flags
		// and the `config.shell.theme` override.
		let forge_ipc = ForgeIpc::new();
		if let Some( ipc ) = &forge_ipc
		{
			ipc.set_theme( current_mode );
		}
		Self::push_system_color_scheme( current_mode );

		let homescreen = crate::homescreen::Homescreen::new( wallpaper );

		let loginmanager_sock_path = std::env::var( "LOGINMANAGER_SESSION_SOCK" ).ok();

		Self {
			launcher,
			icon_cache: RefCell::new( icon_cache ),
			system_state: SystemState::default(),
			system_poller: SystemPoller::new(),
			quick_settings,
			dock_reveal: DockReveal::new(),
			running_apps: RunningApps::new(),
			splash: SplashState::new(),
			pending_token_requests: Vec::new(),
			calendar_visible:    false,
			calendar_view_year:  0,
			calendar_view_month: 1,
			power_menu: PowerMenu::new(),
			homescreen,
			screen_width: 0,
			screen_height: 0,
			display_scale: 1,
			forge_ipc,
			osd: Osd::new(),
			media_state:  None,
			mpris_poller: mpris::MprisPoller::new(),
			theme_preference,
			current_mode,
			config,
			current_page: 0,
			drag_dx:      0.0,
			page_anim:    None,
			pages_sub_active:      false,
			pages_enter_handoff:   0,
			pages_exit_handoff:    0,
			pages_last_center:     std::cell::Cell::new( None ),
			pages_drag_last_applied: None,
			dragging_item:      None,
			drag_origin_cursor: None,
			drag_moved:         false,
			edit_mode: false,
			edit_menu: None,
			app_info:  None,
			loginmanager_sock_path,
		}
	}

	/// Apply a resolved theme mode shell-wide: update the cached
	/// `current_mode`, push it into ltk's active state, sync the QS Dark
	/// pill, re-decode every wallpaper bundle (homescreen, launcher, QS
	/// lockscreen) so they pick the matching variant from the active
	/// theme, and forward the mode to forge over IPC.
	///
	/// No-op when the mode already matches `current_mode`. Used by the
	/// `Tick` arm for `ThemePreference::Auto` sunrise / sunset transitions
	/// — those should NOT alter `theme_preference`, so callers that mean
	/// "the user explicitly asked for this mode" should go through
	/// [`Self::set_theme_explicit`] instead.
	pub( crate ) fn apply_theme_mode( &mut self, mode: ltk::ThemeMode )
	{
		if mode == self.current_mode { return; }
		self.current_mode = mode;
		ltk::set_active_mode( mode );
		self.quick_settings.toggle_state.dark_mode = mode == ltk::ThemeMode::Dark;
		// Re-decode after `set_active_mode` so `theme_branding_image`
		// resolves against the new mode's `branding/{mode}/` tree. The
		// homescreen wallpaper is decoded once and the `Arc` is shared
		// with the launcher; the lockscreen is re-decoded inside
		// `quick_settings.reload_wallpaper`.
		let wallpaper = Arc::new( crate::assets::decode_wallpaper_bundle( self.screen_width, self.screen_height ) );
		self.homescreen.set_wallpaper( wallpaper );
		self.homescreen.reload_logo();
		// The launcher uses the lockscreen (not the homescreen) wallpaper
		// of the active mode and owns its own bundle, so it re-decodes
		// independently rather than sharing the homescreen `Arc`.
		self.launcher.reload_wallpaper( self.screen_width, self.screen_height );
		self.quick_settings.reload_wallpaper( self.screen_width, self.screen_height );
		if let Some( ipc ) = &self.forge_ipc
		{
			ipc.set_theme( mode );
		}
		Self::push_system_color_scheme( mode );
	}

	fn push_system_color_scheme( mode: ltk::ThemeMode )
	{
		let value = match mode
		{
			ltk::ThemeMode::Dark  => "prefer-dark",
			ltk::ThemeMode::Light => "prefer-light",
		};
		let _ = std::process::Command::new( "gsettings" )
			.args( [ "set", "org.gnome.desktop.interface", "color-scheme", value ] )
			.stdin( std::process::Stdio::null() )
			.stdout( std::process::Stdio::null() )
			.stderr( std::process::Stdio::null() )
			.spawn();
	}

	/// Promote `mode` to a concrete [`ltk::ThemePreference`] and apply it.
	///
	/// The promotion is what keeps the user's choice from getting clobbered
	/// on the next `Tick`: that arm re-runs `theme_preference.resolve(hour)`
	/// every second, so an `Auto` preference (or a CLI-forced `--dark` /
	/// `--light` that pins `theme_preference` to a specific variant) will
	/// otherwise undo a QS toggle the moment the next tick fires. By
	/// rewriting `theme_preference` to the chosen variant here we make the
	/// re-resolution a no-op for the rest of the session. Used for any
	/// EXPLICIT theme change — QS pill, `Message::ExternalColorScheme` from
	/// `gsettings monitor`, future settings UI.
	pub( crate ) fn set_theme_explicit( &mut self, mode: ltk::ThemeMode )
	{
		self.theme_preference = match mode
		{
			ltk::ThemeMode::Dark  => ltk::ThemePreference::Dark,
			ltk::ThemeMode::Light => ltk::ThemePreference::Light,
		};
		self.apply_theme_mode( mode );
	}

	/// Whether the homescreen should render the mobile (paged) layout right
	/// now. Mirrors [`crate::homescreen::Homescreen::view`]'s own resolution
	/// of `DisplayMode` so drop hit-testing and paging stay in sync.
	pub( crate ) fn use_mobile_layout( &self ) -> bool
	{
		let is_landscape = self.screen_width > self.screen_height;
		!self.config.display.mode.use_desktop( is_landscape )
	}
}

