//===-- ProtocolBase.cpp --------------------------------------------------===//
//
// 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 "Protocol/ProtocolBase.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/StringSwitch.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/JSON.h"
#include <optional>
#include <utility>

using namespace llvm;

static bool mapRaw(const json::Value &Params, StringLiteral Prop,
                   std::optional<json::Value> &V, json::Path P) {
  const auto *O = Params.getAsObject();
  if (!O) {
    P.report("expected object");
    return false;
  }
  const json::Value *E = O->get(Prop);
  if (E)
    V = std::move(*E);
  return true;
}

namespace lldb_dap::protocol {

enum MessageType : unsigned {
  eMessageTypeRequest,
  eMessageTypeResponse,
  eMessageTypeEvent
};

bool fromJSON(const json::Value &Params, MessageType &M, json::Path P) {
  auto rawType = Params.getAsString();
  if (!rawType) {
    P.report("expected a string");
    return false;
  }
  std::optional<MessageType> type =
      StringSwitch<std::optional<MessageType>>(*rawType)
          .Case("request", eMessageTypeRequest)
          .Case("response", eMessageTypeResponse)
          .Case("event", eMessageTypeEvent)
          .Default(std::nullopt);
  if (!type) {
    P.report("unexpected value, expected 'request', 'response' or 'event'");
    return false;
  }
  M = *type;
  return true;
}

json::Value toJSON(const Request &R) {
  assert(R.seq != kCalculateSeq && "invalid seq");

  json::Object Result{
      {"type", "request"},
      {"seq", R.seq},
      {"command", R.command},
  };

  if (R.arguments)
    Result.insert({"arguments", R.arguments});

  return std::move(Result);
}

bool fromJSON(json::Value const &Params, Request &R, json::Path P) {
  json::ObjectMapper O(Params, P);
  if (!O)
    return false;

  MessageType type;
  if (!O.map("type", type) || !O.map("command", R.command) ||
      !O.map("seq", R.seq))
    return false;

  if (type != eMessageTypeRequest) {
    P.field("type").report("expected to be 'request'");
    return false;
  }

  if (R.command.empty()) {
    P.field("command").report("expected to not be ''");
    return false;
  }

  if (!R.seq) {
    P.field("seq").report("expected to not be '0'");
    return false;
  }

  return mapRaw(Params, "arguments", R.arguments, P);
}

bool operator==(const Request &a, const Request &b) {
  return a.seq == b.seq && a.command == b.command && a.arguments == b.arguments;
}

json::Value toJSON(const Response &R) {
  assert(R.seq != kCalculateSeq && "invalid seq");

  json::Object Result{{"type", "response"},
                      {"seq", R.seq},
                      {"command", R.command},
                      {"request_seq", R.request_seq},
                      {"success", R.success}};

  if (R.message) {
    assert(!R.success && "message can only be used if success is false");
    if (const auto *messageEnum = std::get_if<ResponseMessage>(&*R.message)) {
      switch (*messageEnum) {
      case eResponseMessageCancelled:
        Result.insert({"message", "cancelled"});
        break;
      case eResponseMessageNotStopped:
        Result.insert({"message", "notStopped"});
        break;
      }
    } else if (const auto *messageString =
                   std::get_if<std::string>(&*R.message)) {
      Result.insert({"message", *messageString});
    }
  }

  if (R.body)
    Result.insert({"body", R.body});

  return std::move(Result);
}

static bool fromJSON(json::Value const &Params,
                     std::variant<ResponseMessage, std::string> &M,
                     json::Path P) {
  auto rawMessage = Params.getAsString();
  if (!rawMessage) {
    P.report("expected a string");
    return false;
  }
  std::optional<ResponseMessage> message =
      StringSwitch<std::optional<ResponseMessage>>(*rawMessage)
          .Case("cancelled", eResponseMessageCancelled)
          .Case("notStopped", eResponseMessageNotStopped)
          .Default(std::nullopt);
  if (message)
    M = *message;
  else if (!rawMessage->empty())
    M = rawMessage->str();
  return true;
}

bool fromJSON(json::Value const &Params, Response &R, json::Path P) {
  json::ObjectMapper O(Params, P);
  if (!O)
    return false;

  MessageType type;
  if (!O.map("type", type) || !O.map("seq", R.seq) ||
      !O.map("command", R.command) || !O.map("request_seq", R.request_seq))
    return false;

  if (type != eMessageTypeResponse) {
    P.field("type").report("expected to be 'response'");
    return false;
  }

  if (R.command.empty()) {
    P.field("command").report("expected to not be empty");
    return false;
  }

  return O.map("success", R.success) && O.map("message", R.message) &&
         mapRaw(Params, "body", R.body, P);
}

bool operator==(const Response &a, const Response &b) {
  return a.request_seq == b.request_seq && a.command == b.command &&
         a.success == b.success && a.message == b.message && a.body == b.body;
}

json::Value toJSON(const ErrorMessage &EM) {
  json::Object Result{{"id", EM.id}, {"format", EM.format}};

  if (EM.variables) {
    json::Object variables;
    for (auto &var : *EM.variables)
      variables[var.first] = var.second;
    Result.insert({"variables", std::move(variables)});
  }
  if (EM.sendTelemetry)
    Result.insert({"sendTelemetry", EM.sendTelemetry});
  if (EM.showUser)
    Result.insert({"showUser", EM.showUser});
  if (EM.url)
    Result.insert({"url", EM.url});
  if (EM.urlLabel)
    Result.insert({"urlLabel", EM.urlLabel});

  return std::move(Result);
}

bool fromJSON(json::Value const &Params, ErrorMessage &EM, json::Path P) {
  json::ObjectMapper O(Params, P);
  return O && O.map("id", EM.id) && O.map("format", EM.format) &&
         O.map("variables", EM.variables) &&
         O.map("sendTelemetry", EM.sendTelemetry) &&
         O.map("showUser", EM.showUser) && O.map("url", EM.url) &&
         O.map("urlLabel", EM.urlLabel);
}

json::Value toJSON(const Event &E) {
  assert(E.seq != kCalculateSeq && "invalid seq");

  json::Object Result{
      {"type", "event"},
      {"seq", E.seq},
      {"event", E.event},
  };

  if (E.body)
    Result.insert({"body", E.body});

  return std::move(Result);
}

bool fromJSON(json::Value const &Params, Event &E, json::Path P) {
  json::ObjectMapper O(Params, P);
  if (!O)
    return false;

  MessageType type;
  if (!O.map("type", type) || !O.map("seq", E.seq) || !O.map("event", E.event))
    return false;

  if (type != eMessageTypeEvent) {
    P.field("type").report("expected to be 'event'");
    return false;
  }

  if (E.event.empty()) {
    P.field("event").report("expected to not be empty");
    return false;
  }

  return mapRaw(Params, "body", E.body, P);
}

bool operator==(const Event &a, const Event &b) {
  return a.event == b.event && a.body == b.body;
}

bool fromJSON(const json::Value &Params, Message &PM, json::Path P) {
  json::ObjectMapper O(Params, P);
  if (!O)
    return false;

  MessageType type;
  if (!O.map("type", type))
    return false;

  switch (type) {
  case eMessageTypeRequest: {
    Request req;
    if (!fromJSON(Params, req, P))
      return false;
    PM = std::move(req);
    return true;
  }
  case eMessageTypeResponse: {
    Response resp;
    if (!fromJSON(Params, resp, P))
      return false;
    PM = std::move(resp);
    return true;
  }
  case eMessageTypeEvent:
    Event evt;
    if (!fromJSON(Params, evt, P))
      return false;
    PM = std::move(evt);
    return true;
  }
  llvm_unreachable("unhandled message type request.");
}

json::Value toJSON(const Message &M) {
  return std::visit([](auto &M) { return toJSON(M); }, M);
}

json::Value toJSON(const ErrorResponseBody &E) {
  json::Object result{};

  if (E.error)
    result.insert({"error", *E.error});

  return result;
}

} // namespace lldb_dap::protocol
