// Copyright 2024 The Bazel Authors. All rights reserved.
//
// 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 "tools/cpp/modules_tools/common/common.h"

#include <iostream>

#include "json.hpp"

void die(const std::string &msg) {
  std::cerr << msg << std::endl;
  std::exit(1);
}

void parse_provides(const JsonValue &provides_data, ModuleDep &dep) {
  if (provides_data.is_null()) {
    return;
  }
  if (!provides_data.is_array()) {
    die("require ddi content 'rules[0][\"provides\"]' is JSON array");
  }
  // Only 1 provide in rule
  // In C++20 Modules, one TU provide only one module.
  // Fortran can provide more than one module per TU.
  // This check is fine for C++20 Modules.
  auto provides = provides_data.as_array();
  if (provides.size() > 1) {
    die("require ddi content 'rules[0][\"provides\"]' has only 1 provide");
  }
  if (provides.size() == 1) {
    auto provide_data = provides[0];
    if (!provide_data.is_object()) {
      die("require ddi content 'rules[0][\"provides\"][0]' is JSON object");
    }
    auto provide_obj = provide_data.as_object();
    if (provide_obj.find("logical-name") == provide_obj.end()) {
      die("require 'logical-name' in 'rules[0][\"provides\"][0]'");
    }
    auto name_data = provide_obj.at("logical-name");
    if (!name_data.is_string()) {
      die("require ddi content 'rules[0][\"provides\"][0][\"logical-name\"]' "
          "is JSON string");
    }
    dep.gen_bmi = true;
    dep.name = name_data.as_string();
  }
}

void parse_requires(const JsonValue &requires_data, ModuleDep &dep) {
  if (requires_data.is_null()) {
    return;
  }
  if (!requires_data.is_array()) {
    die("require ddi content 'rules[0][\"requires\"]' is JSON array");
  }
  for (const auto &item_data : requires_data.as_array()) {
    if (!item_data.is_object()) {
      die("require JSON object, but got " + item_data.dump());
    }
    auto item_obj = item_data.as_object();
    if (item_obj.find("logical-name") == item_obj.end()) {
      die("requrie 'logical-name' in 'rules[0][\"requires\"]' item");
    }
    auto name_data = item_obj.at("logical-name");
    if (!name_data.is_string()) {
      die("require JSON string, but got " + name_data.dump());
    }
    dep.require_list.push_back(name_data.as_string());
  }
}

ModuleDep parse_ddi(std::istream &ddi_stream) {
  ModuleDep dep{};
  std::string ddi_string((std::istreambuf_iterator<char>(ddi_stream)),
                         std::istreambuf_iterator<char>());
  JsonValue data = parse_json(ddi_string);
  if (!data.is_object()) {
    die("require ddi content is JSON object");
  }

  auto data_obj = data.as_object();
  if (data_obj.find("rules") == data_obj.end()) {
    die("require 'rules' in ddi content");
  }

  auto rules_data = data.as_object().at("rules");
  if (!rules_data.is_array()) {
    die("require ddi content 'rules' is JSON array");
  }
  auto rules = rules_data.as_array();
  // Only 1 rule in DDI file
  // DDI files can contain multiple rules (in general).
  // bazel does per-TU scanning rather than batch scanning.
  // Therefore, report error if multiple rules here
  if (rules.size() > 1) {
    die("require ddi content 'rules' has only 1 rule");
  }
  if (rules.empty()) {
    return dep;
  }
  auto rule_data = rules[0];
  if (!rule_data.is_object()) {
    die("require ddi content 'rules[0]' is JSON object");
  }
  auto rule = rule_data.as_object();
  auto provides_data = rule["provides"];
  auto requires_data = rule["requires"];
  parse_provides(provides_data, dep);
  parse_requires(requires_data, dep);
  return dep;
}

Cpp20ModulesInfo parse_info(std::istream &info_stream) {
  std::string info_string((std::istreambuf_iterator<char>(info_stream)),
                          std::istreambuf_iterator<char>());
  JsonValue data = parse_json(info_string);
  if (!data.is_object()) {
    die("require content is JSON object");
  }
  auto data_obj = data.as_object();
  if (data_obj.find("modules") == data_obj.end()) {
    die("require 'modules' in JSON object");
  }
  auto modules_data = data_obj.at("modules");
  if (!modules_data.is_object()) {
    die("require 'modules' is JSON object");
  }
  if (data_obj.find("usages") == data_obj.end()) {
    die("require 'usages' in JSON object");
  }
  auto usages_data = data_obj.at("usages");
  if (!usages_data.is_object()) {
    die("require 'usages' is JSON object");
  }
  Cpp20ModulesInfo info;
  for (const auto &item_data : modules_data.as_object()) {
    auto name = item_data.first;
    auto bmi_data = item_data.second;
    if (!bmi_data.is_string()) {
      die("require JSON string, but got " + bmi_data.dump());
    }
    info.modules[name] = bmi_data.as_string();
  }
  for (const auto &item_data : usages_data.as_object()) {
    auto name = item_data.first;
    auto require_list_data = item_data.second;
    if (!require_list_data.is_array()) {
      die("require JSON array");
    }
    std::vector<std::string> require_list;
    for (const auto &require_item_data : require_list_data.as_array()) {
      if (!require_item_data.is_string()) {
        die("require JSON string, but got " + require_item_data.dump());
      }
      require_list.push_back(require_item_data.as_string());
    }
    info.usages[name] = require_list;
  }
  return info;
}

