// 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.

#ifndef V8_TORQUE_LS_MESSAGE_H_
#define V8_TORQUE_LS_MESSAGE_H_

#include "src/base/logging.h"
#include "src/torque/ls/json.h"
#include "src/torque/ls/message-macros.h"
#include "src/torque/source-positions.h"

namespace v8 {
namespace internal {
namespace torque {
namespace ls {

// Base class for Messages and Objects that are backed by either a
// JsonValue or a reference to a JsonObject.
// Helper methods are used by macros to implement typed accessors.
class BaseJsonAccessor {
 public:
  template <class T>
  T GetObject(const std::string& property) {
    return T(GetObjectProperty(property));
  }

  bool HasProperty(const std::string& property) const {
    return object().count(property) > 0;
  }

  void SetNull(const std::string& property) {
    object()[property] = JsonValue::JsonNull();
  }

  bool IsNull(const std::string& property) const {
    return HasProperty(property) &&
           object().at(property).tag == JsonValue::IS_NULL;
  }

 protected:
  virtual const JsonObject& object() const = 0;
  virtual JsonObject& object() = 0;

  JsonObject& GetObjectProperty(const std::string& property) {
    if (!object()[property].IsObject()) {
      object()[property] = JsonValue::From(JsonObject{});
    }
    return object()[property].ToObject();
  }

  JsonArray& GetArrayProperty(const std::string& property) {
    if (!object()[property].IsArray()) {
      object()[property] = JsonValue::From(JsonArray{});
    }
    return object()[property].ToArray();
  }

  JsonObject& AddObjectElementToArrayProperty(const std::string& property) {
    JsonArray& array = GetArrayProperty(property);
    array.push_back(JsonValue::From(JsonObject{}));

    return array.back().ToObject();
  }
};

// Base class for Requests, Responses and Notifications.
// In contrast to "BaseObject", a Message owns the backing JsonValue of the
// whole object tree; i.e. value_ serves as root.
class Message : public BaseJsonAccessor {
 public:
  Message() {
    value_ = JsonValue::From(JsonObject{});
    set_jsonrpc("2.0");
  }
  explicit Message(JsonValue value) : value_(std::move(value)) {
    CHECK(value_.tag == JsonValue::OBJECT);
  }

  JsonValue& GetJsonValue() { return value_; }

  JSON_STRING_ACCESSORS(jsonrpc)

 protected:
  const JsonObject& object() const { return value_.ToObject(); }
  JsonObject& object() { return value_.ToObject(); }

 private:
  JsonValue value_;
};

// Base class for complex type that might be part of a Message.
// Instead of creating theses directly, use the accessors on the
// root Message or a parent object.
class NestedJsonAccessor : public BaseJsonAccessor {
 public:
  explicit NestedJsonAccessor(JsonObject& object) : object_(object) {}

  const JsonObject& object() const { return object_; }
  JsonObject& object() { return object_; }

 private:
  JsonObject& object_;
};

class ResponseError : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_INT_ACCESSORS(code)
  JSON_STRING_ACCESSORS(message)
};

class InitializeParams : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_INT_ACCESSORS(processId)
  JSON_STRING_ACCESSORS(rootPath)
  JSON_STRING_ACCESSORS(rootUri)
  JSON_STRING_ACCESSORS(trace)
};

class FileListParams : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  // TODO(szuend): Implement read accessor for string
  //               arrays. "files" is managed directly.
};

class FileSystemWatcher : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_STRING_ACCESSORS(globPattern)
  JSON_INT_ACCESSORS(kind)

  enum WatchKind {
    kCreate = 1,
    kChange = 2,
    kDelete = 4,

    kAll = kCreate | kChange | kDelete,
  };
};

class DidChangeWatchedFilesRegistrationOptions : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_ARRAY_OBJECT_ACCESSORS(FileSystemWatcher, watchers)
};

class FileEvent : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_STRING_ACCESSORS(uri)
  JSON_INT_ACCESSORS(type)
};

class DidChangeWatchedFilesParams : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_ARRAY_OBJECT_ACCESSORS(FileEvent, changes)
};

class SaveOptions : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_BOOL_ACCESSORS(includeText)
};

class TextDocumentSyncOptions : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_BOOL_ACCESSORS(openClose)
  JSON_INT_ACCESSORS(change)
  JSON_BOOL_ACCESSORS(willSave)
  JSON_BOOL_ACCESSORS(willSaveWaitUntil)
  JSON_OBJECT_ACCESSORS(SaveOptions, save)
};

class ServerCapabilities : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_OBJECT_ACCESSORS(TextDocumentSyncOptions, textDocumentSync)
  JSON_BOOL_ACCESSORS(definitionProvider)
  JSON_BOOL_ACCESSORS(documentSymbolProvider)
};

class InitializeResult : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_OBJECT_ACCESSORS(ServerCapabilities, capabilities)
};

class Registration : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_STRING_ACCESSORS(id)
  JSON_STRING_ACCESSORS(method)
  JSON_DYNAMIC_OBJECT_ACCESSORS(registerOptions)
};

class RegistrationParams : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_ARRAY_OBJECT_ACCESSORS(Registration, registrations)
};

class JsonPosition : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_INT_ACCESSORS(line)
  JSON_INT_ACCESSORS(character)
};

class Range : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_OBJECT_ACCESSORS(JsonPosition, start)
  JSON_OBJECT_ACCESSORS(JsonPosition, end)
};

class Location : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_STRING_ACCESSORS(uri)
  JSON_OBJECT_ACCESSORS(Range, range)

  void SetTo(SourcePosition position) {
    set_uri(SourceFileMap::AbsolutePath(position.source));
    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);
  }
};

class TextDocumentIdentifier : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_STRING_ACCESSORS(uri)
};

class TextDocumentPositionParams : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_OBJECT_ACCESSORS(TextDocumentIdentifier, textDocument)
  JSON_OBJECT_ACCESSORS(JsonPosition, position)
};

class Diagnostic : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  enum DiagnosticSeverity {
    kError = 1,
    kWarning = 2,
    kInformation = 3,
    kHint = 4
  };

  JSON_OBJECT_ACCESSORS(Range, range)
  JSON_INT_ACCESSORS(severity)
  JSON_STRING_ACCESSORS(source)
  JSON_STRING_ACCESSORS(message)
};

class PublishDiagnosticsParams : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_STRING_ACCESSORS(uri)
  JSON_ARRAY_OBJECT_ACCESSORS(Diagnostic, diagnostics)
};

enum SymbolKind {
  kFile = 1,
  kNamespace = 3,
  kClass = 5,
  kMethod = 6,
  kProperty = 7,
  kField = 8,
  kConstructor = 9,
  kFunction = 12,
  kVariable = 13,
  kConstant = 14,
  kStruct = 23,
};

class DocumentSymbolParams : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_OBJECT_ACCESSORS(TextDocumentIdentifier, textDocument)
};

class SymbolInformation : public NestedJsonAccessor {
 public:
  using NestedJsonAccessor::NestedJsonAccessor;

  JSON_STRING_ACCESSORS(name)
  JSON_INT_ACCESSORS(kind)
  JSON_OBJECT_ACCESSORS(Location, location)
  JSON_STRING_ACCESSORS(containerName)
};

template <class T>
class Request : public Message {
 public:
  explicit Request(JsonValue value) : Message(std::move(value)) {}
  Request() : Message() {}

  JSON_INT_ACCESSORS(id)
  JSON_STRING_ACCESSORS(method)
  JSON_OBJECT_ACCESSORS(T, params)
};
using InitializeRequest = Request<InitializeParams>;
using RegistrationRequest = Request<RegistrationParams>;
using TorqueFileListNotification = Request<FileListParams>;
using GotoDefinitionRequest = Request<TextDocumentPositionParams>;
using DidChangeWatchedFilesNotification = Request<DidChangeWatchedFilesParams>;
using PublishDiagnosticsNotification = Request<PublishDiagnosticsParams>;
using DocumentSymbolRequest = Request<DocumentSymbolParams>;

template <class T>
class Response : public Message {
 public:
  explicit Response(JsonValue value) : Message(std::move(value)) {}
  Response() : Message() {}

  JSON_INT_ACCESSORS(id)
  JSON_OBJECT_ACCESSORS(ResponseError, error)
  JSON_OBJECT_ACCESSORS(T, result)
};
using InitializeResponse = Response<InitializeResult>;
using GotoDefinitionResponse = Response<Location>;

// Same as "Response" but the result is T[] instead of T.
template <class T>
class ResponseArrayResult : public Message {
 public:
  explicit ResponseArrayResult(JsonValue value) : Message(std::move(value)) {}
  ResponseArrayResult() : Message() {}

  JSON_INT_ACCESSORS(id)
  JSON_OBJECT_ACCESSORS(ResponseError, error)
  JSON_ARRAY_OBJECT_ACCESSORS(T, result)
};
using DocumentSymbolResponse = ResponseArrayResult<SymbolInformation>;

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

#endif  // V8_TORQUE_LS_MESSAGE_H_