use std::iter;
use std::path::PathBuf;
use std::process::Child;

use crate::stats::univariate::Sample;
use criterion_plot::prelude::*;

mod distributions;
mod iteration_times;
mod pdf;
mod regression;
mod summary;
mod t_test;
use self::distributions::*;
use self::iteration_times::*;
use self::pdf::*;
use self::regression::*;
use self::summary::*;
use self::t_test::*;

use crate::measurement::ValueFormatter;
use crate::report::{BenchmarkId, ValueType};
use crate::stats::bivariate::Data;

use super::{PlotContext, PlotData, Plotter};
use crate::format;

fn gnuplot_escape(string: &str) -> String {
    string.replace("_", "\\_").replace("'", "''")
}

static DEFAULT_FONT: &str = "Helvetica";
static KDE_POINTS: usize = 500;
static SIZE: Size = Size(1280, 720);

const LINEWIDTH: LineWidth = LineWidth(2.);
const POINT_SIZE: PointSize = PointSize(0.75);

const DARK_BLUE: Color = Color::Rgb(31, 120, 180);
const DARK_ORANGE: Color = Color::Rgb(255, 127, 0);
const DARK_RED: Color = Color::Rgb(227, 26, 28);

fn debug_script(path: &PathBuf, figure: &Figure) {
    if crate::debug_enabled() {
        let mut script_path = path.clone();
        script_path.set_extension("gnuplot");
        info!("Writing gnuplot script to {:?}", script_path);
        let result = figure.save(script_path.as_path());
        if let Err(e) = result {
            error!("Failed to write debug output: {}", e);
        }
    }
}

/// Private
trait Append<T> {
    /// Private
    fn append_(self, item: T) -> Self;
}

// NB I wish this was in the standard library
impl<T> Append<T> for Vec<T> {
    fn append_(mut self, item: T) -> Vec<T> {
        self.push(item);
        self
    }
}

#[derive(Default)]
pub(crate) struct Gnuplot {
    process_list: Vec<Child>,
}

impl Plotter for Gnuplot {
    fn pdf(&mut self, ctx: PlotContext<'_>, data: PlotData<'_>) {
        let size = ctx.size.map(|(w, h)| Size(w, h));
        self.process_list.push(if ctx.is_thumbnail {
            if let Some(cmp) = data.comparison {
                pdf_comparison_small(
                    ctx.id,
                    ctx.context,
                    data.formatter,
                    data.measurements,
                    cmp,
                    size,
                )
            } else {
                pdf_small(ctx.id, ctx.context, data.formatter, data.measurements, size)
            }
        } else if let Some(cmp) = data.comparison {
            pdf_comparison(
                ctx.id,
                ctx.context,
                data.formatter,
                data.measurements,
                cmp,
                size,
            )
        } else {
            pdf(ctx.id, ctx.context, data.formatter, data.measurements, size)
        });
    }

    fn regression(&mut self, ctx: PlotContext<'_>, data: PlotData<'_>) {
        let size = ctx.size.map(|(w, h)| Size(w, h));
        self.process_list.push(if ctx.is_thumbnail {
            if let Some(cmp) = data.comparison {
                let base_data = Data::new(&cmp.base_iter_counts, &cmp.base_sample_times);
                regression_comparison_small(
                    ctx.id,
                    ctx.context,
                    data.formatter,
                    data.measurements,
                    cmp,
                    &base_data,
                    size,
                )
            } else {
                regression_small(ctx.id, ctx.context, data.formatter, data.measurements, size)
            }
        } else if let Some(cmp) = data.comparison {
            let base_data = Data::new(&cmp.base_iter_counts, &cmp.base_sample_times);
            regression_comparison(
                ctx.id,
                ctx.context,
                data.formatter,
                data.measurements,
                cmp,
                &base_data,
                size,
            )
        } else {
            regression(ctx.id, ctx.context, data.formatter, data.measurements, size)
        });
    }

    fn iteration_times(&mut self, ctx: PlotContext<'_>, data: PlotData<'_>) {
        let size = ctx.size.map(|(w, h)| Size(w, h));
        self.process_list.push(if ctx.is_thumbnail {
            if let Some(cmp) = data.comparison {
                iteration_times_comparison_small(
                    ctx.id,
                    ctx.context,
                    data.formatter,
                    data.measurements,
                    cmp,
                    size,
                )
            } else {
                iteration_times_small(ctx.id, ctx.context, data.formatter, data.measurements, size)
            }
        } else if let Some(cmp) = data.comparison {
            iteration_times_comparison(
                ctx.id,
                ctx.context,
                data.formatter,
                data.measurements,
                cmp,
                size,
            )
        } else {
            iteration_times(ctx.id, ctx.context, data.formatter, data.measurements, size)
        });
    }

    fn abs_distributions(&mut self, ctx: PlotContext<'_>, data: PlotData<'_>) {
        let size = ctx.size.map(|(w, h)| Size(w, h));
        self.process_list.extend(abs_distributions(
            ctx.id,
            ctx.context,
            data.formatter,
            data.measurements,
            size,
        ));
    }

    fn rel_distributions(&mut self, ctx: PlotContext<'_>, data: PlotData<'_>) {
        let size = ctx.size.map(|(w, h)| Size(w, h));
        if let Some(cmp) = data.comparison {
            self.process_list.extend(rel_distributions(
                ctx.id,
                ctx.context,
                data.measurements,
                cmp,
                size,
            ));
        } else {
            error!("Comparison data is not provided for a relative distribution figure");
        }
    }

    fn t_test(&mut self, ctx: PlotContext<'_>, data: PlotData<'_>) {
        let size = ctx.size.map(|(w, h)| Size(w, h));
        if let Some(cmp) = data.comparison {
            self.process_list
                .push(t_test(ctx.id, ctx.context, data.measurements, cmp, size));
        } else {
            error!("Comparison data is not provided for t_test plot");
        }
    }

    fn line_comparison(
        &mut self,
        ctx: PlotContext<'_>,
        formatter: &dyn ValueFormatter,
        all_curves: &[&(&BenchmarkId, Vec<f64>)],
        value_type: ValueType,
    ) {
        let path = ctx.line_comparison_path();
        self.process_list.push(line_comparison(
            formatter,
            ctx.id.as_title(),
            all_curves,
            &path,
            value_type,
            ctx.context.plot_config.summary_scale,
        ));
    }

    fn violin(
        &mut self,
        ctx: PlotContext<'_>,
        formatter: &dyn ValueFormatter,
        all_curves: &[&(&BenchmarkId, Vec<f64>)],
    ) {
        let violin_path = ctx.violin_path();

        self.process_list.push(violin(
            formatter,
            ctx.id.as_title(),
            all_curves,
            &violin_path,
            ctx.context.plot_config.summary_scale,
        ));
    }

    fn wait(&mut self) {
        let start = std::time::Instant::now();
        let child_count = self.process_list.len();
        for child in self.process_list.drain(..) {
            match child.wait_with_output() {
                Ok(ref out) if out.status.success() => {}
                Ok(out) => error!("Error in Gnuplot: {}", String::from_utf8_lossy(&out.stderr)),
                Err(e) => error!("Got IO error while waiting for Gnuplot to complete: {}", e),
            }
        }
        let elapsed = &start.elapsed();
        info!(
            "Waiting for {} gnuplot processes took {}",
            child_count,
            format::time(crate::DurationExt::to_nanos(elapsed) as f64)
        );
    }
}
