//! Simple plotting library for [`egui`](https://github.com/emilk/egui).
//!
//! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!

use std::{ops::RangeInclusive, sync::Arc};

use egui::ahash::HashMap;
use epaint::util::FloatOrd;
use epaint::Hsva;

use axis::AxisWidget;
use items::PlotItem;
use legend::LegendWidget;

use egui::*;

pub use items::{
    Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape,
    Orientation, PlotImage, PlotPoint, PlotPoints, Points, Polygon, Text, VLine,
};
pub use legend::{Corner, Legend};
pub use transform::{PlotBounds, PlotTransform};

use items::{horizontal_line, rulers_color, vertical_line};

pub use axis::{Axis, AxisHints, HPlacement, Placement, VPlacement};

mod axis;
mod items;
mod legend;
mod transform;

type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String;
type LabelFormatter = Option<Box<LabelFormatterFn>>;

type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>;
type GridSpacer = Box<GridSpacerFn>;

type CoordinatesFormatterFn = dyn Fn(&PlotPoint, &PlotBounds) -> String;

/// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`].
pub struct CoordinatesFormatter {
    function: Box<CoordinatesFormatterFn>,
}

impl CoordinatesFormatter {
    /// Create a new formatter based on the pointer coordinate and the plot bounds.
    pub fn new(function: impl Fn(&PlotPoint, &PlotBounds) -> String + 'static) -> Self {
        Self {
            function: Box::new(function),
        }
    }

    /// Show a fixed number of decimal places.
    pub fn with_decimals(num_decimals: usize) -> Self {
        Self {
            function: Box::new(move |value, _| {
                format!("x: {:.d$}\ny: {:.d$}", value.x, value.y, d = num_decimals)
            }),
        }
    }

    fn format(&self, value: &PlotPoint, bounds: &PlotBounds) -> String {
        (self.function)(value, bounds)
    }
}

impl Default for CoordinatesFormatter {
    fn default() -> Self {
        Self::with_decimals(3)
    }
}

// ----------------------------------------------------------------------------

const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO(emilk): large enough for a wide label

/// Information about the plot that has to persist between frames.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone)]
struct PlotMemory {
    /// Indicates if the plot uses automatic bounds. This is disengaged whenever the user modifies
    /// the bounds, for example by moving or zooming.
    auto_bounds: Vec2b,

    hovered_entry: Option<String>,
    hidden_items: ahash::HashSet<String>,
    last_plot_transform: PlotTransform,

    /// Allows to remember the first click position when performing a boxed zoom
    last_click_pos_for_zoom: Option<Pos2>,
}

#[cfg(feature = "serde")]
impl PlotMemory {
    pub fn load(ctx: &Context, id: Id) -> Option<Self> {
        ctx.data_mut(|d| d.get_persisted(id))
    }

    pub fn store(self, ctx: &Context, id: Id) {
        ctx.data_mut(|d| d.insert_persisted(id, self));
    }
}

#[cfg(not(feature = "serde"))]
impl PlotMemory {
    pub fn load(ctx: &Context, id: Id) -> Option<Self> {
        ctx.data_mut(|d| d.get_temp(id))
    }

    pub fn store(self, ctx: &Context, id: Id) {
        ctx.data_mut(|d| d.insert_temp(id, self));
    }
}

// ----------------------------------------------------------------------------

/// Indicates a vertical or horizontal cursor line in plot coordinates.
#[derive(Copy, Clone, PartialEq)]
enum Cursor {
    Horizontal { y: f64 },
    Vertical { x: f64 },
}

/// Contains the cursors drawn for a plot widget in a single frame.
#[derive(PartialEq, Clone)]
struct PlotFrameCursors {
    id: Id,
    cursors: Vec<Cursor>,
}

#[derive(Default, Clone)]
struct CursorLinkGroups(HashMap<Id, Vec<PlotFrameCursors>>);

#[derive(Clone)]
struct LinkedBounds {
    bounds: PlotBounds,
    auto_bounds: Vec2b,
}

#[derive(Default, Clone)]
struct BoundsLinkGroups(HashMap<Id, LinkedBounds>);

// ----------------------------------------------------------------------------

/// What [`Plot::show`] returns.
pub struct PlotResponse<R> {
    /// What the user closure returned.
    pub inner: R,

    /// The response of the plot.
    pub response: Response,

    /// The transform between screen coordinates and plot coordinates.
    pub transform: PlotTransform,
}

// ----------------------------------------------------------------------------

/// A 2D plot, e.g. a graph of a function.
///
/// [`Plot`] supports multiple lines and points.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// use egui_plot::{Line, Plot, PlotPoints};
/// let sin: PlotPoints = (0..1000).map(|i| {
///     let x = i as f64 * 0.01;
///     [x, x.sin()]
/// }).collect();
/// let line = Line::new(sin);
/// Plot::new("my_plot").view_aspect(2.0).show(ui, |plot_ui| plot_ui.line(line));
/// # });
/// ```
pub struct Plot {
    id_source: Id,

    center_axis: Vec2b,
    allow_zoom: Vec2b,
    allow_drag: Vec2b,
    allow_scroll: bool,
    allow_double_click_reset: bool,
    allow_boxed_zoom: bool,
    default_auto_bounds: Vec2b,
    min_auto_bounds: PlotBounds,
    margin_fraction: Vec2,
    boxed_zoom_pointer_button: PointerButton,
    linked_axes: Option<(Id, Vec2b)>,
    linked_cursors: Option<(Id, Vec2b)>,

    min_size: Vec2,
    width: Option<f32>,
    height: Option<f32>,
    data_aspect: Option<f32>,
    view_aspect: Option<f32>,

    reset: bool,

    show_x: bool,
    show_y: bool,
    label_formatter: LabelFormatter,
    coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
    x_axes: Vec<AxisHints>, // default x axes
    y_axes: Vec<AxisHints>, // default y axes
    legend_config: Option<Legend>,
    show_background: bool,
    show_axes: Vec2b,
    show_grid: Vec2b,
    grid_spacers: [GridSpacer; 2],
    sharp_grid_lines: bool,
    clamp_grid: bool,
}

impl Plot {
    /// Give a unique id for each plot within the same [`Ui`].
    pub fn new(id_source: impl std::hash::Hash) -> Self {
        Self {
            id_source: Id::new(id_source),

            center_axis: false.into(),
            allow_zoom: true.into(),
            allow_drag: true.into(),
            allow_scroll: true,
            allow_double_click_reset: true,
            allow_boxed_zoom: true,
            default_auto_bounds: true.into(),
            min_auto_bounds: PlotBounds::NOTHING,
            margin_fraction: Vec2::splat(0.05),
            boxed_zoom_pointer_button: PointerButton::Secondary,
            linked_axes: None,
            linked_cursors: None,

            min_size: Vec2::splat(64.0),
            width: None,
            height: None,
            data_aspect: None,
            view_aspect: None,

            reset: false,

            show_x: true,
            show_y: true,
            label_formatter: None,
            coordinates_formatter: None,
            x_axes: vec![Default::default()],
            y_axes: vec![Default::default()],
            legend_config: None,
            show_background: true,
            show_axes: true.into(),
            show_grid: true.into(),
            grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
            sharp_grid_lines: true,
            clamp_grid: false,
        }
    }

    /// width / height ratio of the data.
    /// For instance, it can be useful to set this to `1.0` for when the two axes show the same
    /// unit.
    /// By default the plot window's aspect ratio is used.
    #[inline]
    pub fn data_aspect(mut self, data_aspect: f32) -> Self {
        self.data_aspect = Some(data_aspect);
        self
    }

    /// width / height ratio of the plot region.
    /// By default no fixed aspect ratio is set (and width/height will fill the ui it is in).
    #[inline]
    pub fn view_aspect(mut self, view_aspect: f32) -> Self {
        self.view_aspect = Some(view_aspect);
        self
    }

    /// Width of plot. By default a plot will fill the ui it is in.
    /// If you set [`Self::view_aspect`], the width can be calculated from the height.
    #[inline]
    pub fn width(mut self, width: f32) -> Self {
        self.min_size.x = width;
        self.width = Some(width);
        self
    }

    /// Height of plot. By default a plot will fill the ui it is in.
    /// If you set [`Self::view_aspect`], the height can be calculated from the width.
    #[inline]
    pub fn height(mut self, height: f32) -> Self {
        self.min_size.y = height;
        self.height = Some(height);
        self
    }

    /// Minimum size of the plot view.
    #[inline]
    pub fn min_size(mut self, min_size: Vec2) -> Self {
        self.min_size = min_size;
        self
    }

    /// Show the x-value (e.g. when hovering). Default: `true`.
    #[inline]
    pub fn show_x(mut self, show_x: bool) -> Self {
        self.show_x = show_x;
        self
    }

    /// Show the y-value (e.g. when hovering). Default: `true`.
    #[inline]
    pub fn show_y(mut self, show_y: bool) -> Self {
        self.show_y = show_y;
        self
    }

    /// Always keep the X-axis centered. Default: `false`.
    #[inline]
    pub fn center_x_axis(mut self, on: bool) -> Self {
        self.center_axis.x = on;
        self
    }

    /// Always keep the Y-axis centered. Default: `false`.
    #[inline]
    pub fn center_y_axis(mut self, on: bool) -> Self {
        self.center_axis.y = on;
        self
    }

    /// Whether to allow zooming in the plot. Default: `true`.
    ///
    /// Note: Allowing zoom in one axis but not the other may lead to unexpected results if used in combination with `data_aspect`.
    #[inline]
    pub fn allow_zoom<T>(mut self, on: T) -> Self
    where
        T: Into<Vec2b>,
    {
        self.allow_zoom = on.into();
        self
    }

    /// Whether to allow scrolling in the plot. Default: `true`.
    #[inline]
    pub fn allow_scroll(mut self, on: bool) -> Self {
        self.allow_scroll = on;
        self
    }

    /// Whether to allow double clicking to reset the view.
    /// Default: `true`.
    #[inline]
    pub fn allow_double_click_reset(mut self, on: bool) -> Self {
        self.allow_double_click_reset = on;
        self
    }

    /// Set the side margin as a fraction of the plot size. Only used for auto bounds.
    ///
    /// For instance, a value of `0.1` will add 10% space on both sides.
    #[inline]
    pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self {
        self.margin_fraction = margin_fraction;
        self
    }

    /// Whether to allow zooming in the plot by dragging out a box with the secondary mouse button.
    ///
    /// Default: `true`.
    #[inline]
    pub fn allow_boxed_zoom(mut self, on: bool) -> Self {
        self.allow_boxed_zoom = on;
        self
    }

    /// Config the button pointer to use for boxed zooming. Default: [`Secondary`](PointerButton::Secondary)
    #[inline]
    pub fn boxed_zoom_pointer_button(mut self, boxed_zoom_pointer_button: PointerButton) -> Self {
        self.boxed_zoom_pointer_button = boxed_zoom_pointer_button;
        self
    }

    /// Whether to allow dragging in the plot to move the bounds. Default: `true`.
    #[inline]
    pub fn allow_drag<T>(mut self, on: T) -> Self
    where
        T: Into<Vec2b>,
    {
        self.allow_drag = on.into();
        self
    }

    /// Provide a function to customize the on-hover label for the x and y axis
    ///
    /// ```
    /// # egui::__run_test_ui(|ui| {
    /// use egui_plot::{Line, Plot, PlotPoints};
    /// let sin: PlotPoints = (0..1000).map(|i| {
    ///     let x = i as f64 * 0.01;
    ///     [x, x.sin()]
    /// }).collect();
    /// let line = Line::new(sin);
    /// Plot::new("my_plot").view_aspect(2.0)
    /// .label_formatter(|name, value| {
    ///     if !name.is_empty() {
    ///         format!("{}: {:.*}%", name, 1, value.y)
    ///     } else {
    ///         "".to_owned()
    ///     }
    /// })
    /// .show(ui, |plot_ui| plot_ui.line(line));
    /// # });
    /// ```
    pub fn label_formatter(
        mut self,
        label_formatter: impl Fn(&str, &PlotPoint) -> String + 'static,
    ) -> Self {
        self.label_formatter = Some(Box::new(label_formatter));
        self
    }

    /// Show the pointer coordinates in the plot.
    pub fn coordinates_formatter(
        mut self,
        position: Corner,
        formatter: CoordinatesFormatter,
    ) -> Self {
        self.coordinates_formatter = Some((position, formatter));
        self
    }

    /// Configure how the grid in the background is spaced apart along the X axis.
    ///
    /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units.
    ///
    /// The function has this signature:
    /// ```ignore
    /// fn step_sizes(input: GridInput) -> Vec<GridMark>;
    /// ```
    ///
    /// This function should return all marks along the visible range of the X axis.
    /// `step_size` also determines how thick/faint each line is drawn.
    /// For example, if x = 80..=230 is visible and you want big marks at steps of
    /// 100 and small ones at 25, you can return:
    /// ```no_run
    /// # use egui_plot::GridMark;
    /// vec![
    ///    // 100s
    ///    GridMark { value: 100.0, step_size: 100.0 },
    ///    GridMark { value: 200.0, step_size: 100.0 },
    ///
    ///    // 25s
    ///    GridMark { value: 125.0, step_size: 25.0 },
    ///    GridMark { value: 150.0, step_size: 25.0 },
    ///    GridMark { value: 175.0, step_size: 25.0 },
    ///    GridMark { value: 225.0, step_size: 25.0 },
    /// ];
    /// # ()
    /// ```
    ///
    /// There are helpers for common cases, see [`log_grid_spacer`] and [`uniform_grid_spacer`].
    #[inline]
    pub fn x_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'static) -> Self {
        self.grid_spacers[0] = Box::new(spacer);
        self
    }

    /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units.
    ///
    /// See [`Self::x_grid_spacer`] for explanation.
    #[inline]
    pub fn y_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'static) -> Self {
        self.grid_spacers[1] = Box::new(spacer);
        self
    }

    /// Clamp the grid to only be visible at the range of data where we have values.
    ///
    /// Default: `false`.
    #[inline]
    pub fn clamp_grid(mut self, clamp_grid: bool) -> Self {
        self.clamp_grid = clamp_grid;
        self
    }

    /// Expand bounds to include the given x value.
    /// For instance, to always show the y axis, call `plot.include_x(0.0)`.
    #[inline]
    pub fn include_x(mut self, x: impl Into<f64>) -> Self {
        self.min_auto_bounds.extend_with_x(x.into());
        self
    }

    /// Expand bounds to include the given y value.
    /// For instance, to always show the x axis, call `plot.include_y(0.0)`.
    #[inline]
    pub fn include_y(mut self, y: impl Into<f64>) -> Self {
        self.min_auto_bounds.extend_with_y(y.into());
        self
    }

    /// Set whether the bounds should be automatically set based on data by default.
    ///
    /// This is enabled by default.
    #[inline]
    pub fn auto_bounds(mut self, auto_bounds: Vec2b) -> Self {
        self.default_auto_bounds = auto_bounds;
        self
    }

    /// Expand bounds to fit all items across the x axis, including values given by `include_x`.
    #[deprecated = "Use `auto_bounds` instead"]
    #[inline]
    pub fn auto_bounds_x(mut self) -> Self {
        self.default_auto_bounds.x = true;
        self
    }

    /// Expand bounds to fit all items across the y axis, including values given by `include_y`.
    #[deprecated = "Use `auto_bounds` instead"]
    #[inline]
    pub fn auto_bounds_y(mut self) -> Self {
        self.default_auto_bounds.y = true;
        self
    }

    /// Show a legend including all named items.
    #[inline]
    pub fn legend(mut self, legend: Legend) -> Self {
        self.legend_config = Some(legend);
        self
    }

    /// Whether or not to show the background [`Rect`].
    ///
    /// Can be useful to disable if the plot is overlaid over existing content.
    /// Default: `true`.
    #[inline]
    pub fn show_background(mut self, show: bool) -> Self {
        self.show_background = show;
        self
    }

    /// Show axis labels and grid tick values on the side of the plot.
    ///
    /// Default: `true`.
    #[inline]
    pub fn show_axes(mut self, show: impl Into<Vec2b>) -> Self {
        self.show_axes = show.into();
        self
    }

    /// Show a grid overlay on the plot.
    ///
    /// Default: `true`.
    #[inline]
    pub fn show_grid(mut self, show: impl Into<Vec2b>) -> Self {
        self.show_grid = show.into();
        self
    }

    /// Add this plot to an axis link group so that this plot will share the bounds with other plots in the
    /// same group. A plot cannot belong to more than one axis group.
    #[inline]
    pub fn link_axis(mut self, group_id: impl Into<Id>, link_x: bool, link_y: bool) -> Self {
        self.linked_axes = Some((
            group_id.into(),
            Vec2b {
                x: link_x,
                y: link_y,
            },
        ));
        self
    }

    /// Add this plot to a cursor link group so that this plot will share the cursor position with other plots
    /// in the same group. A plot cannot belong to more than one cursor group.
    #[inline]
    pub fn link_cursor(mut self, group_id: impl Into<Id>, link_x: bool, link_y: bool) -> Self {
        self.linked_cursors = Some((
            group_id.into(),
            Vec2b {
                x: link_x,
                y: link_y,
            },
        ));
        self
    }

    /// Round grid positions to full pixels to avoid aliasing. Improves plot appearance but might have an
    /// undesired effect when shifting the plot bounds. Enabled by default.
    #[inline]
    pub fn sharp_grid_lines(mut self, enabled: bool) -> Self {
        self.sharp_grid_lines = enabled;
        self
    }

    /// Resets the plot.
    #[inline]
    pub fn reset(mut self) -> Self {
        self.reset = true;
        self
    }

    /// Set the x axis label of the main X-axis.
    ///
    /// Default: no label.
    #[inline]
    pub fn x_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
        if let Some(main) = self.x_axes.first_mut() {
            main.label = label.into();
        }
        self
    }

    /// Set the y axis label of the main Y-axis.
    ///
    /// Default: no label.
    #[inline]
    pub fn y_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
        if let Some(main) = self.y_axes.first_mut() {
            main.label = label.into();
        }
        self
    }

    /// Set the position of the main X-axis.
    #[inline]
    pub fn x_axis_position(mut self, placement: axis::VPlacement) -> Self {
        if let Some(main) = self.x_axes.first_mut() {
            main.placement = placement.into();
        }
        self
    }

    /// Set the position of the main Y-axis.
    #[inline]
    pub fn y_axis_position(mut self, placement: axis::HPlacement) -> Self {
        if let Some(main) = self.y_axes.first_mut() {
            main.placement = placement.into();
        }
        self
    }

    /// Specify custom formatter for ticks on the main X-axis.
    ///
    /// Arguments of `fmt`:
    /// * raw tick value as `f64`.
    /// * maximum requested number of characters per tick label.
    /// * currently shown range on this axis.
    pub fn x_axis_formatter(
        mut self,
        fmt: impl Fn(f64, usize, &RangeInclusive<f64>) -> String + 'static,
    ) -> Self {
        if let Some(main) = self.x_axes.first_mut() {
            main.formatter = Arc::new(fmt);
        }
        self
    }

    /// Specify custom formatter for ticks on the main Y-axis.
    ///
    /// Arguments of `fmt`:
    /// * raw tick value as `f64`.
    /// * maximum requested number of characters per tick label.
    /// * currently shown range on this axis.
    pub fn y_axis_formatter(
        mut self,
        fmt: impl Fn(f64, usize, &RangeInclusive<f64>) -> String + 'static,
    ) -> Self {
        if let Some(main) = self.y_axes.first_mut() {
            main.formatter = Arc::new(fmt);
        }
        self
    }

    /// Set the main Y-axis-width by number of digits
    ///
    /// The default is 5 digits.
    ///
    /// > Todo: This is experimental. Changing the font size might break this.
    #[inline]
    pub fn y_axis_width(mut self, digits: usize) -> Self {
        if let Some(main) = self.y_axes.first_mut() {
            main.digits = digits;
        }
        self
    }

    /// Set custom configuration for X-axis
    ///
    /// More than one axis may be specified. The first specified axis is considered the main axis.
    #[inline]
    pub fn custom_x_axes(mut self, hints: Vec<AxisHints>) -> Self {
        self.x_axes = hints;
        self
    }

    /// Set custom configuration for left Y-axis
    ///
    /// More than one axis may be specified. The first specified axis is considered the main axis.
    #[inline]
    pub fn custom_y_axes(mut self, hints: Vec<AxisHints>) -> Self {
        self.y_axes = hints;
        self
    }

    /// Interact with and add items to the plot and finally draw it.
    pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> PlotResponse<R> {
        self.show_dyn(ui, Box::new(build_fn))
    }

    fn show_dyn<'a, R>(
        self,
        ui: &mut Ui,
        build_fn: Box<dyn FnOnce(&mut PlotUi) -> R + 'a>,
    ) -> PlotResponse<R> {
        let Self {
            id_source,
            center_axis,
            allow_zoom,
            allow_drag,
            allow_scroll,
            allow_double_click_reset,
            allow_boxed_zoom,
            boxed_zoom_pointer_button,
            default_auto_bounds,
            min_auto_bounds,
            margin_fraction,
            width,
            height,
            min_size,
            data_aspect,
            view_aspect,
            mut show_x,
            mut show_y,
            label_formatter,
            coordinates_formatter,
            x_axes,
            y_axes,
            legend_config,
            reset,
            show_background,
            show_axes,
            show_grid,
            linked_axes,
            linked_cursors,

            clamp_grid,
            grid_spacers,
            sharp_grid_lines,
        } = self;

        // Determine position of widget.
        let pos = ui.available_rect_before_wrap().min;
        // Determine size of widget.
        let size = {
            let width = width
                .unwrap_or_else(|| {
                    if let (Some(height), Some(aspect)) = (height, view_aspect) {
                        height * aspect
                    } else {
                        ui.available_size_before_wrap().x
                    }
                })
                .at_least(min_size.x);

            let height = height
                .unwrap_or_else(|| {
                    if let Some(aspect) = view_aspect {
                        width / aspect
                    } else {
                        ui.available_size_before_wrap().y
                    }
                })
                .at_least(min_size.y);
            vec2(width, height)
        };
        // Determine complete rect of widget.
        let complete_rect = Rect {
            min: pos,
            max: pos + size,
        };
        // Next we want to create this layout.
        // Indices are only examples.
        //
        //  left                     right
        //  +---+---------x----------+   +
        //  |   |      X-axis 3      |
        //  |   +--------------------+    top
        //  |   |      X-axis 2      |
        //  +-+-+--------------------+-+-+
        //  |y|y|                    |y|y|
        //  |-|-|                    |-|-|
        //  |A|A|                    |A|A|
        // y|x|x|    Plot Window     |x|x|
        //  |i|i|                    |i|i|
        //  |s|s|                    |s|s|
        //  |1|0|                    |2|3|
        //  +-+-+--------------------+-+-+
        //      |      X-axis 0      |   |
        //      +--------------------+   | bottom
        //      |      X-axis 1      |   |
        //  +   +--------------------+---+
        //

        let mut plot_rect: Rect = {
            // Calcuclate the space needed for each axis labels.
            let mut margin = Margin::ZERO;
            if show_axes.x {
                for cfg in &x_axes {
                    match cfg.placement {
                        axis::Placement::LeftBottom => {
                            margin.bottom += cfg.thickness(Axis::X);
                        }
                        axis::Placement::RightTop => {
                            margin.top += cfg.thickness(Axis::X);
                        }
                    }
                }
            }
            if show_axes.y {
                for cfg in &y_axes {
                    match cfg.placement {
                        axis::Placement::LeftBottom => {
                            margin.left += cfg.thickness(Axis::Y);
                        }
                        axis::Placement::RightTop => {
                            margin.right += cfg.thickness(Axis::Y);
                        }
                    }
                }
            }

            // determine plot rectangle
            margin.shrink_rect(complete_rect)
        };

        let [mut x_axis_widgets, mut y_axis_widgets] =
            axis_widgets(show_axes, plot_rect, [&x_axes, &y_axes]);

        // If too little space, remove axis widgets
        if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 {
            y_axis_widgets.clear();
            x_axis_widgets.clear();
            plot_rect = complete_rect;
        }

        // Allocate the plot window.
        let response = ui.allocate_rect(plot_rect, Sense::drag());
        let rect = plot_rect;

        // Load or initialize the memory.
        let plot_id = ui.make_persistent_id(id_source);
        ui.ctx().check_for_id_clash(plot_id, rect, "Plot");
        let memory = if reset {
            if let Some((name, _)) = linked_axes.as_ref() {
                ui.data_mut(|data| {
                    let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL);
                    link_groups.0.remove(name);
                });
            };
            None
        } else {
            PlotMemory::load(ui.ctx(), plot_id)
        }
        .unwrap_or_else(|| PlotMemory {
            auto_bounds: default_auto_bounds,
            hovered_entry: None,
            hidden_items: Default::default(),
            last_plot_transform: PlotTransform::new(
                rect,
                min_auto_bounds,
                center_axis.x,
                center_axis.y,
            ),
            last_click_pos_for_zoom: None,
        });

        let PlotMemory {
            mut auto_bounds,
            mut hovered_entry,
            mut hidden_items,
            last_plot_transform,
            mut last_click_pos_for_zoom,
        } = memory;

        // Call the plot build function.
        let mut plot_ui = PlotUi {
            items: Vec::new(),
            next_auto_color_idx: 0,
            last_plot_transform,
            last_auto_bounds: auto_bounds,
            response,
            bounds_modifications: Vec::new(),
            ctx: ui.ctx().clone(),
        };
        let inner = build_fn(&mut plot_ui);
        let PlotUi {
            mut items,
            mut response,
            last_plot_transform,
            bounds_modifications,
            ..
        } = plot_ui;

        // Background
        if show_background {
            ui.painter()
                .with_clip_rect(rect)
                .add(epaint::RectShape::new(
                    rect,
                    Rounding::same(2.0),
                    ui.visuals().extreme_bg_color,
                    ui.visuals().widgets.noninteractive.bg_stroke,
                ));
        }

        // --- Legend ---
        let legend = legend_config
            .and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items));
        // Don't show hover cursor when hovering over legend.
        if hovered_entry.is_some() {
            show_x = false;
            show_y = false;
        }
        // Remove the deselected items.
        items.retain(|item| !hidden_items.contains(item.name()));
        // Highlight the hovered items.
        if let Some(hovered_name) = &hovered_entry {
            items
                .iter_mut()
                .filter(|entry| entry.name() == hovered_name)
                .for_each(|entry| entry.highlight());
        }
        // Move highlighted items to front.
        items.sort_by_key(|item| item.highlighted());

        // --- Bound computation ---
        let mut bounds = *last_plot_transform.bounds();

        // Find the cursors from other plots we need to draw
        let draw_cursors: Vec<Cursor> = if let Some((id, _)) = linked_cursors.as_ref() {
            ui.data_mut(|data| {
                let frames: &mut CursorLinkGroups = data.get_temp_mut_or_default(Id::NULL);
                let cursors = frames.0.entry(*id).or_default();

                // Look for our previous frame
                let index = cursors
                    .iter()
                    .enumerate()
                    .find(|(_, frame)| frame.id == plot_id)
                    .map(|(i, _)| i);

                // Remove our previous frame and all older frames as these are no longer displayed. This avoids
                // unbounded growth, as we add an entry each time we draw a plot.
                index.map(|index| cursors.drain(0..=index));

                // Gather all cursors of the remaining frames. This will be all the cursors of the
                // other plots in the group. We want to draw these in the current plot too.
                cursors
                    .iter()
                    .flat_map(|frame| frame.cursors.iter().copied())
                    .collect()
            })
        } else {
            Vec::new()
        };

        // Transfer the bounds from a link group.
        if let Some((id, axes)) = linked_axes.as_ref() {
            ui.data_mut(|data| {
                let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL);
                if let Some(linked_bounds) = link_groups.0.get(id) {
                    if axes.x {
                        bounds.set_x(&linked_bounds.bounds);
                        auto_bounds.x = linked_bounds.auto_bounds.x;
                    }
                    if axes.y {
                        bounds.set_y(&linked_bounds.bounds);
                        auto_bounds.y = linked_bounds.auto_bounds.y;
                    }
                };
            });
        };

        // Allow double-clicking to reset to the initial bounds.
        if allow_double_click_reset && response.double_clicked() {
            auto_bounds = true.into();
        }

        // Apply bounds modifications.
        for modification in bounds_modifications {
            match modification {
                BoundsModification::Set(new_bounds) => {
                    bounds = new_bounds;
                    auto_bounds = false.into();
                }
                BoundsModification::Translate(delta) => {
                    bounds.translate(delta);
                    auto_bounds = false.into();
                }
                BoundsModification::AutoBounds(new_auto_bounds) => auto_bounds = new_auto_bounds,
                BoundsModification::Zoom(zoom_factor, center) => {
                    bounds.zoom(zoom_factor, center);
                    auto_bounds = false.into();
                }
            }
        }

        // Reset bounds to initial bounds if they haven't been modified.
        if auto_bounds.x {
            bounds.set_x(&min_auto_bounds);
        }
        if auto_bounds.y {
            bounds.set_y(&min_auto_bounds);
        }

        let auto_x = auto_bounds.x && (!min_auto_bounds.is_valid_x() || default_auto_bounds.x);
        let auto_y = auto_bounds.y && (!min_auto_bounds.is_valid_y() || default_auto_bounds.y);

        // Set bounds automatically based on content.
        if auto_x || auto_y {
            for item in &items {
                let item_bounds = item.bounds();
                if auto_x {
                    bounds.merge_x(&item_bounds);
                }
                if auto_y {
                    bounds.merge_y(&item_bounds);
                }
            }

            if auto_x {
                bounds.add_relative_margin_x(margin_fraction);
            }

            if auto_y {
                bounds.add_relative_margin_y(margin_fraction);
            }
        }

        let mut transform = PlotTransform::new(rect, bounds, center_axis.x, center_axis.y);

        // Enforce aspect ratio
        if let Some(data_aspect) = data_aspect {
            if let Some((_, linked_axes)) = &linked_axes {
                let change_x = linked_axes.y && !linked_axes.x;
                transform.set_aspect_by_changing_axis(data_aspect as f64, change_x);
            } else if default_auto_bounds.any() {
                transform.set_aspect_by_expanding(data_aspect as f64);
            } else {
                transform.set_aspect_by_changing_axis(data_aspect as f64, false);
            }
        }

        // Dragging
        if allow_drag.any() && response.dragged_by(PointerButton::Primary) {
            response = response.on_hover_cursor(CursorIcon::Grabbing);
            let mut delta = -response.drag_delta();
            if !allow_drag.x {
                delta.x = 0.0;
            }
            if !allow_drag.y {
                delta.y = 0.0;
            }
            transform.translate_bounds(delta);
            auto_bounds = !allow_drag;
        }

        // Zooming
        let mut boxed_zoom_rect = None;
        if allow_boxed_zoom {
            // Save last click to allow boxed zooming
            if response.drag_started() && response.dragged_by(boxed_zoom_pointer_button) {
                // it would be best for egui that input has a memory of the last click pos because it's a common pattern
                last_click_pos_for_zoom = response.hover_pos();
            }
            let box_start_pos = last_click_pos_for_zoom;
            let box_end_pos = response.hover_pos();
            if let (Some(box_start_pos), Some(box_end_pos)) = (box_start_pos, box_end_pos) {
                // while dragging prepare a Shape and draw it later on top of the plot
                if response.dragged_by(boxed_zoom_pointer_button) {
                    response = response.on_hover_cursor(CursorIcon::ZoomIn);
                    let rect = epaint::Rect::from_two_pos(box_start_pos, box_end_pos);
                    boxed_zoom_rect = Some((
                        epaint::RectShape::stroke(
                            rect,
                            0.0,
                            epaint::Stroke::new(4., Color32::DARK_BLUE),
                        ), // Outer stroke
                        epaint::RectShape::stroke(
                            rect,
                            0.0,
                            epaint::Stroke::new(2., Color32::WHITE),
                        ), // Inner stroke
                    ));
                }
                // when the click is release perform the zoom
                if response.drag_released() {
                    let box_start_pos = transform.value_from_position(box_start_pos);
                    let box_end_pos = transform.value_from_position(box_end_pos);
                    let new_bounds = PlotBounds {
                        min: [
                            box_start_pos.x.min(box_end_pos.x),
                            box_start_pos.y.min(box_end_pos.y),
                        ],
                        max: [
                            box_start_pos.x.max(box_end_pos.x),
                            box_start_pos.y.max(box_end_pos.y),
                        ],
                    };
                    if new_bounds.is_valid() {
                        transform.set_bounds(new_bounds);
                        auto_bounds = false.into();
                    }
                    // reset the boxed zoom state
                    last_click_pos_for_zoom = None;
                }
            }
        }

        let hover_pos = response.hover_pos();
        if let Some(hover_pos) = hover_pos {
            if allow_zoom.any() {
                let mut zoom_factor = if data_aspect.is_some() {
                    Vec2::splat(ui.input(|i| i.zoom_delta()))
                } else {
                    ui.input(|i| i.zoom_delta_2d())
                };
                if !allow_zoom.x {
                    zoom_factor.x = 1.0;
                }
                if !allow_zoom.y {
                    zoom_factor.y = 1.0;
                }
                if zoom_factor != Vec2::splat(1.0) {
                    transform.zoom(zoom_factor, hover_pos);
                    auto_bounds = !allow_zoom;
                }
            }
            if allow_scroll {
                let scroll_delta = ui.input(|i| i.scroll_delta);
                if scroll_delta != Vec2::ZERO {
                    transform.translate_bounds(-scroll_delta);
                    auto_bounds = false.into();
                }
            }
        }

        // --- transform initialized

        // Add legend widgets to plot
        let bounds = transform.bounds();
        let x_axis_range = bounds.range_x();
        let x_steps = Arc::new({
            let input = GridInput {
                bounds: (bounds.min[0], bounds.max[0]),
                base_step_size: transform.dvalue_dpos()[0] * MIN_LINE_SPACING_IN_POINTS * 2.0,
            };
            (grid_spacers[0])(input)
        });
        let y_axis_range = bounds.range_y();
        let y_steps = Arc::new({
            let input = GridInput {
                bounds: (bounds.min[1], bounds.max[1]),
                base_step_size: transform.dvalue_dpos()[1] * MIN_LINE_SPACING_IN_POINTS * 2.0,
            };
            (grid_spacers[1])(input)
        });
        for mut widget in x_axis_widgets {
            widget.range = x_axis_range.clone();
            widget.transform = Some(transform);
            widget.steps = x_steps.clone();
            widget.ui(ui, Axis::X);
        }
        for mut widget in y_axis_widgets {
            widget.range = y_axis_range.clone();
            widget.transform = Some(transform);
            widget.steps = y_steps.clone();
            widget.ui(ui, Axis::Y);
        }

        // Initialize values from functions.
        for item in &mut items {
            item.initialize(transform.bounds().range_x());
        }

        let prepared = PreparedPlot {
            items,
            show_x,
            show_y,
            label_formatter,
            coordinates_formatter,
            show_grid,
            transform,
            draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x),
            draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y),
            draw_cursors,
            grid_spacers,
            sharp_grid_lines,
            clamp_grid,
        };

        let plot_cursors = prepared.ui(ui, &response);

        if let Some(boxed_zoom_rect) = boxed_zoom_rect {
            ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.0);
            ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.1);
        }

        if let Some(mut legend) = legend {
            ui.add(&mut legend);
            hidden_items = legend.hidden_items();
            hovered_entry = legend.hovered_entry_name();
        }

        if let Some((id, _)) = linked_cursors.as_ref() {
            // Push the frame we just drew to the list of frames
            ui.data_mut(|data| {
                let frames: &mut CursorLinkGroups = data.get_temp_mut_or_default(Id::NULL);
                let cursors = frames.0.entry(*id).or_default();
                cursors.push(PlotFrameCursors {
                    id: plot_id,
                    cursors: plot_cursors,
                });
            });
        }

        if let Some((id, _)) = linked_axes.as_ref() {
            // Save the linked bounds.
            ui.data_mut(|data| {
                let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL);
                link_groups.0.insert(
                    *id,
                    LinkedBounds {
                        bounds: *transform.bounds(),
                        auto_bounds,
                    },
                );
            });
        }

        let memory = PlotMemory {
            auto_bounds,
            hovered_entry,
            hidden_items,
            last_plot_transform: transform,
            last_click_pos_for_zoom,
        };
        memory.store(ui.ctx(), plot_id);

        let response = if show_x || show_y {
            response.on_hover_cursor(CursorIcon::Crosshair)
        } else {
            response
        };
        ui.advance_cursor_after_rect(complete_rect);
        PlotResponse {
            inner,
            response,
            transform,
        }
    }
}

fn axis_widgets(
    show_axes: Vec2b,
    plot_rect: Rect,
    [x_axes, y_axes]: [&[AxisHints]; 2],
) -> [Vec<AxisWidget>; 2] {
    let mut x_axis_widgets = Vec::<AxisWidget>::new();
    let mut y_axis_widgets = Vec::<AxisWidget>::new();

    // Widget count per border of plot in order left, top, right, bottom
    struct NumWidgets {
        left: usize,
        top: usize,
        right: usize,
        bottom: usize,
    }
    let mut num_widgets = NumWidgets {
        left: 0,
        top: 0,
        right: 0,
        bottom: 0,
    };
    if show_axes.x {
        for cfg in x_axes {
            let size_y = Vec2::new(0.0, cfg.thickness(Axis::X));
            let rect = match cfg.placement {
                axis::Placement::LeftBottom => {
                    let off = num_widgets.bottom as f32;
                    num_widgets.bottom += 1;
                    Rect {
                        min: plot_rect.left_bottom() + size_y * off,
                        max: plot_rect.right_bottom() + size_y * (off + 1.0),
                    }
                }
                axis::Placement::RightTop => {
                    let off = num_widgets.top as f32;
                    num_widgets.top += 1;
                    Rect {
                        min: plot_rect.left_top() - size_y * (off + 1.0),
                        max: plot_rect.right_top() - size_y * off,
                    }
                }
            };
            x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
        }
    }
    if show_axes.y {
        for cfg in y_axes {
            let size_x = Vec2::new(cfg.thickness(Axis::Y), 0.0);
            let rect = match cfg.placement {
                axis::Placement::LeftBottom => {
                    let off = num_widgets.left as f32;
                    num_widgets.left += 1;
                    Rect {
                        min: plot_rect.left_top() - size_x * (off + 1.0),
                        max: plot_rect.left_bottom() - size_x * off,
                    }
                }
                axis::Placement::RightTop => {
                    let off = num_widgets.right as f32;
                    num_widgets.right += 1;
                    Rect {
                        min: plot_rect.right_top() + size_x * off,
                        max: plot_rect.right_bottom() + size_x * (off + 1.0),
                    }
                }
            };
            y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
        }
    }

    [x_axis_widgets, y_axis_widgets]
}

/// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply
/// them at the right time, as other modifications need to happen first.
enum BoundsModification {
    Set(PlotBounds),
    Translate(Vec2),
    AutoBounds(Vec2b),
    Zoom(Vec2, PlotPoint),
}

/// Provides methods to interact with a plot while building it. It is the single argument of the closure
/// provided to [`Plot::show`]. See [`Plot`] for an example of how to use it.
pub struct PlotUi {
    items: Vec<Box<dyn PlotItem>>,
    next_auto_color_idx: usize,
    last_plot_transform: PlotTransform,
    last_auto_bounds: Vec2b,
    response: Response,
    bounds_modifications: Vec<BoundsModification>,
    ctx: Context,
}

impl PlotUi {
    fn auto_color(&mut self) -> Color32 {
        let i = self.next_auto_color_idx;
        self.next_auto_color_idx += 1;
        let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
        let h = i as f32 * golden_ratio;
        Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO(emilk): OkLab or some other perspective color space
    }

    pub fn ctx(&self) -> &Context {
        &self.ctx
    }

    /// The plot bounds as they were in the last frame. If called on the first frame and the bounds were not
    /// further specified in the plot builder, this will return bounds centered on the origin. The bounds do
    /// not change until the plot is drawn.
    pub fn plot_bounds(&self) -> PlotBounds {
        *self.last_plot_transform.bounds()
    }

    /// Set the plot bounds. Can be useful for implementing alternative plot navigation methods.
    pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) {
        self.bounds_modifications
            .push(BoundsModification::Set(plot_bounds));
    }

    /// Move the plot bounds. Can be useful for implementing alternative plot navigation methods.
    pub fn translate_bounds(&mut self, delta_pos: Vec2) {
        self.bounds_modifications
            .push(BoundsModification::Translate(delta_pos));
    }

    /// Whether the plot axes were in auto-bounds mode in the last frame. If called on the first
    /// frame, this is the [`Plot`]'s default auto-bounds mode.
    pub fn auto_bounds(&self) -> Vec2b {
        self.last_auto_bounds
    }

    /// Set the auto-bounds mode for the plot axes.
    pub fn set_auto_bounds(&mut self, auto_bounds: Vec2b) {
        self.bounds_modifications
            .push(BoundsModification::AutoBounds(auto_bounds));
    }

    /// Can be used to check if the plot was hovered or clicked.
    pub fn response(&self) -> &Response {
        &self.response
    }

    /// Scale the plot bounds around a position in screen coordinates.
    ///
    /// Can be useful for implementing alternative plot navigation methods.
    ///
    /// The plot bounds are divided by `zoom_factor`, therefore:
    /// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data.
    /// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail.
    pub fn zoom_bounds(&mut self, zoom_factor: Vec2, center: PlotPoint) {
        self.bounds_modifications
            .push(BoundsModification::Zoom(zoom_factor, center));
    }

    /// Scale the plot bounds around the hovered position, if any.
    ///
    /// Can be useful for implementing alternative plot navigation methods.
    ///
    /// The plot bounds are divided by `zoom_factor`, therefore:
    /// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data.
    /// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail.
    pub fn zoom_bounds_around_hovered(&mut self, zoom_factor: Vec2) {
        if let Some(hover_pos) = self.pointer_coordinate() {
            self.zoom_bounds(zoom_factor, hover_pos);
        }
    }

    /// The pointer position in plot coordinates. Independent of whether the pointer is in the plot area.
    pub fn pointer_coordinate(&self) -> Option<PlotPoint> {
        // We need to subtract the drag delta to keep in sync with the frame-delayed screen transform:
        let last_pos = self.ctx().input(|i| i.pointer.latest_pos())? - self.response.drag_delta();
        let value = self.plot_from_screen(last_pos);
        Some(value)
    }

    /// The pointer drag delta in plot coordinates.
    pub fn pointer_coordinate_drag_delta(&self) -> Vec2 {
        let delta = self.response.drag_delta();
        let dp_dv = self.last_plot_transform.dpos_dvalue();
        Vec2::new(delta.x / dp_dv[0] as f32, delta.y / dp_dv[1] as f32)
    }

    /// Read the transform between plot coordinates and screen coordinates.
    pub fn transform(&self) -> &PlotTransform {
        &self.last_plot_transform
    }

    /// Transform the plot coordinates to screen coordinates.
    pub fn screen_from_plot(&self, position: PlotPoint) -> Pos2 {
        self.last_plot_transform.position_from_point(&position)
    }

    /// Transform the screen coordinates to plot coordinates.
    pub fn plot_from_screen(&self, position: Pos2) -> PlotPoint {
        self.last_plot_transform.value_from_position(position)
    }

    /// Add a data line.
    pub fn line(&mut self, mut line: Line) {
        if line.series.is_empty() {
            return;
        };

        // Give the stroke an automatic color if no color has been assigned.
        if line.stroke.color == Color32::TRANSPARENT {
            line.stroke.color = self.auto_color();
        }
        self.items.push(Box::new(line));
    }

    /// Add a polygon. The polygon has to be convex.
    pub fn polygon(&mut self, mut polygon: Polygon) {
        if polygon.series.is_empty() {
            return;
        };

        // Give the stroke an automatic color if no color has been assigned.
        if polygon.stroke.color == Color32::TRANSPARENT {
            polygon.stroke.color = self.auto_color();
        }
        self.items.push(Box::new(polygon));
    }

    /// Add a text.
    pub fn text(&mut self, text: Text) {
        if text.text.is_empty() {
            return;
        };

        self.items.push(Box::new(text));
    }

    /// Add data points.
    pub fn points(&mut self, mut points: Points) {
        if points.series.is_empty() {
            return;
        };

        // Give the points an automatic color if no color has been assigned.
        if points.color == Color32::TRANSPARENT {
            points.color = self.auto_color();
        }
        self.items.push(Box::new(points));
    }

    /// Add arrows.
    pub fn arrows(&mut self, mut arrows: Arrows) {
        if arrows.origins.is_empty() || arrows.tips.is_empty() {
            return;
        };

        // Give the arrows an automatic color if no color has been assigned.
        if arrows.color == Color32::TRANSPARENT {
            arrows.color = self.auto_color();
        }
        self.items.push(Box::new(arrows));
    }

    /// Add an image.
    pub fn image(&mut self, image: PlotImage) {
        self.items.push(Box::new(image));
    }

    /// Add a horizontal line.
    /// Can be useful e.g. to show min/max bounds or similar.
    /// Always fills the full width of the plot.
    pub fn hline(&mut self, mut hline: HLine) {
        if hline.stroke.color == Color32::TRANSPARENT {
            hline.stroke.color = self.auto_color();
        }
        self.items.push(Box::new(hline));
    }

    /// Add a vertical line.
    /// Can be useful e.g. to show min/max bounds or similar.
    /// Always fills the full height of the plot.
    pub fn vline(&mut self, mut vline: VLine) {
        if vline.stroke.color == Color32::TRANSPARENT {
            vline.stroke.color = self.auto_color();
        }
        self.items.push(Box::new(vline));
    }

    /// Add a box plot diagram.
    pub fn box_plot(&mut self, mut box_plot: BoxPlot) {
        if box_plot.boxes.is_empty() {
            return;
        }

        // Give the elements an automatic color if no color has been assigned.
        if box_plot.default_color == Color32::TRANSPARENT {
            box_plot = box_plot.color(self.auto_color());
        }
        self.items.push(Box::new(box_plot));
    }

    /// Add a bar chart.
    pub fn bar_chart(&mut self, mut chart: BarChart) {
        if chart.bars.is_empty() {
            return;
        }

        // Give the elements an automatic color if no color has been assigned.
        if chart.default_color == Color32::TRANSPARENT {
            chart = chart.color(self.auto_color());
        }
        self.items.push(Box::new(chart));
    }
}

// ----------------------------------------------------------------------------
// Grid

/// Input for "grid spacer" functions.
///
/// See [`Plot::x_grid_spacer()`] and [`Plot::y_grid_spacer()`].
pub struct GridInput {
    /// Min/max of the visible data range (the values at the two edges of the plot,
    /// for the current axis).
    pub bounds: (f64, f64),

    /// Recommended (but not required) lower-bound on the step size returned by custom grid spacers.
    ///
    /// Computed as the ratio between the diagram's bounds (in plot coordinates) and the viewport
    /// (in frame/window coordinates), scaled up to represent the minimal possible step.
    pub base_step_size: f64,
}

/// One mark (horizontal or vertical line) in the background grid of a plot.
#[derive(Debug, Clone, Copy)]
pub struct GridMark {
    /// X or Y value in the plot.
    pub value: f64,

    /// The (approximate) distance to the next value of same thickness.
    ///
    /// Determines how thick the grid line is painted. It's not important that `step_size`
    /// matches the difference between two `value`s precisely, but rather that grid marks of
    /// same thickness have same `step_size`. For example, months can have a different number
    /// of days, but consistently using a `step_size` of 30 days is a valid approximation.
    pub step_size: f64,
}

/// Recursively splits the grid into `base` subdivisions (e.g. 100, 10, 1).
///
/// The logarithmic base, expressing how many times each grid unit is subdivided.
/// 10 is a typical value, others are possible though.
pub fn log_grid_spacer(log_base: i64) -> GridSpacer {
    let log_base = log_base as f64;
    let step_sizes = move |input: GridInput| -> Vec<GridMark> {
        // The distance between two of the thinnest grid lines is "rounded" up
        // to the next-bigger power of base
        let smallest_visible_unit = next_power(input.base_step_size, log_base);

        let step_sizes = [
            smallest_visible_unit,
            smallest_visible_unit * log_base,
            smallest_visible_unit * log_base * log_base,
        ];

        generate_marks(step_sizes, input.bounds)
    };

    Box::new(step_sizes)
}

/// Splits the grid into uniform-sized spacings (e.g. 100, 25, 1).
///
/// This function should return 3 positive step sizes, designating where the lines in the grid are drawn.
/// Lines are thicker for larger step sizes. Ordering of returned value is irrelevant.
///
/// Why only 3 step sizes? Three is the number of different line thicknesses that egui typically uses in the grid.
/// Ideally, those 3 are not hardcoded values, but depend on the visible range (accessible through `GridInput`).
pub fn uniform_grid_spacer(spacer: impl Fn(GridInput) -> [f64; 3] + 'static) -> GridSpacer {
    let get_marks = move |input: GridInput| -> Vec<GridMark> {
        let bounds = input.bounds;
        let step_sizes = spacer(input);
        generate_marks(step_sizes, bounds)
    };

    Box::new(get_marks)
}

// ----------------------------------------------------------------------------

struct PreparedPlot {
    items: Vec<Box<dyn PlotItem>>,
    show_x: bool,
    show_y: bool,
    label_formatter: LabelFormatter,
    coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
    // axis_formatters: [AxisFormatter; 2],
    transform: PlotTransform,
    show_grid: Vec2b,
    grid_spacers: [GridSpacer; 2],
    draw_cursor_x: bool,
    draw_cursor_y: bool,
    draw_cursors: Vec<Cursor>,

    sharp_grid_lines: bool,
    clamp_grid: bool,
}

impl PreparedPlot {
    fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
        let mut axes_shapes = Vec::new();

        if self.show_grid.x {
            self.paint_grid(ui, &mut axes_shapes, Axis::X);
        }
        if self.show_grid.y {
            self.paint_grid(ui, &mut axes_shapes, Axis::Y);
        }

        // Sort the axes by strength so that those with higher strength are drawn in front.
        axes_shapes.sort_by(|(_, strength1), (_, strength2)| strength1.total_cmp(strength2));

        let mut shapes = axes_shapes.into_iter().map(|(shape, _)| shape).collect();

        let transform = &self.transform;

        let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
        plot_ui.set_clip_rect(*transform.frame());
        for item in &self.items {
            item.shapes(&mut plot_ui, transform, &mut shapes);
        }

        let hover_pos = response.hover_pos();
        let cursors = if let Some(pointer) = hover_pos {
            self.hover(ui, pointer, &mut shapes)
        } else {
            Vec::new()
        };

        // Draw cursors
        let line_color = rulers_color(ui);

        let mut draw_cursor = |cursors: &Vec<Cursor>, always| {
            for &cursor in cursors {
                match cursor {
                    Cursor::Horizontal { y } => {
                        if self.draw_cursor_y || always {
                            shapes.push(horizontal_line(
                                transform.position_from_point(&PlotPoint::new(0.0, y)),
                                &self.transform,
                                line_color,
                            ));
                        }
                    }
                    Cursor::Vertical { x } => {
                        if self.draw_cursor_x || always {
                            shapes.push(vertical_line(
                                transform.position_from_point(&PlotPoint::new(x, 0.0)),
                                &self.transform,
                                line_color,
                            ));
                        }
                    }
                }
            }
        };

        draw_cursor(&self.draw_cursors, false);
        draw_cursor(&cursors, true);

        let painter = ui.painter().with_clip_rect(*transform.frame());
        painter.extend(shapes);

        if let Some((corner, formatter)) = self.coordinates_formatter.as_ref() {
            let hover_pos = response.hover_pos();
            if let Some(pointer) = hover_pos {
                let font_id = TextStyle::Monospace.resolve(ui.style());
                let coordinate = transform.value_from_position(pointer);
                let text = formatter.format(&coordinate, transform.bounds());
                let padded_frame = transform.frame().shrink(4.0);
                let (anchor, position) = match corner {
                    Corner::LeftTop => (Align2::LEFT_TOP, padded_frame.left_top()),
                    Corner::RightTop => (Align2::RIGHT_TOP, padded_frame.right_top()),
                    Corner::LeftBottom => (Align2::LEFT_BOTTOM, padded_frame.left_bottom()),
                    Corner::RightBottom => (Align2::RIGHT_BOTTOM, padded_frame.right_bottom()),
                };
                painter.text(position, anchor, text, font_id, ui.visuals().text_color());
            }
        }

        cursors
    }

    fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis) {
        #![allow(clippy::collapsible_else_if)]
        let Self {
            transform,
            // axis_formatters,
            grid_spacers,
            clamp_grid,
            ..
        } = self;

        let iaxis = usize::from(axis);

        // Where on the cross-dimension to show the label values
        let bounds = transform.bounds();
        let value_cross = 0.0_f64.clamp(bounds.min[1 - iaxis], bounds.max[1 - iaxis]);

        let input = GridInput {
            bounds: (bounds.min[iaxis], bounds.max[iaxis]),
            base_step_size: transform.dvalue_dpos()[iaxis] * MIN_LINE_SPACING_IN_POINTS,
        };
        let steps = (grid_spacers[iaxis])(input);

        let clamp_range = clamp_grid.then(|| {
            let mut tight_bounds = PlotBounds::NOTHING;
            for item in &self.items {
                let item_bounds = item.bounds();
                tight_bounds.merge_x(&item_bounds);
                tight_bounds.merge_y(&item_bounds);
            }
            tight_bounds
        });

        for step in steps {
            let value_main = step.value;

            if let Some(clamp_range) = clamp_range {
                match axis {
                    Axis::X => {
                        if !clamp_range.range_x().contains(&value_main) {
                            continue;
                        };
                    }
                    Axis::Y => {
                        if !clamp_range.range_y().contains(&value_main) {
                            continue;
                        };
                    }
                }
            }

            let value = match axis {
                Axis::X => PlotPoint::new(value_main, value_cross),
                Axis::Y => PlotPoint::new(value_cross, value_main),
            };

            let pos_in_gui = transform.position_from_point(&value);
            let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32;

            if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
                let line_strength = remap_clamp(
                    spacing_in_points,
                    MIN_LINE_SPACING_IN_POINTS as f32..=300.0,
                    0.0..=1.0,
                );

                let line_color = color_from_strength(ui, line_strength);

                let mut p0 = pos_in_gui;
                let mut p1 = pos_in_gui;
                p0[1 - iaxis] = transform.frame().min[1 - iaxis];
                p1[1 - iaxis] = transform.frame().max[1 - iaxis];

                if let Some(clamp_range) = clamp_range {
                    match axis {
                        Axis::X => {
                            p0.y = transform.position_from_point_y(clamp_range.min[1]);
                            p1.y = transform.position_from_point_y(clamp_range.max[1]);
                        }
                        Axis::Y => {
                            p0.x = transform.position_from_point_x(clamp_range.min[0]);
                            p1.x = transform.position_from_point_x(clamp_range.max[0]);
                        }
                    }
                }

                if self.sharp_grid_lines {
                    // Round to avoid aliasing
                    p0 = ui.painter().round_pos_to_pixels(p0);
                    p1 = ui.painter().round_pos_to_pixels(p1);
                }

                shapes.push((
                    Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)),
                    line_strength,
                ));
            }
        }
    }

    fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec<Shape>) -> Vec<Cursor> {
        let Self {
            transform,
            show_x,
            show_y,
            label_formatter,
            items,
            ..
        } = self;

        if !show_x && !show_y {
            return Vec::new();
        }

        let interact_radius_sq = (16.0_f32).powi(2);

        let candidates = items.iter().filter_map(|item| {
            let item = &**item;
            let closest = item.find_closest(pointer, transform);

            Some(item).zip(closest)
        });

        let closest = candidates
            .min_by_key(|(_, elem)| elem.dist_sq.ord())
            .filter(|(_, elem)| elem.dist_sq <= interact_radius_sq);

        let mut cursors = Vec::new();

        let plot = items::PlotConfig {
            ui,
            transform,
            show_x: *show_x,
            show_y: *show_y,
        };

        if let Some((item, elem)) = closest {
            item.on_hover(elem, shapes, &mut cursors, &plot, label_formatter);
        } else {
            let value = transform.value_from_position(pointer);
            items::rulers_at_value(
                pointer,
                value,
                "",
                &plot,
                shapes,
                &mut cursors,
                label_formatter,
            );
        }

        cursors
    }
}

/// Returns next bigger power in given base
/// e.g.
/// ```ignore
/// use egui_plot::next_power;
/// assert_eq!(next_power(0.01, 10.0), 0.01);
/// assert_eq!(next_power(0.02, 10.0), 0.1);
/// assert_eq!(next_power(0.2,  10.0), 1);
/// ```
fn next_power(value: f64, base: f64) -> f64 {
    assert_ne!(value, 0.0); // can be negative (typical for Y axis)
    base.powi(value.abs().log(base).ceil() as i32)
}

/// Fill in all values between [min, max] which are a multiple of `step_size`
fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec<GridMark> {
    let mut steps = vec![];
    fill_marks_between(&mut steps, step_sizes[0], bounds);
    fill_marks_between(&mut steps, step_sizes[1], bounds);
    fill_marks_between(&mut steps, step_sizes[2], bounds);
    steps
}

/// Fill in all values between [min, max] which are a multiple of `step_size`
fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64, f64)) {
    assert!(max > min);
    let first = (min / step_size).ceil() as i64;
    let last = (max / step_size).ceil() as i64;

    let marks_iter = (first..last).map(|i| {
        let value = (i as f64) * step_size;
        GridMark { value, step_size }
    });
    out.extend(marks_iter);
}

/// Helper for formatting a number so that we always show at least a few decimals,
/// unless it is an integer, in which case we never show any decimals.
pub fn format_number(number: f64, num_decimals: usize) -> String {
    let is_integral = number as i64 as f64 == number;
    if is_integral {
        // perfect integer - show it as such:
        format!("{number:.0}")
    } else {
        // make sure we tell the user it is not an integer by always showing a decimal or two:
        format!("{:.*}", num_decimals.at_least(1), number)
    }
}

/// Determine a color from a 0-1 strength value.
pub fn color_from_strength(ui: &Ui, strength: f32) -> Color32 {
    let bg = ui.visuals().extreme_bg_color;
    let fg = ui.visuals().widgets.open.fg_stroke.color;
    let mix = 0.5 * strength.sqrt();
    Color32::from_rgb(
        lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
        lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
        lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
    )
}
