! "Player notepad" extension by David Fisher (version 1.0, November 23rd 2007)
!
! Copyright David Fisher, 2007 (davidfisher@australiaonline.net.au).
!
! This extension may be freely used, modified, archived and distributed
! as long as this copyright notice remains intact.
!
! An out-of-game notepad which the player can use to write and edit notes
! during a game.
!
! Note that this extension overrides the BeforeParsing and UnknownVerb
! entry points; if you override these routines in your game, delete the
! versions below and call nbBeforeParsing() or nbUnknownVerb() at the
! end of your routines.
!
! Type "notepad" for usage.
!

[ BeforeParsing;
  nbBeforeParsing();
];

[ UnknownVerb;
  return nbUnknownVerb();
];

[ NotepadInfoSub;
  print "An inbuilt notepad may be used to take notes during the game.
    To add a new note, start a line with
    ~=~. For example:^^
    @@32 =can't find the chicken
    ^^Use ~=~ on a line by itself (or ~notes~) to read your notes so far.
    ^^Each note is assigned a number, starting from 1. You can read an
    individual note by typing its number, or change it by following
    the number with ~=~ and the new text:^^
    @@32 23=there is no chicken
    ^^Lastly, you can erase a note by saying ~erase~ or ~del/delete~
    followed by its number (the rest of the notes are then renumbered).^^
    To read these notes again, type ~notepad~.^";
];

Verb meta 'notepad' 'notebook'
     *           -> NotepadInfo;

Verb meta 'notes'
     *           -> ShowNotes;   ! same thing as "=" on a line by itself

Verb meta 'erase' 'delete' 'del'
     *           -> DeleteNothing
     * number    -> Delete
     * 'notes'/'notepad'/'notebook' -> DeleteAll
     * noun      -> DeleteNoun;

! (used internally; player can't type this verb)
Verb meta '.='
     *           -> Notepad
     * topic     -> Notepad;

! size of notepad (number of characters + 1 byte per note)
Constant NOTE_SIZE 4000;

! a warning is given if there is only this much space left in the notepad
Constant WARN_REMAIN 100;

! the notes are all stored in a single array
Array notes -> NOTE_SIZE + 1;

Global note_end = 0;   ! position of next note (length of notes array)
Global note_num = 0;   ! number of notes

Global notepad_cmd = false;

[ nbUnknownVerb;
  if (notepad_cmd) { return '.='; }
  rfalse;
];

[ nbBeforeParsing    i j;
  notepad_cmd = false;
  j = bufferEndPos();

  ! check for '=' or number 

  for (i = WORDSIZE : i <= j : i++)
  {
     if (buffer->i ~= ' ') { break; }
  }

  if (i > j) { return; }

  if (buffer->i == '=' || isDigit(buffer->i))
  {
     ! will be handled by the UnknownVerb entry point
     notepad_cmd = true;

     ! temporarily substitute dots and commas
     for (i++ : i <= j : i++)
     {
        ! will be undone in NotepadSub()
        if (buffer->i == '.') { buffer->i = 1; }
        else if (buffer->i == ',') { buffer->i = 2; }
     }
  }
  Tokenise__(buffer, parse);
];

[ NotepadSub   i j n;
  j = bufferEndPos();
  for (i = WORDSIZE : i <= j : i++)
  {
     if (buffer->i ~= ' ') { break; }
  }

  for (n = i : n <= j : n++)
  {
     ! undo changes done in BeforeParsing()
     if (buffer->n == 1) { buffer->n = '.'; }
     else if (buffer->n == 2) { buffer->n = ','; }
  }

  if (buffer->i == '=')
  {
     for (i++ : i <= j : i++)
     {
        if (buffer->i ~= ' ') { break; }
     }

     if (i > j) { nbList(); }
     else { nbAppend(i); }
  }
  else
  if (isDigit(buffer->i)) { nbNumber(i); }
  else
  {
     print "Sorry, that command was not understood.^";
  }
];

[ ShowNotesSub;
  nbList();
];

[ toUpper ch;
  
  if (ch >= 'a' && ch <= 'z') { return ch - 'a' + 'A'; }
  return ch;
];

! lists all notes if n_only is 0

[ nbList n_only   n index upper ch start;

  if (note_end == 0)
  {
     print "The notepad is empty.^";
     return;
  }

  index = 1;
  upper = true;
  start = true;

  for (n = 0 : n < note_end : n++)
  {
     if (start)
     {
	if (n_only == 0 or index) { print index, ". "; }
        upper = true;
	start = false;
     }
     if (notes->n == 0)
     {
	if (n_only == 0 or index)
	{
	   if (n > 0)
	   {
	      if (~~(notes->(n-1) == '.' or '!' or '?' ||
	             (n > 1 && notes->(n-1) == '"' &&
		      notes->(n-2) == '.' or '!' or '?')))
	      { print "."; }
	   }

           new_line;
	}
        index++;
	start = true;
     }
     else
     {
	if (n_only == 0 or index)
	{
	   ch = notes->n;

	   ! ZCode makes everything lower case; start the sentence with
	   ! an upper case letter
	   if (upper && ch ~= '.' or '!' or '?' or ' ' or '"' or ''')
	   { ch = toUpper(ch); upper = false; }

           print (char) ch;
	   if (ch == '!' or '?') { upper = true; }
	   else
	   if (ch == '.')
	   {
	      if (n > 0 && notes->(n-1) == '.') { upper = false; }
	      else { upper = true; }
	   }
	}
     }
  }
];

[ notepadFullMsg;
  print "[The notepad is full. Try deleting unneeded entries.]^";
];

[ nbAppend pos   j len percent;
  
  j = bufferEndPos();
  while (j >= 0 && j == ' ') { j--; }
  if (j < 0)
  {
     ! should never happen
     print "Sorry, there was a problem understanding the command.^";
     return;
  }

  len = j - pos + 1;

  if (note_end + len + 1 > NOTE_SIZE)
  {
     notepadFullMsg();
     return;
  }

  for ( : pos <= j : pos++)
  {
     notes->note_end = buffer->pos;
     note_end++;
  }

  notes->note_end = 0;
  note_end++;

  note_num++;
  print "Noted.  [", note_num, "]";

  if (NOTE_SIZE - note_end <= WARN_REMAIN)
  {
     ! avoid overflowing 16 bit numbers
     percent = 100 -
        ((NOTE_SIZE - note_end + (NOTE_SIZE / 100) - 1) * 100 / NOTE_SIZE);
     print " (", percent, "% full)";
  }
  new_line;
];

[ isDigit ch;
  return (ch >= '0' && ch <= '9');
];

[ nbNumber pos   i j val;
  j = bufferEndPos(); 

  for (i = pos : i <= j && isDigit(buffer->i) : i++)
  {
     if (val >= 10000)
     {
        print "Number too large.^";
        return;
     }
     val = val * 10 + (buffer->i - '0');
  }

  ! skip spaces
  for ( : i <= j && buffer->i == ' ' : i++)
  { }

  if (i > j)
  {
     if (checkVal(val)) { nbList(val); }
  }
  else
  if (buffer->i == '=')
  {
     if (checkVal(val,
        "To add a new entry, just say ~=~ followed by the text"))
     { replaceEntry(val, i + 1); }
  }
  else
  {
     print "Extra characters after command (did you mean to say ~", val,
        " = ...~?)^";
  }
];

[ findNBEntry index   n upto;
  upto = 1;
  for (n = 0 : n < note_end && upto < index : n++)
  {
     if (notes->n == 0) { upto++; }
  }

  if (n >= note_end)
  {
     print "[Internal error #1 in notepad.]^";
     return -1;
  }

  return n;
];

! find position of next zero byte in note array
! Returns -1 if goes past end of array

[ findNextZeroPos pos;
  for ( : pos < note_end && notes->pos ~= 0 : pos++)
  { }

  if (pos >= note_end)
  {
     print "[Internal error #2 in notepad.]^";
     return -1;
  }
  return pos;
];

[ bufferEndPos;
  return WORDSIZE + buffer->1 - 1;
];

[ replaceEntry index pos   n n2 i j endpos len old_len;
  j = bufferEndPos();

  ! skip spaces
  for ( : pos <= j && buffer->pos == ' ' : pos++)
  { }

  if (pos > j)
  {
     deleteEntry(index);
     return;
  }

  for (endpos = j : endpos >= pos && buffer->endpos == ' ' : endpos--)
  { }

  if (endpos < pos)
  {
     ! should be impossible, but just in case
     deleteEntry(index);
     return;
  }

  n = findNBEntry(index);
  if (n == -1) { return; }

  n2 = findNextZeroPos(n);
  if (n2 == -1) { return; }

  len = endpos - pos + 1;
  old_len = n2 - n;

  if (note_end + len - old_len > NOTE_SIZE)
  {
     notepadFullMsg();
     return;
  }

  if (len > old_len) { moveNBup(n + old_len, len - old_len); }
  else if (len < old_len) { moveNBdown(n + old_len, old_len - len); }

  for (i = 0 : i < len : i++)
  {
     notes->(n+i) = buffer->(pos+i);
  }

  print "Replaced.^";
];

[ moveNBup from_pos amount   n;
  for (n = note_end - 1 : n >= from_pos : n--)
  { notes->(n+amount) = notes->n; }

  note_end = note_end + amount;
];

[ moveNBdown from_pos amount   n;
  for (n = from_pos : n < note_end : n++)
  { notes->(n-amount) = notes->n; }

  note_end = note_end - amount;
];

[ checkVal val empty_msg_str;
  if (val <= 0)
  {
     print "The notepad is numbered from 1.^";
     rfalse;
  }

  if (note_num == 0)
  {
     print "The notepad is empty.^";
     if (empty_msg_str ~= nothing)
     {
	new_line;
        print (string) empty_msg_str;
        print ".^";
     }
     rfalse;
  }

  if (val > note_num)
  {
     print "There ";
     if (note_num == 1) { print "is only one entry"; }
     else { print "are only ", (number) note_num, " entries"; }
     print " in the notepad.^";
     rfalse;
  }
  rtrue;
];

[ DeleteNothingSub;
  if (note_num == 0)
  {
     print "The notepad is empty.^";
  }
  else
  if (note_num == 1)
  {
     print "(notepad entry 1)^^";
     deleteEntry(1);
  }
  else
  {
     print "You'll have to say which notepad entry to delete (1-",
        note_num, ").^";
  }
];

[ DeleteAllSub;
  print "You can only erase one entry from the notepad at a time.^";
];

[ DeleteNounSub;
  print "You can't delete ", (the) noun, "!^";
];

[ DeleteSub    index;
  index = noun;
  if (~~checkVal(index)) { return; }
  deleteEntry(index);
];

[ deleteEntry index   n n2;
  n = findNBEntry(index);
  if (n == -1) { return; }

  n2 = findNextZeroPos(n);
  if (n2 == -1) { return; }

  moveNBdown(n2 + 1, n2 - n + 1);
  print "Deleted.^";
  note_num--;
];

Object the_notepad "notepad"
has scenery
with name 'notepad' 'notebook' 'notes',
found_in [; rtrue; ],
before [;
   meta = true;
   ! (don't assume that Examine = Read or that there is a ##Read action)
   if (action == ##Examine || verb_word == 'read')
   {
      ShowNotesSub();
      if (self.gave_tip == false)
      {
	 self.gave_tip = true;
         print "^[Tip: you can just say ~=~ or ~notes~ to read your notes.]^";
      }
      rtrue;
   }
   else
   {
      print "The notepad is not a physical object in the game, it is
         just a way to write notes as you play. Type ~notepad~ for more
         information.^";
   }
   rtrue;
],
gave_tip false;

