mod fit;
mod graphics_display;
pub(crate) mod image_data;
pub(crate) mod terminal;

pub use graphics_display::GraphicsDisplay;

pub(crate) use fit::rendering_area;

use {
    crate::{
        display::W,
        errors::ProgramError,
        image::zune_compat::DynamicImage,
        kitty::KittyGraphicsDisplay,
    },
    crokey::crossterm::style::Color,
    std::path::{Path, PathBuf},
    termimad::Area,
};

pub type ImageId = usize;

/// A terminal-graphics protocol able to draw an image into a screen area.
pub trait GraphicsRenderer: Send {
    /// Clear `area` and draw `src` (already fitted to the area's pixels) into it.
    /// Returns `Some(id)` for protocols whose images persist above the cells and
    /// must be erased later (Kitty); `None` for protocols drawn into the cells and
    /// cleared by normal repaint (Sixel).
    fn print(
        &mut self,
        w: &mut W,
        src: &DynamicImage,
        src_path: &Path,
        area: &Area,
        bg: Color,
    ) -> Result<Option<ImageId>, ProgramError>;

    /// Erase a previously drawn image by id. No-op for inline protocols.
    fn erase_image(&self, w: &mut W, id: ImageId) -> Result<(), ProgramError>;

    fn delete_temp_files(&mut self) {}

    /// Return the (width, height) of a terminal cell in pixels, as cached at
    /// construction time. Used to avoid re-querying the terminal every repaint.
    fn cell_size(&self) -> (u32, u32);

    /// Upper bound, in pixels, on the size an image should be fitted to before
    /// rendering, if the protocol/terminal imposes one. Larger images would be
    /// cropped (e.g. xterm crops oversized Sixel). `None` means no known bound
    /// (e.g. Kitty).
    fn max_render_size(&self) -> Option<(u32, u32)> {
        None
    }

    /// Whether a changed/removed inline image requires a full-screen clear to
    /// erase, rather than being cleared by repainting the cells it occupied.
    /// Default false (Kitty erases by id; most Sixel terminals drop it when the
    /// cells are overwritten). True for Konsole, which keeps Sixel until the
    /// screen is cleared; the manager then issues `ESC [ 2J` + a full redraw.
    fn needs_reclear_on_change(&self) -> bool {
        false
    }

    /// Constraints to apply when fitting an image for this renderer
    /// (e.g. Sixel on Konsole pads to a whole cell box: width to a cell-width multiple, height to a multiple of 6). Default: none.
    fn fit_constraints(&self, bg: Color) -> crate::image::FitConstraints {
        let _ = bg;
        crate::image::FitConstraints::default()
    }

    /// Called once at the end of each frame. Renderers drop any per-frame state
    /// here (e.g. a within-frame encode cache) so nothing is reused across
    /// frames, where the underlying image may have changed. Default no-op.
    fn end_frame(&mut self) {}
}

/// Outcome of an attempt to draw an image with a terminal graphics protocol.
///
/// This distinguishes "an image was drawn" from "there's an id to erase later":
/// Kitty draws and returns an id (its images float above the cells and must be
/// erased explicitly); Sixel draws into the cells and has no id. Both cases mean
/// the caller must NOT fall back to the text (half-block) rendering — only
/// `Unsupported` should trigger that fallback.
pub enum ImageRendering {
    /// An image was drawn into the area. `Some(id)` for protocols whose images
    /// must be erased later (Kitty); `None` for inline protocols (Sixel).
    Drawn(Option<ImageId>),
    /// No graphics protocol is available; the caller should use its text
    /// (half-block) fallback.
    Unsupported,
}

use {
    crate::app::AppContext,
    once_cell::sync::Lazy,
    std::sync::Mutex,
};

static MANAGER: Lazy<Mutex<GraphicsManager>> = Lazy::new(|| {
    Mutex::new(GraphicsManager {
        rendered_images: Vec::new(),
        renderer: MaybeRenderer::Untested,
        frame_sixels: Vec::new(),
        prev_sixels: Vec::new(),
        forced_redraw: false,
    })
});

pub fn manager() -> &'static Mutex<GraphicsManager> {
    &MANAGER
}

/// Detect the terminal-graphics renderer (once) so a later draw can use it.
///
/// This queries the terminal, which needs raw mode (enabled here, idempotently)
/// and requires the input-reader thread to be parked — so it must be called only
/// from a safe window: at startup before that thread is spawned, or during
/// command handling. It's memoized: after the first call the renderer is cached
/// and this does no further terminal I/O. No-op when stdout isn't a terminal.
pub fn prepare_renderer(con: &AppContext) -> Result<(), ProgramError> {
    if !con.is_tty {
        return Ok(());
    }
    crokey::crossterm::terminal::enable_raw_mode()?;
    if let Ok(mut mgr) = manager().lock() {
        // runs detection and memoizes it; the returned handle isn't needed here,
        // drawing later fetches the renderer from the cache
        mgr.renderer(con);
    }
    Ok(())
}

#[derive(Debug)]
struct RenderedImage {
    image_id: ImageId,
    drawing_count: usize,
}


enum MaybeRenderer {
    Untested,
    Disabled,
    Enabled { renderer: Box<dyn GraphicsRenderer> },
}

/// Choose a graphics renderer for this terminal, honoring `graphics-display`
/// (overridable per-run by the BROOT_GRAPHICS_PROTOCOL env var).
fn select_renderer(con: &AppContext) -> Option<Box<dyn GraphicsRenderer>> {
    let build_kitty = || {
        crate::kitty::build_kitty_renderer(con).map(|r| Box::new(r) as Box<dyn GraphicsRenderer>)
    };
    #[cfg(feature = "sixel")]
    let build_sixel = || {
        if crate::sixel::detect_sixel_support() {
            crate::sixel::SixelRenderer::new().map(|r| Box::new(r) as Box<dyn GraphicsRenderer>)
        } else {
            None
        }
    };
    // Without the `sixel` feature there's no renderer to build (and no DA1 probe).
    #[cfg(not(feature = "sixel"))]
    let build_sixel = || -> Option<Box<dyn GraphicsRenderer>> { None };

    // env override (BROOT_GRAPHICS_PROTOCOL) takes precedence over the config
    let display = graphics_display::graphics_display_from_env().unwrap_or(con.graphics_display);
    match display {
        GraphicsDisplay::None => {
            debug!("terminal graphics disabled (graphics-display = none)");
            None
        }
        GraphicsDisplay::Kitty => {
            let r = build_kitty();
            if r.is_none() {
                if con.kitty_graphics_display == KittyGraphicsDisplay::None {
                    warn!("graphics-display = kitty is overridden by kitty-graphics-display = none");
                } else {
                    warn!("graphics-display = kitty but Kitty graphics are unavailable");
                }
            }
            r
        }
        GraphicsDisplay::Sixel => {
            #[cfg(feature = "sixel")]
            {
                // Forcing the protocol bypasses the DA1 detection probe (matches the
                // BROOT_GRAPHICS_PROTOCOL=sixel override); Auto still probes.
                let r = crate::sixel::SixelRenderer::new()
                    .map(|r| Box::new(r) as Box<dyn GraphicsRenderer>);
                if r.is_none() {
                    warn!("graphics-display = sixel but the Sixel renderer failed to initialize");
                }
                r
            }
            #[cfg(not(feature = "sixel"))]
            {
                warn!("graphics-display = sixel but broot was built without the `sixel` feature");
                None
            }
        }
        GraphicsDisplay::Auto => {
            if let Some(r) = build_kitty() {
                debug!("using kitty graphics protocol");
                Some(r)
            } else if let Some(r) = build_sixel() {
                debug!("using sixel graphics protocol");
                Some(r)
            } else {
                debug!("no terminal graphics protocol available");
                None
            }
        }
    }
}

pub struct GraphicsManager {
    rendered_images: Vec<RenderedImage>,
    renderer: MaybeRenderer,
    /// `(path, area, source_dims)` of the inline (Sixel) images drawn or kept in
    /// the current draw pass. Unlike Kitty (erased by id), a Sixel is drawn into
    /// the cells and, on terminals that keep it until the screen is cleared
    /// (Konsole), can't be removed by repainting text. Tracking what's on screen
    /// frame to frame lets `reclear_needed` notice when one changes/leaves/moves.
    /// Source dimensions are part of the identity so a different-resolution
    /// in-place file swap (same path + area) is still detected as a change.
    frame_sixels: Vec<(PathBuf, Area, (u32, u32))>,
    /// `frame_sixels` from the last committed frame.
    prev_sixels: Vec<(PathBuf, Area, (u32, u32))>,
    /// Set during the redraw pass after a full-screen clear, so images that
    /// would otherwise be kept in place repaint (the clear wiped them).
    forced_redraw: bool,
}

impl GraphicsManager {
    fn renderer_if_tested(&mut self) -> Option<&mut dyn GraphicsRenderer> {
        match &mut self.renderer {
            MaybeRenderer::Enabled { renderer } => Some(renderer.as_mut()),
            _ => None,
        }
    }
    pub fn delete_temp_files(&mut self) {
        if let MaybeRenderer::Enabled { renderer } = &mut self.renderer {
            renderer.delete_temp_files();
        }
    }
    pub fn renderer(&mut self, con: &AppContext) -> Option<&mut dyn GraphicsRenderer> {
        if matches!(self.renderer, MaybeRenderer::Disabled) {
            return None;
        }
        if matches!(self.renderer, MaybeRenderer::Enabled { .. }) {
            return self.renderer_if_tested();
        }
        let chosen = select_renderer(con);
        self.renderer = match chosen {
            Some(renderer) => MaybeRenderer::Enabled { renderer },
            None => MaybeRenderer::Disabled,
        };
        self.renderer_if_tested()
    }
    pub fn keep(&mut self, kept_id: ImageId, drawing_count: usize) {
        for image in &mut self.rendered_images {
            if image.image_id == kept_id {
                image.drawing_count = drawing_count;
            }
        }
    }
    #[allow(clippy::too_many_arguments)]
    pub fn try_print_image(
        &mut self,
        w: &mut W,
        src: &crate::image::SourceImage,
        src_path: &Path,
        area: &Area,
        bg: Color,
        drawing_count: usize,
    ) -> Result<ImageRendering, ProgramError> {
        // Only draw when graphics were already detected (done in a safe window,
        // see app.rs). Never probe here: drawing happens while the input reader
        // is reading the terminal, so a detection query would race it.
        if let Some(renderer) = self.renderer_if_tested() {
            let (cell_width, cell_height) = renderer.cell_size();
            let mut area_width = area.width as u32 * cell_width;
            let mut area_height = area.height as u32 * cell_height;
            // Fit within the protocol/terminal's render bound so the image isn't
            // cropped (e.g. xterm crops Sixel larger than its current geometry).
            if let Some((max_w, max_h)) = renderer.max_render_size() {
                area_width = area_width.min(max_w);
                area_height = area_height.min(max_h);
            }
            // Fitting (decode + resize) is the main CPU cost before rendering,
            // for any protocol; log the target so a slow render is diagnosable.
            debug!("fitting image to {area_width}x{area_height}px for render");
            let constraints = renderer.fit_constraints(bg);
            let img = src.fitting(area_width, area_height, None, constraints)?;
            let id = renderer.print(w, &img, src_path, area, bg)?;
            if let Some(new_id) = id {
                // Kitty: tracked by id and erased via escape; not an inline image.
                self.rendered_images.push(RenderedImage {
                    image_id: new_id,
                    drawing_count,
                });
            } else {
                // Sixel: drawn into the cells, no id. Record it so reclear
                // detection knows what's on screen this frame.
                self.note_inline(src_path, area, src.dimensions());
            }
            // An image was drawn (Kitty with an id, or Sixel with none); either
            // way the caller must not draw the text fallback over it.
            Ok(ImageRendering::Drawn(id))
        } else {
            Ok(ImageRendering::Unsupported)
        }
    }
    /// Start a draw pass: forget the inline (Sixel) images noted in the previous
    /// pass of this frame. Call before each pass over the panels.
    pub fn start_pass(&mut self) {
        self.frame_sixels.clear();
    }
    /// Record that an inline (Sixel) image for `path` (with source pixel size
    /// `dims`) occupies `area` this pass (drawn or kept in place). Drives
    /// `reclear_needed`; `dims` distinguishes an in-place file swap that keeps
    /// the same path and area.
    pub fn note_inline(&mut self, path: &Path, area: &Area, dims: (u32, u32)) {
        self.frame_sixels.push((path.to_path_buf(), area.clone(), dims));
    }
    /// Whether a full-screen clear + redraw is needed before this frame is
    /// final: an inline image shown last frame is no longer shown identically
    /// (changed, left, or moved), on a terminal that keeps Sixel until the
    /// screen is cleared (Konsole). False elsewhere — repainting cells suffices.
    pub fn reclear_needed(&self) -> bool {
        let renderer_needs = match &self.renderer {
            MaybeRenderer::Enabled { renderer } => renderer.needs_reclear_on_change(),
            _ => false,
        };
        renderer_needs
            && self
                .prev_sixels
                .iter()
                .any(|s| !self.frame_sixels.contains(s))
    }
    /// Whether the current pass is a post-clear redraw, in which an image that
    /// would normally be kept in place must instead be repainted (the clear
    /// removed it). Read by `ImageView` when deciding `must_draw`.
    pub fn forced_redraw(&self) -> bool {
        self.forced_redraw
    }
    pub fn set_forced_redraw(&mut self, on: bool) {
        self.forced_redraw = on;
    }
    /// Remember this frame's inline images for next-frame comparison, and let
    /// the renderer drop per-frame state (the encode cache). Call once after the
    /// final pass.
    pub fn commit_frame(&mut self) {
        self.prev_sixels = std::mem::take(&mut self.frame_sixels);
        if let MaybeRenderer::Enabled { renderer } = &mut self.renderer {
            renderer.end_frame();
        }
    }
    pub fn erase_images_before(
        &mut self,
        w: &mut W,
        drawing_count: usize,
    ) -> Result<(), ProgramError> {
        let mut kept_images = Vec::new();
        let stale: Vec<RenderedImage> = std::mem::take(&mut self.rendered_images);
        for image in stale {
            if image.drawing_count >= drawing_count {
                kept_images.push(image);
            } else if let MaybeRenderer::Enabled { renderer } = &self.renderer {
                renderer.erase_image(w, image.image_id)?;
            }
        }
        self.rendered_images = kept_images;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{Arc, Mutex};

    /// Minimal renderer for the manager tests.
    struct TestRenderer {
        reclear: bool,
        /// Returned by `max_render_size()`.
        max_size: Option<(u32, u32)>,
        /// print()'s returned image id: `Some` mimics a Kitty-style protocol
        /// (tracked and erased by id), `None` an inline one (Sixel).
        print_returns_id: Option<ImageId>,
        /// Fitted dims from the last `print()` call, shared because the renderer
        /// is boxed into the manager (`Arc<Mutex>`: the trait requires `Send`).
        last_print_dims: Arc<Mutex<Option<(u32, u32)>>>,
    }
    impl Default for TestRenderer {
        fn default() -> Self {
            Self {
                reclear: false,
                max_size: None,
                print_returns_id: None,
                last_print_dims: Arc::new(Mutex::new(None)),
            }
        }
    }
    impl GraphicsRenderer for TestRenderer {
        fn print(
            &mut self,
            _w: &mut W,
            src: &DynamicImage,
            _src_path: &Path,
            _area: &Area,
            _bg: Color,
        ) -> Result<Option<ImageId>, ProgramError> {
            *self.last_print_dims.lock().unwrap() = Some(src.dimensions());
            Ok(self.print_returns_id)
        }
        fn erase_image(&self, _w: &mut W, _id: ImageId) -> Result<(), ProgramError> {
            Ok(())
        }
        fn cell_size(&self) -> (u32, u32) {
            (10, 20)
        }
        fn needs_reclear_on_change(&self) -> bool {
            self.reclear
        }
        fn max_render_size(&self) -> Option<(u32, u32)> {
            self.max_size
        }
    }

    fn manager(reclear: bool) -> GraphicsManager {
        manager_with(TestRenderer { reclear, ..Default::default() })
    }

    /// A manager wrapping a caller-configured `TestRenderer`.
    fn manager_with(renderer: TestRenderer) -> GraphicsManager {
        GraphicsManager {
            rendered_images: Vec::new(),
            renderer: MaybeRenderer::Enabled {
                renderer: Box::new(renderer),
            },
            frame_sixels: Vec::new(),
            prev_sixels: Vec::new(),
            forced_redraw: false,
        }
    }

    /// A solid-color `SourceImage` bitmap.
    fn solid_bitmap(w: u32, h: u32, rgba: [u8; 4]) -> crate::image::SourceImage {
        let data: Vec<u8> = std::iter::repeat_n(rgba, (w * h) as usize).flatten().collect();
        crate::image::SourceImage::Bitmap(DynamicImage::from_rgba8(w, h, data).unwrap())
    }

    fn area() -> Area {
        Area::new(0, 0, 10, 10)
    }

    const DIMS: (u32, u32) = (640, 480);

    #[test]
    fn no_reclear_when_nothing_shown_last_frame() {
        let mut m = manager(true);
        m.start_pass();
        m.note_inline(Path::new("a.png"), &area(), DIMS);
        // first appearance: nothing to erase
        assert!(!m.reclear_needed());
    }

    #[test]
    fn no_reclear_when_same_image_kept() {
        let mut m = manager(true);
        m.start_pass();
        m.note_inline(Path::new("a.png"), &area(), DIMS);
        m.commit_frame();
        m.start_pass();
        m.note_inline(Path::new("a.png"), &area(), DIMS);
        assert!(!m.reclear_needed());
    }

    #[test]
    fn reclear_when_image_changes() {
        let mut m = manager(true);
        m.start_pass();
        m.note_inline(Path::new("a.png"), &area(), DIMS);
        m.commit_frame();
        m.start_pass();
        m.note_inline(Path::new("b.png"), &area(), DIMS);
        assert!(m.reclear_needed());
    }

    #[test]
    fn reclear_when_image_leaves() {
        let mut m = manager(true);
        m.start_pass();
        m.note_inline(Path::new("a.png"), &area(), DIMS);
        m.commit_frame();
        m.start_pass(); // preview hidden or became text: nothing noted
        assert!(m.reclear_needed());
    }

    #[test]
    fn reclear_when_image_moves() {
        let mut m = manager(true);
        m.start_pass();
        m.note_inline(Path::new("a.png"), &area(), DIMS);
        m.commit_frame();
        m.start_pass();
        m.note_inline(Path::new("a.png"), &Area::new(5, 0, 10, 10), DIMS); // resized/moved
        assert!(m.reclear_needed());
    }

    #[test]
    fn reclear_when_file_swapped_to_different_resolution() {
        // same path + area, but the file changed in place to a new resolution
        let mut m = manager(true);
        m.start_pass();
        m.note_inline(Path::new("a.png"), &area(), (640, 480));
        m.commit_frame();
        m.start_pass();
        m.note_inline(Path::new("a.png"), &area(), (800, 600));
        assert!(m.reclear_needed());
    }

    #[test]
    fn never_reclear_when_renderer_opts_out() {
        let mut m = manager(false); // e.g. Kitty / non-Konsole Sixel
        m.start_pass();
        m.note_inline(Path::new("a.png"), &area(), DIMS);
        m.commit_frame();
        m.start_pass(); // image left
        assert!(!m.reclear_needed());
    }

    #[test]
    fn commit_frame_rolls_current_into_previous() {
        let mut m = manager(true);
        m.start_pass();
        m.note_inline(Path::new("a.png"), &area(), DIMS);
        m.commit_frame();
        assert_eq!(m.prev_sixels.len(), 1);
        assert!(m.frame_sixels.is_empty());
    }

    #[test]
    fn start_pass_clears_current_frame() {
        let mut m = manager(true);
        m.note_inline(Path::new("a.png"), &area(), DIMS);
        m.start_pass();
        assert!(m.frame_sixels.is_empty());
    }

    #[test]
    fn forced_redraw_toggles() {
        let mut m = manager(true);
        assert!(!m.forced_redraw());
        m.set_forced_redraw(true);
        assert!(m.forced_redraw());
        m.set_forced_redraw(false);
        assert!(!m.forced_redraw());
    }

    #[test]
    fn default_fit_constraints_is_unconstrained() {
        let r = TestRenderer { reclear: false, ..Default::default() };
        let c = r.fit_constraints(Color::Reset);
        assert_eq!(c.width_multiple, 1);
        assert_eq!(c.height_multiple, 1);
        assert!(c.pad.is_none());
    }

    // TestRenderer::print never writes to `w`, so nothing queues on the writer.
    #[test]
    fn try_print_image_clamps_to_max_render_size() {
        let dims = Arc::new(Mutex::new(None));
        let renderer = TestRenderer {
            max_size: Some((16, 16)),
            last_print_dims: Arc::clone(&dims),
            ..Default::default()
        };
        let mut m = manager_with(renderer);
        // 200x100 source, well above the 16x16 clamp.
        let src = solid_bitmap(200, 100, [1, 2, 3, 255]);
        let mut w = crate::display::writer();
        m.try_print_image(&mut w, &src, Path::new("a.png"), &area(), Color::Reset, 1)
            .unwrap();
        let (pw, ph) = dims.lock().unwrap().expect("print() should have been called");
        assert!(pw <= 16, "fitted width {pw} exceeds the 16px clamp");
        assert!(ph <= 16, "fitted height {ph} exceeds the 16px clamp");
    }

    #[test]
    fn try_print_image_tracks_kitty_id_not_inline() {
        let renderer = TestRenderer {
            print_returns_id: Some(7),
            ..Default::default()
        };
        let mut m = manager_with(renderer);
        let src = solid_bitmap(4, 4, [1, 2, 3, 255]);
        let mut w = crate::display::writer();
        let result = m
            .try_print_image(&mut w, &src, Path::new("a.png"), &area(), Color::Reset, 1)
            .unwrap();
        assert!(matches!(result, ImageRendering::Drawn(Some(7))));
        assert_eq!(m.rendered_images.len(), 1);
        assert_eq!(m.rendered_images[0].image_id, 7);
        assert!(m.frame_sixels.is_empty());
    }

    #[test]
    fn try_print_image_notes_inline_not_kitty_id() {
        let renderer = TestRenderer::default(); // print_returns_id: None
        let mut m = manager_with(renderer);
        let src = solid_bitmap(4, 4, [1, 2, 3, 255]);
        let mut w = crate::display::writer();
        let result = m
            .try_print_image(&mut w, &src, Path::new("a.png"), &area(), Color::Reset, 1)
            .unwrap();
        assert!(matches!(result, ImageRendering::Drawn(None)));
        assert_eq!(m.frame_sixels.len(), 1);
        assert!(m.rendered_images.is_empty());
    }

    #[test]
    fn erase_images_before_drops_stale_and_keeps_current() {
        let mut m = manager(false);
        m.rendered_images = vec![
            RenderedImage { image_id: 1, drawing_count: 5 },
            RenderedImage { image_id: 2, drawing_count: 2 },
        ];
        let mut w = crate::display::writer();
        m.erase_images_before(&mut w, 3).unwrap();
        assert_eq!(m.rendered_images.len(), 1);
        assert_eq!(m.rendered_images[0].image_id, 1);
    }

    #[test]
    fn erase_images_before_boundary_equal_count_is_kept() {
        let mut m = manager(false);
        m.rendered_images = vec![RenderedImage { image_id: 1, drawing_count: 3 }];
        let mut w = crate::display::writer();
        m.erase_images_before(&mut w, 3).unwrap();
        assert_eq!(m.rendered_images.len(), 1);
        assert_eq!(m.rendered_images[0].image_id, 1);
    }

    #[test]
    fn keep_updates_drawing_count_of_matching_id_only() {
        let mut m = manager(false);
        m.rendered_images = vec![
            RenderedImage { image_id: 1, drawing_count: 1 },
            RenderedImage { image_id: 2, drawing_count: 1 },
        ];
        m.keep(1, 99);
        assert_eq!(m.rendered_images[0].drawing_count, 99);
        assert_eq!(m.rendered_images[1].drawing_count, 1);
    }
}
