/*
** menus.t allows you to add simple menus to your HTML TADS game.
** For an example of their use, see the Arrival or Common Ground
** source (in if-archive/games/tads/source). Feel free to use in
** your own TADS 2 games.
**
** Copyright (c) 1998, 1999 Stephen Granade. All rights reserved.
**
** Special thanks to Mike Roberts, whose help with this module was
** invaluable.
*/

#pragma C+

// The keys used to navigate the menus are held in the keyList array of the
// menuItem. Each navigation option is bound to two keys. The #defines below
// are for accessing the keys associated with the navigation options (quit,
// previous menu, up a selection, down a selection, select a menu item).
// Since each navigation option is bound to two keys, the #defines consist
// of odd numbers.
#define M_QUIT      1
#define M_PREV      3
#define M_UP        5
#define M_DOWN      7
#define M_SEL       9

// The menuItem is the topmost object in a menu tree.
class menuItem: object
   title = ""                  // The name of the menu
   myContents = []             // The submenuItems, topicItems, and longTopicItems
   bgcolor = 'statusbg'        // Background color of the menu
   fgcolor = 'statustext'      // Foreground color of the menu
   indent = '10'               // # of spaces to indent the menu's contents
   allowHTML = true            // Assume the game is running in HTML mode
                               // (i.e. "\H+" has been printed)
   keyList = ['q' '' 'p' '[left]' 'u' '[up]' 'd' '[down]' 'ENTER' '\n'
       '[right]' ' ']

   // topBanner creates the HTML TADS banner for the menu. The banner contains
   // the title of the menu on the left and the navigation keys on the right
   topBanner = {
       "<banner id=MenuTitle><body bgcolor=\"<<self.fgcolor>>\"
           text=\"<<self.bgcolor>>\">";
       "<<self.title>>
           <tab align=\"right\">\^<<self.keyList[M_QUIT]>>=quit \^<<
           self.keyList[M_PREV]>>=previous menu<br>
           <tab align=\"right\">\^<<self.keyList[M_UP]>>=up \^<<
           self.keyList[M_DOWN]>>=down \^<<self.keyList[M_SEL]>>=select";
       "</banner>";
   }

   // Call menu.display when you're ready to show the menu.
   display = {
       local i, selection, len = length(self.myContents), key = '', loc;

       // If we are running on an HTML TADS interpreter and the game uses
       // HTML formatting, use HTML tags to make the menus prettier
       if (systemInfo(__SYSINFO_SYSINFO) == true &&
           systemInfo(__SYSINFO_HTML) == 1 && self.allowHTML) {
           self.displayHTML;
           return;
       }

       // For standard TADS, print the title, then the menu options
       // (numbered), then ask the player to make a selection.
       do {
           "\b\(<<self.title>>\)\b";

           for (i = 1; i <= len; i++) {
               if (i < 10) "\ ";
               "<<i>>.\ <<(self.myContents[i]).title>>\n";
           }
           "\bSelect a topic number, or press &rsquo;<<
               self.keyList[M_QUIT]>>&lsquo; to quit.\ ";

           do {                                // The input loop
               key = lower(inputkey());        // Get the key
               loc = find(self.keyList, key);  // Get the key's pos in keyList
               selection = cvtnum(key);        // Turn key into a #, if possible
               if (loc && (loc % 2 == 0))      // If loc is even, make it odd
                   loc--;
           } while (selection == 0 && loc != M_QUIT);

           "<<key>>\n";                        // Print the selection
           // If selection is a number, then the player selected that menu
           // option. Call that submenu or topic's display routine. If the
           // routine returns nil, the player selected QUIT, so we should
           // quit as well.
           if (selection != 0) {
               if (!((self.myContents[selection]).display(self)))
                   loc = M_QUIT;
           }
       } while (loc != M_QUIT && loc != M_PREV);   // Loop until player quits
   }

   displayHTML = {
       local i, selection = 1, len = length(self.myContents), key = '', loc,
           events;

       // Erase the status line and print the topmost menu banner
       self.removeStatusLine;
       self.topBanner;

       // For HTML TADS, set up a separate menu item banner. In it,
       // print the menu's contents. Next to the current selection, print
       // a greater-than sign. Each of the menu items is an HREF, so the
       // player can select it using the mouse
       do {
           "<banner border id=MenuBody><body bgcolor=\"<<self.bgcolor>>\"
               text=\"<<self.fgcolor>>\">";
           "<table><tr><td width=\"<<self.indent>>\"> </td><td>";
           for (i = 1; i <= len; i++) {
               // To get the alignment right, we have to print '>' on each
               // and every line. However, we print it in the background
               // color to make it invisible everywhere but in front of the
               // current selection.
               if (selection != i)
                   "<font color=\"<<self.bgcolor>>\">&gt;</font>";
               else "&gt;";
               // Make each selection a plain (i.e. unhilighted) HREF
               "<a plain href=\"<<i>>\">";
               (self.myContents[i]).title;
               "</a><br>";
           }
           "</td></tr></table>";
           "</banner>";

           do {                            // The input loop. For HTML TADS,
               events = inputevent();      // we look at events rather than
                                           // just keystrokes
               if (events[1] == INPUT_EVENT_HREF) {    // For HREFs, set the
                   selection = cvtnum(events[2]);      // selection # to the
                   loc = M_SEL;                        // HREF's value
               }
               else {
                   key = lower(events[2]); // Otherwise, assume the event is
                                           // a keystroke and get the key
                   loc = find(self.keyList, key);  // Find key's pos in keyList
                   if (loc && (loc % 2 == 0))  // If loc is even, make it odd
                       loc--;
               }
               // First off, handle arrow keys
               if (loc == M_UP && selection != 1)
                   selection--;
               else if (loc == M_DOWN && selection != len)
                   selection++;
           } while (loc == nil);   // Loop until the user presses a valid key

           if (loc >= M_SEL) {     // The player made a selection, so show it
               if (!((self.myContents[selection]).displayHTML(self)))
                   loc = M_QUIT;
           }
       } while (loc != M_QUIT && loc != M_PREV);

       // Clean up after ourselves
       "<banner remove id=MenuTitle><banner remove id=MenuBody>";
   }

   removeStatusLine = {
       "<banner remove id=StatusLine>";
   }
;

// submenuItem is exactly like menuItem, except that it refers to the top-
// level menu item to get the keyList
class submenuItem: object
   title = ""
   myContents = []
   bgcolor = 'statusbg'
   fgcolor = 'statustext'
   indent = '10'

   display(topMenu) = {
       local i, selection, len = length(self.myContents), key = '', loc;

       do {
           "\b\(<<self.title>>\)\b";
           for (i = 1; i <= len; i++) {
               if (i < 10) "\ ";
               "<<i>>.\ <<(self.myContents[i]).title>>\n";
           }
           "\bSelect a topic number, or press &rsquo;<<
               topMenu.keyList[M_QUIT]>>&lsquo; to quit,
               &rsquo;<<topMenu.keyList[M_PREV]>>&lsquo; to go to the
               previous menu.\ ";

           do {
               key = lower(inputkey());
               loc = find(topMenu.keyList, key);
               selection = cvtnum(key);
               if (loc && (loc % 2 == 0))
                   loc--;
           } while (selection == 0 && loc != M_QUIT && loc != M_PREV);
           "<<key>>\n";
           if (selection != 0) {
               if (!((self.myContents[selection]).display(topMenu)))
                   return nil;
           }
       } while (loc != M_QUIT && loc != M_PREV);

       return (loc == M_PREV);
   }

   displayHTML(topMenu) = {
       local i, selection = 1, len = length(self.myContents), key = '', loc,
           events;

       while (true) {
           do {
               "<banner border id=MenuBody><body bgcolor=\"<<self.bgcolor>>\"
                   text=\"<<self.fgcolor>>\">";
               "<table><tr><td width=\"<<self.indent>>\"> </td><td>";
               for (i = 1; i <= len; i++) {
                   if (selection != i)
                       "<font color=\"<<self.bgcolor>>\">&gt;</font>";
                   else "&gt;";
                   (self.myContents[i]).title;
                   "<br>";
               }
               "</td></tr></table>";
               "</banner>";

               do {
                   events = inputevent();
                   if (events[1] == INPUT_EVENT_HREF) {
                       selection = cvtnum(events[2]);
                       loc = M_SEL;
                   }
                   else {
                       key = lower(events[2]);
                       loc = find(topMenu.keyList, key);
                       if (loc && (loc % 2 == 0))
                           loc--;
                   }
                   if (loc == M_UP && selection != 1)
                       selection--;
                   else if (loc == M_DOWN && selection != len)
                       selection++;
               } while (loc == nil);
           } while (loc == M_UP || loc == M_DOWN);

           if (loc >= M_SEL) {
               if (!((self.myContents[selection]).displayHTML(topMenu)))
                   return nil;
           }
           else return (loc == M_PREV);
       }
   }
;

// topicItem displays a series of entries successively. It's intended to be
// used for displaying a group of hints. Unlike [sub]menuItem, myContents
// contains a list of strings to be displayed
class topicItem: object
   title = ""              // The name of this topic
   myContents = []         // A list of strings to be displayed
   bgcolor = 'statusbg'    // The background color
   fgcolor = 'statustext'  // The foreground color
   indent = '30'           // How far to indent all of the strings
   lastDisplayed = 1       // The last string displayed
   chunkSize = 6           // How many strings to display at once, max
                           // (only valid under HTML TADS)
   goodbye = '[The End]'   // The ending phrase

   display(topMenu) = {
       local i, len = length(self.myContents), key = '', loc;

       "\b\(<<self.title>>\)\b";
       // Print all of the strings up to and including lastDisplayed. Also,
       // append "[#/#]" after them to show which hint out of how many
       // each is.
       for (i = 1; i <= self.lastDisplayed; i++) {
           "<<self.myContents[i]>> [<<i>>/<<len>>]\b";
           // If we're at the end, let the player know by printing the
           // goodbye message
           if (i == len)
               "<<self.goodbye>>\n";
       }

       while (true) {
           key = lower(inputkey());
           loc = find(topMenu.keyList, key);
           if (loc && (loc % 2 == 0))
               loc--;
           if (loc == M_QUIT)
               return nil;
           if (loc == M_PREV || self.lastDisplayed == len)
                return true;
           self.lastDisplayed++;
           "<<self.myContents[self.lastDisplayed]>> [<<
               self.lastDisplayed>>/<<len>>]\b";
           if (self.lastDisplayed == len)
               "<<self.goodbye>>\n";
       }
   }

   displayHTML(topMenu) = {
       local i, selection = 1, len = length(self.myContents), key = '', loc,
           firstTime = true, topTopic = 1, chunk = self.chunkSize, events;

       while (true) {
           // Set up the banner
           "<banner border id=MenuBody><body bgcolor=\"<<self.bgcolor>>\"
               text=\"<<self.fgcolor>>\">";
           "<table><tr><td width=\"<<self.indent>>\"> </td><td>";
           // The firstTime variable flags whether or not this is our first
           // trip through this loop. If it's not, we need to adjust the
           // # of displayed hints, among other things.
           if (firstTime)
               i = 1;
           else {
               // Only display a # of strings equal to or less than chunkSize
               if (topTopic != 1 &&
                   self.lastDisplayed - topTopic < self.chunkSize)
                   i = topTopic;
               else i = self.lastDisplayed - self.chunkSize + 1;
               if (i < 1)
                   i = 1;
           }
           while (i <= self.lastDisplayed) {
               // If we display a chunk's worth of strings and this is our
               // first time through the loop we pause, then keep printing
               // out the rest of the strings
               if (i > chunk && firstTime) {
                   "</td></tr></table>";
                   "</banner>";
                   events = inputevent();
                   if (events[1] == INPUT_EVENT_HREF) {    // There should be
                       selection = cvtnum(events[2]);      // no HREF events,
                       loc = M_SEL;                    // but just in case...
                   }
                   else {
                       key = lower(events[2]);
                       loc = find(topMenu.keyList, key);
                       if (loc && (loc % 2 == 0))
                           loc--;
                   }
                   if (loc == M_QUIT || loc == M_PREV)
                       return (loc != M_QUIT);
                   "<banner border id=MenuBody><body
                       bgcolor=\"<<self.bgcolor>>\"
                       text=\"<<self.fgcolor>>\">";
                   "<table><tr><td width=\"<<self.indent>>\"> </td><td>";
                   // Increment the counter which keeps track of the string
                   // # at which the next chunk ends. Also bump up the
                   // current top topic string #.
                   chunk += self.chunkSize;
                   topTopic = i;
               }
               "<<self.myContents[i]>> [<<i>>/<<len>>]<BR>";
               if (i == len)
                   say(self.goodbye);
               i++;
           }
           "</td></tr></table>";
           "</banner>";

           key = lower(inputkey());
           loc = find(topMenu.keyList, key);
           if (loc == M_QUIT || loc == M_PREV ||
               self.lastDisplayed == len) {
               return (loc != M_QUIT);
           }
           self.lastDisplayed++;
           firstTime = nil;    // This is no longer our first time through
                               // the loop
       }
   }
;

// longTopicItems are used to print out big long gobs of text on a subject.
// Use it for printing long treatises on your design philosophy and the like.
class longTopicItem: object
   title = ""
   myContents = ''             // This can be a string or a routine
   goodbye = '[The End]'       // The goodbye message

   display(topMenu) = {
       local key, loc;

       clearscreen();
       "<CENTER>\(<<self.title>>\)</CENTER>\b";

       // Simply dump out our contents, then print the goodbye message.
       "<<self.myContents>>\b<<self.goodbye>>";

       key = lower(inputkey());
       loc = find(topMenu.keyList, key);
       if (loc && (loc % 2 == 0))
           loc--;
       clearscreen();
       return (loc != M_QUIT);
   }

   displayHTML(topMenu) = {
       local ret;

       // longTopicItems are displayed the same in HTML TADS as they are
       // in standard TADS, barring some banner fiddling.
       ret = self.display(topMenu);
       topMenu.topBanner;
       return ret;
   }
;