/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * 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 "analyzer.h"

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "cc_analyzer.pb.h"
#include "clang/Tooling/CompilationDatabase.h"
#include "clang/Tooling/JSONCompilationDatabase.h"
#include "include_scanner.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/Twine.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/VirtualFileSystem.h"

namespace tools::ide_query::cc_analyzer {
namespace {
llvm::Expected<std::unique_ptr<clang::tooling::CompilationDatabase>> LoadCompDB(
    llvm::StringRef comp_db_path) {
  std::string err;
  std::unique_ptr<clang::tooling::CompilationDatabase> db =
      clang::tooling::JSONCompilationDatabase::loadFromFile(
          comp_db_path, err, clang::tooling::JSONCommandLineSyntax::AutoDetect);
  if (!db) {
    return llvm::createStringError(llvm::inconvertibleErrorCode(),
                                   "Failed to load CDB: " + err);
  }
  // Provide some heuristic support for missing files.
  return inferMissingCompileCommands(std::move(db));
}
}  // namespace

::cc_analyzer::DepsResponse GetDeps(::cc_analyzer::RepoState state) {
  ::cc_analyzer::DepsResponse results;
  auto db = LoadCompDB(state.comp_db_path());
  if (!db) {
    results.mutable_status()->set_code(::cc_analyzer::Status::FAILURE);
    results.mutable_status()->set_message(llvm::toString(db.takeError()));
    return results;
  }
  for (llvm::StringRef active_file : state.active_file_path()) {
    auto& result = *results.add_deps();

    llvm::SmallString<256> abs_file(state.repo_dir());
    llvm::sys::path::append(abs_file, active_file);
    auto cmds = db->get()->getCompileCommands(active_file);
    if (cmds.empty()) {
      result.mutable_status()->set_code(::cc_analyzer::Status::FAILURE);
      result.mutable_status()->set_message(
          llvm::Twine("Can't find compile flags for file: ", abs_file).str());
      continue;
    }
    result.set_source_file(active_file.str());
    llvm::StringRef file = cmds[0].Filename;
    if (llvm::StringRef actual_file(cmds[0].Heuristic);
        actual_file.consume_front("inferred from ")) {
      file = actual_file;
    }
    // TODO: Query ninja graph to figure out a minimal set of targets to build.
    result.add_build_target(file.str() + "^");
  }
  return results;
}

::cc_analyzer::IdeAnalysis GetBuildInputs(::cc_analyzer::RepoState state) {
  auto db = LoadCompDB(state.comp_db_path());
  ::cc_analyzer::IdeAnalysis results;
  if (!db) {
    results.mutable_status()->set_code(::cc_analyzer::Status::FAILURE);
    results.mutable_status()->set_message(llvm::toString(db.takeError()));
    return results;
  }
  std::string repo_dir = state.repo_dir();
  if (!repo_dir.empty() && repo_dir.back() == '/') repo_dir.pop_back();

  llvm::SmallString<256> genfile_root_abs(repo_dir);
  llvm::sys::path::append(genfile_root_abs, state.out_dir());
  if (genfile_root_abs.empty() || genfile_root_abs.back() != '/') {
    genfile_root_abs.push_back('/');
  }

  for (llvm::StringRef active_file : state.active_file_path()) {
    auto& result = *results.add_sources();
    result.set_path(active_file.str());

    llvm::SmallString<256> abs_file(repo_dir);
    llvm::sys::path::append(abs_file, active_file);
    auto cmds = db->get()->getCompileCommands(abs_file);
    if (cmds.empty()) {
      result.mutable_status()->set_code(::cc_analyzer::Status::FAILURE);
      result.mutable_status()->set_message(
          llvm::Twine("Can't find compile flags for file: ", abs_file).str());
      continue;
    }
    const auto& cmd = cmds.front();
    llvm::StringRef working_dir = cmd.Directory;
    if (!working_dir.consume_front(repo_dir)) {
      result.mutable_status()->set_code(::cc_analyzer::Status::FAILURE);
      result.mutable_status()->set_message("Command working dir " +
                                           working_dir.str() +
                                           " outside repository " + repo_dir);
      continue;
    }
    working_dir = working_dir.ltrim('/');
    result.set_working_dir(working_dir.str());
    for (auto& arg : cmd.CommandLine) result.add_compiler_arguments(arg);

    auto includes =
        ScanIncludes(cmds.front(), llvm::vfs::createPhysicalFileSystem());
    if (!includes) {
      result.mutable_status()->set_code(::cc_analyzer::Status::FAILURE);
      result.mutable_status()->set_message(
          llvm::toString(includes.takeError()));
      continue;
    }

    for (auto& [req_input, contents] : *includes) {
      llvm::StringRef req_input_ref(req_input);
      // We're only interested in generated files.
      if (!req_input_ref.consume_front(genfile_root_abs)) continue;
      auto& genfile = *result.add_generated();
      genfile.set_path(req_input_ref.str());
      genfile.set_contents(std::move(contents));
    }
  }
  return results;
}
}  // namespace tools::ide_query::cc_analyzer
