(* ETH Oberon, Copyright 2001 ETH Zuerich Institut fuer Computersysteme, ETH Zentrum, CH-8092 Zuerich.
Refer to the "General ETH Oberon System Source License" contract available at: http://www.oberon.ethz.ch/ *)

MODULE ethBTrees;  (** portable *) (* ejz,  *)
  IMPORT Files;

(** BTrees is a utility module that manages b-trees with string (64 characters) or longint keys. Each key is linked to
    a longint value (org) which normaly is an offset to where the data for that key is stored. *)

  CONST
    Done* = 0; NotFound* = 1; EntryChanged* = 2; (** res codes *)

    Tag = 2425;
    PageSize = 2*1024;
    BoolSize = 1;
    IntSize = 2;
    LIntSize = 4;

    LInt = 0;
    LIntKeySize = LIntSize;
    LIntPageN = (PageSize-BoolSize-LIntSize-IntSize-LIntSize) DIV (2*(LIntKeySize+2*LIntSize));
    LIntPageSize = LIntSize+BoolSize+IntSize+LIntSize+2*LIntPageN*(LIntKeySize+2*LIntSize);

    Str = 1;
    StrKeySize* = 64; (** The maximum length of a string key. *)
    StrPageN = (PageSize-BoolSize-LIntSize-IntSize-LIntSize) DIV (2*(StrKeySize+2*LIntSize));
    StrPageSize = LIntSize+BoolSize+IntSize+LIntSize+2*StrPageN*(StrKeySize+2*LIntSize);

  TYPE
    Page = POINTER TO PageDesc;
    PageDesc = RECORD
      org: LONGINT; (* pos of this page *)
      p0: LONGINT; (* pos of left page *)
      m: INTEGER; (* number of entries *)
      dirty: BOOLEAN; (* page changed *)
      discard: BOOLEAN; (* TRUE: page is deleted *)
      next: Page (* next page in cache *)
    END;
    Tree* = POINTER TO TreeDesc; (** handle to a b-tree index *)
    TreeDesc = RECORD
      F: Files.File; (* index file *)
      cache: Page; (* list of cached pages *)
      org: LONGINT; (* pos of btree header *)
      root: LONGINT; (* pos of root page *)
      free: LONGINT; (* pos of first free page *)
      class: INTEGER; (* 0: LInt, 1: Str *)
      noCache: INTEGER; (* number of pages in cache *)
      maxCache: INTEGER (* max. number of pages in cache *)
    END;

    Entry = RECORD
      org: LONGINT; (* pos of data for key *)
      p: LONGINT (* pos of right page *)
    END;

    LIntEntry = RECORD (Entry)
      key: LONGINT
    END;
    LIntPage = POINTER TO LIntPageDesc;
    LIntPageDesc = RECORD (PageDesc)
      e: ARRAY 2*LIntPageN OF LIntEntry
    END;
    EnumLIntProc* = PROCEDURE (key, org: LONGINT; VAR cont: BOOLEAN); (** enumerator for longin keys *)

    StrEntry = RECORD (Entry)
      key: ARRAY StrKeySize OF CHAR
    END;
    StrPage = POINTER TO StrPageDesc;
    StrPageDesc = RECORD (PageDesc)
      e: ARRAY 2*StrPageN OF StrEntry
    END;
    EnumStrProc* = PROCEDURE (key: ARRAY OF CHAR; org: LONGINT; VAR cont: BOOLEAN); (** enumerator for string keys *)

  VAR
    MINStrKey*, MAXStrKey*: ARRAY StrKeySize OF CHAR; (** first and last string key *)

(* Allocate space for a new page. *)
  PROCEDURE AllocSpace(T: Tree; size: LONGINT): LONGINT;
    VAR
      R: Files.Rider;
      pos: LONGINT;
  BEGIN
    IF T.free > T.org THEN
      pos := T.free; Files.Set(R, T.F, pos);
      Files.ReadLInt(R, T.free) (* next free *)
    ELSE
      pos := Files.Length(T.F); Files.Set(R, T.F, pos);
      WHILE size > 0 DO
        Files.Write(R, 0X); DEC(size)
      END
    END;
    RETURN pos
  END AllocSpace;

  PROCEDURE ToFree(T: Tree; P: Page);
    VAR R: Files.Rider;
  BEGIN
    Files.Set(R, T.F, P.org); Files.WriteLInt(R, T.free);
    Files.Set(R, T.F, T.org);
    Files.WriteInt(R, Tag); Files.WriteInt(R, T.class);
    Files.WriteInt(R, T.maxCache);
    T.free := P.org; Files.WriteLInt(R, T.free)
  END ToFree;

(* Force write back of a page. *)
  PROCEDURE WriteLIntPage(T: Tree; p: LIntPage);
    VAR
      R: Files.Rider;
      i: LONGINT;
  BEGIN
    ASSERT(p.org <= Files.Length(T.F));
    Files.Set(R, T.F, p.org);
    Files.WriteLInt(R, p.org);
    Files.WriteInt(R, p.m);
    Files.WriteBool(R, p.discard);
    Files.WriteLInt(R, p.p0);
    FOR i := 0 TO p.m-1 DO
      Files.WriteLInt(R, p.e[i].key);
      Files.WriteLInt(R, p.e[i].org);
      Files.WriteLInt(R, p.e[i].p)
    END;
    p.dirty := FALSE
  END WriteLIntPage;

(* Allocate a new (memory) page. *)
  PROCEDURE NewLIntPage(T: Tree): LIntPage;
    VAR
      p0, pm, pp: Page;
      p: LIntPage;
  BEGIN
    NEW(p); INC(T.noCache);
    IF T.noCache > T.maxCache THEN
      pp := NIL; pm := NIL; p0 := T.cache;
      WHILE p0 # NIL DO
        pp := pm; pm := p0; p0 := p0.next
      END;
      IF pm.dirty THEN
        WriteLIntPage(T, pm(LIntPage))
      END;
      IF pp # NIL THEN
        pp.next := pm.next
      ELSE
        T.cache := pm.next
      END;
      T.noCache := T.maxCache
    END;
    p.next := T.cache; T.cache := p;
    p.m := 0; p.p0 := -1; p.org := -1;
    p.dirty := TRUE; p.discard := FALSE;
    RETURN p
  END NewLIntPage;

(* Read page at offset org. *)
  PROCEDURE ReadLIntPage(T: Tree; org: LONGINT; VAR p: LIntPage);
    VAR
      R: Files.Rider;
      p0: Page;
      i: LONGINT;
  BEGIN
    IF org < 0 THEN
      p := NIL; RETURN
    END;
    p0 := T.cache;
    WHILE (p0 # NIL) & (p0.org # org) DO
      p0 := p0.next
    END;
    IF p0 = NIL THEN
      p := NewLIntPage(T);
      Files.Set(R, T.F, org);
      Files.ReadLInt(R, p.org); ASSERT(p.org = org);
      Files.ReadInt(R, p.m);
      Files.ReadBool(R, p.discard); ASSERT(~p.discard);
      Files.ReadLInt(R, p.p0);
      FOR i := 0 TO p.m-1 DO
        Files.ReadLInt(R, p.e[i].key);
        Files.ReadLInt(R, p.e[i].org);
        Files.ReadLInt(R, p.e[i].p)
      END;
      p.dirty := FALSE
    ELSE
      p := p0(LIntPage);
      IF (p.next = NIL) & (p # T.cache) THEN
        p0 := T.cache;
        WHILE p0.next # p DO
          p0 := p0.next
        END;
        p0.next := NIL;
        p.next := T.cache; T.cache := p
      END        
    END
  END ReadLIntPage;

(** Search for key in T. If the key could be found res = Done else res = NotFound. *) 
  PROCEDURE SearchLInt*(T: Tree; key: LONGINT; VAR org: LONGINT; VAR res: INTEGER);
    VAR
      i, L, R: LONGINT;
      a: LIntPage;
  BEGIN
    ASSERT(T.class = LInt);
    ReadLIntPage(T, T.root, a);
    LOOP 
      L := 0; R := a.m;
      WHILE L < R DO
        i := (L+R) DIV 2;
        IF key <= a.e[i].key THEN
          R := i
        ELSE
          L := i+1
        END
      END;
      IF (R < a.m) & (a.e[R].key = key) THEN
        res := Done; org := a.e[R].org;
        RETURN
      END;
      IF R = 0 THEN
        ReadLIntPage(T, a.p0, a)
      ELSE
        ReadLIntPage(T, a.e[R-1].p, a)
      END;
      IF a = NIL THEN
        res := NotFound; org := -1;
        RETURN
      END
    END  
  END SearchLInt;

  PROCEDURE insertLInt(T: Tree; key, org: LONGINT; a: LIntPage; VAR h: BOOLEAN; VAR v: LIntEntry; VAR res: INTEGER);
    VAR
      i, L, R: LONGINT;
      b: LIntPage;
      u: LIntEntry;
  BEGIN
    L := 0; R := a.m;
    WHILE L < R DO
      i := (L+R) DIV 2;
      IF key <= a.e[i].key THEN
        R := i
      ELSE
        L := i+1
      END
    END;
    IF (R < a.m) & (a.e[R].key = key) THEN
      res := EntryChanged;
      a.dirty := TRUE; a.e[R].org := org
    ELSE
      IF R = 0 THEN
        ReadLIntPage(T, a.p0, b)
      ELSE
        ReadLIntPage(T, a.e[R-1].p, b)
      END;
      IF b = NIL THEN
        res := Done;
        u.p := -1; h := TRUE;
        u.key := key; u.org := org
      ELSE
        insertLInt(T, key, org, b, h, u, res)
      END;
      IF h THEN
        ReadLIntPage(T, a.org, a); (* ensure a is still cached *)
        a.dirty := TRUE;
        IF a.m < 2*LIntPageN THEN
          h := FALSE; i := a.m;
          WHILE i > R DO
            DEC(i); a.e[i+1] := a.e[i]
          END;
          a.e[R] := u; INC(a.m)
        ELSE
          b := NewLIntPage(T);
          b.dirty := TRUE; b.org := AllocSpace(T, LIntPageSize);
          IF R < LIntPageN THEN
            i := LIntPageN-1; v := a.e[i];
            WHILE i > R DO
              DEC(i); a.e[i+1] := a.e[i]
            END;
            a.e[R] := u;
            i := 0;
            WHILE i < LIntPageN DO
              b.e[i] := a.e[i+LIntPageN]; INC(i)
            END
          ELSE
            DEC(R, LIntPageN);
            i := 0;
            IF R = 0 THEN
              v := u
            ELSE
              v := a.e[LIntPageN];
              WHILE i < R-1 DO
                b.e[i] := a.e[i+LIntPageN+1]; INC(i)
              END;
              b.e[i] := u; INC(i)
            END;
            WHILE i < LIntPageN DO
              b.e[i] := a.e[i+LIntPageN]; INC(i)
            END
          END;
          a.m := LIntPageN; b.m := LIntPageN;
          b.p0 := v.p; v.p := b.org
        END
      END
    END
  END insertLInt;

(** Insert a new key into T. If a new key was inserted, res = Done else res = EntryChanged. *)
  PROCEDURE InsertLInt*(T: Tree; key, org: LONGINT; VAR res: INTEGER);
    VAR
      u: LIntEntry;
      r, q: LIntPage;
      h: BOOLEAN;
  BEGIN
    ASSERT(T.class = LInt);
    h := FALSE; u.p := -1;
    ReadLIntPage(T, T.root, r);
    insertLInt(T, key, org, r, h, u, res);
    IF h THEN
      ReadLIntPage(T, T.root, q);
      q.dirty := TRUE; q.org := AllocSpace(T, LIntPageSize);
      r := NewLIntPage(T);
      r.m := 1; r.dirty := TRUE;
      r.org := T.root; r.p0 := q.org;
      r.e[0] := u
    END
  END InsertLInt;

  PROCEDURE underflowLInt(T: Tree; c, a: LIntPage; s: LONGINT; VAR h: BOOLEAN);
    VAR
      b: LIntPage;
      i, k: LONGINT;
  BEGIN
    IF s < c.m THEN
      ReadLIntPage(T, c.e[s].p, b); k := (b.m-LIntPageN+1) DIV 2;
      ReadLIntPage(T, a.org, a); (* ensure a is still cached *)
      ReadLIntPage(T, c.org, c); (* ensure c is still cached *)
      a.dirty := TRUE; c.dirty := TRUE;
      a.e[LIntPageN-1] := c.e[s]; a.e[LIntPageN-1].p := b.p0;
      IF k > 0 THEN
        i := 0;
        WHILE i < k-1 DO
          a.e[i+LIntPageN] := b.e[i]; INC(i)
        END;
        c.e[s] := b.e[k-1]; b.p0 := c.e[s].p;
        c.e[s].p := b.org; b.m := b.m-SHORT(k);
        b.dirty := TRUE; i := 0;
        WHILE i < b.m DO
          b.e[i] := b.e[i+k]; INC(i)
        END;
        a.m := LIntPageN-1+SHORT(k); h := FALSE
      ELSE
        i := 0;
        WHILE i < LIntPageN DO
          a.e[i+LIntPageN] := b.e[i]; INC(i)
        END;
        i := s; DEC(c.m);
        WHILE i < c.m DO
          c.e[i] := c.e[i+1]; INC(i)
        END;
        a.m := 2*LIntPageN; h := c.m < LIntPageN
      END
    ELSE
      DEC(s);
      IF s = 0 THEN
        ReadLIntPage(T, c.p0, b)
      ELSE
        ReadLIntPage(T, c.e[s-1].p, b)
      END;
      ReadLIntPage(T, a.org, a); (* ensure a is still cached *)
      ReadLIntPage(T, c.org, c); (* ensure c is still cached *)
      k := (b.m-LIntPageN+1) DIV 2; b.dirty := TRUE;
      IF k > 0 THEN
        a.dirty := TRUE; c.dirty := TRUE;
        i := LIntPageN-1;
        WHILE i > 0 DO
          DEC(i); a.e[i+k] := a.e[i]
        END;
        i := k-1; a.e[i] := c.e[s]; a.e[i].p := a.p0;
        b.m := b.m-SHORT(k);
        WHILE i > 0 DO
          DEC(i); a.e[i] := b.e[i+b.m+1]
        END;
        c.e[s] := b.e[b.m]; a.p0 := c.e[s].p;
        c.e[s].p := a.org; a.m := LIntPageN-1+SHORT(k); h := FALSE
      ELSE
        c.dirty := TRUE;
        c.e[s].p := a.p0; b.e[LIntPageN] := c.e[s]; i := 0;
        WHILE i < LIntPageN-1 DO
          b.e[i+LIntPageN+1] := a.e[i]; INC(i)
        END;
        b.m := 2*LIntPageN; DEC(c.m); h := c.m < LIntPageN
      END
    END
  END underflowLInt;
  
  PROCEDURE deleteLInt(T: Tree; key: LONGINT; a: LIntPage; VAR h: BOOLEAN; VAR res: INTEGER);
    VAR
      i, L, R: LONGINT;
      q: LIntPage;
    PROCEDURE del(p: LIntPage; VAR h: BOOLEAN);
      VAR
        k: LONGINT;
        q: LIntPage;
    BEGIN
      k := p.m-1; ReadLIntPage(T, p.e[k].p, q);
      IF q # NIL THEN
        del(q, h);
        IF h THEN underflowLInt(T, p, q, p.m, h) END
      ELSE
        p.dirty := TRUE; a.dirty := TRUE;
        p.e[k].p := a.e[R].p; a.e[R] := p.e[k];
        DEC(p.m); h := p.m < LIntPageN
      END
    END del;
  BEGIN
    L := 0; R := a.m;
    WHILE L < R DO
      i := (L+R) DIV 2;
      IF key <= a.e[i].key THEN
        R := i
      ELSE
        L := i+1
      END
    END ;
    IF R = 0 THEN
      ReadLIntPage(T, a.p0, q)
    ELSE
      ReadLIntPage(T, a.e[R-1].p, q)
    END;
    IF (R < a.m) & (a.e[R].key = key) THEN
      res := Done;
      IF q = NIL THEN
        a.dirty := TRUE;
        DEC(a.m); h := a.m < LIntPageN; i := R;
        WHILE i < a.m DO
          a.e[i] := a.e[i+1]; INC(i)
        END
      ELSE
        del(q, h);
        IF h THEN underflowLInt(T, a, q, R, h) END
      END
    ELSIF q # NIL THEN
      deleteLInt(T, key, q, h, res);
      IF h THEN underflowLInt(T, a, q, R, h) END
    END
  END deleteLInt;

(** Delete key from T. If key was deleted res = Done else res = NotFound. *)
  PROCEDURE DeleteLInt*(T: Tree; key: LONGINT; VAR res: INTEGER);
    VAR
      p: Page;
      r, r0: LIntPage;
      h: BOOLEAN;
  BEGIN
    ASSERT(T.class = LInt); res := NotFound;
    ReadLIntPage(T, T.root, r);
    deleteLInt(T, key, r, h, res);
    IF (res = Done) & h THEN
      ReadLIntPage(T, T.root, r);
      IF r.m = 0 THEN
        IF r.p0 >= 0 THEN
          p := T.cache;
          WHILE p # NIL DO
            IF p.dirty THEN
              WriteLIntPage(T, p(LIntPage))
            END;
            p := p.next
          END;
          ReadLIntPage(T, r.p0, r0);
          r.org := r0.org; r.dirty := TRUE; r.discard := TRUE; r.next := NIL;
          WriteLIntPage(T, r); ToFree(T, r);
          r0.org := T.root; r0.dirty := TRUE; r0.next := NIL;
          T.cache := r0; T.noCache := 1
        END
      END
    END
  END DeleteLInt;

  PROCEDURE enumerateLInt(T: Tree; p: LIntPage; min, max: LONGINT; enum: EnumLIntProc; VAR cont: BOOLEAN);
    VAR
      key, lkey, i: LONGINT;
      q: LIntPage;
  BEGIN
    IF p # NIL THEN
      lkey := MIN(LONGINT); i := 0;
      WHILE (i < p.m) & (lkey < max) DO
        key := p.e[i].key;
        IF key >= min THEN
          IF key > min THEN
            IF i = 0 THEN
              ReadLIntPage(T, p.p0, q)
            ELSE
              ReadLIntPage(T, p.e[i-1].p, q)
            END;
            enumerateLInt(T, q, min, max, enum, cont)
          END;
          IF cont & (key <= max) THEN
            enum(key, p.e[i].org, cont)
          END
        END;
        lkey := key; INC(i)
      END;
      IF cont & (lkey < max) THEN
        ReadLIntPage(T, p.e[p.m-1].p, q);
        enumerateLInt(T, q, min, max, enum, cont)
      END
    END
  END enumerateLInt;

(** Enumerate all keys in T witch range from min upto max (key >= min) & (key <= max). *)
  PROCEDURE EnumLInt*(T: Tree; min, max: LONGINT; enum: EnumLIntProc);
    VAR
      r: LIntPage;
      cont: BOOLEAN;
  BEGIN
    ASSERT(T.class = LInt);
    ReadLIntPage(T, T.root, r);
    IF r.m > 0 THEN
      cont := TRUE;
      enumerateLInt(T, r, min, max, enum, cont)
    END
  END EnumLInt;

  PROCEDURE minLIntKey(T: Tree; p: LIntPage; VAR key: LONGINT);
  BEGIN
    IF p # NIL THEN
      key := p.e[0].key;
      ReadLIntPage(T, p.p0, p);
      minLIntKey(T, p, key)
    END
  END minLIntKey;

(** Searches the smallest key used in T. *)
  PROCEDURE MinLIntKey*(T: Tree; VAR key: LONGINT; VAR res: INTEGER);
    VAR r: LIntPage;
  BEGIN
    ASSERT(T.class = LInt);
    ReadLIntPage(T, T.root, r);
    IF r.m > 0 THEN
      minLIntKey(T, r, key); res := Done
    ELSE
      key := MAX(LONGINT); res := NotFound
    END
  END MinLIntKey;

  PROCEDURE maxLIntKey(T: Tree; p: LIntPage; VAR key: LONGINT);
  BEGIN
    IF (p # NIL) & (p.m > 0) THEN
      key := p.e[p.m-1].key;
      ReadLIntPage(T, p.e[p.m-1].p, p);
      maxLIntKey(T, p, key)
    END
  END maxLIntKey;

(** Searches the biggest key used in T. *)
  PROCEDURE MaxLIntKey*(T: Tree; VAR key: LONGINT; VAR res: INTEGER);
    VAR r: LIntPage;
  BEGIN
    ASSERT(T.class = LInt);
    ReadLIntPage(T, T.root, r);
    IF r.m > 0 THEN
      maxLIntKey(T, r, key); res := Done
    ELSE
      key := MIN(LONGINT); res := NotFound
    END  
  END MaxLIntKey;

(** Create a new b-tree with longint keys. The tree is written to F starting at org.
    cache gives the minumum number of keys which should fit into the page cache. *)
  PROCEDURE NewLInt*(F: Files.File; org: LONGINT; cache: INTEGER): Tree;
    VAR
      T: Tree;
      R: Files.Rider;
  BEGIN
    NEW(T);
    T.maxCache := (cache+2*LIntPageN-1) DIV (2*LIntPageN);
    IF T.maxCache < 4 THEN
      T.maxCache := 4
    END;
    T.F := F; T.org := org;
    Files.Set(R, F, org);
    Files.WriteInt(R, Tag); Files.WriteInt(R, LInt);
    Files.WriteInt(R, T.maxCache);
    T.free := -1; Files.WriteLInt(R, T.free);
    T.root := AllocSpace(T, LIntPageSize);
    T.class := LInt; T.noCache := 0;
    T.cache := NewLIntPage(T);
    T.cache.dirty := TRUE;
    T.cache.org := T.root;
    RETURN T
  END NewLInt;

(* Force write back of a page. *)
  PROCEDURE WriteStrPage(T: Tree; p: StrPage);
    VAR
      R: Files.Rider;
      i: LONGINT;
  BEGIN
    ASSERT(p.org <= Files.Length(T.F));
    Files.Set(R, T.F, p.org);
    Files.WriteLInt(R, p.org);
    Files.WriteInt(R, p.m);
    Files.WriteBool(R, p.discard);
    Files.WriteLInt(R, p.p0);
    FOR i := 0 TO p.m-1 DO
      Files.WriteBytes(R, p.e[i].key, StrKeySize);
      Files.WriteLInt(R, p.e[i].org);
      Files.WriteLInt(R, p.e[i].p)
    END;
    p.dirty := FALSE
  END WriteStrPage;

(* Allocate a new (memory) page. *)
  PROCEDURE NewStrPage(T: Tree): StrPage;
    VAR
      p0, pm, pp: Page;
      p: StrPage;
  BEGIN
    NEW(p); INC(T.noCache);
    IF T.noCache > T.maxCache THEN
      pp := NIL; pm := NIL; p0 := T.cache;
      WHILE p0 # NIL DO
        pp := pm; pm := p0; p0 := p0.next
      END;
      IF pm.dirty THEN
        WriteStrPage(T, pm(StrPage))
      END;
      IF pp # NIL THEN
        pp.next := pm.next
      ELSE
        T.cache := pm.next
      END;
      T.noCache := T.maxCache
    END;
    p.next := T.cache; T.cache := p;
    p.m := 0; p.p0 := -1; p.org := -1;
    p.dirty := TRUE; p.discard := FALSE;
    RETURN p
  END NewStrPage;

(* Read page at offset org. *)
  PROCEDURE ReadStrPage(T: Tree; org: LONGINT; VAR p: StrPage);
    VAR
      R: Files.Rider;
      p0: Page;
      i: LONGINT;
  BEGIN
    IF org < 0 THEN
      p := NIL; RETURN
    END;
    p0 := T.cache;
    WHILE (p0 # NIL) & (p0.org # org) DO
      p0 := p0.next
    END;
    IF p0 = NIL THEN
      p := NewStrPage(T);
      Files.Set(R, T.F, org);
      Files.ReadLInt(R, p.org); ASSERT(p.org = org);
      Files.ReadInt(R, p.m);
      Files.ReadBool(R, p.discard); ASSERT(~p.discard);
      Files.ReadLInt(R, p.p0);
      FOR i := 0 TO p.m-1 DO
        Files.ReadBytes(R, p.e[i].key, StrKeySize);
        Files.ReadLInt(R, p.e[i].org);
        Files.ReadLInt(R, p.e[i].p)
      END;
      p.dirty := FALSE
    ELSE
      p := p0(StrPage);
      IF (p.next = NIL) & (p # T.cache) THEN
        p0 := T.cache;
        WHILE p0.next # p DO
          p0 := p0.next
        END;
        p0.next := NIL;
        p.next := T.cache; T.cache := p
      END
    END
  END ReadStrPage;

(** Search for key in T. If the key could be found res = Done else res = NotFound. *)
  PROCEDURE SearchStr*(T: Tree; key: ARRAY OF CHAR; VAR org: LONGINT; VAR res: INTEGER);
    VAR
      i, L, R: LONGINT;
      a: StrPage;
      sKey: ARRAY StrKeySize OF CHAR;
  BEGIN
    ASSERT(T.class = Str); COPY(key, sKey);
    ReadStrPage(T, T.root, a);
    LOOP 
      L := 0; R := a.m;
      WHILE L < R DO
        i := (L+R) DIV 2;
        IF sKey <= a.e[i].key THEN
          R := i
        ELSE
          L := i+1
        END
      END;
      IF (R < a.m) & (a.e[R].key = sKey) THEN
        res := Done; org := a.e[R].org;
        RETURN
      END;
      IF R = 0 THEN
        ReadStrPage(T, a.p0, a)
      ELSE
        ReadStrPage(T, a.e[R-1].p, a)
      END;
      IF a = NIL THEN
        res := NotFound; org := -1;
        RETURN
      END
    END
  END SearchStr;

  PROCEDURE insertStr(T: Tree; VAR key: ARRAY OF CHAR; org: LONGINT; a: StrPage; VAR h: BOOLEAN; VAR v: StrEntry; VAR res: INTEGER);
    VAR
      i, L, R: LONGINT;
      b: StrPage;
      u: StrEntry;
  BEGIN
    L := 0; R := a.m;
    WHILE L < R DO
      i := (L+R) DIV 2;
      IF key <= a.e[i].key THEN
        R := i
      ELSE
        L := i+1
      END
    END;
    IF (R < a.m) & (a.e[R].key = key) THEN
      res := EntryChanged;
      a.dirty := TRUE; a.e[R].org := org
    ELSE
      IF R = 0 THEN
        ReadStrPage(T, a.p0, b)
      ELSE
        ReadStrPage(T, a.e[R-1].p, b)
      END;
      IF b = NIL THEN
        res := Done;
        u.p := -1; h := TRUE;
        COPY(key, u.key); u.org := org
      ELSE
        insertStr(T, key, org, b, h, u, res)
      END;
      IF h THEN
        ReadStrPage(T, a.org, a); (* ensure a is still cached *)
        a.dirty := TRUE;
        IF a.m < 2*StrPageN THEN
          h := FALSE; i := a.m;
          WHILE i > R DO
            DEC(i); a.e[i+1] := a.e[i]
          END;
          a.e[R] := u; INC(a.m)
        ELSE
          b := NewStrPage(T);
          b.dirty := TRUE; b.org := AllocSpace(T, StrPageSize);
          IF R < StrPageN THEN
            i := StrPageN-1; v := a.e[i];
            WHILE i > R DO
              DEC(i); a.e[i+1] := a.e[i]
            END;
            a.e[R] := u;
            i := 0;
            WHILE i < StrPageN DO
              b.e[i] := a.e[i+StrPageN]; INC(i)
            END
          ELSE
            DEC(R, StrPageN);
            i := 0;
            IF R = 0 THEN
              v := u
            ELSE
              v := a.e[StrPageN];
              WHILE i < R-1 DO
                b.e[i] := a.e[i+StrPageN+1]; INC(i)
              END;
              b.e[i] := u; INC(i)
            END;
            WHILE i < StrPageN DO
              b.e[i] := a.e[i+StrPageN]; INC(i)
            END
          END;
          a.m := StrPageN; b.m := StrPageN;
          b.p0 := v.p; v.p := b.org
        END
      END
    END
  END insertStr;

(** Insert a new key into T. If a new key was inserted, res = Done else res = EntryChanged. *)
  PROCEDURE InsertStr*(T: Tree; key: ARRAY OF CHAR; org: LONGINT; VAR res: INTEGER);
    VAR
      u: StrEntry;
      r, q: StrPage;
      h: BOOLEAN;
      sKey: ARRAY StrKeySize OF CHAR;
  BEGIN
    ASSERT(T.class = Str); COPY(key, sKey);
    h := FALSE; u.p := -1;
    ReadStrPage(T, T.root, r);
    insertStr(T, sKey, org, r, h, u, res);
    IF h THEN
      ReadStrPage(T, T.root, q);
      q.dirty := TRUE; q.org := AllocSpace(T, StrPageSize);
      r := NewStrPage(T);
      r.m := 1; r.dirty := TRUE;
      r.org := T.root; r.p0 := q.org;
      r.e[0] := u
    END
  END InsertStr;

  PROCEDURE underflowStr(T: Tree; c, a: StrPage; s: LONGINT; VAR h: BOOLEAN);
    VAR
      b: StrPage;
      i, k: LONGINT;
  BEGIN
    IF s < c.m THEN
      ReadStrPage(T, c.e[s].p, b); k := (b.m-StrPageN+1) DIV 2;
      ReadStrPage(T, a.org, a); (* ensure a is still cached *)
      ReadStrPage(T, c.org, c); (* ensure c is still cached *)
      a.dirty := TRUE; c.dirty := TRUE;
      a.e[StrPageN-1] := c.e[s]; a.e[StrPageN-1].p := b.p0;
      IF k > 0 THEN
        i := 0;
        WHILE i < k-1 DO
          a.e[i+StrPageN] := b.e[i]; INC(i)
        END;
        c.e[s] := b.e[k-1]; b.p0 := c.e[s].p;
        c.e[s].p := b.org; b.m := b.m-SHORT(k);
        b.dirty := TRUE; i := 0;
        WHILE i < b.m DO
          b.e[i] := b.e[i+k]; INC(i)
        END;
        a.m := StrPageN-1+SHORT(k); h := FALSE
      ELSE
        i := 0;
        WHILE i < StrPageN DO
          a.e[i+StrPageN] := b.e[i]; INC(i)
        END;
        i := s; DEC(c.m);
        WHILE i < c.m DO
          c.e[i] := c.e[i+1]; INC(i)
        END;
        a.m := 2*StrPageN; h := c.m < StrPageN
      END
    ELSE
      DEC(s);
      IF s = 0 THEN
        ReadStrPage(T, c.p0, b)
      ELSE
        ReadStrPage(T, c.e[s-1].p, b)
      END;
      ReadStrPage(T, a.org, a); (* ensure a is still cached *)
      ReadStrPage(T, c.org, c); (* ensure c is still cached *)
      k := (b.m-StrPageN+1) DIV 2; b.dirty := TRUE;
      IF k > 0 THEN
        a.dirty := TRUE; c.dirty := TRUE;
        i := StrPageN-1;
        WHILE i > 0 DO
          DEC(i); a.e[i+k] := a.e[i]
        END;
        i := k-1; a.e[i] := c.e[s]; a.e[i].p := a.p0;
        b.m := b.m-SHORT(k);
        WHILE i > 0 DO
          DEC(i); a.e[i] := b.e[i+b.m+1]
        END;
        c.e[s] := b.e[b.m]; a.p0 := c.e[s].p;
        c.e[s].p := a.org; a.m := StrPageN-1+SHORT(k); h := FALSE
      ELSE
        c.dirty := TRUE;
        c.e[s].p := a.p0; b.e[StrPageN] := c.e[s]; i := 0;
        WHILE i < StrPageN-1 DO
          b.e[i+StrPageN+1] := a.e[i]; INC(i)
        END;
        b.m := 2*StrPageN; DEC(c.m); h := c.m < StrPageN
      END
    END
  END underflowStr;
  
  PROCEDURE deleteStr(T: Tree; VAR key: ARRAY OF CHAR; a: StrPage; VAR h: BOOLEAN; VAR res: INTEGER);
    VAR
      i, L, R: LONGINT;
      q: StrPage;
    PROCEDURE del(p: StrPage; VAR h: BOOLEAN);
      VAR
        k: LONGINT;
        q: StrPage;
    BEGIN
      k := p.m-1; ReadStrPage(T, p.e[k].p, q);
      IF q # NIL THEN
        del(q, h);
        IF h THEN underflowStr(T, p, q, p.m, h) END
      ELSE
        p.dirty := TRUE; a.dirty := TRUE;
        p.e[k].p := a.e[R].p; a.e[R] := p.e[k];
        DEC(p.m); h := p.m < StrPageN
      END
    END del;
  BEGIN
    L := 0; R := a.m;
    WHILE L < R DO
      i := (L+R) DIV 2;
      IF key <= a.e[i].key THEN
        R := i
      ELSE
        L := i+1
      END
    END ;
    IF R = 0 THEN
      ReadStrPage(T, a.p0, q)
    ELSE
      ReadStrPage(T, a.e[R-1].p, q)
    END;
    IF (R < a.m) & (a.e[R].key = key) THEN
      res := Done;
      IF q = NIL THEN
        a.dirty := TRUE;
        DEC(a.m); h := a.m < StrPageN; i := R;
        WHILE i < a.m DO
          a.e[i] := a.e[i+1]; INC(i)
        END
      ELSE
        del(q, h);
        IF h THEN underflowStr(T, a, q, R, h) END
      END
    ELSIF q # NIL THEN
      deleteStr(T, key, q, h, res);
      IF h THEN underflowStr(T, a, q, R, h) END
    END
  END deleteStr;

(** Delete key from T. If key was deleted res = Done else res = NotFound. *)
  PROCEDURE DeleteStr*(T: Tree; key: ARRAY OF CHAR; VAR res: INTEGER);
    VAR
      p: Page;
      r, r0: StrPage;
      sKey: ARRAY StrKeySize OF CHAR;
      h: BOOLEAN;
  BEGIN
    ASSERT(T.class = Str); COPY(key, sKey); res := NotFound;
    ReadStrPage(T, T.root, r);
    deleteStr(T, sKey, r, h, res);
    IF (res = Done) & h THEN
      ReadStrPage(T, T.root, r);
      IF r.m = 0 THEN
        IF r.p0 >= 0 THEN
          p := T.cache;
          WHILE p # NIL DO
            IF p.dirty THEN
              WriteStrPage(T, p(StrPage))
            END;
            p := p.next
          END;
          ReadStrPage(T, r.p0, r0);
          r.org := r0.org; r.dirty := TRUE; r.discard := TRUE; r.next := NIL;
          WriteStrPage(T, r); ToFree(T, r);
          r0.org := T.root; r0.dirty := TRUE; r0.next := NIL;
          T.cache := r0; T.noCache := 1
        END
      END
    END
  END DeleteStr;

  PROCEDURE enumerateStr(T: Tree; p: StrPage; VAR min, max: ARRAY OF CHAR; enum: EnumStrProc; VAR cont: BOOLEAN);
    VAR
      key, lkey: ARRAY StrKeySize OF CHAR;
      q: StrPage;
      i: LONGINT;
  BEGIN
    IF p # NIL THEN
      COPY(MINStrKey, lkey); i := 0;
      WHILE (i < p.m) & (lkey < max) DO
        COPY(p.e[i].key, key);
        IF key >= min THEN
          IF key > min THEN
            IF i = 0 THEN
              ReadStrPage(T, p.p0, q)
            ELSE
              ReadStrPage(T, p.e[i-1].p, q)
            END;
            enumerateStr(T, q, min, max, enum, cont)
          END;
          IF cont & (key <= max) THEN
            enum(key, p.e[i].org, cont)
          END
        END;
        COPY(key, lkey); INC(i)
      END;
      IF cont & (lkey < max) THEN
        ReadStrPage(T, p.e[p.m-1].p, q);
        enumerateStr(T, q, min, max, enum, cont)
      END
    END
  END enumerateStr;

(** Enumerate all keys in T witch range from min upto max (key >= min) & (key <= max). *)
  PROCEDURE EnumStr*(T: Tree; min, max: ARRAY OF CHAR; enum: EnumStrProc);
    VAR
      r: StrPage;
      cont: BOOLEAN;
  BEGIN
    ASSERT(T.class = Str);
    ReadStrPage(T, T.root, r);
    IF r.m > 0 THEN
      cont := TRUE;
      enumerateStr(T, r, min, max, enum, cont)
    END
  END EnumStr;

  PROCEDURE minStrKey(T: Tree; p: StrPage; VAR key: ARRAY OF CHAR);
  BEGIN
    IF p # NIL THEN
      COPY(p.e[0].key, key);
      ReadStrPage(T, p.p0, p);
      minStrKey(T, p, key)
    END
  END minStrKey;

(** Searches the smallest key used in T. *)
  PROCEDURE MinStrKey*(T: Tree; VAR key: ARRAY OF CHAR; VAR res: INTEGER);
    VAR r: StrPage;
  BEGIN
    ASSERT(T.class = Str);
    ReadStrPage(T, T.root, r);
    IF r.m > 0 THEN
      minStrKey(T, r, key); res := Done
    ELSE
      res := NotFound
    END
  END MinStrKey;

  PROCEDURE maxStrKey(T: Tree; p: StrPage; VAR key: ARRAY OF CHAR);
  BEGIN
    IF (p # NIL) & (p.m > 0) THEN
      COPY(p.e[p.m-1].key, key);
      ReadStrPage(T, p.e[p.m-1].p, p);
      maxStrKey(T, p, key)
    END
  END maxStrKey;

(** Searches the biggest key used in T. *)
  PROCEDURE MaxStrKey*(T: Tree; VAR key: ARRAY OF CHAR; VAR res: INTEGER);
    VAR r: StrPage;
  BEGIN
    ASSERT(T.class = Str);
    ReadStrPage(T, T.root, r);
    IF r.m > 0 THEN
      maxStrKey(T, r, key); res := Done
    ELSE
      res := NotFound
    END  
  END MaxStrKey;

(** Create a new b-tree with string keys. The tree is written to F starting at org.
    cache gives the minumum number of keys which should fit into the page cache. *)
  PROCEDURE NewStr*(F: Files.File; org: LONGINT; cache: INTEGER): Tree;
    VAR
      T: Tree;
      R: Files.Rider;
  BEGIN
    NEW(T);
    T.maxCache := (cache+2*StrPageN-1) DIV (2*StrPageN);
    IF T.maxCache < 4 THEN
      T.maxCache := 4
    END;
    T.F := F; T.org := org;
    Files.Set(R, F, org);
    Files.WriteInt(R, Tag); Files.WriteInt(R, Str);
    Files.WriteInt(R, T.maxCache);
    T.free := -1; Files.WriteLInt(R, T.free);
    T.root := AllocSpace(T, StrPageSize);
    T.class := Str; T.noCache := 0;
    T.cache := NewStrPage(T);
    T.cache.dirty := TRUE;
    T.cache.org := T.root;
    RETURN T
  END NewStr;

(** Reopen the b-tree written to F starting at org. *)
  PROCEDURE Old*(F: Files.File; org: LONGINT): Tree;
    VAR
      T: Tree;
      R: Files.Rider;
      tag: INTEGER;
  BEGIN
    NEW(T); T.F := F; T.org := org;
    Files.Set(R, F, org);
    Files.ReadInt(R, tag); ASSERT(tag = Tag); Files.ReadInt(R, T.class);
    Files.ReadInt(R, T.maxCache); Files.ReadLInt(R, T.free);
    IF T.maxCache < 4 THEN T.maxCache := 4 END;
    T.root := Files.Pos(R); T.noCache := 0; T.cache := NIL;    
    RETURN T
  END Old;

(** Flush the page-cache of T to disk. *)
  PROCEDURE Flush*(T: Tree);
    VAR
      R: Files.Rider;
      p: Page;
  BEGIN
    Files.Set(R, T.F, T.org);
    Files.WriteInt(R, Tag); Files.WriteInt(R, T.class);
    Files.WriteInt(R, T.maxCache); Files.WriteLInt(R, T.free);
    p := T.cache;
    WHILE p # NIL DO
      IF p.dirty THEN
        CASE T.class OF
          LInt: WriteLIntPage(T, p(LIntPage))
          |Str: WriteStrPage(T, p(StrPage))
        END
      END;
      p := p.next
    END;
    Files.Close(T.F)
  END Flush;

(** Return the file used by T. *)
  PROCEDURE Base*(T: Tree): Files.File;
  BEGIN
    RETURN T.F
  END Base;

  PROCEDURE Init();
    VAR i: LONGINT;
  BEGIN
    FOR i := 0 TO StrKeySize-2 DO
      MINStrKey[i] := 0X;
      MAXStrKey[i] := 0FFX
    END;
    MINStrKey[StrKeySize-1] := 0X;
    MAXStrKey[StrKeySize-1] := 0X
  END Init;

BEGIN
  Init()
END ethBTrees.
