use std::borrow::Borrow;
use std::ops::Range;

use super::axes3d::Axes3dStyle;
use super::{DualCoordChartContext, MeshStyle, SeriesAnno, SeriesLabelStyle};

use crate::coord::cartesian::{Cartesian2d, Cartesian3d, MeshLine};
use crate::coord::ranged1d::{AsRangedCoord, KeyPointHint, Ranged, ValueFormatter};
use crate::coord::ranged3d::{ProjectionMatrix, ProjectionMatrixBuilder};
use crate::coord::{CoordTranslate, ReverseCoordTranslate, Shift};

use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
use crate::element::{Drawable, EmptyElement, PathElement, PointCollection, Polygon, Text};
use crate::style::text_anchor::{HPos, Pos, VPos};
use crate::style::{ShapeStyle, TextStyle};

use plotters_backend::{BackendCoord, DrawingBackend, FontTransform};

/// The context of the chart. This is the core object of Plotters.
/// Any plot/chart is abstracted as this type, and any data series can be placed to the chart
/// context.
///
/// - To draw a series on a chart context, use [ChartContext::draw_series](struct.ChartContext.html#method.draw_series)
/// - To draw a single element to the chart, you may want to use [ChartContext::plotting_area](struct.ChartContext.html#method.plotting_area)
///
pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> {
    pub(super) x_label_area: [Option<DrawingArea<DB, Shift>>; 2],
    pub(super) y_label_area: [Option<DrawingArea<DB, Shift>>; 2],
    pub(super) drawing_area: DrawingArea<DB, CT>,
    pub(super) series_anno: Vec<SeriesAnno<'a, DB>>,
    pub(super) drawing_area_pos: (i32, i32),
}

impl<'a, DB, XT, YT, X, Y> ChartContext<'a, DB, Cartesian2d<X, Y>>
where
    DB: DrawingBackend,
    X: Ranged<ValueType = XT> + ValueFormatter<XT>,
    Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
{
    pub(crate) fn is_overlapping_drawing_area(
        &self,
        area: Option<&DrawingArea<DB, Shift>>,
    ) -> bool {
        if let Some(area) = area {
            let (x0, y0) = area.get_base_pixel();
            let (w, h) = area.dim_in_pixel();
            let (x1, y1) = (x0 + w as i32, y0 + h as i32);
            let (dx0, dy0) = self.drawing_area.get_base_pixel();
            let (w, h) = self.drawing_area.dim_in_pixel();
            let (dx1, dy1) = (dx0 + w as i32, dy0 + h as i32);

            let (ox0, ox1) = (x0.max(dx0), x1.min(dx1));
            let (oy0, oy1) = (y0.max(dy0), y1.min(dy1));

            ox1 > ox0 && oy1 > oy0
        } else {
            false
        }
    }

    /// Initialize a mesh configuration object and mesh drawing can be finalized by calling
    /// the function `MeshStyle::draw`.
    pub fn configure_mesh(&mut self) -> MeshStyle<'a, '_, X, Y, DB> {
        MeshStyle::new(self)
    }
}

impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> {
    /// Convert the chart context into an closure that can be used for coordinate translation
    pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Option<CT::From> {
        let coord_spec = self.drawing_area.into_coord_spec();
        move |coord| coord_spec.reverse_translate(coord)
    }
}

impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
    /// Configure the styles for drawing series labels in the chart
    pub fn configure_series_labels<'b>(&'b mut self) -> SeriesLabelStyle<'a, 'b, DB, CT>
    where
        DB: 'a,
    {
        SeriesLabelStyle::new(self)
    }

    /// Get a reference of underlying plotting area
    pub fn plotting_area(&self) -> &DrawingArea<DB, CT> {
        &self.drawing_area
    }

    /// Cast the reference to a chart context to a reference to underlying coordinate specification.
    pub fn as_coord_spec(&self) -> &CT {
        self.drawing_area.as_coord_spec()
    }

    // TODO: All draw_series_impl is overly strict about lifetime, because we don't have stable HKT,
    //       what we can ensure is for all lifetime 'b the element reference &'b E is a iterator
    //       of points reference with the same lifetime.
    //       However, this doesn't work if the coordinate doesn't live longer than the backend,
    //       this is unnecessarily strict
    pub(super) fn draw_series_impl<E, R, S>(
        &mut self,
        series: S,
    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
    where
        for<'b> &'b E: PointCollection<'b, CT::From>,
        E: Drawable<DB>,
        R: Borrow<E>,
        S: IntoIterator<Item = R>,
    {
        for element in series {
            self.drawing_area.draw(element.borrow())?;
        }
        Ok(())
    }

    pub(super) fn alloc_series_anno(&mut self) -> &mut SeriesAnno<'a, DB> {
        let idx = self.series_anno.len();
        self.series_anno.push(SeriesAnno::new());
        &mut self.series_anno[idx]
    }

    /// Draw a data series. A data series in Plotters is abstracted as an iterator of elements
    pub fn draw_series<E, R, S>(
        &mut self,
        series: S,
    ) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
    where
        for<'b> &'b E: PointCollection<'b, CT::From>,
        E: Drawable<DB>,
        R: Borrow<E>,
        S: IntoIterator<Item = R>,
    {
        self.draw_series_impl(series)?;
        Ok(self.alloc_series_anno())
    }
}

impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesian2d<X, Y>> {
    /// Get the range of X axis
    pub fn x_range(&self) -> Range<X::ValueType> {
        self.drawing_area.get_x_range()
    }

    /// Get range of the Y axis
    pub fn y_range(&self) -> Range<Y::ValueType> {
        self.drawing_area.get_y_range()
    }

    /// Maps the coordinate to the backend coordinate. This is typically used
    /// with an interactive chart.
    pub fn backend_coord(&self, coord: &(X::ValueType, Y::ValueType)) -> BackendCoord {
        self.drawing_area.map_coordinate(coord)
    }

    /// The actual function that draws the mesh lines.
    /// It also returns the label that suppose to be there.
    #[allow(clippy::type_complexity)]
    fn draw_mesh_lines<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>(
        &mut self,
        (r, c): (YH, XH),
        (x_mesh, y_mesh): (bool, bool),
        mesh_line_style: &ShapeStyle,
        mut fmt_label: FmtLabel,
    ) -> Result<(Vec<(i32, String)>, Vec<(i32, String)>), DrawingAreaErrorKind<DB::ErrorType>>
    where
        FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
    {
        let mut x_labels = vec![];
        let mut y_labels = vec![];
        self.drawing_area.draw_mesh(
            |b, l| {
                let draw;
                match l {
                    MeshLine::XMesh((x, _), _, _) => {
                        if let Some(label_text) = fmt_label(&l) {
                            x_labels.push((x, label_text));
                        }
                        draw = x_mesh;
                    }
                    MeshLine::YMesh((_, y), _, _) => {
                        if let Some(label_text) = fmt_label(&l) {
                            y_labels.push((y, label_text));
                        }
                        draw = y_mesh;
                    }
                };
                if draw {
                    l.draw(b, mesh_line_style)
                } else {
                    Ok(())
                }
            },
            r,
            c,
        )?;
        Ok((x_labels, y_labels))
    }

    fn draw_axis(
        &self,
        area: &DrawingArea<DB, Shift>,
        axis_style: Option<&ShapeStyle>,
        orientation: (i16, i16),
        inward_labels: bool,
    ) -> Result<Range<i32>, DrawingAreaErrorKind<DB::ErrorType>> {
        let (x0, y0) = self.drawing_area.get_base_pixel();
        let (tw, th) = area.dim_in_pixel();

        let mut axis_range = if orientation.0 == 0 {
            self.drawing_area.get_x_axis_pixel_range()
        } else {
            self.drawing_area.get_y_axis_pixel_range()
        };

        /* At this point, the coordinate system tells us the pixel range
         * after the translation.
         * However, we need to use the logic coordinate system for drawing. */
        if orientation.0 == 0 {
            axis_range.start -= x0;
            axis_range.end -= x0;
        } else {
            axis_range.start -= y0;
            axis_range.end -= y0;
        }

        if let Some(axis_style) = axis_style {
            let mut x0 = if orientation.0 > 0 { 0 } else { tw as i32 - 1 };
            let mut y0 = if orientation.1 > 0 { 0 } else { th as i32 - 1 };
            let mut x1 = if orientation.0 >= 0 { 0 } else { tw as i32 - 1 };
            let mut y1 = if orientation.1 >= 0 { 0 } else { th as i32 - 1 };

            if inward_labels {
                if orientation.0 == 0 {
                    if y0 == 0 {
                        y0 = th as i32 - 1;
                        y1 = th as i32 - 1;
                    } else {
                        y0 = 0;
                        y1 = 0;
                    }
                } else if x0 == 0 {
                    x0 = tw as i32 - 1;
                    x1 = tw as i32 - 1;
                } else {
                    x0 = 0;
                    x1 = 0;
                }
            }

            if orientation.0 == 0 {
                x0 = axis_range.start;
                x1 = axis_range.end;
            } else {
                y0 = axis_range.start;
                y1 = axis_range.end;
            }

            area.draw(&PathElement::new(
                vec![(x0, y0), (x1, y1)],
                axis_style.clone(),
            ))?;
        }

        Ok(axis_range)
    }

    // TODO: consider make this function less complicated
    #[allow(clippy::too_many_arguments)]
    #[allow(clippy::cognitive_complexity)]
    fn draw_axis_and_labels(
        &self,
        area: Option<&DrawingArea<DB, Shift>>,
        axis_style: Option<&ShapeStyle>,
        labels: &[(i32, String)],
        label_style: &TextStyle,
        label_offset: i32,
        orientation: (i16, i16),
        axis_desc: Option<(&str, &TextStyle)>,
        tick_size: i32,
    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
        let area = if let Some(target) = area {
            target
        } else {
            return Ok(());
        };

        let (x0, y0) = self.drawing_area.get_base_pixel();
        let (tw, th) = area.dim_in_pixel();

        /* This is the minimal distance from the axis to the box of the labels */
        let label_dist = tick_size.abs() * 2;

        /* Draw the axis and get the axis range so that we can do further label
         * and tick mark drawing */
        let axis_range = self.draw_axis(area, axis_style, orientation, tick_size < 0)?;

        /* To make the right label area looks nice, it's a little bit tricky, since for a that is
         * very long, we actually prefer left alignment instead of right alignment.
         * Otherwise, the right alignment looks better. So we estimate the max and min label width
         * So that we are able decide if we should apply right alignment for the text. */
        let label_width: Vec<_> = labels
            .iter()
            .map(|(_, text)| {
                if orientation.0 > 0 && orientation.1 == 0 && tick_size >= 0 {
                    let ((x0, _), (x1, _)) = label_style
                        .font
                        .layout_box(text)
                        .unwrap_or(((0, 0), (0, 0)));
                    x1 - x0
                } else {
                    // Don't ever do the layout estimationfor the drawing area that is either not
                    // the right one or the tick mark is inward.
                    0
                }
            })
            .collect();

        let min_width = *label_width.iter().min().unwrap_or(&1).max(&1);
        let max_width = *label_width
            .iter()
            .filter(|&&x| x < min_width * 2)
            .max()
            .unwrap_or(&min_width);
        let right_align_width = (min_width * 2).min(max_width);

        /* Then we need to draw the tick mark and the label */
        for ((p, t), w) in labels.iter().zip(label_width.into_iter()) {
            /* Make sure we are actually in the visible range */
            let rp = if orientation.0 == 0 { *p - x0 } else { *p - y0 };

            if rp < axis_range.start.min(axis_range.end)
                || axis_range.end.max(axis_range.start) < rp
            {
                continue;
            }

            let (cx, cy, h_pos, v_pos) = if tick_size >= 0 {
                match orientation {
                    // Right
                    (dx, dy) if dx > 0 && dy == 0 => {
                        if w >= right_align_width {
                            (label_dist, *p - y0, HPos::Left, VPos::Center)
                        } else {
                            (
                                label_dist + right_align_width,
                                *p - y0,
                                HPos::Right,
                                VPos::Center,
                            )
                        }
                    }
                    // Left
                    (dx, dy) if dx < 0 && dy == 0 => {
                        (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
                    }
                    // Bottom
                    (dx, dy) if dx == 0 && dy > 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
                    // Top
                    (dx, dy) if dx == 0 && dy < 0 => {
                        (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
                    }
                    _ => panic!("Bug: Invalid orientation specification"),
                }
            } else {
                match orientation {
                    // Right
                    (dx, dy) if dx > 0 && dy == 0 => {
                        (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
                    }
                    // Left
                    (dx, dy) if dx < 0 && dy == 0 => {
                        (label_dist, *p - y0, HPos::Left, VPos::Center)
                    }
                    // Bottom
                    (dx, dy) if dx == 0 && dy > 0 => {
                        (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
                    }
                    // Top
                    (dx, dy) if dx == 0 && dy < 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
                    _ => panic!("Bug: Invalid orientation specification"),
                }
            };

            let (text_x, text_y) = if orientation.0 == 0 {
                (cx + label_offset, cy)
            } else {
                (cx, cy + label_offset)
            };

            let label_style = &label_style.pos(Pos::new(h_pos, v_pos));
            area.draw_text(&t, label_style, (text_x, text_y))?;

            if tick_size != 0 {
                if let Some(style) = axis_style {
                    let xmax = tw as i32 - 1;
                    let ymax = th as i32 - 1;
                    let (kx0, ky0, kx1, ky1) = if tick_size > 0 {
                        match orientation {
                            (dx, dy) if dx > 0 && dy == 0 => (0, *p - y0, tick_size, *p - y0),
                            (dx, dy) if dx < 0 && dy == 0 => {
                                (xmax - tick_size, *p - y0, xmax, *p - y0)
                            }
                            (dx, dy) if dx == 0 && dy > 0 => (*p - x0, 0, *p - x0, tick_size),
                            (dx, dy) if dx == 0 && dy < 0 => {
                                (*p - x0, ymax - tick_size, *p - x0, ymax)
                            }
                            _ => panic!("Bug: Invalid orientation specification"),
                        }
                    } else {
                        match orientation {
                            (dx, dy) if dx > 0 && dy == 0 => {
                                (xmax, *p - y0, xmax + tick_size, *p - y0)
                            }
                            (dx, dy) if dx < 0 && dy == 0 => (0, *p - y0, -tick_size, *p - y0),
                            (dx, dy) if dx == 0 && dy > 0 => {
                                (*p - x0, ymax, *p - x0, ymax + tick_size)
                            }
                            (dx, dy) if dx == 0 && dy < 0 => (*p - x0, 0, *p - x0, -tick_size),
                            _ => panic!("Bug: Invalid orientation specification"),
                        }
                    };
                    let line = PathElement::new(vec![(kx0, ky0), (kx1, ky1)], style.clone());
                    area.draw(&line)?;
                }
            }
        }

        if let Some((text, style)) = axis_desc {
            let actual_style = if orientation.0 == 0 {
                style.clone()
            } else if orientation.0 == -1 {
                style.transform(FontTransform::Rotate270)
            } else {
                style.transform(FontTransform::Rotate90)
            };

            let (x0, y0, h_pos, v_pos) = match orientation {
                // Right
                (dx, dy) if dx > 0 && dy == 0 => (tw, th / 2, HPos::Center, VPos::Top),
                // Left
                (dx, dy) if dx < 0 && dy == 0 => (0, th / 2, HPos::Center, VPos::Top),
                // Bottom
                (dx, dy) if dx == 0 && dy > 0 => (tw / 2, th, HPos::Center, VPos::Bottom),
                // Top
                (dx, dy) if dx == 0 && dy < 0 => (tw / 2, 0, HPos::Center, VPos::Top),
                _ => panic!("Bug: Invalid orientation specification"),
            };

            let actual_style = &actual_style.pos(Pos::new(h_pos, v_pos));
            area.draw_text(&text, &actual_style, (x0 as i32, y0 as i32))?;
        }

        Ok(())
    }

    #[allow(clippy::too_many_arguments)]
    pub(super) fn draw_mesh<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>(
        &mut self,
        (r, c): (YH, XH),
        mesh_line_style: &ShapeStyle,
        x_label_style: &TextStyle,
        y_label_style: &TextStyle,
        fmt_label: FmtLabel,
        x_mesh: bool,
        y_mesh: bool,
        x_label_offset: i32,
        y_label_offset: i32,
        x_axis: bool,
        y_axis: bool,
        axis_style: &ShapeStyle,
        axis_desc_style: &TextStyle,
        x_desc: Option<String>,
        y_desc: Option<String>,
        x_tick_size: [i32; 2],
        y_tick_size: [i32; 2],
    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
    where
        FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
    {
        let (x_labels, y_labels) =
            self.draw_mesh_lines((r, c), (x_mesh, y_mesh), mesh_line_style, fmt_label)?;

        for idx in 0..2 {
            self.draw_axis_and_labels(
                self.x_label_area[idx].as_ref(),
                if x_axis { Some(axis_style) } else { None },
                &x_labels[..],
                x_label_style,
                x_label_offset,
                (0, -1 + idx as i16 * 2),
                x_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
                x_tick_size[idx],
            )?;

            self.draw_axis_and_labels(
                self.y_label_area[idx].as_ref(),
                if y_axis { Some(axis_style) } else { None },
                &y_labels[..],
                y_label_style,
                y_label_offset,
                (-1 + idx as i16 * 2, 0),
                y_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
                y_tick_size[idx],
            )?;
        }

        Ok(())
    }

    /// Convert this chart context into a dual axis chart context and attach a second coordinate spec
    /// on the chart context. For more detailed information, see documentation for [struct DualCoordChartContext](struct.DualCoordChartContext.html)
    ///
    /// - `x_coord`: The coordinate spec for the X axis
    /// - `y_coord`: The coordinate spec for the Y axis
    /// - **returns** The newly created dual spec chart context
    #[allow(clippy::type_complexity)]
    pub fn set_secondary_coord<SX: AsRangedCoord, SY: AsRangedCoord>(
        self,
        x_coord: SX,
        y_coord: SY,
    ) -> DualCoordChartContext<
        'a,
        DB,
        Cartesian2d<X, Y>,
        Cartesian2d<SX::CoordDescType, SY::CoordDescType>,
    > {
        let mut pixel_range = self.drawing_area.get_pixel_range();
        pixel_range.1 = pixel_range.1.end..pixel_range.1.start;

        DualCoordChartContext::new(self, Cartesian2d::new(x_coord, y_coord, pixel_range))
    }
}

pub(super) struct KeyPoints3d<X: Ranged, Y: Ranged, Z: Ranged> {
    pub(super) x_points: Vec<X::ValueType>,
    pub(super) y_points: Vec<Y::ValueType>,
    pub(super) z_points: Vec<Z::ValueType>,
}

#[derive(Clone, Debug)]
pub(super) enum Coord3D<X, Y, Z> {
    X(X),
    Y(Y),
    Z(Z),
}

impl<X, Y, Z> Coord3D<X, Y, Z> {
    fn get_x(&self) -> &X {
        match self {
            Coord3D::X(ret) => ret,
            _ => panic!("Invalid call!"),
        }
    }
    fn get_y(&self) -> &Y {
        match self {
            Coord3D::Y(ret) => ret,
            _ => panic!("Invalid call!"),
        }
    }
    fn get_z(&self) -> &Z {
        match self {
            Coord3D::Z(ret) => ret,
            _ => panic!("Invalid call!"),
        }
    }

    fn build_coord([x, y, z]: [&Self; 3]) -> (X, Y, Z)
    where
        X: Clone,
        Y: Clone,
        Z: Clone,
    {
        (x.get_x().clone(), y.get_y().clone(), z.get_z().clone())
    }
}
impl<'a, DB, X, Y, Z, XT, YT, ZT> ChartContext<'a, DB, Cartesian3d<X, Y, Z>>
where
    DB: DrawingBackend,
    X: Ranged<ValueType = XT> + ValueFormatter<XT>,
    Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
    Z: Ranged<ValueType = ZT> + ValueFormatter<ZT>,
{
    pub fn configure_axes(&mut self) -> Axes3dStyle<'a, '_, X, Y, Z, DB> {
        Axes3dStyle::new(self)
    }
}
impl<'a, DB, X: Ranged, Y: Ranged, Z: Ranged> ChartContext<'a, DB, Cartesian3d<X, Y, Z>>
where
    DB: DrawingBackend,
{
    /// Override the 3D projection matrix. This function allows to override the default projection
    /// matrix.
    /// - `pf`: A function that takes the default projection matrix configuration and returns the
    /// projection matrix. This function will allow you to adjust the pitch, yaw angle and the
    /// centeral point of the projection, etc. You can also build a projection matrix which is not
    /// relies on the default configuration as well.
    pub fn with_projection<P: FnOnce(ProjectionMatrixBuilder) -> ProjectionMatrix>(
        &mut self,
        pf: P,
    ) -> &mut Self {
        let (actual_x, actual_y) = self.drawing_area.get_pixel_range();
        self.drawing_area
            .as_coord_spec_mut()
            .set_projection(actual_x, actual_y, pf);
        self
    }
}

impl<'a, DB, X: Ranged, Y: Ranged, Z: Ranged> ChartContext<'a, DB, Cartesian3d<X, Y, Z>>
where
    DB: DrawingBackend,
    X::ValueType: Clone,
    Y::ValueType: Clone,
    Z::ValueType: Clone,
{
    pub(super) fn get_key_points<XH: KeyPointHint, YH: KeyPointHint, ZH: KeyPointHint>(
        &self,
        x_hint: XH,
        y_hint: YH,
        z_hint: ZH,
    ) -> KeyPoints3d<X, Y, Z> {
        let coord = self.plotting_area().as_coord_spec();
        let x_points = coord.logic_x.key_points(x_hint);
        let y_points = coord.logic_y.key_points(y_hint);
        let z_points = coord.logic_z.key_points(z_hint);
        KeyPoints3d {
            x_points,
            y_points,
            z_points,
        }
    }
    pub(super) fn draw_axis_ticks(
        &mut self,
        axis: [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2],
        labels: &[(
            [Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3],
            String,
        )],
        tick_size: i32,
        style: ShapeStyle,
        font: TextStyle,
    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
        let coord = self.plotting_area().as_coord_spec();
        let begin = coord.translate(&Coord3D::build_coord([
            &axis[0][0],
            &axis[0][1],
            &axis[0][2],
        ]));
        let end = coord.translate(&Coord3D::build_coord([
            &axis[1][0],
            &axis[1][1],
            &axis[1][2],
        ]));
        let axis_dir = (end.0 - begin.0, end.1 - begin.1);
        let (x_range, y_range) = self.plotting_area().get_pixel_range();
        let x_mid = (x_range.start + x_range.end) / 2;
        let y_mid = (y_range.start + y_range.end) / 2;

        let x_dir = if begin.0 < x_mid {
            (-tick_size, 0)
        } else {
            (tick_size, 0)
        };

        let y_dir = if begin.1 < y_mid {
            (0, -tick_size)
        } else {
            (0, tick_size)
        };

        let x_score = (x_dir.0 * axis_dir.0 + x_dir.1 * axis_dir.1).abs();
        let y_score = (y_dir.0 * axis_dir.0 + y_dir.1 * axis_dir.1).abs();

        let dir = if x_score < y_score { x_dir } else { y_dir };

        for (pos, text) in labels {
            let logic_pos = Coord3D::build_coord([&pos[0], &pos[1], &pos[2]]);
            let mut font = font.clone();
            if dir.0 < 0 {
                font.pos = Pos::new(HPos::Right, VPos::Center);
            } else if dir.0 > 0 {
                font.pos = Pos::new(HPos::Left, VPos::Center);
            };
            if dir.1 < 0 {
                font.pos = Pos::new(HPos::Center, VPos::Bottom);
            } else if dir.1 > 0 {
                font.pos = Pos::new(HPos::Center, VPos::Top);
            };
            let element = EmptyElement::at(logic_pos)
                + PathElement::new(vec![(0, 0), dir], style.clone())
                + Text::new(text.to_string(), (dir.0 * 2, dir.1 * 2), font.clone());
            self.plotting_area().draw(&element)?;
        }
        Ok(())
    }
    pub(super) fn draw_axis(
        &mut self,
        idx: usize,
        panels: &[[[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2]; 3],
        style: ShapeStyle,
    ) -> Result<
        [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2],
        DrawingAreaErrorKind<DB::ErrorType>,
    > {
        let coord = self.plotting_area().as_coord_spec();
        let x_range = coord.logic_x.range();
        let y_range = coord.logic_y.range();
        let z_range = coord.logic_z.range();

        let ranges: [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 2]; 3] = [
            [Coord3D::X(x_range.start), Coord3D::X(x_range.end)],
            [Coord3D::Y(y_range.start), Coord3D::Y(y_range.end)],
            [Coord3D::Z(z_range.start), Coord3D::Z(z_range.end)],
        ];

        let (start, end) = {
            let mut start = [&ranges[0][0], &ranges[1][0], &ranges[2][0]];
            let mut end = [&ranges[0][1], &ranges[1][1], &ranges[2][1]];

            let mut plan = vec![];

            for i in 0..3 {
                if i == idx {
                    continue;
                }
                start[i] = &panels[i][0][i];
                end[i] = &panels[i][0][i];
                for j in 0..3 {
                    if i != idx && i != j && j != idx {
                        for k in 0..2 {
                            start[j] = &panels[i][k][j];
                            end[j] = &panels[i][k][j];
                            plan.push((start, end));
                        }
                    }
                }
            }
            plan.into_iter()
                .min_by_key(|&(s, e)| {
                    let d = coord.projected_depth(s[0].get_x(), s[1].get_y(), s[2].get_z());
                    let d = d + coord.projected_depth(e[0].get_x(), e[1].get_y(), e[2].get_z());
                    let (_, y1) = coord.translate(&Coord3D::build_coord(s));
                    let (_, y2) = coord.translate(&Coord3D::build_coord(e));
                    let y = y1 + y2;
                    (d, y)
                })
                .unwrap()
        };

        self.plotting_area().draw(&PathElement::new(
            vec![Coord3D::build_coord(start), Coord3D::build_coord(end)],
            style.clone(),
        ))?;

        Ok([
            [start[0].clone(), start[1].clone(), start[2].clone()],
            [end[0].clone(), end[1].clone(), end[2].clone()],
        ])
    }
    pub(super) fn draw_axis_panels(
        &mut self,
        bold_points: &KeyPoints3d<X, Y, Z>,
        light_points: &KeyPoints3d<X, Y, Z>,
        panel_style: ShapeStyle,
        bold_grid_style: ShapeStyle,
        light_grid_style: ShapeStyle,
    ) -> Result<
        [[[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2]; 3],
        DrawingAreaErrorKind<DB::ErrorType>,
    > {
        let mut r_iter = (0..3).map(|idx| {
            self.draw_axis_panel(
                idx,
                bold_points,
                light_points,
                panel_style.clone(),
                bold_grid_style.clone(),
                light_grid_style.clone(),
            )
        });
        Ok([
            r_iter.next().unwrap()?,
            r_iter.next().unwrap()?,
            r_iter.next().unwrap()?,
        ])
    }
    fn draw_axis_panel(
        &mut self,
        idx: usize,
        bold_points: &KeyPoints3d<X, Y, Z>,
        light_points: &KeyPoints3d<X, Y, Z>,
        panel_style: ShapeStyle,
        bold_grid_style: ShapeStyle,
        light_grid_style: ShapeStyle,
    ) -> Result<
        [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2],
        DrawingAreaErrorKind<DB::ErrorType>,
    > {
        let coord = self.plotting_area().as_coord_spec();
        let x_range = coord.logic_x.range();
        let y_range = coord.logic_y.range();
        let z_range = coord.logic_z.range();

        let ranges: [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 2]; 3] = [
            [Coord3D::X(x_range.start), Coord3D::X(x_range.end)],
            [Coord3D::Y(y_range.start), Coord3D::Y(y_range.end)],
            [Coord3D::Z(z_range.start), Coord3D::Z(z_range.end)],
        ];

        let (mut panel, start, end) = {
            let a = [&ranges[0][0], &ranges[1][0], &ranges[2][0]];
            let mut b = [&ranges[0][1], &ranges[1][1], &ranges[2][1]];
            let mut c = a;
            let d = b;

            b[idx] = &ranges[idx][0];
            c[idx] = &ranges[idx][1];

            let (a, b) = if coord.projected_depth(a[0].get_x(), a[1].get_y(), a[2].get_z())
                >= coord.projected_depth(c[0].get_x(), c[1].get_y(), c[2].get_z())
            {
                (a, b)
            } else {
                (c, d)
            };

            let mut m = a.clone();
            m[(idx + 1) % 3] = b[(idx + 1) % 3];
            let mut n = a.clone();
            n[(idx + 2) % 3] = b[(idx + 2) % 3];

            (
                vec![
                    Coord3D::build_coord(a),
                    Coord3D::build_coord(m),
                    Coord3D::build_coord(b),
                    Coord3D::build_coord(n),
                ],
                a,
                b,
            )
        };
        self.plotting_area()
            .draw(&Polygon::new(panel.clone(), panel_style.clone()))?;
        panel.push(panel[0].clone());
        self.plotting_area()
            .draw(&PathElement::new(panel, bold_grid_style.clone()))?;

        for (kps, style) in vec![
            (light_points, light_grid_style),
            (bold_points, bold_grid_style),
        ]
        .into_iter()
        {
            for idx in (0..3).filter(|&i| i != idx) {
                let kps: Vec<_> = match idx {
                    0 => kps.x_points.iter().map(|x| Coord3D::X(x.clone())).collect(),
                    1 => kps.y_points.iter().map(|y| Coord3D::Y(y.clone())).collect(),
                    _ => kps.z_points.iter().map(|z| Coord3D::Z(z.clone())).collect(),
                };
                for kp in kps.iter() {
                    let mut kp_start = start;
                    let mut kp_end = end;
                    kp_start[idx] = kp;
                    kp_end[idx] = kp;
                    self.plotting_area().draw(&PathElement::new(
                        vec![Coord3D::build_coord(kp_start), Coord3D::build_coord(kp_end)],
                        style.clone(),
                    ))?;
                }
            }
        }

        Ok([
            [start[0].clone(), start[1].clone(), start[2].clone()],
            [end[0].clone(), end[1].clone(), end[2].clone()],
        ])
    }
}

#[cfg(test)]
mod test {
    use crate::prelude::*;

    #[test]
    fn test_chart_context() {
        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});

        drawing_area.fill(&WHITE).expect("Fill");

        let mut chart = ChartBuilder::on(&drawing_area)
            .caption("Test Title", ("serif", 10))
            .x_label_area_size(20)
            .y_label_area_size(20)
            .set_label_area_size(LabelAreaPosition::Top, 20)
            .set_label_area_size(LabelAreaPosition::Right, 20)
            .build_cartesian_2d(0..10, 0..10)
            .expect("Create chart")
            .set_secondary_coord(0.0..1.0, 0.0..1.0);

        chart
            .configure_mesh()
            .x_desc("X")
            .y_desc("Y")
            .draw()
            .expect("Draw mesh");
        chart
            .configure_secondary_axes()
            .x_desc("X")
            .y_desc("Y")
            .draw()
            .expect("Draw Secondary axes");

        chart
            .draw_series(std::iter::once(Circle::new((5, 5), 5, &RED)))
            .expect("Drawing error");
        chart
            .draw_secondary_series(std::iter::once(Circle::new((0.3, 0.8), 5, &GREEN)))
            .expect("Drawing error")
            .label("Test label")
            .legend(|(x, y)| Rectangle::new([(x - 10, y - 5), (x, y + 5)], &GREEN));

        chart
            .configure_series_labels()
            .position(SeriesLabelPosition::UpperMiddle)
            .draw()
            .expect("Drawing error");
    }

    #[test]
    fn test_chart_context_3d() {
        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});

        drawing_area.fill(&WHITE).expect("Fill");

        let mut chart = ChartBuilder::on(&drawing_area)
            .caption("Test Title", ("serif", 10))
            .x_label_area_size(20)
            .y_label_area_size(20)
            .set_label_area_size(LabelAreaPosition::Top, 20)
            .set_label_area_size(LabelAreaPosition::Right, 20)
            .build_cartesian_3d(0..10, 0..10, 0..10)
            .expect("Create chart");

        chart.with_projection(|mut pb| {
            pb.yaw = 0.5;
            pb.pitch = 0.5;
            pb.scale = 0.5;
            pb.into_matrix()
        });

        chart.configure_axes().draw().expect("Drawing axes");

        chart
            .draw_series(std::iter::once(Circle::new((5, 5, 5), 5, &RED)))
            .expect("Drawing error");
    }
}
