//! The switcher's cached visual assets: theme colors, panel background
//! compositing, labels, fills and the close button.

use std::sync::mpsc;

use smithay::backend::renderer::gles::GlesRenderer;

use crate::canvas::{Color, SoftCanvas};
use crate::drawing::{RenderTexture, Texture};
use crate::output::Canvas;
use crate::text::TextRenderer;

use super::*;


impl AppSwitcher {
    pub(super) fn theme_key() -> String {
        format!(
            "{}:{:?}",
            ltk::theme::active_theme_id(),
            ltk::theme::active_mode()
        )
    }

    pub(super) fn spawn_bg_decode_thread(
        raster: u32,
    ) -> (Option<mpsc::Receiver<Option<(Vec<u8>, u32, u32)>>>, bool) {
        let Some(path) = ltk::theme_branding_image("lockscreen", 0, 0) else {
            return (None, false);
        };
        let (tx, rx) = mpsc::sync_channel(1);
        let spawned = std::thread::Builder::new()
            .name("app-switcher-bg".into())
            .spawn(move || {
                let _ = tx.send(load_bg_pixels(path, raster));
            })
            .is_ok();
        if spawned {
            (Some(rx), true)
        } else {
            (None, false)
        }
    }

    /// Re-check the active theme/mode every frame. When it changes,
    /// drop the cached lockscreen rgba + composed texture + close
    /// button raster + theme-coloured solid rects / labels, then start
    /// a fresh decode of the new theme's lockscreen image. Cheap when
    /// nothing has changed (one HashMap lookup + a `String` compare).
    pub fn tick_theme(&mut self) {
        let key = Self::theme_key();
        if key == self.current_theme_key {
            return;
        }
        self.current_theme_key = key;
        self.panel_bg_rgba = None;
        self.panel_bg_composed.clear();
        self.panel_bg_compose_pending = None;
        self.close_btn_tex = None;
        self.solid_rect_cache.clear();
        self.label_cache.clear();
        let (rx, loaded) = Self::spawn_bg_decode_thread(self.bg_raster_target);
        self.panel_bg_pending = rx;
        self.panel_bg_loaded = loaded;
    }


    pub(super) fn make_rounded_texture(
        renderer: &mut GlesRenderer,
        scale: f64,
        w_log: i32,
        h_log: i32,
        rgba: [u8; 4],
        radius_log: f32,
    ) -> RenderTexture {
        let w_px = ((w_log as f64) * scale).round().max(1.0) as u32;
        let h_px = ((h_log as f64) * scale).round().max(1.0) as u32;
        let mut canvas = SoftCanvas::new(w_px, h_px);
        canvas.clear();
        let color = Color::rgba8(rgba[0], rgba[1], rgba[2], rgba[3]);
        canvas.fill_rect(
            0.0,
            0.0,
            w_px as f32,
            h_px as f32,
            color,
            (radius_log as f64 * scale) as f32,
        );
        let tex = Texture::from_buffer(
            renderer,
            scale,
            canvas.data(),
            w_px as i32,
            h_px as i32,
            false,
        );
        RenderTexture::new(tex)
    }


    /// Crustace pushes the live pill height (logical px) via the
    /// `set_dock_zone` IPC action. We honour it as the bottom margin
    /// so the alt-tab panel never paints over the dock when the user
    /// brings the cursor down to reveal it.
    pub(super) fn try_load_panel_bg(&mut self, _renderer: &mut GlesRenderer, _scale: f64) {
        if !self.panel_bg_loaded {
            self.panel_bg_loaded = true;
            if let Some(path) = ltk::theme_branding_image("lockscreen", 0, 0) {
                let raster = self.bg_raster_target;
                let (tx, rx) = mpsc::sync_channel(1);
                std::thread::Builder::new()
                    .name("app-switcher-bg".into())
                    .spawn(move || {
                        let _ = tx.send(load_bg_pixels(path, raster));
                    })
                    .ok();
                self.panel_bg_pending = Some(rx);
            }
        }

        if self.panel_bg_rgba.is_none() {
            if let Some(rx) = self.panel_bg_pending.take() {
                match rx.try_recv() {
                    Ok(Some((rgba, w, h))) => {
                        self.panel_bg_rgba = Some((rgba, w, h));
                    }
                    Ok(None) => {} // decode failed; solid fallback is used permanently
                    Err(mpsc::TryRecvError::Empty) => {
                        self.panel_bg_pending = Some(rx); // still decoding
                    }
                    Err(mpsc::TryRecvError::Disconnected) => {} // thread panic
                }
            }
        }
    }


    /// Build (or reuse) the panel-sized composition: the lockscreen image
    /// cover-scaled and masked to the panel's rounded silhouette so the
    /// switcher's corners read rounded instead of square. Returns the
    /// freshly cached texture, or `None` while the image hasn't decoded
    /// yet (caller renders the `panel_bg_solid` fallback in that case).
    pub(super) fn ensure_panel_bg_composed(
        &mut self,
        renderer: &mut GlesRenderer,
        scale: f64,
        panel_w_log: i32,
        panel_h_log: i32,
        scale_key: i32,
        radius_log: f32,
    ) -> Option<RenderTexture> {
        let key = (panel_w_log, panel_h_log, scale_key);
        if let Some(tex) = self.panel_bg_composed.get(&key) {
            return Some(tex.clone());
        }

        // An off-thread compose may be in flight. Take it out so the field is
        // free to reassign, then decide: upload it, put it back (still busy),
        // or drop it (finished/stale) and fall through to a fresh compose.
        if let Some((pk, rx)) = self.panel_bg_compose_pending.take() {
            if pk == key {
                match rx.try_recv() {
                    Ok((rgba, w_px, h_px)) => {
                        let tex = Texture::from_buffer(
                            renderer, scale, &rgba, w_px as i32, h_px as i32, false,
                        );
                        let rt = RenderTexture::new(tex);
                        self.panel_bg_composed.insert(key, rt.clone());
                        return Some(rt);
                    },
                    Err(mpsc::TryRecvError::Empty) => {
                        self.panel_bg_compose_pending = Some((pk, rx));
                        return None;
                    },
                    Err(mpsc::TryRecvError::Disconnected) => {},
                }
            } else {
                // Another screen's compose is in flight; put it back and wait
                // our turn. Dropping it would have two panels cancelling each
                // other's compose every frame, and neither would ever finish.
                self.panel_bg_compose_pending = Some((pk, rx));
                return None;
            }
        }

        let panel_w_px = ((panel_w_log as f64) * scale).round().max(1.0) as u32;
        let panel_h_px = ((panel_h_log as f64) * scale).round().max(1.0) as u32;

        // Upgrade the backdrop master when the panel outgrows it (a 4K/8K
        // output would otherwise cover-scale a 1920 px raster): raise the
        // target and re-arm the decode thread; the solid fallback covers
        // until the sharper master lands.
        let need = panel_w_px.max(panel_h_px).max(PANEL_BG_RASTER_SIZE);
        if need > self.bg_raster_target {
            self.bg_raster_target = need;
            self.panel_bg_rgba = None;
            self.panel_bg_composed.clear();
            self.panel_bg_pending = None;
            self.panel_bg_loaded = false;
            return None;
        }

        // Nothing cached or in flight for this key: compose on a worker thread
        // (the cover-scale + Lanczos resize is the heavy part) and show the
        // solid fallback until it lands.
        let (rgba, img_w, img_h) = match self.panel_bg_rgba.as_ref() {
            Some((r, w, h)) if *w > 0 && *h > 0 => (r.clone(), *w, *h),
            _ => return None,
        };
        let radius_px = (radius_log as f64 * scale) as f32;
        let (tx, rx) = mpsc::sync_channel(1);
        let spawned = std::thread::Builder::new()
            .name("app-switcher-bg-compose".into())
            .spawn(move || {
                let rgba =
                    compose_panel_bg_rgba(rgba, img_w, img_h, panel_w_px, panel_h_px, radius_px);
                let _ = tx.send((rgba, panel_w_px, panel_h_px));
            })
            .is_ok();
        if spawned {
            self.panel_bg_compose_pending = Some((key, rx));
        }
        None
    }


    /// Build the size/scale-dependent caches (panel solid fill, frosted
    /// background, close button) ahead of the first open so the opening
    /// animation doesn't stall while they raster on the first swipe-up. Called
    /// on idle frames while the switcher is closed; every step is self-guarded,
    /// so it only does work the first time (and after a theme / size change).
    /// Entry-dependent caches (labels, thumbnails) still build lazily on open.
    pub fn prewarm(&mut self, renderer: &mut GlesRenderer, canvas: &Canvas) {
        let scale = canvas.scale();
        let screen = canvas.size();
        if screen.w <= 0 || screen.h <= 0 {
            return;
        }
        let (panel_w, panel_h) = (screen.w, screen.h);
        let scale_key = (scale * 1000.0).round() as i32;
        let panel_radius = 0.0;

        self.try_load_panel_bg(renderer, scale);
        let _ = self.ensure_panel_bg_composed(renderer, scale, panel_w, panel_h, scale_key, panel_radius);

        let cur_panel_key = (panel_w, panel_h, scale_key);
        self.panel_bg_solid.entry(cur_panel_key).or_insert_with(|| {
            Self::make_rounded_texture(
                renderer,
                scale,
                panel_w,
                panel_h,
                PANEL_BG_FALLBACK,
                panel_radius,
            )
        });
        let _ = self.ensure_close_btn(renderer, scale, scale_key);
    }


    /// Hit-test a click in logical coordinates.
    pub(super) fn ensure_label(
        &mut self,
        text: &str,
        font_size: f32,
        color: [u8; 4],
        renderer: &mut GlesRenderer,
        text_renderer: &mut TextRenderer,
        scale: f64,
    ) -> Option<(RenderTexture, i32, i32)> {
        // Key on the physical size actually rasterised, not the logical one:
        // otherwise a scale change serves labels baked for the old scale.
        let px = crate::text::text_px(font_size, scale);
        let key = (text.to_string(), (px * 10.0) as u16, color);
        if let Some(v) = self.label_cache.get(&key) {
            return Some(v.clone());
        }
        let (buf, w, h) = text_renderer.render_line(text, px, color);
        if w == 0 || h == 0 {
            return None;
        }
        let tex = RenderTexture::new(Texture::from_buffer(
            renderer, scale, &buf, w as i32, h as i32, false,
        ));
        let entry = (tex, w as i32, h as i32);
        self.label_cache.insert(key, entry.clone());
        Some(entry)
    }


    /// Solid-colour rectangle texture cached by `(w, h, scale, rgba)`, used
    /// to paint the selected-tile outline. Square corners + per-frame
    /// reuse keeps the cost flat as the user steps through tiles.
    pub(super) fn ensure_solid_rect(
        &mut self,
        renderer: &mut GlesRenderer,
        scale: f64,
        w_log: i32,
        h_log: i32,
        scale_key: i32,
        rgba: [u8; 4],
    ) -> RenderTexture {
        let key = (w_log, h_log, scale_key, rgba);
        if let Some(t) = self.solid_rect_cache.get(&key) {
            return t.clone();
        }
        let t = Self::make_rounded_texture(renderer, scale, w_log, h_log, rgba, 0.0);
        self.solid_rect_cache.insert(key, t.clone());
        t
    }


    /// Rasterise the SSD-style `window_button(Close)` once per scale via a
    /// dedicated ltk `UiSurface`, and cache the resulting GL texture. Sharing
    /// one texture across every tile keeps the close button visually in sync
    /// with the title bar without paying per-frame rasterisation.
    pub(super) fn ensure_close_btn(
        &mut self,
        renderer: &mut GlesRenderer,
        scale: f64,
        scale_key: i32,
    ) -> Option<RenderTexture> {
        if let Some((sk, t)) = &self.close_btn_tex {
            if *sk == scale_key {
                return Some(t.clone());
            }
        }
        let size_px = ((CLOSE_BTN_SIZE as f64) * scale).round().max(1.0) as u32;
        let ui = self
            .close_btn_ui
            .get_or_insert_with(|| ltk::core::UiSurface::new(size_px, size_px));
        if ui.size() != (size_px, size_px) {
            ui.resize(size_px, size_px);
        }
        ui.set_dpi_scale(scale as f32);

        // Wrap the button in a container with the SSD title-bar background
        // colour and a half-side radius so the glyph sits inside a circle
        // that matches the window decoration. `on_press(())` keeps the
        // button in its enabled visual — forge dispatches the actual close
        // via its own click hit-test, so the message is never consumed.
        let bar_bg = ltk::theme::window_controls().bar_bg;
        let tree: ltk::Element<()> = ltk::container(
            ltk::window_button(ltk::WindowButtonKind::Close)
                .size(CLOSE_BTN_SIZE as f32)
                .on_press(()),
        )
        .background(bar_bg)
        .radius(ltk::Corners::all((CLOSE_BTN_SIZE as f32) / 2.0))
        .into();
        let opts = ltk::core::RenderOptions::full_canvas(size_px, size_px)
            .background(ltk::Color::TRANSPARENT);
        let _ = ui.render(&tree, opts);

        let mut rgba = vec![0u8; (size_px as usize) * (size_px as usize) * 4];
        if let ltk::core::Canvas::Software(sc) = ui.canvas() {
            sc.write_to_wayland_buf(&mut rgba, false);
        }
        let tex = Texture::from_buffer(
            renderer,
            scale,
            &rgba,
            size_px as i32,
            size_px as i32,
            false,
        );
        let rt = RenderTexture::new(tex);
        self.close_btn_tex = Some((scale_key, rt.clone()));
        Some(rt)
    }
}

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

/// Cover-scale the decoded lockscreen rgba to the panel size and mask it to the
/// panel's rounded silhouette, returning the composed rgba. Runs on a worker
/// thread (the Lanczos resize is the expensive part); the caller uploads the
/// result to the GPU.
fn compose_panel_bg_rgba(
    rgba: Vec<u8>,
    img_w: u32,
    img_h: u32,
    panel_w_px: u32,
    panel_h_px: u32,
    radius_px: f32,
) -> Vec<u8> {
    let mut canvas = SoftCanvas::new(panel_w_px, panel_h_px);
    canvas.clear();
    // Rounded fallback fill: any transparency in the image falls back to the
    // panel's navy rather than leaving holes in the silhouette.
    let bg = Color::rgba8(
        PANEL_BG_FALLBACK[0],
        PANEL_BG_FALLBACK[1],
        PANEL_BG_FALLBACK[2],
        PANEL_BG_FALLBACK[3],
    );
    canvas.fill_rect(0.0, 0.0, panel_w_px as f32, panel_h_px as f32, bg, radius_px);

    // Cover-scale: image fills both axes preserving aspect; the overflow on the
    // long axis ends up in the corner band the mask wipes anyway.
    let cover = (panel_w_px as f64 / img_w as f64).max(panel_h_px as f64 / img_h as f64);
    let scaled_w = (img_w as f64 * cover).round() as u32;
    let scaled_h = (img_h as f64 * cover).round() as u32;
    if let Some(img_buf) = image::RgbaImage::from_raw(img_w, img_h, rgba) {
        let scaled = image::imageops::resize(
            &img_buf,
            scaled_w.max(1),
            scaled_h.max(1),
            image::imageops::FilterType::Lanczos3,
        );
        let off_x = (panel_w_px as i32 - scaled_w as i32) / 2;
        let off_y = (panel_h_px as i32 - scaled_h as i32) / 2;
        canvas.draw_image_rgba(scaled.as_raw(), scaled_w, scaled_h, off_x, off_y);
    }

    canvas.mask_outside_rounded_rect(radius_px);
    canvas.data().to_vec()
}

fn load_bg_pixels(path: std::path::PathBuf, raster: u32) -> Option<(Vec<u8>, u32, u32)> {
    let is_svg = path
        .extension()
        .and_then(|e| e.to_str())
        .map(|e| e.eq_ignore_ascii_case("svg"))
        .unwrap_or(false);
    let (rgba, w, h) = if is_svg {
        let bytes = match std::fs::read(&path) {
            Ok(b) => b,
            Err(_) => {
                tracing::warn!("app_switcher: failed to read panel bg {}", path.display());
                return None;
            }
        };
        let (rgba_arc, w, h) = match ltk::decode_svg_bytes(&bytes, raster) {
            Some(v) => v,
            None => {
                tracing::warn!("app_switcher: SVG decode failed for {}", path.display());
                return None;
            }
        };
        ((*rgba_arc).clone(), w, h)
    } else {
        let img = match image::open(&path) {
            Ok(i) => i,
            Err(_) => {
                tracing::warn!("app_switcher: image::open failed for {}", path.display());
                return None;
            }
        };
        let rgba = img.into_rgba8();
        let (w, h) = rgba.dimensions();
        (rgba.into_raw(), w, h)
    };

    // Pre-blur the panel background so the switcher reads as a frosted
    // backdrop instead of a sharp wallpaper. Done once on the decode
    // thread; the GPU-side composition stays free of any per-frame blur
    // shader passes.
    let blurred = match image::RgbaImage::from_raw(w, h, rgba) {
        Some(buf) => image::imageops::blur(&buf, 18.0),
        None => return None,
    };
    Some((blurred.into_raw(), w, h))
}


