//! The console shell's per-frame screen compose/transition render path.

use crate::anim::{approach, ease_out_cubic};
use crate::glyphs::{hint_bar, GlyphStyle};
use crate::library::LibraryShared;
use crate::model::HostRow;
use crate::screens::{Bg, Ctx, Screen};
use crate::theme::{white, Fonts, PanelStroke, W, WHITE};
use pf_client_core::gamepad::PadInfo;
use pf_client_core::trust;
use skia_safe::{Canvas, Color4f, Rect};
use std::time::Instant;

use super::{Motion, Shell, BOTTOM_BAND, TOP_BAND};

impl Shell {
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn render(
        &mut self,
        canvas: &Canvas,
        width: u32,
        height: u32,
        fonts: &Fonts,
        pad: Option<&str>,
        pad_pref: Option<punktfunk_core::config::GamepadPref>,
        pads: &[PadInfo],
    ) {
        let now = Instant::now();
        let dt = self
            .last_frame
            .replace(now)
            .map_or(1.0 / 60.0, |t| (now - t).as_secs_f64().clamp(0.0, 0.05));
        self.sync();
        self.pads = pads.to_vec();
        self.glyphs = GlyphStyle::from_pref(pad_pref);
        self.chip = Some(pad.map_or_else(
            || "No controller — keyboard works too".to_string(),
            str::to_owned,
        ));

        let (w, h) = (f64::from(width), f64::from(height));
        let k = (h / 800.0).clamp(0.75, 3.0);
        let t = self.t();

        // Advance the transition; a finished pop finally drops its leaving screen.
        let motion_p = match &mut self.motion {
            Motion::None => None,
            Motion::Push(p) => {
                p.advance(dt);
                let v = p.value();
                if p.done() {
                    self.motion = Motion::None;
                    None
                } else {
                    Some(v)
                }
            }
            Motion::Pop { t, .. } => {
                t.advance(dt);
                let v = t.value();
                if t.done() {
                    self.motion = Motion::None;
                    None
                } else {
                    Some(v)
                }
            }
        };

        // Backdrop crossfade follows the top screen.
        let bg_target = match self.stack.last().expect("non-empty").background() {
            Bg::Aurora => 0.0,
            Bg::Form => 1.0,
        };
        self.bg_mix = approach(self.bg_mix, bg_target, dt, 0.12);
        if (self.bg_mix - bg_target).abs() < 0.005 {
            self.bg_mix = bg_target;
        }
        if self.bg_mix < 1.0 {
            self.draw_aurora(canvas, w, h, t);
        } else {
            canvas.clear(Color4f::new(0.0, 0.0, 0.0, 1.0));
        }
        if self.bg_mix > 0.0 {
            canvas.save_layer_alpha_f(None, self.bg_mix as f32);
            crate::theme::draw_form_background(canvas, w, h);
            canvas.restore();
        }

        // The screens, through the transition choreography.
        let content = Rect::from_ltrb(
            0.0,
            (TOP_BAND * k) as f32,
            w as f32,
            (h - BOTTOM_BAND * k) as f32,
        );
        // One paint recipe per layer: (alpha, slide, scale). Everything below borrows
        // disjoint fields of `self` per call, so the borrow checker stays happy.
        let mut env = LayerEnv {
            canvas,
            w,
            h,
            content,
            k,
            dt,
            fonts,
            hosts: &self.hosts,
            library: &self.library,
            settings: &mut self.settings,
            pads: &self.pads,
            deck: self.deck,
            device_name: &self.device_name,
            t,
            glyphs: self.glyphs,
            // A modal card owns B/A while it's up — the screen's legend would lie.
            show_hints: self.connecting.is_none() && self.wake.is_none(),
        };
        match (&mut self.motion, motion_p) {
            (Motion::Push(_), Some(raw)) => {
                let p = ease_out_cubic(raw);
                let n = self.stack.len();
                // Outgoing recedes underneath…
                if n >= 2 {
                    let (below, top) = self.stack.split_at_mut(n - 1);
                    env.paint(&mut below[n - 2], 1.0 - p, 0.0, 1.0 - 0.04 * p);
                    // …while the incoming slides up out of a fade.
                    env.paint(&mut top[0], p, 36.0 * k * (1.0 - p), 0.985 + 0.015 * p);
                } else {
                    env.paint(
                        &mut self.stack[0],
                        p,
                        36.0 * k * (1.0 - p),
                        0.985 + 0.015 * p,
                    );
                }
            }
            (Motion::Pop { leaving, .. }, Some(raw)) => {
                let p = ease_out_cubic(raw);
                // The revealed screen grows back in…
                let n = self.stack.len();
                env.paint(&mut self.stack[n - 1], 0.4 + 0.6 * p, 0.0, 0.96 + 0.04 * p);
                // …while the leaving one slides down into a fade.
                env.paint(leaving.as_mut(), 1.0 - p, 36.0 * k * p, 1.0);
            }
            _ => {
                let n = self.stack.len();
                env.paint(&mut self.stack[n - 1], 1.0, 0.0, 1.0);
            }
        }

        // Persistent chrome: the controller chip (top-right, above every layer).
        if let Some(chip) = &self.chip {
            let size = 12.0 * k;
            let tw = f64::from(fonts.measure(chip, W::Medium, size));
            let (bh, pad_x) = (24.0 * k, 12.0 * k);
            let bx = w - 24.0 * k - tw - 2.0 * pad_x;
            let rect = Rect::from_xywh(
                bx as f32,
                (18.0 * k) as f32,
                (tw + 2.0 * pad_x) as f32,
                bh as f32,
            );
            crate::theme::panel(
                canvas,
                rect,
                (bh / 2.0 / k) as f32,
                None,
                PanelStroke::Plain(0.12),
                k as f32,
            );
            fonts.draw(
                canvas,
                chip,
                bx + pad_x,
                18.0 * k + 16.0 * k,
                W::Medium,
                size,
                white(0.7),
            );
        }

        self.draw_overlays(canvas, w, h, k, dt, t, fonts);
    }
}

/// Everything one screen layer needs to paint — bundled so the transition arms stay
/// readable and each `paint` call borrows `Shell` fields disjointly.
struct LayerEnv<'a> {
    canvas: &'a Canvas,
    w: f64,
    h: f64,
    content: Rect,
    k: f64,
    dt: f64,
    fonts: &'a Fonts,
    hosts: &'a [HostRow],
    library: &'a LibraryShared,
    settings: &'a mut trust::Settings,
    pads: &'a [PadInfo],
    deck: bool,
    device_name: &'a str,
    t: f64,
    glyphs: GlyphStyle,
    show_hints: bool,
}

impl LayerEnv<'_> {
    /// One screen composited as a unit: `alpha` fade, `dy` vertical slide, `scale`
    /// about the screen center — its pinned title and hint bar ride inside the layer,
    /// so chrome travels with content through a transition.
    fn paint(&mut self, screen: &mut Screen, alpha: f64, dy: f64, scale: f64) {
        let canvas = self.canvas;
        canvas.save_layer_alpha_f(None, alpha.clamp(0.0, 1.0) as f32);
        canvas.translate((0.0, dy as f32));
        let (cx, cy) = ((self.w / 2.0) as f32, (self.h / 2.0) as f32);
        canvas.translate((cx, cy));
        canvas.scale((scale as f32, scale as f32));
        canvas.translate((-cx, -cy));

        let mut ctx = Ctx {
            hosts: self.hosts,
            library: self.library,
            settings: self.settings,
            pads: self.pads,
            deck: self.deck,
            device_name: self.device_name,
            t: self.t,
        };
        self.fonts.centered(
            canvas,
            &screen.title(&ctx),
            W::Bold,
            30.0 * self.k,
            WHITE,
            self.w / 2.0,
            18.0 * self.k,
            self.w * 0.7,
        );
        screen.render(canvas, self.content, self.k, self.dt, self.fonts, &mut ctx);
        if self.show_hints {
            let hints = screen.hints(&ctx);
            hint_bar(
                canvas,
                self.fonts,
                &hints,
                self.glyphs,
                18.0 * self.k,
                self.h - 18.0 * self.k,
                self.k,
            );
        }
        canvas.restore();
    }
}
