/* EX_VOTE.C - This program demonstrates an online voting program that is    */
/*             written using OpenDoors. The Vote program allows users to     */
/*             create questions or surveys for other users to respond to.    */
/*             Users are also able to view the results of voting on each     */
/*             topic. The program supports up to 200 questions, and can be   */
/*             configured to either allow or disallow viewing of results     */
/*             prior to voting.                                              */
/*                                                                           */
/*             This program shows how to do the following:                   */
/*                                                                           */
/*                - How to display text using od_printf(), using imbedded    */
/*                  strings to change the display color.                     */
/*                - Add support for standard command-line options.           */
/*                - Use the OpenDoors configuration file system, including   */
/*                  adding your own configuration file options.              */
/*                - Activate the OpenDoors log file system and write         */
/*                  information to the log file.                             */
/*                - Display a menu from an external ASCI/ANSI/Avatar/RIP     */
/*                  file.                                                    */
/*                - How to setup a user file and general data file, and how  */
/*                  to access it in a multi-node compatible way (if          */
/*                  MULTINODE_AWARE is defined).                             */
/*                                                                           */
/*             To recompile this program, follow the instructions in the     */
/*             OpenDoors manual. For a DOS compiler, be sure to set your     */
/*             compiler to use the large memory model, and add the           */
/*             ODOORL.LIB file to your project/makefile.                     */

/* Uncomment the following line for multi-node compatible file access. */
/* #define MULTINODE_AWARE */

/* Include standard C header files required by Vote. */
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>
#include <ctype.h>

/* Include the OpenDoors header file. This line must be done in any program */
/* using OpenDoors.                                                         */
#include "OpenDoor.h"

#ifdef MULTINODE_AWARE
#include <filewrap.h>
#endif
#include "genwrap.h"

/* Manifest constants used by Vote */
#define NO_QUESTION              -1
#define NEW_ANSWER               -1

#define QUESTIONS_VOTED_ON        0x0001
#define QUESTIONS_NOT_VOTED_ON    0x0002

#define MAX_QUESTIONS             200
#define MAX_USERS                 30000
#define MAX_ANSWERS               15
#define QUESTION_STR_SIZE         71
#define ANSWER_STR_SIZE           31

#define USER_FILENAME             "vote.usr"
#define QUESTION_FILENAME         "vote.qst"

#define FILE_ACCESS_MAX_WAIT      20

#define QUESTION_PAGE_SIZE        17


/* Structure of records stored in the VOTE.USR file */
typedef struct
{
   char szUserName[36];
   BYTE bVotedOnQuestion[MAX_QUESTIONS];
} tUserRecord;
              
tUserRecord CurrentUserRecord;
int nCurrentUserNumber;


/* Structure of records stored in the VOTE.QST file */
typedef struct
{
   char szQuestion[72];
   char aszAnswer[MAX_ANSWERS][32];
   INT32 nTotalAnswers;
   DWORD auVotesForAnswer[MAX_ANSWERS];
   DWORD uTotalVotes;
   DWORD bCanAddAnswers;
   char szCreatorName[36];
   time_t lCreationTime;
} tQuestionRecord;


/* Global variables. */
int nViewResultsFrom = QUESTIONS_VOTED_ON;
int nQuestionsVotedOn = 0;


/* Prototypes for functions that form EX_VOTE */
void CustomConfigFunction(char *pszKeyword, char *pszOptions);
void BeforeExitFunction(void);
void VoteOnQuestion(void);
void ViewResults(void);
int GetQuestion(int nQuestion, tQuestionRecord *pQuestionRecord);
void AddQuestion(void);
int ChooseQuestion(int nFromWhichQuestions, char *pszTitle, int *nLocation);
void DisplayQuestionResult(tQuestionRecord *pQuestionRecord);
int ReadOrAddCurrentUser(void);
void WriteCurrentUser(void);
FILE *ExclusiveFileOpen(char *pszFileName, char *pszMode, int *phHandle);
void ExclusiveFileClose(FILE *pfFile, int hHandle);
void WaitForEnter(void);


/* main() or WinMain() function - Program execution begins here. */
#ifdef ODPLAT_WIN32
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
   LPSTR lpszCmdLine, int nCmdShow)
#else
int main(int argc, char *argv[])
#endif
{
   /* Variable to store user's choice from the menu */
   char chMenuChoice = '\0';
   char chYesOrNo;

#ifdef ODPLAT_WIN32
   /* In Windows, pass in nCmdShow value to OpenDoors. */
   od_control.od_cmd_show = nCmdShow;

   /* Ignore unused parameters. */
   (void)hInstance;
   (void)hPrevInstance;
#endif
   
   /* Set program's name for use by OpenDoors. */
   strcpy(od_control.od_prog_name, "Vote");
   strcpy(od_control.od_prog_version, "Version 6.00");
   strcpy(od_control.od_prog_copyright, "Copyright 1991-1996 by Brian Pirie");

   /* Call the standard command-line parsing function. You will probably     */
   /* want to do this in most programs that you write using OpenDoors, as it */
   /* automatically provides support for many standard command-line options  */
   /* that will make the use and setup of your program easer. For details,   */
   /* run the vote program with the /help command line option.               */
#ifdef ODPLAT_WIN32
   od_parse_cmd_line(lpszCmdLine);
#else
   od_parse_cmd_line(argc, argv);
#endif

   /* Enable use of OpenDoors configuration file system. */
   od_control.od_config_file = INCLUDE_CONFIG_FILE;

   /* Set function to process custom configuration file lines. */
   od_control.od_config_function = CustomConfigFunction;

   /* Include the OpenDoors multiple personality system, which allows    */
   /* the system operator to set the sysop statusline / function key set */
   /* to mimic the BBS software of their choice.                         */
   od_control.od_mps = INCLUDE_MPS;
   
   /* Include the OpenDoors log file system, which will record when the */
   /* door runs, and major activites that the user performs.            */ 
   od_control.od_logfile = INCLUDE_LOGFILE;

   /* Set filename for log file. If not set, DOOR.LOG will be used by */
   /* default.                                                        */
   strcpy(od_control.od_logfile_name, "vote.log");

   /* Set function to be called before program exits. */
   od_control.od_before_exit = BeforeExitFunction;

   /* Initialize OpenDoors. This function call is optional, and can be used */
   /* to force OpenDoors to read the door informtion file and begin door    */
   /* operations. If a call to od_init() is not included in your program,   */
   /* OpenDoors initialization will be performed at the time of your first  */
   /* call to any OpenDoors function. */
   od_init();

   /* Call the Vote function ReadOrAddCurrentUser() to read the current   */
   /* user's record from the Vote user file, or to add the user to the    */
   /* file if this is the first time that they have used Vote.            */
   if(!ReadOrAddCurrentUser())
   {
      /* If unable to obtain a user record for the current user, then exit */
      /* the door after displaying an error message.                       */
      od_printf("Unable to access user file. File may be locked or full.\n\r");
      WaitForEnter();
      od_exit(1, FALSE);
   }   

   /* Loop until the user choses to exit the door. For each iteration of  */
   /* this loop, we display the main menu, get the user's choice from the */
   /* menu, and perform the appropriate action for their choice.          */

   while(chMenuChoice != 'E' && chMenuChoice != 'H')
   {
      /* Clear the screen */
      od_clr_scr();

      /* Display main menu. */
      
      /* First, attempt to display menu from an VOTE.ASC/ANS/AVT/RIP file. */
      if((chMenuChoice = od_hotkey_menu("VOTE", "VRADPEH", TRUE)) == 0)
      {
         /* If the VOTE file could not be displayed, display our own menu. */
         od_printf("`bright red`                     Vote - OpenDoors 6.00 example program\n\r");\
         od_printf("`dark red`");
         if(od_control.user_ansi || od_control.user_avatar)
         {
            od_repeat((unsigned char)196, 79);
         }
         else
         {
            od_repeat('-', 79);
         }
         od_printf("\n\r\n\r\n\r`dark green`");
         od_printf("                        [`bright green`V`dark green`] Vote on a question\n\r\n\r");
         od_printf("                        [`bright green`R`dark green`] View the results of question\n\r\n\r");
         od_printf("                        [`bright green`A`dark green`] Add a new question\n\r\n\r");
         od_printf("                        [`bright green`P`dark green`] Page system operator for chat\n\r\n\r");
         od_printf("                        [`bright green`E`dark green`] Exit door and return to the BBS\n\r\n\r");
         od_printf("                        [`bright green`H`dark green`] End call (hangup)\n\r\n\r\n\r");
         od_printf("`bright white`Press the key corresponding to the option of your choice. (%d mins)\n\r`dark green`",
            od_control.user_timelimit);
                                                                                                       \
         /* Get the user's choice from the main menu. This choice may only be */
         /* V, R, A, D, P, E or H.                                            */
         chMenuChoice = od_get_answer("VRADPEH");
      }

      /* Perform the appropriate action based on the user's choice */
      switch(chMenuChoice)
      {
         case 'V':
            /* Call Vote's function to vote on question */
            VoteOnQuestion();
            break;
            
         case 'R':
            /* Call Vote's function to view the results of voting */
            ViewResults();
            break;
            
         case 'A':
            /* Call Vote's function to add a new question. */
            AddQuestion();
            break;
            
         case 'P':
            /* If the user pressed P, allow them page the system operator. */
            od_page();
            break;

         case 'H':
            /* If the user pressed H, ask whether they wish to hangup. */
            od_printf("\n\rAre you sure you wish to hangup? (Y/N) ");

            /* Get user's response */
            chYesOrNo = od_get_answer("YN");

            if(chYesOrNo == 'N')
            {
               /* If user answered no, then reset menu choice, so that */
               /* program will not exit.                               */
               chMenuChoice = '\0';
            }
            break;
      }
   }

   if(chMenuChoice == 'H')
   {
      /* If the user chooses to hangup, then hangup when exiting. */
      od_exit(0, TRUE);
   }
   else
   {
      /* Otherwise, exit normally (without hanging up). */
      od_printf("Returning to BBS, please wait...\n\r");
      od_exit(0, FALSE);
   }

   return(0);
}


/* CustomConfigFunction() is called by OpenDoors to process custom */
/* configuration file keywords that Vote uses.                     */
void CustomConfigFunction(char *pszKeyword, char *pszOptions)
{
   if(stricmp(pszKeyword, "ViewUnanswered") == 0)
   {
      /* If keyword is ViewUnanswered, set local variable based on contents */
      /* of options string.                                                 */
      if(stricmp(pszOptions, "Yes") == 0)
      {
         nViewResultsFrom = QUESTIONS_VOTED_ON | QUESTIONS_NOT_VOTED_ON;
      }
      else if(stricmp(pszOptions, "No") == 0)
      {
         nViewResultsFrom = QUESTIONS_VOTED_ON;
      }
   }
}


/* Vote configures OpenDoors to call the BeforeExitFunction() before      */
/* the door exists for any reason. You can use this function to close any */
/* files or perform any other operations that you wish to have peformed   */
/* before OpenDoors exists for any reason. The od_control.od_before_exit  */
/* variable sets the function to be called before program exit.           */
void BeforeExitFunction(void)
{
   char szLogMessage[80];
   
   /* Write number of messages voted on to log file. */
   sprintf(szLogMessage, "User has voted on %d question(s)",
      nQuestionsVotedOn);
   od_log_write(szLogMessage);
}


/* Vote calls the VoteOnQuestion() function when the user chooses the     */
/* vote command from the main menu. This function displays a list of      */
/* available topics, asks for the user's answer to the topic they select, */
/* and display's the results of voting on that topic.                     */
void VoteOnQuestion(void)
{
   int nQuestion;
   int nAnswer;
   tQuestionRecord QuestionRecord;
   char szNewAnswer[ANSWER_STR_SIZE];
   char szUserInput[3];
   FILE *fpFile;
   int hFile;
   int nPageLocation = 0;

   /* Loop until the user chooses to return to the main menu, or until */
   /* there are no more questions to vote on.                          */
   for(;;)   
   {
      /* Allow the user to choose a question from the list of questions */
      /* that they have not voted on.                                   */
      nQuestion = ChooseQuestion(QUESTIONS_NOT_VOTED_ON,
         "                              Vote On A Question\n\r",
         &nPageLocation);
   

      /* If the user did not choose a question, return to main menu. */   
      if(nQuestion == NO_QUESTION)
      {
         return;
      }

      /* Read the question chosen by the user. */
      if(!GetQuestion(nQuestion, &QuestionRecord))
      {
         /* If unable to access file, return to main menu. */
         return;
      }
   
      /* Don't allow addition of new answers if maximum number of answers */
      /* have already been added.                                         */
      if(QuestionRecord.nTotalAnswers >= MAX_ANSWERS)
      {
         QuestionRecord.bCanAddAnswers = FALSE;
      }
   
      /* Loop until user makes a valid respose. */
      for(;;)
      {
         /* Display question to user. */

         /* Clear the screen. */
         od_clr_scr();

         /* Display question itself. */
         od_printf("`bright red`%s\n\r\n\r", QuestionRecord.szQuestion);

         /* Loop for each answer to the question. */   
         for(nAnswer = 0; nAnswer < QuestionRecord.nTotalAnswers; ++nAnswer)
         {
            /* Display answer number and answer. */
            od_printf("`bright green`%d. `dark green`%s\n\r",
               nAnswer + 1,
               QuestionRecord.aszAnswer[nAnswer]);
         }

         /* Display prompt to user. */
         od_printf("\n\r`bright white`Enter answer number, ");
         if(QuestionRecord.bCanAddAnswers)
         {
            od_printf("[A] to add your own response, ");
         }
         od_printf("[Q] to quit: `dark green`");
   
         /* Get response from user. */
         od_input_str(szUserInput, 2, ' ', 255);
         /* Add a blank line. */      
         od_printf("\n\r");
   
         /* If user entered Q, return to main menu. */
         if(stricmp(szUserInput, "Q") == 0)
         {
            return;
         }

         /* If user enetered A, and adding answers is premitted ... */
         else if (stricmp(szUserInput, "A") == 0
            && QuestionRecord.bCanAddAnswers)
         {
            /* ... Prompt for answer from user. */
            od_printf("`bright green`Please enter your new answer:\n\r");
            od_printf("`dark green`[------------------------------]\n\r ");
         
            /* Get string from user. */
            od_input_str(szNewAnswer, ANSWER_STR_SIZE - 1, ' ', 255);
         
            /* Record that user entered a new answer answer. */
            nAnswer = NEW_ANSWER;

            /* If user entered a valid answer, then exit loop. */
            if(strlen(szNewAnswer) > 0)
            {
               break;
            }         
         }

         /* Otherwise, attempt to get answer number from user. */      
         nAnswer = atoi(szUserInput) - 1;

         /* If user input is not a valid answer. */      
         if(nAnswer < 0 || nAnswer >= QuestionRecord.nTotalAnswers)
         {
            /* Display message. */
            od_printf("That is not a valid response.\n\r");
            WaitForEnter();
         }
         else
         {
            /* Otherwise, exit loop. */
            break;
         }
      }

      /* Add user's vote to question. */
   
      /* Open question file for exclusive access by this node. */
      fpFile = ExclusiveFileOpen(QUESTION_FILENAME, "r+b", &hFile);
      if(fpFile == NULL)
      {
         /* If unable to access file, display error and return. */
         od_printf("Unable to access the question file.\n\r");
         WaitForEnter();
         return;
      }
   
      /* Read the answer record from disk, because it may have been changed. */
      /* by another node. */
      fseek(fpFile, (long)nQuestion * sizeof(tQuestionRecord), SEEK_SET);
      if(fread(&QuestionRecord, sizeof(tQuestionRecord), 1, fpFile) != 1)
      {
         /* If unable to access file, display error and return. */
         ExclusiveFileClose(fpFile, hFile);
         od_printf("Unable to read from question file.\n\r");
         WaitForEnter();
         return;
      }
   
      /* If user entered their own answer, try to add it to the question. */
      if(nAnswer == NEW_ANSWER)
      {
         /* Check that there is still room for another answer. */
         if(QuestionRecord.nTotalAnswers >= MAX_ANSWERS)
         {
            ExclusiveFileClose(fpFile, hFile);
            od_printf("Sorry, this question already has the maximum number of answers.\n\r");
            WaitForEnter();
            return;
         }
      
         /* Set answer number to number of new answer. */
         nAnswer = QuestionRecord.nTotalAnswers;
      
         /* Add 1 to total number of answers. */
         ++QuestionRecord.nTotalAnswers;
      
         /* Initialize new answer string and count. */
         strcpy(QuestionRecord.aszAnswer[nAnswer], szNewAnswer);
         QuestionRecord.auVotesForAnswer[nAnswer] = 0;
      }
   
      /* Add user's vote to question. */
      ++QuestionRecord.auVotesForAnswer[nAnswer];
      ++QuestionRecord.uTotalVotes;
   
      /* Write the question record back to the file. */
      fseek(fpFile, (long)nQuestion * sizeof(tQuestionRecord), SEEK_SET);
      if(fwrite(&QuestionRecord, sizeof(tQuestionRecord), 1, fpFile) != 1)
      {
         /* If unable to access file, display error and return. */
         ExclusiveFileClose(fpFile, hFile);
         od_printf("Unable to write question to file.\n\r");
         WaitForEnter();
         return;
      }
   
      /* Close the question file to allow access by other nodes. */
      ExclusiveFileClose(fpFile, hFile);
   
      /* Record that user has voted on this question. */
      CurrentUserRecord.bVotedOnQuestion[nQuestion] = TRUE;
   
      /* Open user file for exclusive access by this node. */
      fpFile = ExclusiveFileOpen(USER_FILENAME, "r+b", &hFile);
      if(fpFile == NULL)
      {
         /* If unable to access file, display error and return. */
         od_printf("Unable to access the user file.\n\r");
         WaitForEnter();
         return;
      }

      /* Update the user's record in the user file. */
      fseek(fpFile, nCurrentUserNumber * sizeof(tUserRecord), SEEK_SET);
      if(fwrite(&CurrentUserRecord, sizeof(tUserRecord), 1, fpFile) != 1)
      {
         /* If unable to access file, display error and return. */
         ExclusiveFileClose(fpFile, hFile);
         od_printf("Unable to write to user file.\n\r");
         WaitForEnter();
         return;
      }
   
      /* Close the user file to allow access by other nodes. */
      ExclusiveFileClose(fpFile, hFile);
   
      /* Display the result of voting on this question to the user. */
      DisplayQuestionResult(&QuestionRecord);

      /* Add 1 to count of questions that the user has voted on. */
      nQuestionsVotedOn++;
   }
}


/* The ViewResults function is called when the user chooses the "view    */
/* results" command from the main menu. This function alows the user to  */
/* choose a question from the list of questions, and then displays the   */
/* results of voting on that question.                                   */
void ViewResults(void)
{
   int nChoice;
   tQuestionRecord QuestionRecord;
   int nPageLocation = 0;

   /* Loop until user chooses to return to main menu. */
   for(;;)
   {   
      /* Allow the user to choose a question from the list of questions that */
      /* they have already voted on.                                         */
      nChoice = ChooseQuestion(nViewResultsFrom,
         "                                 View Results\n\r", &nPageLocation);

      /* If the user did not choose a question, return to main menu. */   
      if(nChoice == NO_QUESTION)
      {
         return;
      }
   
      /* Read the specified question number from the question file. */
      if(!GetQuestion(nChoice, &QuestionRecord))
      {
         return;
      }
   
      /* Display the results for the selected question. */
      DisplayQuestionResult(&QuestionRecord);
   }
}


/* The GetQuestion function read the record for the specified question */
/* number from the question file.                                      */
int GetQuestion(int nQuestion, tQuestionRecord *pQuestionRecord)
{
   FILE *fpQuestionFile;
   int hQuestionFile;

   /* Open the question file for exculsive access by this node. */
   fpQuestionFile = ExclusiveFileOpen(QUESTION_FILENAME, "r+b",
      &hQuestionFile);
   if(fpQuestionFile == NULL)
   {
      /* If unable to access file, display error and return. */
      od_printf("Unable to access the question file.\n\r");
      WaitForEnter();
      return(FALSE);
   }
   
   /* Move to location of question in file. */
   fseek(fpQuestionFile, (long)nQuestion * sizeof(tQuestionRecord), SEEK_SET);
   
   /* Read the question from the file. */
   if(fread(pQuestionRecord, sizeof(tQuestionRecord), 1, fpQuestionFile) != 1)
   {
      /* If unable to access file, display error and return. */
      ExclusiveFileClose(fpQuestionFile, hQuestionFile);
      od_printf("Unable to read from question file.\n\r");
      WaitForEnter();
      return(FALSE);;
   }
   
   /* Close the question file to allow access by other nodes. */
   ExclusiveFileClose(fpQuestionFile, hQuestionFile);

   /* Return with success. */
   return(TRUE);
}
 

/* The AddQuestion() function is called when the user chooses the "add    */
/* question" option from the main menu. This function allows the user     */
/* to enter a new question, possible responses, and save the question for */
/* other users to vote on.                                                */
void AddQuestion(void)
{
   tQuestionRecord QuestionRecord;
   FILE *fpQuestionFile;
   int hQuestionFile;
   char szLogMessage[100];

   /* Clear the screen. */
   od_clr_scr();
   
   /* Display screen header. */
   od_printf("`bright red`                                Add A Question\n\r");
   od_printf("`dark red`");
   if(od_control.user_ansi || od_control.user_avatar)
   {
      od_repeat((unsigned char)196, 79);
   }
   else
   {
      od_repeat('-', 79);
   }
   od_printf("\n\r\n\r");
   
   /* Obtain quesiton text from the user. */
   od_printf("`bright green`Enter Your Question (blank line cancels)\n\r");
   od_printf("`dark green`[----------------------------------------------------------------------]\n\r ");
   od_input_str(QuestionRecord.szQuestion, QUESTION_STR_SIZE - 1, ' ', 255);
   
   /* If question was empty, then return to main menu. */
   if(strlen(QuestionRecord.szQuestion) == 0)
   {
      return;
   }
   
   /* Display prompt for answers. */
   od_printf("\n\r`bright green`Enter Possible Answers (blank line when done)\n\r");
   od_printf("`dark green`   [------------------------------]\n\r");
   
   /* Loop, getting answers from user. */
   for(QuestionRecord.nTotalAnswers = 0;
       QuestionRecord.nTotalAnswers < MAX_ANSWERS;
       QuestionRecord.nTotalAnswers++)
   {
      /* Display prompt with answer number. */
      od_printf("`bright green`%2d: `dark green`", QuestionRecord.nTotalAnswers + 1);
      
      /* Get string from user. */
      od_input_str(QuestionRecord.aszAnswer[QuestionRecord.nTotalAnswers],
         ANSWER_STR_SIZE - 1, ' ', 255);
         
      /* If string was empty, then exit loop. */
      if(strlen(QuestionRecord.aszAnswer[QuestionRecord.nTotalAnswers]) == 0)
      {
         break;
      }
      
      /* Reset count of votes for this answer to zero. */
      QuestionRecord.auVotesForAnswer[QuestionRecord.nTotalAnswers] = 0;
   }
   
   /* If no answers were supplied, then cancel, returning to main menu. */
   if(QuestionRecord.nTotalAnswers == 0)
   {
      return;
   }

   /* Ask whether users should be able to add their own answers. */
   od_printf("\n\r`bright green`Should voters be able to add their own options? (Y/N) `dark green`");
   
   /* Get answer from user. */
   if(od_get_answer("YN") == 'Y')
   {
      /* If user pressed the 'Y' key. */
      od_printf("Yes\n\r\n\r");
      
      /* Record user's response. */
      QuestionRecord.bCanAddAnswers = TRUE;
   }
   else
   {
      /* If user pressed the 'N' key. */
      od_printf("No\n\r\n\r");

      /* Record user's response. */
      QuestionRecord.bCanAddAnswers = FALSE;
   }
   
   /* Confirm save of new question. */
   od_printf("`bright green`Do you wish to save this new question? (Y/N) `dark green`");
   
   /* If user does not want to save the question, return to main menu now. */
   if(od_get_answer("YN") == 'N')
   {
      return;
   }

   /* Set total number of votes for this question to 0. */   
   QuestionRecord.uTotalVotes = 0;
   
   /* Set creator name and creation time for this question. */
   strcpy(QuestionRecord.szCreatorName, od_control.user_name);
   QuestionRecord.lCreationTime = time(NULL);
   
   /* Open question file for exclusive access by this node. */
   fpQuestionFile = ExclusiveFileOpen(QUESTION_FILENAME, "a+b",
      &hQuestionFile);
   if(fpQuestionFile == NULL)
   {
      od_printf("Unable to access the question file.\n\r");
      WaitForEnter();
      return;
   }
   
   /* Determine number of records in question file. */
   fseek(fpQuestionFile, 0, SEEK_END);
   
   /* If question file is full, display message and return to main menu */
   /* after closing file.                                               */
   if(ftell(fpQuestionFile) / sizeof(tQuestionRecord) >= MAX_QUESTIONS)
   {
      ExclusiveFileClose(fpQuestionFile, hQuestionFile);
      od_printf("Cannot add another question, Vote is limited to %d questions.\n\r", MAX_QUESTIONS);
      WaitForEnter();
      return;
   }
   
   /* Add new question to file. */
   if(fwrite(&QuestionRecord, sizeof(QuestionRecord), 1, fpQuestionFile) != 1)
   {
      ExclusiveFileClose(fpQuestionFile, hQuestionFile);
      od_printf("Unable to write to question file.\n\r");
      WaitForEnter();
      return;
   }
   
   /* Close question file, allowing other nodes to access file. */
   ExclusiveFileClose(fpQuestionFile, hQuestionFile);

   /* Record in the logfile that user has added a new question. */
   sprintf(szLogMessage, "User adding questions: %s",
      QuestionRecord.szQuestion);
   od_log_write(szLogMessage);
}


/* The ChooseQuestion() function provides a list of questions and allows   */
/* the user to choose a particular question, cancel back to the main menu, */ 
/* and page up and down in the list of questions. Depending upon the value */
/* of the nFromWhichQuestions parameter, this function will present a list */
/* of questions that the user has voted on, a list of questions that the   */
/* user has not voted on, or a list of all questions.                      */
int ChooseQuestion(int nFromWhichQuestions, char *pszTitle, int *nLocation)
{
   int nCurrent;
   int nFileQuestion = 0;
   int nPagedToQuestion = *nLocation;
   int nDisplayedQuestion = 0;
   char bVotedOnQuestion;
   char chCurrent;
   tQuestionRecord QuestionRecord;
   FILE *fpQuestionFile;
   int hQuestionFile;
   static char szQuestionName[MAX_QUESTIONS][QUESTION_STR_SIZE];
   static int nQuestionNumber[MAX_QUESTIONS];
   
   /* Attempt to open question file. */
   fpQuestionFile = ExclusiveFileOpen(QUESTION_FILENAME, "r+b",
      &hQuestionFile);

   /* If unable to open question file, assume that no questions have been */
   /* created.                                                            */
   if(fpQuestionFile == NULL)
   {
      /* Display "no questions yet" message. */
      od_printf("\n\rNo questions have been created so far.\n\r");
      
      /* Wait for user to press enter. */
      WaitForEnter();
      
      /* Indicate that no question has been chosen. */
      return(NO_QUESTION);
   }
   
   /* Loop for every question record in the file. */
   while(fread(&QuestionRecord, sizeof(QuestionRecord), 1, fpQuestionFile) == 1)
   {
      /* Determine whether or not the user has voted on this question. */
      bVotedOnQuestion = CurrentUserRecord.bVotedOnQuestion[nFileQuestion];
      
      /* If this is the kind of question that the user is choosing from */
      /* right now.                                                     */
      if((bVotedOnQuestion && (nFromWhichQuestions & QUESTIONS_VOTED_ON)) ||
         (!bVotedOnQuestion && (nFromWhichQuestions & QUESTIONS_NOT_VOTED_ON)))
      {
         /* Add this question to list to be displayed. */
         strcpy(szQuestionName[nDisplayedQuestion],
            QuestionRecord.szQuestion);
         nQuestionNumber[nDisplayedQuestion] = nFileQuestion;
         
         /* Add one to number of questions to be displayed in list. */
         nDisplayedQuestion++;
      }
      
      /* Move to next question in file. */
      ++nFileQuestion;
   }   
   
   /* Close question file to allow other nodes to access the file. */
   ExclusiveFileClose(fpQuestionFile, hQuestionFile);

   /* If there are no questions for the user to choose, display an */
   /* appropriate message and return. */
   if(nDisplayedQuestion == 0)
   {
      /* If we were to list all questions. */
      if((nFromWhichQuestions & QUESTIONS_VOTED_ON)
         && (nFromWhichQuestions & QUESTIONS_NOT_VOTED_ON))
      {
         od_printf("\n\rThere are no questions.\n\r");
      }
      /* If we were to list questions that the user has voted on. */
      else if(nFromWhichQuestions & QUESTIONS_VOTED_ON)
      {
         od_printf("\n\rThere are no questions that you have voted on.\n\r");
      }
      /* Otherwise, we were to list questions that use has not voted on. */
      else
      {
         od_printf("\n\rYou have voted on all the questions.\n\r");
      }
      
      /* Wait for user to press enter key. */
      WaitForEnter();
      
      /* Return, indicating that no question was chosen. */
      return(NO_QUESTION);
   }

   /* Ensure that initial paged to location is within range. */
   while(nPagedToQuestion >= nDisplayedQuestion)
   {
      nPagedToQuestion -= QUESTION_PAGE_SIZE;
   }

   /* Loop, displaying current page of questions, until the user makes a */
   /* choice.                                                            */
   for(;;)
   {
      /* Clear the screen. */
      od_clr_scr();

      /* Display header. */
      od_printf("`bright red`");
      od_printf(pszTitle);
      od_printf("`dark red`");
      if(od_control.user_ansi || od_control.user_avatar)
      {
         od_repeat((unsigned char)196, 79);
      }
      else
      {
         od_repeat('-', 79);
      }
      od_printf("\n\r");
   
      /* Display list of questions on this page. */
      for(nCurrent = 0;
         nCurrent < QUESTION_PAGE_SIZE
         && nCurrent < (nDisplayedQuestion - nPagedToQuestion);
         ++nCurrent)
      {
         /* Determine character to display for current line. */
         if(nCurrent < 9)
         {
            chCurrent = (char)('1' + nCurrent);
         }
         else
         {
            chCurrent = (char)('A' + (nCurrent - 9));
         }
      
         /* Display this question's title. */
         od_printf("`bright green`%c.`dark green`", chCurrent);
         od_printf(" %s\n\r", szQuestionName[nCurrent + nPagedToQuestion]);
      }

      /* Display prompt for input. */
      od_printf("\n\r`bright white`[Page %d]  Choose a question or",
         (nPagedToQuestion / QUESTION_PAGE_SIZE) + 1);
      if(nPagedToQuestion < nDisplayedQuestion - QUESTION_PAGE_SIZE)
      {
         od_printf(" [N]ext page,");
      }
      if(nPagedToQuestion > 0)
      {
         od_printf(" [P]revious page,");
      }
      od_printf(" [Q]uit.\n\r");
      
      /* Loop until the user makes a valid choice. */
      for(;;)
      {      
         /* Get input from user */
         chCurrent = (char)od_get_key(TRUE);
         chCurrent = (char)toupper(chCurrent);
      
         /* Respond to user's input. */
      
         /* If user pressed Q key. */
         if(chCurrent == 'Q')
         {
            /* Return without a choosing a question. */
            return(NO_QUESTION);
         }
      
         /* If user pressed P key. */
         else if(chCurrent == 'P')
         {
            /* If we are not at the first page. */
            if(nPagedToQuestion > 0)
            {
               /* Move paged to location up one page. */
               nPagedToQuestion -= QUESTION_PAGE_SIZE;
               
               /* Exit user input loop to display next page. */
               break;
            }
         }
      
         /* If user pressed N key. */
         else if(chCurrent == 'N')
         {
            /* If there is more questions after this page. */
            if(nPagedToQuestion < nDisplayedQuestion - QUESTION_PAGE_SIZE)
            {
               /* Move paged.to location down one page. */
               nPagedToQuestion += QUESTION_PAGE_SIZE;

               /* Exit user input loop to display next page. */
               break;
            }
         }
      
         /* Otherwise, check whether the user chose a valid question. */
         else if ((chCurrent >= '1' && chCurrent <= '9')
            || (chCurrent >= 'A' && chCurrent <= 'H'))
         {
            /* Get question number from key pressed. */
            if(chCurrent >= '1' && chCurrent <= '9')
            {
               nCurrent = chCurrent - '1';
            }
            else
            {
               nCurrent = (chCurrent - 'A') + 9;
            }
         
            /* Add current paged to position to user's choice. */
            nCurrent += nPagedToQuestion;

            /* If this is valid question number. */            
            if(nCurrent < nDisplayedQuestion)
            {
               /* Set caller's current question number. */
               *nLocation = nPagedToQuestion;
            
               /* Return actual question number in file. */
               return(nQuestionNumber[nCurrent]);
            }
         }
      }
   }
}


/* The DisplayQuestionResult() function is called to display the results */
/* of voting on a paricular question, and is passed the question record  */
/* of the question. This function is called when the user selects a      */
/* question using the "view results" option, and is also called after    */
/* the user has voted on a question, to display the results of voting on */
/* that question.                                                        */
void DisplayQuestionResult(tQuestionRecord *pQuestionRecord)
{
   int nAnswer;
   int uPercent;

   /* Clear the screen. */
   od_clr_scr();

   /* Check that there have been votes on this question. */
   if(pQuestionRecord->uTotalVotes == 0)
   {
      /* If there have been no votes for this question, display a message */
      /* and return.                                                      */
      od_printf("Nobody has voted on this question yet.\n\r");
      WaitForEnter();
      return;
   }

   /* Display question itself. */
   od_printf("`bright red`%s\n\r", pQuestionRecord->szQuestion);

   /* Display author's name. */
   od_printf("`dark red`Question created by %s on %s\n\r",
      pQuestionRecord->szCreatorName,
      ctime(&pQuestionRecord->lCreationTime));
   
   /* Display heading for responses. */
   od_printf("`bright green`Response                        Votes  Percent  Graph\n\r`dark green`");
   if(od_control.user_ansi || od_control.user_avatar)
   {
      od_repeat((unsigned char)196, 79);
   }
   else
   {
      od_repeat('-', 79);
   }
   od_printf("\n\r");

   /* Loop for each answer to the question. */   
   for(nAnswer = 0; nAnswer < pQuestionRecord->nTotalAnswers; ++nAnswer)
   {
      /* Determine percent of users who voted for this answer. */
      uPercent = (pQuestionRecord->auVotesForAnswer[nAnswer] * 100)
         / pQuestionRecord->uTotalVotes;
      
      /* Display answer, total votes and percentage of votes. */
      od_printf("`dark green`%-30.30s  %-5u  %3u%%     `bright white`",
         pQuestionRecord->aszAnswer[nAnswer],
         pQuestionRecord->auVotesForAnswer[nAnswer],
         uPercent);

      /* Display a bar graph corresponding to percent of users who voted */
      /* for this answer.                                                */
      if(od_control.user_ansi || od_control.user_avatar)
      {
         od_repeat((unsigned char)220, (unsigned char)((uPercent * 31) / 100));
      }
      else
      {
         od_repeat('=', (unsigned char)((uPercent * 31) / 100));
      }

      /* Move to next line. */
      od_printf("\n\r");
   }
   
   /* Display footer. */
   od_printf("`dark green`");
   if(od_control.user_ansi || od_control.user_avatar)
   {
      od_repeat((unsigned char)196, 79);
   }
   else
   {
      od_repeat('-', 79);
   }
   od_printf("\n\r");
   od_printf("`dark green`                         TOTAL: %u\n\r\n\r",
      pQuestionRecord->uTotalVotes);
   
   /* Wait for user to press enter. */
   WaitForEnter();
}


/* The ReadOrAddCurrentUser() function is used by Vote to search the      */
/* Vote user file for the record containing information on the user who   */
/* is currently using the door. If this is the first time that the user   */
/* has used this door, then their record will not exist in the user file. */
/* In this case, this function will add a new record for the current      */
/* user. This function returns TRUE on success, or FALSE on failure.      */
int ReadOrAddCurrentUser(void)
{
   FILE *fpUserFile;
   int hUserFile;
   int bGotUser = FALSE;
   int nQuestion;

   /* Attempt to open the user file for exclusize access by this node.     */
   /* This function will wait up to the pre-set amount of time (as defined */   
   /* near the beginning of this file) for access to the user file.        */
   fpUserFile = ExclusiveFileOpen(USER_FILENAME, "a+b", &hUserFile);

   /* If unable to open user file, return with failure. */   
   if(fpUserFile == NULL)
   {
      return(FALSE);
   }

   /* Begin with the current user record number set to 0. */
   nCurrentUserNumber = 0;

   /* Loop for each record in the file */
   while(fread(&CurrentUserRecord, sizeof(tUserRecord), 1, fpUserFile) == 1)
   {
      /* If name in record matches the current user name ... */
      if(strcmp(CurrentUserRecord.szUserName, od_control.user_name) == 0)
      {
         /* ... then record that we have found the user's record, */
         bGotUser = TRUE;
         
         /* and exit the loop. */
         break;
      }

      /* Move user record number to next user record. */      
      nCurrentUserNumber++;
   }

   /* If the user was not found in the file, attempt to add them as a */
   /* new user if the user file is not already full.                  */
   if(!bGotUser && nCurrentUserNumber < MAX_USERS)
   {
      /* Place the user's name in the current user record. */
      strcpy(CurrentUserRecord.szUserName, od_control.user_name);
      
      /* Record that user hasn't voted on any of the questions. */
      for(nQuestion = 0; nQuestion < MAX_QUESTIONS; ++nQuestion)
      {
         CurrentUserRecord.bVotedOnQuestion[nQuestion] = FALSE;
      }
      
      /* Write the new record to the file. */
      if(fwrite(&CurrentUserRecord, sizeof(tUserRecord), 1, fpUserFile) == 1)
      {
         /* If write succeeded, record that we now have a valid user record. */
         bGotUser = TRUE;
      }
   }

   /* Close the user file to allow other nodes to access it. */
   ExclusiveFileClose(fpUserFile, hUserFile);

   /* Return, indciating whether or not a valid user record now exists for */
   /* the user that is currently online.                                   */   
   return(bGotUser);
}


/* The WriteCurrentUser() function is called to save the information on the */
/* user who is currently using the door, to the VOTE.USR file.              */
void WriteCurrentUser(void)
{
   FILE *fpUserFile;
   int hUserFile;

   /* Attempt to open the user file for exclusize access by this node.     */
   /* This function will wait up to the pre-set amount of time (as defined */   
   /* near the beginning of this file) for access to the user file.        */
   fpUserFile = ExclusiveFileOpen(USER_FILENAME, "r+b", &hUserFile);

   /* If unable to access the user file, display an error message and */
   /* return.                                                         */
   if(fpUserFile == NULL)
   {
      od_printf("Unable to access the user file.\n\r");
      WaitForEnter();
      return;
   }
   
   /* Move to appropriate location in user file for the current user's */
   /* record. */
   fseek(fpUserFile, (long)nCurrentUserNumber * sizeof(tUserRecord), SEEK_SET);

   /* Write the new record to the file. */
   if(fwrite(&CurrentUserRecord, sizeof(tUserRecord), 1, fpUserFile) == 1)
   {
      /* If unable to write the record, display an error message. */
      ExclusiveFileClose(fpUserFile, hUserFile);
      od_printf("Unable to update your user record file.\n\r");
      WaitForEnter();
      return;
   }
   
   /* Close the user file to allow other nodes to access it again. */
   ExclusiveFileClose(fpUserFile, hUserFile);
}


/* This function is used by Vote to open a file. If Vote has been compiled */
/* with #define MULTINODE_AWARE uncommented (see the beginning of this     */
/* file), file access is performed in a multinode-aware way. This implies  */
/* that the file is opened of exclusive access, using share-aware open     */
/* functions that may not be available using all compilers.                */
FILE *ExclusiveFileOpen(char *pszFileName, char *pszMode, int *phHandle)
{
#ifdef MULTINODE_AWARE
   /* If Vote is being compiled for multinode-aware file access, then   */
   /* attempt to use compiler-specific share-aware file open functions. */
   FILE *fpFile = NULL;
   time_t StartTime = time(NULL);
   int hFile;

   /* Attempt to open the file while there is still time remaining. */    
   while((hFile = sopen(pszFileName, O_BINARY | O_RDWR, SH_DENYRW,
      S_IREAD | S_IWRITE)) == -1)
   {
      /* If we have been unable to open the file for more than the */
      /* maximum wait time, or if open failed for a reason other   */
      /* than file access, then attempt to create a new file and   */
      /* exit the loop.                                            */
      if(errno != EACCES ||
         difftime(time(NULL), StartTime) >= FILE_ACCESS_MAX_WAIT)
      {
         hFile = sopen(pszFileName, O_BINARY | O_CREAT, SH_DENYRW,
            S_IREAD | S_IWRITE);
         break;
      }

      /* If we were unable to open the file, call od_kernel, so that    */
      /* OpenDoors can continue to respond to sysop function keys, loss */
      /* of connection, etc.                                            */
      od_kernel();
   }

   /* Attempt to obtain a FILE * corresponding to the handle. */
   if(hFile != -1)
   {
      fpFile = fdopen(hFile, pszMode);
      if(fpFile == NULL)
      {
         close(hFile);
      }
   }

   /* Pass file handle back to the caller. */
   *phHandle = hFile;

   /* Return FILE pointer for opened file, if any. */   
   return(fpFile);
#else
   /* Ignore unused parameters. */
   (void)phHandle;

   /* If Vote is not being compiled for multinode-aware mode, then just */
   /* use fopen to access the file.                                     */
   return(fopen(pszFileName, pszMode));
#endif
}


/* The ExclusiveFileClose() function closes a file that was opened using */
/* ExclusiveFileOpen().                                                  */
void ExclusiveFileClose(FILE *pfFile, int hHandle)
{
   fclose(pfFile);
#ifdef MULTINODE_AWARE
   close(hHandle);
#else
   /* Ignore unused parameters. */
   (void)hHandle;
#endif
}


/* The WaitForEnter() function is used by Vote to create its custom   */
/* "Press [ENTER] to continue." prompt.                               */
void WaitForEnter(void)
{
   /* Display prompt. */
   od_printf("`bright white`Press [ENTER] to continue.\n\r");
   
   /* Wait for a Carriage Return or Line Feed character from the user. */
   od_get_answer("\n\r");
}