/*
 * Copyright (c) 2023 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <benchmark/benchmark.h>
#include <err.h>
#include <getopt.h>
#include <inttypes.h>
#include <math.h>
#include <sys/resource.h>
#include <sys/stat.h>

#include <map>
#include <mutex>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

#include "util.h"

static const std::vector<int> commonArgs {
    8,
    16,
    32,
    64,
    512,
    1 * K,
    8 * K,
    16 * K,
    32 * K,
    64 * K,
    128 * K,
};

static const std::vector<int> limitSizes{
    1,
    2,
    3,
    4,
    5,
    6,
    7,
};

std::map<std::string, std::pair<benchmark_func, std::string>> g_allBenchmarks;
std::mutex g_benchmarkLock;
typedef std::vector<std::vector<int64_t>> args_vector;

static struct option g_benchmarkLongOptions[] = {
    {"musl_cpu", required_argument, nullptr, 'c'},
    {"musl_iterations", required_argument, nullptr, 'i'},
    {"help", no_argument, nullptr, 'h'},
    {nullptr, 0, nullptr, 0},
    };

void PrintUsageAndExit()
{
    printf("Usage:\n");
    printf("musl_benchmarks [--musl_cpu=<cpu_to_isolate>]\n");
    printf("                [--musl_iterations=<num_iter>]\n");
    printf("                [<original benchmark flags>]\n");
    printf("benchmark flags:\n");

    int argc = 2;
    char argv0[] = "musl_benchmark";
    char argv1[] = "--help";
    char *argv[3]{argv0, argv1, nullptr};
    benchmark::Initialize(&argc, argv);
    exit(1);
}

void ShiftOptions(int argc, char **argv, std::vector<char *> *argvAfterShift)
{
    (*argvAfterShift)[0] = argv[0];
    for (int i = 1; i < argc; ++i) {
        char *optarg = argv[i];
        size_t index = 0;
        // Find if musl defined this arg.
        while (g_benchmarkLongOptions[index].name && strncmp(g_benchmarkLongOptions[index].name, optarg + 2,
            strlen(g_benchmarkLongOptions[index].name))) {
            ++index;
        }
        // Not defined.
        if (!g_benchmarkLongOptions[index].name) {
            argvAfterShift->push_back(optarg);
        } else if ((g_benchmarkLongOptions[index].has_arg == required_argument) && !strchr(optarg, '=')) {
            i++;
        }
    }
    argvAfterShift->push_back(nullptr);
}

bench_opts_t ParseOptions(int argc, char **argv)
{
    bench_opts_t opts;
    int opt;
    extern int opterr;
    opterr = 0; // Don't show unrecognized option error.

    while ((opt = getopt_long(argc, argv, "c:i:a:h", g_benchmarkLongOptions, 0)) != -1) {
        switch (opt) {
            case 'c':
                if (*optarg) {
                    char *errorCheck;
                    opts.cpuNum = strtol(optarg, &errorCheck, 10);
                    if (*errorCheck) {
                        errx(1, "ERROR: Args %s is not a valid integer.", optarg);
                    }
                } else {
                    printf("ERROR: no argument specified for musl_cpu.\n");
                    PrintUsageAndExit();
                }
                break;
            case 'i':
                if (*optarg) {
                    char *errorCheck;
                    opts.iterNum = strtol(optarg, &errorCheck, 10);
                    if (*errorCheck != '\0' or opts.iterNum < 0) {
                        errx(1, "ERROR: Args %s is not a valid number of iterations.", optarg);
                    }
                } else {
                    printf("ERROR: no argument specified for musl_iterations.\n");
                    PrintUsageAndExit();
                }
                break;
            case 'h':
                PrintUsageAndExit();
                break;
            case '?':
                break;
            default:
                exit(1);
        }
    }
    return opts;
}

void LockAndRun(benchmark::State &state, benchmark_func func, int cpuNum)
{
    if (cpuNum >= 0) {
        cpu_set_t cpuset;
        CPU_ZERO(&cpuset);
        CPU_SET(cpuNum, &cpuset);

        if (sched_setaffinity(0, sizeof(cpuset), &cpuset) != 0) {
            printf("lock CPU failed, ERROR:%s\n", strerror(errno));
        }
    }

    reinterpret_cast<void (*)(benchmark::State &)>(func)(state);
}

args_vector *ResolveArgs(args_vector *argsVector, std::string args,
    std::map<std::string, args_vector> &presetArgs)
{
    // Get it from preset args.
    if (presetArgs.count(args)) {
        return &presetArgs[args];
    }

    // Convert string to int.
    argsVector->push_back(std::vector<int64_t>());
    std::stringstream sstream(args);
    std::string argstr;
    while (sstream >> argstr) {
        char *errorCheck;
        int converted = static_cast<int>(strtol(argstr.c_str(), &errorCheck, 10));
        if (*errorCheck) {
            errx(1, "ERROR: Args str %s contains an invalid macro or int.", args.c_str());
        }
        (*argsVector)[0].push_back(converted);
    }
    return argsVector;
}

static args_vector GetArgs(const std::vector<int> &sizes)
{
    args_vector args;
    for (int size : sizes) {
        args.push_back({size});
    }
    return args;
}

static args_vector GetArgs(const std::vector<int> &sizes, int value)
{
    args_vector args;
    for (int size : sizes) {
        args.push_back({size, value});
    }
    return args;
}

static args_vector GetArgs(const std::vector<int> &sizes, int value1, int value2)
{
    args_vector args;
    for (int size : sizes) {
        args.push_back({size, value1, value2});
    }
    return args;
}

static args_vector GetArgs(const std::vector<int> &sizes, const std::vector<int> &limits, int value)
{
    args_vector args;
    for (int size : sizes) {
        for (int limit : limits) {
            args.push_back({size, limit, value});
        }
    }
    return args;
}

std::map<std::string, args_vector> GetPresetArgs()
{
    std::map<std::string, args_vector> presetArgs {
        {"COMMON_ARGS", GetArgs(commonArgs)},
        {"ALIGNED_ONEBUF", GetArgs(commonArgs, 0)},
        {"ALIGNED_TWOBUF", GetArgs(commonArgs, 0, 0)},
        {"STRING_LIMIT", GetArgs(commonArgs, limitSizes, 0)},
        {"MATH_COMMON", args_vector{{0}, {1}, {2}, {3}, {4}, {5}}},
        {"BENCHMARK_VARIABLE", args_vector{{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}}},
        {"REALPATH_VARIABLE", args_vector{{0}, {1}, {2}, {3}, {4}}},
        {"MMAP_SIZE", args_vector{{8}, {16}, {32}, {64}, {128}, {512}}},
    };

    return presetArgs;
}

void RegisterSingleBenchmark(bench_opts_t opts, const std::string &funcName, args_vector *runArgs)
{
    if (g_allBenchmarks.find(funcName) == g_allBenchmarks.end()) {
        errx(1, "ERROR: No benchmark for function %s", funcName.c_str());
    }

    benchmark_func func = g_allBenchmarks.at(funcName).first;
    for (const std::vector<int64_t> &args : (*runArgs)) {
        // It will call LockAndRun(func, opts.cpuNum).
        auto registration = benchmark::RegisterBenchmark(funcName.c_str(), LockAndRun, func, opts.cpuNum)->Args(args);
        if (opts.iterNum > 0) {
            registration->Iterations(opts.iterNum);
        }
    }
}

void RegisterAllBenchmarks(const bench_opts_t &opts, std::map<std::string, args_vector> &presetArgs)
{
    for (auto &entry : g_allBenchmarks) {
        auto &funcInfo = entry.second;
        args_vector arg_vector;
        args_vector *runArgs = ResolveArgs(&arg_vector, funcInfo.second, presetArgs);
        RegisterSingleBenchmark(opts, entry.first, runArgs);
    }
}

int main(int argc, char **argv)
{
    std::map<std::string, args_vector> presetArgs = GetPresetArgs();
    bench_opts_t opts = ParseOptions(argc, argv);
    std::vector<char *> argvAfterShift(argc);
    ShiftOptions(argc, argv, &argvAfterShift);
    RegisterAllBenchmarks(opts, presetArgs);
    if (setpriority(PRIO_PROCESS, 0, -20)) {
        perror("Set priority of process failed.\n");
    }
    int argcAfterShift = argvAfterShift.size();
    benchmark::Initialize(&argcAfterShift, argvAfterShift.data());
    benchmark::RunSpecifiedBenchmarks();
}
