// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include "toolchain/language_server/context.h"

#include <memory>
#include <optional>
#include <utility>

#include "common/check.h"
#include "common/raw_string_ostream.h"
#include "llvm/TargetParser/Host.h"
#include "toolchain/base/clang_invocation.h"
#include "toolchain/base/shared_value_stores.h"
#include "toolchain/check/check.h"
#include "toolchain/diagnostics/diagnostic.h"
#include "toolchain/diagnostics/diagnostic_consumer.h"
#include "toolchain/diagnostics/diagnostic_emitter.h"
#include "toolchain/lex/lex.h"
#include "toolchain/lex/tokenized_buffer.h"
#include "toolchain/parse/parse.h"
#include "toolchain/parse/tree_and_subtrees.h"

namespace Carbon::LanguageServer {

namespace {
// A consumer for turning diagnostics into a `textDocument/publishDiagnostics`
// notification.
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics
class DiagnosticConsumer : public Diagnostics::Consumer {
 public:
  // Initializes params with the target file information.
  explicit DiagnosticConsumer(Context* context,
                              const clang::clangd::URIForFile& uri,
                              std::optional<int64_t> version)
      : context_(context), params_{.uri = uri, .version = version} {}

  // Turns a diagnostic into an LSP diagnostic.
  auto HandleDiagnostic(Diagnostics::Diagnostic diagnostic) -> void override {
    const auto& message = diagnostic.messages[0];
    if (message.loc.filename != params_.uri.file()) {
      // `pushDiagnostic` requires diagnostics to be associated with a location
      // in the current file. Suppress diagnostics rooted in other files.
      // TODO: Consider if there's a better way to handle this.
      RawStringOstream stream;
      Diagnostics::StreamConsumer consumer(&stream);
      consumer.HandleDiagnostic(diagnostic);

      CARBON_DIAGNOSTIC(LanguageServerDiagnosticInWrongFile, Warning,
                        "dropping diagnostic in {0}:\n{1}", std::string,
                        std::string);
      context_->file_emitter().Emit(
          params_.uri.file(), LanguageServerDiagnosticInWrongFile,
          message.loc.filename.str(), stream.TakeStr());
      return;
    }

    // Add the main message.
    params_.diagnostics.push_back(clang::clangd::Diagnostic{
        .range = GetRange(message.loc),
        .severity = GetSeverity(diagnostic.level),
        .source = "carbon",
        .message = message.Format(),
    });
    // TODO: Figure out constructing URIs for note locations.
  }

  // Returns the constructed request.
  auto params() -> const clang::clangd::PublishDiagnosticsParams& {
    return params_;
  }

 private:
  // Returns the LSP range for a diagnostic. Note that Carbon uses 1-based
  // numbers while LSP uses 0-based.
  auto GetRange(const Diagnostics::Loc& loc) -> clang::clangd::Range {
    return {.start = {.line = loc.line_number - 1,
                      .character = loc.column_number - 1},
            .end = {.line = loc.line_number,
                    .character = loc.column_number + loc.length}};
  }

  // Converts a diagnostic level to an LSP severity.
  auto GetSeverity(Diagnostics::Level level) -> int {
    // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticSeverity
    enum class DiagnosticSeverity {
      Error = 1,
      Warning = 2,
      Information = 3,
      Hint = 4,
    };

    switch (level) {
      case Diagnostics::Level::Error:
        return static_cast<int>(DiagnosticSeverity::Error);
      case Diagnostics::Level::Warning:
        return static_cast<int>(DiagnosticSeverity::Warning);
      default:
        CARBON_FATAL("Unexpected diagnostic level: {0}", level);
    }
  }

  Context* context_;
  clang::clangd::PublishDiagnosticsParams params_;
};
}  // namespace

auto Context::File::SetText(Context& context, std::optional<int64_t> version,
                            llvm::StringRef text) -> void {
  // Clear state dependent on the source text.
  tree_and_subtrees_.reset();
  tree_.reset();
  tokens_.reset();
  value_stores_.reset();
  source_.reset();

  // A consumer to gather diagnostics for the file.
  DiagnosticConsumer consumer(&context, uri_, version);

  // TODO: Make the processing asynchronous, to better handle rapid text
  // updates.
  CARBON_CHECK(!source_ && !value_stores_ && !tokens_ && !tree_,
               "We currently cache everything together");
  // TODO: Diagnostics should be passed to the LSP instead of dropped.
  std::optional source =
      SourceBuffer::MakeFromStringCopy(uri_.file(), text, consumer);
  if (!source) {
    // Failing here should be rare, but provide stub data for recovery so that
    // we can have a simple API.
    source = SourceBuffer::MakeFromStringCopy(uri_.file(), "", consumer);
    CARBON_CHECK(source, "Making an empty buffer should always succeed");
  }
  source_ = std::make_unique<SourceBuffer>(std::move(*source));
  value_stores_ = std::make_unique<SharedValueStores>();

  Lex::LexOptions lex_options;
  lex_options.consumer = &consumer;
  tokens_ = std::make_unique<Lex::TokenizedBuffer>(
      Lex::Lex(*value_stores_, *source_, lex_options));

  Parse::ParseOptions parse_options;
  parse_options.consumer = &consumer;
  parse_options.vlog_stream = context.vlog_stream();
  tree_ = std::make_unique<Parse::Tree>(Parse::Parse(*tokens_, parse_options));
  tree_and_subtrees_ =
      std::make_unique<Parse::TreeAndSubtrees>(*tokens_, *tree_);

  SemIR::File sem_ir(tree_.get(), SemIR::CheckIRId(0), tree_->packaging_decl(),
                     *value_stores_, uri_.file().str());
  std::unique_ptr<clang::ASTUnit> clang_ast_unt;
  // TODO: Support cross-file checking when multiple files have edits.
  llvm::SmallVector<Check::Unit> units = {{{.consumer = &consumer,
                                            .value_stores = value_stores_.get(),
                                            .timings = nullptr,
                                            .sem_ir = &sem_ir,
                                            .total_ir_count = 1,
                                            .clang_ast_unit = &clang_ast_unt}}};

  auto getter = [this]() -> const Parse::TreeAndSubtrees& {
    return *tree_and_subtrees_;
  };
  // TODO: Include any unsaved files as an overlay on the real file system.
  llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> fs =
      llvm::vfs::getRealFileSystem();

  // TODO: Include the prelude. Make sure `total_ir_count` includes the files.
  Check::CheckParseTreesOptions check_options;
  check_options.vlog_stream = context.vlog_stream();
  auto getters =
      Parse::GetTreeAndSubtreesStore::MakeWithExplicitSize(1, getter);

  auto clang_invocation =
      BuildClangInvocation(consumer, fs, {context.installation().clang_path()});

  Check::CheckParseTrees(units, getters, fs, check_options,
                         std::move(clang_invocation));

  // Note we need to publish diagnostics even when empty.
  // TODO: Consider caching previously published diagnostics and only publishing
  // when they change.
  context.PublishDiagnostics(consumer.params());
}

auto Context::LookupFile(llvm::StringRef filename) -> File* {
  if (!filename.ends_with(".carbon")) {
    CARBON_DIAGNOSTIC(LanguageServerFileUnsupported, Warning,
                      "non-Carbon file requested");
    file_emitter_.Emit(filename, LanguageServerFileUnsupported);
    return nullptr;
  }

  if (auto lookup_result = files().Lookup(filename)) {
    return &lookup_result.value();
  } else {
    CARBON_DIAGNOSTIC(LanguageServerFileUnknown, Warning,
                      "unknown file requested");
    file_emitter_.Emit(filename, LanguageServerFileUnknown);
    return nullptr;
  }
}

}  // namespace Carbon::LanguageServer
