(: Store for XQuery document data , type, text,uri
 on save parse is created and stored
 data is stored in webSocket attributes
@author Andy Bunce
:)
module namespace docs="lsp/docs";
import module namespace pos="lsp/position" at "position.xqm";
import module namespace rpc = 'rpc' at 'jsonrpc.xqm';
import module namespace lsp-diags = 'lsp-diags' at 'providers/diagnostics.xqm';
import module namespace ast = 'lsp/ast' at 'ast/ast.xqm';

(: save $textDocument data as session $socket properties 
@return uri
:)
declare function docs:open(
    $socket as xs:string,
    $params as map(*)
) as xs:string
{
let $text as xs:string:=$params?textDocument?text
let $uri:=$params?textDocument?uri
let $files:=ws:get($socket,"files",{})
let $files:=if(map:contains($files,$uri)) 
            then $files 
            else 
                 let $uuid:=random:uuid()
                 return map:put($files,$uri,{
                "textDocument": "file-" || $uuid,
                "parse": "parse-" || $uuid })
let $keys:=$files($uri)
return (
    ws:set($socket,"files",$files),
    ws:set($socket,$keys?textDocument,$params?textDocument),
    $uri
    )
};

(: change $textDocument data as session $socket properties 
@return uri
:)
declare function docs:change(
    $socket as xs:string,
    $params as map(*)
) as xs:string
{
let $uri:=$params?textDocument?uri
let $key:=docs:key($socket,$uri,"textDocument")

let $file:=if(exists($key))
           then ws:get($socket,$key)
           else error(xs:QName("docs:change"), "no file" || $uri)

let $ver:=$params?textDocument?version
let $IGNORE_VER:=true()
let $text:=if($IGNORE_VER or $ver eq 1+ $file?version)
          then pos:apply-changes($file?text,$params?contentChanges)
          else error(xs:QName("docs:change"),`badver got {$ver} expecting: {1+ $file?version}`)

let $file:=$file=>map:put("version",$ver)=>map:put("text",$text)

return ( ws:set($socket,$key,$file)
       , $uri)
   
};

(:~ close a file :)
declare function docs:close($socket as xs:string,$uri as xs:string)
as map(*){
let $files:=ws:get($socket,"files",{})
return if(map:contains($files,$uri))
       then let $props:=$files($uri)
       return (
             map:items($props)!ws:delete($socket,.),
             ws:set($socket,"files",map:remove($files,$uri))
             )
       else ()
};

(:~ list of all files :)
declare function docs:list($socket as xs:string)
as xs:string*{
ws:get($socket,"files",{})=>map:keys()

};

(: document info :)
declare type docs:Property as enum(
    "textDocument",
    "parse"
);

(: get $property key for $file from session $socket :)
declare function docs:key(
    $socket as xs:string,
    $file as xs:string,
    $property as docs:Property
) as xs:string?
{
    ws:get($socket,"files")($file)($property)
};

(: get $property key for $file from session $socket :)
declare function docs:get(
    $socket as xs:string,
    $file as xs:string,
    $property as docs:Property
) as item()?
{
    ws:get($socket,
    docs:key($socket,$file,$property)
    )
};

(:~ generate XML parse, save it and publish diagnostics :)
declare function docs:parse(
    $socket as xs:string,
    $file as xs:string
)as map(*)?
{
  let $text:=docs:get($socket,$file,"textDocument")?text
  let $xml:= ast:build($text, {"uri": $file, "abstract":false(), "position":true()})
  let $diags:=lsp-diags:list($file, $text, $xml)=>prof:time("⏱️ diags " || $file)
  return (
    ws:set($socket,docs:key($socket,$file,"parse"),$xml),
    {"jsonrpc": "2.0",
     "method":"textDocument/publishDiagnostics",
     "params":{"uri": $file, "diagnostics": array{$diags}}
}
  )
};

declare function docs:lineEnd($txt as xs:string)
as xs:string{
  let $nl:= char('\r') || char('\n')
  return switch(){
    case contains($txt,$nl) return $nl
    case contains($txt,char('\r')) return char('\r')
    case contains($txt,char('\n')) return char('\n')
    default return char('\n')
  }
  
};