/*
 * Copyright (C) 2018 Simon Fels <morphis@gravedo.de>
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3, as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranties of
 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "anbox/cmds/check_features.h"
#include "anbox/utils.h"

#include "cpu_features_macros.h"
#if defined(CPU_FEATURES_ARCH_X86)
#include "cpuinfo_x86.h"
#endif

namespace {
std::vector<std::string> cpu_whitelist = {
  // QEMU does not necessarily expose correctly that it supports SSE and friends even
  // when started with `-cpu qemu64,+ssse3,+sse4.1,+sse4.2,+x2apic`
  "QEMU",

  // The following CPUs do not support AVX and without it cpu_features can't detect
  // if SSE & friends are supported. See https://github.com/google/cpu_features/issues/4

  // Intel Core i7 M620
  "M 620",
  // Intel Core i5 M460
  "M 460",
  // Intel Celeron N2840
  "N2840",
  // Intel Core i7 Q720
  "Q 720",
  // Intel Pentium T4500
  "T4500", 
  // Intel Core i7 Q720
  "Q 720",
  // Intel Xeon E5520
  "E5520"
  // Intel Core2 Duo T6500
  "T6500"
};
} // namespace

anbox::cmds::CheckFeatures::CheckFeatures()
    : CommandWithFlagsAndAction{
          cli::Name{"check-features"}, cli::Usage{"check-features"},
          cli::Description{"Check that the host system supports all necessary features"}} {

  action([this](const cli::Command::Context&) {
#if defined(CPU_FEATURES_ARCH_X86)
    const auto info = cpu_features::GetX86Info();
    std::vector<std::string> missing_features;

#define CHECK_BOOL(x, name) \
  if (!x) \
    missing_features.push_back(name)

    CHECK_BOOL(info.features.sse4_1, "SSE 4.1");
    CHECK_BOOL(info.features.sse4_2, "SSE 4.2");
    CHECK_BOOL(info.features.ssse3, "SSSE 3");

    char brand_string[49];
    cpu_features::FillX86BrandString(brand_string);
    std::string brand(brand_string);

    // Check if we have a CPU which's features we can't detect correctly
    auto is_whitelisted = false;
    for (const auto &entry : cpu_whitelist) {
      if (brand.find(entry) != std::string::npos) {
        is_whitelisted = true;
        break;
      }
    }

    if (missing_features.size() > 0 && !is_whitelisted && !sanity_check_for_features()) {
      std::cerr << "The CPU of your computer (" << brand_string << ") does not support all" << std::endl
                << "features Anbox requires." << std::endl
                << "It is missing support for the following features: ";

      for (size_t n = 0; n < missing_features.size(); n++) {
        const auto feature = missing_features[n];
        std::cerr << feature;
        if (n < missing_features.size() - 1)
          std::cerr << ", ";
      }
      std::cerr << std::endl;
      std::cerr << "You can for example find more information about SSE" << std::endl
                << "here https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions" << std::endl;

      return EXIT_FAILURE;
    }

    std::cout << "Your computer does meet all requirements to run Anbox" << std::endl;

    return EXIT_SUCCESS;
#else
    std::cerr << "You're running Anbox on a not yet supported architecture" << std::endl;
    return EXIT_FAILURE;
#endif
  });
}

// In case that the CPU supports AVX we take the decision as from our analysis
// of the output from the cpu_features library. If it does not we have to check
// further via the compiler builtins if we the CPU supports all mandatory features
// or not. In case that any is missing we will fail the test.
//
// This uses the compiler builtin function __builtin_cpu_supports which allows us
// to detect certain CPU features.
// See https://gcc.gnu.org/onlinedocs/gcc/x86-Built-in-Functions.html
bool anbox::cmds::CheckFeatures::sanity_check_for_features() {
#if defined(CPU_FEATURES_ARCH_X86)
  if (__builtin_cpu_supports("avx"))
    return true;

  std::vector<std::string> missing_features;

#define CHECK_FEATURE(name) \
  if (!__builtin_cpu_supports(name)) \
    missing_features.push_back(name);

  CHECK_FEATURE("sse4.1");
  CHECK_FEATURE("sse4.2");
  CHECK_FEATURE("ssse3");

  return missing_features.empty();
#else
  return true;
#endif
}
