#include "benchmark_kernels.hpp"

#include <algorithm>
#include <array>
#include <bit>
#include <charconv>
#include <chrono>
#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <numeric>
#include <random>
#include <string_view>
#include <vector>

#if defined(__APPLE__)
#include <pthread.h>
#include <sys/qos.h>
#endif

namespace {
using Clock = std::chrono::steady_clock;
using I = std::int32_t;
constexpr std::size_t size = 4'096;
constexpr std::size_t samples = 15;
constexpr double target_ns = 25'000'000.0;

alignas(64) std::array<I, size> input;
alignas(64) std::array<I, size> output;
volatile std::uint64_t sink = 0;

struct Case {
    const char* name;
    Kernel kernel;
    std::size_t repeats = 1;
    std::vector<double> times;
};

// The checksum runs after the timer stops. No volatile accesses or compiler
// barriers appear in the arithmetic loops.
double measure(const Case& test, std::size_t count, std::int32_t denominator,
               const Divide& divide) {
    const auto start = Clock::now();
    for (std::size_t repeat = 0; repeat < test.repeats; ++repeat) {
        test.kernel(input.data(), output.data(), count, denominator, divide);
    }
    const auto end = Clock::now();
    std::uint64_t checksum = 0;
    for (std::size_t i = 0; i < count; ++i) {
        checksum += static_cast<std::uint32_t>(output[i]);
    }
    sink = checksum;
    return std::chrono::duration<double, std::nano>(end - start).count();
}

void verify(const Case& test, std::size_t count, I denominator, const Divide& divide) {
    test.kernel(input.data(), output.data(), count, denominator, divide);
    for (std::size_t i = 0; i < count; ++i) {
        if (output[i] != input[i] / denominator) {
            std::cerr << "Incorrect result from " << test.name << " at " << i << '\n';
            std::exit(EXIT_FAILURE);
        }
    }
}

void suite(const char* scenario, std::size_t count, std::vector<Case> cases,
           I denominator, const Divide& divide, std::ofstream& csv,
           std::mt19937& order_random) {
    for (auto& test : cases) {
        verify(test, count, denominator, divide);
        // Calibration also warms the code, data, and CPU. Each final sample
        // does enough work to make timer overhead small. The per-batch function
        // call remains included, which matters for the smallest batch sizes.
        while (measure(test, count, denominator, divide) < target_ns) {
            test.repeats *= 2;
        }
    }

    std::vector<std::size_t> order(cases.size());
    std::iota(order.begin(), order.end(), std::size_t{0});
    for (std::size_t sample = 0; sample < samples; ++sample) {
        std::shuffle(order.begin(), order.end(), order_random);
        for (const auto index : order) {
            auto& test = cases[index];
            const auto elapsed = measure(test, count, denominator, divide);
            const auto per_value = elapsed /
                (static_cast<double>(test.repeats) * static_cast<double>(count));
            test.times.push_back(per_value);
            csv << scenario << ',' << count << ',' << test.name << ',' << sample
                << ',' << test.repeats << ',' << elapsed << ',' << per_value << '\n';
        }
    }

    for (auto& test : cases) {
        std::sort(test.times.begin(), test.times.end());
        std::cout << scenario << ',' << count << ',' << test.name << ','
                  << test.times[samples / 2] << ',' << test.times.front() << ','
                  << test.times.back() << '\n';
    }
    std::cout << std::flush;
    csv << std::flush;
}
} // namespace

int main(int argc, char** argv) {
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " 3 samples.csv\n";
        return EXIT_FAILURE;
    }
    I denominator = 0;
    const std::string_view argument(argv[1]);
    const auto parsed = std::from_chars(argument.data(), argument.data() + argument.size(),
                                        denominator);
    if (parsed.ec != std::errc{} || parsed.ptr != argument.data() + argument.size()
        || denominator != 3) {
        std::cerr << "Use denominator 3 to match the compile-time-constant kernel\n";
        return EXIT_FAILURE;
    }
    std::ofstream csv(argv[2]);
    if (!csv) {
        std::cerr << "Cannot open samples output\n";
        return EXIT_FAILURE;
    }

#if defined(__APPLE__)
    const int qos = pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED, 0);
    std::cerr << "QoS request status: " << qos << " (0 = success; no core pinning)\n";
#endif
    std::cerr << "Compiler: " << __VERSION__ << '\n';
    std::cerr << "15 shuffled samples per case, >=25 ms calibration target, "
                 "4096 signed inputs, runtime denominator " << denominator << '\n';

    std::mt19937 random(0xD171DEu);
    for (auto& value : input) {
        value = std::bit_cast<I>(static_cast<std::uint32_t>(random()));
    }
    input[0] = std::numeric_limits<I>::min();
    input[1] = std::numeric_limits<I>::max();
    input[2] = 0;
    input[3] = -1;
    input[4] = 1;
    const Divide divide(denominator);

    csv << "scenario,items,method,sample,repeats,elapsed_ns,ns_per_value\n"
        << std::fixed << std::setprecision(9);
    std::cout << "scenario,items,method,median_ns_per_value,min_ns_per_value,max_ns_per_value\n"
              << std::fixed << std::setprecision(6);

    suite("steady", size,
          {{"runtime", runtime_divide, 1, {}},
           {"constant", constant_divide, 1, {}},
           {"precomputed", precomputed_divide, 1, {}}},
          denominator, divide, csv, random);

    for (const std::size_t count : {1u, 4u, 16u, 64u, 256u, 1024u, 4096u}) {
        suite("construction", count,
              {{"runtime", runtime_divide, 1, {}},
               {"construct", construct_and_divide, 1, {}}},
              denominator, divide, csv, random);
    }
    std::cerr << "Final output checksum: " << sink << '\n';
}
