//  This may look like C code, but it is really -*- C++ -*-

//  ------------------------------------------------------------------
//  The Goldware Library
//  Copyright (C) 1990-1999 Odinn Sorensen
//  Copyright (C) 1999-2000 Alexander S. Aganichev
//  ------------------------------------------------------------------
//  This library is free software; you can redistribute it and/or
//  modify it under the terms of the GNU Library General Public
//  License as published by the Free Software Foundation; either
//  version 2 of the License, or (at your option) any later version.
//
//  This library 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
//  Library General Public License for more details.
//
//  You should have received a copy of the GNU Library General Public
//  License along with this program; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
//  MA 02111-1307, USA
//  ------------------------------------------------------------------
//  $Id$
//  ------------------------------------------------------------------
//  GCUI: Golded+ Character-oriented User Interface.
//  Pseudo graphical menu.
//  Based on CXL by Mike Smedley.
//  ------------------------------------------------------------------
//  wmenubeg()  starts a menu definition
//  wmenuitem() defines a menu item
//  wmenuend()  ends a menu definition
//  wmenuget()  processes the defined menu
//  ------------------------------------------------------------------

#include <gctype.h>
#include <cstdio>
#include <cstdlib>
#include <gmemdbg.h>
#include <gkbdcode.h>
#include <gkbdbase.h>
#include <gmoubase.h>
#include <gvidall.h>
#include <gwinall.h>
#include <gwinhelp.h>

#if defined(__USE_ALLOCA__)
  #include <malloc.h>
#endif


//  ------------------------------------------------------------------

int _overtagid;
int _finaltagid;

int gmnudropthrough = NO;


//  ------------------------------------------------------------------
//  prototypes for local functions

static _item_t* down_item(_item_t *curr);
static _item_t* left_item(_item_t *curr);
static _item_t* right_item(_item_t *curr);
static _item_t* up_item(_item_t *curr);


//  ------------------------------------------------------------------
//  menu item movement definitions

#define ITM_LT  0
#define ITM_RT  1
#define ITM_UP  2
#define ITM_DN  3
#define ITM_FR  4
#define ITM_LS  5


//  ------------------------------------------------------------------
//  array of function pointers to some of the item movement functions

static _item_t *(*mnu_funcs[4])(_item_t *) = {
  left_item,
  right_item,
  up_item,
  down_item
};

//  ------------------------------------------------------------------
//  flag used for initial display of menu selections

static int dispdesc=YES;


//  ------------------------------------------------------------------
// this function will calculate the width of the selection bar

static int calc_bar_width(_menu_t *wmenu,_item_t *witem)
{
    register int width;

    width=strlen(witem->str);
    if(wmenu->barwidth) width=wmenu->barwidth;
    return(width);
}


//  ------------------------------------------------------------------
// this function will calculate the window column
// at the center of the given menu item

static int calc_center_item(_item_t *item)
{
    return( ((int)item->wcol) + (strlen(item->str)/2) );
}


//  ------------------------------------------------------------------
//  this function will hide the mouse cursor
//  if mouse cursor mode is on

static void hide_mouse_cursor_mnu(void) {

  #ifdef GOLD_MOUSE
  if(gmou.FreeCursor())
    gmou.HideCursor();
  #endif
}


//  ------------------------------------------------------------------
//  this function will display the mouse
//  cursor if mouse cursor mode is on

static void show_mouse_cursor_mnu(void) {

  #ifdef GOLD_MOUSE
  if(gmou.FreeCursor()) {
    gmou.ShowCursor();
    gmou.SetCursor(0,0xFFFF,((LGREY_|_LGREY)<<8));
  }
  #endif
}


//  ------------------------------------------------------------------
//  this function calls the given function

static void mnu_call_func(VfvCP func) {

    _menu_t *menu;
    int w;
    int err;

    hide_mouse_cursor_mnu();
    menu=gwin.cmenu;
    w=whandle();
    err=whelpush();
    (*func)();
    wactiv(w);
    if(!err) whelpop();
    gwin.cmenu=menu;
    show_mouse_cursor_mnu();
}


//  ------------------------------------------------------------------
// this function will call a menu item's "after" function if it exists

static void call_after(_item_t *citem)
{
    if(citem->after!=NULL)
      mnu_call_func(citem->after);
}


//  ------------------------------------------------------------------
// this function will call a menu item's "before" function if it exists

static void call_before(_item_t *citem)
{
    if(citem->before!=NULL)
      mnu_call_func(citem->before);
}


//  ------------------------------------------------------------------
// this function closes the current menu's window and reactivates
// the menu window open prior to opening the current menu window

static void close_window(int w)
{
    if(!gwin.cmenu->usecurr) {
        hide_mouse_cursor_mnu();
        wclose();
        wactiv(w);
        show_mouse_cursor_mnu();
    }
}


//  ------------------------------------------------------------------
// this function displays a menu selection, using selection bar if specified

static void disp_item(_item_t *witem,int bar)
{
#if defined(__USE_ALLOCA__)
    char *buf = (char *)alloca(sizeof(vatch)*gvid->numcols);
#else
    __extension__ char buf[sizeof(vatch)*gvid->numcols];
#endif
    char ch;
    vattr chattr;
    _wrec_t* whp;
    register const char* p;
    register vatch* ptr=(vatch*)buf;
    int i, textend,width,wcol,found=NO;

    // if mouse cursor is on, temporarily hide it
    hide_mouse_cursor_mnu();

    // initialize width of output and end of text
    p = witem->str;
    width = calc_bar_width(gwin.cmenu,witem);
    textend = gwin.cmenu->textpos+strlen(p)-1;
    wgotoxy(witem->wrow,wcol=witem->wcol);
    if (width > (gvid->numcols-2)) width = gvid->numcols - 2;

    // display separators
    if (witem->fmask & M_SEPAR)
    {
      const int &border = gwin.active->border;
      const int &btype = gwin.active->btype;
      const vattr &attr = gwin.active->loattr;
      vatch line = vcatch(_box_table(btype, 1), attr);

      if (border) *ptr++ = vcatch(_box_table(btype, 9), attr);
      for (i = 0; i < width; i++) *ptr++ = line;
      if (border) *ptr = vcatch(_box_table(btype, 10), attr);

      int row = gwin.active->srow + witem->wrow + border;
      int col = gwin.active->scol + wcol;
      vputws(row, col, (vatch*)buf, width+2);
    }
    else
    {
      // display menu item including selection bar
      for(i=0; i<width; i++) {

        // see if currently in bar region.  if so, then use
        // a space for the character. otherwise use the
        // character from the current position in the string
        ch = ((i<gwin.cmenu->textpos) or (i>textend)) ? ' ' : (*p++);

        // select attribute of character to be displayed based upon if
        // selection bar was specified, if the menu item is non-selectable,
        // if the character is a tag character, or none of the above.
        if(bar)
          chattr = gwin.cmenu->barattr;
        else if(witem->fmask&M_NOSEL)
          chattr = gwin.cmenu->noselattr;
        else if((ch==witem->schar) and not found) {
          found = YES;
          chattr = gwin.cmenu->scharattr;
        }
        else
          chattr = gwin.cmenu->textattr;

        // display character in selected attribute
        //wprintc(witem->wrow,wcol++,chattr,ch);

        // Build menu line buffer

        *ptr++ = vcatch(ch,chattr);
      }

      // Display complete buffer
      wprintws(witem->wrow, wcol, (vatch*)buf, width);
    }

    // display text description, if one exists
    if((witem->desc!=NULL) and dispdesc) {
      whp = wfindrec(witem->dwhdl);
      i = (1 + whp->ecol - whp->scol) - (whp->border ? 2 : 0);
      sprintf(buf, "%-*.*s", i, i, witem->desc);
      wwprints(witem->dwhdl, witem->dwrow, witem->dwcol, witem->dattr, buf);
    }

    // if mouse cursor is hidden, unhide it
    show_mouse_cursor_mnu();
}


//  ------------------------------------------------------------------
// this function finds the next menu selection down

static _item_t * down_item(_item_t *curr)
{
    _item_t *best,*temp;
    int brow,bcol,tcol,trow,crow,ccol,tdist,bdist;

    // initialize best record to NULL
    best=NULL;
    brow=bcol=32767;

    // calculate window column at center of current item
    crow=(int)curr->wrow;
    ccol=calc_center_item(curr);

    // search backwards through linked list, testing each item
    for(temp=gwin.cmenu->item;temp!=NULL;temp=temp->prev) {

        // calculate window column at center of test item
        trow=(int)temp->wrow;
        tcol=calc_center_item(temp);

        if(trow>crow) {
            tdist=abs((ccol-tcol));
            bdist=abs((ccol-bcol));
            if((trow<brow) or ((trow==brow) and (tdist<bdist))) {
                best=temp;
                brow=trow;
                bcol=tcol;
            }
        }
    }

    // if there wasn't an item downwards, then wrap around
    if(best==NULL) {
        if((temp=(_item_t*)throw_malloc(sizeof(_item_t)))==NULL) {
            best=curr;
        }
        else {
            *temp=*curr;
            temp->wrow=-1;
            best=down_item(temp);
            throw_free(temp);
        }
    }
    else
        // see if menu selection is non-selectable
        if(best->fmask&M_NOSEL)
            best=down_item(best);

    // return best record
    return(best);
}


//  ------------------------------------------------------------------
// this function finds the upper-leftmost menu selection

static _item_t * first_item(void)
{
    _item_t *best,*temp;

    // initialize best record to highest record in linked list
    best=gwin.cmenu->item;

    // search backwards through linked list, testing each item
    for(temp=best->prev;temp!=NULL;temp=temp->prev) {
        if((temp->wrow<best->wrow) or ((temp->wrow==best->wrow) and (temp->wcol<best->wcol)))
            best=temp;
    }

    // see if menu selection is non-selectable
    if(best->fmask&M_NOSEL) best=right_item(best);

    // return best record
    return(best);
}


//  ------------------------------------------------------------------
// this is a recursive function that frees a menu and all of its submenus

static void free_menu(_menu_t *wmenu)
{
    _item_t *witem;

    // free all items in menu, including sub-menus
    while(wmenu->item!=NULL) {
        if(wmenu->item->child!=NULL) free_menu((_menu_t*)wmenu->item->child);
        witem=wmenu->item->prev;
        throw_free(wmenu->item);
        wmenu->item=witem;
        if(wmenu->item!=NULL) wmenu->item->next=NULL;
    }

    // free the menu itself
    throw_free(wmenu);
}


//  ------------------------------------------------------------------
// this function will find the bottom-rightmost menu selection

static _item_t * last_item(void)
{
    _item_t *best,*temp;
    int bcol;

    // initialize best record to highest record in linked list
    best=gwin.cmenu->item;
    bcol=best->wcol;

    // search backwards through linked list, testing each item
    for(temp=best->prev;temp!=NULL;temp=temp->prev) {
        if((temp->wrow>best->wrow) or ((temp->wrow==best->wrow) and (temp->wcol>bcol))) {
            best=temp;
            bcol=best->wcol;
        }
    }

    // see if menu selection is non-selectable
    if(best->fmask&M_NOSEL) best=left_item(best);

    // return best record
    return(best);
}


//  ------------------------------------------------------------------
// this function is called after a menu bar move

static void post_move(_item_t *citem)
{
    gwin.cmenu->citem=citem;
    gwin.help=citem->help;
    disp_item(citem,1);
    call_before(citem);
}


//  ------------------------------------------------------------------
// this function prepares for a menu bar move

static void pre_move(_item_t *citem)
{
    disp_item(citem,0);
    call_after(citem);
}


//  ------------------------------------------------------------------
// this function moves the selection bar to another menu item,
// automatically taking care of necessary pre/post move actions

static _item_t * goto_item(_item_t *citem,int which)
{
    _item_t *item;

    if(which==ITM_FR)       item=first_item();
    else if(which==ITM_LS)  item=last_item();
    else                    item=(*mnu_funcs[which])(citem);
    if(item!=citem) {
        pre_move(citem);
        post_move(citem=item);
    }
    return(citem);
}


//  ------------------------------------------------------------------
// this function will find the menu selection to the left of current

static _item_t * left_item(_item_t *curr)
{
    _item_t *best,*temp;
    int wwidth,bpos,tpos,cpos;

    // calculate window width and current position
    wwidth=gwin.cmenu->ecol - gwin.cmenu->scol + 1;
    cpos=(curr->wrow * wwidth) + curr->wcol;

    // initialize best record to NULL, and best position to -1
    best=NULL;
    bpos=-1;

    // search backwards through linked list, testing each item
    for(temp=gwin.cmenu->item;temp!=NULL;temp=temp->prev) {

        // calculate position of test each item
        tpos=(temp->wrow*wwidth) + temp->wcol;

        // compare position of test item with best item, current item
        if((tpos>bpos) and (tpos<cpos)) {
            best=temp;
            bpos=tpos;
        }
    }

    // if there wasn't a item to the left, then wrap around
    if(best==NULL)
        best=last_item();
    else
        // see if menu selection is non-selectable
        if(best->fmask&M_NOSEL)
            best=left_item(best);

    // return best record
    return(best);
}


//  ------------------------------------------------------------------
// this function determines if the mouse cursor is on a menu item

#ifdef GOLD_MOUSE
static _item_t *mouse_on_item(_menu_t *menu,int mcrow,int mccol)
{
    int srow,scol,border,start,end;
    _item_t *item,*found;

    found  = NULL;
    srow   = menu->srow;
    scol   = menu->scol;
    border = menu->btype==5?0:1;
    for(item=menu->item;item!=NULL;item=item->prev) {
        if(mcrow==(srow+border+item->wrow)) {
            start = scol+border+item->wcol;
            end   = start+calc_bar_width(menu,item)-1;
            if((mccol>=start) and (mccol<=end)) {
                found=item;
                break;
            }
        }
    }
    return(found);
}
#endif


//  ------------------------------------------------------------------

static void pre_exit(int w,int close)
{
    _menu_t *wmenu;

    hide_mouse_cursor_mnu();

    // if not using current window for menu, then close it
    if(close) close_window(w);

    // if at highest menu then free the whole menu structure
    if(gwin.cmenu==gwin.menu) {
        wmenu=gwin.menu->prev;
        if(gwin.cmenu!=NULL)
          free_menu(gwin.cmenu);
        gwin.menu=wmenu;
        if(gwin.menu!=NULL)
          gwin.menu->next=NULL;
        gwin.cmenu=gwin.menu;
    }
}


//  ------------------------------------------------------------------
// this function reads the mouse for input

static gkey read_mouse(_item_t* citem) {

  #ifdef GOLD_MOUSE
  register _item_t *item;

  // if free-floating mouse cursor support is on
  if(gmou.FreeCursor()) {

    // clear mouse button queue
    gmou.ClearEvents();

    // loop until a key is pressed
    while(!kbxhit() and gkbd.kbuf==NULL) {

      // call the keyboard loop function
      //if(gkbd.kbloop!=NULL)
        //(*gkbd.kbloop)();

      // if left button was pressed, and mouse cursor is on
      // a selectable menu item, then move selection bar to
      // that item, and select it.  If mouse cursor is on
      // a main menu item of a pull-down menu system, then
      // stuff that item's selection character into the CXL
      // keyboard buffer and return Esc to close the current
      // pull-down menu.
      gmou.GetLeftRelease();
      if(gmou.Count()) {
        item = mouse_on_item(gwin.cmenu,gmou.Row(),gmou.Column());
        if(item == NULL) {
          if(gwin.cmenu->menutype&M_PD) {
            item = mouse_on_item(gwin.cmenu->parent,gmou.Row(),gmou.Column());
            if(item and (!(item->fmask&M_NOSEL))) {
              kbput(item->schar);
              return Key_Esc;
            }
          }
        }
        else {
          if(!(item->fmask&M_NOSEL)) {
            if(citem != item) {
              pre_move(citem);
              gwin.cmenu->citem = citem = item;
              post_move(item);
            }
            return Key_Ent;
          }
        }
      }

      // if right button was pressed, simulate pressing the Esc key
      gmou.GetRightRelease();
      if(gmou.Count())
        return Key_Esc;
    }
  }
  #endif

  // return zero - it means a key was pressed
  return 0;
}


//  ------------------------------------------------------------------
// this function will find the menu selection to the right of current

static _item_t * right_item(_item_t *curr)
{
    _item_t *best,*temp;
    int wwidth,bpos,tpos,cpos;

    // calculate window width and current position
    wwidth=gwin.cmenu->ecol - gwin.cmenu->scol + 1;
    cpos=(curr->wrow * wwidth) + curr->wcol;

    // initialize best record to NULL, and best position to 32767
    best=NULL;
    bpos=32767;

    // search backwards through linked list, testing items
    for(temp=gwin.cmenu->item;temp!=NULL;temp=temp->prev) {

        // calculate position of test item
        tpos=(temp->wrow*wwidth) + temp->wcol;

        // compare position of test item with best item, current item
        if((tpos<bpos) and (tpos>cpos)) {
            best=temp;
            bpos=tpos;
        }
    }

    // if there wasn't a item to the right, then wrap around
    if(best==NULL)
        best=first_item();
    else
        // see if menu selection is non-selectable
        if(best->fmask&M_NOSEL)
            best=right_item(best);

    // return best record
    return(best);
}


//  ------------------------------------------------------------------
// this function finds the previous menu selection upwards

static _item_t * up_item(_item_t *curr)
{
    _item_t *best,*temp;
    int brow,bcol,tcol,crow,trow,ccol,tdist,bdist;

    // initialize best record to NULL
    best = NULL;
    brow = -1;
    bcol = 32767;

    // calculate window column at center of current item
    crow=(int)curr->wrow;
    ccol=calc_center_item(curr);

    // search backwards through linked list, testing items
    for(temp=gwin.cmenu->item;temp!=NULL;temp=temp->prev) {

        // calculate window column at center of test item
        trow=(int)temp->wrow;
        tcol=calc_center_item(temp);

        if(trow<crow) {
            tdist=abs(ccol-tcol);
            bdist=abs(ccol-bcol);
            if((trow>brow) or ((trow==brow) and (tdist<bdist))) {
                best=temp;
                brow=trow;
                bcol=tcol;
            }
        }
    }

    // if there wasn't a item to the left, then wrap around
    if(best==NULL) {
        if((temp=(_item_t*)throw_malloc(sizeof(_item_t)))==NULL) {
            best=curr;
        }
        else {
            *temp=*curr;
            temp->wrow=255;
            best=up_item(temp);
            throw_free(temp);
        }
    }
    else
        // see if menu selection is non-selectable
        if(best->fmask&M_NOSEL)
            best=up_item(best);

    // return best record
    return(best);
}


//  ------------------------------------------------------------------

int wmenubeg(int srow, int scol, int erow, int ecol, int btype, vattr battr, vattr wattr, VfvCP open, int menutype) {

    _menu_t* wmenu;

    // if this is a submenu, then make sure that it is under a menu item
    if(gwin.mlevel>gwin.ilevel) return(gwin.werrno=W_NOITMDEF);

    // allocate memory for new menu record
    if((wmenu=(_menu_t*)throw_malloc(sizeof(_menu_t)))==NULL)
        return(gwin.werrno=W_ALLOCERR);

    // if menu is not a submenu, make it a new base menu record
    if(!gwin.mlevel) {
        if(gwin.menu!=NULL) gwin.menu->next=wmenu;
        wmenu->prev=gwin.menu;
        wmenu->next=NULL;
        wmenu->parent=NULL;
        gwin.menu=gwin.cmenu=wmenu;
    }
    else {
        if(menutype & M_PD)
          gwin.cmenu->item->fmask |= M_HASPD;   // Has a pull-down menu
        gwin.cmenu->item->fmask &= ~M_CLOSE;    // Don't close parent menu
        wmenu->parent=gwin.cmenu;
        gwin.cmenu = (_menu_t*)(gwin.cmenu->item->child = wmenu);
    }

    // save info in menu record
    wmenu->srow=srow;
    wmenu->scol=scol;
    wmenu->erow=erow;
    wmenu->ecol=ecol;
    wmenu->btype=btype;
    wmenu->battr=battr;
    wmenu->loattr=battr;
    wmenu->sbattr=battr;
    wmenu->wattr=wattr;
    wmenu->menutype=menutype;
    wmenu->open=open;
    wmenu->usecurr=NO;
    wmenu->item=NULL;
    wmenu->title = "";
    wmenu->titlepos = -1;
    wmenu->titleattr = BLACK_|_BLACK;
    wmenu->shadattr = DEFATTR;
    wmenu->items = 0;

    // increment menu level
    gwin.mlevel++;

    // return normally
    return(gwin.werrno=W_NOERROR);
}


//  ------------------------------------------------------------------

int wmenuitem(int wrow, int wcol, const char* str, char schar, int tagid, int fmask, VfvCP select, gkey hotkey, int help) {

    _item_t* witem;

    // make sure that wmenubeg() or wmenubegc() has been called
    if(!gwin.mlevel)
      return (gwin.werrno=W_NOMNUBEG);

    // allocate memory for new item record
    witem = (_item_t*)throw_malloc(sizeof(_item_t));
    if(witem==NULL)
      return (gwin.werrno=W_ALLOCERR);

    // add new menu item record to linked list
    if(gwin.cmenu->item)
      gwin.cmenu->item->next = witem;
    witem->prev = gwin.cmenu->item;
    witem->next = NULL;
    gwin.cmenu->item = witem;

    // save info in item record
    witem->wrow   = wrow;
    witem->wcol   = wcol;
    witem->str    = str;
    witem->schar  = schar;
    witem->tagid  = tagid;
    witem->fmask  = fmask;
    witem->select = select;
    witem->hotkey = hotkey;
    witem->desc   = NULL;
    witem->dwhdl  = -1;
    witem->dwrow  = 0;
    witem->dwcol  = 0;
    witem->dattr  = BLACK_|_BLACK;
    witem->redisp = NO;
    witem->help   = help;
    witem->child  = NULL;
    witem->before = NULL;
    witem->after  = NULL;

    gwin.cmenu->items++;

    int border = (gwin.cmenu->btype == 5) ? 0 : 1;

    int width = 1 + (gwin.cmenu->ecol-border) - (gwin.cmenu->scol+border);
    size_t _titlen = gwin.cmenu->title ? strlen(gwin.cmenu->title) : 0;
    size_t _strlen = strlen(str);
    size_t length = maximum_of_two(_strlen, _titlen);
    if((int)length > width)
      gwin.cmenu->ecol += length - width;

    if(gwin.cmenu->menutype & M_VERT) {
      int height = 1 + (gwin.cmenu->erow-border) - (gwin.cmenu->srow+border);
      if(gwin.cmenu->items > height)
        gwin.cmenu->erow += gwin.cmenu->items - height;
    }

    // set item level == menu level
    gwin.ilevel = gwin.mlevel;

    // return normally
    return (gwin.werrno=W_NOERROR);
}


//  ------------------------------------------------------------------

int wmenuend(int taginit, int menutype, int barwidth, int textpos, vattr textattr, vattr scharattr, vattr noselattr, vattr barattr) {

    _item_t* item;
    int w_width, border, found;

    // make sure at least 1 menu item has been defined
    if(not gwin.mlevel or (gwin.mlevel>gwin.ilevel))
        return(gwin.werrno=W_NOITMDEF);

    // make sure that the specified initial tagid matches the
    // tagid of one of the defined menu items in this menu
    for(found=NO,item=gwin.cmenu->item;item!=NULL;item=item->prev) {
        if(item->tagid==taginit) {
            found=YES;
            break;
        }
    }
    if(!found) return(gwin.werrno=W_INVTAGID);

    // if bar width > window width, then bar width = window width
    border=(gwin.cmenu->btype==5)?0:1;
    w_width=(gwin.cmenu->ecol-border)-(gwin.cmenu->scol+border)+1;
    if(barwidth>w_width) barwidth=w_width;

    // add rest of menu information to menu record
    gwin.cmenu->citem     = NULL;
    gwin.cmenu->tagcurr   = taginit;
    gwin.cmenu->menutype |= menutype;
    gwin.cmenu->barwidth  = barwidth;
    gwin.cmenu->textpos   = barwidth?textpos:0;
    gwin.cmenu->textattr  = textattr;
    gwin.cmenu->scharattr = scharattr;
    gwin.cmenu->noselattr = noselattr;
    gwin.cmenu->barattr   = barattr;

    // set current menu pointer to parent menu
    if((gwin.cmenu=gwin.cmenu->parent)==NULL) gwin.cmenu=gwin.menu;

    // decrement menu and item levels
    gwin.mlevel--;
    gwin.ilevel--;

    // return with no error
    return(gwin.werrno=W_NOERROR);
}


//  ------------------------------------------------------------------

int wmenuget() {

    int valid, pulldown=NO, menuerr, winerr, err;
    _item_t* citem;
    _item_t* item;
    int w = -1;
    gkey xch;
    char ch;
    static int _depth = 0;

    if (_depth == 0)
    {
      _overtagid = -1;
      _finaltagid = -1;
    }

    // make sure we have a defined menu
    if(gwin.cmenu==NULL) {
        gwin.werrno=W_NOMNUDEF;
        return(-1);
    }

    // make sure that wmenuend() was called
    if(gwin.mlevel or gwin.ilevel) {
        gwin.werrno=W_NOMNUEND;
        return(-1);
    }

    // open menu's window (unless menu is to use current window)
    if(!gwin.cmenu->usecurr) {
        w=whandle();
        hide_mouse_cursor_mnu();
        if(!wopen(gwin.cmenu->srow,gwin.cmenu->scol,gwin.cmenu->erow,gwin.cmenu->ecol,gwin.cmenu->btype,gwin.cmenu->battr,gwin.cmenu->wattr,gwin.cmenu->sbattr,gwin.cmenu->loattr))
          return -1;
        if(gwin.cmenu->shadattr != DEFATTR)
          wshadow(gwin.cmenu->shadattr);
        if(gwin.cmenu->title and *gwin.cmenu->title)
          wtitle(gwin.cmenu->title, gwin.cmenu->titlepos, gwin.cmenu->titleattr);
        show_mouse_cursor_mnu();

        // save environment info, call menu open
        // function, then restore environment info
        if(gwin.cmenu->open!=NULL)
          mnu_call_func(gwin.cmenu->open);
    }

    // if mouse cursor mode is on, show cursor
    show_mouse_cursor_mnu();

    // find first item in linked list
    if((item=gwin.cmenu->item)!=NULL) for(;item->prev!=NULL;item=item->prev);

    // display all menu items
    dispdesc=NO;
    valid=NO;
    while(item!=NULL) {
        disp_item(item,0);
        if(!(item->fmask&M_NOSEL)) valid=YES;
        item=item->next;
    }
    dispdesc=YES;

    // make sure there is at least 1 selectable menu item
    if(!valid) {
        pre_exit(w,YES);
        gwin.werrno=W_NOSELECT;
        return(-1);
    }

    // search linked list of item records for the item matching
    // the last item.  If there was no last item, search for
    // the initial tag ID position.  If no initial tag ID position
    // was specified, then find the upper-leftmost menu item.
    citem=NULL;
    if(gwin.cmenu->menutype&M_SAVE) {
        for(citem=gwin.cmenu->item;citem!=NULL;citem=citem->prev) {
            if((gwin.cmenu->citem==citem) and not (citem->fmask&M_NOSEL))
                break;
        }
    }
    if(citem==NULL) {
        citem=wmenuifind(gwin.cmenu->tagcurr);
        if((citem==NULL) or (citem->fmask&M_NOSEL)) citem=first_item();
    }

    // call the current menu item's 'before'
    // function and display current menu item
    post_move(citem);

    // main process of function

    for(;;) {

        // update current menu item and help category
        gwin.cmenu->citem=citem;
        gwin.help=citem->help;

        // read mouse/keyboard for keypress, then test the keypress
        gwin.menu->hotkey = false;
        gkbd.inmenu=true;
        xch = read_mouse(citem);
        citem=gwin.cmenu->citem;
        if(!xch) {
          xch = getxch();
        }
        gkbd.inmenu=false;

        _overtagid = citem->tagid;

        switch(xch) {

            case Key_Esc:

                // if Esc checking is on, erase selection bar,
                // free menu records and return to caller
                if(gwin.esc or (gwin.cmenu!=gwin.menu)) {
                    ESCAPE_KEY:
                    pre_move(citem);
                    pre_exit(w,YES);
                    gwin.werrno=W_ESCPRESS;
                    return(-1);
                }
                break;

            case Key_Home:

                // hide selection bar and find upper-leftmost menu item
                citem=goto_item(citem,ITM_FR);
                break;

            case Key_Lft:

                // if current menu is a pull-down menu,
                // then erase selection bar, free menu records,
                // and return special code to caller
                if(gwin.cmenu->menutype&M_PD) {
                    pre_move(citem);
                    pre_exit(w,YES);
                    gwin.werrno=W_NOERROR;
                    return(M_PREVPD);
                }

                // if current menu is a horizontal menu, then hide
                // selection bar and find menu item to the left
                if(gwin.cmenu->menutype&M_HORZ)
                    citem=goto_item(citem,ITM_LT);

                // if pull-down menu flag is set, select this item
                if(pulldown and (citem->child!=NULL))
                    goto ENTER;
                break;

            case Key_Up:

                // if current menu is a vertical menu, then hide
                // selection bar and find menu item upwards
                if(gwin.cmenu->menutype&(M_VERT|M_PD))
                    citem=goto_item(citem,ITM_UP);
                break;

            case Key_Rgt:

                // if current menu is a pull-down menu,
                // then erase selection bar, free menu records,
                // and return special code to caller
                if(gwin.cmenu->menutype&M_PD) {
                    pre_move(citem);
                    pre_exit(w,YES);
                    gwin.werrno=W_NOERROR;
                    return(M_NEXTPD);
                }

                // if current menu is a horizontal menu, then hide
                // selection bar and find menu item to the right
                if(gwin.cmenu->menutype&M_HORZ)
                    citem=goto_item(citem,ITM_RT);

                // if pulldown flag is set, then select this item
                if(pulldown and (citem->child!=NULL))
                    goto ENTER;
                break;

            case Key_Dwn:

                // if current item has a pull-down menu, select it
                if(citem->fmask&M_HASPD)
                    goto ENTER;

                // if current menu is a vertical menu, then hide
                // selection bar and find menu item downwards
                if(gwin.cmenu->menutype&(M_VERT|M_PD))
                    citem=goto_item(citem,ITM_DN);
                break;

            case Key_End:

                // hide selection bar and find
                // lower-rightmost menu item
                citem=goto_item(citem,ITM_LS);
                break;

            case Key_Ent:

                ENTER:

                // if current menu item has a pull-down menu
                // attached, then set the pulldown flag
                if(citem->fmask&M_HASPD)
                    pulldown=YES;

                // display menu item with selection bar
                disp_item(citem,1);

                menuerr=0;

                // if current menu item has a child menu
                if(citem->child!=NULL) {

                    // save environment info, process child
                    // menu, then restore environment info
                    gwin.cmenu=(_menu_t*)citem->child;
                    hide_mouse_cursor_mnu();
                    err=whelpush();
                    _depth++;
                    menuerr=wmenuget();
                    _depth--;
                    winerr=gwin.werrno;
                    if(!err) whelpop();
                    show_mouse_cursor_mnu();
                    gwin.cmenu=gwin.cmenu->parent;

                    // if an error was returned by
                    // child menu, free menu records
                    // and pass error code to caller
                    if((menuerr==-1) and (winerr!=W_ESCPRESS)) {
                        call_after(citem);
                        menuerr=winerr;
                        pre_exit(w,YES);
                        gwin.werrno=menuerr;
                        return(-1);
                    }
                }

                // if the M_CLOSB feature is on, then close the menu's
                // window before the selection function is called.
                if(citem->fmask&M_CLOSB) close_window(w);

                // this is used as a flag to see if the selection bar's
                // position has been changed by the select function.
                gwin.cmenu->tagcurr=-1;

                // if current menu item has a select function, call it
                if(citem->select!=NULL)
                  mnu_call_func(citem->select);

                // if the M_CLOSB feature is on, then free the current
                // menu a before the selection function is called.
                if(citem->fmask&M_CLOSB) {
                    call_after(citem);
                    _finaltagid = citem->tagid;
                    pre_exit(w,NO);
                    gwin.werrno=W_NOERROR;
                    return _finaltagid;
                }

                // check all menu items in current menu to
                // see if their redisplay flag has been set
                for(item=gwin.cmenu->item;item!=NULL;item=item->prev) {
                    if(item->redisp) {
                        disp_item(item,(item==citem));
                        item->redisp=NO;
                    }
                }

                // see if selection bar position was changed by select
                // function.  if so, then move selection bar to new item
                if(gwin.cmenu->tagcurr!=-1) {
                    item=wmenuifind(gwin.cmenu->tagcurr);
                    if((item!=NULL) and not (item->fmask&M_NOSEL)) {
                        pre_move(citem);
                        post_move(gwin.cmenu->citem=citem=item);
                        break;
                    }
                }

                // if current menu item has a pull-down attached
                if(citem->fmask&M_HASPD) {

                    // if child menu returned previous pull-down menu
                    // flag, find menu item to the left and select it
                    if(menuerr==M_PREVPD) {
                        citem=goto_item(citem,ITM_LT);
                        if(citem->fmask&M_HASPD) goto ENTER;
                        break;
                    }

                    // if child menu returned next pull-down menu
                    // flag, find menu item to the right and select it
                   if(menuerr==M_NEXTPD) {
                        citem=goto_item(citem,ITM_RT);
                        if(citem->fmask&M_HASPD) goto ENTER;
                        break;
                    }
                }

                // turn off pulldown flag
                pulldown=NO;

                // if child menu returned an exit-all-menus flag,
                // then free menu records and pass exit-all-menus
                // flag onto caller
                if((menuerr==M_EXITALL) or (citem->fmask&M_CLALL)) {
                    disp_item(citem,1);
                    call_after(citem);
                    if(citem->fmask&M_CLALL)
                        if(_finaltagid == -1)
                            _finaltagid = citem->tagid;
                    pre_exit(w,YES);
                    gwin.werrno=W_NOERROR;
                    return M_EXITALL;
                }

                // unless an exit-this-menu flag was returned by the
                // child menu, or current item has close-menu
                // specified, free menu records, and return tag
                // identifier of current menu item
                if((citem->child!=NULL) or (citem->select!=NULL))
                    if((menuerr!=M_EXIT) and (not (citem->fmask&M_CLOSE)))
                        break;
                call_after(citem);
                _finaltagid = citem->tagid;
                pre_exit(w,YES);
                gwin.werrno=W_NOERROR;
                return _finaltagid;

            default:

                // separate ASCII code from keypress code, if ASCII
                // code is zero, then it must be an extended key
                ch = char(xch & 0xFF);
                if (!ch and gmnudropthrough)
                {
                    if((xch != Key_PgDn) and (xch != Key_PgUp))
                        kbput(xch);
                    goto ESCAPE_KEY;
                }

                // scan through list of items for one that
                // has a tag identifier matching keypress
                valid=YES;
                item=citem->next;
                for(;;) {
                    while(item!=NULL) {
                        if ((g_toupper(ch)==g_toupper(item->schar)) && !(item->fmask & M_NOSEL))
                        {
                          if (!gwin.menu->hotkey) _overtagid = item->tagid;
                          goto FARBREAK;
                        }
                        if(citem==item) {
                            valid=NO;
                            goto FARBREAK;
                        }
                        item=item->next;
                    }
                    for(item=gwin.cmenu->item; item->prev!=NULL; item=item->prev) {}
                }

                FARBREAK:

                // if a matching tag identifier was found,
                // then hide selection bar, and if quick-key
                // selection is allowed, select the found menu item
                if(valid) {
                    if(item!=citem) {
                        pre_move(citem);
                        post_move(gwin.cmenu->citem=citem=item);
                    }
                    if(!(gwin.cmenu->menutype&M_NOQS)) goto ENTER;
                }
        }
    }
}


//  ------------------------------------------------------------------
//  Starts a menu definition in active window

int wmenubegc() {

  // call wmenubeg() using current window parameters
  if(wmenubeg(gwin.active->srow,
              gwin.active->scol,
              gwin.active->erow,
              gwin.active->ecol,
              gwin.active->btype,
              gwin.active->battr,
              gwin.active->wattr,
              NULL)) {
    return gwin.werrno;
  }

  // turn on use-current-window flag
  gwin.cmenu->usecurr=YES;

  // return normally
  return gwin.werrno=W_NOERROR;
}


//  ------------------------------------------------------------------

static _item_t* search_menu(_menu_t* menu, int tagid) {

  _item_t *witem, *item;

  // do while more items in this menu
  for(witem=menu->item; witem!=NULL; witem=witem->prev) {

    // if tagid of found item matches item we're
    // searching for, then return that item's address
    if(witem->tagid==tagid)
      return witem;

    // if current item has a child menu, search it
    if(witem->child!=NULL) {
      item = search_menu((_menu_t*)witem->child,tagid);
      if(item!=NULL)
        return item;
    }
  }

  // return address of item found
  return witem;
}


//  ------------------------------------------------------------------
//  Finds item record in a menu structure

_item_t* wmenuifind(int tagid) {

  _item_t* item;

  // check for existance of a menu
  if(gwin.cmenu==NULL) {
    gwin.werrno=W_NOMNUDEF;
    return NULL;
  }

  // start search process at root of menu structure
  item = search_menu(gwin.menu,tagid);

  // return to caller
  gwin.werrno = (item==NULL) ? W_NOTFOUND : W_NOERROR;
  return item;
}


//  ------------------------------------------------------------------
//  Adds a text description to menu item

int wmenuitxt(int whdl, int wrow, int wcol, vattr attr, const char* str) {

  // make sure at least 1 menu item has been defined
  if(!gwin.mlevel or gwin.mlevel>gwin.ilevel)
    return gwin.werrno=W_NOITMDEF;

  // add description info to menu record
  _item_t* citem = gwin.cmenu->item;
  citem->dwhdl = whdl;
  citem->dwrow = wrow;
  citem->dwcol = wcol;
  citem->dattr = attr;
  citem->desc  = str;

  // return normally
  return gwin.werrno=W_NOERROR;
}


//  ------------------------------------------------------------------

int wmenuiba(VfvCP before, VfvCP after) {

  // make sure at least 1 menu item has been defined
  if(!gwin.mlevel or gwin.mlevel > gwin.ilevel)
      return(gwin.werrno=W_NOITMDEF);

  // update menu item record
  gwin.cmenu->item->before = before;
  gwin.cmenu->item->after = after;

  // return normally
  return (gwin.werrno=W_NOERROR);
}


//  ------------------------------------------------------------------

int wmenuidsab(int tagid) {

  struct _item_t *item;

  // check for existance of a menu
  if(gwin.cmenu==NULL)
    return (gwin.werrno=W_NOMNUDEF);

  // find address of requested menu item
  if((item=wmenuifind(tagid))==NULL)
    return gwin.werrno;

  // disable menu item by making it nonselectable
  item->fmask |= M_NOSEL;

  // set menu item's redisplay flag
  item->redisp = true;

  // return normally
  return (gwin.werrno=W_NOERROR);
}


//  ------------------------------------------------------------------

int wmenuienab(int tagid) {

  struct _item_t *item;

  // check for existance of a menu
  if(gwin.cmenu==NULL)
    return (gwin.werrno=W_NOMNUDEF);

  // find address of requested menu item
  if((item=wmenuifind(tagid))==NULL)
    return gwin.werrno;

  // enable menu item by making it selectable
  item->fmask &= (~M_NOSEL);

  // set menu item's redisplay flag
  item->redisp = true;

  // return normally
  return (gwin.werrno=W_NOERROR);
}


//  ------------------------------------------------------------------

int wmenuinext(int tagid) {

  //  see if tagid is valid
  if(wmenuifind(tagid)==NULL)
    return gwin.werrno;

  // set current menu's current tagid to input tagid
  wmenumcurr()->tagcurr = tagid;

  // return normally
  return (gwin.werrno=W_NOERROR);
}


//  ------------------------------------------------------------------
