/-
Copyright (c) 2020 Marc Huisinga. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.

Authors: Marc Huisinga, Wojciech Nawrocki
-/
import Init.Control
import Init.System.IO
import Lean.Data.RBTree
import Lean.Data.Json

/-! Implementation of JSON-RPC 2.0 (https://www.jsonrpc.org/specification)
for use in the LSP server. -/

namespace Lean.JsonRpc

open Json

/-- In JSON-RPC, each request from the client editor to the language server comes with a
request id so that the corresponding response can be identified or cancelled. -/
inductive RequestID where
  | str (s : String)
  | num (n : JsonNumber)
  | null
  deriving Inhabited, BEq, Ord

instance : OfNat RequestID n := ⟨RequestID.num n⟩

instance : ToString RequestID where
  toString
  | RequestID.str s => s!"\"{s}\""
  | RequestID.num n => toString n
  | RequestID.null => "null"

/-- Error codes defined by
[JSON-RPC](https://www.jsonrpc.org/specification#error_object) and
[LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#errorCodes). -/
inductive ErrorCode where
  /-- Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text. -/
  | parseError
  /-- The JSON sent is not a valid Request object. -/
  | invalidRequest
  /-- The method does not exist / is not available. -/
  | methodNotFound
  /-- Invalid method parameter(s). -/
  | invalidParams
  /-- Internal JSON-RPC error. -/
  | internalError
  /-- Error code indicating that a server received a notification or
  request before the server has received the `initialize` request. -/
  | serverNotInitialized
  | unknownErrorCode
  -- LSP-specific codes below.
  /-- The server detected that the content of a document got
  modified outside normal conditions. A server should
  NOT send this error code if it detects a content change
  in it unprocessed messages. The result even computed
  on an older state might still be useful for the client.

  If a client decides that a result is not of any use anymore
  the client should cancel the request. -/
  | contentModified
  /-- The client has canceled a request and a server as detected the cancel. -/
  | requestCancelled
  -- Lean-specific codes below.
  | rpcNeedsReconnect
  | workerExited
  | workerCrashed
  deriving Inhabited, BEq

instance : FromJson ErrorCode := ⟨fun
  | num (-32700 : Int) => return ErrorCode.parseError
  | num (-32600 : Int) => return ErrorCode.invalidRequest
  | num (-32601 : Int) => return ErrorCode.methodNotFound
  | num (-32602 : Int) => return ErrorCode.invalidParams
  | num (-32603 : Int) => return ErrorCode.internalError
  | num (-32002 : Int) => return ErrorCode.serverNotInitialized
  | num (-32001 : Int) => return ErrorCode.unknownErrorCode
  | num (-32801 : Int) => return ErrorCode.contentModified
  | num (-32800 : Int) => return ErrorCode.requestCancelled
  | num (-32900 : Int) => return ErrorCode.rpcNeedsReconnect
  | num (-32901 : Int) => return ErrorCode.workerExited
  | num (-32902 : Int) => return ErrorCode.workerCrashed
  | _  => throw "expected error code"⟩

instance : ToJson ErrorCode := ⟨fun
  | ErrorCode.parseError           => (-32700 : Int)
  | ErrorCode.invalidRequest       => (-32600 : Int)
  | ErrorCode.methodNotFound       => (-32601 : Int)
  | ErrorCode.invalidParams        => (-32602 : Int)
  | ErrorCode.internalError        => (-32603 : Int)
  | ErrorCode.serverNotInitialized => (-32002 : Int)
  | ErrorCode.unknownErrorCode     => (-32001 : Int)
  | ErrorCode.contentModified      => (-32801 : Int)
  | ErrorCode.requestCancelled     => (-32800 : Int)
  | ErrorCode.rpcNeedsReconnect    => (-32900 : Int)
  | ErrorCode.workerExited         => (-32901 : Int)
  | ErrorCode.workerCrashed        => (-32902 : Int)⟩

/-- A JSON-RPC message.

Uses separate constructors for notifications and errors because client and server
behavior is expected to be wildly different for both.
-/
inductive Message where
  /-- A request message to describe a request between the client and the server. Every processed request must send a response back to the sender of the request. -/
  | request (id : RequestID) (method : String) (params? : Option Structured)
  /-- A notification message. A processed notification message must not send a response back. They work like events. -/
  | notification (method : String) (params? : Option Structured)
  /-- A Response Message sent as a result of a request. -/
  | response (id : RequestID) (result : Json)
  /-- A non-successful response. -/
  | responseError (id : RequestID) (code : ErrorCode) (message : String) (data? : Option Json)

def Batch := Array Message

/-- Generic version of `Message.request`.

A request message to describe a request between the client and the server. Every processed request must send a response back to the sender of the request.

- [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage)
- [JSON-RPC](https://www.jsonrpc.org/specification#request_object)
-/
structure Request (α : Type u) where
  id     : RequestID
  method : String
  param  : α
  deriving Inhabited, BEq

instance [ToJson α] : Coe (Request α) Message :=
  ⟨fun r => Message.request r.id r.method (toStructured? r.param).toOption⟩

/-- Generic version of `Message.notification`.

A notification message. A processed notification message must not send a response back. They work like events.

- [JSON-RPC](https://www.jsonrpc.org/specification#notification)
- [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage).
-/
structure Notification (α : Type u) where
  method : String
  param  : α
  deriving Inhabited, BEq

instance [ToJson α] : Coe (Notification α) Message :=
  ⟨fun r => Message.notification r.method (toStructured? r.param).toOption⟩

/-- Generic version of `Message.response`.

A Response Message sent as a result of a request. If a request doesn’t provide a
result value the receiver of a request still needs to return a response message
to conform to the JSON-RPC specification. The result property of the ResponseMessage
should be set to null in this case to signal a successful request.

References:
- [JSON-RPC](https://www.jsonrpc.org/specification#response_object)
- [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage)
-/
structure Response (α : Type u) where
  id     : RequestID
  result : α
  deriving Inhabited, BEq

instance [ToJson α] : Coe (Response α) Message :=
  ⟨fun r => Message.response r.id (toJson r.result)⟩

/-- Generic version of `Message.responseError`.

References:
- [JSON-RPC](https://www.jsonrpc.org/specification#error_object)
- [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseError).
-/
structure ResponseError (α : Type u) where
  id      : RequestID
  code    : ErrorCode
  /-- A string providing a short description of the error. -/
  message : String
  /-- A primitive or structured value that contains additional
      information about the error. Can be omitted. -/
  data?   : Option α := none
  deriving Inhabited, BEq

instance [ToJson α] : Coe (ResponseError α) Message :=
  ⟨fun r => Message.responseError r.id r.code r.message (r.data?.map toJson)⟩

instance : Coe String RequestID := ⟨RequestID.str⟩
instance : Coe JsonNumber RequestID := ⟨RequestID.num⟩

private def RequestID.lt : RequestID → RequestID → Bool
  | RequestID.str a, RequestID.str b            => a < b
  | RequestID.num a, RequestID.num b            => a < b
  | RequestID.null,  RequestID.num _            => true
  | RequestID.null,  RequestID.str _            => true
  | RequestID.num _, RequestID.str _            => true
  | _, _ /- str < *, num < null, null < null -/ => false

private def RequestID.ltProp : LT RequestID :=
  ⟨fun a b => RequestID.lt a b = true⟩

instance : LT RequestID :=
  RequestID.ltProp

instance (a b : RequestID) : Decidable (a < b) :=
  inferInstanceAs (Decidable (RequestID.lt a b = true))

instance : FromJson RequestID := ⟨fun j =>
  match j with
  | str s => return RequestID.str s
  | num n => return RequestID.num n
  | _     => throw "a request id needs to be a number or a string"⟩

instance : ToJson RequestID := ⟨fun rid =>
  match rid with
  | RequestID.str s => s
  | RequestID.num n => num n
  | RequestID.null  => null⟩

instance : ToJson Message := ⟨fun m =>
  mkObj $ ⟨"jsonrpc", "2.0"⟩ :: match m with
  | Message.request id method params? =>
    [ ⟨"id", toJson id⟩,
      ⟨"method", method⟩
    ] ++ opt "params" params?
  | Message.notification method params? =>
    ⟨"method", method⟩ ::
    opt "params" params?
  | Message.response id result =>
    [ ⟨"id", toJson id⟩,
      ⟨"result", result⟩]
  | Message.responseError id code message data? =>
    [ ⟨"id", toJson id⟩,
      ⟨"error", mkObj $ [
          ⟨"code", toJson code⟩,
          ⟨"message", message⟩
        ] ++ opt "data" data?⟩
    ]⟩

instance : FromJson Message where
  fromJson? j := do
    let "2.0" ← j.getObjVal? "jsonrpc" | throw "only version 2.0 of JSON RPC is supported"
    (do let id ← j.getObjValAs? RequestID "id"
        let method ← j.getObjValAs? String "method"
        let params? := j.getObjValAs? Structured "params"
        pure (Message.request id method params?.toOption)) <|>
    (do let method ← j.getObjValAs? String "method"
        let params? := j.getObjValAs? Structured "params"
        pure (Message.notification method params?.toOption)) <|>
    (do let id ← j.getObjValAs? RequestID "id"
        let result ← j.getObjVal? "result"
        pure (Message.response id result)) <|>
    (do let id ← j.getObjValAs? RequestID "id"
        let err ← j.getObjVal? "error"
        let code ← err.getObjValAs? ErrorCode "code"
        let message ← err.getObjValAs? String "message"
        let data? := err.getObjVal? "data"
        pure (Message.responseError id code message data?.toOption))

-- TODO(WN): temporary until we have deriving FromJson
instance [FromJson α] : FromJson (Notification α) where
  fromJson? j := do
    let msg : Message ← fromJson? j
    if let Message.notification method params? := msg then
      let params := params?
      let param : α ← fromJson? (toJson params)
      pure $ ⟨method, param⟩
    else throw "not a notfication"

end Lean.JsonRpc

namespace IO.FS.Stream

open Lean
open Lean.JsonRpc

section
  def readMessage (h : FS.Stream) (nBytes : Nat) : IO Message := do
    let j ← h.readJson nBytes
    match fromJson? j with
    | Except.ok m => pure m
    | Except.error inner => throw $ userError s!"JSON '{j.compress}' did not have the format of a JSON-RPC message.\n{inner}"

  def readRequestAs (h : FS.Stream) (nBytes : Nat) (expectedMethod : String) (α) [FromJson α] : IO (Request α) := do
    let m ← h.readMessage nBytes
    match m with
    | Message.request id method params? =>
      if method = expectedMethod then
        let j := toJson params?
        match fromJson? j with
        | Except.ok v => pure ⟨id, expectedMethod, v⟩
        | Except.error inner => throw $ userError s!"Unexpected param '{j.compress}' for method '{expectedMethod}'\n{inner}"
      else
        throw $ userError s!"Expected method '{expectedMethod}', got method '{method}'"
    | _ => throw $ userError s!"Expected JSON-RPC request, got: '{(toJson m).compress}'"

  def readNotificationAs (h : FS.Stream) (nBytes : Nat) (expectedMethod : String) (α) [FromJson α] : IO (Notification α) := do
    let m ← h.readMessage nBytes
    match m with
    | Message.notification method params? =>
      if method = expectedMethod then
        let j := toJson params?
        match fromJson? j with
        | Except.ok v => pure ⟨expectedMethod, v⟩
        | Except.error inner => throw $ userError s!"Unexpected param '{j.compress}' for method '{expectedMethod}'\n{inner}"
      else
        throw $ userError s!"Expected method '{expectedMethod}', got method '{method}'"
    | _ => throw $ userError s!"Expected JSON-RPC notification, got: '{(toJson m).compress}'"

  partial def readResponseAs (h : FS.Stream) (nBytes : Nat) (expectedID : RequestID) (α) [FromJson α] : IO (Response α) := do
    let m ← h.readMessage nBytes
    match m with
    | Message.response id result =>
      if id == expectedID then
        match fromJson? result with
        | Except.ok v => pure ⟨expectedID, v⟩
        | Except.error inner => throw $ userError s!"Unexpected result '{result.compress}'\n{inner}"
      else
        throw $ userError s!"Expected id {expectedID}, got id {id}"
    | Message.notification .. => readResponseAs h nBytes expectedID α
    | _ => throw $ userError s!"Expected JSON-RPC response, got: '{(toJson m).compress}'"
end

section
  variable [ToJson α]

  def writeMessage (h : FS.Stream) (m : Message) : IO Unit :=
    h.writeJson (toJson m)

  def writeRequest (h : FS.Stream) (r : Request α) : IO Unit :=
    h.writeMessage r

  def writeNotification (h : FS.Stream) (n : Notification α) : IO Unit :=
    h.writeMessage n

  def writeResponse (h : FS.Stream) (r : Response α) : IO Unit :=
    h.writeMessage r

  def writeResponseError (h : FS.Stream) (e : ResponseError Unit) : IO Unit :=
    h.writeMessage (Message.responseError e.id e.code e.message none)

  def writeResponseErrorWithData (h : FS.Stream) (e : ResponseError α) : IO Unit :=
    h.writeMessage e
end

end IO.FS.Stream
