(:~
positions in text
:)
module namespace pos="lsp/position";
import module namespace lspt = 'lsp-typedefs' at "lsp-typedefs.xqm";



(:~ find index from Position :)
declare function pos:resolvePosition($text as xs:string, $pos as lspt:Position) 
as lspt:num
{
  let $nl:= index-of(string-to-codepoints($text),10)
  let $off:=if($pos?line eq 0)
            then 0
            else $nl[$pos?line]
  let $_:=substring($text,$off+1,1)=>string-to-codepoints()=>trace("LF? ")
  return $off+$pos?character 
};

(:~  convert index into Position :)
declare function pos:toPosition($text as xs:string,
                               $index as lspt:num
                             )
as lspt:Position {
 let $nl:=  if($index gt string-length($text))
            then error(xs:QName("pos:range"),`out of range: index={$index},length={string-length($text)}`)
            else index-of(string-to-codepoints($text),10)
 let $line:=pos:lineAt($nl,$index)
 let $off:=if($line eq 0)
           then 0
           else $nl[$line]
return lspt:Position($line,  $index - $off)            
};
 


(:~  line number for $pos :)
declare function pos:lineAt($nl as xs:integer*,$pos as lspt:num)
as xs:integer
{
 if(empty($nl) or $pos le head($nl))
 then 0
 else if($pos gt foot($nl))
      then count($nl)
      else let $s:=do-until(
          {"min":1,"max":count($nl)},
          fn($r){ 
             let $mid:=round(($r?min+$r?max) div 2,0,"away-from-zero")
             return if ($nl[$mid] lt $pos)
                    then map:put($r,"min",$mid)
                    else map:put($r,"max",$mid -1)
},
          fn($r){$r?max eq $r?min}
          )
           return $s?max
};

(:~  format position for text display :)
declare function pos:ln-col($pos as lspt:Position,$offset as xs:integer:=0)
{
  `Ln { $pos?line + $offset}, Col { $pos?character + $offset}`
}; 

(:~ update $text with changes $chs from didChange 
:)
declare function pos:apply-changes($text as xs:string, $chs as array(*))
as xs:string{
array:fold-left($chs,$text,pos:apply-change#2)
}; 

(:~ text updated single change :)
declare function pos:apply-change($text as xs:string, $ch as map(*))
as xs:string{
 if(exists($ch?range))
 then substring($text,1, pos:resolvePosition($text, $ch?range?start)) 
      || $ch?text ||
      substring($text,pos:resolvePosition($text, $ch?range?end))
 else $ch?text 
}; 

(:~ full range for $text :)
declare function pos:full-range($text as xs:string)
as lspt:Range{
 lspt:Range(pos:toPosition($text,0), pos:toPosition($text, string-length($text)))
}; 

(:~   range for $text from indices:)
declare function pos:range-from-ast(
 $ast as element(*),
 $text as xs:string)
as lspt:Range{
 lspt:Range(
   pos:toPosition($text,number($ast/@start)-1), 
   pos:toPosition($text, number($ast/@end))
 )
}; 
