//! Where the widgets the tour talks about are on screen, this pass.
//!
//! The tour's window is drawn last, long after the tabs that own the widgets
//! it points at, and it has no handle on any of them. So each interesting
//! widget publishes its rectangle here as it lays itself out, and the tour
//! reads them back at the end of the same pass to paint its glows.
//!
//! Two rules keep the registry honest. Nothing is recorded unless
//! [`set_active`] was called with `true` earlier in the frame — with the tour
//! closed, which is nearly always, marking costs one bool read. And every
//! entry is stamped with the pass that wrote it, so a widget that stopped
//! being drawn (a hidden column, another tab) stops being highlighted
//! immediately rather than glowing at a stale position.

use crate::app::Tab;

/// A widget the tour can point at. Not every widget: only the ones a page
/// names.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Spot {
    /// The *Add folder… / path box / Add* row of Manage Index's folder list.
    IndexedFolderAdd,
    /// The query box on the Search tab.
    SearchBar,
    /// The `?` button that opens the query-syntax window.
    QueryHelp,
    /// The Rank column: its header cell unioned with every cell below it.
    RankColumn,
    /// The whole bottom panel.
    StatusBar,
    /// The Fuzzy label and its checkbox together.
    FuzzyToggle,
    /// One entry in the tab strip.
    TabButton(Tab),
}

/// The registry itself, parked in the context's temp store.
#[derive(Clone, Default)]
struct Spots {
    /// Whether a tour is on screen; written before the panels draw.
    active: bool,
    /// The pass `marks` describes. Marks from any other pass are discarded.
    pass: u64,
    marks: Vec<(Spot, egui::Rect)>,
}

fn id() -> egui::Id {
    egui::Id::new("quicksearch-spotlight")
}

/// `data_mut` holds the context's own lock, so everything the closure needs
/// from `ctx` — the pass number, in practice — must be read before the call:
/// asking egui a second question from inside it deadlocks.
fn with<R>(ctx: &egui::Context, f: impl FnOnce(&mut Spots) -> R) -> R {
    ctx.data_mut(|d| f(d.get_temp_mut_or_default::<Spots>(id())))
}

/// Turn marking on or off. Call once per frame, before anything that marks.
pub fn set_active(ctx: &egui::Context, active: bool) {
    with(ctx, |spots| {
        if !active {
            spots.marks.clear();
        }
        spots.active = active;
    });
}

/// Record where `spot` is. Repeat marks for one spot in a single pass are
/// unioned — that is how the Rank column adds up from its separate cells.
pub fn mark(ctx: &egui::Context, spot: Spot, rect: egui::Rect) {
    let pass = ctx.cumulative_pass_nr();
    with(ctx, |spots| {
        if !spots.active {
            return;
        }
        if spots.pass != pass {
            spots.pass = pass;
            spots.marks.clear();
        }
        match spots.marks.iter_mut().find(|(s, _)| *s == spot) {
            Some((_, known)) => *known = known.union(rect),
            None => spots.marks.push((spot, rect)),
        }
    });
}

/// Where `spot` was drawn this pass, or `None` if it was not drawn at all.
pub fn rect(ctx: &egui::Context, spot: Spot) -> Option<egui::Rect> {
    let pass = ctx.cumulative_pass_nr();
    with(ctx, |spots| {
        if spots.pass != pass {
            return None;
        }
        spots
            .marks
            .iter()
            .find(|(s, _)| *s == spot)
            .map(|(_, rect)| *rect)
    })
}

/// Mark a widget from its response, in the manner of [`crate::tips::Tipped`].
pub trait Spotlit {
    fn spot(self, spot: Spot) -> Self;
}

impl Spotlit for egui::Response {
    fn spot(self, spot: Spot) -> egui::Response {
        mark(&self.ctx, spot, self.rect);
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// One pass, running `body` inside it, so marks land against a real
    /// `cumulative_pass_nr` rather than the zero a bare context reports.
    fn pass(ctx: &egui::Context, body: impl FnOnce(&egui::Context)) {
        let mut body = Some(body);
        let _ = ctx.run(egui::RawInput::default(), |ctx| {
            if let Some(body) = body.take() {
                body(ctx);
            }
        });
    }

    fn rect_at(x: f32) -> egui::Rect {
        egui::Rect::from_min_size(egui::pos2(x, x), egui::vec2(10.0, 10.0))
    }

    #[test]
    fn a_mark_is_readable_within_its_own_pass() {
        let ctx = egui::Context::default();
        pass(&ctx, |ctx| {
            set_active(ctx, true);
            mark(ctx, Spot::SearchBar, rect_at(4.0));
            assert_eq!(rect(ctx, Spot::SearchBar), Some(rect_at(4.0)));
            assert_eq!(rect(ctx, Spot::StatusBar), None, "never marked");
        });
    }

    /// The point of the pass stamp: a widget that is no longer drawn must
    /// stop reporting a position, not keep glowing where it used to be.
    #[test]
    fn a_mark_does_not_survive_into_the_next_pass() {
        let ctx = egui::Context::default();
        pass(&ctx, |ctx| {
            set_active(ctx, true);
            mark(ctx, Spot::SearchBar, rect_at(4.0));
        });
        pass(&ctx, |ctx| {
            assert_eq!(rect(ctx, Spot::SearchBar), None);
        });
    }

    #[test]
    fn nothing_is_recorded_while_inactive() {
        let ctx = egui::Context::default();
        pass(&ctx, |ctx| {
            mark(ctx, Spot::SearchBar, rect_at(4.0));
            assert_eq!(rect(ctx, Spot::SearchBar), None, "no tour, no marks");

            set_active(ctx, true);
            mark(ctx, Spot::SearchBar, rect_at(4.0));
            assert!(rect(ctx, Spot::SearchBar).is_some());

            // Dismissing the tour drops what was marked for it.
            set_active(ctx, false);
            assert_eq!(rect(ctx, Spot::SearchBar), None);
        });
    }

    /// A table column is marked once per cell; the glow has to cover all of them.
    #[test]
    fn repeat_marks_for_one_spot_union() {
        let ctx = egui::Context::default();
        pass(&ctx, |ctx| {
            set_active(ctx, true);
            mark(ctx, Spot::RankColumn, rect_at(0.0));
            mark(ctx, Spot::RankColumn, rect_at(40.0));
            assert_eq!(
                rect(ctx, Spot::RankColumn),
                Some(rect_at(0.0).union(rect_at(40.0)))
            );
        });
    }

    /// Tabs are distinct spots, not one shared "some tab" entry.
    #[test]
    fn tab_buttons_are_told_apart() {
        let ctx = egui::Context::default();
        pass(&ctx, |ctx| {
            set_active(ctx, true);
            mark(ctx, Spot::TabButton(Tab::Duplicates), rect_at(0.0));
            mark(ctx, Spot::TabButton(Tab::Settings), rect_at(40.0));
            assert_eq!(
                rect(ctx, Spot::TabButton(Tab::Duplicates)),
                Some(rect_at(0.0))
            );
            assert_eq!(
                rect(ctx, Spot::TabButton(Tab::Settings)),
                Some(rect_at(40.0))
            );
            assert_eq!(rect(ctx, Spot::TabButton(Tab::Help)), None);
        });
    }
}
