MODULE StdStamps;
(**

   project   = "BlackBox"
   organization   = "www.oberon.ch"
   contributors   = "Oberon microsystems"
   version   = "System/Rsrc/About"
   copyright   = "System/Rsrc/About"
   license   = "Docu/BB-License"
   changes   = ""
   issues   = ""

**)

(*

   StdStamps are used to keep track of document changes, in particular program texts.
   StdStamps carry a sequence number and a fingerprint of the document with them.
   Each time the document (and therefore its fingerprint) is changed and stored,
   the sequence number is incremented. (When determining the fingerprint of the
   document, whitespace is ignored, except in string literals.)
   
   Each StdStamp also keeps track of the history of most recent changes.
   For the last maxHistoryEntries sequence numbers, the date and time,
   and an optional one-line comment is stored. To avoid too many entries in the history
   while working on a module, the most recent history entry is overwritten upon the
   generation of a new sequence number if the current date is the same as the date in
   the history entry.
*)

   IMPORT

      SYSTEM, (* SYSTEM.ROT only, for fingerprint calculation *)
      Strings, Dates, StdCmds,
      Ports, Models, Stores, Containers, Properties, Views, Controllers, Fonts,
      TextModels, TextSetters, TextMappers, TextViews, TextRulers;
   CONST

      setCommentKey = "#Std:Set Comment";
      maxHistoryEntries = 25;
      minVersion = 0; origStampVersion = 0; thisVersion = 2;
      
   TYPE
      History = ARRAY maxHistoryEntries OF RECORD
         fprint, snr: INTEGER;   (* fingerprint, sequence number *)
         date: INTEGER;         (* days since 1/1/1 *)
         time: INTEGER;         (* min + 64 * hour *)
         comment: POINTER TO ARRAY OF CHAR;   (* nil if no comment*)
      END;
         
      StdView = POINTER TO RECORD (Views.View)
         (*--snr: LONGINT;*)         
         nentries: INTEGER;   (* number of entries in history *)
         history: History;         (* newest entry in history[0] *)
         cache: ARRAY 64 OF CHAR;
      END;
      SetCmtOp = POINTER TO RECORD (Stores.Operation)

         stamp: StdView;
         oldcomment: POINTER TO ARRAY OF CHAR;
      END;
   VAR

      comment*: RECORD
         s*: ARRAY 64 OF CHAR;
      END;
   PROCEDURE (op: SetCmtOp) Do;


      VAR temp: POINTER TO ARRAY OF CHAR;
   BEGIN
      temp := op.stamp.history[0].comment;
      op.stamp.history[0].comment := op.oldcomment;
      op.oldcomment := temp;
   END Do;
   PROCEDURE Format (v: StdView);

      VAR s: ARRAY 64 OF CHAR; d: Dates.Date; t: INTEGER;
   BEGIN
      t := v.history[0].time;
      Dates.DayToDate(v.history[0].date, d);
      Dates.DateToString(d, Dates.plainAbbreviated, s); v.cache := s$;
      Strings.IntToStringForm(v.history[0].snr, Strings.decimal, 4, "0", FALSE, s);
      v.cache := v.cache + " (" + s + ")"
   END Format;
   PROCEDURE FontContext (v: StdView): Fonts.Font;


      VAR c: Models.Context;
   BEGIN
      c := v.context;
      IF (c # NIL) & (c IS TextModels.Context) THEN
         RETURN c(TextModels.Context).Attr().font;
      ELSE
         RETURN Fonts.dir.Default()
      END;
   END FontContext;
   PROCEDURE CalcFP (t: TextModels.Model): INTEGER;

      CONST sglQuote = "'"; dblQuote = '"';
      VAR fp: INTEGER;rd: TextModels.Reader; ch, quoteChar: CHAR;
   BEGIN
      quoteChar := 0X; fp := 0;
      rd := t.NewReader(NIL); rd.ReadChar(ch);
      WHILE ~rd.eot DO
         IF ch = quoteChar THEN quoteChar := 0X;
         ELSIF (quoteChar = 0X) & ((ch = dblQuote) OR (ch = sglQuote)) THEN quoteChar := ch;
         END;
         IF (quoteChar = 0X) & (21X <= ch) & (ch # 8BX) & (ch # 8FX) & (ch # 0A0X) (* not in string literal *)
            OR (quoteChar # 0X) & (20X <= ch) (* within string literal *)
         THEN
            fp := SYSTEM.ROT(fp, 1) + 13 * ORD(ch);
         END;
         rd.ReadChar(ch);
      END;
      RETURN fp;
   END CalcFP;
   PROCEDURE Update (v: StdView; forcenew: BOOLEAN);

      VAR fp: INTEGER; i: INTEGER; ndays: INTEGER; d: Dates.Date; t: Dates.Time;
   BEGIN
      IF (v.context # NIL) & (v.context IS TextModels.Context) THEN
         fp := CalcFP(v.context(TextModels.Context).ThisModel());
         IF (fp # v.history[0].fprint) OR forcenew THEN
            Dates.GetDate(d); Dates.GetTime(t);
            ndays := Dates.Day(d);
            IF (ndays # v.history[0].date) OR forcenew THEN
               (* move down entries in history list *)
               i := maxHistoryEntries-1;
               WHILE i > 0 DO
                  v.history[i] := v.history[i-1];
                  DEC(i);
               END;
               v.history[0].comment := NIL;
            END;
            IF v.nentries < maxHistoryEntries THEN INC(v.nentries) END;
            INC(v.history[0].snr);
            v.history[0].fprint := fp;
            v.history[0].date := ndays;
            v.history[0].time := t.minute + t.hour*64;
            Format(v);
            Views.Update(v, Views.keepFrames);
         END;
      END;
   END Update;
   PROCEDURE (v: StdView) Externalize (VAR wr: Stores.Writer);

      VAR i, len: INTEGER;
   BEGIN
      Update(v, FALSE);
      v.Externalize^(wr);
      wr.WriteVersion(thisVersion);
      (*--wr.WriteLInt(v.snr);*)
      wr.WriteXInt(v.nentries);
      FOR i := 0 TO v.nentries-1 DO
         wr.WriteInt(v.history[i].fprint);
         wr.WriteInt(v.history[i].snr);
         wr.WriteInt(v.history[i].date);
         wr.WriteXInt(v.history[i].time);
         IF v.history[i].comment # NIL THEN
            len := LEN(v.history[i].comment$);
            wr.WriteXInt(len);
            wr.WriteXString(v.history[i].comment^);
         ELSE wr.WriteXInt(0);
         END
      END;
   END Externalize;
   PROCEDURE (v: StdView) Internalize (VAR rd: Stores.Reader);

      VAR version: INTEGER; format: BYTE; i, len: INTEGER;
         d: Dates.Date; t: Dates.Time;
   BEGIN
      v.Internalize^(rd);
      IF ~rd.cancelled THEN
         rd.ReadVersion(minVersion, thisVersion, version);
         IF ~rd.cancelled THEN
            IF version = origStampVersion THEN (* deal with old StdStamp format *)
               (* would like to calculate fingerprint, but hosting model not available at this time *)
               v.history[0].fprint := 0;
               v.history[0].snr := 1; v.nentries := 1;
               rd.ReadXInt(d.year); rd.ReadXInt(d.month); rd.ReadXInt(d.day);
               rd.ReadXInt(t.hour); rd.ReadXInt(t.minute); rd.ReadXInt(t.second);
               rd.ReadByte(format); (* format not used anymore *)
               v.history[0].date := Dates.Day(d);
               v.history[0].time := t.minute + t.hour*64;
            ELSE
               IF version = 1 THEN rd.ReadInt(v.history[0].snr) END; (* red text: to be removed soon *)
               rd.ReadXInt(v.nentries);
               FOR i := 0 TO v.nentries-1 DO
                  rd.ReadInt(v.history[i].fprint);
                  IF version > 1 THEN rd.ReadInt(v.history[i].snr)
                  ELSIF (* (version = 1) & *) i > 0 THEN v.history[i].snr := v.history[i-1].snr - 1;
                  END; (* red text: to be removed soon *)
                  rd.ReadInt(v.history[i].date);
                  rd.ReadXInt(v.history[i].time);
                  rd.ReadXInt(len);
                  IF len > 0 THEN
                     NEW(v.history[i].comment, len + 1);
                     rd.ReadXString(v.history[i].comment^);
                  ELSE v.history[i].comment := NIL;
                  END
               END;
            END;
            Format(v);
         END
      END
   END Internalize;
   PROCEDURE (v: StdView) CopyFromSimpleView (source: Views.View);

      VAR i: INTEGER;
   BEGIN
      (* v.CopyFrom^(source); *)
      WITH source: StdView DO
         (*--v.snr := source.snr;*)
         v.nentries := source.nentries;
         v.history := source.history;
         v.cache := source.cache;
         FOR i := 0 TO v.nentries - 1 DO
            IF source.history[i].comment # NIL THEN
               NEW(v.history[i].comment, LEN(source.history[i].comment$) + 1);
               v.history[i].comment^ := source.history[i].comment^$;
            END
         END
      END
   END CopyFromSimpleView;
   PROCEDURE (v: StdView) Restore (f: Views.Frame; l, t, r, b: INTEGER);

      VAR a: TextModels.Attributes; color: Ports.Color; c: Models.Context; font: Fonts.Font;
         asc, dsc, fw: INTEGER;
   BEGIN
      c := v.context;
      IF (c # NIL) & (c IS TextModels.Context) THEN
         a := v.context(TextModels.Context).Attr();
         font := a.font;
         color := a.color;
      ELSE font := Fonts.dir.Default(); color := Ports.black;
      END;
      font.GetBounds(asc, dsc, fw);
      f.DrawLine(f.l, asc + f.dot, f.r, asc + f.dot, 1, Ports.grey25 );
      f.DrawString(0, asc, color, v.cache, font);
   END Restore;
   PROCEDURE SizePref (v: StdView; VAR p: Properties.SizePref);

      VAR font: Fonts.Font; asc, dsc, w: INTEGER; d: Dates.Date; s: ARRAY 64 OF CHAR;
   BEGIN
      font := FontContext(v);
      font.GetBounds(asc, dsc, w);
      d.day := 28; d.month := 1; d.year := 2222; p.w := 0;
      WHILE d.month <= 12 DO
         Dates.DateToString(d, Dates.plainAbbreviated, s);
         s := s + " (0000)";
         w := font.StringWidth(s);
         IF w > p.w THEN p.w := w END;
         INC(d.month)
      END;
      p.h := asc + dsc;
   END SizePref;
   PROCEDURE (v: StdView) HandlePropMsg (VAR msg: Properties.Message);

      VAR font: Fonts.Font; asc, w: INTEGER;
   BEGIN
      WITH msg: Properties.Preference DO
         WITH msg: Properties.SizePref DO
            SizePref(v, msg)
         | msg: Properties.ResizePref DO
            msg.fixed := TRUE
         | msg: Properties.FocusPref DO
            msg.hotFocus := TRUE
         | msg: TextSetters.Pref DO
            font := FontContext(v);
            font.GetBounds(asc, msg.dsc, w);
         ELSE
         END
      ELSE
      END
   END HandlePropMsg;
   PROCEDURE NewRuler (): TextRulers.Ruler;

      CONST mm = Ports.mm;
      VAR r: TextRulers.Ruler;
   BEGIN
      r := TextRulers.dir.New(NIL);
      TextRulers.SetRight(r, 140 * mm);
      TextRulers.AddTab(r, 15 * mm); TextRulers.AddTab(r, 35 * mm); TextRulers.AddTab(r, 75 * mm);
      RETURN r
   END NewRuler;
   PROCEDURE ShowHistory (v: StdView);

      VAR text: TextModels.Model; f: TextMappers.Formatter;
         i: INTEGER; d: Dates.Date; s: ARRAY 64 OF CHAR;
         tv: TextViews.View; attr: TextModels.Attributes;
   BEGIN
      text := TextModels.dir.New();
      f.ConnectTo(text);
      attr := f.rider.attr;
      f.rider.SetAttr(TextModels.NewStyle(attr, {Fonts.italic}));
      f.WriteString("seq nr."); f.WriteTab;
      f.WriteString("fingerprint"); f.WriteTab;
      f.WriteString("date and time"); f.WriteTab;
      f.WriteString("comment"); f.WriteLn;
      f.rider.SetAttr(attr); f.WriteLn;
      (*--n := v.snr;*)
      FOR i := 0 TO v.nentries-1 DO
         f.WriteIntForm(v.history[i].snr, 10, 4, "0", FALSE);
         (*--DEC(n);*)
         f.WriteTab;
         f.WriteIntForm(v.history[i].fprint, TextMappers.hexadecimal, 8, "0", FALSE);
         f.WriteTab;
         Dates.DayToDate(v.history[i].date, d);
         Dates.DateToString(d, Dates.plainAbbreviated, s);
         f.WriteString(s);
         f.WriteString("");
         f.WriteIntForm(v.history[i].time DIV 64, 10, 2, "0", FALSE);
         f.WriteString(":");
         f.WriteIntForm(v.history[i].time MOD 64, 10, 2, "0", FALSE);
         IF v.history[i].comment # NIL THEN
            f.WriteTab;
            f.WriteString( v.history[i].comment^);
         END;
         f.WriteLn;
      END;
      tv := TextViews.dir.New(text);
      tv.SetDefaults(NewRuler(), TextViews.dir.defAttr);
      tv.ThisController().SetOpts({Containers.noFocus, Containers.noCaret});
      Views.OpenAux(tv, "History");
   END ShowHistory;
   PROCEDURE Track (v: StdView; f: Views.Frame; x, y: INTEGER; buttons: SET);

      VAR c: Models.Context; w, h: INTEGER; isDown, in, in0: BOOLEAN; m: SET;
   BEGIN
      c := v.context; c.GetSize(w, h); in0 := FALSE; in := TRUE;
      REPEAT
         IF in # in0 THEN
            f.MarkRect(0, 0, w, h, Ports.fill, Ports.invert, Ports.show); in0 := in
         END;
         f.Input(x, y, m, isDown);
         in := (0 <= x) & (x < w) & (0 <= y) & (y < h)
      UNTIL ~isDown;
      IF in0 THEN
         f.MarkRect(0, 0, w, h, Ports.fill, Ports.invert, Ports.hide);
         IF Controllers.modify IN m THEN
            IF v.history[0].comment # NIL THEN comment.s := v.history[0].comment^$;
            ELSE comment.s := "";
            END;
            StdCmds.OpenToolDialog("Std/Rsrc/Stamps", "Comment");
         ELSE ShowHistory(v);
         END
      END
   END Track;
   PROCEDURE (v: StdView) HandleCtrlMsg (

            f: Views.Frame; VAR msg: Controllers.Message; VAR focus: Views.View);
   BEGIN
      WITH msg: Controllers.TrackMsg DO
         Track(v, f, msg.x, msg.y, msg.modifiers)
      | msg: Controllers.PollCursorMsg DO
         msg.cursor := Ports.refCursor
      ELSE
      END
   END HandleCtrlMsg;
   (* ------------ programming interface: ---------------------- *)


   PROCEDURE GetFirstInText* (t: TextModels.Model): Views.View;

      VAR r: TextModels.Reader; v: Views.View;
   BEGIN
      IF t # NIL THEN
         r := t.NewReader(NIL);
         REPEAT r.ReadView(v) UNTIL (v = NIL) OR (v IS StdView);
         RETURN v;
      ELSE RETURN NIL;
      END;
   END GetFirstInText;
   PROCEDURE IsStamp* (v: Views.View): BOOLEAN;

   BEGIN
      RETURN v IS StdView;
   END IsStamp;
   PROCEDURE GetInfo* (v: Views.View; VAR snr, historylen: INTEGER);

   BEGIN
      ASSERT(v IS StdView, 20);
      WITH v: StdView DO
         snr := v.history[0].snr; historylen := v.nentries;
      END
   END GetInfo;
   PROCEDURE GetData* (v: Views.View; entryno: INTEGER;

            VAR fprint: INTEGER; VAR date: Dates.Date; VAR time: Dates.Time);
   BEGIN
      ASSERT(v IS StdView, 20);
      WITH v: StdView DO
         IF entryno <= v.nentries THEN
            fprint := v.history[entryno].fprint;
            Dates.DayToDate(v.history[entryno].date, date);
            time.minute := v.history[entryno].time MOD 64;
            time.minute := v.history[entryno].time DIV 64;
            time.second := 0;
         END
      END
   END GetData;
   (** Insert new history entry with comment in v. *)

   PROCEDURE Stamp* (v: Views.View; comment: ARRAY OF CHAR);
   BEGIN
      ASSERT(v IS StdView, 20);
      WITH v: StdView DO
         Update(v, TRUE);
         NEW(v.history[0].comment, LEN(comment$) + 1);
         v.history[0].comment^ := comment$;
      END
   END Stamp;
   PROCEDURE New* (): Views.View;

      VAR v: StdView; d: Dates.Date; t: Dates.Time;
   BEGIN
      NEW(v); v.history[0].snr := 0; v.nentries := 0;
      v.history[0].fprint := 0;
      Dates.GetDate(d); Dates.GetTime(t);
      v.history[0].date := Dates.Day(d);
      v.history[0].time := t.minute + t.hour*64;
      Format(v);
      RETURN v;
   END New;
   PROCEDURE SetComment*;

      VAR v: Views.View; op: SetCmtOp;
   BEGIN
      v := GetFirstInText(TextViews.FocusText());
      IF v # NIL THEN
         WITH v: StdView DO
            NEW(op); op.stamp := v;
            NEW(op.oldcomment, LEN(comment.s$) + 1);
            op.oldcomment^ := comment.s$;
            Views.Do(v, setCommentKey, op);
         END
      END
   END SetComment;
   PROCEDURE Deposit*;

   BEGIN
      Views.Deposit(New())
   END Deposit;
END StdStamps.