// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <algorithm>
#include "src/torque/ls/message-handler.h"

#include "src/torque/ls/globals.h"
#include "src/torque/ls/json-parser.h"
#include "src/torque/ls/message-pipe.h"
#include "src/torque/ls/message.h"
#include "src/torque/server-data.h"
#include "src/torque/source-positions.h"
#include "src/torque/torque-compiler.h"

namespace v8 {
namespace internal {
namespace torque {

DEFINE_CONTEXTUAL_VARIABLE(Logger)
DEFINE_CONTEXTUAL_VARIABLE(TorqueFileList)
DEFINE_CONTEXTUAL_VARIABLE(DiagnosticsFiles)

namespace ls {

static const char kContentLength[] = "Content-Length: ";
static const size_t kContentLengthSize = sizeof(kContentLength) - 1;

#ifdef V8_OS_WIN
// On Windows, in text mode, \n is translated to \r\n.
constexpr const char* kProtocolLineEnding = "\n\n";
#else
constexpr const char* kProtocolLineEnding = "\r\n\r\n";
#endif

JsonValue ReadMessage() {
  std::string line;
  std::getline(std::cin, line);

  if (line.rfind(kContentLength) != 0) {
    // Invalid message, we just crash.
    Logger::Log("[fatal] Did not find Content-Length ...\n");
    v8::base::OS::Abort();
  }

  const int content_length = std::atoi(line.substr(kContentLengthSize).c_str());
  std::getline(std::cin, line);
  std::string content(content_length, ' ');
  std::cin.read(&content[0], content_length);

  Logger::Log("[incoming] ", content, "\n\n");

  return ParseJson(content).value;
}

void WriteMessage(JsonValue message) {
  std::string content = SerializeToString(message);

  Logger::Log("[outgoing] ", content, "\n\n");

  std::cout << kContentLength << content.size() << kProtocolLineEnding;
  std::cout << content << std::flush;
}

namespace {

void ResetCompilationErrorDiagnostics(MessageWriter writer) {
  for (const SourceId& source : DiagnosticsFiles::Get()) {
    PublishDiagnosticsNotification notification;
    notification.set_method("textDocument/publishDiagnostics");

    std::string error_file = SourceFileMap::AbsolutePath(source);
    notification.params().set_uri(error_file);
    // Trigger empty array creation.
    USE(notification.params().diagnostics_size());

    writer(std::move(notification.GetJsonValue()));
  }
  DiagnosticsFiles::Get() = {};
}

// Each notification must contain all diagnostics for a specific file,
// because sending multiple notifications per file resets previously sent
// diagnostics. Thus, two steps are needed:
//   1) collect all notifications in this class.
//   2) send one notification per entry (per file).
class DiagnosticCollector {
 public:
  void AddTorqueMessage(const TorqueMessage& message) {
    SourceId id =
        message.position ? message.position->source : SourceId::Invalid();
    auto& notification = GetOrCreateNotificationForSource(id);

    Diagnostic diagnostic = notification.params().add_diagnostics();
    diagnostic.set_severity(ServerityFor(message.kind));
    diagnostic.set_message(message.message);
    diagnostic.set_source("Torque Compiler");

    if (message.position) {
      PopulateRangeFromSourcePosition(diagnostic.range(), *message.position);
    }
  }

  std::map<SourceId, PublishDiagnosticsNotification>& notifications() {
    return notifications_;
  }

 private:
  PublishDiagnosticsNotification& GetOrCreateNotificationForSource(
      SourceId id) {
    auto iter = notifications_.find(id);
    if (iter != notifications_.end()) return iter->second;

    PublishDiagnosticsNotification& notification = notifications_[id];
    notification.set_method("textDocument/publishDiagnostics");

    std::string file =
        id.IsValid() ? SourceFileMap::AbsolutePath(id) : "<unknown>";
    notification.params().set_uri(file);
    return notification;
  }

  void PopulateRangeFromSourcePosition(Range range,
                                       const SourcePosition& position) {
    range.start().set_line(position.start.line);
    range.start().set_character(position.start.column);
    range.end().set_line(position.end.line);
    range.end().set_character(position.end.column);
  }

  Diagnostic::DiagnosticSeverity ServerityFor(TorqueMessage::Kind kind) {
    switch (kind) {
      case TorqueMessage::Kind::kError:
        return Diagnostic::kError;
      case TorqueMessage::Kind::kLint:
        return Diagnostic::kWarning;
    }
  }

  std::map<SourceId, PublishDiagnosticsNotification> notifications_;
};

void SendCompilationDiagnostics(const TorqueCompilerResult& result,
                                MessageWriter writer) {
  DiagnosticCollector collector;

  // TODO(szuend): Split up messages by SourceId and sort them by line number.
  for (const TorqueMessage& message : result.messages) {
    collector.AddTorqueMessage(message);
  }

  for (auto& pair : collector.notifications()) {
    PublishDiagnosticsNotification& notification = pair.second;
    writer(std::move(notification.GetJsonValue()));

    // Record all source files for which notifications are sent, so they
    // can be reset before the next compiler run.
    const SourceId& source = pair.first;
    if (source.IsValid()) DiagnosticsFiles::Get().push_back(source);
  }
}

}  // namespace

void CompilationFinished(TorqueCompilerResult result, MessageWriter writer) {
  LanguageServerData::Get() = std::move(result.language_server_data);
  SourceFileMap::Get() = *result.source_file_map;

  SendCompilationDiagnostics(result, writer);
}

namespace {

void RecompileTorque(MessageWriter writer) {
  Logger::Log("[info] Start compilation run ...\n");

  TorqueCompilerOptions options;
  options.output_directory = "";
  options.collect_language_server_data = true;
  options.force_assert_statements = true;

  TorqueCompilerResult result = CompileTorque(TorqueFileList::Get(), options);

  Logger::Log("[info] Finished compilation run ...\n");

  CompilationFinished(std::move(result), writer);
}

void RecompileTorqueWithDiagnostics(MessageWriter writer) {
  ResetCompilationErrorDiagnostics(writer);
  RecompileTorque(writer);
}

void HandleInitializeRequest(InitializeRequest request, MessageWriter writer) {
  InitializeResponse response;
  response.set_id(request.id());
  response.result().capabilities().textDocumentSync();
  response.result().capabilities().set_definitionProvider(true);
  response.result().capabilities().set_documentSymbolProvider(true);

  // TODO(szuend): Register for document synchronisation here,
  //               so we work with the content that the client
  //               provides, not directly read from files.
  // TODO(szuend): Check that the client actually supports dynamic
  //               "workspace/didChangeWatchedFiles" capability.
  // TODO(szuend): Check if client supports "LocationLink". This will
  //               influence the result of "goto definition".
  writer(std::move(response.GetJsonValue()));
}

void HandleInitializedNotification(MessageWriter writer) {
  RegistrationRequest request;
  // TODO(szuend): The language server needs a "global" request id counter.
  request.set_id(2000);
  request.set_method("client/registerCapability");

  Registration reg = request.params().add_registrations();
  auto options =
      reg.registerOptions<DidChangeWatchedFilesRegistrationOptions>();
  FileSystemWatcher watcher = options.add_watchers();
  watcher.set_globPattern("**/*.tq");
  watcher.set_kind(FileSystemWatcher::WatchKind::kAll);

  reg.set_id("did-change-id");
  reg.set_method("workspace/didChangeWatchedFiles");

  writer(std::move(request.GetJsonValue()));
}

void HandleTorqueFileListNotification(TorqueFileListNotification notification,
                                      MessageWriter writer) {
  CHECK_EQ(notification.params().object()["files"].tag, JsonValue::ARRAY);

  std::vector<std::string>& files = TorqueFileList::Get();
  Logger::Log("[info] Initial file list:\n");
  for (const auto& file_json :
       notification.params().object()["files"].ToArray()) {
    CHECK(file_json.IsString());

    // We only consider file URIs (there shouldn't be anything else).
    // Internally we store the URI instead of the path, eliminating the need
    // to encode it again.
    files.push_back(file_json.ToString());
    Logger::Log("    ", file_json.ToString(), "\n");
  }
  RecompileTorqueWithDiagnostics(writer);
}

void HandleGotoDefinitionRequest(GotoDefinitionRequest request,
                                 MessageWriter writer) {
  GotoDefinitionResponse response;
  response.set_id(request.id());

  SourceId id =
      SourceFileMap::GetSourceId(request.params().textDocument().uri());

  // Unknown source files cause an empty response which corresponds with
  // the definition not beeing found.
  if (!id.IsValid()) {
    response.SetNull("result");
    writer(std::move(response.GetJsonValue()));
    return;
  }

  LineAndColumn pos{request.params().position().line(),
                    request.params().position().character()};

  if (auto maybe_definition = LanguageServerData::FindDefinition(id, pos)) {
    SourcePosition definition = *maybe_definition;
    response.result().SetTo(definition);
  } else {
    response.SetNull("result");
  }

  writer(std::move(response.GetJsonValue()));
}

void HandleChangeWatchedFilesNotification(
    DidChangeWatchedFilesNotification notification, MessageWriter writer) {
  // TODO(szuend): Implement updates to the TorqueFile list when create/delete
  //               notifications are received. Currently we simply re-compile.
  RecompileTorqueWithDiagnostics(writer);
}

void HandleDocumentSymbolRequest(DocumentSymbolRequest request,
                                 MessageWriter writer) {
  DocumentSymbolResponse response;
  response.set_id(request.id());

  SourceId id =
      SourceFileMap::GetSourceId(request.params().textDocument().uri());

  for (const auto& symbol : LanguageServerData::SymbolsForSourceId(id)) {
    DCHECK(symbol->IsUserDefined());
    if (symbol->IsMacro()) {
      Macro* macro = Macro::cast(symbol);
      SymbolInformation symbol = response.add_result();
      symbol.set_name(macro->ReadableName());
      symbol.set_kind(SymbolKind::kFunction);
      symbol.location().SetTo(macro->Position());
    } else if (symbol->IsBuiltin()) {
      Builtin* builtin = Builtin::cast(symbol);
      SymbolInformation symbol = response.add_result();
      symbol.set_name(builtin->ReadableName());
      symbol.set_kind(SymbolKind::kFunction);
      symbol.location().SetTo(builtin->Position());
    } else if (symbol->IsGeneric()) {
      Generic* generic = Generic::cast(symbol);
      SymbolInformation symbol = response.add_result();
      symbol.set_name(generic->name());
      symbol.set_kind(SymbolKind::kFunction);
      symbol.location().SetTo(generic->Position());
    } else if (symbol->IsTypeAlias()) {
      const Type* type = TypeAlias::cast(symbol)->type();
      SymbolKind kind =
          type->IsClassType() ? SymbolKind::kClass : SymbolKind::kStruct;

      SymbolInformation sym = response.add_result();
      sym.set_name(type->ToString());
      sym.set_kind(kind);
      sym.location().SetTo(symbol->Position());
    }
  }

  // Trigger empty array creation in case no symbols were found.
  USE(response.result_size());

  writer(std::move(response.GetJsonValue()));
}

}  // namespace

void HandleMessage(JsonValue raw_message, MessageWriter writer) {
  Request<bool> request(std::move(raw_message));

  // We ignore responses for now. They are matched to requests
  // by id and don't have a method set.
  // TODO(szuend): Implement proper response handling for requests
  //               that originate from the server.
  if (!request.has_method()) {
    Logger::Log("[info] Unhandled response with id ", request.id(), "\n\n");
    return;
  }

  const std::string method = request.method();
  if (method == "initialize") {
    HandleInitializeRequest(
        InitializeRequest(std::move(request.GetJsonValue())), writer);
  } else if (method == "initialized") {
    HandleInitializedNotification(writer);
  } else if (method == "torque/fileList") {
    HandleTorqueFileListNotification(
        TorqueFileListNotification(std::move(request.GetJsonValue())), writer);
  } else if (method == "textDocument/definition") {
    HandleGotoDefinitionRequest(
        GotoDefinitionRequest(std::move(request.GetJsonValue())), writer);
  } else if (method == "workspace/didChangeWatchedFiles") {
    HandleChangeWatchedFilesNotification(
        DidChangeWatchedFilesNotification(std::move(request.GetJsonValue())),
        writer);
  } else if (method == "textDocument/documentSymbol") {
    HandleDocumentSymbolRequest(
        DocumentSymbolRequest(std::move(request.GetJsonValue())), writer);
  } else {
    Logger::Log("[error] Message of type ", method, " is not handled!\n\n");
  }
}

}  // namespace ls
}  // namespace torque
}  // namespace internal
}  // namespace v8