//===--- XPCTransport.cpp - sending and receiving LSP messages over XPC ---===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "Conversion.h"
#include "Protocol.h" // For LSPError
#include "Transport.h"
#include "support/Logger.h"
#include "llvm/Support/Errno.h"

#include <optional>
#include <xpc/xpc.h>

using namespace llvm;
using namespace clang;
using namespace clangd;

namespace {

json::Object encodeError(Error E) {
  std::string Message;
  ErrorCode Code = ErrorCode::UnknownErrorCode;
  if (Error Unhandled =
          handleErrors(std::move(E), [&](const LSPError &L) -> Error {
            Message = L.Message;
            Code = L.Code;
            return Error::success();
          }))
    Message = toString(std::move(Unhandled));

  return json::Object{
      {"message", std::move(Message)},
      {"code", int64_t(Code)},
  };
}

Error decodeError(const json::Object &O) {
  std::string Msg =
      std::string(O.getString("message").value_or("Unspecified error"));
  if (auto Code = O.getInteger("code"))
    return make_error<LSPError>(std::move(Msg), ErrorCode(*Code));
  return error("{0}", Msg);
}

// C "closure" for XPCTransport::loop() method
namespace xpcClosure {
void connection_handler(xpc_connection_t clientConnection);
} // namespace xpcClosure

class XPCTransport : public Transport {
public:
  XPCTransport() {}

  void notify(StringRef Method, json::Value Params) override {
    sendMessage(json::Object{
        {"jsonrpc", "2.0"},
        {"method", Method},
        {"params", std::move(Params)},
    });
  }
  void call(StringRef Method, json::Value Params, json::Value ID) override {
    sendMessage(json::Object{
        {"jsonrpc", "2.0"},
        {"id", std::move(ID)},
        {"method", Method},
        {"params", std::move(Params)},
    });
  }
  void reply(json::Value ID, Expected<json::Value> Result) override {
    if (Result) {
      sendMessage(json::Object{
          {"jsonrpc", "2.0"},
          {"id", std::move(ID)},
          {"result", std::move(*Result)},
      });
    } else {
      sendMessage(json::Object{
          {"jsonrpc", "2.0"},
          {"id", std::move(ID)},
          {"error", encodeError(Result.takeError())},
      });
    }
  }

  Error loop(MessageHandler &Handler) override;

private:
  // Needs access to handleMessage() and resetClientConnection()
  friend void xpcClosure::connection_handler(xpc_connection_t clientConnection);

  // Dispatches incoming message to Handler onNotify/onCall/onReply.
  bool handleMessage(json::Value Message, MessageHandler &Handler);
  void sendMessage(json::Value Message) {
    xpc_object_t response = jsonToXpc(Message);
    xpc_connection_send_message(clientConnection, response);
    xpc_release(response);
  }
  void resetClientConnection(xpc_connection_t newClientConnection) {
    clientConnection = newClientConnection;
  }
  xpc_connection_t clientConnection;
};

bool XPCTransport::handleMessage(json::Value Message, MessageHandler &Handler) {
  // Message must be an object with "jsonrpc":"2.0".
  auto *Object = Message.getAsObject();
  if (!Object ||
      Object->getString("jsonrpc") != std::optional<StringRef>("2.0")) {
    elog("Not a JSON-RPC 2.0 message: {0:2}", Message);
    return false;
  }
  // ID may be any JSON value. If absent, this is a notification.
  std::optional<json::Value> ID;
  if (auto *I = Object->get("id"))
    ID = std::move(*I);
  auto Method = Object->getString("method");
  if (!Method) { // This is a response.
    if (!ID) {
      elog("No method and no response ID: {0:2}", Message);
      return false;
    }
    if (auto *Err = Object->getObject("error"))
      return Handler.onReply(std::move(*ID), decodeError(*Err));
    // Result should be given, use null if not.
    json::Value Result = nullptr;
    if (auto *R = Object->get("result"))
      Result = std::move(*R);
    return Handler.onReply(std::move(*ID), std::move(Result));
  }
  // Params should be given, use null if not.
  json::Value Params = nullptr;
  if (auto *P = Object->get("params"))
    Params = std::move(*P);

  if (ID)
    return Handler.onCall(*Method, std::move(Params), std::move(*ID));
  else
    return Handler.onNotify(*Method, std::move(Params));
}

namespace xpcClosure {
// "owner" of this "closure object" - necessary for propagating connection to
// XPCTransport so it can send messages to the client.
XPCTransport *TransportObject = nullptr;
Transport::MessageHandler *HandlerPtr = nullptr;

void connection_handler(xpc_connection_t clientConnection) {
  xpc_connection_set_target_queue(clientConnection, dispatch_get_main_queue());

  xpc_transaction_begin();

  TransportObject->resetClientConnection(clientConnection);

  xpc_connection_set_event_handler(clientConnection, ^(xpc_object_t message) {
    if (message == XPC_ERROR_CONNECTION_INVALID) {
      // connection is being terminated
      log("Received XPC_ERROR_CONNECTION_INVALID message - returning from the "
          "event_handler.");
      return;
    }

    if (xpc_get_type(message) != XPC_TYPE_DICTIONARY) {
      log("Received XPC message of unknown type - returning from the "
          "event_handler.");
      return;
    }

    const json::Value Doc = xpcToJson(message);
    if (Doc == json::Value(nullptr)) {
      log("XPC message was converted to Null JSON message - returning from the "
          "event_handler.");
      return;
    }

    vlog("<<< {0}\n", Doc);

    if (!TransportObject->handleMessage(std::move(Doc), *HandlerPtr)) {
      log("Received exit notification - cancelling connection.");
      xpc_connection_cancel(xpc_dictionary_get_remote_connection(message));
      xpc_transaction_end();
    }
  });

  xpc_connection_resume(clientConnection);
}
} // namespace xpcClosure

Error XPCTransport::loop(MessageHandler &Handler) {
  assert(xpcClosure::TransportObject == nullptr &&
         "TransportObject has already been set.");
  // This looks scary since lifetime of this (or any) XPCTransport object has
  // to fully contain lifetime of any XPC connection. In practise any Transport
  // object is destroyed only at the end of main() which is always after
  // exit of xpc_main().
  xpcClosure::TransportObject = this;

  assert(xpcClosure::HandlerPtr == nullptr &&
         "HandlerPtr has already been set.");
  xpcClosure::HandlerPtr = &Handler;

  xpc_main(xpcClosure::connection_handler);
  // xpc_main doesn't ever return
  return errorCodeToError(std::make_error_code(std::errc::io_error));
}

} // namespace

namespace clang {
namespace clangd {

std::unique_ptr<Transport> newXPCTransport() {
  return std::make_unique<XPCTransport>();
}

} // namespace clangd
} // namespace clang
