#
# https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/
#

# Request message:
#
# Content-Length: ...\r\n
# \r\n
# {
#    "jsonrpc": "2.0",
#    "id": 1,
#    "method": "textDocument/completion",
#    "params": {
#       ...
#    }
# }
#
# Response msg:
#
# Content-Length: ...\r\n
# \r\n
# {
#    "jsonrpc": "2.0",
#    "id": 1,
#    "result": ...,
#    "error": ...
# }
#
#	The result of a request is REQUIRED on success.
#	It MUST NOT exist if there was an error invoking the method.
#  In case of an error, and error objet should bre returned.
#

package json
link ximage

$define CONTENT_LENGTH "Content-Length: "

# JRPC_Message is a class that represents a JSON-RPC message
# It is used to parse the JSON-RPC message and to create the JSON-RPC message
class JRPC_Message(msg, kind, json_table)

   # Parse the JSON-RPC message and set the kind of the message
   method parse_json(s)
      msg := s
      json_table := &null

      if json_table := jtous(s) then {
         guess_kind()
         return self
      } else
         kind := "unknown"
   end

   # Guess the kind of the message based on the JSON-RPC message
   method guess_kind()
      if \json_table["error"] then
         kind := "error"
      else if \json_table["method"] then
         kind := if \json_table["id"] then "request" else "notification"
      else if \json_table["result"] then
         kind := "response"
      else
         kind := "unknown"

      return kind
   end

   # Get the JSON RPC kind of the message
   method get_kind()
      return kind
   end
   # Get the JSON RPC id of the message
   method get_id()
      return json_table["id"]
   end
   # Get the JSON RPC method of the message
   method get_method()
      return json_table["method"]
   end
   # Get the JSON RPC params of the message
   method get_params()
      return json_table["params"]
   end
   # many requests operate on a file, stored in uri param
   # used by ulsp server to get the file path
   method get_param_uri()
      return json_table["params"]["textDocument"]["uri"]
   end

   # Get the result of returned by a json rpc request
   method get_result()
      return json_table["result"]
   end
   # Get the error of returned by a json rpc request
   method get_error()
      return json_table["error"]
   end
   # Get the raw message content
   method get_content()
      return msg
   end

   method to_json()
      if msg := tojson(json_table) then
         return msg
   end

   # Initialize the JSON RPC message
   method init_msg()
      /json_table := ["jsonrpc" : "2.0"]
   end

   # Set the result of the JSON RPC message
   method set_result(result)
      init_msg()
      json_table["result"] := result
   end
   # Set the id of the JSON RPC message
   method set_id(id)
      init_msg()
      return json_table["id"] := id
   end
   # Set the method of the JSON RPC message
   method set_method(meth)
      init_msg()
      json_table["method"] := meth
      guess_kind()
      return meth
   end
   # Set the params of the JSON RPC message
   method set_params(params)
      init_msg()
      json_table["params"] := params
      return params
   end

   # Make a result response JSON RPC message
   method make_result_response(result)
      init_msg()
      delete(json_table, "method", "params", "error")
      # JSON-RPC 2.0: result can be null, so preserve null
      json_table["result"] := result
      guess_kind()
      if msg := tojson(json_table) then
         return self
   end

   # Make a request JSON RPC message
   method make_request(id, meth, params)
      init_msg()
      if /json_table then
         json_table := [
            "id" : id;
            "method" : meth;
            "params" : params
            ]
      else {
         delete(json_table, "result", "error")
         json_table["id"] := id
         json_table["method"] := meth
         json_table["params"] := params
      }
      guess_kind()
      if msg := tojson(json_table) then
         return self
   end

   method make_notification(meth, params)
      init_msg()
      if /json_table then
         json_table := [
            "method" : meth;
            "params" : params
            ]
      else {
         delete(json_table, "id", "result", "error")
         json_table["method"] := meth
         json_table["params"] := params
      }
      guess_kind()
      if msg := tojson(json_table) then
         return self
   end

   # ErrorCodes
   # # Defined by JSON-RPC
   # ParseError: -32700;
   # InvalidRequest: -32600;
   # MethodNotFound: -32601;
   # InvalidParams: -32602;
   # InternalError:  -32603;

   #
   # Error code indicating that a server received a notification or
   # request before the server has received the `initialize` request.
   #
   # ServerNotInitialized: -32002;
   # UnknownErrorCode: -32001;

   #
   # A request failed but it was syntactically correct, e.g the
   # method name was known and the parameters were valid. The error
   # message should contain human readable information about why
   # the request failed.
   #
   # RequestFailed: -32803;

   #
   # The server cancelled the request. This error code should
   # only be used for requests that explicitly support being
   # server cancellable.
   #
   # ServerCancelled: -32802;

   #
   # 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 its 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: -32801;

   #
   # The client has canceled a request and a server has detected
   # the cancel.
   #
   # RequestCancelled: -32800;

   # Make an error response JSON RPC message
   method make_error_response(err_code, err_msg, err_data)
      init_msg()
      if /json_table then
         json_table := [
            "error" : [
               "code" : err_code;
               "message" : err_msg;
               "data" : \err_data | ""
            ]
         ]
      else {
         delete(json_table, "method", "params", "result")
         json_table["error"] := [
            "code" : err_code;
            "message" : err_msg;
            "data" : \err_data | ""
         ]
      }
      guess_kind()
      if msg := tojson(json_table) then
         return self
   end

   # Initialize the JSON RPC message
   initially
      kind := "unknown"
end


class JSON_HTTPSocket(sock)
   #
   # parse the http header in the form of
   #   ...
   #   Content-Length: <SIZE>
   #   ...
   #   \r\n\r\n
   # and return the length of the message
   method get_http_header(timeout)
      local len, data := "", size := 22, chunk
      /timeout := -1
      repeat {
         # Wait for a message to arrive, read the header
         # assume the min header size is 22, because the shortest
         # msgs will be at least 2 digits, I.e header size >=22
         *select(\sock, timeout) > 0  | fail
         
         chunk := ready(sock, size-*data)
         \chunk | fail
         chunk ~== "" | fail
         data ||:= chunk

         if *data < size then next

         data ? {
            if = CONTENT_LENGTH then {
               len := integer(tab(many(&digits))) | fail
               # Make sure we have the whole header.
               # For long messages, 5 digits size, read one byte at a time until we
               # find end. This should be very uncommon
               ="\r\n\r\n" & return len
               size +:=1
            }
            else
               data := tab(find(CONTENT_LENGTH)) | ""
         }
      }
   end

   # Get the data from the socket
   method get_data(size, timeout)
      local data := "", chunk
      /timeout := -1
      while *data < size do {
         *select(sock, timeout) > 0 | fail
         
         chunk := ready(sock, size - *data)
         \chunk | fail
         chunk ~== "" | fail
         data ||:= chunk
      }
      return data
   end

   method get_msg(timeout)
      local len, body, t
      len := get_http_header(timeout) | fail
      body := get_data(len, timeout) | fail
      t := jtous(body) | fail
      return t
   end

   method send_msg(t)
      local body
      body := tojson(t) | fail
      writes(sock, CONTENT_LENGTH, *body, "\r\n\r\n", body)
   end

   # Initialize the JSON HTTPSocket
   initially(addr, opt)
      if string(addr) then
         sock := open(addr,opt)
      else
         sock := addr

end


class JRPC_HTTPSocket : JSON_HTTPSocket()

   # Get the JSON RPC message from the socket
   method get_msg(timeout)
      local msg_body, len
      len := get_http_header(timeout) | fail
      msg_body := get_data(len, timeout) | fail
      if msg_body[1] == "{" & msg_body[-1] == "}" then {
         return JRPC_Message().parse_json(msg_body)
      }
   end

   # Send the JSON RPC message to the socket
   method send_msg(msg)
      local data
      data := msg.get_content()
      writes(sock, CONTENT_LENGTH, *data, "\r\n\r\n", data)
   end

   initially(addr, opt)
      return self.JSON_HTTPSocket.initially(addr, opt)
      
end
