//! Core runtime primitives for Liora native GPUI applications.
//!
//! This crate owns application-wide theme configuration, system-theme
//! synchronization, overlay/portal registries, z-index policy, and small
//! helpers shared by every Liora component crate. Applications normally call
//! `liora_components::init_liora` or `liora::init_liora`; use this crate
//! directly when building lower-level integrations or custom component crates.

use gpui::{
    Animation, AnimationExt, App, Bounds, Context, Global, Hsla, Pixels, SharedString, TextRun,
    Window, WindowAppearance, WindowBounds, prelude::*, px,
};

use std::borrow::Cow;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;

pub mod fonts;

pub use fonts::*;

static NEXT_UNIQUE_ID: AtomicU64 = AtomicU64::new(1);

/// Ordered font fallback lists used by Liora-rendered UI and code surfaces.
///
/// The default value intentionally leaves the UI list empty, so GPUI keeps using
/// its platform/system default (`.SystemUIFont`) for normal text. The code list
/// is also optional; when it is empty, Liora asks GPUI for the generic
/// `Monospace` family for code-oriented surfaces. Applications that want
/// branded typography can load font bytes with [`load_custom_fonts`] and then
/// provide ordered fallback lists through [`FontConfig`].
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FontConfig {
    /// Ordered UI font candidates. The first currently available family wins;
    /// if none are visible to GPUI, the final candidate is used as the declared
    /// fallback.
    pub ui_families: Vec<SharedString>,
    /// Ordered code font candidates. The first currently available family wins;
    /// if none are visible to GPUI, the final candidate is used as the declared
    /// fallback. Empty lists resolve to GPUI's generic `Monospace`.
    pub code_families: Vec<SharedString>,
}

impl FontConfig {
    /// Returns the system-default typography policy.
    pub fn system() -> Self {
        Self::default()
    }

    /// Returns a config that uses `families` as the ordered UI fallback list.
    pub fn with_ui_families(
        mut self,
        families: impl IntoIterator<Item = impl Into<SharedString>>,
    ) -> Self {
        self.ui_families = families.into_iter().map(Into::into).collect();
        self
    }

    /// Returns a config that uses `families` as the ordered code fallback list.
    pub fn with_code_families(
        mut self,
        families: impl IntoIterator<Item = impl Into<SharedString>>,
    ) -> Self {
        self.code_families = families.into_iter().map(Into::into).collect();
        self
    }

    /// Returns the ordered UI fallback list.
    pub fn ui_families(&self) -> &[SharedString] {
        &self.ui_families
    }

    /// Returns the ordered code fallback list.
    pub fn code_families(&self) -> &[SharedString] {
        &self.code_families
    }
}

/// Complete options for initializing Liora core state.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LioraOptions {
    /// Startup theme mode. Defaults to following the operating system.
    pub theme_mode: ThemeMode,
    /// Optional app typography overrides. Defaults to system fonts.
    pub fonts: FontConfig,
}

impl LioraOptions {
    /// Creates options that follow the operating-system theme and font choices.
    pub fn system() -> Self {
        Self::default()
    }

    /// Returns options with an explicit theme mode.
    pub fn with_theme_mode(mut self, mode: ThemeMode) -> Self {
        self.theme_mode = mode;
        self
    }

    /// Returns options with custom Liora font families.
    pub fn with_fonts(mut self, fonts: FontConfig) -> Self {
        self.fonts = fonts;
        self
    }
}

/// Registers application-provided font files with GPUI's text system.
///
/// This function only makes font faces available for later resolution; it does
/// not change Liora's default typography. To actually use a family, pass a
/// matching [`FontConfig`] through [`init_liora_with_options`] or update the
/// running config with [`set_font_config`]. Font bytes normally come from
/// `include_bytes!`, an asset source, or an application settings directory.
pub fn load_custom_fonts(
    cx: &mut App,
    fonts: impl IntoIterator<Item = Cow<'static, [u8]>>,
) -> gpui::Result<()> {
    cx.text_system().add_fonts(fonts.into_iter().collect())
}

/// Generate a process-wide unique, monotonically increasing numeric id.
pub fn next_unique_id() -> u64 {
    NEXT_UNIQUE_ID.fetch_add(1, Ordering::Relaxed)
}

/// Generate a process-wide unique id string with a stable component prefix.
///
/// Important: GPUI interactive state is keyed by `ElementId`, so call this only
/// when constructing a persistent component/entity instance. Do not call it from
/// a per-frame `render` path for a `RenderOnce` component, because that would
/// assign a new ID every frame and break hover/click/portal state.
pub fn unique_id(prefix: &str) -> gpui::SharedString {
    format!("{}-{}", prefix, next_unique_id()).into()
}

/// Return a stable process-wide unique id for the current element path.
///
/// This is safe inside render paths because GPUI stores the generated value in
/// keyed element state and reuses it for the same element across frames. The
/// `key` must itself be stable for the visual element being rendered.
pub fn stable_unique_id(
    key: impl Into<gpui::SharedString>,
    prefix: &str,
    window: &mut Window,
    cx: &mut App,
) -> gpui::SharedString {
    let prefix = prefix.to_string();
    let key = gpui::ElementId::from(key.into());
    window
        .use_keyed_state(key, cx, move |_, _| unique_id(&prefix))
        .read(cx)
        .clone()
}

/// Documents and exposes the popper module APIs.
pub mod popper;

pub use popper::*;

pub use liora_theme::Theme;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
/// Options that control theme mode behavior.
pub enum ThemeMode {
    #[default]
    /// Follows the operating system appearance when resolving the active theme.
    System,
    /// Forces the light Liora theme regardless of system appearance.
    Light,
    /// Forces the dark Liora theme regardless of system appearance.
    Dark,
}

impl ThemeMode {
    /// Returns the stable user-facing label for this value.
    pub fn label(self) -> &'static str {
        match self {
            Self::System => "System",
            Self::Light => "Light",
            Self::Dark => "Dark",
        }
    }

    /// Returns the serialized value used by forms, configuration, or persistence.
    pub fn value(self) -> &'static str {
        match self {
            Self::System => "system",
            Self::Light => "light",
            Self::Dark => "dark",
        }
    }

    /// Parses a serialized value into the corresponding strongly typed option.
    pub fn from_value(value: &str) -> Option<Self> {
        match value {
            "system" => Some(Self::System),
            "light" => Some(Self::Light),
            "dark" => Some(Self::Dark),
            _ => None,
        }
    }

    /// Resolves this mode into a concrete light or dark theme.
    pub fn resolve(self, appearance: WindowAppearance) -> Theme {
        match self {
            Self::System => theme_for_window_appearance(appearance),
            Self::Light => Theme::light(),
            Self::Dark => Theme::dark(),
        }
    }

    /// Creates this value from theme.
    pub fn from_theme(theme: &Theme) -> Self {
        match theme.name.as_str() {
            "dark" => Self::Dark,
            _ => Self::Light,
        }
    }
}

/// Return startup bounds for a window that should request GPUI maximized state.
///
/// This helper uses the official pinned Zed GPUI git API surface. It preserves
/// the caller's `WindowBounds::Maximized` intent and uses the primary display
/// bounds as the restore/fallback geometry; exact first-frame behavior is
/// decided by the upstream GPUI backend selected by the application root.
pub fn startup_maximized_window_bounds(
    cx: &App,
    fallback_size: gpui::Size<Pixels>,
) -> WindowBounds {
    let bounds = cx
        .primary_display()
        .map(|display| display.bounds())
        .unwrap_or(Bounds {
            origin: gpui::Point::default(),
            size: fallback_size,
        });
    WindowBounds::Maximized(bounds)
}

/// Metadata and icon bytes used to publish a Linux desktop identity.
///
/// Wayland does not let a client set a titlebar or taskbar icon directly.
/// Compositors resolve the icon by matching the window `app_id` to a desktop
/// entry and then loading that entry's `Icon=` name from the icon theme. Liora
/// apps call [`ensure_linux_desktop_identity`] before opening their first GPUI
/// window so `cargo run -p <app>` gets the same icon identity as an installed
/// package without requiring root privileges.
#[derive(Clone, Debug)]
pub struct LinuxDesktopIdentity<'a> {
    /// Stable GPUI/Wayland app id and desktop/icon filename stem.
    pub app_id: &'a str,
    /// Complete `.desktop` file contents for this app.
    pub desktop_entry: Cow<'a, str>,
    /// PNG icon bytes installed into hicolor size directories.
    pub png_icons: &'a [LinuxDesktopPngIcon<'a>],
    /// SVG icon bytes installed into hicolor scalable apps.
    pub svg_icon: &'a [u8],
}

/// PNG icon bytes for one hicolor app-icon size bucket.
#[derive(Clone, Copy, Debug)]
pub struct LinuxDesktopPngIcon<'a> {
    /// Square icon size in logical pixels.
    pub size: u16,
    /// PNG bytes for this size, normally 8-bit RGBA for maximum shell support.
    pub bytes: &'a [u8],
}

/// Builds a user-level Linux desktop entry for a running Liora app.
///
/// Packaged installers use the static files under `packaging/linux`. Direct
/// development runs should instead register the currently running executable,
/// because `TryExec=<binary>` entries can be ignored when `cargo run` launches a
/// target/debug binary that is not on `PATH`.
pub fn linux_desktop_entry(
    name: &str,
    generic_name: &str,
    comment: &str,
    executable: &std::path::Path,
    icon: &str,
    categories: &str,
    keywords: &str,
) -> String {
    format!(
        "[Desktop Entry]\nVersion=1.0\nType=Application\nName={name}\nGenericName={generic_name}\nComment={comment}\nExec={}\nIcon={icon}\nCategories={categories}\nKeywords={keywords}\nStartupNotify=true\nTerminal=false\n",
        desktop_exec_value(executable),
    )
}

/// Returns the user-scoped hicolor app-icon path for an app id and icon size.
///
/// Use this when generating a development `.desktop` entry that should bypass
/// desktop-environment icon-theme cache latency by pointing `Icon=` at the
/// exact PNG file Liora registers.
pub fn linux_desktop_png_icon_path(app_id: &str, size: u16) -> Option<std::path::PathBuf> {
    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
    {
        linux_xdg_data_home().ok().map(|data_home| {
            data_home
                .join("icons")
                .join("hicolor")
                .join(format!("{size}x{size}"))
                .join("apps")
                .join(format!("{app_id}.png"))
        })
    }

    #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
    {
        let _ = (app_id, size);
        None
    }
}

fn desktop_exec_value(executable: &std::path::Path) -> String {
    let value = executable.to_string_lossy();
    if value
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | '.' | '_' | '-' | '+'))
    {
        value.into_owned()
    } else {
        format!("\"{}\"", value.replace('\\', "\\\\").replace('\"', "\\\""))
    }
}

/// Installs a user-scoped Linux desktop entry and hicolor icons for an app.
///
/// The operation is intentionally best-effort and idempotent: it writes only to
/// `$XDG_DATA_HOME` or `~/.local/share`, skips non-Linux targets, and returns an
/// error instead of panicking when the host environment has no usable home/data
/// directory. Applications should call this before creating their first window
/// and log failures, because the app can still run when icon registration is
/// unavailable.
pub fn ensure_linux_desktop_identity(identity: LinuxDesktopIdentity<'_>) -> std::io::Result<()> {
    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
    {
        ensure_linux_desktop_identity_impl(linux_xdg_data_home()?, identity, true)
    }

    #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
    {
        let _ = identity;
        Ok(())
    }
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn ensure_linux_desktop_identity_impl(
    data_home: std::path::PathBuf,
    identity: LinuxDesktopIdentity<'_>,
    refresh_cache: bool,
) -> std::io::Result<()> {
    let mut changed = write_if_changed(
        &data_home
            .join("applications")
            .join(format!("{}.desktop", identity.app_id)),
        identity.desktop_entry.as_bytes(),
    )?;
    for icon in identity.png_icons {
        changed |= write_if_changed(
            &data_home
                .join("icons")
                .join("hicolor")
                .join(format!("{}x{}", icon.size, icon.size))
                .join("apps")
                .join(format!("{}.png", identity.app_id)),
            icon.bytes,
        )?;
    }
    changed |= write_if_changed(
        &data_home
            .join("icons")
            .join("hicolor")
            .join("scalable")
            .join("apps")
            .join(format!("{}.svg", identity.app_id)),
        identity.svg_icon,
    )?;
    changed |= ensure_hicolor_index_theme(&data_home)?;
    if changed && refresh_cache {
        refresh_linux_desktop_identity_cache(&data_home);
    }
    Ok(())
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn refresh_linux_desktop_identity_cache(data_home: &std::path::Path) {
    let apps_dir = data_home.join("applications");
    let _ = std::fs::remove_file(
        data_home
            .join("icons")
            .join("hicolor")
            .join(".icon-theme.cache"),
    );
    let _ = std::process::Command::new("update-desktop-database")
        .arg(&apps_dir)
        .status();
    for command in ["kbuildsycoca6", "kbuildsycoca5"] {
        let _ = std::process::Command::new(command)
            .arg("--noincremental")
            .status();
    }
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn ensure_hicolor_index_theme(data_home: &std::path::Path) -> std::io::Result<bool> {
    write_if_changed(
        &data_home.join("icons").join("hicolor").join("index.theme"),
        HICOLOR_INDEX_THEME.as_bytes(),
    )
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
const HICOLOR_INDEX_THEME: &str = "[Icon Theme]\nName=Hicolor\nComment=Fallback icon theme\nHidden=true\nDirectories=16x16/apps,24x24/apps,32x32/apps,48x48/apps,64x64/apps,128x128/apps,256x256/apps,512x512/apps,scalable/apps\n\n[16x16/apps]\nSize=16\nContext=Applications\nType=Threshold\n\n[24x24/apps]\nSize=24\nContext=Applications\nType=Threshold\n\n[32x32/apps]\nSize=32\nContext=Applications\nType=Threshold\n\n[48x48/apps]\nSize=48\nContext=Applications\nType=Threshold\n\n[64x64/apps]\nSize=64\nContext=Applications\nType=Threshold\n\n[128x128/apps]\nSize=128\nContext=Applications\nType=Threshold\n\n[256x256/apps]\nSize=256\nContext=Applications\nType=Threshold\n\n[512x512/apps]\nSize=512\nContext=Applications\nType=Threshold\n\n[scalable/apps]\nSize=512\nMinSize=16\nMaxSize=512\nContext=Applications\nType=Scalable\n";

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn linux_xdg_data_home() -> std::io::Result<std::path::PathBuf> {
    if let Some(data_home) = std::env::var_os("XDG_DATA_HOME") {
        return Ok(std::path::PathBuf::from(data_home));
    }
    std::env::var_os("HOME")
        .map(std::path::PathBuf::from)
        .map(|home| home.join(".local").join("share"))
        .ok_or_else(|| {
            std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "neither XDG_DATA_HOME nor HOME is set",
            )
        })
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn write_if_changed(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<bool> {
    if std::fs::read(path).is_ok_and(|existing| existing == bytes) {
        return Ok(false);
    }
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(path, bytes)?;
    Ok(true)
}

fn startup_system_appearance(cx: &App) -> WindowAppearance {
    platform_system_appearance().unwrap_or_else(|| cx.window_appearance())
}

fn current_system_appearance(window: &Window, _cx: &App) -> WindowAppearance {
    platform_system_appearance().unwrap_or_else(|| window.appearance())
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn platform_system_appearance() -> Option<WindowAppearance> {
    gtk_theme_env_appearance()
        .or_else(gtk_settings_appearance)
        .or_else(gsettings_color_scheme_appearance)
}

#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
fn platform_system_appearance() -> Option<WindowAppearance> {
    None
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn gtk_theme_env_appearance() -> Option<WindowAppearance> {
    std::env::var("GTK_THEME")
        .ok()
        .and_then(|theme| appearance_from_theme_name(&theme))
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn gtk_settings_appearance() -> Option<WindowAppearance> {
    ["gtk-4.0", "gtk-3.0"]
        .into_iter()
        .filter_map(|version| {
            std::env::var_os("HOME").map(|home| {
                std::path::PathBuf::from(home)
                    .join(".config")
                    .join(version)
                    .join("settings.ini")
            })
        })
        .filter_map(|path| std::fs::read_to_string(path).ok())
        .find_map(|settings| appearance_from_gtk_settings(&settings))
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn gsettings_color_scheme_appearance() -> Option<WindowAppearance> {
    let output = std::process::Command::new("gsettings")
        .args(["get", "org.gnome.desktop.interface", "color-scheme"])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let value = String::from_utf8_lossy(&output.stdout);
    appearance_from_color_scheme(&value)
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn appearance_from_color_scheme(value: &str) -> Option<WindowAppearance> {
    let value = value
        .trim()
        .trim_matches('\'')
        .trim_matches('"')
        .to_ascii_lowercase();
    if value.contains("prefer-dark") {
        Some(WindowAppearance::Dark)
    } else if value.contains("prefer-light") || value == "default" {
        Some(WindowAppearance::Light)
    } else {
        None
    }
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn appearance_from_gtk_settings(settings: &str) -> Option<WindowAppearance> {
    for line in settings.lines() {
        let line = line.trim();
        if line.starts_with('#') || line.starts_with(';') || line.is_empty() {
            continue;
        }
        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        let key = key.trim();
        let value = value.trim();
        if key == "gtk-application-prefer-dark-theme" {
            return match value.to_ascii_lowercase().as_str() {
                "true" | "1" => Some(WindowAppearance::Dark),
                "false" | "0" => Some(WindowAppearance::Light),
                _ => None,
            };
        }
        if key == "gtk-theme-name"
            && let Some(appearance) = appearance_from_theme_name(value)
        {
            return Some(appearance);
        }
    }
    None
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn appearance_from_theme_name(theme: &str) -> Option<WindowAppearance> {
    let theme = theme.to_ascii_lowercase();
    if theme.contains("dark") {
        Some(WindowAppearance::Dark)
    } else if theme.contains("light") {
        Some(WindowAppearance::Light)
    } else {
        None
    }
}

/// Maps a GPUI window appearance to the matching Liora theme.
pub fn theme_for_window_appearance(appearance: WindowAppearance) -> Theme {
    match appearance {
        WindowAppearance::Light | WindowAppearance::VibrantLight => Theme::light(),
        WindowAppearance::Dark | WindowAppearance::VibrantDark => Theme::dark(),
    }
}

/// Runtime state used by Liora config behavior.
pub struct Config {
    /// Active Liora theme tokens stored in GPUI global state.
    pub theme: Theme,
    /// Configured theme mode used to resolve light or dark tokens.
    pub theme_mode: ThemeMode,
    /// Optional UI/code font family overrides.
    pub fonts: FontConfig,
    /// Base z-index offset for overlay layering.
    pub z_index_base: u32,
}

impl Global for Config {}

impl Config {
    /// Updates the stored theme mode value and keeps the existing component identity.
    pub fn set_theme_mode(&mut self, mode: ThemeMode, appearance: WindowAppearance) {
        self.theme_mode = mode;
        self.theme = mode.resolve(appearance);
    }

    /// Replaces the app typography policy while preserving the active theme.
    pub fn set_font_config(&mut self, fonts: FontConfig) {
        self.fonts = fonts;
    }

    /// Synchronizes the active theme from the current system/window appearance.
    pub fn sync_system_theme(&mut self, appearance: WindowAppearance) -> bool {
        if self.theme_mode != ThemeMode::System {
            return false;
        }
        let theme = ThemeMode::System.resolve(appearance);
        let changed = self.theme.name != theme.name;
        self.theme = theme;
        changed
    }
}

/// Initializes Liora core state with an explicit concrete theme.
pub fn init_liora(cx: &mut App, theme: Theme) {
    let theme_mode = ThemeMode::from_theme(&theme);
    set_core_config(
        cx,
        Config {
            theme,
            theme_mode,
            fonts: FontConfig::default(),
            z_index_base: 1000,
        },
    );
}

fn set_core_config(cx: &mut App, config: Config) {
    cx.set_global(config);
    cx.set_global(crate::popper::ZIndexStack::default());
    cx.set_global(crate::popper::ActiveTooltip(Vec::new()));
    cx.set_global(crate::popper::ActivePopover(Vec::new()));
    cx.set_global(crate::popper::ActiveModal(Vec::new()));
    cx.set_global(crate::popper::ActiveDrawer(Vec::new()));
}

/// Initializes Liora core state from a theme mode, including system mode resolution.
pub fn init_liora_with_mode(cx: &mut App, mode: ThemeMode) {
    init_liora_with_options(cx, LioraOptions::default().with_theme_mode(mode));
}

/// Initializes Liora core state from full startup options.
pub fn init_liora_with_options(cx: &mut App, options: LioraOptions) {
    let appearance = startup_system_appearance(cx);
    set_core_config(
        cx,
        Config {
            theme: options.theme_mode.resolve(appearance),
            theme_mode: options.theme_mode,
            fonts: options.fonts,
            z_index_base: 1000,
        },
    );
}

/// Replaces Liora font-family overrides for subsequent renders.
pub fn set_font_config(cx: &mut App, fonts: FontConfig) {
    cx.global_mut::<Config>().set_font_config(fonts);
}

/// Resolves an ordered fallback list against families currently visible to GPUI.
///
/// GPUI's public text style currently accepts one `font_family` value, so Liora
/// performs the ordered fallback selection before assigning that field. If no
/// candidate is reported as available, Liora returns the last configured
/// candidate because callers normally put the broadest platform fallback there.
pub fn resolve_font_family_from_available(
    candidates: &[SharedString],
    available: &[String],
    default: Option<SharedString>,
) -> Option<SharedString> {
    if candidates.is_empty() {
        return default;
    }

    candidates
        .iter()
        .find(|candidate| available.iter().any(|family| family == candidate.as_ref()))
        .cloned()
        .or_else(|| candidates.last().cloned())
}

/// Returns the resolved UI font family, if the app overrides the system default.
pub fn ui_font_family(cx: &App) -> Option<SharedString> {
    let config = cx.global::<Config>();
    let available = cx.text_system().all_font_names();
    resolve_font_family_from_available(&config.fonts.ui_families, &available, None)
}

/// Returns the resolved code font family or GPUI's generic monospace family.
pub fn code_font_family(cx: &App) -> SharedString {
    let config = cx.global::<Config>();
    let available = cx.text_system().all_font_names();
    resolve_font_family_from_available(
        &config.fonts.code_families,
        &available,
        Some("Monospace".into()),
    )
    .unwrap_or_else(|| "Monospace".into())
}

/// Applies a new theme mode and refreshes the active GPUI window.
pub fn apply_theme_mode(window: &mut Window, cx: &mut App, mode: ThemeMode) {
    let appearance = current_system_appearance(window, cx);
    cx.global_mut::<Config>().set_theme_mode(mode, appearance);
    window.refresh();
}

/// Synchronizes the active theme from the current system/window appearance.
pub fn sync_system_theme(window: &mut Window, cx: &mut App) {
    let appearance = current_system_appearance(window, cx);
    if cx.global_mut::<Config>().sync_system_theme(appearance) {
        window.refresh();
    }
}

/// Attach System theme tracking to a concrete GPUI window.
///
/// `init_liora_with_mode(cx, ThemeMode::System)` runs before a window exists and
/// can only use the app-level appearance snapshot. Following Zed's main-window
/// pattern, create the window with `WindowOptions { show: false, .. }`, call this
/// at the start of the `open_window` callback before constructing the root view,
/// then activate the returned window handle after `open_window` completes.
pub fn attach_system_theme_observer(window: &mut Window, cx: &mut App) {
    sync_system_theme(window, cx);
    window
        .observe_window_appearance(|window, cx| sync_system_theme(window, cx))
        .detach();
}

/// Renders the render active popover in window layer into native GPUI elements.
pub fn render_active_popover_in_window(_window: &mut gpui::Window, cx: &mut App) {
    for entry in cx.global::<crate::popper::ActivePopover>().0.clone() {
        push_portal(
            move |_window, _cx| entry.view.clone().into_any_element(),
            cx,
        );
    }
}

/// Renders the render active modal in window layer into native GPUI elements.
pub fn render_active_modal_in_window(_window: &mut gpui::Window, cx: &mut App) {
    for entry in cx.global::<crate::popper::ActiveModal>().0.clone() {
        push_portal(
            move |_window, _cx| entry.view.clone().into_any_element(),
            cx,
        );
    }
}

/// Renders the render active drawer in window layer into native GPUI elements.
pub fn render_active_drawer_in_window(_window: &mut gpui::Window, cx: &mut App) {
    for entry in cx.global::<crate::popper::ActiveDrawer>().0.clone() {
        push_portal(
            move |_window, _cx| entry.view.clone().into_any_element(),
            cx,
        );
    }
}

/// Splits tooltip content into GPUI-safe display lines.
///
/// GPUI's low-level text shaper accepts a single line only and intentionally
/// panics when a string contains newlines. Chart tooltips and downstream apps
/// may provide multiline help text, so overlay rendering normalizes content at
/// the Liora boundary before measuring each line independently.
pub fn tooltip_content_lines(content: &SharedString) -> Vec<SharedString> {
    let lines: Vec<SharedString> = content
        .split('\n')
        .map(|line| line.trim_end_matches('\r'))
        .map(SharedString::from)
        .collect();

    if lines.is_empty() {
        vec![SharedString::default()]
    } else {
        lines
    }
}

/// Renders the render active tooltip in window layer into native GPUI elements.
pub fn render_active_tooltip_in_window(window: &mut gpui::Window, cx: &mut App) {
    let mouse_pos = window.mouse_position();
    cx.global_mut::<crate::popper::ActiveTooltip>()
        .0
        .retain(|data| data.anchor_bounds.contains(&mouse_pos));

    let active = cx.global::<crate::popper::ActiveTooltip>().0.clone();
    for (tooltip_index, data) in active.into_iter().enumerate() {
        let theme = cx.global::<Config>().theme.clone();

        // Measure each visual line independently. GPUI shape_line is
        // intentionally single-line only and will panic on newline input.
        let font_size = px(theme.font_size.sm);
        let text_style = window.text_style();
        let lines = tooltip_content_lines(&data.content);
        let max_line_width = lines
            .iter()
            .map(|line| {
                let run = TextRun {
                    len: line.len(),
                    font: text_style.font(),
                    color: theme.neutral.card,
                    background_color: None,
                    underline: None,
                    strikethrough: None,
                };
                window
                    .text_system()
                    .shape_line(line.clone(), font_size, &[run], None)
                    .width
            })
            .fold(px(0.0), |max_width, width| max_width.max(width));

        let padding_h = px(12.0);
        let padding_v = px(6.0);
        let line_height = window.line_height();
        let content_size = gpui::Size {
            width: max_line_width + padding_h * 2.0,
            height: line_height * lines.len() + padding_v * 2.0,
        };

        push_passive_portal(
            move |window, _cx| {
                let viewport = Bounds {
                    origin: gpui::Point::default(),
                    size: window.viewport_size(),
                };

                let popper = Popper {
                    anchor_bounds: data.anchor_bounds,
                    placement: data.placement,
                    offset: data.offset,
                };

                let (pos, _final_placement) =
                    popper.calculate_position_with_flip(content_size, viewport);

                gpui::div()
                    .absolute()
                    .cursor_default()
                    .top(pos.y)
                    .left(pos.x)
                    .w(content_size.width)
                    .h(content_size.height)
                    .bg(theme.neutral.text_1)
                    .text_color(theme.neutral.card)
                    .px(padding_h)
                    .py(padding_v)
                    .flex()
                    .flex_col()
                    .items_start()
                    .justify_center()
                    .gap(px(2.0))
                    .rounded(px(theme.radius.sm))
                    .shadow_lg()
                    .text_size(font_size)
                    .children(lines.iter().cloned())
                    .with_animation(
                        ("liora-tooltip-motion", tooltip_index),
                        Animation::new(Duration::from_millis(220))
                            .with_easing(gpui::ease_out_quint()),
                        |tooltip, delta| tooltip.opacity(delta),
                    )
                    .into_any_element()
            },
            cx,
        );
    }
}

#[cfg(test)]
mod tooltip_text_tests {
    use super::*;

    #[test]
    fn tooltip_renderer_never_shapes_multiline_content_as_one_line() {
        let source = include_str!("lib.rs").split("#[cfg(test)]").next().unwrap();

        assert!(
            !source.contains(".shape_line(data.content.clone()"),
            "GPUI shape_line panics when the text argument contains newlines"
        );
        assert!(
            source.contains("tooltip_content_lines(&data.content)")
                || source.contains("tooltip_content_lines("),
            "tooltip rendering should split multiline content before measuring and rendering"
        );
    }

    #[test]
    fn tooltip_content_lines_preserves_multiline_tooltips_without_newline_lines() {
        let lines = tooltip_content_lines(&SharedString::from("Mon\nO 100  H 110\nL 96  C 108"));

        assert_eq!(lines, vec!["Mon", "O 100  H 110", "L 96  C 108"]);
        assert!(lines.iter().all(|line| !line.contains('\n')));
    }
}

#[cfg(test)]
mod theme_mode_tests {
    use super::*;

    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
    #[test]
    fn linux_startup_appearance_parses_synchronous_dark_preferences() {
        assert_eq!(
            appearance_from_color_scheme("'prefer-dark'"),
            Some(WindowAppearance::Dark)
        );
        assert_eq!(
            appearance_from_color_scheme("prefer-light"),
            Some(WindowAppearance::Light)
        );
        assert_eq!(
            appearance_from_gtk_settings(
                "[Settings]\ngtk-application-prefer-dark-theme=true\ngtk-theme-name=Breeze\n"
            ),
            Some(WindowAppearance::Dark)
        );
        assert_eq!(
            appearance_from_theme_name("Adwaita-dark"),
            Some(WindowAppearance::Dark)
        );
    }

    #[test]
    fn theme_mode_values_and_labels_are_stable() {
        assert_eq!(ThemeMode::System.value(), "system");
        assert_eq!(ThemeMode::Light.label(), "Light");
        assert_eq!(ThemeMode::from_value("dark"), Some(ThemeMode::Dark));
        assert_eq!(ThemeMode::from_theme(&Theme::dark()), ThemeMode::Dark);
        assert_eq!(ThemeMode::from_theme(&Theme::light()), ThemeMode::Light);
        assert_eq!(ThemeMode::from_value("unknown"), None);
    }

    #[test]
    fn system_theme_resolves_from_window_appearance() {
        assert_eq!(
            ThemeMode::System.resolve(WindowAppearance::Light).name,
            Theme::light().name
        );
        assert_eq!(
            ThemeMode::System
                .resolve(WindowAppearance::VibrantDark)
                .name,
            Theme::dark().name
        );
    }

    #[test]
    fn config_syncs_only_in_system_mode() {
        let mut config = Config {
            theme: Theme::light(),
            theme_mode: ThemeMode::Light,
            fonts: FontConfig::default(),
            z_index_base: 1000,
        };
        assert!(!config.sync_system_theme(WindowAppearance::Dark));
        assert_eq!(config.theme.name, "light");

        config.set_theme_mode(ThemeMode::System, WindowAppearance::Dark);
        assert_eq!(config.theme.name, "dark");
        assert!(!config.sync_system_theme(WindowAppearance::VibrantDark));
        assert!(config.sync_system_theme(WindowAppearance::Light));
        assert_eq!(config.theme.name, "light");
    }

    #[test]
    fn font_config_defaults_to_system_fonts_and_keeps_custom_families_explicit() {
        let default = FontConfig::default();
        assert!(default.ui_families.is_empty());
        assert!(default.code_families.is_empty());

        let custom = FontConfig::system()
            .with_ui_families(["Inter", "Segoe UI"])
            .with_code_families(["JetBrains Mono", "Monospace"]);
        assert_eq!(
            custom.ui_families(),
            &[SharedString::from("Inter"), SharedString::from("Segoe UI")]
        );
        assert_eq!(
            custom.code_families(),
            &[
                SharedString::from("JetBrains Mono"),
                SharedString::from("Monospace")
            ]
        );

        let options = LioraOptions::system()
            .with_theme_mode(ThemeMode::Dark)
            .with_fonts(custom.clone());
        assert_eq!(options.theme_mode, ThemeMode::Dark);
        assert_eq!(options.fonts, custom);
    }

    #[test]
    fn font_config_accepts_ordered_fallback_family_lists() {
        let custom = FontConfig::system()
            .with_ui_families(["PingFang SC", "Segoe UI", "Arial"])
            .with_code_families(["JetBrains Mono", "SF Mono", "Monospace"]);

        assert_eq!(
            custom.ui_families(),
            &[
                SharedString::from("PingFang SC"),
                SharedString::from("Segoe UI"),
                SharedString::from("Arial")
            ]
        );
        assert_eq!(
            custom.code_families(),
            &[
                SharedString::from("JetBrains Mono"),
                SharedString::from("SF Mono"),
                SharedString::from("Monospace")
            ]
        );
    }

    #[test]
    fn font_family_resolution_uses_first_available_then_final_fallback() {
        let candidates = [
            SharedString::from("PingFang SC"),
            SharedString::from("Segoe UI"),
            SharedString::from("Arial"),
        ];
        let available = [String::from("Segoe UI")];

        assert_eq!(
            resolve_font_family_from_available(&candidates, &available, None).as_deref(),
            Some("Segoe UI")
        );
        assert_eq!(
            resolve_font_family_from_available(&candidates, &[], None).as_deref(),
            Some("Arial")
        );
        assert_eq!(
            resolve_font_family_from_available(&[], &available, Some("Monospace".into()))
                .as_deref(),
            Some("Monospace")
        );
    }

    #[test]
    fn system_theme_observer_syncs_immediately_and_stays_attached() {
        let source = include_str!("lib.rs");
        let start = source
            .find("pub fn attach_system_theme_observer")
            .expect("system theme observer helper should exist");
        let body = &source[start
            ..source[start..]
                .find("pub fn render_active_popover_in_window")
                .expect("next function should follow observer helper")
                + start];

        let sync_call = format!("{}(window, cx);", "sync_system_theme");
        let observe_call = format!("{}", "observe_window_appearance");
        let sync_index = body
            .find(&sync_call)
            .expect("observer helper should sync the current window appearance immediately");
        let observe_index = body
            .find(&observe_call)
            .expect("observer helper should observe later appearance changes");
        assert!(sync_index < observe_index);
        assert!(body.contains(".detach();"));
    }
}

#[cfg(test)]
mod motion_tests {
    #[test]
    fn tooltip_rendering_uses_gpui_motion() {
        let source = include_str!("lib.rs").split("#[cfg(test)]").next().unwrap();

        assert!(source.contains("tooltip-motion"));
        assert!(source.contains("with_animation("));
    }
}

/// Returns the active Liora theme stored in the GPUI application context.
pub fn liora_theme<'a, V>(cx: &'a Context<'a, V>) -> &'a Theme {
    &cx.global::<Config>().theme
}

/// Convenience accessors for reading Liora theme data from GPUI contexts.
pub trait ContextExt {
    /// Returns the active Liora theme for the current context.
    fn liora(&self) -> &Theme;
}

impl<'a, V> ContextExt for Context<'a, V> {
    fn liora(&self) -> &Theme {
        liora_theme(self)
    }
}

/// Element extension points reserved for applying Liora-wide styling helpers.
pub trait ElementExt {
    /// Returns the active Liora theme for the current context.
    fn liora(self, cx: &mut App) -> Self;
}

impl ElementExt for gpui::Div {
    fn liora(self, _cx: &mut App) -> Self {
        self
    }
}

/// Returns the z-index reserved for popup overlays.
pub fn z_index_popup<V>(cx: &Context<'_, V>) -> u32 {
    cx.global::<Config>().z_index_base + 100
}

/// Returns the z-index reserved for modal overlays.
pub fn z_index_modal<V>(cx: &Context<'_, V>) -> u32 {
    cx.global::<Config>().z_index_base + 200
}

/// Returns the z-index reserved for notifications.
pub fn z_index_notification<V>(cx: &Context<'_, V>) -> u32 {
    cx.global::<Config>().z_index_base + 300
}

/// Returns the z-index reserved for tooltip overlays.
pub fn z_index_tooltip<V>(cx: &Context<'_, V>) -> u32 {
    cx.global::<Config>().z_index_base + 400
}

/// Converts a packed RGB integer into a GPUI HSLA color.
pub fn hex_color(hex: u32) -> Hsla {
    gpui::rgb(hex).into()
}

#[cfg(test)]
mod unique_id_tests {
    use super::*;

    #[test]
    fn generated_ids_are_prefixed_and_unique() {
        let first = unique_id("component");
        let second = unique_id("component");

        assert!(first.as_ref().starts_with("component-"));
        assert!(second.as_ref().starts_with("component-"));
        assert_ne!(first, second);
    }
}

#[cfg(test)]
mod desktop_identity_tests {
    use super::*;

    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
    #[test]
    fn linux_desktop_identity_installs_desktop_entry_and_hicolor_icons() {
        let temp_root = std::env::temp_dir().join(format!(
            "liora-desktop-identity-test-{}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&temp_root);
        std::fs::create_dir_all(&temp_root).expect("test temp root should be creatable");

        ensure_linux_desktop_identity_impl(
            temp_root.clone(),
            LinuxDesktopIdentity {
                app_id: "liora-test",
                desktop_entry: Cow::Borrowed(
                    "[Desktop Entry]\nType=Application\nName=Liora Test\nIcon=liora-test\n",
                ),
                png_icons: &[LinuxDesktopPngIcon {
                    size: 48,
                    bytes: b"png",
                }],
                svg_icon: b"svg",
            },
            false,
        )
        .expect("identity registration should write into the supplied XDG data home");

        assert_eq!(
            std::fs::read_to_string(temp_root.join("applications/liora-test.desktop"))
                .expect("desktop entry should be installed"),
            "[Desktop Entry]\nType=Application\nName=Liora Test\nIcon=liora-test\n"
        );
        assert_eq!(
            std::fs::read(temp_root.join("icons/hicolor/48x48/apps/liora-test.png"))
                .expect("PNG hicolor icon should be installed"),
            b"png"
        );
        assert_eq!(
            std::fs::read(temp_root.join("icons/hicolor/scalable/apps/liora-test.svg"))
                .expect("SVG hicolor icon should be installed"),
            b"svg"
        );
        assert!(
            std::fs::read_to_string(temp_root.join("icons/hicolor/index.theme"))
                .expect("hicolor index theme should be installed")
                .contains("Directories=16x16/apps")
        );

        let _ = std::fs::remove_dir_all(temp_root);
    }
}
