/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */
/* 
 * Copyright 1997 - 2007 by Eric House (xwords@eehouse.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.
 */


#include "nwgamest.h"
#include "strutils.h"
#include "LocalizedStrIncludes.h"

#ifdef CPLUS
extern "C" {
#endif

#define NG_NUM_COLS ((XP_U16)(NG_COL_PASSWD+1))

struct NewGameCtx {
    NewGameEnableColProc enableColProc;
    NewGameEnableAttrProc enableAttrProc;
    NewGameSetColProc setColProc;
    NewGameGetColProc getColProc;
    NewGameSetAttrProc setAttrProc;
    XW_UtilCtxt* util;
    void* closure;

    /* Palm needs to store cleartext passwords separately in order to
       store '***' in the visible field */
    XP_TriEnable enabled[NG_NUM_COLS][MAX_NUM_PLAYERS];
    XP_U16 nPlayersShown;       /* real nPlayers lives in gi */
    XP_U16 nPlayersTotal;       /* used only until changedNPlayers set */
    XP_U16 nLocalPlayers;       /* not changed except in newg_load */
    DeviceRole role;
    XP_Bool isNewGame;
    XP_Bool changedNPlayers;
    XP_TriEnable juggleEnabled;

    MPSLOT
};

static void enableOne( NewGameCtx* ngc, XP_U16 player, NewGameColumn col,
                       XP_TriEnable enable, XP_Bool force );
static void adjustAllRows( NewGameCtx* ngc, XP_Bool force );
static void adjustOneRow( NewGameCtx* ngc, XP_U16 player, XP_Bool force );
static void setRoleStrings( NewGameCtx* ngc );
static void considerEnableJuggle( NewGameCtx* ngc );
static void storePlayer( NewGameCtx* ngc, XP_U16 player, LocalPlayer* lp );
static void loadPlayer( NewGameCtx* ngc, XP_U16 player, 
                        const LocalPlayer* lp );
#ifndef XWFEATURE_STANDALONE_ONLY
static XP_Bool changeRole( NewGameCtx* ngc, DeviceRole role );
static XP_Bool checkConsistent( NewGameCtx* ngc, XP_Bool warnUser );
#else
# define checkConsistent( ngc, warnUser ) XP_TRUE
#endif

NewGameCtx*
newg_make( MPFORMAL XP_Bool isNewGame, 
           XW_UtilCtxt* util,
           NewGameEnableColProc enableColProc, 
           NewGameEnableAttrProc enableAttrProc, 
           NewGameGetColProc getColProc, NewGameSetColProc setColProc,
           NewGameSetAttrProc setAttrProc, void* closure )
{
    NewGameCtx* result = XP_MALLOC( mpool, sizeof(*result) );
    XP_MEMSET( result, 0, sizeof(*result) );

    result->enableColProc = enableColProc;
    result->enableAttrProc = enableAttrProc;
    result->setColProc = setColProc;
    result->getColProc = getColProc;
    result->setAttrProc = setAttrProc;
    result->closure = closure;
    result->isNewGame = isNewGame;
    result->util = util;
    MPASSIGN(result->mpool, mpool);

    return result;
} /* newg_make */

void
newg_destroy( NewGameCtx* ngc )
{
    XP_FREE( ngc->mpool, ngc );
} /* newg_destroy */

void
newg_load( NewGameCtx* ngc, const CurGameInfo* gi )
{
    void* closure = ngc->closure;
    NGValue value;
    XP_U16 nPlayers, nLoaded;
    XP_S16 ii, jj;
    DeviceRole role;
    XP_Bool localOnly;
    XP_Bool shown[MAX_NUM_PLAYERS] = { XP_FALSE, XP_FALSE, XP_FALSE, XP_FALSE};

    ngc->juggleEnabled = TRI_ENAB_NONE;
    for ( ii = 0; ii < NG_NUM_COLS; ++ii ) {
        for ( jj = 0; jj < MAX_NUM_PLAYERS; ++jj ) {
            ngc->enabled[ii][jj] = TRI_ENAB_NONE;
        }
    }

    ngc->role = role = gi->serverRole;
    localOnly = role == SERVER_ISCLIENT && ngc->isNewGame;
#ifndef XWFEATURE_STANDALONE_ONLY
    value.ng_role = role;
    (*ngc->setAttrProc)( closure, NG_ATTR_ROLE, value );
    (*ngc->enableAttrProc)( closure, NG_ATTR_ROLE, ngc->isNewGame? 
                            TRI_ENAB_ENABLED : TRI_ENAB_DISABLED );
#endif

    nPlayers = gi->nPlayers;
    ngc->nPlayersTotal = nPlayers;
#ifndef XWFEATURE_STANDALONE_ONLY
    for ( ii = nPlayers - 1; ii >= 0; --ii ) {
        if ( gi->players[ii].isLocal ) {
            ++ngc->nLocalPlayers;
        }
    }
#endif
    if ( localOnly ) {
        nPlayers = ngc->nLocalPlayers;
    }
    ngc->nPlayersShown = nPlayers;
        
    value.ng_u16 = ngc->nPlayersShown;
    (*ngc->setAttrProc)( closure, NG_ATTR_NPLAYERS, value );
    (*ngc->enableAttrProc)( closure, NG_ATTR_NPLAYERS, ngc->isNewGame?
                            TRI_ENAB_ENABLED : TRI_ENAB_DISABLED );

    setRoleStrings( ngc );
    considerEnableJuggle( ngc );   

    /* Load local players first */
    nLoaded = 0;
    do {
        for ( ii = 0; ii < MAX_NUM_PLAYERS; ++ii ) {
            if ( !shown[ii] ) {
                const LocalPlayer* lp = &gi->players[ii];
                if ( !localOnly
                     || (lp->isLocal && (nLoaded < ngc->nLocalPlayers)) ) {
                    shown[ii] = XP_TRUE;
                    loadPlayer( ngc, nLoaded++, lp );
                }
            }
        }
        XP_ASSERT( localOnly || nLoaded == MAX_NUM_PLAYERS );
        localOnly = XP_FALSE;   /* for second pass */
    } while ( nLoaded < MAX_NUM_PLAYERS );
    
    adjustAllRows( ngc, XP_TRUE );
} /* newg_load */

typedef struct NGCopyClosure {
    XP_U16 player;
    NewGameColumn col;
    NewGameCtx* ngc;
    LocalPlayer* lp;
} NGCopyClosure;

static void
cpToLP( NGValue value, const void* cbClosure )
{
    NGCopyClosure* cpcl = (NGCopyClosure*)cbClosure;
    LocalPlayer* lp = cpcl->lp;
    XP_UCHAR** strAddr = NULL;
    
    switch ( cpcl->col ) {
#ifndef XWFEATURE_STANDALONE_ONLY
    case NG_COL_REMOTE:
        lp->isLocal = !value.ng_bool;
        break;
#endif
    case NG_COL_NAME:
        strAddr = &lp->name;
        break;
    case NG_COL_PASSWD:
        strAddr = &lp->password;
        break;
    case NG_COL_ROBOT:
        lp->isRobot = value.ng_bool;
        break;
    }

    if ( !!strAddr ) {
        /* This is leaking!!!  But doesn't leak if am playing via IR, at least
           in the simulator.  */
        replaceStringIfDifferent( cpcl->ngc->mpool, strAddr,
                                  value.ng_cp );
    }
} /* cpToLP */

XP_Bool
newg_store( NewGameCtx* ngc, CurGameInfo* gi, 
            XP_Bool XP_UNUSED_STANDALONE(warn) )
{
    XP_U16 player;
    XP_Bool consistent = checkConsistent( ngc, warn );

    if ( consistent ) {
        gi->nPlayers = ngc->nPlayersShown;
#ifndef XWFEATURE_STANDALONE_ONLY
        gi->serverRole = ngc->role;
#endif

        for ( player = 0; player < MAX_NUM_PLAYERS; ++player ) {
            storePlayer( ngc, player, &gi->players[player] );
        }
    }
    return consistent;
} /* newg_store */

void
newg_colChanged( NewGameCtx* ngc, XP_U16 player )
{
    /* Sometimes we'll get this notification for inactive rows, e.g. when
       setting default values. */
    if ( player < ngc->nPlayersShown ) {
        adjustOneRow( ngc, player, XP_FALSE );
    }
}

void
newg_attrChanged( NewGameCtx* ngc, NewGameAttr attr, NGValue value )
{
    XP_Bool changed = XP_FALSE;
    if ( attr == NG_ATTR_NPLAYERS ) {
        if ( ngc->nPlayersShown != value.ng_u16 ) {
            ngc->nPlayersShown = value.ng_u16;
            ngc->changedNPlayers = XP_TRUE;
            changed = XP_TRUE;
        }
#ifndef XWFEATURE_STANDALONE_ONLY
    } else if ( NG_ATTR_ROLE == attr ) { 
        changed = changeRole( ngc, value.ng_role );
#endif
    } else {
        XP_ASSERT( 0 );
    }

    if ( changed ) {
        considerEnableJuggle( ngc );
        adjustAllRows( ngc, XP_FALSE );
    }
}

typedef struct DeepValue {
    NGValue value;
    NewGameColumn col;
    MPSLOT
} DeepValue;

static void
deepCopy( NGValue value, const void* closure )
{
    DeepValue* dvp = (DeepValue*)closure;
    switch ( dvp->col ) {
    case NG_COL_ROBOT:
#ifndef XWFEATURE_STANDALONE_ONLY
    case NG_COL_REMOTE:
#endif
        dvp->value.ng_bool = value.ng_bool;
        break;
    case NG_COL_NAME:
    case NG_COL_PASSWD:
        dvp->value.ng_cp = copyString( dvp->mpool, value.ng_cp );
        break;
    }
}

XP_Bool
newg_juggle( NewGameCtx* ngc )
{
    XP_Bool changed = XP_FALSE;
    XP_U16 nPlayers = ngc->nPlayersShown;

    XP_ASSERT( ngc->isNewGame );
    
    if ( nPlayers > 1 ) {
        LocalPlayer tmpPlayers[MAX_NUM_PLAYERS];
        XP_U16 pos[MAX_NUM_PLAYERS];
        XP_U16 player;

        /* Get a randomly juggled array of numbers 0..nPlayers-1.  Then the
           number at pos[n] inicates where the entry currently at n should
           be. */
        changed = randIntArray( pos, nPlayers );
        if ( changed ) {

            /* Deep-copy off to tmp storage.  But skip lines that won't be moved
               in the juggle. */
            XP_MEMSET( &tmpPlayers, 0, sizeof(tmpPlayers) );
            for ( player = 0; player < nPlayers; ++player ) {
                if ( player != pos[player] ) {
                    storePlayer( ngc, player, &tmpPlayers[player] );
                }
            }

            for ( player = 0; player < nPlayers; ++player ) {
                if ( player != pos[player] ) {
                    LocalPlayer* lp = &tmpPlayers[player];
                    XP_U16 dest = pos[player];

                    loadPlayer( ngc, dest, lp );

                    if ( !!lp->name ) {
                        XP_FREE( ngc->mpool, lp->name );
                    }
                    if ( !!lp->password ) {
                        XP_FREE( ngc->mpool, lp->password );
                    }

                    adjustOneRow( ngc, dest, XP_FALSE );
                }
            }
        }
    }
    return changed;
} /* newg_juggle */

#ifndef XWFEATURE_STANDALONE_ONLY
static XP_Bool
checkConsistent( NewGameCtx* ngc, XP_Bool warnUser )
{
    XP_Bool consistent;
    XP_U16 i;

    /* If ISSERVER, make sure there's at least one non-local player. */
    consistent = ngc->role != SERVER_ISSERVER;
    for ( i = 0; !consistent && i < ngc->nPlayersShown; ++i ) {
        DeepValue dValue;
        dValue.col = NG_COL_REMOTE;
        (*ngc->getColProc)( ngc->closure, i, NG_COL_REMOTE,
                            deepCopy, &dValue );
        if ( dValue.value.ng_bool ) {
            consistent = XP_TRUE;
        }
    }
    if ( !consistent && warnUser ) {
        util_userError( ngc->util, ERR_REG_SERVER_SANS_REMOTE );
    }

    /* Add other consistency checks, and error messages, here. */

    return consistent;
} /* checkConsistent */
#endif

static void
enableOne( NewGameCtx* ngc, XP_U16 player, NewGameColumn col, 
           XP_TriEnable enable, XP_Bool force )
{
    XP_TriEnable* esp = &ngc->enabled[col][player];
    if ( force || (*esp != enable) ) {
        (*ngc->enableColProc)( ngc->closure, player, col, enable );
    }
    *esp = enable;
} /* enableOne */

static void
adjustAllRows( NewGameCtx* ngc, XP_Bool force )
{
    XP_U16 player;
    for ( player = 0; player < MAX_NUM_PLAYERS; ++player ) {
        adjustOneRow( ngc, player, force );
    }
} /* adjustAllRows */

static void
adjustOneRow( NewGameCtx* ngc, XP_U16 player, XP_Bool force )
{
    XP_TriEnable enable[NG_NUM_COLS];
    NewGameColumn col;
    XP_Bool isLocal = XP_TRUE;
    XP_Bool isNewGame = ngc->isNewGame;
    DeviceRole role = ngc->role;
    DeepValue dValue;

    for ( col = 0; col < NG_NUM_COLS; ++col ) {
        enable[col] = TRI_ENAB_HIDDEN;
    }

    /* If there aren't this many players, all are disabled */
    if ( player >= ngc->nPlayersShown ) {
        /* do nothing: all are hidden above */
    } else {
#ifndef XWFEATURE_STANDALONE_ONLY
        /* If standalone or client, remote is hidden.  If server but not
           new game, it's disabled */
        if ( (role == SERVER_ISSERVER ) 
             || (role == SERVER_ISCLIENT && !isNewGame ) ) {
            if ( isNewGame ) {
                enable[NG_COL_REMOTE] = TRI_ENAB_ENABLED;
            } else {
                enable[NG_COL_REMOTE] = TRI_ENAB_DISABLED;
            }
            dValue.col = NG_COL_REMOTE;
            (*ngc->getColProc)( ngc->closure, player, NG_COL_REMOTE,
                                deepCopy, &dValue );
            isLocal = !dValue.value.ng_bool;
        }
#endif

        /* If remote is enabled and set, then if it's a new game all else is
           hidden.  But if it's not a new game, they're disabled.  Password is
           always hidden if robot is set. */
        if ( isLocal ) {
            XP_TriEnable tmp;

            /* No changing name or robotness since they're sent to remote
               host. */
            tmp = (isNewGame || role == SERVER_STANDALONE)? 
                TRI_ENAB_ENABLED:TRI_ENAB_DISABLED;
            enable[NG_COL_NAME] = tmp;
            enable[NG_COL_ROBOT] = tmp;

            /* Password and game info (the not isNewGame case): passwords are
               not transmitted: they're local only.  There's no harm in
               allowing local players to change them.  So passwords should be
               enabled whenever it's not a robot regardless of both isNewGame
               and role. */

            dValue.col = NG_COL_ROBOT;
            (*ngc->getColProc)( ngc->closure, player, NG_COL_ROBOT, deepCopy,
                                &dValue );
            if ( !dValue.value.ng_bool ) {
                /* If it's a robot, leave it hidden */
                enable[NG_COL_PASSWD] = TRI_ENAB_ENABLED;
            }
                                  
        } else {
            if ( isNewGame ) {
                /* leave 'em hidden */
            } else {
                enable[NG_COL_NAME] = TRI_ENAB_DISABLED;
                enable[NG_COL_ROBOT] = TRI_ENAB_DISABLED;
                /* leave passwd hidden */
            }
        }
    }

    for ( col = 0; col < NG_NUM_COLS; ++col ) {
        enableOne( ngc, player, col, enable[col], force );
    }
} /* adjustOneRow */

/* changeRole.  When role changes, number of players displayed, and which
 * players, may change.  Host shows all players (up to nPlayers).  Guest shows
 * only local players, but if role changes should show the rest.  Change from
 * Host or Standalone to guest should reduce the number shown.
 * 
 * Here's the fun part: what happens when user changes nPlayers, then changes
 * role?  Say we're a guest with one player.  User makes it two, than makes us
 * host.  Do we pull in a new player?  No.  Let's not change any of this stuff
 * ONCE USER'S CHANGED NPLAYERS.  Goal is to prevent his having to do that for
 * the most common case, which is playing again with the same players.  In
 * that case changing role then back again should not lose/change data.
 */
#ifndef XWFEATURE_STANDALONE_ONLY
static XP_Bool
changeRole( NewGameCtx* ngc, DeviceRole newRole )
{
    DeviceRole oldRole = ngc->role;
    XP_Bool changing = oldRole != newRole;
    if ( changing ) {
        if ( !ngc->changedNPlayers ) {
            NGValue value;
            if ( newRole == SERVER_ISCLIENT ) {
                value.ng_u16 = ngc->nLocalPlayers;
            } else {
                value.ng_u16 = ngc->nPlayersTotal;
            }
            if ( value.ng_u16 != ngc->nPlayersShown ) {
                ngc->nPlayersShown = value.ng_u16;
                (*ngc->setAttrProc)( ngc->closure, NG_ATTR_NPLAYERS, value );
            }
        }
        ngc->role = newRole;
        setRoleStrings( ngc );
    }
    return changing;
}
#endif

static void
setRoleStrings( NewGameCtx* ngc )
{
    XP_U16 strID;
    NGValue value;
    void* closure = ngc->closure;
    /* Tell client to set/change players label text, and also to add remote
     checkbox column header if required. */

#ifndef XWFEATURE_STANDALONE_ONLY
    (*ngc->enableAttrProc)( closure, NG_ATTR_REMHEADER, 
                            ( (ngc->role == SERVER_ISSERVER)
                              || (!ngc->isNewGame
                                  && (ngc->role != SERVER_STANDALONE)) )?
                            TRI_ENAB_ENABLED : TRI_ENAB_HIDDEN );
#endif

    if ( 0 ) {
#ifndef XWFEATURE_STANDALONE_ONLY
    } else if ( ngc->role == SERVER_ISCLIENT && ngc->isNewGame ) {
        strID = STR_LOCALPLAYERS;
#endif
    } else {
        strID = STR_TOTALPLAYERS;
    }

    value.ng_cp = util_getUserString( ngc->util, strID );
    (*ngc->setAttrProc)( closure, NG_ATTR_NPLAYHEADER, value );
} /* setRoleStrings */

static void
considerEnableJuggle( NewGameCtx* ngc )
{
    XP_TriEnable newEnable;
    newEnable = (ngc->isNewGame && ngc->nPlayersShown > 1)?
        TRI_ENAB_ENABLED : TRI_ENAB_HIDDEN;

    if ( newEnable != ngc->juggleEnabled ) {
        (*ngc->enableAttrProc)( ngc->closure, NG_ATTR_CANJUGGLE, newEnable );
        ngc->juggleEnabled = newEnable;
    }
} /* considerEnableJuggle */

static void
storePlayer( NewGameCtx* ngc, XP_U16 player, LocalPlayer* lp )
{
    void* closure = ngc->closure;
    NGCopyClosure cpcl;
    cpcl.player = player;
    cpcl.ngc = ngc;
    cpcl.lp = lp;

    for ( cpcl.col = 0; cpcl.col < NG_NUM_COLS; ++cpcl.col ) {
        (*ngc->getColProc)( closure, cpcl.player, cpcl.col, 
                            cpToLP, &cpcl );
    }
}

static void
loadPlayer( NewGameCtx* ngc, XP_U16 player, const LocalPlayer* lp )
{
    NGValue value;
    void* closure = ngc->closure;

#ifndef XWFEATURE_STANDALONE_ONLY
    value.ng_bool = !lp->isLocal;
    (*ngc->setColProc)(closure, player, NG_COL_REMOTE, value );
#endif
    value.ng_cp = lp->name;
    (*ngc->setColProc)(closure, player, NG_COL_NAME, value );

    value.ng_cp = lp->password;
    (*ngc->setColProc)(closure, player, NG_COL_PASSWD, value );

    value.ng_bool = lp->isRobot;
    (*ngc->setColProc)(closure, player, NG_COL_ROBOT, value );
}

#ifdef CPLUS
}
#endif