// Forge built-in topbar — replaces the external liberux-topbar layer-shell client.
// Renders directly into the compositor frame using GLES textures (like ssd.rs).
//
// Layout (logical px):
//   [ notif | spacer | clock | spacer | cellular wifi bluetooth battery battery% ]
//
// Click handling routes to forge actions (notifications, quick settings) which
// crustace receives via IPC and renders as overlay surfaces.

use chrono::Local;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle};

use crate::drawing::{ForgeElement, RenderTexture, Texture};
use crate::icons::IconCache;
use crate::output::Canvas;
use crate::sysmon::SystemState;
use crate::text::TextRenderer;

use ltk::theme::{active_mode, set_active_mode, ThemeMode};
use ltk::theme_palette;
use ltk::Color;

pub const TOPBAR_HEIGHT: i32 = 28;          // logical pixels
const ICON_SIZE: i32        = 18;           // logical pixels
const PAD_H: i32            = 8;
const ICON_GAP: i32         = 6;
const TEXT_GAP: i32         = 4;
const FONT_SIZE: f32        = 13.0;

// Notification badge — theme-independent by design (alert red).
const BADGE_BG:  [u8; 4] = [220, 60, 60, 255];
const BADGE_TXT: [u8; 4] = [255, 255, 255, 255];
const BADGE_FONT_SIZE: f32 = 10.0;
const BADGE_SIZE: i32 = 12;        // logical px (square badge)

fn color_to_rgba8(c: Color) -> [u8; 4] {
    [
        (c.r.clamp(0.0, 1.0) * 255.0) as u8,
        (c.g.clamp(0.0, 1.0) * 255.0) as u8,
        (c.b.clamp(0.0, 1.0) * 255.0) as u8,
        (c.a.clamp(0.0, 1.0) * 255.0) as u8,
    ]
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TopbarAction {
    OpenNotifications,
    OpenQuickSettings,
    ToggleCalendar,
}

/// Cached icon texture keyed by a state string ("battery-good", "wifi-3", ...).
struct CachedIcon {
    tex:    RenderTexture,
    px_w:   i32,
}

pub struct Topbar {
    bar_bg:        RenderTexture,
    badge_bg:      RenderTexture,
    // Last-rendered text strings so we can avoid re-rasterizing every frame.
    clock_str:     String,
    clock_tex:     Option<RenderTexture>,
    clock_size:    (i32, i32),
    battery_str:   String,
    battery_tex:   Option<RenderTexture>,
    battery_size:  (i32, i32),
    badge_str:     String,
    badge_tex:     Option<RenderTexture>,
    badge_size:    (i32, i32),
    /// Notification count badge value (0 = no badge).
    pub notification_count: u32,
    icons:         std::collections::HashMap<&'static str, CachedIcon>,
    icon_cache:    IconCache,
    /// Primary icon names whose full fallback chain has already failed once.
    /// Used so `ensure_icon_with_fallback` can warn the first time a chain
    /// comes up empty — typically because no symbolic icon theme is
    /// installed on the device — without spamming the log every frame.
    warned_missing: std::collections::HashSet<&'static str>,
    /// Cached foreground colours drawn from `ltk::theme_palette()`.
    /// When ltk's active mode changes we invalidate text/icon caches and
    /// rebuild `bar_bg` so every glyph and pictogram follows the palette.
    mode_cached:   Option<ThemeMode>,
    /// Output scale the cached rasters were baked at.
    last_scale:    f64,
    fg_text:       [u8; 4],
    fg_icon:       [u8; 4],
    // Hit areas in logical coords, recomputed on render.
    hit_notif:     Option<Rectangle<i32, Logical>>,
    hit_status:    Option<Rectangle<i32, Logical>>,
}

impl std::fmt::Debug for Topbar {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Topbar").finish()
    }
}

impl Topbar {
    pub fn new() -> Self {
        let palette = theme_palette();
        Self {
            bar_bg:       RenderTexture::new(Texture::from_color(color_to_rgba8(palette.surface), false)),
            badge_bg:     RenderTexture::new(Texture::from_color(BADGE_BG, false)),
            clock_str:    String::new(),
            clock_tex:    None,
            clock_size:   (0, 0),
            battery_str:  String::new(),
            battery_tex:  None,
            battery_size: (0, 0),
            badge_str:    String::new(),
            badge_tex:    None,
            badge_size:   (0, 0),
            notification_count: 0,
            icons:        std::collections::HashMap::new(),
            icon_cache:   IconCache::new(),
            warned_missing: std::collections::HashSet::new(),
            mode_cached:  None,
            last_scale:   0.0,
            fg_text:      color_to_rgba8(palette.text_primary),
            fg_icon:      color_to_rgba8(palette.icon),
            hit_notif:    None,
            hit_status:   None,
        }
    }

    pub fn set_notification_count(&mut self, count: u32) {
        self.notification_count = count;
    }

    /// Logical height reserved by the topbar.
    pub fn height() -> i32 { TOPBAR_HEIGHT }

    fn current_clock() -> String {
        Local::now().format("%H:%M").to_string()
    }

    fn battery_icon_name(percent: u8, charging: bool) -> &'static str {
        if charging {
            return match percent {
                0..=10  => "battery-empty-charging-symbolic",
                11..=30 => "battery-low-charging-symbolic",
                31..=70 => "battery-good-charging-symbolic",
                _       => "battery-full-charging-symbolic",
            };
        }
        match percent {
            0..=10  => "battery-empty-symbolic",
            11..=30 => "battery-low-symbolic",
            31..=70 => "battery-good-symbolic",
            _       => "battery-full-symbolic",
        }
    }

    fn wifi_icon_name(strength: u8, connected: bool) -> &'static str {
        if !connected { return "network-wireless-offline-symbolic"; }
        match strength {
            0..=25  => "network-wireless-signal-weak-symbolic",
            26..=50 => "network-wireless-signal-ok-symbolic",
            51..=75 => "network-wireless-signal-good-symbolic",
            _       => "network-wireless-signal-excellent-symbolic",
        }
    }

    /// Try a sequence of icon names; return the first that loads.
    fn ensure_icon_with_fallback(
        &mut self,
        names:    &[&'static str],
        size_log: i32,
        renderer: &mut GlesRenderer,
        scale:    f64,
    ) -> Option<&'static str> {
        for &n in names {
            self.ensure_icon(n, size_log, renderer, scale);
            if self.icons.contains_key(n) { return Some(n); }
        }
        // Whole fallback chain failed. Almost always means the device is
        // missing the symbolic icon theme that supplies these glyphs
        // (`adwaita-icon-theme` ships them under `symbolic/status/`).
        // Without this warning the lateral topbar icons just silently
        // disappear, which is exactly the "i simboli laterali non si
        // vedono in modalità mobile" symptom on the Librem 5 base image.
        if let Some(&primary) = names.first() {
            if self.warned_missing.insert(primary) {
                tracing::warn!(
                    "topbar icon missing (symbolic theme not installed?): tried {:?}",
                    names,
                );
            }
        }
        None
    }

    /// Drop the rasters whose cache key is the string alone, so the next
    /// frame regenerates them at the current text scale.
    pub fn invalidate_text(&mut self) {
        self.clock_str.clear();
        self.clock_tex = None;
        self.battery_str.clear();
        self.battery_tex = None;
        self.badge_str.clear();
        self.badge_tex = None;
    }

    fn ensure_text(
        &mut self,
        which:        Which,
        text:         &str,
        renderer:     &mut GlesRenderer,
        text_renderer:&mut TextRenderer,
        scale:        f64,
    ) {
        let fg = self.fg_text;
        let (cur_str, cur_tex, cur_size, font_size, color) = match which {
            Which::Clock   => (&mut self.clock_str,   &mut self.clock_tex,   &mut self.clock_size,   FONT_SIZE,       fg),
            Which::Battery => (&mut self.battery_str, &mut self.battery_tex, &mut self.battery_size, FONT_SIZE,       fg),
            Which::Badge   => (&mut self.badge_str,   &mut self.badge_tex,   &mut self.badge_size,   BADGE_FONT_SIZE, BADGE_TXT),
        };
        if cur_str == text && cur_tex.is_some() { return; }
        *cur_str = text.to_string();
        let (buf, w, h) = text_renderer.render_line(text, crate::text::text_px(font_size, scale), color);
        if w > 0 && h > 0 {
            *cur_tex  = Some(RenderTexture::new(
                Texture::from_buffer(renderer, scale, &buf, w as i32, h as i32, false),
            ));
            *cur_size = (w as i32, h as i32);
        } else {
            *cur_tex  = None;
            *cur_size = (0, 0);
        }
    }

    fn ensure_icon(
        &mut self,
        name:     &'static str,
        size_log: i32,
        renderer: &mut GlesRenderer,
        scale:    f64,
    ) {
        if self.icons.contains_key(name) { return; }
        let size_phys = (size_log as f64 * scale) as u32;
        let Some((rgba, w, h)) = self.icon_cache.get(name, size_phys).cloned() else {
            return;
        };
        // Recolor the symbolic icon to the palette's `icon` token so it
        // keeps enough contrast against the bar surface in every variant.
        let tint = Color {
            r: self.fg_icon[0] as f32 / 255.0,
            g: self.fg_icon[1] as f32 / 255.0,
            b: self.fg_icon[2] as f32 / 255.0,
            a: self.fg_icon[3] as f32 / 255.0,
        };
        // `ltk::tint_symbolic` writes straight-alpha `(tint.rgb, src.a)`.
        // smithay's GLES frame uses `GL_ONE, GL_ONE_MINUS_SRC_ALPHA` blend,
        // which assumes premultiplied — feeding straight-alpha made every
        // transparent pixel contribute full tint colour additively, so the
        // icon's 18×18 bounding box rendered as a flat rectangle instead
        // of the glyph shape (bright white square in Dark on the Librem,
        // muddy navy-on-navy in Light). Premultiply the RGB by the final
        // alpha here so the blend sees the correct shape.
        let mut tinted = ltk::tint_symbolic(&rgba, tint);
        for px in tinted.chunks_exact_mut(4) {
            let a = px[3] as u32;
            px[0] = ((px[0] as u32 * a) / 255) as u8;
            px[1] = ((px[1] as u32 * a) / 255) as u8;
            px[2] = ((px[2] as u32 * a) / 255) as u8;
        }
        let tex = RenderTexture::new(
            Texture::from_buffer(renderer, scale, &tinted, w as i32, h as i32, false),
        );
        self.icons.insert(name, CachedIcon { tex, px_w: w as i32 });
    }

    /// Build the topbar as ForgeElements. Called from Windows::render().
    /// `bar_width` is the canvas logical width.
    pub fn render(
        &mut self,
        textures:      &mut Vec<ForgeElement>,
        canvas:        &Canvas,
        renderer:      &mut GlesRenderer,
        text_renderer: &mut TextRenderer,
        sys:           &SystemState,
        bar_width:     i32,
    ) {
        let scale = canvas.scale();

        // Bridge the system color-scheme we polled in sysmon into ltk's
        // active state so this process (topbar + ssd + every widget) picks
        // the same variant. `set_active_mode` is a no-op cost when unchanged.
        let desired = if sys.dark_mode { ThemeMode::Dark } else { ThemeMode::Light };
        if active_mode() != desired {
            set_active_mode(desired);
        }

        // If the ltk mode flipped since our last render, flush caches so the
        // next ensure_text / ensure_icon regenerate with the new palette.
        let current_mode = active_mode();
        if self.mode_cached != Some(current_mode) {
            self.mode_cached = Some(current_mode);
            let palette = theme_palette();
            self.fg_text = color_to_rgba8(palette.text_primary);
            self.fg_icon = color_to_rgba8(palette.icon);
            self.bar_bg  = RenderTexture::new(Texture::from_color(color_to_rgba8(palette.surface), false));
            self.clock_str.clear();   self.clock_tex   = None;
            self.battery_str.clear(); self.battery_tex = None;
            self.icons.clear();
        }

        // `ensure_text` and `ensure_icon` key their caches on the string and
        // the icon name alone, so a scale change would otherwise keep serving
        // rasters baked for the previous one.
        if (self.last_scale - scale).abs() > 0.001 {
            self.last_scale = scale;
            self.clock_str.clear();   self.clock_tex   = None;
            self.battery_str.clear(); self.battery_tex = None;
            self.badge_str.clear();   self.badge_tex   = None;
            self.icons.clear();
        }

        // Element-list ordering note: smithay's `draw_render_elements`
        // iterates `textures` and treats elements added EARLIER as being
        // ON TOP of elements added later (each element pre-empts the
        // damage region of the ones after it). Concretely, `textures[0]`
        // is the topmost layer of the frame. So the topbar adds its
        // foreground first (clock, battery%, status icons, notification
        // icon + badge) and the `bar_bg` last — that way the bar surface
        // sits behind the glyphs and pictograms instead of being painted
        // on top of them. The previous order had `bar_bg` first and the
        // 60 % surface alpha then washed every glyph through the bar
        // colour (white-ish in Light, navy in Dark) — in Dark mode that
        // produced the "navy clock and battery on a navy bar" rendering
        // Pedro screenshotted on the Librem.
        let bar_log = Rectangle::<i32, Logical>::new(
            Point::from((0, 0)),
            (bar_width, TOPBAR_HEIGHT).into(),
        );
        let bar_phys = bar_log.to_physical_precise_round(scale);

        // ── Clock (centered) ──────────────────────────────────────────────
        let clock_text = Self::current_clock();
        self.ensure_text(Which::Clock, &clock_text, renderer, text_renderer, scale);
        if let Some(tex) = &self.clock_tex {
            let (tw, th) = self.clock_size;
            // Position in physical px (clock_size is already physical)
            let x_phys = (bar_width as f64 * scale / 2.0) as i32 - tw / 2;
            let y_phys = ((TOPBAR_HEIGHT as f64 * scale - th as f64) / 2.0) as i32;
            let pos = Point::<i32, Physical>::from((x_phys, y_phys));
            ForgeElement::add_element(textures, tex.clone(), pos, bar_phys, None, scale);
        }

        // ── Right side: status icons + battery% ───────────────────────────
        // Layout right-to-left so we don't have to know the total width upfront.
        let mut cursor_x = bar_width - PAD_H;  // logical
        let mid_y_log    = TOPBAR_HEIGHT / 2;
        let icon_top_log = mid_y_log - ICON_SIZE / 2;

        // Battery percent text
        if sys.battery.present {
            let bp = format!("{}%", sys.battery.percent);
            self.ensure_text(Which::Battery, &bp, renderer, text_renderer, scale);
            if let Some(tex) = &self.battery_tex {
                let (tw, th) = self.battery_size;
                let log_w = (tw as f64 / scale).ceil() as i32;
                cursor_x -= log_w;
                let x_phys = (cursor_x as f64 * scale) as i32;
                let y_phys = ((TOPBAR_HEIGHT as f64 * scale - th as f64) / 2.0) as i32;
                let pos = Point::<i32, Physical>::from((x_phys, y_phys));
                ForgeElement::add_element(textures, tex.clone(), pos, bar_phys, None, scale);
                cursor_x -= TEXT_GAP;
            }
            // Battery icon (try symbolic, then plain "battery-symbolic" fallback)
            let primary = Self::battery_icon_name(sys.battery.percent, sys.battery.charging);
            let icon = self.ensure_icon_with_fallback(
                &[primary, "battery-symbolic"],
                ICON_SIZE, renderer, scale,
            ).unwrap_or(primary);
            if let Some(ci) = self.icons.get(icon) {
                let log_w = (ci.px_w as f64 / scale).ceil() as i32;
                cursor_x -= log_w;
                let pos_log = Point::<i32, Logical>::from((cursor_x, icon_top_log));
                let rect_log = Rectangle::new(pos_log, (log_w, ICON_SIZE).into());
                let rect_phys = rect_log.to_physical_precise_round(scale);
                ForgeElement::add_element(textures, ci.tex.clone(), rect_phys.loc, rect_phys, None, scale);
                cursor_x -= ICON_GAP;
            }
        }

        // Bluetooth icon (only when on)
        if sys.bluetooth_on {
            let icon = self.ensure_icon_with_fallback(
                &["bluetooth-active-symbolic", "bluetooth-symbolic"],
                ICON_SIZE, renderer, scale,
            ).unwrap_or("bluetooth-symbolic");
            if let Some(ci) = self.icons.get(icon) {
                let log_w = (ci.px_w as f64 / scale).ceil() as i32;
                cursor_x -= log_w;
                let pos_log = Point::<i32, Logical>::from((cursor_x, icon_top_log));
                let rect_log = Rectangle::new(pos_log, (log_w, ICON_SIZE).into());
                let rect_phys = rect_log.to_physical_precise_round(scale);
                ForgeElement::add_element(textures, ci.tex.clone(), rect_phys.loc, rect_phys, None, scale);
                cursor_x -= ICON_GAP;
            }
        }

        // VPN icon (only when an active tunnel is up). NetworkManager and
        // wireguard surfaces both publish through `sys.vpn.active` — see
        // `sysmon.rs::vpn_state`. Renders right of WiFi so the user can
        // tell at a glance whether the connection is going through a VPN.
        if sys.vpn.active {
            let icon = self.ensure_icon_with_fallback(
                &[
                    "network-vpn-symbolic",
                    "network-vpn-acquiring-symbolic",
                    "channel-secure-symbolic",
                ],
                ICON_SIZE, renderer, scale,
            ).unwrap_or("network-vpn-symbolic");
            if let Some(ci) = self.icons.get(icon) {
                let log_w = (ci.px_w as f64 / scale).ceil() as i32;
                cursor_x -= log_w;
                let pos_log = Point::<i32, Logical>::from((cursor_x, icon_top_log));
                let rect_log = Rectangle::new(pos_log, (log_w, ICON_SIZE).into());
                let rect_phys = rect_log.to_physical_precise_round(scale);
                ForgeElement::add_element(textures, ci.tex.clone(), rect_phys.loc, rect_phys, None, scale);
                cursor_x -= ICON_GAP;
            }
        }

        // WiFi icon — fallback to plain wireless-symbolic if signal variant missing.
        let wifi_primary = Self::wifi_icon_name(sys.wifi.strength, sys.wifi.connected);
        let wifi_icon = self.ensure_icon_with_fallback(
            &[wifi_primary, "network-wireless-symbolic"],
            ICON_SIZE, renderer, scale,
        ).unwrap_or(wifi_primary);
        if let Some(ci) = self.icons.get(wifi_icon) {
            let log_w = (ci.px_w as f64 / scale).ceil() as i32;
            cursor_x -= log_w;
            let pos_log = Point::<i32, Logical>::from((cursor_x, icon_top_log));
            let rect_log = Rectangle::new(pos_log, (log_w, ICON_SIZE).into());
            let rect_phys = rect_log.to_physical_precise_round(scale);
            ForgeElement::add_element(textures, ci.tex.clone(), rect_phys.loc, rect_phys, None, scale);
            cursor_x -= ICON_GAP;
        }

        // Cellular icon (only when present)
        if sys.cellular.present {
            let primary = if sys.cellular.signal_quality > 50 {
                "network-cellular-signal-good-symbolic"
            } else if sys.cellular.signal_quality > 0 {
                "network-cellular-signal-weak-symbolic"
            } else {
                "network-cellular-signal-none-symbolic"
            };
            let icon = self.ensure_icon_with_fallback(
                &[primary, "network-cellular-symbolic"],
                ICON_SIZE, renderer, scale,
            ).unwrap_or(primary);
            if let Some(ci) = self.icons.get(icon) {
                let log_w = (ci.px_w as f64 / scale).ceil() as i32;
                cursor_x -= log_w;
                let pos_log = Point::<i32, Logical>::from((cursor_x, icon_top_log));
                let rect_log = Rectangle::new(pos_log, (log_w, ICON_SIZE).into());
                let rect_phys = rect_log.to_physical_precise_round(scale);
                ForgeElement::add_element(textures, ci.tex.clone(), rect_phys.loc, rect_phys, None, scale);
                cursor_x -= ICON_GAP;
            }
        }

        // Status hit area = right block from cursor_x to bar_width
        self.hit_status = Some(Rectangle::new(
            Point::from((cursor_x, 0)),
            (bar_width - cursor_x, TOPBAR_HEIGHT).into(),
        ));

        // ── Left side: notification icon ──────────────────────────────────
        let notif_icon = self.ensure_icon_with_fallback(
            &[
                "notification-symbolic",
                "notifications-symbolic",
                "preferences-system-notifications-symbolic",
            ],
            ICON_SIZE, renderer, scale,
        ).unwrap_or("notification-symbolic");
        if let Some(ci) = self.icons.get(notif_icon) {
            let log_w = (ci.px_w as f64 / scale).ceil() as i32;
            let pos_log = Point::<i32, Logical>::from((PAD_H, icon_top_log));
            let rect_log = Rectangle::new(pos_log, (log_w, ICON_SIZE).into());
            let rect_phys = rect_log.to_physical_precise_round(scale);
            ForgeElement::add_element(textures, ci.tex.clone(), rect_phys.loc, rect_phys, None, scale);
            self.hit_notif = Some(Rectangle::new(
                Point::from((0, 0)),
                (PAD_H + log_w + PAD_H, TOPBAR_HEIGHT).into(),
            ));

            // Notification count badge — top-right of the notification icon.
            if self.notification_count > 0 {
                // Show the exact count up to 99; cap at "99+" so the badge
                // never overflows its 12 px disc with three+ digits.
                let label = if self.notification_count > 99 { "99+".to_string() }
                            else { self.notification_count.to_string() };
                self.ensure_text(Which::Badge, &label, renderer, text_renderer, scale);
                let badge_x_log = PAD_H + log_w - BADGE_SIZE / 2;
                let badge_y_log = (icon_top_log - BADGE_SIZE / 2).max(0);
                let badge_log = Rectangle::<i32, Logical>::new(
                    (badge_x_log, badge_y_log).into(),
                    (BADGE_SIZE, BADGE_SIZE).into(),
                );
                let badge_phys = badge_log.to_physical_precise_round(scale);
                ForgeElement::add_element(textures, self.badge_bg.clone(), badge_phys.loc, badge_phys, None, scale);
                if let Some(tex) = &self.badge_tex {
                    let (tw, th) = self.badge_size;
                    let cx_phys = ((badge_x_log + BADGE_SIZE / 2) as f64 * scale) as i32 - tw / 2;
                    let cy_phys = ((badge_y_log + BADGE_SIZE / 2) as f64 * scale) as i32 - th / 2;
                    let pos = Point::<i32, Physical>::from((cx_phys, cy_phys));
                    ForgeElement::add_element(textures, tex.clone(), pos, badge_phys, None, scale);
                }
            }
        } else {
            // If icon not found, still allow tapping the leftmost area
            self.hit_notif = Some(Rectangle::new(
                Point::from((0, 0)),
                (PAD_H * 4, TOPBAR_HEIGHT).into(),
            ));
        }

        // Bar surface — added last so it lands at the back of the
        // element stack (see ordering note at the top of this function).
        ForgeElement::add_element(textures, self.bar_bg.clone(), bar_phys.loc, bar_phys, None, scale);
    }

    /// Hit-test a logical-coordinate point against the topbar.
    /// Returns `None` if the point is below the topbar.
    /// Notification area on the left, status area on the right, clock area in
    /// the middle (between the two).
    pub fn hit_test(&self, point: Point<f64, Logical>) -> Option<TopbarAction> {
        let py = point.y as i32;
        if py < 0 || py >= TOPBAR_HEIGHT { return None; }
        let px = point.x as i32;
        if let Some(r) = &self.hit_notif {
            if px >= r.loc.x && px < r.loc.x + r.size.w {
                return Some(TopbarAction::OpenNotifications);
            }
        }
        if let Some(r) = &self.hit_status {
            if px >= r.loc.x && px < r.loc.x + r.size.w {
                return Some(TopbarAction::OpenQuickSettings);
            }
        }
        // Anything else inside the topbar strip is the clock/center region.
        Some(TopbarAction::ToggleCalendar)
    }
}

#[derive(Copy, Clone)]
enum Which { Clock, Battery, Badge }
