/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */
#include "cmBuildDatabase.h"

#include <cstdlib>
#include <set>
#include <utility>

#include <cm/memory>
#include <cm/string_view>
#include <cmext/string_view>

#include <cm3p/json/reader.h>
#include <cm3p/json/value.h>
#include <cm3p/json/writer.h>

#include "cmsys/FStream.hxx"

#include "cmComputeLinkInformation.h"
#include "cmFileSet.h"
#include "cmGeneratedFileStream.h"
#include "cmGeneratorTarget.h"
#include "cmGlobalGenerator.h"
#include "cmListFileCache.h"
#include "cmLocalGenerator.h"
#include "cmSourceFile.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"

namespace {

std::string PlaceholderName = "<__CMAKE_UNKNOWN>";

}

cmBuildDatabase::cmBuildDatabase() = default;
cmBuildDatabase::cmBuildDatabase(cmBuildDatabase const&) = default;
cmBuildDatabase::~cmBuildDatabase() = default;

cmBuildDatabase::LookupTable cmBuildDatabase::GenerateLookupTable()
{
  LookupTable lut;

  for (auto& Set_ : this->Sets) {
    for (auto& TranslationUnit_ : Set_.TranslationUnits) {
      // This table is from source path to TU instance. This is fine because a
      // single target (where this is used) cannot contain the same source file
      // multiple times.
      lut[TranslationUnit_.Source] = &TranslationUnit_;
    }
  }

  return lut;
}

bool cmBuildDatabase::HasPlaceholderNames() const
{
  for (auto const& Set_ : this->Sets) {
    for (auto const& TranslationUnit_ : Set_.TranslationUnits) {
      for (auto const& provide : TranslationUnit_.Provides) {
        if (provide.first == PlaceholderName) {
          return true;
        }
        if (provide.second == PlaceholderName) {
          return true;
        }
      }
    }
  }

  return false;
}

void cmBuildDatabase::Write(std::string const& path) const
{
  Json::Value mcdb = Json::objectValue;

  mcdb["version"] = 1;
  mcdb["revision"] = 0;

  Json::Value& sets = mcdb["sets"] = Json::arrayValue;

  for (auto const& Set_ : this->Sets) {
    Json::Value set = Json::objectValue;

    set["name"] = Set_.Name;
    set["family-name"] = Set_.FamilyName;

    Json::Value& visible_sets = set["visible-sets"] = Json::arrayValue;
    for (auto const& VisibleSet : Set_.VisibleSets) {
      visible_sets.append(VisibleSet);
    }

    Json::Value& tus = set["translation-units"] = Json::arrayValue;
    for (auto const& TranslationUnit_ : Set_.TranslationUnits) {
      Json::Value tu = Json::objectValue;

      if (!TranslationUnit_.WorkDirectory.empty()) {
        tu["work-directory"] = TranslationUnit_.WorkDirectory;
      }
      tu["source"] = TranslationUnit_.Source;
      if (TranslationUnit_.Object) {
        tu["object"] = *TranslationUnit_.Object;
      }
      tu["private"] = TranslationUnit_.Private;

      Json::Value& reqs = tu["requires"] = Json::arrayValue;
      for (auto const& Require : TranslationUnit_.Requires) {
        reqs.append(Require);
      }

      Json::Value& provides = tu["provides"] = Json::objectValue;
      for (auto const& Provide : TranslationUnit_.Provides) {
        provides[Provide.first] = Provide.second;
      }

      Json::Value& baseline_arguments = tu["baseline-arguments"] =
        Json::arrayValue;
      for (auto const& BaselineArgument : TranslationUnit_.BaselineArguments) {
        baseline_arguments.append(BaselineArgument);
      }

      Json::Value& local_arguments = tu["local-arguments"] = Json::arrayValue;
      for (auto const& LocalArgument : TranslationUnit_.LocalArguments) {
        local_arguments.append(LocalArgument);
      }

      Json::Value& arguments = tu["arguments"] = Json::arrayValue;
      for (auto const& Argument : TranslationUnit_.Arguments) {
        arguments.append(Argument);
      }

      tus.append(tu);
    }

    sets.append(set);
  }

  cmGeneratedFileStream mcdbf(path);
  mcdbf << mcdb;
}

static bool ParseFilename(Json::Value const& val, std::string& result)
{
  if (val.isString()) {
    result = val.asString();
  } else {
    return false;
  }

  return true;
}

#define PARSE_BLOB(val, res)                                                  \
  do {                                                                        \
    if (!ParseFilename(val, res)) {                                           \
      cmSystemTools::Error(cmStrCat("-E cmake_module_compile_db failed to ",  \
                                    "parse ", path, ": invalid blob"));       \
      return {};                                                              \
    }                                                                         \
  } while (0)

#define PARSE_FILENAME(val, res, make_full)                                   \
  do {                                                                        \
    if (!ParseFilename(val, res)) {                                           \
      cmSystemTools::Error(cmStrCat("-E cmake_module_compile_db failed to ",  \
                                    "parse ", path, ": invalid filename"));   \
      return {};                                                              \
    }                                                                         \
                                                                              \
    if (make_full && work_directory && !work_directory->empty() &&            \
        !cmSystemTools::FileIsFullPath(res)) {                                \
      res = cmStrCat(*work_directory, '/', res);                              \
    }                                                                         \
  } while (0)

std::unique_ptr<cmBuildDatabase> cmBuildDatabase::Load(std::string const& path)
{
  Json::Value mcdb;
  {
    cmsys::ifstream mcdbf(path.c_str(), std::ios::in | std::ios::binary);
    Json::Reader reader;
    if (!reader.parse(mcdbf, mcdb, false)) {
      cmSystemTools::Error(
        cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                 reader.getFormattedErrorMessages()));
      return {};
    }
  }

  Json::Value const& version = mcdb["version"];
  if (version.asUInt() > 1) {
    cmSystemTools::Error(
      cmStrCat("-E cmake_module_compile_db failed to parse ", path,
               ": version ", version.asString()));
    return {};
  }

  auto db = cm::make_unique<cmBuildDatabase>();

  Json::Value const& sets = mcdb["sets"];
  if (sets.isArray()) {
    for (auto const& set : sets) {
      Set Set_;

      Json::Value const& name = set["name"];
      if (!name.isString()) {
        cmSystemTools::Error(
          cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                   ": name is not a string"));
        return {};
      }
      Set_.Name = name.asString();

      Json::Value const& family_name = set["family-name"];
      if (!family_name.isString()) {
        cmSystemTools::Error(
          cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                   ": family-name is not a string"));
        return {};
      }
      Set_.FamilyName = family_name.asString();

      Json::Value const& visible_sets = set["visible-sets"];
      if (!visible_sets.isArray()) {
        cmSystemTools::Error(
          cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                   ": visible-sets is not an array"));
        return {};
      }
      for (auto const& visible_set : visible_sets) {
        if (!visible_set.isString()) {
          cmSystemTools::Error(
            cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                     ": a visible-sets item is not a string"));
          return {};
        }

        Set_.VisibleSets.emplace_back(visible_set.asString());
      }

      Json::Value const& translation_units = set["translation-units"];
      if (!translation_units.isArray()) {
        cmSystemTools::Error(
          cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                   ": translation-units is not an array"));
        return {};
      }
      for (auto const& translation_unit : translation_units) {
        if (!translation_unit.isObject()) {
          cmSystemTools::Error(
            cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                     ": a translation-units item is not an object"));
          return {};
        }

        TranslationUnit TranslationUnit_;

        cm::optional<std::string> work_directory;
        Json::Value const& workdir = translation_unit["work-directory"];
        if (workdir.isString()) {
          PARSE_BLOB(workdir, TranslationUnit_.WorkDirectory);
          work_directory = TranslationUnit_.WorkDirectory;
        } else if (!workdir.isNull()) {
          cmSystemTools::Error(
            cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                     ": work-directory is not a string"));
          return {};
        }

        Json::Value const& source = translation_unit["source"];
        PARSE_FILENAME(source, TranslationUnit_.Source, true);

        if (translation_unit.isMember("object")) {
          Json::Value const& object = translation_unit["object"];
          if (!object.isNull()) {
            TranslationUnit_.Object = "";
            PARSE_FILENAME(object, *TranslationUnit_.Object, false);
          }
        }

        if (translation_unit.isMember("private")) {
          Json::Value const& priv = translation_unit["private"];
          if (!priv.isBool()) {
            cmSystemTools::Error(
              cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                       ": private is not a boolean"));
            return {};
          }
          TranslationUnit_.Private = priv.asBool();
        }

        if (translation_unit.isMember("requires")) {
          Json::Value const& reqs = translation_unit["requires"];
          if (!reqs.isArray()) {
            cmSystemTools::Error(
              cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                       ": requires is not an array"));
            return {};
          }

          for (auto const& require : reqs) {
            if (!require.isString()) {
              cmSystemTools::Error(
                cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                         ": a requires item is not a string"));
              return {};
            }

            TranslationUnit_.Requires.emplace_back(require.asString());
          }
        }

        if (translation_unit.isMember("provides")) {
          Json::Value const& provides = translation_unit["provides"];
          if (!provides.isObject()) {
            cmSystemTools::Error(
              cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                       ": provides is not an object"));
            return {};
          }

          for (auto i = provides.begin(); i != provides.end(); ++i) {
            if (!i->isString()) {
              cmSystemTools::Error(
                cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                         ": a provides value is not a string"));
              return {};
            }

            TranslationUnit_.Provides[i.key().asString()] = i->asString();
          }
        }

        if (translation_unit.isMember("baseline-arguments")) {
          Json::Value const& baseline_arguments =
            translation_unit["baseline-arguments"];
          if (!baseline_arguments.isArray()) {
            cmSystemTools::Error(
              cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                       ": baseline_arguments is not an array"));
            return {};
          }

          for (auto const& baseline_argument : baseline_arguments) {
            if (baseline_argument.isString()) {
              TranslationUnit_.BaselineArguments.emplace_back(
                baseline_argument.asString());
            } else {
              cmSystemTools::Error(
                cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                         ": a baseline argument is not a string"));
              return {};
            }
          }
        }

        if (translation_unit.isMember("local-arguments")) {
          Json::Value const& local_arguments =
            translation_unit["local-arguments"];
          if (!local_arguments.isArray()) {
            cmSystemTools::Error(
              cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                       ": local_arguments is not an array"));
            return {};
          }

          for (auto const& local_argument : local_arguments) {
            if (local_argument.isString()) {
              TranslationUnit_.LocalArguments.emplace_back(
                local_argument.asString());
            } else {
              cmSystemTools::Error(
                cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                         ": a local argument is not a string"));
              return {};
            }
          }
        }

        if (translation_unit.isMember("arguments")) {
          Json::Value const& arguments = translation_unit["arguments"];
          if (!arguments.isArray()) {
            cmSystemTools::Error(
              cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                       ": arguments is not an array"));
            return {};
          }

          for (auto const& argument : arguments) {
            if (argument.isString()) {
              TranslationUnit_.Arguments.emplace_back(argument.asString());
            } else {
              cmSystemTools::Error(
                cmStrCat("-E cmake_module_compile_db failed to parse ", path,
                         ": an argument is not a string"));
              return {};
            }
          }
        }

        Set_.TranslationUnits.emplace_back(std::move(TranslationUnit_));
      }

      db->Sets.emplace_back(std::move(Set_));
    }
  }

  return db;
}

cmBuildDatabase cmBuildDatabase::Merge(
  std::vector<cmBuildDatabase> const& components)
{
  cmBuildDatabase db;

  for (auto const& component : components) {
    db.Sets.insert(db.Sets.end(), component.Sets.begin(),
                   component.Sets.end());
  }

  return db;
}

cmBuildDatabase cmBuildDatabase::ForTarget(cmGeneratorTarget* gt,
                                           std::string const& config)
{
  cmBuildDatabase db;

  Set set;
  set.Name = cmStrCat(gt->GetName(), '@', config);
  set.FamilyName = gt->GetFamilyName();
  if (auto* cli = gt->GetLinkInformation(config)) {
    std::set<cmGeneratorTarget const*> emitted;
    std::vector<cmGeneratorTarget const*> targets;
    for (auto const& item : cli->GetItems()) {
      auto const* linkee = item.Target;
      if (linkee && linkee->HaveCxx20ModuleSources() &&
          !linkee->IsImported() && emitted.insert(linkee).second) {
        set.VisibleSets.push_back(cmStrCat(linkee->GetName(), '@', config));
      }
    }
  }

  for (auto const& sfbt : gt->GetSourceFiles(config)) {
    auto const* sf = sfbt.Value;

    bool isCXXModule = false;
    bool isPrivate = true;
    if (sf->GetLanguage() != "CXX"_s) {
      auto const* fs = gt->GetFileSetForSource(config, sf);
      if (fs && fs->GetType() == "CXX_MODULES"_s) {
        isCXXModule = true;
        isPrivate = !cmFileSetVisibilityIsForInterface(fs->GetVisibility());
      }
    }

    TranslationUnit tu;

    // FIXME: Makefiles will want this to be the current working directory.
    tu.WorkDirectory = gt->GetLocalGenerator()->GetBinaryDirectory();
    tu.Source = sf->GetFullPath();
    if (!gt->IsSynthetic()) {
      auto* gg = gt->GetGlobalGenerator();
      std::string const objectDir = gg->ConvertToOutputPath(
        cmStrCat(gt->GetSupportDirectory(), gg->GetConfigDirectory(config)));
      std::string const objectFileName = gt->GetObjectName(sf);
      tu.Object = cmStrCat(objectDir, '/', objectFileName);
    }
    if (isCXXModule) {
      tu.Provides[PlaceholderName] = PlaceholderName;
    }

    cmGeneratorTarget::ClassifiedFlags classifiedFlags =
      gt->GetClassifiedFlagsForSource(sf, config);
    for (auto const& classifiedFlag : classifiedFlags) {
      if (classifiedFlag.Classification ==
          cmGeneratorTarget::FlagClassification::BaselineFlag) {
        tu.BaselineArguments.push_back(classifiedFlag.Flag);
        tu.LocalArguments.push_back(classifiedFlag.Flag);
      } else if (classifiedFlag.Classification ==
                 cmGeneratorTarget::FlagClassification::PrivateFlag) {
        tu.LocalArguments.push_back(classifiedFlag.Flag);
      }
      tu.Arguments.push_back(classifiedFlag.Flag);
    }
    tu.Private = isPrivate;

    set.TranslationUnits.emplace_back(std::move(tu));
  }

  db.Sets.emplace_back(std::move(set));

  return db;
}

int cmcmd_cmake_module_compile_db(
  std::vector<std::string>::const_iterator argBeg,
  std::vector<std::string>::const_iterator argEnd)
{
  std::string const* command = nullptr;
  std::string const* output = nullptr;
  std::vector<std::string const*> inputs;

  bool next_is_output = false;
  for (auto i = argBeg; i != argEnd; ++i) {
    // The first argument is always the command.
    if (!command) {
      command = &(*i);
      continue;
    }

    if (*i == "-o"_s) {
      next_is_output = true;
      continue;
    }
    if (next_is_output) {
      if (output) {
        cmSystemTools::Error(
          "-E cmake_module_compile_db only supports one output file");
        return EXIT_FAILURE;
      }

      output = &(*i);
      next_is_output = false;
      continue;
    }

    inputs.emplace_back(&(*i));
  }

  if (!command) {
    cmSystemTools::Error("-E cmake_module_compile_db requires a subcommand");
    return EXIT_FAILURE;
  }

  int ret = EXIT_SUCCESS;

  if (*command == "verify"_s) {
    if (output) {
      cmSystemTools::Error(
        "-E cmake_module_compile_db verify does not support an output");
      return EXIT_FAILURE;
    }

    for (auto const* i : inputs) {
      auto db = cmBuildDatabase::Load(*i);
      if (!db) {
        cmSystemTools::Error(cmStrCat("failed to verify ", *i));
        ret = EXIT_FAILURE;
      }
    }
  } else if (*command == "merge"_s) {
    if (!output) {
      cmSystemTools::Error(
        "-E cmake_module_compile_db verify requires an output");
      return EXIT_FAILURE;
    }

    std::vector<cmBuildDatabase> dbs;

    for (auto const* i : inputs) {
      auto db = cmBuildDatabase::Load(*i);
      if (!db) {
        cmSystemTools::Error(cmStrCat("failed to read ", *i));
        return EXIT_FAILURE;
      }

      dbs.emplace_back(std::move(*db));
    }

    auto db = cmBuildDatabase::Merge(dbs);
    db.Write(*output);
  } else {
    cmSystemTools::Error(
      cmStrCat("-E cmake_module_compile_db unknown subcommand ", *command));
    return EXIT_FAILURE;
  }

  return ret;
}
