//! The switcher's element passes: the per-screen panel projection and the
//! tile composition.

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

use crate::drawing::ForgeElement;
use crate::output::Canvas;
use crate::text::TextRenderer;

use super::*;
use super::assets::ltk_color_to_rgba8;

impl AppSwitcher {
    /// Advance the fade / swipe / reposition clocks — once per frame, ahead
    /// of every screen's panel projection — and return the panel alpha +
    /// grow scale, or `None` when there is nothing to draw.
    pub fn render(
        &mut self,
        textures: &mut Vec<ForgeElement>,
        canvas: &Canvas,
        renderer: &mut GlesRenderer,
        text_renderer: &mut TextRenderer,
    ) {
        let Some(clock) = self.tick_frame() else { return };
        self.frame_clock = clock;
        self.render_panel(
            textures,
            canvas,
            renderer,
            text_renderer,
            None,
            smithay::utils::Point::default(),
            clock,
        );
    }

    /// Project the panel onto an extend screen: that screen's own entries,
    /// laid out for its canvas, hit-rects translated by `origin` back into
    /// the global space so the input paths work unchanged.
    pub fn render_extra(
        &mut self,
        textures: &mut Vec<ForgeElement>,
        canvas: &Canvas,
        renderer: &mut GlesRenderer,
        text_renderer: &mut TextRenderer,
        screen: &str,
        origin: smithay::utils::Point<i32, smithay::utils::Logical>,
    ) {
        if !self.visible
            || self.entries.is_empty()
            || self.tile_rects.len() != self.entries.len()
        {
            return;
        }
        let clock = self.frame_clock;
        self.render_panel(textures, canvas, renderer, text_renderer, Some(screen), origin, clock);
    }

    fn render_panel(
        &mut self,
        textures: &mut Vec<ForgeElement>,
        canvas: &Canvas,
        renderer: &mut GlesRenderer,
        text_renderer: &mut TextRenderer,
        panel_screen: Option<&str>,
        origin: smithay::utils::Point<i32, smithay::utils::Logical>,
        (panel_alpha, panel_scale): (f32, f64),
    ) {
        let insert_at = textures.len();

        self.try_load_panel_bg(renderer, canvas.scale());

        let scale = canvas.scale();
        let screen = canvas.size();
        let is_mobile = canvas.is_mobile();
        let avail = canvas.available();

        let (panel_x, panel_y, panel_w, panel_h) = (0, 0, screen.w, screen.h);

        let panel_log =
            Rectangle::<i32, Logical>::new((panel_x, panel_y).into(), (panel_w, panel_h).into());
        let panel_phys = panel_log.to_physical_precise_round(scale);
        let panel_center = smithay::utils::Point::<i32, smithay::utils::Physical>::from((
            panel_phys.loc.x + panel_phys.size.w / 2,
            panel_phys.loc.y + panel_phys.size.h / 2,
        ));
        let scale_key = (scale * 1000.0).round() as i32;
        let panel_radius = 0.0;

        // Panel background: solid rounded fill as always-on backdrop, plus the
        // theme's lockscreen image overlaid on top once decoded.
        let cur_panel_key = (panel_w, panel_h, scale_key);
        let bg = 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,
                )
            })
            .clone();
        ForgeElement::add_element_panel_composited(
            textures,
            bg,
            panel_phys.loc,
            panel_phys,
            None,
            scale,
            panel_alpha,
            panel_scale,
            panel_center,
        );
        // Composed lockscreen-image backdrop, per panel geometry — every
        // screen shows the same themed image, composed once per size from
        // the single decoded master.
        if let Some(bg_img) = self.ensure_panel_bg_composed(
            renderer,
            scale,
            panel_w,
            panel_h,
            scale_key,
            panel_radius,
        ) {
            ForgeElement::add_element_panel_composited(
                textures,
                bg_img,
                panel_phys.loc,
                panel_phys,
                None,
                scale,
                panel_alpha,
                panel_scale,
                panel_center,
            );
        }

        // This screen's own entries, by global index. A tile in flight is
        // left out of the grid and drawn under the pointer instead, so it
        // reads as travelling rather than duplicated.
        let dragging = self.tile_drag.filter(|d| d.moved);
        let panel_rect = Rectangle::<i32, Logical>::new(origin, screen);
        let subset: Vec<usize> = self
            .entries
            .iter()
            .enumerate()
            .filter(|(i, e)| {
                e.output.as_deref() == panel_screen
                    && dragging.map_or(true, |d| d.entry != *i)
            })
            .map(|(i, _)| i)
            .collect();
        let n = subset.len();
        let layout_ease = self.frame_ease;
        let content_rect = if is_mobile {
            let pad = ((panel_w.min(panel_h)) as f32 * PANEL_PAD_FRAC).max(8.0);
            ltk::Rect {
                x: panel_x as f32 + pad,
                y: panel_y as f32 + pad,
                width: (panel_w as f32 - 2.0 * pad).max(1.0),
                height: (panel_h as f32 - 2.0 * pad).max(1.0),
            }
        } else {
            let h_margin = (screen.w as f32 * 0.01) as i32;
            let v_margin = (screen.h as f32 * 0.01) as i32;
            let cx = avail.loc.x + h_margin;
            let cy = avail.loc.y + v_margin;
            let cw = (avail.size.w - 2 * h_margin).max(120);
            // The dock strip only exists on the primary.
            let dock = if panel_screen.is_none() { self.dock_height } else { 0 };
            let bottom = screen.h - dock - v_margin;
            let ch = (bottom - cy).max(120);
            ltk::Rect {
                x: cx as f32,
                y: cy as f32,
                width: cw as f32,
                height: ch as f32,
            }
        };
        // The LTK layout helpers ask the canvas about font metrics for text
        // children; with spacer placeholders the canvas is unused, so a
        // throwaway 1×1 one is enough.
        let ltk_canvas = ltk::core::Canvas::new(1, 1);

        let label_band = (FONT_SIZE * 1.6) as f32;
        let cells: Vec<ltk::Rect> = if is_mobile && n > 1 {
            // Carousel: clamp + ease the offset, then ask LTK for the rects.
            let stride = content_rect.width * CAROUSEL_FOCUSED_FRAC + CAROUSEL_GAP;
            let min_off = -(((n - 1) as f32) * stride);
            self.carousel_offset = self.carousel_offset.clamp(min_off, 0.0);
            // While not dragging and no inertia in flight, ease toward the
            // snap target so a keyboard step / fresh open animates rather
            // than jumps. If we just settled out of inertia, adopt the
            // nearest tile as the new selection first — without this the
            // carousel snaps back to whatever was selected before the drag.
            if self.carousel_drag.is_none() && self.carousel_velocity.abs() < f32::EPSILON {
                if self.carousel_settling {
                    let focused = (-self.carousel_offset / stride)
                        .round()
                        .clamp(0.0, (n - 1) as f32) as usize;
                    self.selected = focused;
                    self.carousel_settling = false;
                }
                let target = -(self.selected as f32) * stride;
                let delta = target - self.carousel_offset;
                if delta.abs() < 0.5 {
                    self.carousel_offset = target;
                    self.carousel_snap = None;
                } else {
                    self.carousel_offset += delta * CAROUSEL_SNAP_EASE;
                    // Keep `animating()` truthy while the ease is in
                    // flight — otherwise the next frame depends on an
                    // unrelated event and the motion stutters.
                    self.carousel_snap = Some(target);
                }
            }
            let mut car = ltk::carousel::<()>()
                .focused_width_frac(CAROUSEL_FOCUSED_FRAC)
                .gap(CAROUSEL_GAP)
                .offset(self.carousel_offset);
            for _ in 0..n {
                car = car.push(ltk::spacer());
            }
            car.layout(content_rect, &ltk_canvas)
                .into_iter()
                .map(|(r, _)| r)
                .collect()
        } else if is_mobile {
            // n == 1: size it like the carousel's focused tile (centred) so a
            // single window looks the same as one among several rather than
            // ballooning to fill the screen.
            let w = content_rect.width * CAROUSEL_FOCUSED_FRAC;
            let x = content_rect.x + (content_rect.width - w) / 2.0;
            vec![ltk::Rect {
                x,
                y: content_rect.y,
                width: w,
                height: content_rect.height,
            }]
        } else {
            // Desktop grid, last partial row centred. WrapGrid sizes rows
            // from children's natural heights, which is zero for placeholder
            // spacers, so we compute cells manually. The gap follows the
            // logical view (capped at TILE_GAP) so a high output scale does
            // not eat the small view with fixed spacing, and per_row is the
            // split that maximises thumbnail size for the entries' aspect —
            // ceil(sqrt(n)) ignored the aspect and left most of each cell as
            // dead letterbox space on small, wide views.
            let gap = TILE_GAP.min(content_rect.width.min(content_rect.height) * 0.02);
            let aspect = {
                let mut sum = 0.0f32;
                let mut cnt = 0u32;
                for &i in &subset {
                    if let Some(s) = self.entries[i].thumb_size {
                        if s.w > 0 && s.h > 0 {
                            sum += s.w as f32 / s.h as f32;
                            cnt += 1;
                        }
                    }
                }
                if cnt > 0 { sum / cnt as f32 } else { 16.0 / 9.0 }
            };
            let mut per_row = 1usize;
            let mut best_fit = 0.0f32;
            for r in 1..=n {
                let rows = (n + r - 1) / r;
                let cw = (content_rect.width - (r - 1) as f32 * gap) / r as f32;
                let ch = (content_rect.height - (rows - 1) as f32 * gap) / rows as f32
                    - label_band;
                if cw <= 0.0 || ch <= 0.0 {
                    continue;
                }
                let fit_h = ch.min(cw / aspect);
                if fit_h > best_fit {
                    best_fit = fit_h;
                    per_row = r;
                }
            }
            let n_rows = ((n + per_row - 1) / per_row).max(1);
            let cell_w = (content_rect.width - (per_row - 1) as f32 * gap) / per_row as f32;
            let cell_h = (content_rect.height - (n_rows - 1) as f32 * gap) / n_rows as f32;
            let mut cells = Vec::with_capacity(n);
            for i in 0..n {
                let row = i / per_row;
                let col_in_row = i % per_row;
                let items_in_row = if row == n_rows - 1 {
                    n - row * per_row
                } else {
                    per_row
                };
                let row_offset = if items_in_row < per_row {
                    (per_row - items_in_row) as f32 * (cell_w + gap) / 2.0
                } else {
                    0.0
                };
                let x = content_rect.x + row_offset + col_in_row as f32 * (cell_w + gap);
                let y = content_rect.y + row as f32 * (cell_h + gap);
                cells.push(ltk::Rect {
                    x,
                    y,
                    width: cell_w,
                    height: cell_h,
                });
            }
            cells
        };

        // Snapshot per-entry view data so the per-tile loop doesn't have to
        // borrow `self.entries` while it mutates label/icon/tile-bg caches.
        let entries_snapshot: Vec<(
            usize,
            EntryKey,
            String,
            String,
            Option<RenderTexture>,
            Option<smithay::utils::Size<i32, Logical>>,
        )> = subset
            .iter()
            .map(|&i| {
                let e = &self.entries[i];
                let label = if e.title.is_empty() {
                    e.app_id.clone()
                } else {
                    e.title.clone()
                };
                (
                    i,
                    (e.object_id.clone(), e.surface_id),
                    e.app_id.clone(),
                    label,
                    e.thumbnail.clone(),
                    e.thumb_size,
                )
            })
            .collect();

        let panel_left = panel_x as f32;
        let panel_right = (panel_x + panel_w) as f32;
        let close_drag = self.close_drag;
        let mut new_last_cells =
            std::collections::HashMap::with_capacity(n);
        for (mut cell, snap) in cells.into_iter().zip(entries_snapshot.into_iter()) {
            let (i, key, app_id, label, thumbnail, thumb_size) = snap;
            let mut tile_alpha = panel_alpha;
            // Reposition in flight: surviving tiles glide from their last
            // drawn rect toward the new layout; entries with no previous
            // rect (new windows) fade + grow in place instead.
            if let Some(ease) = layout_ease {
                match self.layout_anim_from.get(&key).copied() {
                    Some(old) => {
                        cell.x = old.x + (cell.x - old.x) * ease;
                        cell.y = old.y + (cell.y - old.y) * ease;
                        cell.width = old.width + (cell.width - old.width) * ease;
                        cell.height = old.height + (cell.height - old.height) * ease;
                    }
                    None => {
                        tile_alpha *= ease;
                        let grow = 0.85 + 0.15 * ease;
                        let (gw, gh) = (cell.width * grow, cell.height * grow);
                        cell.x += (cell.width - gw) / 2.0;
                        cell.y += (cell.height - gh) / 2.0;
                        cell.width = gw;
                        cell.height = gh;
                    }
                }
            }
            new_last_cells.insert(key, cell);
            // Mobile carousel skips tiles whose cell does not intersect the
            // panel viewport — clipping happens at composition, but we'd
            // still pay the rasterisation cost without this guard.
            if is_mobile && (cell.x + cell.width <= panel_left || cell.x >= panel_right) {
                self.tile_rects[i] = Rectangle::default();
                self.close_rects[i] = Rectangle::default();
                continue;
            }
            // A vertical close-drag lifts the grabbed tile with the finger
            // and fades it toward the commit point so it reads as peeling
            // off the deck before it closes.
            if let Some((idx, dy)) = close_drag {
                if idx == i {
                    cell.y += dy;
                    let f = (-dy / CAROUSEL_CLOSE_COMMIT_PX).clamp(0.0, 1.0);
                    tile_alpha *= 1.0 - 0.6 * f;
                }
            }
            self.render_tile(
                textures,
                renderer,
                text_renderer,
                cell,
                i,
                &app_id,
                &label,
                thumbnail,
                thumb_size,
                scale,
                scale_key,
                is_mobile,
                label_band,
                tile_alpha,
                panel_scale,
                panel_center,
                origin,
            );
        }
        self.last_cells = new_last_cells;

        // The tile in flight, centred on the pointer, painted by the panel of
        // the screen the pointer is over — so it crosses from one screen's
        // scene into the other as the cursor does.
        if let Some(drag) = dragging {
            if panel_rect.to_f64().contains(drag.pos) {
                let local = drag.pos - origin.to_f64();
                let cell = ltk::Rect {
                    x: local.x as f32 - drag.size.w as f32 / 2.0,
                    y: local.y as f32 - drag.size.h as f32 / 2.0,
                    width: drag.size.w as f32,
                    height: drag.size.h as f32,
                };
                let e = &self.entries[drag.entry];
                let label = if e.title.is_empty() { e.app_id.clone() } else { e.title.clone() };
                let (app_id, thumbnail, thumb_size) =
                    (e.app_id.clone(), e.thumbnail.clone(), e.thumb_size);
                self.render_tile(
                    textures, renderer, text_renderer, cell, drag.entry, &app_id, &label,
                    thumbnail, thumb_size, scale, scale_key, is_mobile, label_band,
                    panel_alpha * 0.85, panel_scale, panel_center, origin,
                );
            }
        }

        textures[insert_at..].reverse();
    }

    fn render_tile(
        &mut self,
        textures: &mut Vec<ForgeElement>,
        renderer: &mut GlesRenderer,
        text_renderer: &mut TextRenderer,
        cell: ltk::Rect,
        entry_idx: usize,
        _app_id: &str,
        label: &str,
        thumbnail: Option<RenderTexture>,
        thumb_size: Option<smithay::utils::Size<i32, smithay::utils::Logical>>,
        scale: f64,
        scale_key: i32,
        is_mobile: bool,
        label_band: f32,
        panel_alpha: f32,
        panel_scale: f64,
        panel_center: smithay::utils::Point<i32, smithay::utils::Physical>,
        origin: smithay::utils::Point<i32, Logical>,
    ) {
        // Reserve the bottom label_band for the title; the remaining area is
        // the tile (thumbnail + bg + close button). Mobile carousels skip the
        // label entirely (the tile already fills the visible area).
        let band = if is_mobile { 0.0 } else { label_band };
        let tile_w_log = cell.width.floor().max(1.0) as i32;
        let tile_h_log = (cell.height - band).floor().max(1.0) as i32;
        let tile_x_log = cell.x.round() as i32;
        let tile_y_log = cell.y.round() as i32;
        let tile_log = Rectangle::<i32, Logical>::new(
            (tile_x_log, tile_y_log).into(),
            (tile_w_log, tile_h_log).into(),
        );
        // Hit slots live in global coordinates: the drawn cell is local to
        // this panel's screen, `origin` is that screen's corner.
        self.tile_rects[entry_idx] = Rectangle::new(tile_log.loc + origin, tile_log.size);

        let selected = entry_idx == self.selected;

        // Compute the thumbnail fit rect up front so the outline (selected
        // only) and the close button can anchor to the visible window
        // content rather than the cell. Falls back to the full tile rect
        // when no thumbnail is available (eg. app-icon fallback).
        let inset = 4;
        let avail_w = (tile_w_log - inset * 2).max(1) as f64;
        let avail_h = (tile_h_log - inset * 2).max(1) as f64;
        let fit_log = match thumb_size {
            Some(ws) if ws.w > 0 && ws.h > 0 => {
                let win_aspect = ws.w as f64 / ws.h as f64;
                let (fit_w, fit_h) = if avail_w / avail_h > win_aspect {
                    ((avail_h * win_aspect) as i32, avail_h as i32)
                } else {
                    (avail_w as i32, (avail_w / win_aspect) as i32)
                };
                let fit_x = tile_x_log + (tile_w_log - fit_w) / 2;
                let fit_y = tile_y_log + (tile_h_log - fit_h) / 2;
                Rectangle::<i32, Logical>::new((fit_x, fit_y).into(), (fit_w, fit_h).into())
            }
            _ => tile_log,
        };

        // Selected outline: 3-pixel ring just outside `fit_log` — the
        // outer 2 px are `palette.text_primary`, the inner 1 px is
        // `palette.surface`. Two solid rects stack, the thumbnail / icon
        // covers the centre, and only the 2+1 px perimeter remains visible.
        if selected {
            let palette = ltk::theme_palette();
            let outer = ltk_color_to_rgba8(palette.text_primary);
            let inner = ltk_color_to_rgba8(palette.surface);
            for (offset, color) in [(3, outer), (1, inner)] {
                let w = fit_log.size.w + 2 * offset;
                let h = fit_log.size.h + 2 * offset;
                let r = Rectangle::<i32, Logical>::new(
                    (fit_log.loc.x - offset, fit_log.loc.y - offset).into(),
                    (w, h).into(),
                );
                let phys = r.to_physical_precise_round(scale);
                let tex = self.ensure_solid_rect(renderer, scale, w, h, scale_key, color);
                ForgeElement::add_element_panel_composited(
                    textures,
                    tex,
                    phys.loc,
                    phys,
                    None,
                    scale,
                    panel_alpha,
                    panel_scale,
                    panel_center,
                );
            }
        }

        // Thumbnail (live window texture) preserving aspect.
        let fit_phys = fit_log.to_physical_precise_round(scale);
        let drew_thumbnail = if let Some(tex) = thumbnail.clone() {
            // Scale the texture's *actual* physical geometry to fill the fit
            // rect. Calibrating off the texture geometry (not the logical
            // `thumb_size`) keeps fractionally-scaled clients — whose buffer
            // carries its own `window_scale`, so `geometry()` ≠ size × output
            // scale — from over- or under-shooting the crop, which painted
            // Firefox as a black rectangle.
            let tex_geom = tex.geometry(smithay::utils::Scale::from(scale));
            if tex_geom.size.w > 0 && tex_geom.size.h > 0 {
                let win_scale = fit_phys.size.w as f64 / tex_geom.size.w as f64;
                // The texture carries its own physical location (CSD shadow
                // inset, etc.). RescaleRenderElement scales that location by
                // `win_scale` around (0, 0); subtract the contribution so the
                // texture's natural top-left lands at `fit_phys.loc`.
                let relocate = smithay::utils::Point::<i32, smithay::utils::Physical>::from((
                    fit_phys.loc.x - (tex_geom.loc.x as f64 * win_scale).round() as i32,
                    fit_phys.loc.y - (tex_geom.loc.y as f64 * win_scale).round() as i32,
                ));
                ForgeElement::add_element_panel_composited(
                    textures,
                    tex,
                    relocate,
                    fit_phys,
                    Some(win_scale),
                    scale,
                    panel_alpha,
                    panel_scale,
                    panel_center,
                );
                true
            } else {
                false
            }
        } else {
            false
        };

        if !drew_thumbnail {
            // No live thumbnail yet — fill the tile with a `palette.surface`
            // rectangle and centre the window title (or app_id) on it.
            // Replaces the previous "search the icon cache by app_id"
            // fallback, which never matched apps like Signal whose icon
            // ships under a different name (e.g. "signal-desktop").
            let palette = ltk::theme_palette();
            let surface = ltk_color_to_rgba8(palette.surface);
            let text_c = ltk_color_to_rgba8(palette.text_primary);
            let bg_tex = self.ensure_solid_rect(
                renderer,
                scale,
                fit_log.size.w,
                fit_log.size.h,
                scale_key,
                surface,
            );
            ForgeElement::add_element_panel_composited(
                textures,
                bg_tex,
                fit_phys.loc,
                fit_phys,
                None,
                scale,
                panel_alpha,
                panel_scale,
                panel_center,
            );

            let font_size = (fit_log.size.h as f32 * 0.08).clamp(14.0, 28.0);
            if let Some((tex, tw, th)) =
                self.ensure_label(label, font_size, text_c, renderer, text_renderer, scale)
            {
                let cx_log = fit_log.loc.x + fit_log.size.w / 2;
                let cy_log = fit_log.loc.y + fit_log.size.h / 2;
                let pos_x_phys = (cx_log as f64 * scale) as i32 - tw / 2;
                let pos_y_phys = (cy_log as f64 * scale) as i32 - th / 2;
                let pos = Point::<i32, Physical>::from((pos_x_phys, pos_y_phys));
                let bounds = Rectangle::<i32, Physical>::new(pos, (tw, th).into());
                ForgeElement::add_element_panel_composited(
                    textures,
                    tex,
                    pos,
                    bounds,
                    None,
                    scale,
                    panel_alpha,
                    panel_scale,
                    panel_center,
                );
            }
        }

        // Close button at the top-right of the visible window thumbnail,
        // 5 px in from the top / right edges.
        let close_log = Rectangle::<i32, Logical>::new(
            (
                fit_log.loc.x + fit_log.size.w - CLOSE_BTN_SIZE - CLOSE_BTN_PAD,
                fit_log.loc.y + CLOSE_BTN_PAD,
            )
                .into(),
            (CLOSE_BTN_SIZE, CLOSE_BTN_SIZE).into(),
        );
        if let Some(tex) = self.ensure_close_btn(renderer, scale, scale_key) {
            let close_phys = close_log.to_physical_precise_round(scale);
            ForgeElement::add_element_panel_composited(
                textures,
                tex,
                close_phys.loc,
                close_phys,
                None,
                scale,
                panel_alpha,
                panel_scale,
                panel_center,
            );
        }
        self.close_rects[entry_idx] = Rectangle::new(close_log.loc + origin, close_log.size);

        // Title label below the tile (desktop only — mobile carousel skips it).
        if !is_mobile {
            let text_c = ltk_color_to_rgba8(ltk::theme_palette().text_primary);
            if let Some((tex, tw, th)) =
                self.ensure_label(label, FONT_SIZE, text_c, renderer, text_renderer, scale)
            {
                let label_x_phys =
                    ((tile_x_log as f64 + (tile_w_log as f64 / 2.0)) * scale) as i32 - tw / 2;
                let label_y_phys =
                    ((fit_log.loc.y + fit_log.size.h + TEXT_OFFSET) as f64 * scale) as i32;
                let pos = Point::<i32, Physical>::from((label_x_phys, label_y_phys));
                let bounds = Rectangle::<i32, Physical>::new(pos, (tw, th).into());
                ForgeElement::add_element_panel_composited(
                    textures,
                    tex,
                    pos,
                    bounds,
                    None,
                    scale,
                    panel_alpha,
                    panel_scale,
                    panel_center,
                );
            }
        }
    }

}
