//! Shared chrome assets: themed cursors, solid fills and the gesture handle.

use super::*;

const GESTURE_HANDLE_DEFAULT_RGB: [u8; 3] = [255; 3];

const GESTURE_NOTCH_RATIO: f64 = 0.2;

#[derive(Debug)]
pub struct Graphics {
    gesture_handle_default: Option<RenderTexture>,

    /// XCursor theme loaded from the active ltk theme; the only source of
    /// pointer imagery.
    cursor_theme: crate::cursor::CursorTheme,
    /// Cached themed cursor textures keyed by shape and quantised scale.
    themed_cursors: HashMap<(CursorIcon, i32), CachedCursor>,
    /// Start of the current shape's animation, reset whenever the key changes.
    cursor_anim: Option<((CursorIcon, i32), Instant)>,
    cursor_animating: bool,

    /// Solid backdrop drawn under every `View::Lock(_)` frame. The
    /// `ext-session-lock-v1` spec requires the compositor to keep the
    /// session locked even after the lock client dies and to render a
    /// solid fallback in the meantime so the frozen desktop never
    /// leaks through. Cached as an opaque black 1×1 texture; the GLES
    /// blitter scales it across the full output.
    pub lock_fallback: RenderTexture,

    /// Translucent accent-coloured rectangle used as the visual
    /// preview while a window is being drag-snapped to a screen
    /// edge or corner. Rendered on top of every other surface so the
    /// user sees the target zone before they release. Premultiplied
    /// alpha at upload time; must NOT declare opaque regions or the
    /// damage tracker culls everything underneath it.
    pub snap_preview: RenderTexture,

    /// Active-corner glow: one quadrant of a disc in the accent colour, alpha
    /// fading from the vertex outwards, rasterised once per corner.
    corner_glows: HashMap<crate::corner_action::Corner, RenderTexture>,
}

impl Graphics {
    pub fn new() -> Self {
        Self {
            gesture_handle_default: None,
            cursor_theme: crate::cursor::CursorTheme::load(),
            themed_cursors: HashMap::new(),
            cursor_anim: None,
            cursor_animating: false,
            lock_fallback: RenderTexture::new(Texture::from_color([0, 0, 0, 255], false)),
            // Eydolon teal accent (#00CEB1) at ~35 % alpha, premultiplied:
            //   r' = 0x00 * 0x59 / 0xff ≈ 0
            //   g' = 0xCE * 0x59 / 0xff ≈ 0x47
            //   b' = 0xB1 * 0x59 / 0xff ≈ 0x3D
            //   a  = 0x59 (≈ 35 %)
            snap_preview: RenderTexture::new(Texture::from_color([0x00, 0x47, 0x3D, 0x59], false)),
            corner_glows: HashMap::new(),
        }
    }

    /// Side, in logical pixels at scale 1, of the [`Self::corner_glow`]
    /// texture; callers rescale it to the wanted radius.
    pub const CORNER_GLOW_TEX: usize = 128;

    pub fn corner_glow(
        &mut self,
        renderer: &mut GlesRenderer,
        corner: crate::corner_action::Corner,
    ) -> RenderTexture {
        use crate::corner_action::Corner;
        if let Some(t) = self.corner_glows.get(&corner) {
            return t.clone();
        }
        const N: usize = Graphics::CORNER_GLOW_TEX;
        let n = N as f64;
        let (vx, vy) = match corner {
            Corner::TopLeft => (0.0, 0.0),
            Corner::TopRight => (n, 0.0),
            Corner::BottomLeft => (0.0, n),
            Corner::BottomRight => (n, n),
        };
        let mut buf = vec![0u8; N * N * 4];
        for y in 0..N {
            for x in 0..N {
                let px = x as f64 + 0.5;
                let py = y as f64 + 0.5;
                let r = ((px - vx).powi(2) + (py - vy).powi(2)).sqrt() / n;
                // Logarithmic falloff: near-solid over most of the radius,
                // dropping to zero only close to the rim.
                let fade = if r >= 1.0 { 0.0 } else { (1.0 + 9.0 * (1.0 - r)).ln() / 10f64.ln() };
                // Same teal as `snap_preview`, at a 60 % peak, premultiplied.
                let a = 0x99 as f64 * fade;
                let i = (y * N + x) * 4;
                buf[i] = 0;
                buf[i + 1] = (0xCE as f64 * a / 255.0).round() as u8;
                buf[i + 2] = (0xB1 as f64 * a / 255.0).round() as u8;
                buf[i + 3] = a.round() as u8;
            }
        }
        let tex = Texture::from_buffer(renderer, 1.0, &buf, N as i32, N as i32, false);
        let rt = RenderTexture::new(tex);
        self.corner_glows.insert(corner, rt.clone());
        rt
    }

    pub fn gesture_handle(
        &mut self,
        renderer: &mut GlesRenderer,
        canvas: &Canvas,
    ) -> RenderTexture {
        let slot = &mut self.gesture_handle_default;
        let color = GESTURE_HANDLE_DEFAULT_RGB;

        let scale = canvas.scale();
        let width = canvas.physical_size().w;
        let height = (GESTURE_HANDLE_HEIGHT as f64 * scale).round() as i32;
        let expected_size: Size<i32, Buffer> = (width, height).into();

        if slot
            .as_ref()
            .is_none_or(|h| h.buffer_size() != expected_size)
        {
            let buffer = generate_gesture_handle_buffer(width, height, &color);
            let texture = Texture::from_buffer(renderer, scale, &buffer, width, height, true);
            *slot = Some(RenderTexture::new(texture));
        }

        slot.as_ref()
            .expect("gesture handle texture just initialised")
            .clone()
    }

    /// Re-resolve the cursor theme when the active ltk theme changes, dropping
    /// the textures derived from the old one.
    pub fn tick_cursor_theme(&mut self) {
        if self.cursor_theme.tick() {
            self.themed_cursors.clear();
            self.cursor_anim = None;
        }
    }

    /// Whether the shape drawn by the last `cursor_for` has more than one frame,
    /// so the caller keeps scheduling repaints.
    pub fn cursor_animating(&self) -> bool {
        self.cursor_animating
    }

    /// Returns the texture and hotspot (in physical pixels) for `icon`, taken
    /// from the loaded XCursor theme. `None` when the theme has no such shape,
    /// in which case no pointer is drawn at all.
    pub fn cursor_for(
        &mut self,
        renderer: &mut GlesRenderer,
        canvas: &Canvas,
        icon: CursorIcon,
    ) -> Option<(RenderTexture, Point<i32, Physical>)> {
        let scale = canvas.scale();
        let key = (icon, (scale * 100.0).round() as i32);
        self.cursor_animating = false;

        if !self.themed_cursors.contains_key(&key) {
            let img = self.cursor_theme.image(icon, scale)?;
            let frames: Vec<_> = img
                .frames
                .iter()
                .map(|f| {
                    let texture = RenderTexture::new(Texture::from_cursor_buffer(
                        renderer, scale, &f.pixels, f.width, f.height,
                    ));
                    (texture, Point::from((f.xhot, f.yhot)), f.delay_ms.max(1))
                })
                .collect();
            let total_ms = frames.iter().map(|(_, _, d)| *d).sum();
            self.themed_cursors
                .insert(key, CachedCursor { frames, total_ms });
        }

        let (count, total_ms) = {
            let cached = self.themed_cursors.get(&key)?;
            (cached.frames.len(), cached.total_ms)
        };
        self.cursor_animating = count > 1;

        let index = if count > 1 && total_ms > 0 {
            let start = match self.cursor_anim {
                Some((k, at)) if k == key => at,
                _ => {
                    let now = Instant::now();
                    self.cursor_anim = Some((key, now));
                    now
                }
            };
            let elapsed = (start.elapsed().as_millis() as u64 % total_ms as u64) as u32;
            let mut acc = 0;
            let cached = self.themed_cursors.get(&key)?;
            cached
                .frames
                .iter()
                .position(|(_, _, delay)| {
                    acc += *delay;
                    elapsed < acc
                })
                .unwrap_or(0)
        } else {
            0
        };

        let cached = self.themed_cursors.get(&key)?;
        let (texture, hotspot, _) = cached.frames.get(index)?;
        Some((texture.clone(), *hotspot))
    }
}

#[derive(Debug)]
struct CachedCursor {
    /// Texture, hotspot and how long each frame is shown. One entry for a
    /// static shape.
    frames: Vec<(RenderTexture, Point<i32, Physical>, u32)>,
    total_ms: u32,
}

/// Maps an SSD resize edge to the cursor shape forge requests for it.
pub fn resize_icon(edge: crate::ssd::ResizeEdge) -> CursorIcon {
    use crate::ssd::ResizeEdge::*;
    match edge {
        N | S => CursorIcon::NsResize,
        E | W => CursorIcon::EwResize,
        NW | SE => CursorIcon::NwseResize,
        NE | SW => CursorIcon::NeswResize,
    }
}

fn generate_gesture_handle_buffer(width: i32, height: i32, color: &[u8; 3]) -> Vec<u8> {
    let mut buffer = vec![0u8; (height * width * 4) as usize];

    let notch_h = (height as f64 * GESTURE_NOTCH_RATIO) as i32;
    let notch_w = (width as f64 * GESTURE_NOTCH_RATIO) as i32;

    let y_start = (height - notch_h) / 2;
    let y_end = (height + notch_h) / 2;
    let x_start = (width - notch_w) / 2;
    let x_end = (width + notch_w) / 2;

    for y in y_start..y_end {
        for x in x_start..x_end {
            let offset = ((y * width + x) * 4) as usize;
            buffer[offset..offset + 3].copy_from_slice(color);
        }
    }

    buffer
}
