/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#include <memory>
#include <vector>

#include <boost/algorithm/string.hpp>
#include <json/json.h>

#include <mariana-trench/Assert.h>
#include <mariana-trench/Context.h>
#include <mariana-trench/Dependencies.h>
#include <mariana-trench/JsonValidation.h>
#include <mariana-trench/Log.h>
#include <mariana-trench/ModelGeneration.h>
#include <mariana-trench/Options.h>
#include <mariana-trench/Timer.h>
#include <mariana-trench/model-generator/ContentProviderGenerator.h>
#include <mariana-trench/model-generator/JsonModelGenerator.h>
#include <mariana-trench/model-generator/ModelGeneratorConfiguration.h>
#include <mariana-trench/model-generator/ServiceSourceGenerator.h>

namespace marianatrench {

namespace {

std::map<std::string, std::unique_ptr<ModelGenerator>> make_model_generators(
    Context& context) {
  std::map<std::string, std::unique_ptr<ModelGenerator>> generators =
      ModelGeneration::make_builtin_model_generators(context);
  // Find JSON model generators in search path.
  for (const auto& path : context.options->model_generator_search_paths()) {
    LOG(3, "Searching for model generators in `{}`...", path);
    for (auto& entry : boost::filesystem::recursive_directory_iterator(path)) {
      if (entry.path().extension() != ".json") {
        continue;
      }

      auto path_copy = entry.path();
      auto name = path_copy.replace_extension("").filename().string();

      try {
        auto [_, inserted] = generators.emplace(
            name,
            std::make_unique<JsonModelGenerator>(name, context, entry.path()));
        if (!inserted) {
          auto error = fmt::format(
              "Duplicate model generator {} defined at {}", name, entry.path());
          throw std::invalid_argument(error);
        }
        LOG(3, "Found model generator `{}`", name);
      } catch (const JsonValidationError&) {
        LOG(3, "Unable to parse generator at `{}`", path);
      }
    }
  }

  return generators;
}

} // namespace

#ifndef MARIANA_TRENCH_FACEBOOK_BUILD
std::map<std::string, std::unique_ptr<ModelGenerator>>
ModelGeneration::make_builtin_model_generators(Context& context) {
  std::vector<std::unique_ptr<ModelGenerator>> builtin_generators;
  builtin_generators.push_back(
      std::make_unique<ContentProviderGenerator>(context));
  builtin_generators.push_back(
      std::make_unique<ServiceSourceGenerator>(context));

  std::map<std::string, std::unique_ptr<ModelGenerator>> builtin_generator_map;
  for (auto& generator : builtin_generators) {
    auto name = generator->name();
    builtin_generator_map.emplace(name, std::move(generator));
  }
  return builtin_generator_map;
}
#endif

std::vector<Model> ModelGeneration::run(Context& context) {
  const auto& options = *context.options;

  const auto& generated_models_directory = options.generated_models_directory();

  if (generated_models_directory) {
    LOG(2,
        "Removing existing model generators under `{}`...",
        *generated_models_directory);
    for (auto& file :
         boost::filesystem::directory_iterator(*generated_models_directory)) {
      const auto& file_path = file.path();
      if (boost::filesystem::is_regular_file(file_path) &&
          boost::ends_with(file_path.filename().string(), ".json")) {
        boost::filesystem::remove(file_path);
      }
    }
  }

  auto builtin_generators = make_model_generators(context);
  std::vector<std::unique_ptr<ModelGenerator>> model_generators;
  const auto& configuration_entries = options.model_generators_configuration();

  std::vector<std::string> nonexistent_model_generators;
  for (const auto& entry : configuration_entries) {
    const std::string& name = entry.name();
    LOG(2, "Found model generator: `{}`", name);

    auto iterator = builtin_generators.find(name);
    if (iterator == builtin_generators.end()) {
      bool generator_exists = std::any_of(
          model_generators.begin(),
          model_generators.end(),
          [&](const auto& generator) { return generator->name() == name; });
      if (!generator_exists) {
        nonexistent_model_generators.push_back(name);
      }
    } else {
      model_generators.push_back(std::move(iterator->second));
      builtin_generators.erase(iterator);
    }
  }

  if (!nonexistent_model_generators.empty()) {
    throw std::invalid_argument(fmt::format(
        "Model generator(s) {} do not exist.",
        boost::algorithm::join(nonexistent_model_generators, ", ")));
  }

  std::vector<Model> generated_models;
  std::size_t iteration = 0;

  LOG(1,
      "Building method mappings for model generation over {} methods",
      context.methods->size());
  Timer method_mapping_timer;
  std::unique_ptr<MethodMappings> method_mappings =
      std::make_unique<MethodMappings>(*context.methods);
  LOG(1,
      "Generated method mappings in {:.2f}s",
      method_mapping_timer.duration_in_seconds());

  for (const auto& model_generator : model_generators) {
    Timer generator_timer;
    LOG(1,
        "Running model generator `{}` ({}/{})",
        model_generator->name(),
        ++iteration,
        model_generators.size());

    std::vector<Model> models;
    models = model_generator->run_optimized(*context.methods, *method_mappings);

    // Remove models for the `null` method
    models.erase(
        std::remove_if(
            models.begin(),
            models.end(),
            [](const Model& model) { return !model.method(); }),
        models.end());

    generated_models.insert(
        generated_models.end(), models.begin(), models.end());

    LOG(2,
        "Generated {} models in {:.2f}s.",
        models.size(),
        generator_timer.duration_in_seconds());

    if (generated_models_directory) {
      // Persist models to file.
      Timer generator_output_timer;
      LOG(2,
          "Writing generated models to `{}`...",
          *generated_models_directory);

      // Merge models
      auto registry = Registry(context, models);
      JsonValidation::write_json_file(
          *generated_models_directory + "/" + model_generator->name() + ".json",
          registry.models_to_json());

      LOG(2,
          "Wrote {} generated models to `{}` in {:.2f}s.",
          registry.models_size(),
          *generated_models_directory,
          generator_output_timer.duration_in_seconds());
    }
  }

  return generated_models;
}

} // namespace marianatrench
