/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */
/* 
 * Copyright 1999-2002 by Eric House (fixin@peak.org).  All rights reserved.
 *
 * This program 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
 * of the License, or (at your option) any later version.
 *
 * This program 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; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#define TEST_CPP 1

#include <assert.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <ctype.h>
#include <timer.h> /* for time_get_onOS */
#include <sys/time.h>

#include "sys.h"
#include "gui.h"
#include "OpenDatabases.h"
#include "comms.h"		/* for CHANNEL_NONE */
#include "LocalizedStrIncludes.h"
//  #include "FieldMgr.h"

#include "ebm_object.h"

extern "C" {
#include <evnt_fun.h> /* jonathan_yavner@franklin.com says put in extern "C" */
#include "xptypes.h"
#include "game.h"
#include "vtabmgr.h"
#include "dictnry.h"
#include "util.h"
#include "memstream.h"
#include "strutils.h"
}

#include "frankmain.h"
#include "frankdraw.h"
#include "frankdict.h"
#include "frankpasswd.h"
#include "frankdlist.h"
#include "frankshowtext.h"
/* #include "frankask.h" */
#include "frankletter.h"
#include "frankplayer.h"
#include "frankgamesdb.h"
#include "franksavedgames.h"
#include "bmps_includes.h"
/* #include "browser.h" */

extern "C" {
#include "lcd.h"
#include "ereader_hostio.h"
}

#include "frankids.h"
class CXWordsWindow;

enum { HINT_REQUEST, SERVER_TIME_REQUEST, NEWGAME_REQUEST, 
       FINALSCORE_REQUEST };

CXWordsWindow *MainWindow;
/* CLabel *Repeat_Label; */
CMenuBar MainMenuBar( MENUBAR_WINDOW_ID, 23 );
/* CPushButton *Edit_Button; */

/* Addrtest variables */
/* class CRecordWindow; */
/* COpenDB DBase; */
//  CFMDatabase FMgr;
/* CRecordWindow *AddrWindow; */


/* callbacks */
static VTableMgr* frank_util_getVTManager( XW_UtilCtxt* uc );
static DictionaryCtxt* frank_util_makeEmptyDict( XW_UtilCtxt* uc );
static void frank_util_userError( XW_UtilCtxt* uc, UtilErrID id );
static XP_U16 frank_util_userQuery( XW_UtilCtxt* uc, UtilQueryID id,
				    XWStreamCtxt* stream );
static void frank_util_askBlankFace( XW_UtilCtxt* uc, DictionaryCtxt* dict,
				     XP_UCHAR* buf );
static XP_Bool frank_util_askPassword( XW_UtilCtxt* uc, const XP_UCHAR* name, 
				       XP_UCHAR* buf, XP_U16* len );
static void frank_util_trayHiddenChange( XW_UtilCtxt* uc, 
					 XW_TrayVisState newState );
static void frank_util_notifyGameOver( XW_UtilCtxt* uc );
static XP_Bool frank_util_hiliteCell( XW_UtilCtxt* uc, XP_U16 col, XP_U16 row );
static XP_Bool frank_util_engineProgressCallback( XW_UtilCtxt* uc );
static void frank_util_setTimer( XW_UtilCtxt* uc, XWTimerReason why );
static void frank_util_requestTime( XW_UtilCtxt* uc );
static XP_U32 frank_util_getCurSeconds( XW_UtilCtxt* uc );
static XWBonusType frank_util_getSquareBonus( XW_UtilCtxt* uc, 
					      ModelCtxt* model,
					      XP_U16 col, XP_U16 row );
static XP_UCHAR* frank_util_getUserString( XW_UtilCtxt* uc, XP_U16 stringCode );
static XP_Bool frank_util_warnIllegalWord( XW_UtilCtxt* uc, BadWordInfo* bwi, 
					   XP_U16 turn, XP_Bool turnLost );
static void frank_util_engineStarting( XW_UtilCtxt* uc );
static void frank_util_engineStopping( XW_UtilCtxt* uc );


typedef struct FrankSavedState {
    U32 magic;
    U16 curGameIndex;
    XP_Bool showProgress;
} FrankSavedState;


CXWordsWindow
class CXWordsWindow : public CWindow {
 public:    /* so util functions can access */
    XWGame fGame;
    CurGameInfo fGameInfo;
    XP_Bool fTimers[2];
    VTableMgr* fVTableMgr;

 private:
    FrankDrawCtx* draw;
    DictionaryCtxt* dict;
    XW_UtilCtxt util;
    FrankDictList* fDictList;

    FrankSavedState fState;

    CGamesDB* gamesDB;

    CommonPrefs cp;

    RECT fProgressRect;
    U16 fProgressCurLine;

    XP_U8 phoniesAction;
    BOOL penDown;
    BOOL drawInProgress;
    BOOL userEventPending;
    XP_Bool fRobotHalted;

public:
    CXWordsWindow( MPFORMAL FrankDictList* dlist );
    ~CXWordsWindow();
//      void init();
    void Draw();
    S32 MsgHandler( MSG_TYPE type, CViewable *from, S32 data );

    void setUserEventPending() { this->userEventPending = TRUE; }
    void clearUserEventPending() { this->userEventPending = FALSE; }
    BOOL getUserEventPending() { return this->userEventPending; }
    void setTimerIfNeeded();
    XP_Bool robotIsHalted() { return fRobotHalted; }
    void updateCtrlsForTray( XW_TrayVisState newState );
    void startProgressBar();
    void advanceProgressBar();
    void finishProgressBar();

 private:
    CButton* addButtonAt( short id, short x, short y, char* str );
    CButton* addButtonAtBitmap( short id, short x, short y, const char* c,
				IMAGE* img );
    void initUtil();
    void initPrefs();
    void loadPrefs();
    void loadGameFromStream( XWStreamCtxt* inStream );
    void makeNewGame( U16 newIndex );
    void loadCurrentGame();
    void saveCurrentGame();
    void resetCurrentGame();
    void positionBoard();
    void disOrEnableFrank( U16 id, XP_Bool enable );
    
    void writeGameToStream( XWStreamCtxt* stream, U16 index );
    XWStreamCtxt* makeMemStream();

 public:
    void doCommit();
    void doHint( XP_Bool reset );
    void doUndo();
    void doHideTray();
    void doHeapDump();

    BOOL newGame( XP_Bool allowCancel );
    void gameInfo();
    void doNewGameMenu();
#ifndef HASBRO_EBM
    void doSavedGames();
#endif
    void doAbout();
    void doEndGame();
    void doTileValues();
    void doGameHistory();

    void fni();
    void displayFinalScores();
    XP_U16 displayTextFromStream( XWStreamCtxt* stream, const char* title,
				  BOOL killStream = TRUE,
				  BOOL includeCancel = FALSE );
    void wrappedEventLoop( CWindow* window );

    MPSLOT
}; /* class CXWordsWindow */

#ifdef MEM_DEBUG
#define MEMPOOL(t) (t)->mpool,
#define MEMPOOL_NOCOMMA(t) (t)->mpool
#else
#define MEMPOOL_NOCOMMA(t)
#define MEMPOOL(t)
#endif

#define V_BUTTON_SPACING 17
#define BUTTON_LEFT 183

CXWordsWindow::CXWordsWindow(MPFORMAL FrankDictList* dlist )
    : CWindow(MAIN_WINDOW_ID, 0, 0, 200, 240, 
			  "Crosswords 4" 
)
{
    short buttonTop = BOARD_TOP;
    MPASSIGN( this->mpool, mpool );
    fDictList = dlist;

    fVTableMgr = make_vtablemgr(MPPARM_NOCOMMA(mpool));

    this->gamesDB = new CGamesDB( MEMPOOL(this) (XP_UCHAR*)"xwords_games" );

    this->penDown = FALSE;
    this->drawInProgress = FALSE;
    fRobotHalted = XP_FALSE;

    this->cp.showBoardArrow = XP_TRUE;
    this->cp.showRobotScores = XP_TRUE;

    CButton* button;
    button = addButtonAtBitmap( MAIN_FLIP_BUTTON_ID, BUTTON_LEFT, buttonTop, 
                                "F", (IMAGE*)&flip );
    buttonTop += button->GetHeight() + 2;
    button = addButtonAtBitmap( MAIN_VALUE_BUTTON_ID, BUTTON_LEFT, buttonTop, 
                                "V", (IMAGE*)&valuebutton );
    buttonTop += button->GetHeight() + 2;
    button = addButtonAtBitmap( MAIN_HINT_BUTTON_ID, BUTTON_LEFT,
                                buttonTop, "?", (IMAGE*)&lightbulb );
    buttonTop += button->GetHeight() + 2;
    (void)addButtonAtBitmap( MAIN_UNDO_BUTTON_ID, BUTTON_LEFT, buttonTop,
                             "U", (IMAGE*)&undo );
    
    fProgressRect.y = buttonTop + V_BUTTON_SPACING + 2;

    /* now start drawing from the bottom */
    buttonTop = 205;
    button = addButtonAt( MAIN_COMMIT_BUTTON_ID,
                          BUTTON_LEFT, buttonTop, "D" );
    (void)addButtonAt( MAIN_HIDE_BUTTON_ID, 
                       BUTTON_LEFT - button->GetWidth(), buttonTop, "H" );
    buttonTop -= V_BUTTON_SPACING;
    (void)addButtonAt( MAIN_TRADE_BUTTON_ID, BUTTON_LEFT, buttonTop, "T" );
    buttonTop -= V_BUTTON_SPACING;
    (void)addButtonAt( MAIN_JUGGLE_BUTTON_ID, BUTTON_LEFT, buttonTop, "J" );

    this->draw = (FrankDrawCtx*)frank_drawctxt_make( MEMPOOL(this) this );

    fProgressRect.x = BUTTON_LEFT + 2;
    fProgressRect.width = 10;
    fProgressRect.height = buttonTop - fProgressRect.y - 2;

    this->initUtil();

    fGame.model = (ModelCtxt*)NULL;
    fGame.server = (ServerCtxt*)NULL;
    fGame.board = (BoardCtxt*)NULL;

    gi_initPlayerInfo( MEMPOOL(this) &fGameInfo, (XP_UCHAR*)"Player %d" );

    U16 nRecords = gamesDB->countRecords();
    if ( nRecords == 0 ) {	/* 1 for prefs, 1 for first game */
        initPrefs();

        fGameInfo.serverRole = SERVER_STANDALONE;
        /* 	fGameInfo.timerEnabled = XP_TRUE; */
        makeNewGame( fState.curGameIndex );

        GUI_EventMessage( MSG_USER, this, NEWGAME_REQUEST );
    } else {
        XP_ASSERT( nRecords >= 2 );
        loadPrefs();

        /* there needs to be a "game" for the saved one to be loaded into. */
        game_makeNewGame( MPPARM(mpool) &fGame, &fGameInfo, &this->util, 
                          (DrawCtx*)this->draw, &this->cp,
                          (TransportSend)NULL, NULL);
        loadCurrentGame();

        positionBoard();
        server_do( fGame.server ); /* in case there's a robot */
        board_invalAll( fGame.board );
    }
} /* CXWordsWindow::CXWordsWindow */

XWStreamCtxt*
CXWordsWindow::makeMemStream()
{
    XWStreamCtxt* stream = mem_stream_make( MEMPOOL(this) 
					    fVTableMgr,
					    this, 
					    CHANNEL_NONE, 
					    (MemStreamCloseCallback)NULL );
    return stream;
} /* makeMemStream */

void
CXWordsWindow::saveCurrentGame()
{
    XWStreamCtxt* stream = makeMemStream();
    writeGameToStream( stream, fState.curGameIndex );

    U16 len = stream_getSize( stream );
    void* ptr = XP_MALLOC( this->mpool, len );
    stream_getBytes( stream, ptr, len );
    this->gamesDB->putNthRecord( fState.curGameIndex, ptr, len );
    XP_FREE( this->mpool, ptr );

    stream_destroy( stream );
} /* saveCurrentGame */

void
CXWordsWindow::positionBoard()
{
    board_setPos( fGame.board, BOARD_LEFT, BOARD_TOP, 
		  XP_FALSE );
    board_setScale( fGame.board, BOARD_SCALE, BOARD_SCALE );

    board_setScoreboardLoc( fGame.board, SCORE_LEFT, SCORE_TOP,
			    this->GetWidth()-SCORE_LEFT-TIMER_WIDTH, 
			    SCORE_HEIGHT, XP_TRUE );

    U16 trayTop = BOARD_TOP + (BOARD_SCALE * 15) + 2;
    board_setTrayLoc( fGame.board, TRAY_LEFT, trayTop, 
		      MIN_TRAY_SCALE, MIN_TRAY_SCALE, 
		      FRANK_DIVIDER_WIDTH );

    board_setTimerLoc( fGame.board, 
		       this->GetWidth() - TIMER_WIDTH, 
		       SCORE_TOP, TIMER_WIDTH, TIMER_HEIGHT );
} /* positionBoard */

void
CXWordsWindow::makeNewGame( U16 newIndex )
{
    if ( !!fGame.model ) {
        saveCurrentGame();
        XP_U32 gameID = frank_util_getCurSeconds( &this->util );
        game_reset( MEMPOOL(this) &fGame, &fGameInfo, gameID, &this->cp, 
                    (TransportSend)NULL, NULL );
    } else {
        game_makeNewGame( MPPARM(mpool) &fGame, &fGameInfo, &this->util, 
                          (DrawCtx*)this->draw, &this->cp, 
                          (TransportSend)NULL, NULL);
        positionBoard();
    }
    this->gamesDB->putNthName( newIndex, (XP_UCHAR*)"untitled game" );
    fState.curGameIndex = newIndex;
} /* makeNewGame */

CButton*
CXWordsWindow::addButtonAt( short id, short x, short y, char* str )
{
    CButton* button = new CButton( id, 0, 0, str );
    this->AddChild( button, x, y );
    return button;
} /* addButtonAt */

CButton* 
CXWordsWindow::addButtonAtBitmap( short id, short x, short y, const char* c,
				  IMAGE* img )
{
    CButton* button = new CButton( id, 0, 0, (const char*)NULL,
				   img, (IMAGE*)NULL, (IMAGE*)NULL );
    this->AddChild( button, x+1, y );

    return button;
} /* addButtonAtBitmap */

void
CXWordsWindow::initUtil()
{
    UtilVtable* vtable = this->util.vtable = new UtilVtable;
    this->util.closure = (void*)this;
    this->util.gameInfo = &fGameInfo;
    MPASSIGN( this->util.mpool, mpool );

/*     vtable->m_util_makeStreamFromAddr = NULL; */
    vtable->m_util_getVTManager = frank_util_getVTManager;
    vtable->m_util_makeEmptyDict = frank_util_makeEmptyDict;
/*     vtable->m_util_yOffsetChange = NULL <--no scrolling */
    vtable->m_util_userError = frank_util_userError;
    vtable->m_util_userQuery = frank_util_userQuery;
    vtable->m_util_askBlankFace = frank_util_askBlankFace;
    vtable->m_util_askPassword = frank_util_askPassword;
    vtable->m_util_trayHiddenChange = frank_util_trayHiddenChange;
    vtable->m_util_notifyGameOver = frank_util_notifyGameOver;
    vtable->m_util_hiliteCell = frank_util_hiliteCell;
    vtable->m_util_engineProgressCallback = frank_util_engineProgressCallback;
    vtable->m_util_setTimer = frank_util_setTimer;
    vtable->m_util_requestTime = frank_util_requestTime;
    vtable->m_util_getCurSeconds = frank_util_getCurSeconds;

    vtable->m_util_getSquareBonus = frank_util_getSquareBonus;
    vtable->m_util_getUserString = frank_util_getUserString;
    vtable->m_util_warnIllegalWord = frank_util_warnIllegalWord;
#ifdef SHOW_PROGRESS
    vtable->m_util_engineStarting = frank_util_engineStarting;
    vtable->m_util_engineStopping = frank_util_engineStopping;
#endif
} /* initUtil */

CXWordsWindow::~CXWordsWindow()
{
    XP_WARNF( "~CXWordsWindow(this=%p) called", this );

    if ( !!this->gamesDB ) {
        this->gamesDB->putNthRecord( 0, &fState, sizeof(fState) );
        saveCurrentGame();

        delete this->gamesDB;
        this->gamesDB = (CGamesDB*)NULL;
    }
    delete fDictList;
}

void CXWordsWindow::Draw()
{
    if ( !this->drawInProgress ) {
	this->drawInProgress = TRUE;
	// don't call CWindow::Draw();  It erases the entire board
	board_draw( fGame.board );

	this->DrawChildren();
	this->drawInProgress = FALSE;
    }
} // CXWordsWindow::Draw

S32
CXWordsWindow::MsgHandler( MSG_TYPE type, CViewable *from, S32 data )
{
    S32 result = 0;
    XP_Key xpkey;
    S16 drag_x;
    S16 drag_y;
    XWTimerReason reason;

    drag_x = (S16) (data >> 16);
    drag_y = (S16) data;

    GUI_DisableTimers();
    switch (type) {

    case MSG_USER:
	switch ( data ) {
	case HINT_REQUEST:
	    doHint( XP_FALSE );	/* will reset if fails */
	    break;
	case SERVER_TIME_REQUEST:
	    this->clearUserEventPending(); /* clear before calling server! */
	    if ( server_do( fGame.server ) ) {
		GUI_NeedUpdate();
	    }
	    break;
	case NEWGAME_REQUEST:
	    this->newGame( XP_FALSE );
	    break;
	case FINALSCORE_REQUEST:
	    this->displayFinalScores();
	    break;
	}
	break;

    case MSG_PEN_DOWN:
	this->penDown = TRUE;
	if ( board_handlePenDown( fGame.board, drag_x, drag_y, 0 ) ) {
	    GUI_NeedUpdate();
	    result = 1;
	}
	board_pushTimerSave( fGame.board );
	break;

    case MSG_PEN_TRACK:
	if ( this->penDown ) {
	    if ( board_handlePenMove( fGame.board, drag_x, drag_y ) ) {
		GUI_NeedUpdate();
		result = 1;
	    }
	}
	break;

    case MSG_PEN_UP:
	if ( this->penDown ) {
	    board_popTimerSave( fGame.board );
	    if ( board_handlePenUp( fGame.board, drag_x, drag_y, 0 ) ) {
		GUI_NeedUpdate();
		result = 1;
	    }
	    this->penDown = FALSE;
	}
	break;

    case MSG_TIMER:
	reason = this->fTimers[TIMER_PENDOWN]?
	    TIMER_PENDOWN:TIMER_TIMERTICK;
	fTimers[reason] = XP_FALSE; /* clear now; board may set it again */
	board_timerFired( fGame.board, reason );
	setTimerIfNeeded();
	GUI_NeedUpdate();	/* Needed off-emulator? PENDING */
	break;

    case MSG_BUTTON_SELECT:
	result = 1;
	switch (from->GetID()) {
	case MAIN_FLIP_BUTTON_ID:
	    if ( board_flip( fGame.board ) ) {
		GUI_NeedUpdate();
	    }
	    break;
	    
	case MAIN_VALUE_BUTTON_ID:
	    if ( board_toggle_showValues( fGame.board ) ) {
		GUI_NeedUpdate();
	    }
	    break;

	case MAIN_HINT_BUTTON_ID:
	    this->doHint( XP_FALSE );
	    break;

	case MAIN_UNDO_BUTTON_ID:
	    this->doUndo();
	    break;

	case MAIN_COMMIT_BUTTON_ID:
	    this->doCommit();
	    break;

	case MAIN_TRADE_BUTTON_ID:
	    if ( board_beginTrade( fGame.board ) ) {
		GUI_NeedUpdate();
	    }
	    break;

	case MAIN_JUGGLE_BUTTON_ID:
	    if ( board_juggleTray( fGame.board ) ) {
		GUI_NeedUpdate();
	    }
	    break;
	case MAIN_HIDE_BUTTON_ID:
	    this->doHideTray();
	    break;
	default:
	    result = 0;
	}
	break;

    case MSG_KEY:
	xpkey = XP_KEY_NONE;
	switch( data ) {

	case K_JOG_ENTER:
	    xpkey = XP_RETURN_KEY;
	    break;

	case K_JOG_DOWN:
	    xpkey = XP_CURSOR_KEY_RIGHT;
	    break;

	case K_JOG_UP:
	    xpkey = XP_CURSOR_KEY_LEFT;
	    break;

	case K_DELETE:
	case K_BACKSPACE:
	    xpkey = XP_CURSOR_KEY_DEL;
	    break;

	default:
	    if ( isalpha( data ) ) {
		xpkey = (XP_Key)toupper(data);
	    }
	    break;
	    
	}

	if ( xpkey != XP_KEY_NONE ) {
	    if ( board_handleKey( fGame.board, xpkey ) ) {
		GUI_NeedUpdate();
		result = 1;
	    }
	}

	break; /* MSG_KEY */

    default:
	break;
    }
    GUI_EnableTimers();

    if ( result == 0 ) {
	result = CWindow::MsgHandler( type, from, data );
    } 
    return result;
} // CXWordsWindow::MsgHandler

void
CXWordsWindow::setTimerIfNeeded()
{
    U32 mSeconds;
    if ( fTimers[TIMER_PENDOWN] ) { /* faster, so higher priority */
	mSeconds = (U32)450;
	XP_DEBUGF( "setting timer %d", TIMER_PENDOWN );
    } else if ( fTimers[TIMER_TIMERTICK] ) {
	mSeconds = (U32)1000;
	XP_DEBUGF( "setting timer %d", TIMER_TIMERTICK );
    } else {
	return;
    }

    SetTimer( mSeconds, XP_FALSE, 0L );
} /* setTimerIfNeeded */

void
CXWordsWindow::disOrEnableFrank( U16 id, XP_Bool enable )
{
    CButton* button = (CButton*)GetChildID( id );
    if ( enable ) {
	button->Enable();
    } else {
	button->Disable();	
    }
} /* disOrEnableFrank */

void
CXWordsWindow::updateCtrlsForTray( XW_TrayVisState newState )
{
    XP_ASSERT( newState != TRAY_HIDDEN );
    XP_Bool isRevealed = newState == TRAY_REVEALED;

    disOrEnableFrank( MAIN_HINT_BUTTON_ID, isRevealed );
    disOrEnableFrank( MAIN_UNDO_BUTTON_ID, isRevealed );
    disOrEnableFrank( MAIN_COMMIT_BUTTON_ID, isRevealed );
    disOrEnableFrank( MAIN_TRADE_BUTTON_ID, isRevealed );
    disOrEnableFrank( MAIN_JUGGLE_BUTTON_ID, isRevealed );
    disOrEnableFrank( MAIN_HIDE_BUTTON_ID, isRevealed );
} /* updateCtrlsForTray */

void
CXWordsWindow::startProgressBar()
{
    if ( fState.showProgress ) {

	DrawRectFilled( &fProgressRect, COLOR_WHITE );
	DrawRect( &fProgressRect, COLOR_BLACK );

	fProgressCurLine = 0;
    }
} /* startProgressBar */

void
CXWordsWindow::finishProgressBar()
{
    if ( fState.showProgress ) {
	DrawRectFilled( &fProgressRect, COLOR_WHITE );
    }
} /* finishProgressBar */

void
CXWordsWindow::advanceProgressBar()
{
    if ( fState.showProgress ) {
	U16 line;
	U16 height = fProgressRect.height - 2; /* don't overwrite top and
						  bottom */
	XP_Bool draw;
	COLOR color;

	fProgressCurLine %= height * 2;
	draw = fProgressCurLine < height;

	line = fProgressCurLine % (height) + 1;
	line = fProgressRect.y + height - line + 1;
	if ( draw ) {
	    color = COLOR_BLACK;
	} else {
	    color = COLOR_WHITE;
	}

	DrawLine( fProgressRect.x+1, line, 
		  fProgressRect.x + fProgressRect.width - 1, line, color );

	++fProgressCurLine;
    }
} /* advanceProgressBar */

void
CXWordsWindow::wrappedEventLoop( CWindow* window )
{
    XP_Bool robotHalted = fRobotHalted;
    fRobotHalted = XP_TRUE;
    board_pushTimerSave( fGame.board );

    GUI_EventLoop( window );

    board_popTimerSave( fGame.board );
    fRobotHalted = robotHalted;
} /* wrappedEventLoop */

void
CXWordsWindow::gameInfo()
{
    BOOL ignore;
    wrappedEventLoop( new CPlayersWindow( MEMPOOL(this) &fGameInfo, fDictList, 
                                          FALSE, FALSE, &ignore ) );
} /* gameInfo */

BOOL
CXWordsWindow::newGame( XP_Bool allowCancel )
{
    BOOL cancelled;
    wrappedEventLoop( new CPlayersWindow( MEMPOOL(this) &fGameInfo, fDictList, 
                                          TRUE, allowCancel, &cancelled ) );

    XP_ASSERT( allowCancel || !cancelled );	/* can't clear cancelled if not
											   allowed to */
    if ( !cancelled ) {
#if 0
        makeNewGame( this->gamesDB->countRecords() );
#else
        XP_U32 gameID = frank_util_getCurSeconds( &this->util );
        game_reset( MPPARM(mpool) &fGame, &fGameInfo, gameID,
                    &this->cp, (TransportSend)NULL, NULL );
        if ( !!fGameInfo.dictName ) {
            DictionaryCtxt* dict = model_getDictionary( fGame.model );
            if ( !!dict && 0==strcmp( (char*)dict_getName(dict), 
                                      (char*)fGameInfo.dictName)){
                /* do nothing; this dict's fine */
            } else {
                if ( !!dict ) {
                    dict_destroy( dict );
                }
                dict = frank_dictionary_make( MPPARM(mpool)
                                              fGameInfo.dictName);
                model_setDictionary( fGame.model, dict );
            }
        }
#endif
        server_do( fGame.server );

        GUI_NeedUpdate();
    }

    return !cancelled;
} /* newGame */

/* ======================================================================== */
void
Init_Window( MPFORMAL FrankDictList* dlist )
{
    MainWindow = new CXWordsWindow( MPPARM(mpool) dlist );
}

void
Init_Menu()
{
    short row;
    CMenu* menu;

    menu = new CMenu( FILEMENU_WINDOW_ID );
    menu->SetNumRows( 4 );	/* 4 with preferences */
    row = 0;
    menu->SetRow( row++, FILEMENU_NEWGAME, "New game", 'n' );
    menu->SetRow( row++, FILEMENU_SAVEDGAMES, "Saved games..." );
    menu->SetSeparatorRow( row++ );
/*     menu->SetRow( row++, FILEMENU_PREFS, "Preferences..." ); */
#ifdef HASBRO_EBM
    menu->SetRow( row++, FILEMENU_ABOUT, "About Franklin Scrabble(tm)..." );
#else
    menu->SetRow( row++, FILEMENU_ABOUT, "About Crosswords..." );
#endif
    MainMenuBar.AddButton( new CPushButton(FILEMENU_BUTTON_ID,0,0,"File"),
			   menu );

    menu = new CMenu( GAMEMENU_WINDOW_ID );
    menu->SetNumRows( 4 );
    row = 0;
    menu->SetRow( row++, GAMEMENU_TVALUES, "Tile values", 'v' );
    menu->SetRow( row++, GAMEMENU_GAMEINFO, "Current game info", 'i' );
    menu->SetRow( row++, GAMEMENU_HISTORY, "Game history", 's' );
    menu->SetRow( row++, GAMEMENU_FINALSCORES, "Final scores", 'f' );
    MainMenuBar.AddButton( new CPushButton(GAMEMENU_BUTTON_ID,0,0,"Game"),
			   menu );

    menu = new CMenu( MOVEMENU_WINDOW_ID );
    menu->SetNumRows( 9 );
    row = 0;
    menu->SetRow( row++, MOVEMENU_HINT, "Hint", 'h' );
    menu->SetRow( row++, MOVEMENU_NEXTHINT, "Next hint", 'n' );
    menu->SetRow( row++, MOVEMENU_REVERT, "Revert move", 'r' );
    menu->SetRow( row++, MOVEMENU_UNDO, "Undo prev. move", 'u' );
    menu->SetSeparatorRow( row++ );
    menu->SetRow( row++, MOVEMENU_DONE, "Done", 'd' );
    menu->SetRow( row++, MOVEMENU_JUGGLE, "Juggle", 'j' );
    menu->SetRow( row++, MOVEMENU_TRADE, "Trade", 't' );
    menu->SetRow( row++, MOVEMENU_HIDETRAY, "Hide tray", 'h' );

    MainMenuBar.AddButton( new CPushButton(MOVEMENU_BUTTON_ID,0,0,"Move"),
			   menu );

#ifdef MEM_DEBUG
    menu = new CMenu( MOVEMENU_WINDOW_ID );
    menu->SetNumRows( 1 );
    row = 0;
    menu->SetRow( row++, DEBUGMENU_HEAPDUMP, "Heap dump" );

    MainMenuBar.AddButton( new CPushButton(DEBUGMENU_BUTTON_ID,0,0,"Debug"),
			   menu );
#endif

}

void
MyErrorHandler( const char *filename, int lineno, const char *failexpr )
{
    if (lineno != -1 || strcmp( failexpr, "Out of memory" )) {
	return;
    }

    GUI_Alert( ALERT_WARNING, 
	       "Operation cancelled - insufficient memory" );
    GUI_SetMallocReserve( 1536 );
    GUI_ClearStack();
}

S32
GUI_main( MSG_TYPE type, CViewable *object, S32 data )
{
    switch (type) {
    case MSG_APP_START: {
        FrankDictList* dlist;
        if (OS_is_present && hostIO_am_I_the_current_task()) {
            HOSTIO_INLINE_BREAKPOINT();
        }

        struct timeval tv;
        gettimeofday( &tv, (struct timezone *)NULL );
        srand( tv.tv_sec /*20*/ /*TIMER_GetTickCountUSecs()*/ );

#ifdef MEM_DEBUG
        MemPoolCtx* mpool = mpool_make();
#endif

        dlist = new FrankDictList( MPPARM_NOCOMMA(mpool) );
        if ( dlist->GetDictCount() > 0 ) {
            Init_Window( MPPARM(mpool) dlist );
            Init_Menu();
            GUI_SetErrorHandler( MyErrorHandler );
        } else {
            delete dlist;
#ifdef MEM_DEBUG
            mpool_destroy( mpool );        
#endif
            GUI_Alert( ALERT_WARNING, 
                       "Crosswords requires at least one dictionary." );
            GUI_Exit();
        }
        return 1;
    }

    case MSG_APP_STOP:
        delete MainWindow;	/* trigger save */
        return 1;

    case MSG_KEY:
        if ( data == K_MENU ) {
            MainMenuBar.Show();
            return 1;
        }
        break;

    case MSG_MENU_SELECT:
        if (data == -1) {
            /* We don't care about menu cancellations */
            return 1;
        }
        switch ((U16) data) {

        case FILEMENU_NEWGAME:
            MainWindow->doNewGameMenu();
            return 1;

        case FILEMENU_SAVEDGAMES:
#ifdef HASBRO_EBM
            GUI_Alert( ALERT_WARNING, "Feature not available in demo version." );
#else
            MainWindow->doSavedGames();
#endif
            return 1;

            /* 	case FILEMENU_PREFS: */
        case FILEMENU_ABOUT:
            MainWindow->doAbout();
            return 1;

        case GAMEMENU_TVALUES:
            MainWindow->doTileValues();
            return 1;

        case GAMEMENU_FINALSCORES:
            MainWindow->doEndGame();
            return 1;
            break;

        case GAMEMENU_GAMEINFO:
            MainWindow->gameInfo();
            return 1;

        case GAMEMENU_HISTORY:
            MainWindow->doGameHistory();
            return 1;

        case MOVEMENU_HINT:
        case MOVEMENU_NEXTHINT:
            MainWindow->doHint( (U16)data == MOVEMENU_HINT );
            break;

        case MOVEMENU_UNDO:
            MainWindow->doUndo();
            break;

        case MOVEMENU_REVERT:
            break;
        case MOVEMENU_DONE:
            MainWindow->doCommit();
            break;
        case MOVEMENU_JUGGLE:
        case MOVEMENU_TRADE:
            break;
        case MOVEMENU_HIDETRAY:
            MainWindow->doHideTray();
            break;
#ifdef MEM_DEBUG
        case DEBUGMENU_HEAPDUMP:
            MainWindow->doHeapDump();
            break;
#endif
        }
        break;
    default:
        fallthru;
    }
    return 0;
} // GUI_main

void
CXWordsWindow::doHint( XP_Bool reset )
{
    XP_Bool workRemains = XP_FALSE;
    XP_Bool done;

    if ( reset ) {
	board_resetEngine( fGame.board );
    }
    done = board_requestHint( fGame.board, &workRemains );
    if ( done ) {
	GUI_NeedUpdate();
    }
    if ( workRemains ) {
	GUI_EventMessage( MSG_USER, this, HINT_REQUEST );
    }
} /* handleHintMenu */

void
CXWordsWindow::doUndo()
{
    if ( server_handleUndo( fGame.server ) ) {
	GUI_NeedUpdate();
    }
} /* doUndo */

void
CXWordsWindow::doHideTray()
{
    if ( board_hideTray( fGame.board ) ) {
	GUI_NeedUpdate();
    }
} /* doHideTray */

#ifdef MEM_DEBUG
void
CXWordsWindow::doHeapDump()
{
    XWStreamCtxt* stream = makeMemStream();
    mpool_stats( mpool, stream );
    
    XP_U16 size = stream_getSize( stream );
    char* buf = (char*)malloc(size+1);
    stream_getBytes( stream, buf, size );
    buf[size] = '\0';
    perror(buf);                /* XP_DEBUGF has 256 byte limit */
    free( buf );
} /* CXWordsWindow::doHeapDump */
#endif

void
CXWordsWindow::doCommit()
{
    if ( board_commitTurn( fGame.board ) ) {
	GUI_NeedUpdate();
    }
} /* doCommit */

/* If there's a game in progress (and there always is, I think), ask user if
 * wants to save.  If does, save that game and make a new one, with a new
 * index, to call the gui on.  Else reset the existing one and call the gui.*/
void
CXWordsWindow::doNewGameMenu()
{
    XP_Bool doit = XP_TRUE;
    /* OK returns 1 */
    if ( 0 == GUI_Alert( ALERT_OK, 
						 "Click \"OK\" to replace the current game with a new "
						 "one, or \"Cancel\" to add without deleting the current "
						 "game." ) ) {
		makeNewGame( this->gamesDB->countRecords() );
    } else if ( 0 == GUI_Alert( ALERT_OK,
				"Are you sure you want to replace"
				" the existing game?" ) ) {
	doit = XP_FALSE;
    }

    if ( doit && newGame( XP_FALSE ) ) { /* don't let user cancel; too late! */
		positionBoard();
    }

    board_invalAll( fGame.board );
    GUI_NeedUpdate();
} /* doNewGameMenu */

#ifndef HASBRO_EBM
void
CXWordsWindow::doSavedGames()
{
    U16 openIndex;		/* what index am I to open? */
    U16 curIndex = fState.curGameIndex; /* may change if lower-index
						 game deleted */
    saveCurrentGame();		/* so can be duped */
    wrappedEventLoop( new CSavedGamesWindow( this->gamesDB, &openIndex, 
					     &curIndex) );

    fState.curGameIndex = curIndex;
    if ( curIndex != openIndex ) {
	fState.curGameIndex = openIndex;
	loadCurrentGame();
	positionBoard();
	server_do( fGame.server ); /* in case there's a robot */
	board_invalAll( fGame.board );
	GUI_NeedUpdate();
    }
} /* doSavedGames */
#endif

void
CXWordsWindow::doAbout()
{
    XP_U16 ignore;
    XWStreamCtxt* stream;

    stream = makeMemStream();
    char* txt = "Crosswords" VERSION_STRING "\n"
	"Copyright 2000-2002 by Eric House (fixin@peak.org).\n"
	"All rights reserved.\n"
	"For further information see www.peak.org/~fixin/xwords/ebm.html.";
    stream_putBytes( stream, txt, strlen(txt) );
    stream_putU8( stream, '\0' );

    wrappedEventLoop( new CShowTextWindow( MEMPOOL(this) stream, 
					   "About Crosswords", 
					   true, false, 
					   &ignore ) );
} /* doAbout */

void
CXWordsWindow::doEndGame()
{
    if ( server_getGameIsOver( fGame.server ) ) {
	this->displayFinalScores();
    } else if ( GUI_Alert( ALERT_OK, 
			   "Are you sure you want to end the game now?" ) 
		!= 0 ) {
	server_endGame( fGame.server );
    }
    GUI_NeedUpdate();
} /* doEndGame */

void
CXWordsWindow::doTileValues()
{
    XWStreamCtxt* stream;

    stream = makeMemStream();
    server_formatPoolCounts( fGame.server, stream, 2 /* cols */ );

    displayTextFromStream( stream, "Tile counts and values" );
} /* doTileValues */

void
CXWordsWindow::doGameHistory()
{
    XP_Bool gameOver = server_getGameIsOver( fGame.server );
    XWStreamCtxt* stream = makeMemStream();
    model_writeGameHistory( fGame.model, stream, fGame.server, gameOver );

    displayTextFromStream( stream, "Game history" );
} /* doGameHistory */

void 
CXWordsWindow::initPrefs()
{
    fState.magic = 0x12345678;
    fState.curGameIndex = 1; /* 0 is prefs record */
    fState.showProgress = XP_TRUE;

    /* save 'em now so we can save 1st game at expected index. */
    this->gamesDB->putNthRecord( 0, &fState, sizeof(fState) );
} /* initPrefs */

void 
CXWordsWindow::loadPrefs()
{
    CGamesDB* gamesDB = this->gamesDB;
    XP_ASSERT( gamesDB->countRecords() > 0 );

    U16 len;
    void* recordP = gamesDB->getNthRecord( 0, &len );

    XP_ASSERT( len == sizeof(fState) );
    XP_ASSERT( !!recordP );
    XP_MEMCPY( &fState, recordP, len );
    gamesDB->recordRelease(0);

    XP_ASSERT( fState.magic == 0x12345678 );
} /* loadPrefs */

void
CXWordsWindow::loadCurrentGame()
{
    U16 len;
    void* recordP = gamesDB->getNthRecord( fState.curGameIndex, &len );

    XWStreamCtxt* inStream = makeMemStream();
    stream_putBytes( inStream, recordP, len );
    gamesDB->recordRelease( fState.curGameIndex );

    loadGameFromStream( inStream );
    stream_destroy( inStream );
} /* loadCurrentGame */

void
CXWordsWindow::writeGameToStream( XWStreamCtxt* stream, U16 index )
{
    /* the dictionary */
    DictionaryCtxt* dict = model_getDictionary( fGame.model );
    XP_UCHAR* dictName = dict_getName( dict );
    stream_putU8( stream, !!dictName );
    if ( !!dictName ) {
	stringToStream( stream, dictName );
    }

    game_saveToStream( &fGame, &fGameInfo, stream );
} /* writeGameToStream */

void
CXWordsWindow::loadGameFromStream( XWStreamCtxt* inStream )
{
    /* the dictionary */
    XP_U8 hasDictName = stream_getU8( inStream );
    DictionaryCtxt* dict = (DictionaryCtxt*)NULL;
    if ( hasDictName ) {
        XP_UCHAR* name = stringFromStream(MEMPOOL(this) inStream);
        dict = frank_dictionary_make( MPPARM(mpool) name );
    }

    game_makeFromStream( MPPARM(mpool) inStream, &fGame, &fGameInfo, 
                         dict, &this->util, (DrawCtx*)this->draw, &this->cp,
                         (TransportSend)NULL, NULL );
} /* loadGameFromStream */

void
CXWordsWindow::fni()
{
    GUI_Alert( ALERT_WARNING, "Feature pending" );
    board_invalAll( fGame.board );
    GUI_NeedUpdate();
} /* fni */

void 
CXWordsWindow::displayFinalScores()
{
    XWStreamCtxt* stream;

    stream = makeMemStream();
    server_writeFinalScores( fGame.server, stream );

    displayTextFromStream( stream, "Final scores" );
} /* displayFinalScores */

XP_U16
CXWordsWindow::displayTextFromStream( XWStreamCtxt* stream, 
				      const char* title, /* should be ID!!! */
				      BOOL killStream,
				      BOOL includeCancel )
{
    XP_U16 result = 0;
    wrappedEventLoop( new CShowTextWindow( MEMPOOL(this) stream, title, 
					   killStream, includeCancel, 
					   &result ) );
    return result;
} /* displayTextFromStream */

extern "C" {

    int
    frank_snprintf( XP_UCHAR* buf, XP_U16 len, XP_UCHAR* format, ... )
    {
	va_list ap;	
    
	va_start(ap, format);

	vsnprintf((char*)buf, len, (char*)format, ap);

	va_end(ap);

	return strlen((char*)buf);
    } /* frank_snprintf */

    void
    frank_debugf( char* format, ... )
    {
	char buf[256];
	va_list ap;
    
	va_start(ap, format);

	vsprintf(buf, format, ap);

	va_end(ap);
    
	perror(buf);
    } // debugf

    XP_UCHAR* 
    frankCopyStr( MPFORMAL const XP_UCHAR* buf )
    {
        XP_U16 len = XP_STRLEN(buf) + 1;
        XP_UCHAR* result = (XP_UCHAR*)XP_MALLOC( mpool, len );
        XP_MEMCPY( result, buf, len );
        return result;
    } /* frankCopyStr */

    unsigned long 
    frank_flipLong( unsigned long l )
    {
        unsigned long result =
            ((l & 0x000000FF) << 24) | 
            ((l & 0x0000FF00) << 8) | 
            ((l & 0x00FF0000) >> 8) | 
            ((l & 0xFF000000) >> 24);
        return result;
    } /* frank_flipLong */

    unsigned short
    frank_flipShort(unsigned short s)
    {
        unsigned short result = 
            ((s & 0x00FF) << 8) | 
            ((s & 0xFF00) >> 8);
    
        return result;
    } /* frank_flipShort */

}
/*****************************************************************************
 * These are the callbacks intstalled in the util vtable
 ****************************************************************************/
static VTableMgr* 
frank_util_getVTManager( XW_UtilCtxt* uc )
{
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;
    return self->fVTableMgr;
} /* frank_util_getVTManager */

static DictionaryCtxt* 
frank_util_makeEmptyDict( XW_UtilCtxt* uc )
{
    return frank_dictionary_make( MPPARM(uc->mpool) (XP_UCHAR*)NULL );
} /* frank_util_makeEmptyDict */

static void
frank_util_userError( XW_UtilCtxt* uc, UtilErrID id )
{
    const char *message;
/*
    BOOL GUI_Alert( ALERT type, const char *text ); 
        Puts up an alert window, which is a small window containing an icon, some text, and one or more buttons. 
        These types of alerts are offered: 
            ALERT_BUG: Insect icon and "Abort" button (click terminates application).  Does not return. 
            ALERT_FATAL: Octagonal icon and "Stop" button (click terminates application).  Does not return. 
            ALERT_ERROR: Exclamation-point icon and "Cancel" button (click returns 0). 
            ALERT_WARNING: Info icon and "OK" button (click returns 0). 
            ALERT_OK: Question-mark icon, buttons "OK" (click returns 1) and "Cancel" (click returns 0). 
            ALERT_RETRY: Exclamation-point icon and buttons "Try again" (returns 1) and "Exit" (returns 0). 
*/
    switch( id ) {
    case ERR_TILES_NOT_IN_LINE:
	message = "All tiles played must be in a line.";
	break;
    case ERR_NO_EMPTIES_IN_TURN:
	message = "Empty squares cannot separate tiles played.";
	break;

    case ERR_TWO_TILES_FIRST_MOVE:
	message = "Must play two or more pieces on the first move.";
	break;
    case ERR_TILES_MUST_CONTACT:
	message = "New pieces must contact others already in place (or "
	    "the middle square on the first move).";
	break;
    case ERR_NOT_YOUR_TURN:
	message = "You can't do that; it's not your turn!";
	break;
    case ERR_NO_PEEK_ROBOT_TILES:
	message = "No peeking at the robot's tiles!";
	break;
    case ERR_CANT_TRADE_MID_MOVE:
	message = "Remove played tiles before trading.";
	break;
    case ERR_TOO_FEW_TILES_LEFT_TO_TRADE:
	message = "Too few tiles left to trade.";
	break;
    default:
	message = "unknown errorcode ID!!!";
	break;
    }

    (void)GUI_Alert( ALERT_ERROR, message ); 
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;
    board_invalAll( self->fGame.board );
    GUI_NeedUpdate();
} /* frank_util_userError */

static XP_U16
frank_util_userQuery( XW_UtilCtxt* uc, UtilQueryID id, XWStreamCtxt* stream )
{
    char* question;
    XP_U16 askResult;
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;

    switch( id ) {
    case QUERY_COMMIT_TURN:
        askResult = self->displayTextFromStream( stream, "Query",
                                                 FALSE, TRUE );
        return askResult;
    case QUERY_COMMIT_TRADE:
        question = "Really trade the selected tiles?";
        break;
    case QUERY_ROBOT_MOVE:
    case QUERY_ROBOT_TRADE:
        XP_LOGF( "handling robot info" );
        askResult = self->displayTextFromStream( stream, "Robot move",
                                                 FALSE, FALSE );
        return askResult;
        break;
    default:
        question = "Unimplemented query code!!!";
        break;
    }

    askResult = GUI_Alert( ALERT_OK, question ); 
    board_invalAll( self->fGame.board );
    GUI_NeedUpdate();
    return askResult;
} /* frank_util_userQuery */

static void
frank_util_askBlankFace( XW_UtilCtxt* uc, DictionaryCtxt* dict, 
			 unsigned char* buf )
{
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;
    self->wrappedEventLoop( new CAskLetterWindow( dict, buf ) );
    /* doesn't need to inval because CAskLetterWindow saves bits behind */
} /* frank_util_askBlankFace */

static XP_Bool
frank_util_askPassword( XW_UtilCtxt* uc, const XP_UCHAR* name, XP_UCHAR* buf, 
			XP_U16* lenp )
{
    XP_Bool ok;
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;
    self->wrappedEventLoop( new CAskPasswdWindow( name, buf, lenp, &ok ) );
    return ok;
} /* frank_util_askPassword */

static void 
frank_util_trayHiddenChange( XW_UtilCtxt* uc, XW_TrayVisState newState )
{
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;
    self->updateCtrlsForTray( newState );
} /* frank_util_trayHiddenChange */

static void
frank_util_notifyGameOver( XW_UtilCtxt* uc )
{
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;
    board_invalAll( self->fGame.board );
    GUI_NeedUpdate();
    GUI_EventMessage( MSG_USER, self, FINALSCORE_REQUEST );
} /* frank_util_notifyGameOver */

static XP_Bool
frank_util_hiliteCell( XW_UtilCtxt* uc, XP_U16 col, XP_U16 row )
{
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;
    XP_Bool halted = self->robotIsHalted();
    if ( !halted ) {
	board_hiliteCellAt( self->fGame.board, col, row );
    }
    BOOL waiting = EVNT_IsWaiting();
    return !waiting && !halted;
} /* frank_util_hiliteCell */

/* Return false to get engine to abort search.
 */
static XP_Bool
frank_util_engineProgressCallback( XW_UtilCtxt* uc )
{
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;

    self->advanceProgressBar();

    BOOL waiting = EVNT_IsWaiting();
    return !waiting && !self->robotIsHalted();
} /* frank_util_engineProgressCallback */

static void
frank_util_setTimer( XW_UtilCtxt* uc, XWTimerReason why )
{
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;

    XP_ASSERT( why == TIMER_PENDOWN ||
	       why == TIMER_TIMERTICK );

    self->fTimers[why] = XP_TRUE;
    self->setTimerIfNeeded();
} /* frank_util_setTimer */

static void
frank_util_requestTime( XW_UtilCtxt* uc )
{
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;
    if ( !self->getUserEventPending() ) {
	GUI_EventMessage( MSG_USER, self, SERVER_TIME_REQUEST );
	self->setUserEventPending();
    }
} /* frank_util_requestTime */

static XP_U32
frank_util_getCurSeconds( XW_UtilCtxt* uc )
{
    struct timeval tv;
    gettimeofday( &tv, (struct timezone *)NULL );
    return tv.tv_sec;
} /* frank_util_getCurSeconds */

#define EM BONUS_NONE
#define DL BONUS_DOUBLE_LETTER
#define DW BONUS_DOUBLE_WORD
#define TL BONUS_TRIPLE_LETTER
#define TW BONUS_TRIPLE_WORD

static XWBonusType
frank_util_getSquareBonus( XW_UtilCtxt* uc, ModelCtxt* model,
			   XP_U16 col, XP_U16 row )
{
    XP_U16 index;
    /* This must be static or won't compile under multilink (for Palm).
       Fix! */
    const char scrabbleBoard[8*8] = {
	TW,EM,EM,DL,EM,EM,EM,TW,
	EM,DW,EM,EM,EM,TL,EM,EM,

	EM,EM,DW,EM,EM,EM,DL,EM,
	DL,EM,EM,DW,EM,EM,EM,DL,
                            
	EM,EM,EM,EM,DW,EM,EM,EM,
	EM,TL,EM,EM,EM,TL,EM,EM,
                            
	EM,EM,DL,EM,EM,EM,DL,EM,
	TW,EM,EM,DL,EM,EM,EM,DW,
    }; /* scrabbleBoard */

    if ( col > 7 ) col = 14 - col;
    if ( row > 7 ) row = 14 - row;
    index = (row*8) + col;
    if ( index >= 8*8 ) {
	return (XWBonusType)EM;
    } else {
	return (XWBonusType)scrabbleBoard[index];
    }
} /* frank_util_getSquareBonus */

static XP_UCHAR* 
frank_util_getUserString( XW_UtilCtxt* uc, XP_U16 stringCode )
{
    switch( stringCode ) {
    case STRD_REMAINING_TILES_ADD:
        return (XP_UCHAR*)"+ %d [all remaining tiles]";
    case STRD_UNUSED_TILES_SUB:
        return (XP_UCHAR*)"- %d [unused tiles]";
    case STR_BONUS_ALL:
        return (XP_UCHAR*)"Bonus for using all tiles: 50\n";
    case STRD_TURN_SCORE:
        return (XP_UCHAR*)"Score for turn: %d\n";
    case STR_COMMIT_CONFIRM:
        return (XP_UCHAR*)"Commit the current move?\n";
    case STR_NONLOCAL_NAME:
        return (XP_UCHAR*)"%s (remote)";
    case STRD_TIME_PENALTY_SUB:
        return (XP_UCHAR*)" - %d [time]";

    case STRD_CUMULATIVE_SCORE:
        return (XP_UCHAR*)"Cumulative score: %d\n";
    case STRS_MOVE_ACROSS:
        return (XP_UCHAR*)"move (from %s across)\n";
    case STRS_MOVE_DOWN:
        return (XP_UCHAR*)"move (from %s down)\n";
    case STRS_TRAY_AT_START:
        return (XP_UCHAR*)"Tray at start: %s\n";

    case STRS_NEW_TILES:
        return (XP_UCHAR*)"New tiles: %s\n";
    case STRSS_TRADED_FOR:
        return (XP_UCHAR*)"Traded %s for %s.";
    case STR_PASS:
        return (XP_UCHAR*)"pass\n";
    case STR_PHONY_REJECTED:
        return (XP_UCHAR*)"Illegal word in move; turn lost!\n";
    case STRD_ROBOT_TRADED:
        return (XP_UCHAR*)"Robot traded %d tiles this turn.";
    case STR_ROBOT_MOVED:
        return (XP_UCHAR*)"The robot made this move:\n";

    default:
        return (XP_UCHAR*)"unknown code ";
    }
} /* frank_util_getUserString */

static void
formatBadWords( BadWordInfo* bwi, char buf[] )
{
    XP_U16 i;

    for ( i = 0, buf[0] = '\0'; ; ) {
	char wordBuf[18];
	sprintf( wordBuf, "\"%s\"", bwi->words[i] );
	strcat( buf, wordBuf );
	if ( ++i == bwi->nWords ) {
	    break;
	}
	strcat( buf, ", " );
    }
} /* formatBadWords */

static XP_Bool
frank_util_warnIllegalWord( XW_UtilCtxt* uc, BadWordInfo* bwi, 
			    XP_U16 turn, XP_Bool turnLost )
{
    char buf[200];
    char wordsBuf[150];
    XP_Bool result;
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;

    formatBadWords( bwi, wordsBuf );

    if ( turnLost ) {
	XP_UCHAR* name = self->fGameInfo.players[turn].name;
	XP_ASSERT( !!name );
	sprintf( buf, "Player %d (%s) played illegal word[s] "
		 "%s; loses turn",
		 turn+1, name, wordsBuf );
	(void)GUI_Alert( ALERT_ERROR, buf ); 
	result = XP_TRUE;
    } else {
	sprintf( buf, "Word %s not in the current dictionary. "
		 "Use it anyway?", wordsBuf );
	result = GUI_Alert( ALERT_OK, buf ); 
    }
    return result;
} /* frank_util_warnIllegalWord */

#ifdef SHOW_PROGRESS
static void
frank_util_engineStarting( XW_UtilCtxt* uc )
{
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;
    self->startProgressBar();
} /* frank_util_engineStarting */

static void
frank_util_engineStopping( XW_UtilCtxt* uc )
{
    CXWordsWindow* self = (CXWordsWindow*)uc->closure;
    self->finishProgressBar();
} /* frank_util_engineStopping */
#endif /* SHOW_PROGRESS */
