/*
 * Copyright 2020 Bloomberg Finance LP
 *
 * 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 <buildboxcommon_exception.h>
#include <buildboxcommon_platformutils.h>
#include <buildboxcommon_systemutils.h>

#include <algorithm>
#include <cerrno>
#include <map>
#include <string>
#include <system_error>
#include <vector>

#include <sys/utsname.h>

using namespace buildboxcommon;

namespace {

/* A table of REAPI ISAs
 *
 * This is a lookup table indexed by commonly used machine architecture names
 * which might be reported by the kernel or by tools like `arch-test`
 */
const std::map<std::string, std::string> archISATable = {
    {"i386", "x86-32"},          {"i486", "x86-32"},
    {"i586", "x86-32"},          {"i686", "x86-32"},

    {"amd64", "x86-64"},         {"x86_64", "x86-64"},

    {"arm", "aarch32"},          {"armhf", "aarch32"},
    {"armel", "aarch32"},        {"armv8l", "aarch32"},
    {"armv8b", "aarch32-be"},    {"arm64", "aarch64"},
    {"aarch64", "aarch64"},

    {"ppc64le", "power-isa-le"}, {"ppc64el", "power-isa-le"},
    {"ppc64", "power-isa-be"},

    {"riscv32", "rv32g"},        {"riscv64", "rv64g"},

    {"sparc", "sparc-v9"},       {"sparc64", "sparc-v9"},
    {"sun4v", "sparc-v9"},

    {"loong64", "la64v100"},     {"loongarch64", "la64v100"},
};

/* Lookup an ISA by architecture name
 *
 */
std::string lookupISA(const std::string &arch)
{
    std::string isa;

    auto it = archISATable.find(arch);
    if (it != archISATable.end())
        isa = it->second;

    return isa;
}

/* Invoke arch-test (if installed) and return ISAs corresponding to the
 * machine architectures which arch-test report as supported
 */
std::unordered_set<std::string> collectArchTestISAs()
{
    std::string archTestPath = SystemUtils::getPathToCommand("arch-test");
    std::unordered_set<std::string> supported;

    if (!archTestPath.empty()) {
        std::vector<std::string> archTestCommand = {archTestPath};

        const auto result =
            SystemUtils::executeCommandWithResult(archTestCommand, true);

        if (result.d_exitCode == 0) {
            auto ss = std::stringstream{result.d_stdOut};

            for (std::string line; std::getline(ss, line, '\n');) {
                std::string isa = lookupISA(line);

                if (!isa.empty())
                    supported.insert(isa);
            }
        }
    }

    return supported;
}

} // namespace

std::string PlatformUtils::getHostOSFamily()
{
#if defined(__APPLE__)
    return "macos";
#else
    struct utsname buf{};
    if (uname(&buf) < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::generic_category, "uname failed");
    }

    std::string osfamily(buf.sysname);

    // Canonicalize to lower case
    std::transform(osfamily.begin(), osfamily.end(), osfamily.begin(),
                   ::tolower);
    return osfamily;
#endif
}

std::string PlatformUtils::getHostISA()
{
    struct utsname buf{};
    if (uname(&buf) < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::generic_category, "uname failed");
    }

    std::string arch(buf.machine);
    std::string isa = lookupISA(arch);

    return isa;
}

std::unordered_set<std::string> PlatformUtils::getSupportedISAs()
{
    std::unordered_set<std::string> supported;
    std::string hostISA = PlatformUtils::getHostISA();
    std::string linux32Path = SystemUtils::getPathToCommand("linux32");

    supported.insert(hostISA);

    /*
     * 32-bit compatibility ISAs are supported with `linux32`
     */
    if (!linux32Path.empty()) {
        if (hostISA == "x86-64") {
            supported.insert("x86-32");
        }
        else if (hostISA == "aarch64") {
            supported.insert("aarch32");
        }
    }

    /*
     * Collect any additional ISAs for which support may be detected with
     * `arch-test`
     */
    std::unordered_set<std::string> archTestISAs = collectArchTestISAs();
    for (const auto &isa : archTestISAs) {
        if (!((hostISA == "x86-64" && isa == "x86-32") ||
              (hostISA == "aarch64" && isa == "aarch32"))) {
            supported.insert(isa);
        }
    }

    return supported;
}

PlatformUtils::ISASupport PlatformUtils::getISASupport(const std::string &isa)
{
    std::string host = PlatformUtils::getHostISA();
    ISASupport support = {false, ISA_SUPPORT_ACTION_NONE};

    /* Native ISA */
    if (isa == host) {
        support.supported = true;
    }
    /* Usable with linux32 */
    else if ((host == "x86-64" && isa == "x86-32") ||
             (host == "aarch64" && isa == "aarch32")) {

        std::string linux32Path = SystemUtils::getPathToCommand("linux32");
        if (!linux32Path.empty()) {
            support.supported = true;
            support.action = ISA_SUPPORT_ACTION_RUN_LINUX32;
        }
    }
    /* Usable with emulation, detected with arch-test */
    else {
        std::unordered_set<std::string> archTestISAs = collectArchTestISAs();
        if (archTestISAs.find(isa) != archTestISAs.end()) {
            support.supported = true;
        }
    }

    return support;
}
