// SPDX-License-Identifier: GPL-2.0
#include "statsvariables.h"
#include "statstranslations.h"
#include "core/dive.h"
#include "core/divemode.h"
#include "core/divesite.h"
#include "core/gas.h"
#include "core/pref.h"
#include "core/qthelper.h" // for get_depth_unit() et al.
#include "core/string-format.h"
#include "core/tag.h"
#include "core/subsurface-time.h"
#include <cmath>
#include <limits>
#include <QLocale>

#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
#define SKIP_EMPTY Qt::SkipEmptyParts
#else
#define SKIP_EMPTY QString::SkipEmptyParts
#endif

static const constexpr double NaN = std::numeric_limits<double>::quiet_NaN();

// Typedefs for year / quarter or month binners
using year_quarter = std::pair<unsigned short, unsigned short>;
using year_month = std::pair<unsigned short, unsigned short>;

// Small helper template: add an item to an unsorted vector, if its not already there.
template<typename T>
static void add_to_vector_unique(std::vector<T> &v, const T &item)
{
	if (std::find(v.begin(), v.end(), item) == v.end())
		v.push_back(item);
}

// Small helper: make a comma separeted list of a vector of QStrings
static QString join_strings(const std::vector<QString> &v)
{
	QString res;
	for (const QString &s: v) {
		if (!res.isEmpty())
			res += ", ";
		res += s;
	}
	return res;
}

// A wrapper around dive site, that caches the name of the dive site
struct DiveSiteWrapper {
	const dive_site *ds;
	QString name;
	DiveSiteWrapper(const dive_site *ds) : ds(ds),
		name(ds ? ds->name : "")
	{
	}
	bool operator<(const DiveSiteWrapper &d2) const {
		if (int cmp = QString::compare(name, d2.name, Qt::CaseInsensitive))
			return cmp < 0;
		return ds < d2.ds; // This is just random, try something better.
	}
	bool operator==(const DiveSiteWrapper &d2) const {
		return ds == d2.ds;
	}
	bool operator!=(const DiveSiteWrapper &d2) const {
		return ds != d2.ds;
	}
	QString format() const {
		return ds ? name : StatsTranslations::tr("no divesite");
	}
};

// Note: usually I dislike functions defined inside class/struct
// declarations ("Java style"). However, for brevity this is done
// in this rather template-heavy source file more or less consistently.

// Templates to define invalid values for variables and test for said values.
// This is used by the binners: returning such a value means "ignore this dive".
template<typename T> T invalid_value();
template<> int invalid_value<int>()
{
	return std::numeric_limits<int>::max();
}
template<> double invalid_value<double>()
{
	return std::numeric_limits<double>::quiet_NaN();
}
template<> QString invalid_value<QString>()
{
	return QString();
}
template<> StatsQuartiles invalid_value<StatsQuartiles>()
{
	double NaN = std::numeric_limits<double>::quiet_NaN();
	return { std::vector<dive *>(), NaN, NaN, NaN, NaN, NaN };
}

static bool is_invalid_value(int i)
{
	return i == std::numeric_limits<int>::max();
}

static bool is_invalid_value(double d)
{
	return std::isnan(d);
}

static bool is_invalid_value(const QString &s)
{
	return s.isEmpty();
}

// Currently, we don't support invalid dates - should we?
static bool is_invalid_value(const year_quarter &)
{
	return false;
}

static bool is_invalid_value(const DiveSiteWrapper &d)
{
	return !d.ds;
}

static bool is_invalid_value(const StatsOperationResults &res)
{
	return !res.isValid();
}

// Describes a gas-content bin. Since we consider O2 and He, these bins
// are effectively two-dimensional. However, they are given a linear order
// by sorting lexicographically.
// This is different from the general type of the gas, which is defined in
// gas.h as the gastype enum. The latter does not bin by fine-grained
// percentages.
struct gas_bin_t {
	// Depending on the gas content, we format the bins differently.
	// Eg. in the absence of helium and inreased oxygen, the bin is
	// formatted as "EAN32". The format is specified by the type enum.
	enum class Type {
		Air,
		Oxygen,
		EAN,
		Trimix
	} type;
	int o2, he;
	static gas_bin_t air() {
		return { Type::Air, 0, 0 };
	}
	static gas_bin_t oxygen() {
		return { Type::Oxygen, 0, 0 };
	}
	static gas_bin_t ean(int o2) {
		return { Type::EAN, o2, 0 };
	}
	static gas_bin_t trimix(int o2, int h2) {
		return { Type::Trimix, o2, h2 };
	}
};

// We never generate gas_bins of invalid gases.
static bool is_invalid_value(const gas_bin_t &)
{
	return false;
}

static bool is_invalid_value(const std::vector<StatsValue> &v)
{
	return v.empty();
}

static bool is_invalid_value(const StatsQuartiles &q)
{
	return q.dives.empty();
}

bool StatsQuartiles::isValid() const
{
	return !is_invalid_value(*this);
}

// Define an ordering for gas types
// invalid < air < ean (including oxygen) < trimix
// The latter two are sorted by (helium, oxygen)
// This is in analogy to the global get_dive_gas() function.
static bool operator<(const gas_bin_t &t1, const gas_bin_t &t2)
{
	if (t1.type != t2.type)
		return (int)t1.type < (int)t2.type;
	switch (t1.type) {
	default:
	case gas_bin_t::Type::Oxygen:
	case gas_bin_t::Type::Air:
		return false;
	case gas_bin_t::Type::EAN:
		return t1.o2 < t2.o2;
	case gas_bin_t::Type::Trimix:
		return std::tie(t1.o2, t1.he) < std::tie(t2.o2, t2.he);
	}
}

static bool operator==(const gas_bin_t &t1, const gas_bin_t &t2)
{
	return std::tie(t1.type, t1.o2, t1.he) ==
	       std::tie(t2.type, t2.o2, t2.he);
}

static bool operator!=(const gas_bin_t &t1, const gas_bin_t &t2)
{
	return !operator==(t1, t2);
}

// First, let's define the virtual destructors of our base classes
StatsBin::~StatsBin()
{
}

StatsBinner::~StatsBinner()
{
}

QString StatsBinner::unitSymbol() const
{
	return QString();
}

StatsVariable::~StatsVariable()
{
}

QString StatsBinner::name() const
{
	return QStringLiteral("N/A"); // Some dummy string that should never reach the UI
}

QString StatsBinner::formatWithUnit(const StatsBin &bin) const
{
	QString unit = unitSymbol();
	QString name = format(bin);
	return unit.isEmpty() ? name : QStringLiteral("%1 %2").arg(name, unit);
}

QString StatsBinner::formatLowerBound(const StatsBin &bin) const
{
	return QStringLiteral("N/A"); // Some dummy string that should never reach the UI
}

QString StatsBinner::formatUpperBound(const StatsBin &bin) const
{
	return QStringLiteral("N/A"); // Some dummy string that should never reach the UI
}

double StatsBinner::lowerBoundToFloat(const StatsBin &bin) const
{
	return 0.0;
}

double StatsBinner::upperBoundToFloat(const StatsBin &bin) const
{
	return 0.0;
}

bool StatsBinner::preferBin(const StatsBin &bin) const
{
	return true;
}

// Default implementation for discrete variables: there are no bins between discrete bins.
std::vector<StatsBinPtr> StatsBinner::bins_between(const StatsBin &bin1, const StatsBin &bin2) const
{
	return {};
}

QString StatsVariable::unitSymbol() const
{
	return {};
}

QString StatsVariable::diveCategories(const dive *) const
{
	return QString();
}

int StatsVariable::decimals() const
{
	return 0;
}

double StatsVariable::toFloat(const dive *d) const
{
	return invalid_value<double>();
}

QString StatsVariable::nameWithUnit() const
{
	QString s = name();
	QString symb = unitSymbol();
	return symb.isEmpty() ? s : QStringLiteral("%1 [%2]").arg(s, symb);
}

QString StatsVariable::nameWithBinnerUnit(const StatsBinner &binner) const
{
	QString s = name();
	QString symb = binner.unitSymbol();
	return symb.isEmpty() ? s : QStringLiteral("%1 [%2]").arg(s, symb);
}

const StatsBinner *StatsVariable::getBinner(int idx) const
{
	std::vector<const StatsBinner *> b = binners();
	if (b.empty())
		return nullptr;
	return idx >= 0 && idx < (int)b.size() ? b[idx] : b[0];
}

std::vector<StatsOperation> StatsVariable::supportedOperations() const
{
	return {};
}

// Attn: The order must correspond to the StatsOperation enum
static const char *operation_names[] = {
	QT_TRANSLATE_NOOP("StatsTranslations", "Median"),
	QT_TRANSLATE_NOOP("StatsTranslations", "Mean"),
	QT_TRANSLATE_NOOP("StatsTranslations", "Time-weighted mean"),
	QT_TRANSLATE_NOOP("StatsTranslations", "Sum"),
	QT_TRANSLATE_NOOP("StatsTranslations", "Minimum"),
	QT_TRANSLATE_NOOP("StatsTranslations", "Maximum")
};

QStringList StatsVariable::supportedOperationNames() const
{
	std::vector<StatsOperation> ops = supportedOperations();
	QStringList res;
	res.reserve(ops.size());
	for (StatsOperation op: ops)
		res.push_back(operationName(op));
	return res;
}

StatsOperation StatsVariable::idxToOperation(int idx) const
{
	std::vector<StatsOperation> ops = supportedOperations();
	if (ops.empty()) {
		qWarning("Stats variable %s does not support operations", qPrintable(name()));
		return StatsOperation::Median; // oops!
	}
	return idx < 0 || idx >= (int)ops.size() ? ops[0] : ops[idx];
}

QString StatsVariable::operationName(StatsOperation op)
{
	int idx = (int)op;
	return idx < 0 || idx >= (int)std::size(operation_names) ? QString()
								 : operation_names[(int)op];
}

double StatsVariable::mean(const std::vector<dive *> &dives) const
{
	StatsOperationResults res = applyOperations(dives);
	return res.isValid() ? res.mean : invalid_value<double>();
}

std::vector<StatsValue> StatsVariable::values(const std::vector<dive *> &dives) const
{
	std::vector<StatsValue> vec;
	vec.reserve(dives.size());
	for (dive *d: dives) {
		double v = toFloat(d);
		if (!is_invalid_value(v))
			vec.push_back({ v, d });
	}
	std::sort(vec.begin(), vec.end(),
		  [](const StatsValue &v1, const StatsValue &v2)
		  { return v1.v < v2.v; });
	return vec;
}

QString StatsVariable::valueWithUnit(const dive *d) const
{
	QLocale loc;
	double v = toFloat(d);
	if (is_invalid_value(v))
		return QStringLiteral("-");
	return QString("%1 %2").arg(loc.toString(v, 'f', decimals()),
				    unitSymbol());
}

// Small helper to calculate quartiles - section of intervals of
// two consecutive elements in a vector. It's not strictly correct
// to interpolate linearly. However, on the one hand we don't know
// the actual distribution, on the other hand for a discrete
// distribution the quartiles are ranges. So what should we do?
static double q1(const StatsValue *v)
{
	return (3.0*v[0].v + v[1].v) / 4.0;
}
static double q2(const StatsValue *v)
{
	return (v[0].v + v[1].v) / 2.0;
}
static double q3(const StatsValue *v)
{
	return (v[0].v + 3.0*v[1].v) / 4.0;
}

StatsQuartiles StatsVariable::quartiles(const std::vector<dive *> &dives) const
{
	return quartiles(values(dives));
}

// This expects the value vector to be sorted!
StatsQuartiles StatsVariable::quartiles(const std::vector<StatsValue> &vec)
{
	int s = (int)vec.size();
	if (s <= 0)
		return invalid_value<StatsQuartiles>();
	std::vector<dive *> dives;
	dives.reserve(vec.size());
	for (const auto &[v, d]: vec)
		dives.push_back(d);

	switch (s % 4) {
	default:
		// gcc doesn't recognize that we catch all possible values. disappointing.
	case 0:
		return { std::move(dives), vec[0].v, q3(&vec[s/4 - 1]), q2(&vec[s/2 - 1]), q1(&vec[s - s/4 - 1]), vec[s - 1].v };
	case 1:
		return { std::move(dives), vec[0].v, vec[s/4].v, vec[s/2].v, vec[s - s/4 - 1].v, vec[s - 1].v };
	case 2:
		return { std::move(dives), vec[0].v, q1(&vec[s/4]), q2(&vec[s/2 - 1]), q3(&vec[s - s/4 - 2]), vec[s - 1].v };
	case 3:
		return { std::move(dives), vec[0].v, q2(&vec[s/4]), vec[s/2].v, q2(&vec[s - s/4 - 2]), vec[s - 1].v };
	}
}

StatsOperationResults StatsVariable::applyOperations(const std::vector<dive *> &dives) const
{
	StatsOperationResults res;
	std::vector<StatsValue> val = values(dives);

	double sumTime = 0.0;
	res.dives.reserve(val.size());
	res.median = quartiles(val).q2;

	if (val.empty())
		return res;

	res.min = std::numeric_limits<double>::max();
	res.max = std::numeric_limits<double>::lowest();
	for (auto [v, d]: val) {
		res.dives.push_back(d);
		res.sum += v;
		res.mean += v;
		sumTime += d->duration.seconds;
		res.timeWeightedMean += v * d->duration.seconds;
		if (v < res.min)
			res.min = v;
		if (v > res.max)
			res.max = v;
	}

	res.mean /= val.size();
	res.timeWeightedMean /= sumTime;
	return res;
}

StatsOperationResults::StatsOperationResults() :
	median(0.0), mean(0.0), timeWeightedMean(0.0), sum(0.0), min(0.0), max(0.0)
{
}

bool StatsOperationResults::isValid() const
{
	return !dives.empty();
}

double StatsOperationResults::get(StatsOperation op) const
{
	switch (op) {
	case StatsOperation::Median: return median;
	case StatsOperation::Mean: return mean;
	case StatsOperation::TimeWeightedMean: return timeWeightedMean;
	case StatsOperation::Sum: return sum;
	case StatsOperation::Min: return min;
	case StatsOperation::Max: return max;
	case StatsOperation::Invalid:
	default: return invalid_value<double>();
	}
}

std::vector<StatsScatterItem> StatsVariable::scatter(const StatsVariable &t2, const std::vector<dive *> &dives) const
{
	std::vector<StatsScatterItem> res;
	res.reserve(dives.size());
	for (dive *d: dives) {
		double v1 = toFloat(d);
		double v2 = t2.toFloat(d);
		if (is_invalid_value(v1) || is_invalid_value(v2))
			continue;
		res.push_back({ v1, v2, d });
	}
	std::sort(res.begin(), res.end(),
		  [](const StatsScatterItem &i1, const StatsScatterItem &i2)
		  { return std::tie(i1.x, i1.y) < std::tie(i2.x, i2.y); }); // use std::tie() for lexicographical comparison
	return res;
}

template <typename T, typename DivesToValueFunc>
std::vector<StatsBinValue<T>> bin_convert(const StatsVariable &variable, const StatsBinner &binner, const std::vector<dive *> &dives,
					  bool fill_empty, DivesToValueFunc func)
{
	std::vector<StatsBinDives> bin_dives = binner.bin_dives(dives, fill_empty);
	std::vector<StatsBinValue<T>> res;
	res.reserve(bin_dives.size());
	for (auto &[bin, dives]: bin_dives) {
		T v = func(dives);
		if (is_invalid_value(v) && (res.empty() || !fill_empty))
			continue;
		res.push_back({ std::move(bin), v });
	}
	if (res.empty())
		return res;

	// Check if we added invalid items at the end.
	// Note: we added at least one valid item.
	auto it = res.end() - 1;
	while (it != res.begin() && is_invalid_value(it->value))
		--it;
	res.erase(it + 1, res.end());
	return res;
}

std::vector<StatsBinQuartiles> StatsVariable::bin_quartiles(const StatsBinner &binner, const std::vector<dive *> &dives, bool fill_empty) const
{
	return bin_convert<StatsQuartiles>(*this, binner, dives, fill_empty,
					   [this](const std::vector<dive *> &d) { return quartiles(d); });
}

std::vector<StatsBinOp> StatsVariable::bin_operations(const StatsBinner &binner, const std::vector<dive *> &dives, bool fill_empty) const
{
	return bin_convert<StatsOperationResults>(*this, binner, dives, fill_empty,
						  [this](const std::vector<dive *> &d) { return applyOperations(d); });
}

std::vector<StatsBinValues> StatsVariable::bin_values(const StatsBinner &binner, const std::vector<dive *> &dives, bool fill_empty) const
{
	return bin_convert<std::vector<StatsValue>>(*this, binner, dives, fill_empty,
						    [this](const std::vector<dive *> &d) { return values(d); });
}

// Silly template, which spares us defining type() member functions.
template<StatsVariable::Type t>
struct StatsVariableTemplate : public StatsVariable {
	Type type() const override { return t; }
};

// A simple bin that is based on copyable value and can be initialized from
// that value. This template spares us from writing one-line constructors.
template<typename Type>
struct SimpleBin : public StatsBin {
	Type value;
	SimpleBin(const Type &v) : value(v) { }

	// This must not be called for different types. It will crash with an exception.
	bool operator<(StatsBin &b) const {
		return value < dynamic_cast<SimpleBin &>(b).value;
	}

	bool operator==(StatsBin &b) const {
		return value == dynamic_cast<SimpleBin &>(b).value;
	}
};
using IntBin = SimpleBin<int>;
using StringBin = SimpleBin<QString>;
using GasTypeBin = SimpleBin<gas_bin_t>;

// A general binner template that works on trivial bins that are based
// on a type that is equality and less-than comparable. The derived class
// must possess:
//  - A to_bin_value() function that turns a dive into a value from
//    which the bins can be constructed.
//  - A lowerBoundToFloatBase() function that turns the value form
//    into a double which is understood by the StatsVariable.
// The bins must possess:
//  - A member variable "value" of the type it is constructed with.
// Note: this uses the curiously recurring template pattern, which I
// dislike, but it is the easiest thing for now.
template<typename Binner, typename Bin>
struct SimpleBinner : public StatsBinner {
public:
	using Type = decltype(Bin::value);
	std::vector<StatsBinDives> bin_dives(const std::vector<dive *> &dives, bool fill_empty) const override;
	const Binner &derived() const {
		return static_cast<const Binner &>(*this);
	}
	const Bin &derived_bin(const StatsBin &bin) const {
		return dynamic_cast<const Bin &>(bin);
	}
};

// Wrapper around std::lower_bound that searches for a value in a
// vector of pairs. Comparison is made with the first element of the pair.
// std::lower_bound does a binary search and this is used to keep a
// vector in ascending order.
template<typename T1, typename T2>
auto pair_lower_bound(std::vector<std::pair<T1, T2>> &v, const T1 &value)
{
	return std::lower_bound(v.begin(), v.end(), value,
	       			[] (const std::pair<T1, T2> &entry, const T1 &value) {
					return entry.first < value;
				});
}

// Register a dive in a (bin_value, value) pair. The second value can be
// anything, for example a count or a list of dives. If the bin does not
// exist, it is created. The add_dive_func() function increase the second
// value accordingly.
template<typename BinValueType, typename ValueType, typename AddDiveFunc>
void register_bin_value(std::vector<std::pair<BinValueType, ValueType>> &v,
			const BinValueType &bin,
			AddDiveFunc add_dive_func)
{
	// Does that value already exist?
	auto it = pair_lower_bound(v, bin);
	if (it == v.end() || it->first != bin)
		it = v.insert(it, { bin, ValueType() });	// Bin does not exist -> insert at proper location.
	add_dive_func(it->second);				// Register dive
}

// Turn a (bin-value, value)-pair vector into a (bin, value)-pair vector.
// The values are moved out of the first vectors.
// If fill_empty is true, missing bins will be completed with a default constructed
// value.
template<typename Bin, typename Binner, typename BinValueType, typename ValueType>
std::vector<StatsBinValue<ValueType>>
value_vector_to_bin_vector(const Binner &binner, std::vector<std::pair<BinValueType, ValueType>> &value_bins,
						      bool fill_empty)
{
	std::vector<StatsBinValue<ValueType>> res;
	res.reserve(value_bins.size());
	for (const auto &[bin_value, value]: value_bins) {
		StatsBinPtr b = std::make_unique<Bin>(bin_value);
		if (fill_empty && !res.empty()) {
			// Add empty bins, if any
			for (StatsBinPtr &bin: binner.bins_between(*res.back().bin, *b))
				res.push_back({ std::move(bin), ValueType() });
		}
		res.push_back({ std::move(b), std::move(value)});
	}
	return res;
}

template<typename Binner, typename Bin>
std::vector<StatsBinDives> SimpleBinner<Binner, Bin>::bin_dives(const std::vector<dive *> &dives, bool fill_empty) const
{
	// First, collect a value / dives vector and then produce the final vector
	// out of that. I wonder if that is premature optimization?
	using Pair = std::pair<Type, std::vector<dive *>>;
	std::vector<Pair> value_bins;
	for (dive *d: dives) {
		Type value = derived().to_bin_value(d);
		if (is_invalid_value(value))
			continue;
		register_bin_value(value_bins, value,
				   [d](std::vector<dive *> &v) { v.push_back(d); });
	}

	// Now, turn that into our result array with allocated bin objects.
	return value_vector_to_bin_vector<Bin>(*this, value_bins, fill_empty);
}

// A simple binner (see above) that works on continuous (or numeric) variables
// and can return bin-ranges. The binner must implement an inc() function
// that turns a bin into the next-higher bin.
template<typename Binner, typename Bin>
struct SimpleContinuousBinner : public SimpleBinner<Binner, Bin>
{
	using SimpleBinner<Binner, Bin>::derived;
	std::vector<StatsBinPtr> bins_between(const StatsBin &bin1, const StatsBin &bin2) const override;

	// By default the value gives the lower bound, so the format is the same
	QString formatLowerBound(const StatsBin &bin) const override {
		return derived().format(bin);
	}

	// For the upper bound, simply go to the next bin
	QString formatUpperBound(const StatsBin &bin) const override {
		Bin b = SimpleBinner<Binner,Bin>::derived_bin(bin);
		derived().inc(b);
		return formatLowerBound(b);
	}

	// Cast to base value type so that the derived class doesn't have to do it
	double lowerBoundToFloat(const StatsBin &bin) const override {
		const Bin &b = SimpleBinner<Binner,Bin>::derived_bin(bin);
		return derived().lowerBoundToFloatBase(b.value);
	}

	// For the upper bound, simply go to the next bin
	double upperBoundToFloat(const StatsBin &bin) const override {
		Bin b = SimpleBinner<Binner,Bin>::derived_bin(bin);
		derived().inc(b);
		return derived().lowerBoundToFloatBase(b.value);
	}
};

// A continuous binner, where the bin is based on an integer value
// and subsequent bins are adjacent integers.
template<typename Binner, typename Bin>
struct IntBinner : public SimpleContinuousBinner<Binner, Bin>
{
	void inc(Bin &bin) const {
		++bin.value;
	}
};

// An integer based binner, where each bin represents an integer
// range with a fixed size.
template<typename Binner, typename Bin>
struct IntRangeBinner : public IntBinner<Binner, Bin> {
	int bin_size;
	IntRangeBinner(int size)
		: bin_size(size)
	{
	}
	QString format(const StatsBin &bin) const override {
		int value = IntBinner<Binner, Bin>::derived_bin(bin).value;
		QLocale loc;
		return StatsTranslations::tr("%1–%2").arg(loc.toString(value * bin_size),
							  loc.toString((value + 1) * bin_size));
	}
	QString formatLowerBound(const StatsBin &bin) const override {
		int value = IntBinner<Binner, Bin>::derived_bin(bin).value;
		return QStringLiteral("%L1").arg(value * bin_size);
	}
	double lowerBoundToFloatBase(int value) const {
		return static_cast<double>(value * bin_size);
	}
};

template<typename Binner, typename Bin>
std::vector<StatsBinPtr> SimpleContinuousBinner<Binner, Bin>::bins_between(const StatsBin &bin1, const StatsBin &bin2) const
{
	const Bin &b1 = SimpleBinner<Binner,Bin>::derived_bin(bin1);
	const Bin &b2 = SimpleBinner<Binner,Bin>::derived_bin(bin2);
	std::vector<StatsBinPtr> res;
	Bin act = b1;
	derived().inc(act);
	while (act.value < b2.value) {
		res.push_back(std::make_unique<Bin>(act));
		derived().inc(act);
	}
	return res;
}

// A binner template for discrete variables that where each dive can belong to
// multiple bins. The bin-type must be less-than comparable. The derived class
// must possess:
//  - A to_bin_values() function that turns a dive into a value from
//    which the bins can be constructed.
// The bins must possess:
//  - A member variable "value" of the type it is constructed with.
template<typename Binner, typename Bin>
struct MultiBinner : public StatsBinner {
public:
	using Type = decltype(Bin::value);
	std::vector<StatsBinDives> bin_dives(const std::vector<dive *> &dives, bool fill_empty) const override;
	const Binner &derived() const {
		return static_cast<const Binner &>(*this);
	}
	const Bin &derived_bin(const StatsBin &bin) const {
		return dynamic_cast<const Bin &>(bin);
	}
};

template<typename Binner, typename Bin>
std::vector<StatsBinDives> MultiBinner<Binner, Bin>::bin_dives(const std::vector<dive *> &dives, bool) const
{
	// First, collect a value / dives vector and then produce the final vector
	// out of that. I wonder if that is premature optimization?
	using Pair = std::pair<Type, std::vector<dive *>>;
	std::vector<Pair> value_bins;
	for (dive *d: dives) {
		for (const Type &val: derived().to_bin_values(d)) {
			if (is_invalid_value(val))
				continue;
			register_bin_value(value_bins, val,
					   [d](std::vector<dive *> &v) { v.push_back(d); });
		}
	}

	// Now, turn that into our result array with allocated bin objects.
	return value_vector_to_bin_vector<Bin>(*this, value_bins, false);
}

// A binner that works on string-based bins whereby each dive can
// produce multiple strings (e.g. dive buddies). The binner must
// feature a to_bin_values() function that produces a vector of
// QStrings and bins that can be constructed from QStrings.
// Other than that, see SimpleBinner.
template<typename Binner, typename Bin>
struct StringBinner : public MultiBinner<Binner, Bin> {
public:
	QString format(const StatsBin &bin) const override {
		return dynamic_cast<const Bin &>(bin).value;
	}
};

// ============ The date of the dive by year, quarter or month ============
// (Note that calendar week is defined differently in different parts of the world and therefore omitted for now)

double date_to_double(int year, int month, int day)
{
	struct tm tm = { 0 };
	tm.tm_year = year;
	tm.tm_mon = month;
	tm.tm_mday = day;
	timestamp_t t = utc_mktime(&tm);
	return t / 86400.0; // Turn seconds since 1970 to days since 1970, if that makes sense...?
}

struct DateYearBinner : public IntBinner<DateYearBinner, IntBin> {
	QString name() const override {
		return StatsTranslations::tr("Yearly");
	}
	QString format(const StatsBin &bin) const override {
		return QString::number(derived_bin(bin).value);
	}
	int to_bin_value(const dive *d) const {
		return utc_year(d->when);
	}
	double lowerBoundToFloatBase(int year) const {
		return date_to_double(year, 0, 0);
	}
};

using DateQuarterBin = SimpleBin<year_quarter>;

struct DateQuarterBinner : public SimpleContinuousBinner<DateQuarterBinner, DateQuarterBin> {
	QString name() const override {
		return StatsTranslations::tr("Quarterly");
	}
	QString format(const StatsBin &bin) const override {
		year_quarter value = derived_bin(bin).value;
		return StatsTranslations::tr("%1 Q%2").arg(QString::number(value.first),
							   QString::number(value.second));
	}
	// As histogram axis: show full year for new years and then Q2, Q3, Q4.
	QString formatLowerBound(const StatsBin &bin) const override {
		year_quarter value = derived_bin(bin).value;
		return value.second == 1
			? QString::number(value.first)
			: StatsTranslations::tr("Q%1").arg(QString::number(value.second));
	}
	double lowerBoundToFloatBase(year_quarter value) const {
		return date_to_double(value.first, (value.second - 1) * 3, 0);
	}
	// Prefer bins that show full years
	bool preferBin(const StatsBin &bin) const override {
		year_quarter value = derived_bin(bin).value;
		return value.second == 1;
	}
	year_quarter to_bin_value(const dive *d) const {
		struct tm tm;
		utc_mkdate(d->when, &tm);

		int year = tm.tm_year;
		switch (tm.tm_mon) {
		case 0 ... 2: return { year, 1 };
		case 3 ... 5: return { year, 2 };
		case 6 ... 8: return { year, 3 };
		default:      return { year, 4 };
		}
	}
	void inc(DateQuarterBin &bin) const {
		if (++bin.value.second > 4) {
			bin.value.second = 1;
			++bin.value.first;
		}
	}
};

using DateMonthBin = SimpleBin<year_month>;

struct DateMonthBinner : public SimpleContinuousBinner<DateMonthBinner, DateMonthBin> {
	QString name() const override {
		return StatsTranslations::tr("Monthly");
	}
	QString format(const StatsBin &bin) const override {
		year_month value = derived_bin(bin).value;
		return QString("%1 %2").arg(monthname(value.second), QString::number(value.first));
	}
	// In histograms, output year for fill years, month otherwise
	QString formatLowerBound(const StatsBin &bin) const override {
		year_month value = derived_bin(bin).value;
		return value.second == 0 ? QString::number(value.first)
					 : QString(monthname(value.second));
	}
	double lowerBoundToFloatBase(year_quarter value) const {
		return date_to_double(value.first, value.second, 0);
	}
	// Prefer bins that show full years
	bool preferBin(const StatsBin &bin) const override {
		year_month value = derived_bin(bin).value;
		return value.second == 0;
	}
	year_month to_bin_value(const dive *d) const {
		struct tm tm;
		utc_mkdate(d->when, &tm);
		return { tm.tm_year, tm.tm_mon };
	}
	void inc(DateMonthBin &bin) const {
		if (++bin.value.second > 11) {
			bin.value.second = 0;
			++bin.value.first;
		}
	}
};

static DateYearBinner date_year_binner;
static DateQuarterBinner date_quarter_binner;
static DateMonthBinner date_month_binner;
struct DateVariable : public StatsVariableTemplate<StatsVariable::Type::Continuous> {
	QString name() const {
		return StatsTranslations::tr("Date");
	}
	double toFloat(const dive *d) const override {
		return d->when / 86400.0;
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &date_year_binner, &date_quarter_binner, &date_month_binner };
	}
};

// ============ Dive depth (max and mean), binned in 5, 10, 20 m or 15, 30, 60 ft bins ============

static int dive_depth_in_mm(const dive *d, bool mean)
{
	return mean ? d->meandepth.mm : d->maxdepth.mm;
}

struct DepthBinner : public IntRangeBinner<DepthBinner, IntBin> {
	bool metric;
	bool mean;
	DepthBinner(int bin_size, bool metric, bool mean) :
		IntRangeBinner(bin_size), metric(metric), mean(mean)
	{
	}
	QString name() const override {
		QLocale loc;
		return StatsTranslations::tr("in %1 %2 steps").arg(loc.toString(bin_size),
								   get_depth_unit(metric));
	}
	QString unitSymbol() const override {
		return get_depth_unit(metric);
	}
	int to_bin_value(const dive *d) const {
		int depth = dive_depth_in_mm(d, mean);
		return metric ? depth / 1000 / bin_size
			      : lrint(mm_to_feet(depth)) / bin_size;
	}
};

struct DepthVariableBase : public StatsVariableTemplate<StatsVariable::Type::Numeric> {
	bool mean;
	DepthBinner meter5, meter10, meter20;
	DepthBinner feet15, feet30, feet60;
	DepthVariableBase(bool mean) :
		mean(mean),
		meter5(5, true, mean), meter10(10, true, mean), meter20(20, true, mean),
		feet15(15, false, mean), feet30(30, false, mean), feet60(60, false, mean)
	{
	}
	QString unitSymbol() const override {
		return get_depth_unit();
	}
	int decimals() const override {
		return 1;
	}
	std::vector<const StatsBinner *> binners() const override {
		if (prefs.units.length == units::METERS)
			return { &meter5, &meter10, &meter20 };
		else
			return { &feet15, &feet30, &feet60 };
	}
	double toFloat(const dive *d) const override {
		int depth = dive_depth_in_mm(d, mean);
		return prefs.units.length == units::METERS ? depth / 1000.0
							   : mm_to_feet(depth);
	}
};

struct MaxDepthVariable : public DepthVariableBase {
	MaxDepthVariable() : DepthVariableBase(false) {
	}
	QString name() const override {
		return StatsTranslations::tr("Max. Depth");
	}
	std::vector<StatsOperation> supportedOperations() const override {
		return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::Sum, StatsOperation::Min, StatsOperation::Max };
	}
};

struct MeanDepthVariable : public DepthVariableBase {
	MeanDepthVariable() : DepthVariableBase(true)
	{
	}
	QString name() const override {
		return StatsTranslations::tr("Mean Depth");
	}
	std::vector<StatsOperation> supportedOperations() const override {
		return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::TimeWeightedMean, StatsOperation::Min, StatsOperation::Max };
	}
};

// ============ Bottom time, binned in 5, 10, 30 min or 1 h bins ============

struct MinuteBinner : public IntRangeBinner<MinuteBinner, IntBin> {
	using IntRangeBinner::IntRangeBinner;
	QString name() const override {
		return StatsTranslations::tr("in %1 min steps").arg(bin_size);
	}
	QString unitSymbol() const override {
		return StatsTranslations::tr("min");
	}
	int to_bin_value(const dive *d) const {
		return d->duration.seconds / 60 / bin_size;
	}
};

struct HourBinner : public IntBinner<HourBinner, IntBin> {
	QString name() const override {
		return StatsTranslations::tr("in hours");
	}
	QString format(const StatsBin &bin) const override {
		return QString::number(derived_bin(bin).value);
	}
	QString unitSymbol() const override {
		return StatsTranslations::tr("h");
	}
	int to_bin_value(const dive *d) const {
		return d->duration.seconds / 3600;
	}
	double lowerBoundToFloatBase(int hour) const {
		return static_cast<double>(hour);
	}
};

static MinuteBinner minute_binner5(5);
static MinuteBinner minute_binner10(10);
static MinuteBinner minute_binner30(30);
static HourBinner hour_binner;
struct DurationVariable : public StatsVariableTemplate<StatsVariable::Type::Numeric> {
	QString name() const override {
		return StatsTranslations::tr("Duration");
	}
	QString unitSymbol() const override {
		return StatsTranslations::tr("min");
	}
	int decimals() const override {
		return 0;
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &minute_binner5, &minute_binner10, &minute_binner30, &hour_binner };
	}
	double toFloat(const dive *d) const override {
		return d->duration.seconds / 60.0;
	}
	std::vector<StatsOperation> supportedOperations() const override {
		return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::Sum, StatsOperation::Min, StatsOperation::Max };
	}
};

// ============ SAC, binned in 2, 5, 10 l/min or 0.1, 0.2, 0.4, 0.8 cuft/min bins ============

struct MetricSACBinner : public IntRangeBinner<MetricSACBinner, IntBin> {
	using IntRangeBinner::IntRangeBinner;
	QString name() const override {
		QLocale loc;
		return StatsTranslations::tr("in %1 %2/min steps").arg(loc.toString(bin_size),
								       get_volume_unit());
	}
	QString unitSymbol() const override {
		return get_volume_unit(true) + StatsTranslations::tr("/min");
	}
	int to_bin_value(const dive *d) const {
		if (d->sac <= 0)
			return invalid_value<int>();
		return d->sac / 1000 / bin_size;
	}
};

// "Imperial" SACs are annoying, since we have to bin to sub-integer precision.
// We store cuft * 100 as an integer, to avoid troubles with floating point semantics.

struct ImperialSACBinner : public IntBinner<ImperialSACBinner, IntBin> {
	int bin_size;
	ImperialSACBinner(double size)
		: bin_size(lrint(size * 100.0))
	{
	}
	QString name() const override {
		QLocale loc;
		return StatsTranslations::tr("in %1 %2/min steps").arg(loc.toString(bin_size / 100.0, 'f', 2),
								       get_volume_unit());
	}
	QString format(const StatsBin &bin) const override {
		int value = derived_bin(bin).value;
		QLocale loc;
		return StatsTranslations::tr("%1–%2").arg(loc.toString((value * bin_size) / 100.0, 'f', 2),
							  loc.toString(((value + 1) * bin_size) / 100.0, 'f', 2));
	}
	QString unitSymbol() const override {
		return get_volume_unit(false) + StatsTranslations::tr("/min");
	}
	QString formatLowerBound(const StatsBin &bin) const override {
		int value = derived_bin(bin).value;
		return QStringLiteral("%L1").arg((value * bin_size) / 100.0, 0, 'f', 2);
	}
	double lowerBoundToFloatBase(int value) const {
		return static_cast<double>((value * bin_size) / 100.0);
	}
	int to_bin_value(const dive *d) const {
		if (d->sac <= 0)
			return invalid_value<int>();
		return lrint(ml_to_cuft(d->sac) * 100.0) / bin_size;
	}
};

MetricSACBinner metric_sac_binner2(2);
MetricSACBinner metric_sac_binner5(5);
MetricSACBinner metric_sac_binner10(10);
ImperialSACBinner imperial_sac_binner1(0.1);
ImperialSACBinner imperial_sac_binner2(0.2);
ImperialSACBinner imperial_sac_binner4(0.4);
ImperialSACBinner imperial_sac_binner8(0.8);

struct SACVariable : public StatsVariableTemplate<StatsVariable::Type::Numeric> {
	QString name() const override {
		return StatsTranslations::tr("SAC");
	}
	QString unitSymbol() const override {
		return get_volume_unit() + StatsTranslations::tr("/min");
	}
	int decimals() const override {
		return prefs.units.volume == units::LITER ? 0 : 2;
	}
	std::vector<const StatsBinner *> binners() const override {
		if (prefs.units.volume == units::LITER)
			return { &metric_sac_binner2, &metric_sac_binner5, &metric_sac_binner10 };
		else
			return { &imperial_sac_binner1, &imperial_sac_binner2, &imperial_sac_binner4, &imperial_sac_binner8 };
	}
	double toFloat(const dive *d) const override {
		if (d->sac <= 0)
			return invalid_value<double>();
		return prefs.units.volume == units::LITER ? d->sac / 1000.0 :
							    ml_to_cuft(d->sac);
	}
	std::vector<StatsOperation> supportedOperations() const override {
		return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::TimeWeightedMean, StatsOperation::Min, StatsOperation::Max };
	}
};

// ============ Water and air temperature, binned in 2, 5, 10, 20 °C/°F bins ============

struct TemperatureBinner : public IntRangeBinner<TemperatureBinner, IntBin> {
	bool air;
	bool metric;
	TemperatureBinner(int bin_size, bool air, bool metric) :
		IntRangeBinner(bin_size),
		air(air),
		metric(metric)
	{
	}
	QString name() const override {
		QLocale loc;
		return StatsTranslations::tr("in %1 %2 steps").arg(loc.toString(bin_size),
								   get_temp_unit(metric));
	}
	QString unitSymbol() const override {
		return get_temp_unit(metric);
	}
	int to_bin_value(const struct dive *d) const {
		temperature_t t = air ? d->airtemp : d->watertemp;
		if (t.mkelvin <= 0)
			return invalid_value<int>();
		int temp = metric ?  static_cast<int>(mkelvin_to_C(t.mkelvin))
				  :  static_cast<int>(mkelvin_to_F(t.mkelvin));
		return temp / bin_size;
	}
};

struct TemperatureVariable : public StatsVariableTemplate<StatsVariable::Type::Numeric> {
	TemperatureBinner bin2C, bin5C, bin10C, bin20C;
	TemperatureBinner bin2F, bin5F, bin10F, bin20F;
	TemperatureVariable(bool air) :
		bin2C(2, air, true), bin5C(5, air, true), bin10C(10, air, true), bin20C(20, air, true),
		bin2F(2, air, false), bin5F(5, air, false), bin10F(10, air, false), bin20F(20, air, false)
	{
	}
	QString unitSymbol() const override {
		return get_temp_unit();
	}
	int decimals() const override {
		return 1;
	}
	double tempToFloat(temperature_t t) const {
		if (t.mkelvin <= 0)
			return invalid_value<double>();
		return prefs.units.temperature == units::CELSIUS ?
				mkelvin_to_C(t.mkelvin) : mkelvin_to_F(t.mkelvin);
	}
	std::vector<const StatsBinner *> binners() const override {
		if (prefs.units.temperature == units::CELSIUS)
			return { &bin2C, &bin5C, &bin10C, &bin20C };
		else
			return { &bin2F, &bin5F, &bin10F, &bin20F };
	}
	std::vector<StatsOperation> supportedOperations() const override {
		return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::TimeWeightedMean, StatsOperation::Min, StatsOperation::Max };
	}
};

struct WaterTemperatureVariable : TemperatureVariable {
	WaterTemperatureVariable() : TemperatureVariable(false)
	{
	}
	QString name() const override {
		return StatsTranslations::tr("Water temperature");
	}
	double toFloat(const dive *d) const override {
		return tempToFloat(d->watertemp);
	}
};

struct AirTemperatureVariable : TemperatureVariable {
	AirTemperatureVariable() : TemperatureVariable(true)
	{
	}
	QString name() const override {
		return StatsTranslations::tr("Air temperature");
	}
	double toFloat(const dive *d) const override {
		return tempToFloat(d->airtemp);
	}
};

// ============ Weight, binned in 1, 2, 5, 10 kg or 2, 4, 10 or 20 lbs bins ============

struct WeightBinner : public IntRangeBinner<WeightBinner, IntBin> {
	bool metric;
	WeightBinner(int bin_size, bool metric) : IntRangeBinner(bin_size), metric(metric)
	{
	}
	QString name() const override {
		QLocale loc;
		return StatsTranslations::tr("in %1 %2 steps").arg(loc.toString(bin_size),
								   get_weight_unit(metric));
	}
	QString unitSymbol() const override {
		return get_weight_unit(metric);
	}
	int to_bin_value(const dive *d) const {
		return metric ? total_weight(d) / 1000 / bin_size
			      : lrint(grams_to_lbs(total_weight(d))) / bin_size;
	}
};

static WeightBinner weight_binner_1kg(1, true);
static WeightBinner weight_binner_2kg(2, true);
static WeightBinner weight_binner_5kg(5, true);
static WeightBinner weight_binner_10kg(10, true);
static WeightBinner weight_binner_2lbs(2, false);
static WeightBinner weight_binner_5lbs(4, false);
static WeightBinner weight_binner_10lbs(10, false);
static WeightBinner weight_binner_20lbs(20, false);

struct WeightVariable : public StatsVariableTemplate<StatsVariable::Type::Numeric> {
	QString name() const override {
		return StatsTranslations::tr("Weight");
	}
	QString unitSymbol() const override {
		return get_weight_unit();
	}
	int decimals() const override {
		return 1;
	}
	std::vector<const StatsBinner *> binners() const override {
		if (prefs.units.weight == units::KG)
			return { &weight_binner_1kg, &weight_binner_2kg, &weight_binner_5kg, &weight_binner_10kg };
		else
			return { &weight_binner_2lbs, &weight_binner_5lbs, &weight_binner_10lbs, &weight_binner_20lbs };
	}
	double toFloat(const dive *d) const override {
		return prefs.units.weight == units::KG ? total_weight(d) / 1000.0
						       : grams_to_lbs(total_weight(d));
	}
	std::vector<StatsOperation> supportedOperations() const override {
		return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::Sum, StatsOperation::Min, StatsOperation::Max };
	}
};

// ============ Dive number ============

// Binning dive numbers can't use the IntRangeBinner,
// because dive numbers start at 1, not 0! Since 0 means
// "no number", these dives aren't registered.
struct DiveNrBinner : public IntBinner<DiveNrBinner, IntBin> {
	int bin_size;
	DiveNrBinner(int bin_size) : bin_size(bin_size)
	{
	}
	QString format(const StatsBin &bin) const override {
		int value = derived_bin(bin).value;
		QLocale loc;
		return StatsTranslations::tr("%1–%2").arg(loc.toString(value * bin_size + 1),
							  loc.toString((value + 1) * bin_size));
	}
	QString formatLowerBound(const StatsBin &bin) const override {
		int value = derived_bin(bin).value;
		return QStringLiteral("%L1").arg(value * bin_size + 1);
	}
	double lowerBoundToFloatBase(int value) const {
		return static_cast<double>(value * bin_size + 1);
	}
	QString name() const override {
		return StatsTranslations::tr("in %L2 steps").arg(bin_size);
	}
	int to_bin_value(const dive *d) const {
		if (d->number <= 0)
			return invalid_value<int>();
		return (d->number - 1) / bin_size;
	}
};

static DiveNrBinner dive_nr_binner_5(5);
static DiveNrBinner dive_nr_binner_10(10);
static DiveNrBinner dive_nr_binner_20(20);
static DiveNrBinner dive_nr_binner_50(50);
static DiveNrBinner dive_nr_binner_100(100);
static DiveNrBinner dive_nr_binner_200(200);

struct DiveNrVariable : public StatsVariableTemplate<StatsVariable::Type::Numeric> {
	QString name() const override {
		return StatsTranslations::tr("Dive #");
	}
	std::vector<const StatsBinner *> binners() const override {
		if (dive_table.nr > 1000)
			return { &dive_nr_binner_20, &dive_nr_binner_50, &dive_nr_binner_100, &dive_nr_binner_200 };
		else
			return { &dive_nr_binner_5, &dive_nr_binner_10, &dive_nr_binner_20, &dive_nr_binner_50 };
	}
	double toFloat(const dive *d) const override {
		return d->number >= 0 ? static_cast<double>(d->number)
				      : invalid_value<double>();
	}
	std::vector<StatsOperation> supportedOperations() const override {
		return { StatsOperation::Median, StatsOperation::Mean };
	}
};

// ============ Dive mode ============

struct DiveModeBinner : public SimpleBinner<DiveModeBinner, IntBin> {
	QString format(const StatsBin &bin) const override {
		return QString(divemode_text_ui[derived_bin(bin).value]);
	}
	int to_bin_value(const dive *d) const {
		int res = (int)d->dc.divemode;
		return res >= 0 && res < NUM_DIVEMODE ? res : OC;
	}
};

static DiveModeBinner dive_mode_binner;
struct DiveModeVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("Dive mode");
	}
	QString diveCategories(const dive *d) const override {
		int mode = (int)d->dc.divemode;
		return mode >= 0 && mode < NUM_DIVEMODE ?
			QString(divemode_text_ui[mode]) : QString();
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &dive_mode_binner };
	}
};

// ============ People, buddies and dive guides ============

struct PeopleBinner : public StringBinner<PeopleBinner, StringBin> {
	std::vector<QString> to_bin_values(const dive *d) const {
		std::vector<QString> dive_people;
		for (const QString &s: QString(d->buddy).split(",", SKIP_EMPTY))
			dive_people.push_back(s.trimmed());
		for (const QString &s: QString(d->divemaster).split(",", SKIP_EMPTY))
			dive_people.push_back(s.trimmed());
		return dive_people;
	}
};

static PeopleBinner people_binner;
struct PeopleVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("People");
	}
	QString diveCategories(const dive *d) const override {
		QString buddy = QString(d->buddy).trimmed();
		QString divemaster = QString(d->divemaster).trimmed();
		if (!buddy.isEmpty() && !divemaster.isEmpty())
			buddy += ", ";
		return buddy + divemaster;
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &people_binner };
	}
};

struct BuddyBinner : public StringBinner<BuddyBinner, StringBin> {
	std::vector<QString> to_bin_values(const dive *d) const {
		std::vector<QString> buddies;
		for (const QString &s: QString(d->buddy).split(",", SKIP_EMPTY))
			buddies.push_back(s.trimmed());
		return buddies;
	}
};

static BuddyBinner buddy_binner;
struct BuddyVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("Buddies");
	}
	QString diveCategories(const dive *d) const override {
		return QString(d->buddy).trimmed();
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &buddy_binner };
	}
};

struct DiveGuideBinner : public StringBinner<DiveGuideBinner, StringBin> {
	std::vector<QString> to_bin_values(const dive *d) const {
		std::vector<QString> dive_guides;
		for (const QString &s: QString(d->divemaster).split(",", SKIP_EMPTY))
			dive_guides.push_back(s.trimmed());
		return dive_guides;
	}
};

static DiveGuideBinner dive_guide_binner;
struct DiveGuideVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("Dive guides");
	}
	QString diveCategories(const dive *d) const override {
		return QString(d->divemaster).trimmed();
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &dive_guide_binner };
	}
};

// ============ Tags ============

struct TagBinner : public StringBinner<TagBinner, StringBin> {
	std::vector<QString> to_bin_values(const dive *d) const {
		std::vector<QString> tags;
		for (const tag_entry *tag = d->tag_list; tag; tag = tag->next)
			tags.push_back(QString(tag->tag->name).trimmed());
		return tags;
	}
};

static TagBinner tag_binner;
struct TagVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("Tags");
	}
	QString diveCategories(const dive *d) const override {
		return get_taglist_string(d->tag_list);
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &tag_binner };
	}
};

// ============ Gas type, in 2%, 5%, 10% and 20% steps  ============
// This is a bit convoluted: We differentiate between four types: air, pure oxygen, EAN and trimix
// The latter two are binned in x% steps. The problem is that we can't use the "simple binner",
// because a dive can have more than one cylinder. Moreover, the string-binner might not be optimal
// because we don't want to format a string for every gas types, when there are thousands of dives!
// Note: when the same dive has multiple cylinders that fall inside a bin, that cylinder will
// only be counted once. Thus, depending on the bin size, the number of entries may change!
// In addition to a binner with percent-steps also provide a general-type binner (air, nitrox, etc.)

// bin gasmix with size given in percent
struct gas_bin_t bin_gasmix(struct gasmix mix, int size)
{
	if (gasmix_is_air(mix))
		return gas_bin_t::air();
	if (mix.o2.permille == 1000)
		return gas_bin_t::oxygen();
	return mix.he.permille == 0 ?
		gas_bin_t::ean(mix.o2.permille / 10 / size * size) :
		gas_bin_t::trimix(mix.o2.permille / 10 / size * size,
				  mix.he.permille / 10 / size * size);
}

struct GasTypeBinner : public MultiBinner<GasTypeBinner, GasTypeBin> {
	int bin_size;
	GasTypeBinner(int size)
		: bin_size(size)
	{
	}
	QString name() const override {
		return StatsTranslations::tr("in %1% steps").arg(bin_size);
	}
	std::vector<gas_bin_t> to_bin_values(const dive *d) const {
		std::vector<gas_bin_t> res;
		res.reserve(d->cylinders.nr);
		for (int i = 0; i < d->cylinders.nr; ++i) {
			struct gasmix mix = d->cylinders.cylinders[i].gasmix;
			if (gasmix_is_invalid(mix))
				continue;
			// Add dive to each bin only once.
			add_to_vector_unique(res, bin_gasmix(mix, bin_size));
		}
		return res;
	}
	QString format(const StatsBin &bin) const override {
		gas_bin_t type = derived_bin(bin).value;
		QLocale loc;
		switch (type.type) {
		default:
		case gas_bin_t::Type::Air:
			return StatsTranslations::tr("Air");
		case gas_bin_t::Type::Oxygen:
			return StatsTranslations::tr("Oxygen");
		case gas_bin_t::Type::EAN:
			return StatsTranslations::tr("EAN%1–%2").arg(loc.toString(type.o2),
								     loc.toString(type.o2 + bin_size - 1));
		case gas_bin_t::Type::Trimix:
			return StatsTranslations::tr("%1/%2–%3/%4").arg(loc.toString(type.o2),
									loc.toString(type.he),
									loc.toString(type.o2 + bin_size - 1),
									loc.toString(type.he + bin_size - 1));
		}
	}
};

struct GasTypeGeneralBinner : public MultiBinner<GasTypeGeneralBinner, IntBin> {
	using MultiBinner::MultiBinner;
	QString name() const override {
		return StatsTranslations::tr("General");
	}
	std::vector<int> to_bin_values(const dive *d) const {
		std::vector<int> res;
		res.reserve(d->cylinders.nr);
		for (int i = 0; i < d->cylinders.nr; ++i) {
			struct gasmix mix = d->cylinders.cylinders[i].gasmix;
			if (gasmix_is_invalid(mix))
				continue;
			res.push_back(gasmix_to_type(mix));
		}
		return res;
	}
	QString format(const StatsBin &bin) const override {
		int type = derived_bin(bin).value;
		return gastype_name((gastype)type);
	}
};

static GasTypeGeneralBinner gas_type_general_binner;
static GasTypeBinner gas_type_binner2(2);
static GasTypeBinner gas_type_binner5(5);
static GasTypeBinner gas_type_binner10(10);
static GasTypeBinner gas_type_binner20(20);

struct GasTypeVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("Gas type");
	}
	QString diveCategories(const dive *d) const override {
		QString res;
		std::vector<gasmix> mixes;	// List multiple cylinders only once
		mixes.reserve(d->cylinders.nr);
		for (int i = 0; i < d->cylinders.nr; ++i) {
			struct gasmix mix = d->cylinders.cylinders[i].gasmix;
			if (gasmix_is_invalid(mix))
				continue;
			if (std::find_if(mixes.begin(), mixes.end(),
					 [mix] (gasmix mix2)
					 { return same_gasmix(mix, mix2); }) != mixes.end())
				continue;
			mixes.push_back(mix);
			if (!res.isEmpty())
				res += ", ";
			res += get_gas_string(mix);
		}
		return res;
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &gas_type_general_binner,
			 &gas_type_binner2, &gas_type_binner5, &gas_type_binner10, &gas_type_binner20 };
	}
};

// ============ O2 and H2 content, binned in 2, 5, 10, 20 % bins ============

// Get the gas content in permille of a dive, based on two flags:
//  - he: get he content, otherwise o2
//  - max_he: get cylinder with maximum he content, otherwise with maximum o2 content
static int get_gas_content(const struct dive *d, bool he, bool max_he)
{
	if (d->cylinders.nr <= 0)
		return invalid_value<int>();
	// If sorting be He, the second sort criterion is O2 descending, because
	// we are interested in the "bottom gas": highest He and lowest O2.
	auto comp = max_he ? [] (const cylinder_t &c1, const cylinder_t &c2)
				{ return std::make_tuple(get_he(c1.gasmix), -get_o2(c1.gasmix)) <
					 std::make_tuple(get_he(c2.gasmix), -get_o2(c2.gasmix)); }
			   : [] (const cylinder_t &c1, const cylinder_t &c2)
				{ return get_o2(c1.gasmix) < get_o2(c2.gasmix); };
	auto it = std::max_element(d->cylinders.cylinders, d->cylinders.cylinders + d->cylinders.nr, comp);
	return he ? get_he(it->gasmix) : get_o2(it->gasmix);
}

// We use the same binner for all gas contents
struct GasContentBinner : public IntRangeBinner<GasContentBinner, IntBin> {
	bool he; // true if this returns He content, otherwise O2
	bool max_he; // true if this takes the gas with maximum helium, otherwise maximum O2
	GasContentBinner(int bin_size, bool he, bool max_he) : IntRangeBinner(bin_size),
		he(he), max_he(max_he)
	{
	}
	QString name() const override {
		return StatsTranslations::tr("In %L1% steps").arg(bin_size);
	}
	QString unitSymbol() const override {
		return "%";
	}
	int to_bin_value(const struct dive *d) const {
		int res = get_gas_content(d, he, max_he);
		// Convert to percent and then bin, but take care not to mangle the invalid value.
		return is_invalid_value(res) ? res : res / 10 / bin_size;
	}
};

struct GasContentVariable : public StatsVariableTemplate<StatsVariable::Type::Numeric> {

	// In the constructor, generate binners with 2, 5, 10 and 20% bins.
	GasContentBinner b1, b2, b3, b4;
	bool he, max_he;
	GasContentVariable(bool he, bool max_he) :
		b1(2, he, max_he), b2(5, he, max_he),
		b3(10, he, max_he), b4(20, he, max_he),
		he(he), max_he(max_he)
	{
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &b1, &b2, &b3, &b4 };
	}
	QString unitSymbol() const override {
		return "%";
	}
	int decimals() const override {
		return 1;
	}
	std::vector<StatsOperation> supportedOperations() const override {
		return { StatsOperation::Median, StatsOperation::Mean, StatsOperation::TimeWeightedMean, StatsOperation::Min, StatsOperation::Max };
	}
	double toFloat(const dive *d) const override {
		int res = get_gas_content(d, he, max_he);
		// Attn: we have to turn invalid-int into invalid-float.
		// Perhaps we should signal invalid with an std::optional kind of object?
		if (is_invalid_value(res))
			return invalid_value<double>();
		return res / 10.0;
	}
};

struct GasContentO2Variable : GasContentVariable {
	GasContentO2Variable() : GasContentVariable(false, false)
	{
	}
	QString name() const override {
		return StatsTranslations::tr("O₂ (max)");
	}
};

struct GasContentO2HeMaxVariable : GasContentVariable {
	GasContentO2HeMaxVariable() : GasContentVariable(false, true)
	{
	}
	QString name() const override {
		return StatsTranslations::tr("O₂ (bottom gas)");
	}
};

struct GasContentHeVariable : GasContentVariable {
	GasContentHeVariable() : GasContentVariable(true, true)
	{
	}
	QString name() const override {
		return StatsTranslations::tr("He (max)");
	}
};

// ============ Suit ============

struct SuitBinner : public StringBinner<SuitBinner, StringBin> {
	std::vector<QString> to_bin_values(const dive *d) const {
		return { QString(d->suit) };
	}
};

static SuitBinner suit_binner;
struct SuitVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("Suit type");
	}
	QString diveCategories(const dive *d) const override {
		return QString(d->suit);
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &suit_binner };
	}
};

// ============ Weightsystem ============

static std::vector<QString> weightsystems(const dive *d)
{
	std::vector<QString> res;
	res.reserve(d->weightsystems.nr);
	for (int i = 0; i < d->weightsystems.nr; ++i)
		add_to_vector_unique(res, QString(d->weightsystems.weightsystems[i].description).trimmed());
	return res;
}

struct WeightsystemBinner : public StringBinner<WeightsystemBinner, StringBin> {
	std::vector<QString> to_bin_values(const dive *d) const {
		return weightsystems(d);
	}
};

static WeightsystemBinner weightsystem_binner;
struct WeightsystemVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("Weightsystem");
	}
	QString diveCategories(const dive *d) const override {
		return join_strings(weightsystems(d));
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &weightsystem_binner };
	}
};

// ============ Cylinder types ============

static std::vector<QString> cylinder_types(const dive *d)
{
	std::vector<QString> res;
	res.reserve(d->cylinders.nr);
	for (int i = 0; i < d->cylinders.nr; ++i)
		add_to_vector_unique(res, QString(d->cylinders.cylinders[i].type.description).trimmed());
	return res;
}

struct CylinderTypeBinner : public StringBinner<CylinderTypeBinner, StringBin> {
	std::vector<QString> to_bin_values(const dive *d) const {
		return cylinder_types(d);
	}
};

static CylinderTypeBinner cylinder_type_binner;
struct CylinderTypeVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("Cylinder type");
	}
	QString diveCategories(const dive *d) const override {
		return join_strings(cylinder_types(d));
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &cylinder_type_binner };
	}
};

// ============ Location (including trip location) ============

using LocationBin = SimpleBin<DiveSiteWrapper>;

struct LocationBinner : public SimpleBinner<LocationBinner, LocationBin> {
	QString format(const StatsBin &bin) const override {
		return derived_bin(bin).value.format();
	}
	const DiveSiteWrapper to_bin_value(const dive *d) const {
		return DiveSiteWrapper(d->dive_site);
	}
};

static LocationBinner location_binner;
struct LocationVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("Dive site");
	}
	QString diveCategories(const dive *d) const override {
		return DiveSiteWrapper(d->dive_site).format();
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &location_binner };
	}
};

// ============ Day of the week ============

struct DayOfWeekBinner : public SimpleBinner<DayOfWeekBinner, IntBin> {
	QString format(const StatsBin &bin) const override {
		return formatDayOfWeek(derived_bin(bin).value);
	}
	int to_bin_value(const dive *d) const {
		return utc_weekday(d->when);
	}
};

static DayOfWeekBinner day_of_week_binner;
struct DayOfWeekVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("Day of week");
	}
	QString diveCategories(const dive *d) const override {
		return formatDayOfWeek(utc_weekday(d->when));
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &day_of_week_binner };
	}
};

// ============ Rating ============
struct RatingBinner : public SimpleBinner<RatingBinner, IntBin> {
	QString format(const StatsBin &bin) const override {
		return QString("🌟").repeated(derived_bin(bin).value);
	}

	int to_bin_value(const dive *d) const {
		int res = (int)d->rating;
		return res;
	}
};

static RatingBinner rating_binner;
struct RatingVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("Rating");
	}
	QString diveCategories(const dive *d) const override {
		int rating = (int)d->rating;
		return QString("🌟").repeated(rating);
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &rating_binner };
	}
};

// ============ Visibility ============
struct VisibilityBinner : public SimpleBinner<VisibilityBinner, IntBin> {
	QString format(const StatsBin &bin) const override {
		return QString("🌟").repeated(derived_bin(bin).value);
	}

	int to_bin_value(const dive *d) const {
		int res = (int)d->visibility;
		return res;
	}
};

static VisibilityBinner visibility_binner;
struct VisibilityVariable : public StatsVariableTemplate<StatsVariable::Type::Discrete> {
	QString name() const override {
		return StatsTranslations::tr("Visibility");
	}
	QString diveCategories(const dive *d) const override {
		int viz = (int)d->visibility;
		return QString("🌟").repeated(viz);
	}
	std::vector<const StatsBinner *> binners() const override {
		return { &visibility_binner };
	}
};

static DateVariable date_variable;
static MaxDepthVariable max_depth_variable;
static MeanDepthVariable mean_depth_variable;
static DurationVariable duration_variable;
static SACVariable sac_variable;
static WaterTemperatureVariable water_temperature_variable;
static AirTemperatureVariable air_temperature_variable;
static WeightVariable weight_variable;
static DiveNrVariable dive_nr_variable;
static DiveModeVariable dive_mode_variable;
static PeopleVariable people_variable;
static BuddyVariable buddy_variable;
static DiveGuideVariable dive_guide_variable;
static TagVariable tag_variable;
static GasTypeVariable gas_type_variable;
static GasContentO2Variable gas_content_o2_variable;
static GasContentO2HeMaxVariable gas_content_o2_he_max_variable;
static GasContentHeVariable gas_content_he_variable;
static SuitVariable suit_variable;
static WeightsystemVariable weightsystem_variable;
static CylinderTypeVariable cylinder_type_variable;
static LocationVariable location_variable;
static DayOfWeekVariable day_of_week_variable;
static RatingVariable rating_variable;
static VisibilityVariable visibility_variable;

const std::vector<const StatsVariable *> stats_variables = {
	&date_variable, &max_depth_variable, &mean_depth_variable, &duration_variable, &sac_variable,
	&water_temperature_variable, &air_temperature_variable, &weight_variable, &dive_nr_variable,
	&gas_content_o2_variable, &gas_content_o2_he_max_variable, &gas_content_he_variable,
	&dive_mode_variable, &people_variable, &buddy_variable, &dive_guide_variable, &tag_variable,
	&gas_type_variable, &suit_variable,
	&weightsystem_variable, &cylinder_type_variable, &location_variable, &day_of_week_variable,
	&rating_variable, &visibility_variable
};