use std::cmp::Ordering;
use std::io::{Write, stdout};
use std::num::NonZeroUsize;

use bstr::ByteSlice;
use colored::Colorize;
use jiff::{civil::date, tz::TimeZone};
use pad::{Alignment, PadStr};
use simd_csv::ByteRecord;
use unicode_width::UnicodeWidthStr;

use crate::CliResult;
use crate::cmd::parallel::Args as ParallelArgs;
use crate::cmd::plot::Aggregation;
use crate::cmd::plot::Granularity;
use crate::cmd::spark::{Series, SparklineRendererOptions};
use crate::collections::{ClusteredInsertHashmap, Counter, CounterSpec};
use crate::config::{Config, Delimiter};
use crate::moonblade::{DynamicNumber, Stats, TemporalExtent, Welford};
use crate::scales::{Extent, ExtentBuilder, HistogramBuilder, Scale, ScaleType, sturges};
use crate::select::SelectedColumns;
use crate::temporal::{AnyTemporal, parse_fuzzy_temporal};
use crate::util::{self, ColorMode, ColorOrStyles, format_number};

const LABELS_TO_SHOW: usize = 5;
const CATEGORIES_TO_SHOW: usize = 10;
const HISTOGRAM_BINS: usize = 35;
const CARDINALITY_RATIO_THRESHOLD: f64 = 0.7;
const SETTLE_THRESHOLD: u64 = 1024;

static TRUE_VALUES: [&[u8]; 5] = [b"true", b"TRUE", b"True", b"yes", b"1"];
static FALSE_VALUES: [&[u8]; 6] = [b"false", b"FALSE", b"False", b"no", b"0", b"<empty>"];
// static NULL_VALUES: [&str; 7] = ["NULL", "null", "na", "NA", "None", "n/a", "N/A"];

fn float_cmp(a: &f64, b: &f64) -> Ordering {
    a.partial_cmp(b).unwrap()
}

pub fn linear_time_median(values: &mut [f64]) -> f64 {
    let n = values.len();
    let mid = n / 2;

    values.select_nth_unstable_by(mid, float_cmp);

    if n % 2 == 1 {
        values[mid]
    } else {
        let lower = *values[..mid].iter().max_by(|a, b| float_cmp(a, b)).unwrap();
        (lower + values[mid]) / 2.0
    }
}

enum ColumnType {
    Numerical {
        is_int: bool,
        extent: Extent<f64>,
        mean: f64,
        median: f64,
        stddev: f64,
        histogram: HistogramBuilder,
    },
    Categorical {
        is_bool: bool,
        cardinality: u64,
        top: Vec<(String, u64)>,
        total: u64,
    },
    Temporal {
        extent: TemporalExtent,
        granularity: Granularity,
        series: Series,
    },
    Labels {
        sample: Vec<String>,
        length_extent: Extent<f64>,
        length_histogram: HistogramBuilder,
    },
    Void,
}

impl ColumnType {
    fn as_str(&self) -> &str {
        match self {
            Self::Numerical { is_int, .. } => {
                if *is_int {
                    "numerical (integers)"
                } else {
                    "numerical (floats)"
                }
            }
            Self::Categorical { is_bool, .. } => {
                if *is_bool {
                    "boolean"
                } else {
                    "categorical"
                }
            }
            Self::Temporal { .. } => "temporal",
            Self::Labels { length_extent, .. } => {
                if length_extent.max() > 250.0 {
                    "text"
                } else {
                    "labels"
                }
            }
            Self::Void => "void",
        }
    }
}

#[derive(Debug)]
struct ColumnEstimator {
    name: String,
    categories: Option<Counter<Vec<u8>>>,
    numbers: Vec<f64>,
    lengths: Vec<usize>,
    times: Vec<AnyTemporal>,
    int_count: u64,
    empty_count: u64,
    count: u64,
    first_seen: Vec<Vec<u8>>,
}

impl ColumnEstimator {
    fn new(name: &[u8]) -> Self {
        Self {
            name: String::from_utf8_lossy(name).into_owned(),
            categories: Some(Counter::new(CounterSpec::Exact)),
            numbers: Vec::new(),
            lengths: Vec::new(),
            times: Vec::new(),
            int_count: 0,
            empty_count: 0,
            count: 0,
            first_seen: Vec::with_capacity(LABELS_TO_SHOW),
        }
    }

    fn process(&mut self, cell: &[u8]) {
        self.count += 1;

        if cell.is_empty() {
            self.empty_count += 1;
            return;
        } else if self.first_seen.len() < LABELS_TO_SHOW {
            self.first_seen.push(cell.to_vec());
        }

        let mut as_string = true;

        if let Ok(n) = DynamicNumber::try_from(cell) {
            match n {
                DynamicNumber::Float(f) => {
                    as_string = false;
                    self.numbers.push(f);
                }
                DynamicNumber::Integer(i) => {
                    if i < 9007199254740991 {
                        self.int_count += 1;
                        self.numbers.push(i as f64);
                        as_string = false;
                    }
                }
            }
        } else if let Ok(t) = parse_fuzzy_temporal(cell, false) {
            if !t.is_time() {
                self.times.push(t.into());
                as_string = false;
            }
        }

        if as_string {
            if let Some(categories) = self.categories.as_mut() {
                categories.add(cell.to_vec());
            } else {
                self.lengths.push(cell.len());
            }
        }

        if self.count == SETTLE_THRESHOLD {
            self.settle();
        }
    }

    fn settle(&mut self) {
        let strings = self.categories.as_ref().unwrap();

        let cardinality_ratio =
            strings.cardinality().unwrap() as f64 / self.non_empty_count() as f64;

        if cardinality_ratio < CARDINALITY_RATIO_THRESHOLD {
            let categories = self.categories.take().unwrap();

            for (category, count) in categories.iter() {
                for _ in 0..count {
                    self.lengths.push(category.len());
                }
            }
        }
    }

    fn finalize(&mut self) {
        if self.is_int() {
            // Years hiding as ints
            if self.numbers.iter().all(|n| *n >= 1900.0 && *n < 2100.0) {
                for n in self.numbers.iter() {
                    self.times.push(AnyTemporal::Date(date(*n as i16, 1, 1)));
                }

                self.numbers.clear();
                self.int_count = 0;
            }

            // Boolean hiding as ints
            let mut one_count: u64 = 0;
            let mut zero_count: u64 = 0;

            if self.numbers.iter().copied().all(|n| {
                if n == 0.0 {
                    zero_count += 1;
                    true
                } else if n == 1.0 {
                    one_count += 1;
                    true
                } else {
                    false
                }
            }) {
                let mut counter = Counter::new(CounterSpec::Exact);

                if zero_count == 0 {
                    counter.add_n(b"<empty>".to_vec(), self.empty_count);
                } else {
                    counter.add_n(b"0".to_vec(), zero_count);
                }

                counter.add_n(b"1".to_vec(), one_count);

                self.categories = Some(counter);
                self.numbers.clear();
                self.int_count = 0;
            }
        }

        // TODO: timestamps
        // TODO: fix some cases of mixed data
    }

    fn name_hash(&self) -> usize {
        let mut sum: usize = 0;

        for byte in self.name.as_bytes() {
            sum += *byte as usize;
        }

        sum
    }

    fn non_empty_count(&self) -> u64 {
        self.count - self.empty_count
    }

    fn is_void(&self) -> bool {
        self.empty_count == self.count
    }

    fn is_numerical(&self) -> bool {
        !self.is_void() && self.non_empty_count() == self.numbers.len() as u64
    }

    fn is_int(&self) -> bool {
        self.non_empty_count() == self.int_count
    }

    fn to_sample(&self) -> Vec<String> {
        self.first_seen
            .iter()
            .map(|cell| String::from_utf8_lossy(cell).into_owned())
            .collect()
    }

    fn infer_type(&mut self, cols: usize) -> CliResult<ColumnType> {
        Ok(
            // Void
            if self.is_void() {
                ColumnType::Void
            }
            // Numerical
            else if self.is_numerical() {
                let mut extent_builder = ExtentBuilder::new();
                let mut welford = Welford::new();

                for x in self.numbers.iter().copied() {
                    extent_builder.process(x);
                    welford.add(x);
                }

                let extent = extent_builder.build().unwrap();
                let bins = sturges(self.numbers.len()).min(HISTOGRAM_BINS);

                let histogram = HistogramBuilder::from_series(bins, extent, &self.numbers);

                ColumnType::Numerical {
                    is_int: self.is_int(),
                    extent,
                    mean: welford.mean().unwrap(),
                    median: linear_time_median(&mut self.numbers),
                    stddev: welford.stddev().unwrap(),
                    histogram,
                }
            }
            // Temporal
            else if !self.times.is_empty() {
                let mut extent = TemporalExtent::new();
                let mut series = Series::temporal(self.times.len());

                for t in self.times.iter() {
                    extent.add(t.clone())?;
                    series.push_time(
                        t.to_lower_bound_timestamp(TimeZone::system())?
                            .as_duration()
                            .as_secs_f64(),
                    );
                }

                let (adjusted_bins, best_unit) = extent.best_discrete_granularity(cols)?.unwrap();

                series.temporal_discretize_and_sort(
                    adjusted_bins,
                    best_unit,
                    &extent,
                    Aggregation::Sum,
                )?;

                series.discretize(cols);

                ColumnType::Temporal {
                    extent,
                    granularity: Granularity::new(best_unit),
                    series,
                }
            }
            // Categorical
            else if let Some(categories) = &self.categories {
                let (total, top) = categories.to_total_and_items(Some(CATEGORIES_TO_SHOW), false);

                let mut is_bool = false;

                if top.len() == 2
                    && ((TRUE_VALUES.contains(&top[0].0.as_ref())
                        && FALSE_VALUES.contains(&top[1].0.as_ref()))
                        || (TRUE_VALUES.contains(&top[1].0.as_ref())
                            && FALSE_VALUES.contains(&top[0].0.as_ref())))
                {
                    is_bool = true;
                }

                ColumnType::Categorical {
                    is_bool,
                    cardinality: categories.cardinality().unwrap(),
                    top: top
                        .iter()
                        .map(|(cell, count)| (String::from_utf8_lossy(cell).into_owned(), *count))
                        .collect(),
                    total,
                }
            }
            // Labels
            else {
                let mut length_extent_builder = ExtentBuilder::new();

                for l in self.lengths.iter().copied() {
                    length_extent_builder.process(l);
                }

                let length_extent = length_extent_builder.build().unwrap();
                let length_extent =
                    Extent::from((length_extent.min() as f64, length_extent.max() as f64));
                let bins = sturges(self.count as usize).min(HISTOGRAM_BINS);
                let mut length_histogram = HistogramBuilder::new(bins, length_extent);

                for l in self.lengths.iter().copied() {
                    length_histogram.add(l as f64);
                }

                ColumnType::Labels {
                    sample: self.to_sample(),
                    length_extent,
                    length_histogram,
                }
            },
        )
    }
}

static USAGE: &str = "
Computes descriptive statistics of CSV data.

If you want to print human-readable output, use the -R/--report flag.

Else this command can be used to generate a CSV output that can be easily piped
into other `xan` commands.

By default, statistics are reported for *every* column in the CSV data, but you
can restrict the set of analyzed columns using the -s/--select flag.

The default set of statistics corresponds to things that can be computed efficiently
on a stream in constant memory, but more can be selected using flags documented
hereafter.

Stats can also be computed per group using the -g/--groupby flag.

If you have more specific needs or want to perform custom aggregations, please be
sure to check the `xan agg` or `xan groupby` commands instead.

Here is what the CSV output will look like:

field              (default) - Name of the described column
count              (default) - Number of non-empty values contained by the column
count_empty        (default) - Number of empty values contained by the column
type               (default) - Most likely type of the column
types              (default) - Pipe-separated list of all types witnessed in the column
sum                (default) - Sum of numerical values
mean               (default) - Mean of numerical values
q1                 (-q, -A)  - First quartile of numerical values
median             (-q, -A)  - Second quartile, i.e. median, of numerical values
q3                 (-q, -A)  - Third quartile of numerical values
log_dist           (-q, -A)  - Sparkline (e.g. ▇▅▄▃▂▃▂▂▂▂) representing numerical distribution
variance           (default) - Population variance of numerical values
stddev             (default) - Population standard deviation of numerical values
min                (default) - Minimum numerical value
max                (default) - Maximum numerical value
approx_cardinality (-a)      - Approximation of the number of distinct string values
approx_q1          (-a)      - Approximation of the first quartile of numerical values
approx_median      (-a)      - Approximation of the median of numerical values
approx_q3          (-a)      - Approximation of the third quartile of numerical values
cardinality        (-c, -A)  - Number of distinct string values
mode               (-c, -A)  - Most frequent string value (tie breaking is arbitrary & random!)
tied_for_mode      (-c, -A)  - Number of values tied for mode
lex_first          (default) - First string in lexical order
lex_last           (default) - Last string in lexical order
min_length         (default) - Minimum string length
max_length         (default) - Maximum string length

Stats can be computed in parallel using the -p/--parallel or -t/--threads flags.
But this cannot work on streams or gzipped files, unless a `.gzi` index (as created
by `bgzip -i`) can be found beside it. Parallelization is not compatible
with the -g/--groupby option.

Note that the output of the -R/--report can easily be piped into a pager
thusly (don't forget to force colors):

    $ xan stats -R data.csv --color always | less -SR

Usage:
    xan stats [options] [<input>]

stats options:
    -s, --select <arg>       Select a subset of columns to compute stats for.
                             See 'xan select --help' for the format details.
                             This is provided here because piping 'xan select'
                             into 'xan stats' will disable the use of indexing.
    -R, --report             Print a human-readable output suitable to understand
                             what your columns contain, along with the relevant
                             data visualizations (bar charts, top lists etc.)
                             Does not work with -g/--groupby.
    --sep <str>              Indicate that cells must be split using given separator.
    -g, --groupby <cols>     If given, will compute stats per group as defined by
                             the given column selection.
    -A, --all                Shorthand for -cq.
    -c, --cardinality        Show cardinality and modes.
                             This requires storing all CSV data in memory.
    -q, --quartiles          Show quartiles.
                             This requires storing all CSV data in memory.
    -a, --approx             Compute approximated statistics.
    --nulls                  Include empty values in the population size for computing
                             mean and standard deviation.
    -p, --parallel           Whether to use parallelization to speed up computation.
                             Will automatically select a suitable number of threads to use
                             based on your number of cores. Use -t, --threads if you want to
                             indicate the number of threads yourself.
    -t, --threads <threads>  Parellize computations using this many threads. Use -p, --parallel
                             if you want the number of threads to be automatically chosen instead.

stats -R/--report options:
    --cols <num>    Width of the graph in terminal columns, i.e. characters.
                    Defaults to using all your terminal's width or 80 if
                    terminal's size cannot be found (i.e. when piping to file).
                    Can also be given as a ratio or percentage of the terminal's width
                    e.g. \"45%\" or \"0.5\".
    --color <when>  When to color the output using ANSI escape codes.
                    Use `auto` for automatic detection, `never` to
                    disable colors completely and `always` to force
                    colors, even when the output could not handle them.
                    [default: auto]

Common options:
    -h, --help             Display this message
    -o, --output <file>    Write output to <file> instead of stdout.
    -n, --no-headers       When set, the first row will NOT be interpreted
                           as column names. i.e., They will be included
                           in statistics.
    -d, --delimiter <arg>  The field delimiter for reading CSV data.
                           Must be a single character.
";

#[derive(Clone, Deserialize)]
struct Args {
    arg_input: Option<String>,
    flag_select: SelectedColumns,
    flag_groupby: Option<SelectedColumns>,
    flag_report: bool,
    flag_color: ColorMode,
    flag_cols: Option<String>,
    flag_sep: Option<String>,
    flag_all: bool,
    flag_cardinality: bool,
    flag_quartiles: bool,
    flag_approx: bool,
    flag_nulls: bool,
    flag_parallel: bool,
    flag_threads: Option<NonZeroUsize>,
    flag_output: Option<String>,
    flag_no_headers: bool,
    flag_delimiter: Option<Delimiter>,
}

impl Args {
    fn new_stats(&self) -> Stats {
        let mut stats = Stats::new();

        if self.flag_nulls {
            stats.include_nulls();
        }

        if self.flag_all || self.flag_cardinality {
            stats.compute_frequencies();
        }

        if self.flag_all || self.flag_quartiles {
            stats.compute_numbers();
        }

        if self.flag_approx {
            stats.compute_approx();
        }

        stats
    }
}

pub fn run(argv: &[&str]) -> CliResult<()> {
    let args: Args = util::get_args(USAGE, argv)?;

    if args.flag_parallel || args.flag_threads.is_some() {
        if args.flag_groupby.is_some() {
            Err("-p/--parallel or -t/--threads cannot be used with -g/--groupby!")?;
        }

        let mut parallel_args = ParallelArgs::single_file(&args.arg_input, args.flag_threads)?;

        parallel_args.cmd_stats = true;
        parallel_args.flag_select = args.flag_select;
        parallel_args.flag_all = args.flag_all;
        parallel_args.flag_cardinality = args.flag_cardinality;
        parallel_args.flag_quartiles = args.flag_quartiles;
        parallel_args.flag_approx = args.flag_approx;
        parallel_args.flag_nulls = args.flag_nulls;

        parallel_args.flag_no_headers = args.flag_no_headers;
        parallel_args.flag_output = args.flag_output;
        parallel_args.flag_delimiter = args.flag_delimiter;

        return parallel_args.run();
    }

    let rconf = Config::new(&args.arg_input)
        .delimiter(args.flag_delimiter)
        .no_headers(args.flag_no_headers)
        .select(args.flag_select.clone());

    let mut rdr = rconf.simd_reader()?;
    let mut wtr = Config::new(&args.flag_output).simd_writer()?;

    let headers = rdr.byte_headers()?.clone();
    let mut sel = rconf.selection(&headers)?;
    let groupby_sel_opt = args
        .flag_groupby
        .as_ref()
        .map(|cols| cols.selection(&headers, !rconf.no_headers))
        .transpose()?;

    // No need to consider the grouping column when aggregating stats
    if let Some(gsel) = &groupby_sel_opt {
        sel.subtract(gsel);
    }

    // Nothing was selected
    if sel.is_empty() {
        return Ok(());
    }

    let field_names: Vec<Vec<u8>> = if args.flag_no_headers {
        sel.iter()
            .map(|i| i.to_string().as_bytes().to_vec())
            .collect()
    } else {
        sel.select(&headers).map(|h| h.to_vec()).collect()
    };

    let mut record = ByteRecord::new();

    // Describe
    if args.flag_report {
        args.flag_color.apply();

        if args.flag_groupby.is_some() {
            Err("-R/--report does not work with -g/--groupby!")?;
        }

        let mut out = stdout();
        let cols = util::acquire_term_cols_ratio(&args.flag_cols)?;

        let mut estimators: Vec<_> = sel
            .select(&headers)
            .enumerate()
            .map(|(i, name)| {
                if rconf.no_headers {
                    ColumnEstimator::new(format!("Column n°{}", i).as_bytes())
                } else {
                    ColumnEstimator::new(name)
                }
            })
            .collect();

        while rdr.read_byte_record(&mut record)? {
            for (estimator, cell) in estimators.iter_mut().zip(sel.select(&record)) {
                if let Some(sep) = &args.flag_sep {
                    for sub_cell in cell.split_str(sep) {
                        estimator.process(sub_cell);
                    }
                } else {
                    estimator.process(cell);
                }
            }
        }

        let sep = "─".repeat(cols).dimmed();

        for mut estimator in estimators {
            estimator.finalize();

            let column_type = estimator.infer_type(cols)?;

            writeln!(
                &mut out,
                "{}",
                estimator
                    .name
                    .pad_to_width_with_alignment(cols, Alignment::Left)
                    .on_cyan()
                    .bold()
            )?;

            if args.flag_color.is_never() {
                writeln!(&mut out, "{}", sep)?;
            }

            writeln!(&mut out, "{}", column_type.as_str())?;
            writeln!(&mut out, "cells: {}", format_number(estimator.count).red())?;

            if estimator.empty_count != 0 {
                let ratio = estimator.empty_count as f64 / estimator.count as f64;

                writeln!(
                    &mut out,
                    "empty cells: {} ({:.2}%)",
                    format_number(estimator.empty_count).dimmed(),
                    ratio * 100.0
                )?;
            }

            match column_type {
                ColumnType::Void => {
                    writeln!(
                        &mut out,
                        "\nThere is nothing is this column but the endless depths of the void!\nZilch, nada, rien, keud!"
                    )?;
                }
                ColumnType::Categorical {
                    is_bool,
                    cardinality,
                    top,
                    total,
                } => {
                    let mut remaining = total;
                    let name_hash = estimator.name_hash();

                    for (_, count) in top.iter() {
                        remaining -= *count;
                    }

                    if !is_bool {
                        writeln!(
                            &mut out,
                            "distinct values: {}",
                            format_number(cardinality).red()
                        )?;
                        writeln!(&mut out, "top {} values:", top.len().to_string().red())?;
                    }

                    let max_count_width = top
                        .iter()
                        .map(|(_, count)| format_number(*count).len())
                        .max()
                        .unwrap()
                        .max(format_number(remaining).len());

                    let max_value_cols = cols.saturating_sub(max_count_width).saturating_sub(16);
                    let mut histogram = vec![0.0; top.len() + (remaining > 0) as usize];
                    let mut color_overrides = vec![None; histogram.len()];

                    for (i, (value, count)) in top.iter().enumerate() {
                        let color = util::colorizer_by_rainbow_with_fallback(i, name_hash, value);
                        histogram[i] = *count as f64;
                        color_overrides[i] = Some(color);

                        writeln!(
                            &mut out,
                            " {}  {:>2} {:>width$} {} {}",
                            color.colorize("■"),
                            (i + 1).to_string().dimmed(),
                            format_number(*count).cyan(),
                            format!("{:>6.2}%", (*count as f64 / total as f64) * 100.0).magenta(),
                            util::highlight_problematic_string_features(
                                &util::unicode_aware_ellipsis(
                                    &util::sanitize_text_for_single_line_printing(value),
                                    max_value_cols
                                )
                            )
                            .green(),
                            width = max_count_width
                        )?;
                    }

                    if remaining > 0 {
                        *histogram.last_mut().unwrap() = remaining as f64;
                        *color_overrides.last_mut().unwrap() = Some(ColorOrStyles::dimmed());

                        writeln!(
                            &mut out,
                            " {}     {:>width$} {} {}",
                            "■".dimmed(),
                            format_number(remaining).cyan(),
                            format!("{:>6.2}%", (remaining as f64 / total as f64) * 100.0)
                                .magenta(),
                            util::unicode_aware_ellipsis("<rest>", max_value_cols).dimmed(),
                            width = max_count_width
                        )?;
                    }

                    let sparkline_scale =
                        Scale::new(ScaleType::Linear, (0.0, top[0].1 as f64), (0.0, 1.0));

                    let mut sparkline_renderer_options = SparklineRendererOptions::new();
                    sparkline_renderer_options.height = 5;
                    sparkline_renderer_options.width = 3;

                    let mut sparkline_renderer = sparkline_renderer_options.build();
                    sparkline_renderer.render_with_color_overrides(
                        &sparkline_scale,
                        &histogram,
                        Some(&color_overrides),
                    );

                    writeln!(&mut out, "\n{}", sparkline_renderer)?;

                    for i in 0..top.len() {
                        write!(&mut out, "{:^3}", (i + 1).to_string().dimmed())?;
                    }

                    if remaining > 0 {
                        write!(&mut out, "{:^3}", "r".dimmed())?;
                    }

                    writeln!(&mut out)?;
                }
                ColumnType::Temporal {
                    extent,
                    granularity,
                    series,
                } => {
                    writeln!(
                        &mut out,
                        "from {} to {} (granularity: {})",
                        extent.earliest().unwrap().to_string().cyan(),
                        extent.latest().unwrap().to_string().cyan(),
                        granularity.to_string().green()
                    )?;

                    let sparkline_scale = series.to_scale(ScaleType::Linear).unwrap();

                    let mut sparkline_renderer_options = SparklineRendererOptions::new();
                    sparkline_renderer_options.height = 3;
                    sparkline_renderer_options.set_striped();

                    let mut sparkline_renderer = sparkline_renderer_options.build();
                    sparkline_renderer.render(&sparkline_scale, &series.numbers);

                    writeln!(&mut out, "\n{}", sparkline_renderer)?;
                }
                ColumnType::Numerical {
                    extent,
                    mean,
                    median,
                    stddev,
                    histogram,
                    ..
                } => {
                    writeln!(
                        &mut out,
                        "({} … {}): {} … {}",
                        "min".blue(),
                        "max".red(),
                        format_number(extent.min()).blue(),
                        format_number(extent.max()).red()
                    )?;
                    writeln!(
                        &mut out,
                        "({} ± {}, {}): {} ± {}, {}",
                        "mean".green(),
                        "stddev".magenta(),
                        "median".yellow(),
                        format_number(mean).green(),
                        format_number(stddev).magenta(),
                        format_number(median).yellow(),
                    )?;

                    let scale_type = if histogram.should_use_log_scale() {
                        ScaleType::ln()
                    } else {
                        ScaleType::Linear
                    };

                    let sparkline_scale = Scale::new(
                        scale_type,
                        (
                            if scale_type.disallows_zero() {
                                1.0
                            } else {
                                0.0
                            },
                            histogram.max_value(),
                        ),
                        (0.0, 1.0),
                    );

                    let mut sparkline_renderer_options = SparklineRendererOptions::new();
                    sparkline_renderer_options.height = 5;
                    sparkline_renderer_options.set_striped();

                    let mut sparkline_renderer = sparkline_renderer_options.build();
                    sparkline_renderer.render(&sparkline_scale, &histogram);
                    sparkline_renderer.render_central_tendency(
                        0,
                        histogram.bins(),
                        histogram.discrete_index(mean),
                        histogram.discrete_index(median),
                        histogram.discrete_index(mean - stddev),
                        histogram.discrete_index(mean + stddev),
                    );

                    writeln!(
                        &mut out,
                        "distribution ({} scale, {}/total): {}/{} ({:.2}%)\n{}",
                        scale_type.to_string().cyan(),
                        "highest".red(),
                        format_number(histogram.max_count()).red(),
                        format_number(estimator.count),
                        (histogram.max_count() as f64 / estimator.count as f64) * 100.0,
                        sparkline_renderer
                    )?;
                }
                ColumnType::Labels {
                    sample,
                    length_histogram,
                    length_extent,
                } => {
                    writeln!(
                        &mut out,
                        "First {} non empty values{}:",
                        sample.len(),
                        if sample.iter().any(|value| value.width() >= 500) {
                            " (truncated)"
                        } else {
                            ""
                        }
                    )?;

                    for value in sample {
                        writeln!(
                            &mut out,
                            "  - {}",
                            util::wrap(
                                &util::unicode_aware_ellipsis(&value, 500),
                                cols.saturating_sub(4),
                                4
                            )
                            .green()
                        )?;
                    }

                    if length_extent.is_constant() {
                        writeln!(
                            &mut out,
                            "all have same length: {}",
                            length_extent.min().to_string().red()
                        )?;
                    } else {
                        let scale_type = if length_histogram.should_use_log_scale() {
                            ScaleType::ln()
                        } else {
                            ScaleType::Linear
                        };

                        let sparkline_scale = Scale::new(
                            scale_type,
                            (
                                if scale_type.disallows_zero() {
                                    1.0
                                } else {
                                    0.0
                                },
                                length_histogram.max_value(),
                            ),
                            (0.0, 1.0),
                        );

                        let mut sparkline_renderer_options = SparklineRendererOptions::new();
                        sparkline_renderer_options.height = 3;
                        sparkline_renderer_options.set_striped();

                        let mut sparkline_renderer = sparkline_renderer_options.build();
                        sparkline_renderer.render(&sparkline_scale, &length_histogram);

                        writeln!(
                            &mut out,
                            "length distribution ({} scale, {} … {}): {} … {}",
                            scale_type.to_string().cyan(),
                            "min".blue(),
                            "max".red(),
                            length_extent.min().to_string().blue(),
                            length_extent.max().to_string().red()
                        )?;
                        writeln!(&mut out, "{}", sparkline_renderer)?;
                    }
                }
            };

            writeln!(&mut out, "\n")?;
        }

        return Ok(());
    }

    // Grouping
    if let Some(gsel) = groupby_sel_opt {
        for h in gsel.select(&headers) {
            record.push_field(h);
        }

        record.extend(&args.new_stats().headers());

        wtr.write_byte_record(&record)?;

        let mut groups: ClusteredInsertHashmap<ByteRecord, Vec<Stats>> =
            ClusteredInsertHashmap::new();

        while rdr.read_byte_record(&mut record)? {
            let group_key = gsel.select(&record).collect();

            let fields = groups.insert_with(group_key, || {
                (0..sel.len()).map(|_| args.new_stats()).collect::<Vec<_>>()
            });

            for (cell, stats) in sel.select(&record).zip(fields.iter_mut()) {
                if let Some(sep) = &args.flag_sep {
                    for sub_cell in cell.split_str(sep) {
                        stats.process(sub_cell);
                    }
                } else {
                    stats.process(cell);
                }
            }
        }

        for (group, fields) in groups.into_iter() {
            for (name, stats) in field_names.iter().zip(fields.into_iter()) {
                record.clear();

                for h in group.iter() {
                    record.push_field(h);
                }

                record.extend(&stats.results(name));

                wtr.write_byte_record(&record)?;
            }
        }

        return Ok(wtr.flush()?);
    }

    // No grouping
    let mut fields = (0..sel.len()).map(|_| args.new_stats()).collect::<Vec<_>>();

    wtr.write_byte_record(&fields[0].headers())?;

    while rdr.read_byte_record(&mut record)? {
        for (cell, stats) in sel.select(&record).zip(fields.iter_mut()) {
            if let Some(sep) = &args.flag_sep {
                for sub_cell in cell.split_str(sep) {
                    stats.process(sub_cell);
                }
            } else {
                stats.process(cell);
            }
        }
    }

    for (name, stats) in field_names.into_iter().zip(fields.into_iter()) {
        wtr.write_byte_record(&stats.results(&name))?;
    }

    Ok(wtr.flush()?)
}
