use tauri::menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem};
use tauri::tray::{MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent};
use tauri::{Emitter, Manager};
#[cfg(not(target_os = "windows"))]
use tauri::tray::MouseButtonState;

use crate::tray_runtime::{
    tray_state_icon, TrayMenuItems, TrayMenuItemsState, TrayMenuLabels, TrayMenuLabelsState,
    TrayPlaybackState, TrayState, TrayTooltip,
};

use super::super::ui::{PAUSE_RENDERING_JS, RESUME_RENDERING_JS};
use tauri::image::Image;

/// Debug builds: mirror the default app icon horizontally so the tray differs from release.
fn app_tray_icon(app: &tauri::AppHandle) -> Image<'static> {
    let icon = app.default_window_icon().expect("default window icon");
    #[cfg(debug_assertions)]
    {
        flip_image_horizontal(icon)
    }
    #[cfg(not(debug_assertions))]
    {
        icon.clone().to_owned()
    }
}

#[cfg(debug_assertions)]
fn flip_image_horizontal(icon: &Image<'_>) -> Image<'static> {
    let width = icon.width();
    let height = icon.height();
    let mut rgba = icon.rgba().to_vec();
    flip_rgba_horizontal(&mut rgba, width, height);
    Image::new_owned(rgba, width, height)
}

#[cfg(debug_assertions)]
fn flip_rgba_horizontal(rgba: &mut [u8], width: u32, height: u32) {
    let w = width as usize;
    let h = height as usize;
    if w == 0 || h == 0 || rgba.len() < w * h * 4 {
        return;
    }
    for y in 0..h {
        let row = y * w * 4;
        for x in 0..w / 2 {
            let l = row + x * 4;
            let r = row + (w - 1 - x) * 4;
            let left = [
                rgba[l],
                rgba[l + 1],
                rgba[l + 2],
                rgba[l + 3],
            ];
            let right = [
                rgba[r],
                rgba[r + 1],
                rgba[r + 2],
                rgba[r + 3],
            ];
            rgba[l..l + 4].copy_from_slice(&right);
            rgba[r..r + 4].copy_from_slice(&left);
        }
    }
}

/// Stable tray-icon id. Without a fixed id, `TrayIconBuilder::new()` assigns a
/// fresh id on every rebuild; on KDE (StatusNotifierItem) each new id registers
/// a new item, and KDE keeps the stale ones in its "hidden icons" list — so
/// toggling the tray off/on piled up duplicate entries (Asra, 1.47-RC4). A
/// constant id makes every rebuild reuse the same item.
const TRAY_ICON_ID: &str = "psysonic-tray";

pub(crate) fn build_tray_icon(app: &tauri::AppHandle) -> tauri::Result<TrayIcon> {
    let labels = app
        .try_state::<TrayMenuLabelsState>()
        .map(|s| s.lock().unwrap().clone())
        .unwrap_or_default();

    let play_pause = MenuItemBuilder::with_id("play_pause", &labels.play_pause).build(app)?;
    let next       = MenuItemBuilder::with_id("next",       &labels.next).build(app)?;
    let previous   = MenuItemBuilder::with_id("previous",   &labels.previous).build(app)?;
    let sep1       = PredefinedMenuItem::separator(app)?;
    let show_hide  = MenuItemBuilder::with_id("show_hide",  &labels.show_hide).build(app)?;
    let sep2       = PredefinedMenuItem::separator(app)?;
    let quit       = MenuItemBuilder::with_id("quit",       &labels.quit).build(app)?;

    let cached_tooltip = app
        .try_state::<TrayTooltip>()
        .and_then(|s| {
            let g = s.lock().ok()?;
            if g.is_empty() { None } else { Some(g.clone()) }
        })
        .unwrap_or_else(|| "Psysonic".to_string());
    #[cfg(any(target_os = "windows", target_os = "linux"))]
    let playback_state = app
        .try_state::<TrayPlaybackState>()
        .map(|s| s.0.lock().unwrap().clone())
        .unwrap_or_else(|| "stop".to_string());
    #[cfg(target_os = "windows")]
    let tooltip_with_icon = format!("{} {}", tray_state_icon(&playback_state), cached_tooltip);

    // Linux/AppIndicator has no hover tooltip; surface the now-playing track as
    // a disabled menu entry at the top instead. The label is updated by
    // `set_tray_tooltip` on every track change.
    #[cfg(target_os = "linux")]
    let (now_playing, sep_now_playing) = {
        let icon = tray_state_icon(&playback_state);
        let label = if cached_tooltip == "Psysonic" {
            format!("{icon} {}", labels.nothing_playing)
        } else {
            format!("{icon} {cached_tooltip}")
        };
        let item = MenuItemBuilder::with_id("now_playing", &label)
            .enabled(false)
            .build(app)?;
        (item, PredefinedMenuItem::separator(app)?)
    };

    #[cfg(target_os = "linux")]
    let menu_builder = MenuBuilder::new(app)
        .item(&now_playing)
        .item(&sep_now_playing);
    #[cfg(not(target_os = "linux"))]
    let menu_builder = MenuBuilder::new(app);

    let menu = menu_builder
        .item(&play_pause)
        .item(&previous)
        .item(&next)
        .item(&sep1)
        .item(&show_hide)
        .item(&sep2)
        .item(&quit)
        .build()?;

    // Persist handles so set_tray_menu_labels and set_tray_tooltip can update
    // them without rebuilding the whole tray icon.
    if let Some(state) = app.try_state::<TrayMenuItemsState>() {
        *state.lock().unwrap() = Some(TrayMenuItems {
            play_pause: play_pause.clone(),
            next: next.clone(),
            previous: previous.clone(),
            show_hide: show_hide.clone(),
            quit: quit.clone(),
            #[cfg(target_os = "linux")]
            now_playing: Some(now_playing.clone()),
            #[cfg(not(target_os = "linux"))]
            now_playing: None,
        });
    }

    #[cfg(target_os = "windows")]
    let tray_builder = TrayIconBuilder::with_id(TRAY_ICON_ID)
        .icon(app_tray_icon(app))
        .menu(&menu)
        .tooltip(&tooltip_with_icon)
        // tray-icon defaults to opening the context menu on every WM_LBUTTONUP when this is true.
        // A left double-click emits Down, Up, DoubleClick, Up — the final Up re-opens the menu right
        // after we hide the window from DoubleClick. We only use left double-click for show/hide
        // (see on_tray_icon_event); keep the menu on right-click like typical Windows tray apps.
        .show_menu_on_left_click(false);
    #[cfg(not(target_os = "windows"))]
    let tray_builder = TrayIconBuilder::with_id(TRAY_ICON_ID)
        .icon(app_tray_icon(app))
        .menu(&menu)
        .tooltip(&cached_tooltip);

    tray_builder
        .on_menu_event(|app, event| match event.id.as_ref() {
            "play_pause" => { let _ = app.emit("tray:play-pause", ()); }
            "next"       => { let _ = app.emit("tray:next", ()); }
            "previous"   => { let _ = app.emit("tray:previous", ()); }
            "show_hide"  => {
                if let Some(win) = app.get_webview_window("main") {
                    if win.is_visible().unwrap_or(false) {
                        let _ = win.eval(PAUSE_RENDERING_JS);
                        let _ = win.hide();
                    } else {
                        let _ = win.eval(RESUME_RENDERING_JS);
                        let _ = win.show();
                        let _ = win.set_focus();
                    }
                }
            }
            "quit" => { let _ = app.emit("app:force-quit", ()); }
            _ => {}
        })
        .on_tray_icon_event(|tray, event| {
            // Windows fires a Click on *every* half of a double-click, so a
            // double-click would toggle the window visibility twice and end up
            // back where it started (the bug #cucadmuh reported). Switch to the
            // Windows-only DoubleClick event there and ignore single clicks;
            // that matches the standard Windows tray convention (Discord, etc).
            #[cfg(target_os = "windows")]
            let should_toggle = matches!(
                event,
                TrayIconEvent::DoubleClick { button: MouseButton::Left, .. }
            );
            #[cfg(not(target_os = "windows"))]
            let should_toggle = matches!(
                event,
                TrayIconEvent::Click {
                    button: MouseButton::Left,
                    button_state: MouseButtonState::Up,
                    ..
                }
            );
            if should_toggle {
                let app = tray.app_handle();
                if let Some(win) = app.get_webview_window("main") {
                    if win.is_visible().unwrap_or(false) {
                        let _ = win.eval(PAUSE_RENDERING_JS);
                        let _ = win.hide();
                    } else {
                        let _ = win.eval(RESUME_RENDERING_JS);
                        let _ = win.show();
                        let _ = win.set_focus();
                    }
                }
            }
        })
        .build(app)
}

/// Creates the tray icon, or `None` if the OS cannot host one.
///
/// On Linux, `libayatana-appindicator3` / `libappindicator3` may be absent (minimal
/// installs, wrong `LD_LIBRARY_PATH`). The `tray-icon` stack can **panic** on `dlopen`
/// failure instead of returning `Err`, so we catch unwind and keep the app running
/// (e.g. cold start with `--player` still works without tray libraries).
pub(crate) fn try_build_tray_icon(app: &tauri::AppHandle) -> Option<TrayIcon> {
    let app = app.clone();
    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| build_tray_icon(&app))) {
        Ok(Ok(tray)) => Some(tray),
        Ok(Err(e)) => {
            crate::app_eprintln!("[Psysonic] System tray unavailable: {e}");
            None
        }
        Err(_) => {
            crate::app_eprintln!(
                "[Psysonic] System tray unavailable — missing libayatana-appindicator3 or libappindicator3 \
                 (install the distro package or set LD_LIBRARY_PATH)"
            );
            None
        }
    }
}

/// Updates the system-tray icon tooltip with the currently playing track.
///
/// `tooltip` should be a compact "Artist – Title" form (no app suffix needed —
/// the tray icon itself identifies the app). An empty string resets to the
/// default `"Psysonic"` tooltip.
///
/// The text is truncated to 127 chars defensively to stay under the historical
/// Windows `NOTIFYICONDATA.szTip` limit (128 bytes including the null terminator).
/// On Linux the visibility depends on the desktop environment / panel —
/// StatusNotifierItem-aware panels (KDE, Cinnamon, GNOME with AppIndicator
/// extension) show it; pure-GNOME without the extension does not.
#[tauri::command]
#[specta::specta]
pub(crate) fn set_tray_tooltip(
    app: tauri::AppHandle,
    tray_state: tauri::State<TrayState>,
    tooltip_cache: tauri::State<TrayTooltip>,
    playback_state_cache: tauri::State<TrayPlaybackState>,
    tooltip: String,
    playback_state: Option<String>,
) -> Result<(), String> {
    let has_track_input = !tooltip.is_empty();
    let state = playback_state.as_deref().unwrap_or(if has_track_input { "play" } else { "stop" });
    let icon = tray_state_icon(state);
    let icon_prefix_len = format!("{icon} ").chars().count();
    let max_text_chars = 127usize.saturating_sub(icon_prefix_len);
    let ellipsis_reserve = 3usize;
    let truncated = if tooltip.chars().count() > max_text_chars {
        let take = max_text_chars.saturating_sub(ellipsis_reserve);
        tooltip.chars().take(take).collect::<String>() + "..."
    } else {
        tooltip
    };
    let has_track = !truncated.is_empty();
    let effective = if has_track { truncated.clone() } else { "Psysonic".to_string() };
    #[cfg(target_os = "windows")]
    let effective_with_icon = format!("{icon} {effective}");

    *tooltip_cache.lock().unwrap() = truncated.clone();
    *playback_state_cache.0.lock().unwrap() = state.to_string();

    if let Some(tray) = tray_state.lock().unwrap().as_ref() {
        #[cfg(target_os = "windows")]
        tray.set_tooltip(Some(&effective_with_icon)).map_err(|e| e.to_string())?;
        #[cfg(not(target_os = "windows"))]
        tray.set_tooltip(Some(&effective)).map_err(|e| e.to_string())?;
    }

    #[cfg(target_os = "linux")]
    {
        if let Some(state) = app.try_state::<TrayMenuItemsState>() {
            if let Some(items) = state.lock().unwrap().as_ref() {
                if let Some(np) = items.now_playing.as_ref() {
                    let label = if has_track {
                        format!("{icon} {effective}")
                    } else {
                        let nothing = app.try_state::<TrayMenuLabelsState>()
                            .map(|s| s.lock().unwrap().nothing_playing.clone())
                            .unwrap_or_else(|| "Nothing playing".to_string());
                        format!("{icon} {nothing}")
                    };
                    let _ = np.set_text(&label);
                }
            }
        }
    }
    #[cfg(not(target_os = "linux"))]
    let _ = &app;

    Ok(())
}

/// Pushes localized labels into the tray menu. Called from the frontend on
/// startup and whenever the i18n language changes. Updates are applied
/// immediately to live menu items via `set_text` (no tray rebuild required)
/// and cached so the labels survive a tray hide/show cycle.
#[tauri::command]
#[specta::specta]
#[allow(clippy::too_many_arguments)]
pub(crate) fn set_tray_menu_labels(
    app: tauri::AppHandle,
    labels_state: tauri::State<TrayMenuLabelsState>,
    items_state: tauri::State<TrayMenuItemsState>,
    tooltip_cache: tauri::State<TrayTooltip>,
    play_pause: String,
    next: String,
    previous: String,
    show_hide: String,
    quit: String,
    nothing_playing: String,
) -> Result<(), String> {
    let new_labels = TrayMenuLabels {
        play_pause,
        next,
        previous,
        show_hide,
        quit,
        nothing_playing,
    };
    *labels_state.lock().unwrap() = new_labels.clone();

    if let Some(items) = items_state.lock().unwrap().as_ref() {
        let _ = items.play_pause.set_text(&new_labels.play_pause);
        let _ = items.next.set_text(&new_labels.next);
        let _ = items.previous.set_text(&new_labels.previous);
        let _ = items.show_hide.set_text(&new_labels.show_hide);
        let _ = items.quit.set_text(&new_labels.quit);

        // Linux now-playing item: only refresh the placeholder. The track
        // text itself is owned by `set_tray_tooltip` and shouldn't be
        // overwritten by an unrelated language change.
        #[cfg(target_os = "linux")]
        if let Some(np) = items.now_playing.as_ref() {
            let has_track = !tooltip_cache.lock().unwrap().is_empty();
            if !has_track {
                let state = app
                    .try_state::<TrayPlaybackState>()
                    .map(|s| s.0.lock().unwrap().clone())
                    .unwrap_or_else(|| "stop".to_string());
                let label = format!("{} {}", tray_state_icon(&state), new_labels.nothing_playing);
                let _ = np.set_text(&label);
            }
        }
    }

    let _ = (&app, &tooltip_cache);
    Ok(())
}

/// Show (`true`) or fully remove (`false`) the system-tray icon.
///
/// The command is strictly idempotent:
/// - `show=true`  when the icon is already present → no-op (prevents duplicate icons).
/// - `show=false` when the icon is already absent  → no-op.
///
/// For removal, `set_visible(false)` is called explicitly before the handle is
/// dropped because some platforms (Windows notification area, certain Linux DEs)
/// process the OS removal asynchronously — hiding first prevents a brief "ghost"
/// icon from appearing alongside a freshly created one.
#[tauri::command]
#[specta::specta]
pub(crate) fn toggle_tray_icon(
    app: tauri::AppHandle,
    tray_state: tauri::State<TrayState>,
    show: bool,
) -> Result<(), String> {
    let mut guard = tray_state.lock().unwrap();

    if show {
        // Early-return when already shown — never build a second icon.
        if guard.is_some() {
            return Ok(());
        }
        let Some(tray) = try_build_tray_icon(&app) else {
            return Err(
                "Tray icon could not be created (missing system libraries on Linux).".into(),
            );
        };
        *guard = Some(tray);
    } else if let Some(tray) = guard.take() {
        // Hide synchronously before dropping so the OS processes the removal
        // before any subsequent show=true call can create a new icon.
        let _ = tray.set_visible(false);
        // `tray` drops here → frees the OS resource (NIM_DELETE / StatusNotifierItem / NSStatusItem).
    }

    Ok(())
}

pub(crate) use crate::audio::stop_audio_engine;

/// Returns `true` if running under a tiling window manager (Hyprland, Niri, Sway, i3,
/// bspwm, AwesomeWM, Openbox, etc.).  Detection is based on environment variables
/// set by the compositor / DE.
#[cfg(target_os = "linux")]
pub(crate) fn is_tiling_wm() -> bool {
    // Direct compositor signatures (most reliable).
    let direct = [
        "HYPRLAND_INSTANCE_SIGNATURE", // Hyprland
        "NIRI_SOCKET",                  // Niri
        "SWAYSOCK",                     // Sway
        "I3SOCK",                       // i3
    ]
    .iter()
    .any(|&var| std::env::var_os(var).is_some());

    if direct {
        return true;
    }

    // Check XDG_CURRENT_DESKTOP for known tiling WMs.
    if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") {
        let desktop = desktop.to_lowercase();
        let tiling_wms = [
            "hyprland", "niri", "sway", "i3", "bspwm", "awesome", "openbox",
            "xmonad", "dwm", "qtile", "herbstluftwm", "leftwm",
        ];
        if tiling_wms.iter().any(|&wm| desktop.contains(wm)) {
            return true;
        }
    }

    false
}

/// Tauri command: returns true when WEBKIT_DISABLE_COMPOSITING_MODE=1 is set.
/// The frontend uses this to apply a CSS class that swaps out GPU-only effects
/// (backdrop-filter, CSS filter, mask-image) for software-friendly equivalents.
#[tauri::command]
#[specta::specta]
pub(crate) fn no_compositing_mode() -> bool {
    std::env::var("WEBKIT_DISABLE_COMPOSITING_MODE")
        .map(|v| v == "1")
        .unwrap_or(false)
}

/// Tauri command: `XDG_SESSION_TYPE` from the host environment (e.g. `wayland`, `x11`).
/// Used for Linux-only UI tweaks such as font rasterisation hints; empty string when unset.
#[tauri::command]
#[specta::specta]
pub(crate) fn linux_xdg_session_type() -> String {
    #[cfg(target_os = "linux")]
    {
        std::env::var("XDG_SESSION_TYPE").unwrap_or_default()
    }
    #[cfg(not(target_os = "linux"))]
    {
        String::new()
    }
}

#[cfg(not(target_os = "linux"))]
pub(crate) fn is_tiling_wm() -> bool {
    false
}

/// Tauri command: lets the frontend know whether we're running under a tiling
/// WM so it can decide whether to render the custom TitleBar component.
#[tauri::command]
#[specta::specta]
pub(crate) fn is_tiling_wm_cmd() -> bool {
    is_tiling_wm()
}

#[cfg(all(test, debug_assertions))]
mod tests {
    use super::*;

    #[test]
    fn flip_rgba_horizontal_mirrors_pixels() {
        // 3×1: A B C → C B A
        let mut rgba = vec![
            1, 0, 0, 255, // A
            2, 0, 0, 255, // B
            3, 0, 0, 255, // C
        ];
        flip_rgba_horizontal(&mut rgba, 3, 1);
        assert_eq!(rgba[0], 3);
        assert_eq!(rgba[4], 2);
        assert_eq!(rgba[8], 1);
    }
}
