// Filename:   menu.C
// Contents:   the methods for the menu object
// Author: Greg Shaw
// Created:    7/22/93

/*
This file is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2, or (at your option) any
later version.

In addition to the permissions in the GNU General Public License, the
Free Software Foundation gives you unlimited permission to link the
compiled version of this file with other programs, and to distribute
those programs without any restriction coming from the use of this
file.  (The General Public License restrictions do apply in other
respects; for example, they cover modification of the file, and
distribution when not linked into another program.)

This file is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; see the file COPYING.  If not, write to
the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.  */

#ifndef _MENU_C_
#define _MENU_C_

#include "bbshdr.h"

#undef DEBUG

// Function:       build
// Purpose:        build a menu from the file(s) passed in
// Input:      path - the path to the menu file
//     top - is this a top level menu (e.g. start at zero in command
//     list)
// Output:     the menu is read into the object or a non-zero result is returned
// Author:     Greg Shaw
// Created:        7/22/93

int Menu::build(char *path, int top)
{
	FILE   *infile;             // input file
	char   tmpstr[MAXPATHLEN+1];// temporary string
	char   tmp2[255];           // another temp str
	char   *btmp;               // temporary string that can be killed
	char   *tptr;               // pointer into tmpstr
	char   *tptr2;              // pointer into tmpstr
	char   *bbsdir;             // BBSDIR env var
	char   num;                 // offset into word
	MenuItem item;              // temporary item
	MenuItem *iptr;             // new menu item
	MenuLine *lptr,*lptr2;      // new menu line
	MacroDef *mptr,*mptr2;      // new macro definition

	// open_chat();        // attempt to connect to chat server
	if (top)                    // top level?
	{
		// same menu?
		if (strcmp(menu_path,path) == 0)
			return(0);          // then we're already setup
		clean_storage();        // not same: delete old menu storage
		strcpy(menu_path,path); // save menu path
	}
	if (bbsdir = getenv("BBSDIR"), bbsdir == NULL)
	{
		sprintf(tmpstr,"BBSDIR env var not set for %s",username());
		ap_log(tmpstr);
		return(-1);
	}
	// add slash
	sprintf(tmpstr,"%s/menus/%s",bbsdir,path);
	if (infile = bopen(tmpstr,"r"), infile == NULL)
	{
		sprintf(tmpstr,"Unable to open menu file %s",path);
		ap_log(tmpstr);
		return(-1);
	}
	while (!feof(infile))
	{
		tptr = tmpstr;
		tmpstr[0] = 0;          // clean out to start
		while (*tptr = fgetc(infile), *tptr != '\r' && *tptr != '\n' && !feof(infile))
			tptr++;
		*tptr = 0;
		// exit if line too small
		if (feof(infile) && strlen(tmpstr) < 7)
			continue;
		// copy into malloced storage for macro expansion elasticity
		if (btmp = (char *)malloc(strlen(tmpstr)+1), btmp == NULL)
		{
			ap_log("menu: Unable to malloc tmp space in build.");
			return(0);
		}
		strcpy(btmp,tmpstr);
		// Ok.  Got line.  Now do macro expansion
		macro_sub(&btmp);
		if (strlen(btmp) > MAXPATHLEN)
		{
			ap_log("menu line too long, greater than MAXPATHLEN");
		}
		strcpy(tmpstr,btmp);
		#ifdef DEBUG
		fprintf(stderr,"line: %s\n",tmpstr);
		fflush(stderr);
		#endif
		tptr = tmpstr;
		tptr2 = tmp2;
		while (*tptr != '|' && *tptr != 0)
			*tptr2++ = *tptr++;
		*tptr2 = 0;
		// get command number
		if (sscanf(tmp2,"%d",&item.com_type) != 1 )
		{
			continue;           // we've hit end of menu file
		}
		#ifdef DEBUG
		fprintf(stderr,"comnum: %d\n",item.com_type);
		fflush(stderr);
		#endif

		// skip to next '|'
		tptr++;
		// get hot-key
		if (*tptr != '|')       // empty field?
			item.key = *tptr;
		else
			item.key = 0;
		#ifdef DEBUG
		fprintf(stderr,"key: %c 0x%x\n",item.key,item.key);
		fflush(stderr);
		#endif

		// skip to next '|'
		while (*tptr++ != '|');
		if (*tptr != '|')       // empty field?
		{                       // no.  grab access level
			if (sscanf(tptr,"%d",&item.acl) != 1)
			{
				sprintf(tmpstr,"menu.acl: %s has an error.",path);
				ap_log(tmpstr);
				bclose(infile);
				return(-1);
			}
		}
		else
			item.acl = 0;
		#ifdef DEBUG
		fprintf(stderr,"acl: %d\n",item.acl);
		fflush(stderr);
		#endif

		// skip to next '|'
		while (*tptr++ != '|');
		// get access level modifier
		if (*tptr != '|')       // empty field?
		{
			if (*tptr != '>' && *tptr != '=' && *tptr != '<')
			{
				sprintf(tmpstr,"menu.aclmo: %s has an error.",path);
				ap_log(tmpstr);
				bclose(infile);
				return(-1);
			}
			else
				item.acl_mod = *tptr;
		}
		else
			// default to
			item.acl_mod = '>';
		#ifdef DEBUG
		fprintf(stderr,"am: %c\n",item.acl_mod);
		fflush(stderr);
		#endif

		// skip to next '|'
		while (*tptr++ != '|');
		// get flags
		if (*tptr != '|')       // empty field?
		{
			if (sscanf(tptr,"%d",&item.flags) != 1)
			{
				sprintf(tmpstr,"menu.flags: %s has an error.",path);
				ap_log(tmpstr);
				bclose(infile);
				return(-1);
			}
		}
		else                    // no flags by default
			item.flags = 0;
		#ifdef DEBUG
		fprintf(stderr,"flags: %d\n",item.flags);
		fflush(stderr);
		#endif

		// skip to next '|'
		while (*tptr++ != '|');
		// get flags modifier
		if (*tptr != '|')       // empty field?
		{
			if (*tptr != '=' && *tptr != '!')
			{
				sprintf(tmpstr,"menu.flagsmod: %s has an error.",path);
				ap_log(tmpstr);
				bclose(infile);
				return(-1);
			}
			else
				item.flags_mod = *tptr;
		}
		else
			// default to =
			item.flags_mod = '=';
		#ifdef DEBUG
		fprintf(stderr,"fm: %c\n",item.flags_mod);
		fflush(stderr);
		#endif

		// skip to next '|'
		while (*tptr++ != '|');
		// get flags modifier
		if (*tptr != '|')       // empty field?
		{
			num = 0;
			while (*tptr != '|')
				item.misc[num++] = *tptr++;
			item.misc[num] = 0;
		}

		#ifdef DEBUG
		fprintf(stderr,"misc: %s\n",item.misc);
		fflush(stderr);
		#endif

		// get text of menu
		// save the text if it 'counts'
		if (*tptr == '|')
			tptr++;             // skip last '|'
		if (can_view(item.acl, item.flags, item.acl_mod, item.flags_mod))
		{
			// Ok.  The person can see the item.  Now check for
			// a menu include
			switch(item.com_type)
			{
				case 15:        // menu include?
					// recursively include menu
					build(item.misc,0);
					break;
				case 16:        // macro definition?
					mptr = macros;
					while (mptr != NULL && mptr->id != item.key)
						mptr = mptr->next;
					// new definition?
					if (mptr == NULL)
					{

						if (mptr = (MacroDef *)malloc(sizeof(MacroDef)), mptr == NULL)
						{
							ap_log("Unable to malloc a new macro record");
							return(-1);
						}
						if (mptr->macro = (char *)malloc(strlen(item.misc)+1), mptr->macro == NULL)
						{
							ap_log("Unable to malloc a new macro");
							return(-1);
						}
						mptr->id = item.key;
						strcpy(mptr->macro,item.misc);
						mptr->next = macros;
						macros = mptr;
					}
					else        // macro redefinition
					{
						realloc((void *)mptr->macro,strlen(item.misc)+1);
						mptr->id = item.key;
						strcpy(mptr->macro,item.misc);
					}
					break;
				case 17:        // macro clear?
					mptr = macros;
					while (mptr->next->id != item.key && mptr != NULL)
						mptr = mptr->next;
					if (mptr == NULL)
					{
						sprintf(tmpstr,"menu: attempted to clear undefined macro %c in %s",item.key, path);
						// non fatal
						ap_log(tmpstr);
					}
					else
					{
						// remove from list
						mptr2 = mptr->next;
						mptr->next = mptr2->next;
						free(mptr2->macro);
						free(mptr2);
					}
					break;
				default:
					// create new record
					// not null?
					if (item.com_type != 0)
					{
						if (iptr = (MenuItem *)malloc(sizeof(MenuItem)), iptr == NULL)
						{
							ap_log("Unable to malloc a new MenuItem");
							return(-1);
						}
						// quick copy
						memcpy((void *)iptr,(void *)&item,sizeof(MenuItem));
						// now add to list
						iptr->next = items;
						items = iptr;
					}
					// now save the text (if it exists)
					if (*tptr != 0)
					{
						if (lptr = (MenuLine *)malloc(sizeof(MenuLine)), lptr == NULL)
						{
							ap_log("Unable to malloc a new MenuItem");
							return(-1);
						}
						if (lptr->line = (char *) malloc(strlen(tptr)+1), lptr->line == NULL)
						{
							ap_log("Unable to malloc a new Menu line");
							return(-1);
						}
						// copy line text
						strcpy(lptr->line, tptr);
						lptr->next = NULL;
						// now add to end of list
						// trivial case
						if (lines == NULL)
							lines = lptr;
						else
						{       // add to end of list
							lptr2 = lines;
							while (lptr2->next != NULL)
								lptr2 = lptr2->next;
							lptr2->next = lptr;
						}
					}
			}
		}
	}
	bclose(infile);
	return(0);
};


// Function:   can_view
// Purpose:    return true if the user has access based on the information
//             passed in
// Input:  acl - the access level of the user
//         flags - the flags of the user
//         am - the access level modifier for the user's acccess level
//         fm - the flags modifier
// Output: true if user has access
// Author: Greg Shaw
// Created:    7/27/93

int Menu::can_view(int acl, int flags, char am, char fm)
{
	int    uacl;                // user's acl
	int uflags;                 // user's flags

	uacl = user.u_acl();
	uflags = user.u_flags();
	if (!((uacl >= acl && am == '>' ) || (uacl < acl && am == '<') ||
		(acl == uacl && am == '=')))
	return(0);                  // no access
	if (flags != 0)
	{
		if ((flags & uflags) && fm == '=')
			return(1);
		if (!(flags & uflags) && fm == '!')
			return(1);
		return(0);
	}
	return(1);                  // access
};


// Function:   clean_storage
// Purpose:    clean up dynamic storage
// Input:      none
// Output:     all dynamic storage is freed
// Author:     Greg Shaw
// Created:        1/2/95

int Menu::clean_storage()
{
	MenuLine   *lptr;
	MenuItem   *iptr;
	MacroDef   *mptr;

	// nuke the menu's text display lines
	while (lines != NULL)
	{
		lptr = lines;
		lines = lines->next;
		free(lptr->line);
		free(lptr);
	}
	// nuke the menu commands
	while (items != NULL)
	{
		iptr = items;
		items = items->next;
		free(iptr);
	}
	// nuke the macro definitions
	while (macros != NULL)
	{
		mptr = macros;
		macros = macros->next;
		free(mptr->macro);
		free(mptr);
	}
	return(0);
};


// Function:       dump
// Purpose:        dump the contents of the menu object
// Input:      none
// Author:     Greg Shaw
// Created:        1/2/95

void Menu::dump(void)
{
	MenuLine   *lptr;
	MenuItem   *iptr;
	MacroDef   *mptr;
	char   tmpstr[255];

	sstrcr_c("Lines: ");
	lptr = lines;
	while (lptr != NULL)
	{
		sprintf(tmpstr,"line: %s",lptr->line);
		sstrcr_c(tmpstr);
		lptr = lptr->next;
	}
	waitcr();
	cr();
	sstrcr_c("Commands: ");
	iptr = items;
	while (iptr != NULL)
	{
		sprintf(tmpstr,"com: t:%d k:%c misc:%s acl: %d am: %c flags: %x fm: %c",
		iptr->com_type,iptr->key,iptr->misc, iptr->acl,
		iptr->acl_mod, iptr->flags, iptr->flags_mod);
		sstrcr_c(tmpstr);
		iptr = iptr->next;
	}
	waitcr();
	cr();
	sstrcr_c("Macros: ");
	mptr = macros;
	while (mptr != NULL)
	{
		sprintf(tmpstr,"macro: k: %c m: %s",mptr->id, mptr->macro);
		sstrcr_c(tmpstr);
		mptr = mptr->next;
	}
	waitcr();

}


// Function:       macro_sub
// Purpose:        substitute macro contents for macro
// Input:      pointer to character pointer
// Output:     (internal object changes)
// Author:     Greg Shaw
// Created:    1/2/95
// Notes:  The pointer passed in *MUST* be malloced rather than a
//     standard character array.  It will probably be resized due
//     to the replacement of 3 characters with a macro.

int Menu::macro_sub(char **line)
{
	// temporary string
	char       tmpstr[MAXPATHLEN+1];
	char   *cptr,*cptr2;
	char   *c;
	char   *newstr;
	MacroDef   *mptr;           // macro list pointer
	static level = 0;

	if (level > MAX_MACRO_LEVEL)// infinite recurse?
	{
		ap_log("menu: macro too complex (too many levels)");
		return(0);
	}
	// macro def found?
	if (cptr = strstr(*line,"\\m"), cptr != NULL)
	{
		// find macro
		mptr = macros;
		while (mptr != NULL && mptr->id != *(cptr+2))
			mptr = mptr->next;
		if (mptr == NULL)       // not found?
		{
			sprintf(tmpstr,"Unable to find macro %c for line %s",*cptr+2,*line);
			ap_log(tmpstr);
			return(0);
		}
		// malloc space for new definition
		if (newstr = (char *)malloc(strlen(*line)+strlen(mptr->macro)+1), newstr == NULL)
		{
			ap_log("Unable to malloc space for expanded line");
			return(0);

		}
		// copy up to definition
		cptr2 = newstr;
		for (c=*line; c != cptr; c++)
			*cptr2++ = *c;
		*cptr2 = 0;             // add null
		// add macro
		strcat(newstr,mptr->macro);
		if (*(cptr+3) != 0)     // is there anything after macro?
			// tack on rest of line
			strcat(newstr,(cptr+3));
		macro_sub(&newstr);     // recursively look for other macros
		// string too small
		if (strlen(*line) < strlen(newstr))
			// for enhanced line?
			// hey, it could happen!
		{
				// realloc line
				c = (char *)realloc(*line,strlen(newstr)+1);
			if (c == NULL)
			{
				ap_log("Unable to realloc macro expanded line");
				return(-1);
			}
			*line = c;
			strcpy(*line,newstr);
			return(0);
		}
	}
	return(0);
}


// Function:   constructor
// Purpose:    initialize values to sane states
// Input:  none
// Output: (internal object changes)
// Author: Greg Shaw
// Created:    7/22/93

Menu::Menu()
{
	prompt = userprompt();
	menu_path[0] = 0;           // erase menu path
	lines = NULL;               // no lines yet
	items = NULL;               // no items yet
	macros = NULL;              // no macros yet
};


// Function:   destructor
// Purpose:    clean up dynamic access
// Input:      none
// Output:     (internal object changes)
// Author:     Greg Shaw
// Created:        1/2/95

Menu::~Menu()
{
	clean_storage();
};


// Function:   run
// Purpose:    run the menu
// Input:  (from user)
// Output: the command that has been selected by the user is returned
// Author: Greg Shaw
// Created:    7/25/93
// Rewritten:  1/2/95

MenuItem *Menu::run()
{
	char *cptr;                 // char ptr
	char *bcast;		    // broadcast/message
	char c;                     // char
	MenuLine   *lptr;           // pointer into line
	MenuItem   *iptr;           // item pointer
	char valkeys[150];          // valid keys in menu
	char timeleft[50];          // amount of time left to user
	int    inactivity;          // how long to wait before inactivity logoff
	long    tl;                 // timeleft
	time_t now;                 // current time
	time_t rightnow;            // current time
	char   bsstr[] =            // backspace
	{
		0x8,0x20,0x8,0x0
	};


	clear_scr();                // clear screen if possible
	lptr = lines;
	while (lptr != NULL)
	{
		sstrcr_c(lptr->line);   // send out line
		// check for char and return true
		if (c = valid_char(), c > 0 || c == HIT_RETURN )
		{
			if (c == HIT_RETURN)// he hit return - re-display menu
				return(NULL);
			else                // he hit something valid
			{
				iptr = items;
				while (iptr->key != c && iptr != NULL)
					iptr = iptr->next;
				if (iptr == NULL)
				{
					ap_log("menu: valid_char sent back invalid character");
				}
				else
				{               // send back command
					return(iptr);
				}

			}
		}
		lptr = lptr->next;
	}
	// ok, he didn't hit anything.  Give him a prompt.
	if (showcoms())             // show command keys?
	{                           // build string with valid keys
		cptr = valkeys;
		*cptr++ = '[';
		iptr = items;
		while (iptr != NULL)
		{
			if (iptr->key != 0)
			{
				*cptr++ = iptr->key;
				*cptr++ = ',';
			}
			iptr = iptr->next;
		}
		*cptr++ = ']';          // add right bracket
		*cptr++ = ' ';          // add trailing space
		*cptr = 0;              // add null
	}
	else
		valkeys[0] = 0;
	time(&now);
	// see how much time he has left
	tl = (user.u_timelimit()*60 - (now - user.user_logon()))/60;
	// subtract time used prior to this logon
	tl -= user.prevtimeused();  // subtract previous time used
	if (user.credituploads())
		tl += user.credit();    // add credited minutes
	if (tl <= 0)                // should he be gone?
	{
		items[0].com_type = -1;     // boot the user
		return(items);
	}
	if (showtime())             // show time left?
	{
		if (tl == 1)
			strcpy(timeleft,gstrl("MINUTESLEFT1"));
		else if (tl < 1)
			strcpy(timeleft,gstrl("MINUTESLEFT2"));
		else
			sprintf(timeleft,gstrl("MINUTESLEFT3"),tl);
		sstr_c(timeleft);
	}
	if (prompt != NULL)
	{
		sstr_c(prompt);
	}
	sstr_c(valkeys);
	inactivity = inactivity_timeout();
	// check for char and return true
	while (c = valid_char(), c < 0 )
	{
		time(&rightnow);        // look for inactivity timeout
		if ((rightnow - now) /60 > inactivity)
		{
			sstrcrl("INACTIVITYTIMEOUT");
			// boot the user
			items[0].com_type = -1;
			ap_log(gstrl("INACTIVITYTIMEOUT"));
			return(&items[0]);
		}
		// check for broadcast/chat
                if (bcast = chatobj.check_broadcast(), bcast != NULL)
                {
                                // private request?
                        if (!chatobj.check_private(bcast,0))
                        {                   // nope, regular message
                                clear_scr();
                                sstrcrl("MESSAGERECEIVED");
                                cr();
                                sstrcr(bcast);
                                cr();
                                cr();
                                waitcr();
				return(NULL);	// restart menu
                        }
                }
		if (c == HIT_WRONG_CHAR)// he typed something, but it didn't count
		{                       // so erase what he typed
			sstr_c(bsstr);
		}
		if (c == HIT_RETURN)    // hit return
			return(NULL);
	}
	cr();
	iptr = items;               // return the key he hit
	while (iptr->key != c && iptr != NULL)
		iptr = iptr->next;
	if (iptr == NULL)
	{
		ap_log("menu: valid_char sent back invalid character");
		return(NULL);
	}
	else
	{                           // send back command
		return(iptr);
	}
};


// Function:   valid_char
// Purpose:    look for a character from the user.  If available, check
//             char against list, return array index into list if found.
// Input:      none
// Output:     the array index of the matching command record
// Author:     Greg Shaw

char Menu::valid_char(void)
{
	char   c;
	MenuItem   *iptr;           // menu item

	c  = 0;
	if (char_avail(0,0) || char_avail(1,1))
		c = gch(0);
	if (c > 0)                  // error or zero for no char
	{
		iptr = items;
		while (iptr != NULL)
		{
			if ( c == iptr->key )
				return(iptr->key);
			else if (c == '\r' || c == '\n')
				return(HIT_RETURN);
			iptr = iptr->next;
		}
		return(HIT_WRONG_CHAR); // he hit something but it wasn't valid
	}
	return(HIT_NOTHING);        // nothing hit
};


#endif                          // _MENU_C_






