(:~
 : jsonrpc
 : @author andy bunce
 :)
module namespace rpc = 'rpc';
import module namespace lsp-text = 'lsp-text' at "lsp-text.xqm";

(:~ build a notification :)
declare function rpc:build-notification ($method as xs:string, $params as map(*)) 
as map(*){
{"jsonrpc": "2.0", "method": $method, "params": $params }
};

(:~ response to $json msg, if result empty just acknowledge :)
declare function rpc:build-response($json as map(*),$result:=())
as map(*){
{ "jsonrpc": "2.0", "id": $json?id,  "result": $result}
};

(:~ response when error :)
declare function rpc:build-error($info as map(*),$json as map(*))
as map(*){
  { "jsonrpc": "2.0", "id": $json?id,  "error": $info  }
};

(: map methods to functions :)
declare variable $rpc:Methods:=map:merge((
  map{
  "initialize" : rpc:method-initialize#1,
  "initialized" : rpc:method-initialized#1,
  "workspace/didChangeConfiguration" :rpc:method-unknown#1
},
  $lsp-text:methods
));

(:~ return map if $msg is jsonrpc else empty :)
declare
function rpc:parse($msg as xs:string) 
as map(*)? 
{
  try {
    let $json:=parse-json($msg)
    return if($json?jsonrpc eq "2.0" and exists($json?method))
           then $json
           else ()
  } catch *{ 
    ()
  }
  
};

(:~ send reply to $json message
 get functions for methods
 evaluate function with message
 send any responses
 :)
declare
function rpc:reply($json as map(*)) 
as empty-sequence() {
  let $f :=(
            rpc:admin-log($json,"➡️"),
            $rpc:Methods?($json?method)
            )
  let $response := $f!.($json)
  return $response!rpc:send(.)
};

(:~ send with logging :)
declare
function rpc:send($msg as map(*)) 
as empty-sequence()
{
  rpc:admin-log($msg,"⬅️"),
  ws:send($msg ,ws:id())
};

(:~ canned initialize response :)
declare
function rpc:method-initialize($json as map(*)) 
as map(*)
{
  ws:set(ws:id(),"client", $json?params),
  rpc:build-response($json,  
             json:doc("etc/capabilities.json",{"format":"w3"}))
};

(:~ initialized response :)
declare
function rpc:method-initialized($json as map(*)) 
as empty-sequence()
{
  ws:set(ws:id(),"initialized", true())
  (: ,rpc:later(5000,"rpc:show-message","This is a test async message 5secs after initialized",ws:id()) :)
};

(:~  unknown method response :)
declare
function rpc:method-unknown($json as map(*)) 
as map(*)?
{
 let $_:=trace($json?method,"unknown")
 return ()
};

(:~ rpc log message :)
declare function rpc:log($msg as xs:string)
as map(*)
{
 rpc:build-notification(
      "window/logMessage",
      {"type":1, "message": $msg}
)
};

(:~ write to admin:log :)
declare function rpc:admin-log($data as item()*,$emoji as xs:string)
as empty-sequence()
{
  let $msg:= json:serialize($data,{ 'format': 'w3', 'indent': 'no' })
  return admin:write-log($emoji || $msg,"LSP")
};


(:~ window/showMessage :)
declare function rpc:show-message($msg as xs:string)
as map(*)
{
{"jsonrpc": "2.0",
     "method":"window/showMessage",
     "params":{"type":3, "message": $msg}
    }
};

(:~ send message after delay :)
declare function rpc:later($msdelay as xs:integer,$fn as xs:string ,$arg, $wsid as xs:string)
{
let $xq:=`import module namespace rpc = 'rpc' at 'jsonrpc.xqm';
declare variable $msdelay external;
declare variable $fn external;
declare variable $arg external;
declare variable $wsid external;

prof:sleep($msdelay),
ws:send(function-lookup(xs:QName($fn),1)($arg),$wsid)
`
let $bindings:={"msdelay": $msdelay,"wsid": $wsid, "fn": $fn,"arg":$arg}
let $opts:={"cache": true(),"base-uri": static-base-uri()}
return job:eval($xq, $bindings,$opts)=>void()
};