/*
 * Copyright 2023 Google LLC
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "bench/Benchmark.h"
#include "tools/flags/CommandLineFlags.h"
#include "tools/testrunners/benchmark/target/BenchmarkTarget.h"
#include "tools/testrunners/common/TestRunner.h"

static DEFINE_int(maxCalibrationAttempts,
                  3,
                  "Try up to this many times to guess loops for a benchmark, or skip the "
                  "benchmark.");
static DEFINE_double(overheadGoal,
                     0.0001,
                     "Loop until timer overhead is at most this fraction of our measurements.");
static DEFINE_int(overheadLoops, 100000, "Loops to estimate timer overhead.");

// Defined in BazelBenchmarkTestRunner.cpp.
SkString humanize(double ms);

void BenchmarkTarget::printGlobalStats() {}

class RasterBenchmarkTarget : public BenchmarkTarget {
public:
    RasterBenchmarkTarget(std::unique_ptr<SurfaceManager> surfaceManager, Benchmark* benchmark)
            : BenchmarkTarget(std::move(surfaceManager), benchmark) {}

    Benchmark::Backend getBackend() const override { return Benchmark::Backend::kRaster; }

    // Based on nanobench's setup_cpu_bench():
    // https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#466.
    std::tuple<int, bool> autoTuneLoops() const override {
        // Estimate timer overhead. Based on:
        // https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#402.
        double overhead = 0;
        for (int i = 0; i < FLAGS_overheadLoops; i++) {
            double start = nowMs();
            overhead += nowMs() - start;
        }
        overhead /= FLAGS_overheadLoops;

        // First figure out approximately how many loops of bench it takes to make overhead
        // negligible.
        double bench_plus_overhead = 0.0;
        int round = 0;
        while (bench_plus_overhead < overhead) {
            if (round++ == FLAGS_maxCalibrationAttempts) {
                TestRunner::Log("Warning: Cannot estimate loops for %s (%s vs. %s); skipping.",
                                fBenchmark->getUniqueName(),
                                humanize(bench_plus_overhead).c_str(),
                                humanize(overhead).c_str());
                return std::make_tuple(0, false);
            }
            bench_plus_overhead = time(1);
        }

        // Later we'll just start and stop the timer once but loop N times.
        // We'll pick N to make timer overhead negligible:
        //
        //          overhead
        //  -------------------------  < FLAGS_overheadGoal
        //  overhead + N * Bench Time
        //
        // where bench_plus_overhead ~=~ overhead + Bench Time.
        //
        // Doing some math, we get:
        //
        //  (overhead / FLAGS_overheadGoal) - overhead
        //  ------------------------------------------  < N
        //       bench_plus_overhead - overhead)
        //
        // Luckily, this also works well in practice. :)
        const double numer = overhead / FLAGS_overheadGoal - overhead;
        const double denom = bench_plus_overhead - overhead;
        int loops = (int)ceil(numer / denom);

        return std::make_tuple(loops, true);
    }
};

class NonRenderingBenchmarkTarget : public RasterBenchmarkTarget {
public:
    NonRenderingBenchmarkTarget(Benchmark* benchmark) : RasterBenchmarkTarget(nullptr, benchmark) {}

    Benchmark::Backend getBackend() const override { return Benchmark::Backend::kNonRendering; }

    SurfaceManager::CpuOrGpu isCpuOrGpuBound() const override {
        return SurfaceManager::CpuOrGpu::kCPU;
    }

    std::map<std::string, std::string> getKeyValuePairs(std::string cpuName,
                                                        std::string gpuName) const override {
        if (cpuName == "") {
            return std::map<std::string, std::string>();
        }
        return {
                {"cpu_or_gpu", "CPU"},
                {"cpu_or_gpu_value", cpuName},
        };
    }
};

std::unique_ptr<BenchmarkTarget> BenchmarkTarget::FromConfig(std::string surfaceConfig,
                                                             Benchmark* benchmark) {
    if (surfaceConfig == "nonrendering") {
        return std::make_unique<NonRenderingBenchmarkTarget>(benchmark);
    }

    std::unique_ptr<SurfaceManager> surfaceManager = SurfaceManager::FromConfig(
            surfaceConfig, {benchmark->getSize().width(), benchmark->getSize().height()});
    if (surfaceManager == nullptr) {
        SK_ABORT("Unknown --surfaceConfig flag value: %s.", surfaceConfig.c_str());
    }

    return std::make_unique<RasterBenchmarkTarget>(std::move(surfaceManager), benchmark);
}
