#include "universal_container/adaptive_sequence.hpp"

#include <algorithm>
#include <bit>
#include <charconv>
#include <chrono>
#include <cmath>
#include <concepts>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <optional>
#include <random>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <vector>

#ifndef UC_BUILD_PROFILE
#define UC_BUILD_PROFILE "unknown"
#endif

namespace {

using Value = std::uint32_t;
using Clock = std::chrono::steady_clock;
// Every compared layout uses the same flat value index.  Value lookup is thus
// comparable, and insert/erase timings include hash-index maintenance.
using Sequence = uc::AdaptiveSequence<Value, true>;

volatile std::uint64_t benchmark_sink = 0;

enum class OperationKind : std::uint8_t { read, find, insert, erase };

struct Operation {
    OperationKind kind = OperationKind::read;
    std::size_t index = 0;
    Value value = 0;
};

struct FindQuery {
    Value value = 0;
    std::size_t expected_index = std::numeric_limits<std::size_t>::max();
};

struct Trace {
    std::vector<Value> initial;
    std::vector<Operation> mixed;
    std::vector<std::size_t> reads;
    std::vector<FindQuery> finds;
    std::vector<Operation> inserts;
    std::vector<Operation> erases;
    std::size_t read_count = 0;
    std::size_t find_count = 0;
    std::size_t find_hit_count = 0;
    std::size_t find_miss_count = 0;
    std::size_t insert_count = 0;
    std::size_t erase_count = 0;
    std::uint64_t expected_mixed_guard = 0;
    std::uint64_t expected_read_guard = 0;
    std::uint64_t expected_find_guard = 0;
    std::uint64_t expected_insert_guard = 0;
    std::uint64_t expected_erase_guard = 0;
};

struct Options {
    std::size_t min_n = 256;
    std::size_t max_n = 65'536;
    std::size_t operations = 100'000;
    std::size_t repeats = 7;
    std::filesystem::path output = "results/benchmarks/focused.csv";
    std::uint64_t seed = 0x6a09'e667'f3bc'c909ULL;
};

struct Result {
    std::size_t n = 0;
    std::string container;
    std::size_t repeat = 0;
    std::uint64_t seed = 0;
    std::string initial_mode;
    std::string mode;
    std::string read_mode;
    std::string find_mode;
    std::string insert_mode;
    std::string erase_mode;
    std::size_t read_leaf = 0;
    std::size_t find_leaf = 0;
    std::size_t insert_leaf = 0;
    std::size_t erase_leaf = 0;
    std::size_t initial_leaf = 0;
    std::size_t leaf = 0;
    std::optional<std::size_t> initial_logical_capacity;
    std::optional<std::size_t> logical_capacity;
    std::optional<std::size_t> initial_hash_buckets;
    std::optional<std::size_t> hash_buckets;
    std::size_t operations = 0;
    std::size_t read_count = 0;
    std::size_t find_count = 0;
    std::size_t find_hit_count = 0;
    std::size_t find_miss_count = 0;
    std::size_t insert_count = 0;
    std::size_t erase_count = 0;
    std::size_t read_batch_count = 0;
    std::size_t find_batch_count = 0;
    std::size_t insert_batch_count = 0;
    std::size_t erase_batch_count = 0;
    double mixed_ops_per_sec = 0.0;
    double read_ops_per_sec = 0.0;
    double find_ops_per_sec = 0.0;
    double insert_ops_per_sec = 0.0;
    double erase_ops_per_sec = 0.0;
    std::size_t bytes = 0;
    std::uint64_t checksum = 0;
};

constexpr std::size_t missing_index = std::numeric_limits<std::size_t>::max();
constexpr std::size_t minimum_fast_batch_operations = 100'000;

[[noreturn]] void usage_error(std::string_view message) {
    throw std::invalid_argument(std::string(message)
        + "\nusage: uc_focused_bench [--min-n N] [--max-n N]"
          " [--operations N] [--repeats N] [--output FILE] [--seed N]");
}

template <class Integer>
Integer parse_integer(std::string_view text, std::string_view option) {
    Integer value{};
    const auto* first = text.data();
    const auto* last = first + text.size();
    const auto [end, error] = std::from_chars(first, last, value);
    if (error != std::errc{} || end != last) {
        usage_error("invalid integer for " + std::string(option));
    }
    return value;
}

Options parse_options(int argc, char** argv) {
    Options options;
    for (int i = 1; i < argc; ++i) {
        const std::string_view option(argv[i]);
        if (option == "--help" || option == "-h") {
            std::cout
                << "usage: uc_focused_bench [--min-n N] [--max-n N]"
                   " [--operations N] [--repeats N] [--output FILE] [--seed N]\n";
            std::exit(0);
        }
        if (i + 1 == argc) {
            usage_error("missing value for " + std::string(option));
        }
        const std::string_view value(argv[++i]);
        if (option == "--min-n") {
            options.min_n = parse_integer<std::size_t>(value, option);
        } else if (option == "--max-n") {
            options.max_n = parse_integer<std::size_t>(value, option);
        } else if (option == "--operations") {
            options.operations = parse_integer<std::size_t>(value, option);
        } else if (option == "--repeats") {
            options.repeats = parse_integer<std::size_t>(value, option);
        } else if (option == "--output") {
            options.output = std::filesystem::path(value);
        } else if (option == "--seed") {
            options.seed = parse_integer<std::uint64_t>(value, option);
        } else {
            usage_error("unknown option: " + std::string(option));
        }
    }

    if (options.min_n == 0 || options.max_n < options.min_n) {
        usage_error("the size range must satisfy 1 <= min-n <= max-n");
    }
    if (options.operations == 0 || options.operations % 100 != 0) {
        usage_error("--operations must be a positive multiple of 100");
    }
    if (options.repeats == 0) {
        usage_error("--repeats must be positive");
    }
    if (options.max_n >= (std::uint64_t{1} << 31)
        || options.operations / 100 >= (std::uint64_t{1} << 31) - options.max_n) {
        usage_error("size plus inserted values must stay below 2^31");
    }
    return options;
}

std::vector<std::size_t> power_of_two_sizes(const Options& options) {
    std::vector<std::size_t> sizes;
    std::size_t n = std::bit_ceil(options.min_n);
    while (n <= options.max_n) {
        sizes.push_back(n);
        if (n > options.max_n / 2) {
            break;
        }
        n *= 2;
    }
    if (sizes.empty()) {
        usage_error("the requested range contains no power-of-two size");
    }
    return sizes;
}

std::size_t ceil_sqrt(std::size_t value) {
    auto root = static_cast<std::size_t>(std::sqrt(static_cast<long double>(value)));
    while (root * root < value) {
        ++root;
    }
    while (root > 1 && (root - 1) * (root - 1) >= value) {
        --root;
    }
    return root;
}

std::size_t random_index(std::mt19937_64& random, std::size_t upper_exclusive) {
    if (upper_exclusive == 0) {
        throw std::logic_error("cannot select an index from an empty sequence");
    }
    return static_cast<std::size_t>(random() % upper_exclusive);
}

std::uint64_t guard_start(std::uint64_t tag) noexcept {
    return 0xcbf2'9ce4'8422'2325ULL ^ (tag * 0x9e37'79b9'7f4a'7c15ULL);
}

void guard_event(std::uint64_t& guard,
                 std::uint64_t tag,
                 std::size_t index,
                 Value value) noexcept {
    // This executes in timed batches, so keep the guard much cheaper than the
    // operation being measured.  The full ordered sequence digest below gives
    // the structural checks a stronger second line of defence.
    const auto word = static_cast<std::uint64_t>(index)
                    ^ (static_cast<std::uint64_t>(value) << 17)
                    ^ (tag << 3);
    guard += std::rotl(word, static_cast<int>(tag & 7U));
    guard ^= guard >> 29;
}

template <class Container>
std::uint64_t sequence_digest(const Container& container) {
    auto digest = guard_start(0x51);
    std::size_t index = 0;
    if constexpr (requires(const Container& value) {
                      value.for_each([](const Value&) {});
                  }) {
        container.for_each([&](const Value& value) {
            guard_event(digest, 0x52, index++, value);
        });
    } else {
        for (const auto value : container) {
            guard_event(digest, 0x52, index++, value);
        }
    }
    guard_event(digest, 0x53, container.size(), static_cast<Value>(container.size()));
    return digest;
}

void finish_guard(std::uint64_t& guard, std::uint64_t state_digest) noexcept {
    guard_event(guard, 0x7f, static_cast<std::size_t>(state_digest),
                static_cast<Value>(state_digest >> 32));
}

Trace make_trace(std::size_t n, std::size_t operation_count, std::uint64_t seed) {
    Trace trace;
    trace.initial.resize(n);
    for (std::size_t i = 0; i < n; ++i) {
        trace.initial[i] = static_cast<Value>(i);
    }

    trace.find_count = operation_count / 100;
    trace.insert_count = operation_count / 100;
    trace.erase_count = operation_count / 100;
    trace.read_count = operation_count - trace.find_count
                     - trace.insert_count - trace.erase_count;
    trace.find_hit_count = (trace.find_count + 1) / 2;
    trace.find_miss_count = trace.find_count - trace.find_hit_count;

    std::mt19937_64 random(seed);
    std::vector<OperationKind> kinds;
    kinds.reserve(operation_count);
    kinds.insert(kinds.end(), trace.read_count, OperationKind::read);
    kinds.insert(kinds.end(), trace.find_count, OperationKind::find);
    kinds.insert(kinds.end(), trace.insert_count, OperationKind::insert);
    kinds.insert(kinds.end(), trace.erase_count, OperationKind::erase);
    std::shuffle(kinds.begin(), kinds.end(), random);

    trace.expected_mixed_guard = guard_start(1);
    auto mixed_shadow = trace.initial;
    trace.mixed.reserve(operation_count);
    std::size_t find_ordinal = 0;
    std::size_t insert_ordinal = 0;
    for (std::size_t operation_index = 0; operation_index < kinds.size(); ++operation_index) {
        if (kinds[operation_index] == OperationKind::erase && mixed_shadow.size() == 1) {
            const auto next_insert = std::find(kinds.begin()
                    + static_cast<std::ptrdiff_t>(operation_index + 1),
                kinds.end(), OperationKind::insert);
            if (next_insert == kinds.end()) {
                throw std::logic_error("trace scheduling could not keep the sequence non-empty");
            }
            std::iter_swap(kinds.begin() + static_cast<std::ptrdiff_t>(operation_index),
                           next_insert);
        }

        Operation operation;
        operation.kind = kinds[operation_index];
        switch (operation.kind) {
        case OperationKind::read:
            operation.index = random_index(random, mixed_shadow.size());
            operation.value = mixed_shadow[operation.index];
            guard_event(trace.expected_mixed_guard, 1, operation.index, operation.value);
            break;
        case OperationKind::find:
            if ((find_ordinal++ & 1U) == 0) {
                operation.index = random_index(random, mixed_shadow.size());
                operation.value = mixed_shadow[operation.index];
            } else {
                operation.index = missing_index;
                operation.value = static_cast<Value>(
                    std::numeric_limits<Value>::max() - find_ordinal);
            }
            guard_event(trace.expected_mixed_guard, 2, operation.index, operation.value);
            break;
        case OperationKind::insert:
            operation.index = static_cast<std::size_t>(random() % (mixed_shadow.size() + 1));
            operation.value = static_cast<Value>(n + insert_ordinal++);
            mixed_shadow.insert(mixed_shadow.begin()
                + static_cast<std::ptrdiff_t>(operation.index), operation.value);
            guard_event(trace.expected_mixed_guard, 3, operation.index, operation.value);
            break;
        case OperationKind::erase:
            operation.index = random_index(random, mixed_shadow.size());
            operation.value = mixed_shadow[operation.index];
            mixed_shadow.erase(mixed_shadow.begin()
                + static_cast<std::ptrdiff_t>(operation.index));
            guard_event(trace.expected_mixed_guard, 4, operation.index, operation.value);
            break;
        }
        trace.mixed.push_back(operation);
    }
    auto mixed_digest = guard_start(0x51);
    for (std::size_t i = 0; i < mixed_shadow.size(); ++i) {
        guard_event(mixed_digest, 0x52, i, mixed_shadow[i]);
    }
    guard_event(mixed_digest, 0x53, mixed_shadow.size(),
                static_cast<Value>(mixed_shadow.size()));
    finish_guard(trace.expected_mixed_guard, mixed_digest);

    const auto read_batch_count = std::max(
        trace.read_count, minimum_fast_batch_operations);
    trace.expected_read_guard = guard_start(2);
    trace.reads.reserve(read_batch_count);
    for (std::size_t i = 0; i < read_batch_count; ++i) {
        const auto index = random_index(random, trace.initial.size());
        trace.reads.push_back(index);
        guard_event(trace.expected_read_guard, 1, index, trace.initial[index]);
    }
    auto initial_digest = guard_start(0x51);
    for (std::size_t i = 0; i < trace.initial.size(); ++i) {
        guard_event(initial_digest, 0x52, i, trace.initial[i]);
    }
    guard_event(initial_digest, 0x53, trace.initial.size(),
                static_cast<Value>(trace.initial.size()));
    finish_guard(trace.expected_read_guard, initial_digest);

    const auto find_batch_count = std::max(
        trace.find_count, minimum_fast_batch_operations);
    trace.expected_find_guard = guard_start(3);
    trace.finds.reserve(find_batch_count);
    for (std::size_t i = 0; i < find_batch_count; ++i) {
        FindQuery query;
        if ((i & 1U) == 0) {
            query.expected_index = random_index(random, trace.initial.size());
            query.value = trace.initial[query.expected_index];
        } else {
            query.value = static_cast<Value>(std::numeric_limits<Value>::max() - i);
        }
        trace.finds.push_back(query);
        guard_event(trace.expected_find_guard, 2, query.expected_index, query.value);
    }
    finish_guard(trace.expected_find_guard, initial_digest);

    trace.expected_insert_guard = guard_start(4);
    auto insert_shadow = trace.initial;
    trace.inserts.reserve(trace.insert_count);
    for (std::size_t i = 0; i < trace.insert_count; ++i) {
        Operation operation;
        operation.kind = OperationKind::insert;
        operation.index = static_cast<std::size_t>(random() % (insert_shadow.size() + 1));
        operation.value = static_cast<Value>(n + i);
        trace.inserts.push_back(operation);
        insert_shadow.insert(insert_shadow.begin()
            + static_cast<std::ptrdiff_t>(operation.index), operation.value);
        guard_event(trace.expected_insert_guard, 3, operation.index, operation.value);
    }
    auto insert_digest = guard_start(0x51);
    for (std::size_t i = 0; i < insert_shadow.size(); ++i) {
        guard_event(insert_digest, 0x52, i, insert_shadow[i]);
    }
    guard_event(insert_digest, 0x53, insert_shadow.size(),
                static_cast<Value>(insert_shadow.size()));
    finish_guard(trace.expected_insert_guard, insert_digest);

    trace.expected_erase_guard = guard_start(5);
    auto erase_shadow = trace.initial;
    const auto erase_batch_count = std::min(trace.erase_count, erase_shadow.size());
    trace.erases.reserve(erase_batch_count);
    for (std::size_t i = 0; i < erase_batch_count; ++i) {
        Operation operation;
        operation.kind = OperationKind::erase;
        operation.index = random_index(random, erase_shadow.size());
        operation.value = erase_shadow[operation.index];
        trace.erases.push_back(operation);
        erase_shadow.erase(erase_shadow.begin()
            + static_cast<std::ptrdiff_t>(operation.index));
        guard_event(trace.expected_erase_guard, 4, operation.index, operation.value);
    }
    auto erase_digest = guard_start(0x51);
    for (std::size_t i = 0; i < erase_shadow.size(); ++i) {
        guard_event(erase_digest, 0x52, i, erase_shadow[i]);
    }
    guard_event(erase_digest, 0x53, erase_shadow.size(),
                static_cast<Value>(erase_shadow.size()));
    finish_guard(trace.expected_erase_guard, erase_digest);

    return trace;
}

template <class Container>
Value indexed_read(Container& container, std::size_t index) {
    return static_cast<Value>(container[index]);
}

template <class Container>
void insert_at(Container& container, std::size_t index, Value value) {
    if constexpr (std::same_as<Container, std::vector<Value>>) {
        container.insert(container.begin() + static_cast<std::ptrdiff_t>(index), value);
    } else {
        container.insert(index, value);
    }
}

template <class Container>
void erase_at(Container& container, std::size_t index) {
    if constexpr (std::same_as<Container, std::vector<Value>>) {
        container.erase(container.begin() + static_cast<std::ptrdiff_t>(index));
    } else {
        container.erase(index);
    }
}

template <class Container>
std::size_t find_value(const Container& container, Value value) {
    if constexpr (std::same_as<Container, std::vector<Value>>) {
        const auto found = std::find(container.begin(), container.end(), value);
        return found == container.end()
            ? missing_index
            : static_cast<std::size_t>(found - container.begin());
    } else {
        return container.find_one(value).value_or(missing_index);
    }
}

template <class Container>
std::optional<std::size_t> logical_capacity(const Container& container) {
    if constexpr (requires(const Container& value) {
                      { value.capacity() } -> std::convertible_to<std::size_t>;
                  }) {
        return static_cast<std::size_t>(container.capacity());
    } else {
        return std::nullopt;
    }
}

template <class Container>
std::optional<std::size_t> hash_bucket_count(const Container& container) {
    if constexpr (requires(const Container& value) {
                      { value.hash_bucket_count() } -> std::convertible_to<std::size_t>;
                  }) {
        return static_cast<std::size_t>(container.hash_bucket_count());
    } else {
        return std::nullopt;
    }
}

template <class Container>
std::size_t allocated_bytes(const Container& container) {
    if constexpr (requires(const Container& value) {
                      { value.allocated_bytes() } -> std::convertible_to<std::size_t>;
                  }) {
        return static_cast<std::size_t>(container.allocated_bytes());
    } else {
        return sizeof(container) + container.capacity() * sizeof(Value);
    }
}

template <class Container>
std::string storage_mode(const Container& container) {
    if constexpr (requires(const Container& value) { value.mode(); }) {
        return container.mode() == uc::StorageMode::vector ? "vector" : "tiered";
    } else {
        return "vector";
    }
}

template <class Container>
std::size_t active_leaf(const Container& container) {
    if constexpr (requires(const Container& value) { value.tiered_config(); }) {
        return container.tiered_config().leaf_capacity;
    } else {
        return 0;
    }
}

template <class Container>
std::string mode_transition(const Container& container,
                            std::string_view initial_mode) {
    const auto final_mode = storage_mode(container);
    return final_mode == initial_mode
        ? final_mode : std::string(initial_mode) + "->" + final_mode;
}

double operations_per_second(std::size_t count, Clock::duration duration) {
    const auto seconds = std::chrono::duration<double>(duration).count();
    return seconds > 0.0 ? static_cast<double>(count) / seconds : 0.0;
}

template <class Builder>
Result run_candidate(const Trace& trace,
                     std::size_t n,
                     std::size_t repeat,
                     std::uint64_t seed,
                     std::string container_name,
                     Builder&& builder) {
    Result result;
    result.n = n;
    result.container = std::move(container_name);
    result.repeat = repeat;
    result.seed = seed;
    result.operations = trace.mixed.size();
    result.read_count = trace.read_count;
    result.find_count = trace.find_count;
    result.find_hit_count = trace.find_hit_count;
    result.find_miss_count = trace.find_miss_count;
    result.insert_count = trace.insert_count;
    result.erase_count = trace.erase_count;
    result.read_batch_count = trace.reads.size();
    result.find_batch_count = trace.finds.size();
    result.insert_batch_count = trace.inserts.size();
    result.erase_batch_count = trace.erases.size();

    auto mixed_container = builder(); // Construction is deliberately outside the timer.
    result.initial_mode = storage_mode(mixed_container);
    result.initial_leaf = active_leaf(mixed_container);
    result.initial_logical_capacity = logical_capacity(mixed_container);
    result.initial_hash_buckets = hash_bucket_count(mixed_container);
    auto mixed_guard = guard_start(1);
    const auto mixed_begin = Clock::now();
    for (const auto& operation : trace.mixed) {
        switch (operation.kind) {
        case OperationKind::read: {
            const auto value = indexed_read(mixed_container, operation.index);
            guard_event(mixed_guard, 1, operation.index, value);
            break;
        }
        case OperationKind::find: {
            const auto index = find_value(mixed_container, operation.value);
            guard_event(mixed_guard, 2, index, operation.value);
            break;
        }
        case OperationKind::insert:
            insert_at(mixed_container, operation.index, operation.value);
            guard_event(mixed_guard, 3, operation.index, operation.value);
            break;
        case OperationKind::erase:
            erase_at(mixed_container, operation.index);
            guard_event(mixed_guard, 4, operation.index, operation.value);
            break;
        }
    }
    const auto mixed_end = Clock::now();
    result.mixed_ops_per_sec = operations_per_second(trace.mixed.size(), mixed_end - mixed_begin);
    result.mode = storage_mode(mixed_container);
    result.leaf = active_leaf(mixed_container);
    result.logical_capacity = logical_capacity(mixed_container);
    result.hash_buckets = hash_bucket_count(mixed_container);
    result.bytes = allocated_bytes(mixed_container);
    finish_guard(mixed_guard, sequence_digest(mixed_container));
    if (mixed_guard != trace.expected_mixed_guard) {
        throw std::runtime_error(result.container + ": mixed workload checksum mismatch");
    }

    auto read_container = builder();
    const auto read_initial_mode = storage_mode(read_container);
    auto read_guard = guard_start(2);
    const auto read_begin = Clock::now();
    for (const auto index : trace.reads) {
        guard_event(read_guard, 1, index, indexed_read(read_container, index));
    }
    const auto read_end = Clock::now();
    result.read_ops_per_sec = operations_per_second(trace.reads.size(), read_end - read_begin);
    result.read_mode = mode_transition(read_container, read_initial_mode);
    result.read_leaf = active_leaf(read_container);
    finish_guard(read_guard, sequence_digest(read_container));
    if (read_guard != trace.expected_read_guard) {
        throw std::runtime_error(result.container + ": read batch checksum mismatch");
    }

    auto find_container = builder();
    const auto find_initial_mode = storage_mode(find_container);
    auto find_guard = guard_start(3);
    const auto find_begin = Clock::now();
    for (const auto& query : trace.finds) {
        guard_event(find_guard, 2, find_value(find_container, query.value), query.value);
    }
    const auto find_end = Clock::now();
    result.find_ops_per_sec = operations_per_second(trace.finds.size(), find_end - find_begin);
    result.find_mode = mode_transition(find_container, find_initial_mode);
    result.find_leaf = active_leaf(find_container);
    finish_guard(find_guard, sequence_digest(find_container));
    if (find_guard != trace.expected_find_guard) {
        throw std::runtime_error(result.container + ": find batch checksum mismatch");
    }

    auto insert_container = builder();
    const auto insert_initial_mode = storage_mode(insert_container);
    auto insert_guard = guard_start(4);
    const auto insert_begin = Clock::now();
    for (const auto& operation : trace.inserts) {
        insert_at(insert_container, operation.index, operation.value);
        guard_event(insert_guard, 3, operation.index, operation.value);
    }
    const auto insert_end = Clock::now();
    result.insert_ops_per_sec = operations_per_second(trace.inserts.size(), insert_end - insert_begin);
    result.insert_mode = mode_transition(insert_container, insert_initial_mode);
    result.insert_leaf = active_leaf(insert_container);
    finish_guard(insert_guard, sequence_digest(insert_container));
    if (insert_guard != trace.expected_insert_guard) {
        throw std::runtime_error(result.container + ": insert batch checksum mismatch");
    }

    auto erase_container = builder();
    const auto erase_initial_mode = storage_mode(erase_container);
    auto erase_guard = guard_start(5);
    const auto erase_begin = Clock::now();
    for (const auto& operation : trace.erases) {
        erase_at(erase_container, operation.index);
        guard_event(erase_guard, 4, operation.index, operation.value);
    }
    const auto erase_end = Clock::now();
    result.erase_ops_per_sec = operations_per_second(trace.erases.size(), erase_end - erase_begin);
    result.erase_mode = mode_transition(erase_container, erase_initial_mode);
    result.erase_leaf = active_leaf(erase_container);
    finish_guard(erase_guard, sequence_digest(erase_container));
    if (erase_guard != trace.expected_erase_guard) {
        throw std::runtime_error(result.container + ": erase batch checksum mismatch");
    }

    result.checksum = mixed_guard;
    guard_event(result.checksum, 0x61, static_cast<std::size_t>(read_guard),
                static_cast<Value>(read_guard >> 32));
    guard_event(result.checksum, 0x62, static_cast<std::size_t>(find_guard),
                static_cast<Value>(find_guard >> 32));
    guard_event(result.checksum, 0x63, static_cast<std::size_t>(insert_guard),
                static_cast<Value>(insert_guard >> 32));
    guard_event(result.checksum, 0x64, static_cast<std::size_t>(erase_guard),
                static_cast<Value>(erase_guard >> 32));
    benchmark_sink ^= result.checksum;
    return result;
}

Sequence build_forced_vector(const std::vector<Value>& initial, uc::TieredConfig config) {
    Sequence sequence(config);
    sequence.force_vector_mode();
    for (const auto value : initial) {
        sequence.push_back(value);
    }
    return sequence;
}

Sequence build_forced_tiered(const std::vector<Value>& initial, uc::TieredConfig config) {
    auto sequence = build_forced_vector(initial, config);
    sequence.force_tiered_mode(config);
    return sequence;
}

Sequence build_adaptive(const std::vector<Value>& initial, uc::TieredConfig config) {
    Sequence sequence(config);
    for (const auto value : initial) {
        sequence.push_back(value);
    }
    sequence.enable_auto_mode();
    (void)sequence.adapt_now();
    return sequence;
}

std::string csv_string(std::string_view text) {
    std::string result;
    result.reserve(text.size() + 2);
    result.push_back('"');
    for (const auto character : text) {
        if (character == '"') {
            result.push_back('"');
        }
        result.push_back(character);
    }
    result.push_back('"');
    return result;
}

void write_csv(const std::filesystem::path& path, const std::vector<Result>& results) {
    if (!path.parent_path().empty()) {
        std::filesystem::create_directories(path.parent_path());
    }
    std::ofstream output(path, std::ios::binary);
    if (!output) {
        throw std::runtime_error("cannot open output file: " + path.string());
    }
    output
        << "profile,n,container,repeat,seed,initial_mode,mixed_mode,initial_leaf,mixed_leaf,"
           "initial_logical_capacity,logical_capacity,initial_hash_buckets,hash_buckets,operations,"
           "read_count,find_count,find_hit_count,find_miss_count,insert_count,erase_count,"
           "read_batch_count,find_batch_count,insert_batch_count,erase_batch_count,"
           "read_mode,read_leaf,find_mode,find_leaf,insert_mode,insert_leaf,erase_mode,erase_leaf,"
           "mixed_ops_per_sec,read_ops_per_sec,find_ops_per_sec,insert_ops_per_sec,"
           "erase_ops_per_sec,bytes,checksum\n";
    output << std::setprecision(17);
    for (const auto& result : results) {
        output << csv_string(UC_BUILD_PROFILE) << ',' << result.n << ','
               << csv_string(result.container) << ',' << result.repeat << ','
               << result.seed << ',' << csv_string(result.initial_mode) << ','
               << csv_string(result.mode) << ',' << result.initial_leaf << ','
               << result.leaf << ',';
        if (result.initial_logical_capacity) {
            output << *result.initial_logical_capacity;
        }
        output << ',';
        if (result.logical_capacity) {
            output << *result.logical_capacity;
        }
        output << ',';
        if (result.initial_hash_buckets) {
            output << *result.initial_hash_buckets;
        }
        output << ',';
        if (result.hash_buckets) {
            output << *result.hash_buckets;
        }
        output << ',' << result.operations << ',' << result.read_count << ','
               << result.find_count << ',' << result.find_hit_count << ','
               << result.find_miss_count << ',' << result.insert_count << ','
               << result.erase_count << ',' << result.read_batch_count << ','
               << result.find_batch_count << ',' << result.insert_batch_count << ','
               << result.erase_batch_count << ',' << csv_string(result.read_mode) << ','
               << result.read_leaf << ',' << csv_string(result.find_mode) << ','
               << result.find_leaf << ',' << csv_string(result.insert_mode) << ','
               << result.insert_leaf << ',' << csv_string(result.erase_mode) << ','
               << result.erase_leaf << ',' << result.mixed_ops_per_sec << ','
               << result.read_ops_per_sec << ',' << result.find_ops_per_sec << ','
               << result.insert_ops_per_sec << ',' << result.erase_ops_per_sec << ','
               << result.bytes << ',' << result.checksum << '\n';
    }
}

double median(std::vector<double> values) {
    std::sort(values.begin(), values.end());
    const auto middle = values.size() / 2;
    if ((values.size() & 1U) != 0) {
        return values[middle];
    }
    return (values[middle - 1] + values[middle]) / 2.0;
}

void print_summary(const std::vector<Result>& results,
                   const std::vector<std::size_t>& sizes) {
    constexpr std::string_view containers[] = {
        "forced_vector_hash", "forced_tiered_hash", "adaptive_hash"
    };
    if (!results.empty()) {
        const auto& sample = results.front();
        std::cout << "\nMixed trace: " << sample.read_count << " indexed reads, "
                  << sample.find_count << " flat-hash find_one calls ("
                  << sample.find_hit_count << " hit / " << sample.find_miss_count
                  << " miss), " << sample.insert_count << " inserts, "
                  << sample.erase_count << " erases.\n";
    }
    std::cout
        << "\nMedian throughput (operations/second)\n"
           "| n | container | mixed mode | mixed leaf | mixed ops/s | read ops/s | find ops/s | insert ops/s | erase ops/s |\n"
           "|---:|:----------|:-----|-----:|------------:|-----------:|-----------:|-------------:|------------:|\n";
    for (const auto n : sizes) {
        for (const auto name : containers) {
            std::vector<double> mixed;
            std::vector<double> reads;
            std::vector<double> finds;
            std::vector<double> inserts;
            std::vector<double> erases;
            const Result* representative = nullptr;
            for (const auto& result : results) {
                if (result.n != n || result.container != name) {
                    continue;
                }
                representative = &result;
                mixed.push_back(result.mixed_ops_per_sec);
                reads.push_back(result.read_ops_per_sec);
                finds.push_back(result.find_ops_per_sec);
                inserts.push_back(result.insert_ops_per_sec);
                erases.push_back(result.erase_ops_per_sec);
            }
            if (!representative) {
                continue;
            }
            const auto displayed_mode = representative->initial_mode == representative->mode
                ? representative->mode
                : representative->initial_mode + "->" + representative->mode;
            std::cout << "| " << n << " | " << name << " | "
                      << displayed_mode << " | " << representative->leaf << " | "
                      << std::fixed << std::setprecision(0)
                      << median(std::move(mixed)) << " | " << median(std::move(reads)) << " | "
                      << median(std::move(finds)) << " | " << median(std::move(inserts)) << " | "
                      << median(std::move(erases)) << " |\n";
        }
    }
}

std::uint64_t repeat_seed(std::uint64_t base, std::size_t n, std::size_t repeat) noexcept {
    auto value = base ^ (static_cast<std::uint64_t>(n) * 0x9e37'79b9'7f4a'7c15ULL);
    value ^= static_cast<std::uint64_t>(repeat + 1) * 0xbf58'476d'1ce4'e5b9ULL;
    value ^= value >> 30;
    value *= 0xbf58'476d'1ce4'e5b9ULL;
    value ^= value >> 27;
    value *= 0x94d0'49bb'1331'11ebULL;
    return value ^ (value >> 31);
}

} // namespace

int main(int argc, char** argv) {
    try {
        const auto options = parse_options(argc, argv);
        const auto sizes = power_of_two_sizes(options);
        std::vector<Result> results;
        results.reserve(sizes.size() * options.repeats * 3);

        const auto total_cells = sizes.size() * options.repeats;
        std::size_t completed_cells = 0;
        for (const auto n : sizes) {
            const uc::TieredConfig config{ceil_sqrt(n), 64, 4};
            for (std::size_t repeat = 0; repeat < options.repeats; ++repeat) {
                const auto seed = repeat_seed(options.seed, n, repeat);
                const auto trace = make_trace(n, options.operations, seed);

                // Rotate the candidate order per paired repeat so cache state,
                // thermal drift, and a fixed first/last position do not always
                // favour the same representation.
                const auto first_candidate = static_cast<std::size_t>(seed % 3);
                for (std::size_t offset = 0; offset < 3; ++offset) {
                    switch ((first_candidate + offset) % 3) {
                    case 0:
                        results.push_back(run_candidate(
                            trace, n, repeat, seed, "forced_vector_hash",
                            [&] { return build_forced_vector(trace.initial, config); }));
                        break;
                    case 1:
                        results.push_back(run_candidate(
                            trace, n, repeat, seed, "forced_tiered_hash",
                            [&] { return build_forced_tiered(trace.initial, config); }));
                        break;
                    default:
                        results.push_back(run_candidate(
                            trace, n, repeat, seed, "adaptive_hash",
                            [&] { return build_adaptive(trace.initial, config); }));
                        break;
                    }
                }

                ++completed_cells;
                const auto percent = completed_cells * 100 / total_cells;
                std::cerr << "\r[" << std::setw(3) << percent << "%] n=" << n
                          << " repeat=" << (repeat + 1) << '/' << options.repeats << std::flush;
            }
        }
        std::cerr << '\n';

        write_csv(options.output, results);
        print_summary(results, sizes);
        std::cout << "\nRaw repeat data: " << options.output.string() << '\n';
        return benchmark_sink == 0x0123'4567'89ab'cdefULL ? 2 : 0;
    } catch (const std::exception& error) {
        std::cerr << "uc_focused_bench: " << error.what() << '\n';
        return 1;
    }
}
