/* -*- compile-command: "cd ../linux && make MEMDEBUG=TRUE -j3"; -*- */
/* 
 * Copyright 1997 - 2011 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.
 */

/* Re: boards that can't fit on the screen.  Let's have an assumption, that
 * the tray is always either below the board or overlapping its bottom.  There
 * is never any board visible below the tray.  But it's possible to have a
 * board small enough that scrolling is necessary even with the tray hidden.
 *
 * Currently we don't specify the board bounds.  We give top,left and the size
 * of cells, and the board figures out the bounds.  That's probably a mistake.
 * Better to give bounds, and maybe a min scale, and let it figure out how
 * many cells can be visible.  Could it also decide if the tray should overlap
 * or be below?  Some platforms have to own that decision since the tray is
 * narrower than the board.  So give them separate bounds-setting functions,
 * and let the board code figure out if they overlap.
 *
 * Problem: the board size must always be a multiple of the scale.  The
 * platform-specific code has an easy time doing that math.  The board can't:
 * it'd have to take bounds, then spit them back out slightly modified.  It'd
 * also have to refuse to work (maybe just assert) if asked to take bounds
 * before it had a min_scale.
 *
 * Another way of looking at it closer to the current: the board's position
 * and the tray's bounds determine the board's bounds.  If the board's vScale
 * times the number of rows places its would-be bottom at or above the bottom
 * of the tray, then it's potentially visible.  If its would-be bottom is
 * above the top of the tray, no scrolling is needed.  But if it's below the
 * tray entirely then scrolling will happen even with the tray hidden.  As
 * above, we assume the board never appears below the tray.
 */

#include "comtypes.h"
#include "board.h"
#include "scorebdp.h"
#include "game.h"
#include "server.h"
#include "comms.h" /* for CHANNEL_NONE */
#include "dictnry.h"
#include "draw.h"
#include "engine.h"
#include "util.h"
#include "mempool.h" /* debug only */
#include "memstream.h"
#include "strutils.h"
#include "LocalizedStrIncludes.h"

#include "boardp.h"
#include "dragdrpp.h"
#include "dbgutil.h"

#ifdef CPLUS
extern "C" {
#endif

static XP_Bool drawCell( BoardCtxt* board, XP_U16 col, XP_U16 row, 
                         XP_Bool skipBlanks );
static void drawBoard( BoardCtxt* board );
static void scrollIfCan( BoardCtxt* board );
#ifdef KEYBOARD_NAV
static XP_Bool cellFocused( const BoardCtxt* board, XP_U16 col, XP_U16 row );
#endif
#ifdef XWFEATURE_MINIWIN
static void drawTradeWindowIf( BoardCtxt* board );
#else
# define drawTradeWindowIf( board )
#endif


#ifdef XWFEATURE_SEARCHLIMIT
static HintAtts figureHintAtts( BoardCtxt* board, XP_U16 col, XP_U16 row );
#else
# define figureHintAtts(b,c,r) HINT_BORDER_NONE
#endif


#ifdef POINTER_SUPPORT
static void drawDragTileIf( BoardCtxt* board );
#endif

#ifdef KEYBOARD_NAV
#ifdef PERIMETER_FOCUS
static void
invalOldPerimeter( BoardCtxt* board )
{
    /* We need to inval the center of the row that's moving into the center
       from a border (at which point it got borders drawn on it.) */
    ScrollData* vsd = &board->sd[SCROLL_V];
    XP_S16 diff = vsd->offset - board->prevYScrollOffset;
    XP_U16 firstRow, lastRow;
    XP_ASSERT( diff != 0 );
    if ( diff < 0 ) {
        /* moving up; inval row previously on bottom */
        firstRow = vsd->offset + 1;
        lastRow = board->prevYScrollOffset;
    } else {
        XP_U16 nVisible = vsd->lastVisible - vsd->offset + 1;
        lastRow = board->prevYScrollOffset + nVisible - 1;
        firstRow = lastRow - diff + 1;
    }
    XP_ASSERT( firstRow <= lastRow );
    while ( firstRow <= lastRow ) {
        board->redrawFlags[firstRow] |= ~0;
        ++firstRow;
    }
} /* invalOldPerimeter */
#endif
#endif

/* if any of a blank's neighbors is invalid, so must the blank become (since
 * they share a border and drawing the neighbor will redraw the blank's border
 * too) We'll want to redraw only those blanks that are themselves already
 * invalid *OR* that become invalid this way, and so we'll build a new
 * BlankQueue of them and replace the old.
 *
 * I'm not sure what happens if two blanks are neighbors.
 */
#define INVAL_BIT_SET(b,c,r) (((b)->redrawFlags[(r)] & (1 <<(c))) != 0)
static void
invalBlanksWithNeighbors( BoardCtxt* board, BlankQueue* bqp ) 
{
    XP_U16 ii;
    XP_U16 lastCol, lastRow;
    BlankQueue invalBlanks;
    XP_U16 nInvalBlanks = 0;

    lastCol = model_numCols(board->model) - 1;
    lastRow = model_numRows(board->model) - 1;

    for ( ii = 0; ii < bqp->nBlanks; ++ii ) {
        XP_U16 modelCol = bqp->col[ii];
        XP_U16 modelRow = bqp->row[ii];
        XP_U16 col, row;

        flipIf( board, modelCol, modelRow, &col, &row );

        if ( INVAL_BIT_SET( board, col, row )
             || (col > 0 && INVAL_BIT_SET( board, col-1, row ))
             || (col < lastCol && INVAL_BIT_SET( board, col+1, row ))
             || (row > 0 && INVAL_BIT_SET( board, col, row-1 ))
             || (row < lastRow && INVAL_BIT_SET( board, col, row+1 )) ) {

            invalCell( board, col, row );

            invalBlanks.col[nInvalBlanks] = (XP_U8)col;
            invalBlanks.row[nInvalBlanks] = (XP_U8)row;
            ++nInvalBlanks;
        }
    }
    invalBlanks.nBlanks = nInvalBlanks;
    XP_MEMCPY( bqp, &invalBlanks, sizeof(*bqp) );
} /* invalBlanksWithNeighbors */


#ifdef XWFEATURE_SEARCHLIMIT
static HintAtts
figureHintAtts( BoardCtxt* board, XP_U16 col, XP_U16 row )
{
    HintAtts result = HINT_BORDER_NONE;

    /* while lets us break to exit... */
    while ( board->trayVisState == TRAY_REVEALED
            && !board->gi->hintsNotAllowed
            && board->gi->allowHintRect ) {
        BdHintLimits limits;
        if ( dragDropGetHintLimits( board, &limits ) ) {
            /* do nothing */
        } else if ( board->selInfo->hasHintRect ) {
            limits = board->selInfo->limits;
        } else {
            break;
        }

        if ( col < limits.left ) break;
        if ( row < limits.top ) break;
        if ( col > limits.right ) break;
        if ( row > limits.bottom ) break;

        if ( col == limits.left ) {
            result |= HINT_BORDER_LEFT;
        }
        if ( col == limits.right ) {
            result |= HINT_BORDER_RIGHT;
        }
        if ( row == limits.top) {
            result |= HINT_BORDER_TOP;
        }
        if ( row == limits.bottom ) {
            result |= HINT_BORDER_BOTTOM;
        }
#ifndef XWFEATURE_SEARCHLIMIT_DOCENTERS
        if ( result == HINT_BORDER_NONE ) {
            result = HINT_BORDER_CENTER;
        }
#endif
        break;
    }

    return result;
} /* figureHintAtts */
#endif

XP_Bool
rectContainsRect( const XP_Rect* rect1, const XP_Rect* rect2 )
{
    return ( rect1->top <= rect2->top
             && rect1->left <= rect2->left
             && rect1->top + rect1->height >= rect2->top + rect2->height
             && rect1->left + rect1->width >= rect2->left + rect2->width );
} /* rectContainsRect */

#ifdef XWFEATURE_MINIWIN
static void
makeMiniWindowForTrade( BoardCtxt* board )
{
    const XP_UCHAR* text;

    text = draw_getMiniWText( board->draw, INTRADE_MW_TEXT );

    makeMiniWindowForText( board, text, MINIWINDOW_TRADING );
} /* makeMiniWindowForTrade */
#endif

#ifdef XWFEATURE_CROSSHAIRS
static CellFlags
flagsForCrosshairs( const BoardCtxt* board, XP_U16 col, XP_U16 row )
{
    CellFlags flags = 0;
    if ( ! board->hideCrosshairs ) {
        XP_Bool inHor, inVert;
        dragDropInCrosshairs( board, col, row, &inHor, &inVert );
        if ( inHor ) {
            flags |= CELL_CROSSHOR;
        }
        if ( inVert ) {
            flags |= CELL_CROSSVERT;
        }
    }
    return flags;
}
#endif

static void
drawBoard( BoardCtxt* board )
{
    if ( board->needsDrawing 
         && draw_boardBegin( board->draw, &board->boardBounds, 
                             board->sd[SCROLL_H].scale,
                             board->sd[SCROLL_V].scale,
                             dfsFor( board, OBJ_BOARD ) ) ) {

        XP_Bool allDrawn = XP_TRUE;
        XP_S16 ii;
        XP_S16 col, row, nVisCols;
        ModelCtxt* model = board->model;
        BoardArrow const* arrow = NULL;
        ScrollData* hsd = &board->sd[SCROLL_H];
        ScrollData* vsd = &board->sd[SCROLL_V];
        BlankQueue bq;

        scrollIfCan( board ); /* this must happen before we count blanks
                                 since it invalidates squares */

        /* This is freaking expensive!!!! PENDING FIXME Can't we start from
           what's invalid rather than scanning the entire model every time
           somebody dirties a single cell? */
        model_listPlacedBlanks( model, board->selPlayer, 
                                board->trayVisState == TRAY_REVEALED, &bq );
        dragDropAppendBlank( board, &bq );
        invalBlanksWithNeighbors( board, &bq );

        /* figure out now, before clearing inval bits, if we'll need to draw
           the arrow later */
        if ( board->trayVisState == TRAY_REVEALED ) {
            BoardArrow const* tmpArrow = &board->selInfo->boardArrow;
            if ( tmpArrow->visible ) {
                XP_U16 col = tmpArrow->col;
                XP_U16 row = tmpArrow->row;
                if ( INVAL_BIT_SET( board, col, row )
                     && !cellOccupied( board, col, row, XP_TRUE ) ) {
                    arrow = tmpArrow;
                }
            }
        }
        
        nVisCols = model_numCols( model ) - board->zoomCount;
        for ( row = vsd->offset; row <= vsd->lastVisible; ++row ) {
            RowFlags rowFlags = board->redrawFlags[row];
            if ( rowFlags != 0 ) {
                RowFlags failedBits = 0;
                for ( col = 0; col < nVisCols; ++col ) {
                    RowFlags colMask = 1 << (col + hsd->offset);
                    if ( 0 != (rowFlags & colMask) ) {
                        if ( !drawCell( board, col + hsd->offset,
                                        row, XP_TRUE )) {
                            failedBits |= colMask;
                            allDrawn = XP_FALSE;
                        }
                    }
                }
                board->redrawFlags[row] = failedBits;
            }
        }

        /* draw the blanks we skipped before */
        for ( ii = 0; ii < bq.nBlanks; ++ii ) {
            if ( !drawCell( board, bq.col[ii], bq.row[ii], XP_FALSE ) ) {
                allDrawn = XP_FALSE;
            }
        }

        if ( !!arrow ) {
            XP_U16 col = arrow->col;
            XP_U16 row = arrow->row;
            XP_Rect arrowRect;
            if ( getCellRect( board, col, row, &arrowRect ) ) {
                XWBonusType bonus;
                HintAtts hintAtts;
                CellFlags flags = CELL_NONE;
                bonus = model_getSquareBonus( model, col, row );
                hintAtts = figureHintAtts( board, col, row );
#ifdef KEYBOARD_NAV
                if ( cellFocused( board, col, row ) ) {
                    flags |= CELL_ISCURSOR;
                }
#endif
#ifdef XWFEATURE_CROSSHAIRS
                flags |= flagsForCrosshairs( board, col, row );
#endif

                draw_drawBoardArrow( board->draw, &arrowRect, bonus, 
                                     arrow->vert, hintAtts, flags );
            }
        }

        /* I doubt the two of these can happen at the same time */
        drawTradeWindowIf( board );
#ifdef POINTER_SUPPORT
        drawDragTileIf( board );
#endif
        draw_objFinished( board->draw, OBJ_BOARD, &board->boardBounds, 
                          dfsFor( board, OBJ_BOARD ) );

        board->needsDrawing = !allDrawn;
    }
} /* drawBoard */


static XP_Bool
drawCell( BoardCtxt* board, XP_U16 col, XP_U16 row, XP_Bool skipBlanks )
{
    XP_Bool success = XP_TRUE;
    XP_Rect cellRect;
    Tile tile;
    XP_Bool isBlank, isEmpty, recent, pending = XP_FALSE;
    XWBonusType bonus;
    ModelCtxt* model = board->model;
    DictionaryCtxt* dict = model_getDictionary( model );
    XP_U16 modelCol, modelRow;

    if ( dict != NULL && getCellRect( board, col, row, &cellRect ) ) {

        /* We want to invert EITHER the current pending tiles OR the most recent
         * move.  So if the tray is visible AND there are tiles missing from it,
         * show them.  Otherwise show the most recent move.
         */
        XP_U16 selPlayer = board->selPlayer;
        XP_U16 curCount = model_getCurrentMoveCount( model, selPlayer );
        XP_Bool showPending = board->trayVisState == TRAY_REVEALED
            && curCount > 0;

        flipIf( board, col, row, &modelCol, &modelRow );

        /* This 'while' is only here so I can 'break' below */
        while ( board->trayVisState == TRAY_HIDDEN ||
                !rectContainsRect( &board->trayBounds, &cellRect ) ) {
            XP_UCHAR ch[4] = {'\0'};
            XP_S16 owner = -1;
            XP_Bool invert = XP_FALSE;
            XP_Bitmaps bitmaps;
            XP_Bitmaps* bptr = NULL;
            const XP_UCHAR* textP = NULL;
            HintAtts hintAtts;
            CellFlags flags = CELL_NONE;
            XP_Bool isOrigin;
            XP_U16 value = 0;

            isEmpty = !model_getTile( model, modelCol, modelRow, showPending,
                                        selPlayer, &tile, &isBlank,
                                        &pending, &recent );
            if ( dragDropIsBeingDragged( board, col, row, &isOrigin ) ) {
                flags |= isOrigin? CELL_DRAGSRC : CELL_DRAGCUR;
                if ( isEmpty && !isOrigin ) {
                    dragDropTileInfo( board, &tile, &isBlank );
                    isEmpty = XP_FALSE;
                }
                showPending = pending = XP_TRUE;
            }

            if ( isEmpty ) {
                isBlank = XP_FALSE;
                flags |= CELL_ISEMPTY;
            } else if ( isBlank && skipBlanks ) {
                break;
            } else {
                Tile valTile = isBlank? dict_getBlankTile( dict ) : tile;
                value = dict_getTileValue( dict, valTile );

                if ( board->showColors ) {
                    owner = (XP_S16)model_getCellOwner( model, modelCol, 
                                                        modelRow );
                }

                invert = showPending? pending : recent;

                if ( board->showCellValues ) {
                    XP_SNPRINTF( ch, VSIZE(ch), "%d", value );
                    textP = ch;
                } else {
                    if ( dict_faceIsBitmap( dict, tile ) ) {
                        dict_getFaceBitmaps( dict, tile, &bitmaps );
                        bptr = &bitmaps;
                    }
                    textP = dict_getTileString( dict, tile );
                }
            }
            bonus = model_getSquareBonus( model, col, row );
            hintAtts = figureHintAtts( board, col, row );

            if ( (col==board->star_row) && (row==board->star_row) ) {
                flags |= CELL_ISSTAR;
            }
            if ( invert ) {
                flags |= CELL_HIGHLIGHT;
            }
            if ( isBlank ) {
                flags |= CELL_ISBLANK;
            }
#ifdef KEYBOARD_NAV
            if ( cellFocused( board, col, row ) ) {
                flags |= CELL_ISCURSOR;
            }
#endif
#ifdef XWFEATURE_CROSSHAIRS
            flags |= flagsForCrosshairs( board, col, row );
#endif

            success = draw_drawCell( board->draw, &cellRect, textP, bptr,
                                     tile, value, owner, bonus, hintAtts, 
                                     flags );
            break;
        }
    }
    return success;
} /* drawCell */

#ifdef KEYBOARD_NAV
DrawFocusState
dfsFor( BoardCtxt* board, BoardObjectType obj )
{
    DrawFocusState dfs;
    if ( (board->focussed == obj) && !board->hideFocus ) {
        if ( board->focusHasDived ) {
            dfs = DFS_DIVED;
        } else {
            dfs = DFS_TOP;
        }
    } else {
        dfs = DFS_NONE;
    }
    return dfs;
} /* dfsFor */

static XP_Bool
cellFocused( const BoardCtxt* board, XP_U16 col, XP_U16 row )
{
    XP_Bool focussed = XP_FALSE;
    const ScrollData* hsd = &board->sd[SCROLL_H];
    const ScrollData* vsd = &board->sd[SCROLL_V];
    if ( (board->focussed == OBJ_BOARD) && !board->hideFocus ) {
        if ( board->focusHasDived ) {
            if ( (col == board->selInfo->bdCursor.col)
                 && (row == board->selInfo->bdCursor.row) ) {
                focussed = XP_TRUE;
            }
        } else {
#ifdef PERIMETER_FOCUS
            focussed = (col == hsd->offset)
                || (col == hsd->lastVisible)
                || (row == vsd->offset)
                || (row == vsd->lastVisible);
#else
            focussed = XP_TRUE;
#endif
        }
    }
    return focussed;
} /* cellFocused */
#endif

#ifdef POINTER_SUPPORT
static void
drawDragTileIf( BoardCtxt* board )
{
    if ( dragDropInProgress( board ) ) {
        XP_U16 col, row;
        if ( dragDropGetBoardTile( board, &col, &row ) ) {
            XP_Rect rect;
            Tile tile;
            XP_Bool isBlank;
            const XP_UCHAR* face;
            XP_Bitmaps bitmaps;
            XP_S16 value;
            CellFlags flags;

            getDragCellRect( board, col, row, &rect );

            dragDropTileInfo( board, &tile, &isBlank );

            face = getTileDrawInfo( board, tile, isBlank, &bitmaps, 
                                    &value );

            flags = CELL_DRAGCUR;
            if ( isBlank ) {
                flags |= CELL_ISBLANK;
            }
            if ( board->hideValsInTray && !board->showCellValues ) {
                flags |= CELL_VALHIDDEN;
            }
            draw_drawTileMidDrag( board->draw, &rect, face, 
                                  bitmaps.nBitmaps > 0 ? &bitmaps : NULL, 
                                  value, board->selPlayer, flags );
        }
    }
} /* drawDragTileIf */
#endif

static XP_S16
sumRowHeights( const BoardCtxt* board, XP_U16 row1, XP_U16 row2 )
{
    const ScrollData* vsd = &board->sd[SCROLL_V];
    XP_S16 sign = row1 > row2 ? -1 : 1;
    XP_S16 result, ii;
    if ( sign < 0 ) {
        XP_U16 tmp = row1;
        row1 = row2;
        row2 = tmp;
    }
    for ( result = 0, ii = row1; ii < row2; ++ii ) {
        result += vsd->dims[ii];
    }
    return result * sign;
}

static void
scrollIfCan( BoardCtxt* board )
{
    ScrollData* vsd = &board->sd[SCROLL_V];
    if ( vsd->offset != board->prevYScrollOffset ) {
        XP_Rect scrollR = board->boardBounds;
        XP_Bool scrolled;
        XP_S16 dist;

#if defined KEYBOARD_NAV && defined PERIMETER_FOCUS
        if ( (board->focussed == OBJ_BOARD)
             && !board->focusHasDived 
             && !board->hideFocus ) {
            invalOldPerimeter( board );
        }
#endif
        invalSelTradeWindow( board );

        dist = sumRowHeights( board, board->prevYScrollOffset, vsd->offset );
        scrolled = draw_vertScrollBoard( board->draw, &scrollR, dist, 
                                         dfsFor( board, OBJ_BOARD ) );

        if ( scrolled ) {
            /* inval the rows that have been scrolled into view.  I'm cheating
               making the client figure the inval rect, but Palm's the first
               client and it does it so well.... */
            invalCellsUnderRect( board, &scrollR );
        } else {
            board_invalAll( board );
        }
        board->prevYScrollOffset = vsd->offset;
    }
} /* scrollIfCan */

#ifdef XWFEATURE_MINIWIN
static void
drawTradeWindowIf( BoardCtxt* board )
{
    if ( board->tradingMiniWindowInvalid &&
         TRADE_IN_PROGRESS(board) && board->trayVisState == TRAY_REVEALED ) {
        MiniWindowStuff* stuff;

        makeMiniWindowForTrade( board );

        stuff = &board->miniWindowStuff[MINIWINDOW_TRADING];
        draw_drawMiniWindow( board->draw, stuff->text,
                             &stuff->rect, (void**)NULL );

        board->tradingMiniWindowInvalid = XP_FALSE;
    }
} /* drawTradeWindowIf */
#endif

XP_Bool
board_draw( BoardCtxt* board )
{
    if ( !!board->draw && board->boardBounds.width > 0 ) {

        drawScoreBoard( board );

        drawTray( board );

        drawBoard( board );
    }
    return !board->needsDrawing && 0 == board->trayInvalBits;
} /* board_draw */

#ifdef CPLUS
}
#endif