// @reference: https://d3js.org/d3-shape/area

use gpui::{Background, Bounds, Path, PathBuilder, Pixels, Point, Window, px};

use crate::plot::{StrokeStyle, origin_point};

#[allow(clippy::type_complexity)]
pub struct Area<T> {
    data: Vec<T>,
    x: Box<dyn Fn(&T) -> Option<f32>>,
    y0: Option<f32>,
    y1: Box<dyn Fn(&T) -> Option<f32>>,
    fill: Background,
    stroke: Background,
    stroke_style: StrokeStyle,
}

impl<T> Default for Area<T> {
    fn default() -> Self {
        Self {
            data: Vec::new(),
            x: Box::new(|_| None),
            y0: None,
            y1: Box::new(|_| None),
            fill: Default::default(),
            stroke: Default::default(),
            stroke_style: Default::default(),
        }
    }
}

impl<T> Area<T> {
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the data of the Area.
    pub fn data<I>(mut self, data: I) -> Self
    where
        I: IntoIterator<Item = T>,
    {
        self.data = data.into_iter().collect();
        self
    }

    /// Set the x of the Area.
    pub fn x<F>(mut self, x: F) -> Self
    where
        F: Fn(&T) -> Option<f32> + 'static,
    {
        self.x = Box::new(x);
        self
    }

    /// Set the y0 of the Area.
    pub fn y0(mut self, y0: f32) -> Self {
        self.y0 = Some(y0);
        self
    }

    /// Set the y1 of the Area.
    pub fn y1<F>(mut self, y1: F) -> Self
    where
        F: Fn(&T) -> Option<f32> + 'static,
    {
        self.y1 = Box::new(y1);
        self
    }

    /// Set the fill color of the Area.
    pub fn fill(mut self, fill: impl Into<Background>) -> Self {
        self.fill = fill.into();
        self
    }

    /// Set the stroke color of the Area.
    pub fn stroke(mut self, stroke: impl Into<Background>) -> Self {
        self.stroke = stroke.into();
        self
    }

    /// Set the stroke style of the Area.
    pub fn stroke_style(mut self, stroke_style: StrokeStyle) -> Self {
        self.stroke_style = stroke_style;
        self
    }

    fn path(&self, bounds: &Bounds<Pixels>) -> (Option<Path<Pixels>>, Option<Path<Pixels>>) {
        let origin = bounds.origin;
        let mut area_builder = PathBuilder::fill();
        let mut line_builder = PathBuilder::stroke(px(1.));

        let mut points = vec![];

        let mut first_x_tick = None;
        let mut last_x_tick = None;
        for (index, v) in self.data.iter().enumerate() {
            if index == 0 {
                first_x_tick = (self.x)(v);
            }
            if index == self.data.len() - 1 {
                last_x_tick = (self.x)(v);
            }
            let x_tick = (self.x)(v);
            let y_tick = (self.y1)(v);

            if let (Some(x), Some(y)) = (x_tick, y_tick) {
                let pos = origin_point(px(x), px(y), origin);

                points.push(pos);
            }
        }

        if points.is_empty() {
            return (None, None);
        }

        if points.len() == 1 {
            area_builder.move_to(points[0]);
            line_builder.move_to(points[0]);
            return (area_builder.build().ok(), line_builder.build().ok());
        }

        match self.stroke_style {
            StrokeStyle::Natural => {
                area_builder.move_to(points[0]);
                line_builder.move_to(points[0]);
                let n = points.len();
                for i in 0..n - 1 {
                    let p0 = if i == 0 { points[0] } else { points[i - 1] };
                    let p1 = points[i];
                    let p2 = points[i + 1];
                    let p3 = if i + 2 < n {
                        points[i + 2]
                    } else {
                        points[n - 1]
                    };

                    // Catmull-Rom to Bezier
                    let c1 = Point::new(p1.x + (p2.x - p0.x) / 6.0, p1.y + (p2.y - p0.y) / 6.0);
                    let c2 = Point::new(p2.x - (p3.x - p1.x) / 6.0, p2.y - (p3.y - p1.y) / 6.0);

                    area_builder.cubic_bezier_to(p2, c1, c2);
                    line_builder.cubic_bezier_to(p2, c1, c2);
                }
            }
            StrokeStyle::Linear => {
                area_builder.move_to(points[0]);
                line_builder.move_to(points[0]);
                for p in &points[1..] {
                    area_builder.line_to(*p);
                    line_builder.line_to(*p);
                }
            }
            StrokeStyle::StepAfter => {
                area_builder.move_to(points[0]);
                line_builder.move_to(points[0]);
                for (i, p) in points.windows(2).enumerate() {
                    area_builder.line_to(Point::new(p[1].x, p[0].y));
                    line_builder.line_to(Point::new(p[1].x, p[0].y));
                    // Don't draw the vertical line for the last point
                    if i < points.len() - 2 {
                        area_builder.line_to(p[1]);
                        line_builder.line_to(p[1]);
                    }
                }
            }
        }

        // Close path
        if let (Some(first), Some(last), Some(y)) = (first_x_tick, last_x_tick, self.y0) {
            area_builder.line_to(origin_point(px(last), px(y), bounds.origin));
            area_builder.line_to(origin_point(px(first), px(y), bounds.origin));
            area_builder.close();
        }

        (area_builder.build().ok(), line_builder.build().ok())
    }

    /// Paint the Area.
    pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window) {
        let (area, line) = self.path(bounds);

        if let Some(area) = area {
            window.paint_path(area, self.fill);
        }
        if let Some(line) = line {
            window.paint_path(line, self.stroke);
        }
    }
}
