\ net2o tools

\ Copyright (C) 2015   Bernd Paysan

\ This program is free software: you can redistribute it and/or modify
\ it under the terms of the GNU Affero General Public License as published by
\ the Free Software Foundation, either version 3 of the License, or
\ (at your option) any later version.

\ This program is distributed in the hope that it will be useful,
\ but WITHOUT ANY WARRANTY; without even the implied warranty of
\ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
\ GNU Affero General Public License for more details.

\ You should have received a copy of the GNU Affero General Public License
\ along with this program.  If not, see <http://www.gnu.org/licenses/>.

require err.fs
require unix/pthread.fs
require unix/mmap.fs
require struct-val.fs
require 64bit.fs
require date.fs
require mini-oof2.fs
require forward.fs
require set-compsem.fs

\ enum

: enum ( n "name" -- n+1 )  dup Constant 1+ ;
: enums ( start n "name1" .. "namen" -- )
    0 ?DO enum LOOP drop ;
: bit ( n "name" -- n*2 )   dup Constant 2* ;
: bits: ( start n "name1" .. "namen" -- )
    0 ?DO bit LOOP drop ;

\ argument handling that works transparent from OS and Forth command line

user-o arg-o

object uclass arg-o
    umethod ?nextarg
    umethod ?@nextarg
    umethod ?peekarg
end-class cmd-args-c

align cmd-args-c , here constant cmd-args^

: cmd-args ( -- )  cmd-args^ arg-o ! ;
cmd-args

:noname ( -- addr u t / f )
    argc @ 1 > IF  next-arg true  ELSE  false  THEN ; to ?nextarg
:noname ( -- addr u t / f )
    argc @ 1 > IF  1 arg true  ELSE  false  THEN ; to ?peekarg
:noname ( -- addr u t / f )
    argc @ 1 > IF
	1 arg drop c@ '@' = IF  next-arg 1 /string true  EXIT  THEN
    THEN  false ; to ?@nextarg

cmd-args-c uclass arg-o
end-class word-args-c

align word-args-c , here constant word-args^

: word-args ( -- )  word-args^ arg-o ! ;

word-args

: parse-name" ( -- addr u )
    >in @ >r parse-name
    over c@ '"' = IF  2drop r@ >in ! '"' parse 2drop \"-parse  THEN  rdrop ;
: ?word-nextarg ( -- addr u t / f )
    parse-name" dup 0= IF  2drop  false  ELSE  true  THEN
; lastxt to ?nextarg
:noname ( -- addr u t / f )  >in @ >r
    parse-name" dup 0= IF  2drop  false  ELSE  true  THEN  r> >in !
; to ?peekarg
:noname ( -- addr u t / f )
    >in @ >r ?word-nextarg 0= IF  rdrop false  EXIT  THEN
    over c@ '@' = IF  rdrop 1 /string true  EXIT  THEN
    r> >in ! 2drop false ; to ?@nextarg

: arg-loop { xt -- }
    begin  ?nextarg  while  xt execute  repeat ;
: arg-loop# ( n xt -- ) { xt -- }
    0 ?DO  ?nextarg 0= ?LEAVE  xt execute  LOOP ;
: @arg-loop { xt -- }
    begin  ?@nextarg  while  xt execute  repeat ;

\ string

: smove ( a-from u-from a-to u-to -- )
    rot 2dup u< IF
	drop move -9 throw
    ELSE
	nip move
    THEN ;

[IFUNDEF] safe/string
: safe/string ( c-addr u n -- c-addr' u' )
\G protect /string against overflows.
    dup negate >r  dup 0> IF
        /string dup r> u>= IF  + 0  THEN
    ELSE
        /string dup r> u< IF  + 1+ -1  THEN
    THEN ;
[THEN]

[IFUNDEF] string-suffix?
    : string-suffix? ( addr1 u1 addr2 u2 -- flag )
	\G return true if addr2 u2 is a suffix of addr1 u1
	tuck 2>r over swap - 0 max /string 2r> str= ;
[THEN]

: -skip ( addr u char -- ) >r
    BEGIN  1- dup  0>= WHILE  2dup + c@ r@ <>  UNTIL  THEN  1+ rdrop ;
[IFUNDEF] -scan
    : -scan ( addr u char -- addr u' ) >r
	BEGIN  dup  WHILE  1- 2dup + c@ r@ =  UNTIL  1+  THEN  rdrop ;
[THEN]

[IFUNDEF] basename
: basename ( addr u -- addr' u' )
    2dup '/' -scan nip /string ;
[THEN]

: str0? ( addr u -- flag )
    \ check if string is all zero
    0 scan nip 0= ;

\ set debugging

debug: dummy(

: +-?( ( addr u -- flag )
    2dup +-? IF [: 1 /string type '(' emit ;] $tmp find-name dup
	IF  >does-code ['] dummy( >does-code =  THEN
    ELSE  2drop false  THEN ;

[IFUNDEF] set-debug
    : set-debug ( addr u -- )
	debug-eval $!
	s" db " debug-eval 1 $ins
	s" (" debug-eval $+!
	debug-eval $@ evaluate ;
[THEN]

: ++debug ( -- )
    BEGIN  ?peekarg  WHILE  +-?(  WHILE  ?nextarg drop set-debug  REPEAT  THEN ;

\ logic memory modifiers

: or!   ( x addr -- )    >r r@ @ or   r> ! ;
: xor!  ( x addr -- )    >r r@ @ xor  r> ! ;
: and!  ( x addr -- )    >r r@ @ and  r> ! ;

: xorc! ( x c-addr -- )  >r r@ c@ xor r> c! ;
: andc! ( x c-addr -- )  >r r@ c@ and r> c! ;
: orc!  ( x c-addr -- )  >r r@ c@ or  r> c! ;

: max!@ ( n addr -- )    >r r@ @ max  r> !@ ;
: umax!@ ( n addr -- )   >r r@ @ umax r> !@ ;

\ user stack, automatic init+dispose

: ustack ( "name" -- )
    \G generate user stack, including initialization and free on thread
    \G start and termination
    User  latestxt >r
    :noname  action-of thread-init compile,
    r@ compile, postpone off postpone ;
    is thread-init
    :noname  r> compile, postpone $off  action-of kill-task  compile,
    postpone ;
    is kill-task ;

[IFUNDEF] NOPE
    : NOPE ( c:sys -- )
	\G removes a control structure sys from the stack
	drop 2drop ; immediate restrict
[THEN]

require bits.fs

\ variable length integers, similar to protobuf, but MSB first

: p@+ ( addr -- u64 addr' )  >r 64#0 r@ 10 bounds
    DO  7 64lshift I c@ $7F and n>64 64or
	I c@ $80 and 0= IF  I 1+ UNLOOP rdrop  EXIT  THEN
    LOOP  r> 10 + ;
[IFDEF] 64bit
    : p-size ( u64 -- n ) \ to speed up: binary tree comparison
	\ flag IF  1  ELSE  2  THEN  equals  flag 2 +
	dup    $FFFFFFFFFFFFFF u<= IF
	    dup       $FFFFFFF u<= IF
		dup      $3FFF u<= IF
		    $00000007F u<= 2 +  EXIT  THEN
		$00000001FFFFF u<= 4 +  EXIT  THEN
	    dup   $3FFFFFFFFFF u<= IF
		$00007FFFFFFFF u<= 6 +  EXIT  THEN
	    $00001FFFFFFFFFFFF u<= 8 +  EXIT  THEN
	$000007FFFFFFFFFFFFFFF u<= 10 + ;
    : p!+ ( u64 addr -- addr' )  over p-size + dup >r >r
	dup $7F and r> 1- dup >r c!  7 rshift
	BEGIN  dup  WHILE  dup $7F and $80 or r> 1- dup >r c! 7 rshift  REPEAT
	drop rdrop r> ;
[ELSE]
    : p-size ( x64 -- n ) \ to speed up: binary tree comparison
	\ flag IF  1  ELSE  2  THEN  equals  flag 2 +
	2dup   $FFFFFFFFFFFFFF. du<= IF
	    2dup      $FFFFFFF. du<= IF
		2dup     $3FFF. du<= IF
		    $00000007F. du<= 2 +  EXIT  THEN
		$00000001FFFFF. du<= 4 +  EXIT  THEN
	    2dup  $3FFFFFFFFFF. du<= IF
		$00007FFFFFFFF. du<= 6 +  EXIT  THEN
	    $00001FFFFFFFFFFFF. du<= 8 +  EXIT  THEN
	$000007FFFFFFFFFFFFFFF. du<= 10 + ;
    : p!+ ( u64 addr -- addr' )  >r 2dup p-size r> + dup >r >r
	over $7F and r> 1- dup >r c!  7 64rshift
	BEGIN  2dup or  WHILE  over $7F and $80 or r> 1- dup >r c! 7 64rshift  REPEAT
	2drop rdrop r> ;
[THEN]

: zz>n ( 64zz -- 64n )
    64dup 1 64rshift 64swap 64>n 1 and negate n>64 64xor ;
: n>zz ( 64n -- 64zz )
    64dup 64-0< n>64 64swap 64-2* 64xor ;

: ps!+ ( 64n addr -- addr' )
    >r n>zz r> p!+ ;
: ps@+ ( addr -- 64n addr' )
    p@+ >r zz>n r> ;

\ bit reversing

: bitreverse8 ( u1 -- u2 )
    0 8 0 DO  2* over 1 and + swap 2/ swap  LOOP  nip ;

Create reverse-table $100 0 [DO] [I] bitreverse8 c, [LOOP]

: reverse8 ( c1 -- c2 ) reverse-table + c@ ;
: reverse ( x1 -- x2 )
    0 cell 0 DO  8 lshift over $FF and reverse8 or
       swap 8 rshift swap  LOOP  nip ;
: reverse$16 ( addrsrc addrdst -- ) { dst } dup >r
    count reverse8 r@ $F + c@ reverse8 dst     c! dst $F + c!
    count reverse8 r@ $E + c@ reverse8 dst 1+  c! dst $E + c!
    count reverse8 r@ $D + c@ reverse8 dst 2 + c! dst $D + c!
    count reverse8 r@ $C + c@ reverse8 dst 3 + c! dst $C + c!
    count reverse8 r@ $B + c@ reverse8 dst 4 + c! dst $B + c!
    count reverse8 r@ $A + c@ reverse8 dst 5 + c! dst $A + c!
    count reverse8 r@ $9 + c@ reverse8 dst 6 + c! dst $9 + c!
    c@    reverse8 r> $8 + c@ reverse8 dst 7 + c! dst $8 + c! ;

\ scoping

Variable scope<>
: scope{ ( "vocabulary" -- scope:addr )
    get-current scope<> >stack also ' execute definitions ;
: }scope ( scope:addr -- )
    previous scope<> stack> set-current ;
: scope: ( "vocabulary" -- scope:addr )
    vocabulary get-current scope<> >stack also lastxt execute definitions ;
: cs-scope: ( "vocabulary" -- scope:addr )
    cs-vocabulary get-current scope<> >stack also lastxt execute definitions ;

: class{ ( parent "scope" -- methods vars )
    class cs-scope: ;
: }class ( methods vars -- )
    context @ }scope
    [: body> name>string type ." -class" ;] $tmp nextname
    end-class ;

: struct{ ( "scope" -- vars )
    >in @ >r
    parse-name [: type ." -struct" ;] $tmp nextname
    begin-structure  r> >in !  cs-scope: ;
: }struct ( vars -- )
    }scope end-structure ;

: with ( "vocabulary" -- )
    also ' execute postpone >o ; immediate restrict
: endwith ( -- )
    postpone o> previous ; immediate restrict

Vocabulary net2o

\ file name sanitizer

: printable? ( addr u -- flag )
    true -rot bounds ?DO  I c@ $80 u>= IF
	    I ['] u8@+ catch IF  drop 0 true
	    ELSE  drop dup I - swap I' u>  THEN
	ELSE  1 I c@ $7F bl within  THEN
	IF  2drop false  LEAVE  THEN  +LOOP ;

: ?sane-file ( addr u -- addr u )
    \G check if file name is sane, and if not, fail
    dup 1- $FFF u>= !!filename!!             \ check nullstring+maxpath
    2dup printable? 0= !!filename!!          \ must be printable
    [IFDEF] cygwin                           \ rules for Windows
	2dup '\' scan nip 0<>   !!filename!! \ no backslash allowed
	2dup ':' scan nip 0<>   !!filename!! \ no colon allowed
    [THEN]
    s" /../"     search         !!filename!! \ no embedded .. allowed
    s" /./"      search         !!filename!! \ no embedded . allowed
    s" //"       search         !!filename!! \ no double slash allowed
    2dup s" ../" string-prefix? !!filename!! \ no parent directory allowed
    2dup s" ./"  string-prefix? !!filename!! \ no same directory allowed
    over c@ '/' =               !!filename!! \ no absolute filename allowed
    2dup '/' scan nip IF
	over c@ '~' =           !!filename!! \ no tilde allowed if it's a path
    THEN ;

$20 buffer: filechars
filechars $20 $FF fill
0 filechars l! \ ctrl chars are all illegal
filechars '/' -bit
filechars #del -bit
: no-fat-chars ( addr u -- ) "\\:?*\q<>|%" ;
no-fat-chars bounds [?DO] filechars [I] c@ -bit [LOOP]

\ '%' is allowed, but we use '%' to replace the others

: .## ( n -- ) s>d <# # # #> type ;
: sane-type ( addr u -- )
    [: bounds ?DO
	  I c@ filechars over bit@
	  IF  emit  ELSE  '%' emit .##  THEN
      LOOP ;] $10 base-execute ;

: fn-sanitize ( addr u -- addr' u' )
    ['] sane-type $tmp ;

false Value chat-sanitize?
false Value hash-sanitize?

: chat-sanitize ( addr u -- addr' u' )
    chat-sanitize? IF  fn-sanitize  THEN ;
: hash-sanitize ( addr u -- addr' u' )
    hash-sanitize? IF  fn-sanitize  THEN ;

\ utf8 sanitizer

: utf8-sanitize ( addr u -- )
    bounds ?DO
	I ['] xc@+ catch IF
	    '�' xemit
	    drop  I I' over -
	    ['] x-size catch IF  2drop  1  THEN
	ELSE  xemit I -  THEN
    +LOOP  nothrow ;

\ config stuff

require config.fs

\ net2o specific configurations

[IFUNDEF] no-file#
    2 Constant ENOENT
    #-512 ENOENT - Constant no-file#
[THEN]
[IFUNDEF] file-exist#
    17 Constant EEXIST
    #-512 EEXIST - Constant file-exist#
[THEN]

: init-dir ( addr u mode -- flag ) >r
    \G create a directory with access mode,
    \G return true if the dictionary is new, false if it already existed
    2dup file-status nip no-file# = IF
	r> mkdir-parents throw  true
    ELSE  2drop rdrop  false  THEN ;

\ dirstack

$10 stack: dirstack

: >dir ( -- )
    s" " $make { w^ dir }
    $4000 dir $!len dir $@ get-dir dir $!len drop
    dir @ dirstack >stack ;
: dir> ( -- )
    dirstack $@len 0= ?EXIT
    dirstack stack> { w^ dir }
    dir $@ set-dir throw  dir $free ;
: dir@ ( -- addr u )
    dirstack $[]# 1- dirstack $[]@ ;

scope{ config

Variable timeouts#
Variable passmode#
Variable logsize#
2Variable savedelta&
2Variable patchlimit&
Variable rootdirs$
Variable prio#
Variable host$		\g set host to this name
Variable port#          \g if not zero, use this port
Variable orighost$	\g if the host is orighost$
Variable date#
$Variable objects$
$Variable chats$
$Variable keys$
$Variable .net2o$
$Variable .net2o-config$
$Variable .net2o-cache$
$Variable invite$

}scope

also config

: .net2o/ ( addr u -- addr' u' ) [: .net2o$ $. '/' emit type ;] $tmp ;
: subdir-config ( -- )
    "keys"    .net2o/ keys$ $!
    "chats"   .net2o/ chats$ $!
    "objects" .net2o/ objects$ $! ;
: $set ( xt addr -- ) dup $free $exec ;
: xdg-config ( env u addr -- ) >r
    getenv 2dup d0= IF  rdrop 2drop  EXIT  THEN
    [: type ." /net2o" ;] r> $set ;
: xdg-dir-config ( -- )
    "XDG_DATA_HOME"   .net2o$        xdg-config
    "XDG_CONFIG_HOME" .net2o-config$ xdg-config
    "XDG_CACHE_HOME"  .net2o-cache$  xdg-config ;
: default-dir-config ( -- )
    "~/.local/share/net2o" .net2o$ $!
    "~/.config/net2o"      .net2o-config$ $!
    "~/.cache/net2o"       .net2o-cache$ $!
    xdg-dir-config
    subdir-config ;
default-dir-config

#2 date# !
#20 logsize# !
pad $400 get-dir rootdirs$ $!
"Hello!" invite$ $!
[defined] android 1 and passmode# ! \ default is all entry is masked out
#14 timeouts# !

$1000.0000. patchlimit& 2! \ 256MB patch limit size
#10.000.000.000. savedelta& 2! \ 10 seconds deltat

: .net2o-config/ ( addr u -- addr' u' ) [: .net2o-config$ $. '/' emit type ;] $tmp ;
: .net2o-cache/ ( addr u -- addr' u' ) [: .net2o-cache$ $. '/' emit type ;] $tmp ;
: ~net2o-cache/ ( addr u -- )
    .net2o-cache/ 2dup $1FF init-dir drop set-dir throw ;
: ~net2o-cache/.. ( addr u -- )
    .net2o-cache/ 2dup $1FF init-dir drop dirname set-dir throw ;
: .keys/  ( addr u -- addr' u' ) [: keys$   $. '/' emit type ;] $tmp ;
: objects/.no-fat-file ( -- addr u )
    [: objects$  $. ." /." no-fat-chars type ;] $tmp ;
: chats/.no-fat-file ( -- addr u )
    [: chats$  $. ." /." no-fat-chars type ;] $tmp ;

: ?.net2o ( -- )
    .net2o$ $@ $1FF init-dir drop
    .net2o-config$ $@ $1FF init-dir drop
    .net2o-cache$ $@ $1FF init-dir drop ;
: ?.net2o/keys ( -- flag ) ?.net2o keys$ $@ $1C0 init-dir ;
: ?.net2o/chats ( -- ) ?.net2o chats$ $@ $1FF init-dir drop ;
: ?.net2o/objects ( -- ) ?.net2o objects$ $@ $1FF init-dir drop ;

: ?create-file ( addr u -- flag )
    2dup file-status IF  drop
	r/w create-file  IF  drop false  ELSE  close-file throw  true  THEN
    ELSE  drop 2drop true  THEN ;

: fsane-init ( -- )
    false to hash-sanitize?  false to chat-sanitize?
    ?.net2o/objects objects/.no-fat-file ?create-file
    0= to hash-sanitize?
    ?.net2o/chats   chats/.no-fat-file   ?create-file
    0= to chat-sanitize? ;

$Variable config-file$  "~/.config/net2o/config" config-file$ $!
Variable configured?

:noname defers 'cold
    configured? off
    pad $400 get-dir rootdirs$ $!
; is 'cold
:noname ( -- )
    config:host$ $off
    config:rootdirs$ $off
    defers 'image ; is 'image

: rootdirs>path ( -- )
    config:rootdirs$ $@ bounds ?DO  I c@ ':' = IF 0 I c! THEN LOOP ;

forward default-host

: ?old-config ( addr u wid -- )
    \G check if we have an old config; then keep it.
    "~/.net2o/config" file-status nip no-file# <> IF
	"~/.net2o" 2dup .net2o$ $! .net2o-config$ $!
	subdir-config
	nip nip "~/.net2o/config" rot
	read-config default-host
    ELSE
	?.net2o default-host write-config
    THEN ;

: ?.net2o-config ( -- )  true configured? !@ ?EXIT
    "NET2O_CONF"  getenv ?dup-IF  config-file$ $!  ELSE  drop  THEN
    config-file$ $@ 2dup file-status nip ['] config >body swap
    no-file# = IF  ?old-config  ELSE  read-config default-host  THEN
    rootdirs>path ;

: init-dirs ( -- ) ?.net2o-config fsane-init ;

previous

\ print time

64Variable tick-adjust
: ticks ( -- u64 )  ntime d>64 tick-adjust 64@ 64+ ;

: ticks-u ( -- u )  ticks 64>n ;

1970 1 1 ymd2day Constant unix-day0

: fsplit ( r -- r n )  fdup floor fdup f>s f- ;

: date? ( -- n )  config:date# @ ;
: datehms? ( -- n )  config:date# @ 7 and ;
$8 Constant #today
$10 Constant #splitdate
$20 Constant #splithour
$40 Constant #splitminute
-1 Value last-day
-1 Value last-hour
-1 Value last-minute

: reset-time ( -- )
    -1 to last-day  -1 to last-hour  -1 to last-minute ;
: today? ( day -- flag )
    ticks 64>f 1e-9 f* 86400 fm/ floor f>s = ;

: .ns ( r -- )  1e-9 f*
    fdup 1e-6 f< IF  1e9 f* 10 0 0 f.rdp ." ns"  EXIT  THEN
    fdup 1e-3 f< IF  1e6 f* 10 3 0 f.rdp ." µs"  EXIT  THEN
    fdup 1e   f< IF  1e3 f* 10 6 0 f.rdp ." ms"  EXIT  THEN
    10 6 0 f.rdp 's' emit ;

: >day ( seconds -- fraction day )
    86400 fm/ fsplit ;
: .day ( day -- )
    unix-day0 + day2ymd
    rot 0 .r '-' emit swap .## '-' emit .## 'T' emit ;
: .timeofday ( fraction/day -- )
    24 fm* fsplit
    date? #splithour and IF
	dup last-hour <> IF  ." ==== " dup .## ." Z ====" cr  THEN  to last-hour
    ELSE  .##  THEN
    datehms? 2 < IF  fdrop  ELSE  60 fm* fsplit
    date? #splitminute and IF
	dup last-minute <> IF  ." === :" dup .## ." m ===" cr  THEN  to last-minute
	ELSE  ':' emit .##  THEN
	datehms? 3 < IF  fdrop  ELSE  ':' emit
	    60 fm* datehms? 4 < IF  f>s .##
	    ELSE  fdup 10e f< IF '0' emit 2  ELSE  3  THEN
		datehms? 1+ 7 min 3 and 3 * dup >r + r@ r> f.rdp  THEN
	THEN  THEN  date? #splithour and 0= IF  'Z' emit  THEN ;
: .deg ( degree -- )
    fdup f0< IF ." -" fnegate THEN
    fsplit 0 .r  $B0 ( '°' ) xemit  60 fm*
    fsplit .##   ''' xemit  60 fm*
    fsplit .##   '.' xemit 100 fm*
    f>s .##      '"' xemit ;
: .never ( -- )
    datehms? 1 > IF ." never" ELSE 'n' emit THEN ;
: .forever ( -- )
    datehms? 1 > IF ." forever" ELSE 'f' emit THEN ;

: f.ticks ( rticks -- )
    1e-9 f* >day
    dup today? date? #today and 0= and
    IF
	drop .timeofday
    ELSE
	date? #splitdate and IF
	    dup last-day <> IF
		." ===== " dup .day ."  =====" cr
	    THEN  to last-day
	ELSE  .day  THEN
	datehms? IF .timeofday ELSE fdrop THEN
    THEN ;

: .ticks ( ticks -- )  date? 0= IF  64drop  EXIT  THEN
    64dup 64-0= IF  .never 64drop EXIT  THEN
    64dup -1 n>64 64= IF  .forever 64drop EXIT  THEN
    64>f f.ticks ;

\ insert into sorted string array, discarding n bytes at the end

: $ins[]# ( addr u $array[] rest -- pos )
    \G insert O(log(n)) into pre-sorted array
    \G @var{pos} is the insertion offset or -1 if not inserted
    \G @var{rest} is the rest of the array chopped off for comparison
    { a[] rest } 0 a[] $[]#
    BEGIN  2dup <  WHILE  2dup + 2/ { left right $# }
	    2dup rest - $# a[] $[]@ rest - compare dup 0= IF
		drop $# a[] $[]@ smove \ overwrite in place
		$# EXIT  THEN
	    0< IF  left $#  ELSE  $# 1+ right  THEN
    REPEAT  drop >r
    { | w^ ins$0 } ins$0 cell a[] r@ cells $ins r@ a[] $[]! r> ;
: $del[]# ( addr u $array[] rest -- )
    \G delete O(log(n)) from pre-sorted array
    { a[] rest } 0 a[] $[]#
    BEGIN  2dup <  WHILE  2dup + 2/ { left right $# }
	    2dup rest - $# a[] $[]@ rest - compare dup 0= IF
		drop $# a[] $[] $off
		a[] $# cells cell $del
		2drop EXIT  THEN
	    0< IF  left $#  ELSE  $# 1+ right  THEN
    REPEAT 2drop 2drop ; \ not found

\ insert into sorted string array, discarding n bytes at the start

: $ins[]/ ( addr u $array n -- pos )
    \G insert O(log(n)) into pre-sorted array
    \G @var{pos} is the insertion offset or -1 if not inserted
    { a[] rest } 0 a[] $[]#
    BEGIN  2dup <  WHILE  2dup + 2/ { left right $# }
	    2dup rest safe/string $# a[] $[]@ rest safe/string compare dup 0= IF
		drop $# a[] $[]@ smove \ overwrite in place
		$# EXIT  THEN
	    0< IF  left $#  ELSE  $# 1+ right  THEN
    REPEAT  drop >r
    { | w^ ins$0 } ins$0 cell a[] r@ cells $ins r@ a[] $[]! r> ;
: $del[]/ ( addr u $array offset -- )
    \G delete O(log(n)) from pre-sorted array
    { a[] rest } 0 a[] $[]#
    BEGIN  2dup <  WHILE  2dup + 2/ { left right $# }
	    2dup rest safe/string $# a[] $[]@ rest safe/string compare dup 0= IF
		drop $# a[] $[] $off
		a[] $# cells cell $del
		2drop EXIT  THEN
	    0< IF  left $#  ELSE  $# 1+ right  THEN
    REPEAT 2drop 2drop ; \ not found

\ insert into sorted string array

: $ins[] ( addr u $array -- pos ) 0 $ins[]# ;
    \G insert O(log(n)) into pre-sorted array
    \G @var{pos} is the insertion offset or -1 if not inserted
: $del[] ( addr u $array -- ) 0 $del[]# ;
    \G delete O(log(n)) from pre-sorted array

\ same with signatures; newest signature replaces older

$41 Constant sigonlysize#
$51 Constant sigsize#
$71 Constant sigpksize#
$91 Constant sigpk2size#
$10 Constant datesize#

: startdate@ ( addr u -- date ) + sigsize# - le-64@ ;
: enddate@ ( addr u -- date ) + sigsize# - 64'+ le-64@ ;
: sigonly@ ( addr u -- addr' u' ) + sigonlysize# - [ sigonlysize# 1- ]L ;
: sigdate@ ( addr u -- addr' u' ) + sigsize# - [ sigsize# 1- ]L ;

: $ins[]sig# ( addr u $array n -- pos )
    \G insert O(log(n)) into pre-sorted array if sigdate is newer
    \G @var{pos} is the insertion offset or -1 if not inserted
    { a[] rest } 0 a[] $[]#
    BEGIN  2dup <  WHILE  2dup + 2/ { left right $# }
	    2dup rest - $# a[] $[]@ rest - compare dup 0= IF
		drop
		2dup rest - + le-64@
		$# a[] $[]@ rest - + le-64@ 64u>=
		IF   $# a[] $[]@ smove  $# \ overwrite in place
		ELSE  2drop  -1  THEN EXIT  THEN
	    0< IF  left $#  ELSE  $# 1+ right  THEN
    REPEAT  drop >r
    { | w^ ins$0 } ins$0 cell a[] r@ cells $ins r@ a[] $[]! r> ;

: $ins[]sig ( addr u $array -- pos ) sigsize# $ins[]sig# ;
    \G @var{pos} is the insertion offset or -1 if not inserted
: $del[]sig ( addr u $array -- ) sigsize# $del[]# ;
: $rep[]sig ( addr u $array -- ) >r
    \G replace if newer in one-element array
    r@ $[]# IF
	2dup startdate@ 0 r@ $[]@ startdate@ 64u<
	IF  2drop rdrop  EXIT  THEN
    THEN
    0 r> $[]! ;

\ list sorted by sig date

: $ins[]date ( addr u $array -- pos )
    \G insert O(log(n)) into pre-sorted array
    \G @var{pos} is the insertion offset or -1 if not inserted
    { a[] } 0 a[] $[]#
    BEGIN  2dup u<  WHILE  2dup + 2/ { left right $# }
	    2dup startdate@ $# a[] $[]@ startdate@ 64over 64over 64= IF
		64drop 64drop
		2dup $# a[] $[]@ compare dup 0= IF
		    drop 2drop  $# invert  EXIT  THEN
		0<  ELSE  64u<  THEN
	    IF  left $#  ELSE  $# 1+ right  THEN
    REPEAT  drop >r
    { | w^ ins$0 } ins$0 cell a[] r@ cells $ins r@ a[] $[]!  r> ;
: $search[]date ( ticks $array -- pos )
    \G search O(log(n)) in pre-sorted array
    \G @var{pos} is the first location of the item >= the requested date
    { a[] } 0 a[] $[]#
    BEGIN  2dup u<  WHILE  2dup + 2/ { left right $# }
	    64dup $# a[] $[]@ startdate@ 64over 64over 64= IF
		64drop 64drop
		0 $# 1- -DO
		    64dup I a[] $[]@ startdate@ 64<> ?LEAVE
		    I to $#
		1 -LOOP
		64drop $#  EXIT  THEN
	    64u< IF  left $#  ELSE  $# 1+ right  THEN
    REPEAT  drop >r r@ a[] $[]@ ?dup-IF
	startdate@ 64u> negate r> +
    ELSE  drop 64drop r>  THEN ;

\ filter entries out of a string array

: $[]filter { addr xt -- }
    \G execute @var{xt} for all elements of the string array @var{addr}.
    \G xt is @var{( addr u -- flag )}, getting one string at a time,
    \G if flag is false, delete the corresponding string.
    0 { idx }  BEGIN  idx addr $[]# <  WHILE
	    idx addr $[]@ xt execute IF
		idx 1+ to idx
	    ELSE
		idx addr $[] $off
		addr idx cells cell $del
	    THEN
    REPEAT ;

: $[]map? { addr xt -- }
    \G execute @var{xt} for all elements of the string array @var{addr}.
    \G xt is @var{( addr u -- flag )}, getting one string at a time
    addr $[]# 0 ?DO  I addr $[]@ xt execute ?LEAVE  LOOP ;

\ colors

: <default> default-color attr! ;
: <warn>    warning-color attr! ;
: <info>    info-color    attr! ;
: <err>     error-color   attr! ;
: <success> success-color attr! ;
: <black>   [ black >fg black >bg or ]l attr! ;
: <white>   [ white >fg white >bg or bold or ]l attr! ;

\ Memory words

\ the policy on allocation and freeing is that both freshly allocated
\ and to-be-freed memory is erased.  This makes sure that no unwanted
\ data will be lurking in that memory, waiting to be leaked out

: alloz ( size -- addr )
    dup >r allocate throw dup r> erase ;
: freez ( addr size -- )
    \G erase and then free - for secret stuff
    over swap erase free throw ;
: ?free ( addr size -- ) >r
    dup @ IF  dup @ r@ freez off  ELSE  drop  THEN  rdrop ;

: allo1 ( size -- addr )
    dup >r allocate throw dup r> $FF fill ;
: allocate-bits ( size -- addr )
    dup >r cell+ allo1 dup r> + off ; \ last cell is off

: ?free+guard ( addr u -- )
    over @ IF  over @ swap 2dup erase  free+guard  off
    ELSE  2drop  THEN ;

\ file stuff

: ?fd ( fd addr u -- fd' ) { addr u } dup ?EXIT drop
    ?.net2o
    addr u r/w open-file dup no-file# = IF
	2drop addr u r/w create-file
    THEN  throw ;

: write@pos-file ( addr u 64pos fd -- ) >r
    64>d r@ reposition-file throw
    r@ write-file throw r> flush-file throw ;

: append-file ( addr u fd -- 64pos ) >r
    r@ file-size throw d>64 64dup { 64: pos } r> write@pos-file pos ;

: touch ( addr u -- )
    w/o create-file throw close-file throw ;

\ copy files

: throw?exists ( throwcode -- )  dup no-file# <> and throw ;

$1F to tmps# \ need more temporaries

Variable tmp-file$
: $tmp-file ( addr u -- addr' u' )
    [: type '+' emit getpid 0 .r ;] tmp-file$ $set
    tmp-file$ $@ ;

: >backup ( addr u -- )
    2dup 2dup [: type '~' emit ;] $tmp rename-file >r
    tmp-file$ $@ 2swap rename-file
    r> throw?exists throw?exists ;

: >new ( addr u -- fd )
    $tmp-file r/w create-file throw ;

: >copy ( addr u -- fd )
    2dup >new { fd1 }
    r/o open-file dup no-file# = IF
	2drop
    ELSE
	throw 0 { fd0 w^ cpy }
	#0. fd0 reposition-file throw
	fd0 cpy $slurp fd0 close-file throw
	cpy $@ fd1 write-file throw cpy $off
	fd1 flush-file throw
    THEN  fd1 ;

: save-file ( addr u xt -- )
    \G save file @var{addr u} by making a copy first,
    \G applying xt ( fd -- ) on that copy, and then
    \G moving the existing file to backup ("~" appended to filename)
    \G and the copy ("+" appended to filename) to the original name.
    >r 2dup >copy r> over >r execute r> close-file throw >backup ;

: new-file ( addr u xt -- )
    \G save file @var{addr u} by making an empty first,
    \G applying xt ( fd -- ) on that file, and then
    \G moving the existing file to backup ("~" appended to filename)
    \G and the new ("+" appended to filename) to the original name.
    >r 2dup >new r> over >r execute r> close-file throw >backup ;

\ help display

: search-help ( pattern xt file-addr u -- )
    open-fpath-file throw
    [: >r BEGIN  refill  WHILE
	      source 2over string-prefix? IF  r@ execute  THEN
      REPEAT rdrop 2drop ;] execute-parsing-named-file ;
compsem: sourcefilename postpone sliteral ['] search-help compile, ;

: .cmd ( addr u -- addr u )
    source 2over nip /string type cr ;

\ single quoted string with escapes of single quote for shell invocation

: 'type' ( addr u -- ) ''' emit
    bounds ?DO  I c@ ''' = IF  .\" '\"'\"'"  ELSE  I c@ emit  THEN  LOOP
    ''' emit ;

\ insert and remove single cell items

: del$one ( addr1 addr2 size -- pos )
    \G @var{pos} is the deletion offset
    >r over @ cell+ - tuck r> $del ;
: next$ ( pos string -- addre addrs )
    $@ rot /string bounds ;
: del$cell ( addr stringaddr -- ) { string }
    string $@ bounds ?DO
	dup I @ = IF
	    string I cell del$one
	    unloop string next$ ?DO NOPE 0
	ELSE  cell  THEN
    +LOOP drop ;

\ unique list of cells

Sema resize-sema

: unique$cell? ( x addr -- flag )
    $@ bounds ?DO  dup I @ = IF  drop false unloop  EXIT  THEN
    cell +LOOP  drop true ;

: +unique$ ( x addr -- )
    [: 2dup unique$cell? IF
	  >r { w^ x } x cell r> $+!
      ELSE  2drop  THEN ;] resize-sema c-section ;

\ xchar tool

: *-width ( addr u -- n )
    0 -rot bounds ?DO  I c@ $C0 $80 within -  LOOP ;

e? max-xchar $100 u< [IF]
    : utf8emit ( xchar -- )
	 '?' over $100 u< select emit ;
    : >utf8$ ( addr u -- addr' u' )
	[: bounds ?DO  I c@ u8emit  LOOP ;] $tmp ;
    : $utf8> ( addr u -- addr' u' )
	[: bounds ?DO  I u8@+ utf8emit
	  I - +LOOP ;] $tmp ;
[ELSE]
    ' xemit alias utf8emit
    ' noop alias >utf8$ immediate
    ' noop alias $utf8> immediate
[THEN]

\ accept* derivative

e? max-xchar $100 < [IF] '*' [ELSE] ( '●' ) '•' [THEN] Value pw*

Variable *insflag

: *type ( addr u -- )
    config:passmode# @ dup 0< IF  drop 2drop  EXIT  THEN
    2 = IF  type  EXIT  THEN
    *-width 0 ?DO  pw* xemit  LOOP ;
: *type1 ( addr u -- )  config:passmode# @ 0<= IF  *type  ELSE  type  THEN ;
: *type2 ( addr u -- )  config:passmode# @ 1 <> IF  *type  EXIT  THEN
    dup IF  2dup over + xchar- over - dup >r 2swap r> /string 2swap
    ELSE  0 0 2swap  THEN
    *-width 0 ?DO  pw* xemit  LOOP
    dup IF  type  ELSE  2drop  THEN ;
: *-width0 ( addr u -- w )
    config:passmode# @ dup 0< IF  drop 2drop 0  EXIT  THEN
    0= IF  *-width  ELSE  x-width  THEN ;
: *-width1 ( addr u -- w )
    config:passmode# @ dup 0< IF  drop 2drop 0  EXIT  THEN
    2 = IF  x-width  ELSE  *-width  THEN ;
: *-width2 ( addr u -- w )
    case  config:passmode# @
	2 of  x-width  endof
	1 of
	    dup IF  2dup over + xchar- over - dup >r 2swap r> /string
		x-width >r *-width r> +  ELSE  nip  THEN  endof
	0 of  *-width  endof
	-1 of  2drop 0  endof
	drop swap
    endcase ;
: .*resizeline ( span addr pos -- span addr pos )
    2dup *insflag @ IF  *-width2  ELSE  *-width1  THEN >r
    setstring$ $@ *-width0 >r
    >edit-rest *-width1 r> r> + +
    dup >r edit-linew @ u< IF
	xedit-startpos  edit-linew @ spaces  edit-linew @ edit-curpos !
    THEN
    r> edit-linew ! ;
: .*all ( span addr pos -- span addr pos )
    xedit-startpos  2dup *insflag @ IF  *type2  ELSE  *type  THEN
    setstring$ $@
    dup IF  ['] *type1 setstring-color color-execute  ELSE  2drop  THEN
    >edit-rest *type  edit-linew @ edit-curpos !  ;
: .*rest ( span addr pos -- span addr pos )
    xedit-startpos
    2dup *insflag @ IF  *-width2  ELSE  *-width1  THEN  edit-curpos !
    2dup *insflag @ IF  *type2  ELSE  *type  THEN ;
: *edit-update ( -- )
    .*resizeline .*all .*rest ;
: (*xins)  *insflag on (xins) ;
: *kill-prefix  *insflag off kill-prefix ;

edit-terminal-c uclass edit-out
end-class *edit-terminal-c

*edit-terminal-c ' new static-a with-allocater Constant *edit-terminal

*edit-terminal edit-out !

' (*xins) is insert-char
' *kill-prefix is everychar
' *edit-update  is edit-update

edit-terminal edit-out !

: accept* ( addr u -- u' )
    \G accept-like input, but types * instead of the character
    \G don't save into history
    get-order n>r history >r  edit-out @ >r  *edit-terminal edit-out !
    0 to history  0 set-order
    ['] accept catch
    r> edit-out !  r> to history  nr> set-order
    throw space ;

\ catch loop

: ?int ( throw-code -- throw-code )  dup -28 = IF  bye  THEN ;

: .loop-err ( throw xt -- )
    .name dup . cr DoError cr ;

: catch-loop { xt -- flag }
    BEGIN   xt catch dup -1 = ?EXIT
	?int dup  WHILE  xt .loop-err  REPEAT
    drop false ;

[IFDEF] EAGAIN
    : ?ior-again ( n -- )
	errno EAGAIN <> and ?ior ;
[THEN]

\ !wrapper: generic wrapper to store a value in a variable
\ and restore it after catching the xt

: !wrapper ( val addr xt -- .. ) { addr xt -- .. }
    addr !@ >r xt catch r> addr ! throw ;

\ evaluate in

: evaluate-in ( addr u voc-addr -- )
    get-order n>r >body 1 set-order ['] evaluate catch
    nr> set-order throw ;

\ blocking event, also available in most recent Gforth

[IFUNDEF] event|
    event: :>restart ( task -- ) restart ;
    
    : event| ( task -- )
	\G send an event and block
	dup up@ = IF \ don't block, just eval if we send to ourselves
	    event> ?events
	ELSE
	    up@ elit, :>restart event> stop
	THEN ;
[THEN]
