

var DEFAULT_BOARD_WIDTH     = 8;
var DEFAULT_BOARD_HEIGHT    = 8;
var DEFAULT_CELL_SIZE       = 51;

// These are attacking moves
var pawn_moves      = [[-1, 1], [1, 1]]
var king_moves      = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]];
var knight_moves    = [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]];
var horiz_moves     = [[-1, 0], [1, 0], [0, -1], [0, 1]];
var diag_moves      = [[-1, -1], [-1, 1], [1, -1], [1, 1]];

// these say when a move is valid
// NO_ATTACK - when a move is valid ONLY if it is not
// attacking, so if the target square already has a piece in
// it, then the move is invalid
// MAY_ATTACK - Is valid if there target square is empty or
// when the target square contains an opposite piece.
// ONLY_ATTACK - Move is valid only if target square contains
// an opposite piece.
var NO_ATTACK       = 0;
var MAY_ATTACK      = 1;
var ONLY_ATTACK     = 2;

function make_piece(color, piece)
{
    return {'color': color, 'ptype': piece};
}

/*
 * A chessboard class.
 *
 * board_table_id   -   ID of the table element in which the board is to be
 * created.  This MUST be a table element.
 *
 * configs: Configs for the board. Options are:
 *      invert_board    -   Whether to show black or white at the bottom of
 *                          the board.  If true, black is shown at the
 *                          bottom.  Default: false.
 *
 *      cell_size       -   Width and Height of the squares on the board.
 *                          Default: 75 pixels.
 */
function ChessBoard(board_table_id, configs)
{
    this.reset = function()
    {
        this._process_configs(configs)

        this.setCurrentPlayer(-1);
        this._highlights    = {};
        this._kings         = [null, null];
        this._curr_player   = -1;
        this._table_id      = board_table_id;
        this.createBoard(DEFAULT_BOARD_WIDTH, DEFAULT_BOARD_HEIGHT);

        // 
        // Set default pieces for a default chess board
        //
        for (var x = 0;x < DEFAULT_BOARD_WIDTH;x ++)
        {
            this.setUnitAt(x, 1, make_piece(WHITE, PAWN));
            this.setUnitAt(x, DEFAULT_BOARD_WIDTH - 2, make_piece(BLACK, PAWN));
        }

        this.setUnitAt(0, 0, make_piece(WHITE, ROOK));
        this.setUnitAt(1, 0, make_piece(WHITE, KNIGHT));
        this.setUnitAt(2, 0, make_piece(WHITE, BISHOP));
        this.setUnitAt(3, 0, make_piece(WHITE, QUEEN));
        this.setUnitAt(4, 0, make_piece(WHITE, KING));
        this.setUnitAt(5, 0, make_piece(WHITE, BISHOP));
        this.setUnitAt(6, 0, make_piece(WHITE, KNIGHT));
        this.setUnitAt(7, 0, make_piece(WHITE, ROOK));

        this.setUnitAt(0, 7, make_piece(BLACK, ROOK));
        this.setUnitAt(1, 7, make_piece(BLACK, KNIGHT));
        this.setUnitAt(2, 7, make_piece(BLACK, BISHOP));
        this.setUnitAt(3, 7, make_piece(BLACK, QUEEN));
        this.setUnitAt(4, 7, make_piece(BLACK, KING));
        this.setUnitAt(5, 7, make_piece(BLACK, BISHOP));
        this.setUnitAt(6, 7, make_piece(BLACK, KNIGHT));
        this.setUnitAt(7, 7, make_piece(BLACK, ROOK));
    };

    // 
    // Gets all moves that are possible to a given cell by player of a
    // given color.
    //
    // Essentially a way of saying "who can attack this position".
    //
    this.getPossibleMovesTo = function(x, y, color)
    {
        var fwd = color == WHITE ? 1 : -1;
        var out = [];

        // check for pawn attacks
        // attacks from x - 1, y + fwd and x + 1, y + fwd
        if ((this._cell_in_bounds(x - 1, y - fwd) &&
                this._pieces[x - 1][y - fwd] &&
                this._pieces[x - 1][y - fwd]['color'] == color &&
                this._pieces[x - 1][y - fwd]['ptype'] == PAWN))
        {
            out.push([x - 1, y - fwd]);
        }
        if ((this._cell_in_bounds(x + 1, y - fwd) &&
                this._pieces[x + 1][y - fwd] &&
                this._pieces[x + 1][y - fwd]['color'] == color &&
                this._pieces[x + 1][y - fwd]['ptype'] == PAWN))
        {
            out.push([x + 1, y - fwd]);
        }

        // check for attack by opp king
        for (var nx = x - 1;nx <= x + 1;nx++)
        {
            for (var ny = y - 1;ny <= y + 1;ny++)
            {
                if ((nx != x || ny != y) &&
                    this._cell_in_bounds(nx, ny) &&
                    this._pieces[nx][ny] &&
                    this._pieces[nx][ny]['color'] == color &&
                    this._pieces[nx][ny]['ptype'] == KING)
                {
                    out.push([nx, ny]);
                }
            }
        }

        // check for knight attacks
        for (var move_index in knight_moves)
        {
            var move = knight_moves[move_index];
            var nx = x + move[0];
            var ny = y + move[1];
            if (this._cell_in_bounds(nx, ny) &&
                (this._pieces[nx][ny] &&
                 this._pieces[nx][ny]['color'] == color &&
                 this._pieces[nx][ny]['ptype'] == KNIGHT))
            {
                out.push([nx, ny]);
            }
        }

        // attacks from diagonal attacks (ie queen and bishop)
        var incs = diag_moves;
        for (var incindex in incs)
        {
            var xinc = incs[incindex][0];
            var yinc = incs[incindex][1];
            var stop = false;
            for (var inc = 1; ! stop ; inc++)
            {
                var nx = x + (inc * xinc);
                var ny = y + (yinc * inc);
                if (this._cell_in_bounds(nx, ny))
                {
                    var p = this._pieces[nx][ny];
                    if (p != null)
                    {
                        if (p['color'] == color &&
                            (p['ptype'] == QUEEN || p['ptype'] == BISHOP))
                        {
                            out.push([nx, ny]);
                        }
                        stop  = true;
                    }
                }
                else 
                {
                    stop = true;
                }
            }
        }

        // attacks from horizontal attacks (ie queen and rook)
        var incs = horiz_moves;
        for (var incindex in incs)
        {
            var xinc = incs[incindex][0];
            var yinc = incs[incindex][1];
            var stop = false;
            for (var inc = 1; ! stop ; inc++)
            {
                var nx = x + (inc * xinc);
                var ny = y + (yinc * inc);
                if (this._cell_in_bounds(nx, ny))
                {
                    var p = this._pieces[nx][ny];
                    if (p != null)
                    {
                        if (p['color'] == color &&
                            (p['ptype'] == QUEEN || p['ptype'] == ROOK))
                        {
                            out.push([nx, ny]);
                        }
                        stop  = true;
                    }
                }
                else 
                {
                    stop = true;
                }
            }
        }

        // attacks from diagonal or horizontal or vertical
        return out;
    }

    this.isCellUnderAttack = function(x, y, color)
    {
        var out = this.getPossibleMovesTo(x, y, color);
        return (out && out.length > 0);
    }

    //
    // Returns a list of positions from a given position.
    // Also we only return positions which wont result in check.
    //
    // TODO: Use the "move number" to restrict castle only to when neither
    // the king or the castle has been moved yet - Does this need a DB
    // entry or can it be deduced from the history?
    //
    this.getPossibleMovesFrom = function(x, y)
    {
        //
        // if no pieces then return null
        //
        var piece = this._pieces[x][y];

        if (piece == null)
            // nothing in this cell so return nothing
            return null;

        var ptype       = piece['ptype'];
        var color       = piece['color'];
        var oppcolor    = color == WHITE ? BLACK : WHITE;
        var fwd         = color == WHITE ? 1 : -1;
        var out         = [];

        // we have to get only moves that dont result in our king being
        // in check OR, dont LEAVE our king in check if we are already
        // in check.  So a move by a queen to attack a knight checking
        // our king is valid.
        var board       = this;
        function test_check_and_add(nx, ny, attack)
        {
            if (board._cell_in_bounds(nx, ny))
            {
                var p2                  = board._pieces[nx][ny];
                if ((p2 == null && attack <= MAY_ATTACK) ||
                    (p2 && p2['color'] == oppcolor && attack >= MAY_ATTACK))
                {
                    board.clearUnitAt(x, y);
                    board.setUnitAt(nx, ny, piece);
                    var king = board._kings[color];

                    if ( ! board.isCellUnderAttack(king['x'], king['y'], oppcolor))
                    {
                        out.push([nx, ny]);
                    }

                    board.setUnitAt(x, y, piece);
                    board.setUnitAt(nx, ny, p2);
                }
            }
        }

        if (ptype == KING)
        {
            // only need to check if the surrounding positions are ok.
            for (var nx = x - 1;nx <= x + 1;nx++)
            {
                for (var ny = y - 1;ny <= y + 1;ny++)
                {
                    if (nx != x || ny != y)
                    {
                        test_check_and_add(nx, ny, MAY_ATTACK);
                    }
                }
            }

            // check castling on a standard board only.
            if (this._board_width == 8 && this._board_height == 8 && x == 4)
            {
                if ((color == WHITE && y == 0) || (color == BLACK && y == 7))
                {
                    if (this._pieces[0][y]['ptype'] == ROOK)
                    {

                        var ok = true;
                        for (var nx = 2;ok && nx < x;nx++)
                        {
                            if (this._pieces[nx][y] != null ||
                                this.isCellUnderAttack(nx, y, oppcolor))
                            {
                                ok = false;
                            }
                        }
                        if (ok)
                            out.push([2, y]);
                    }
                    if (this._pieces[7][y]['ptype'] == ROOK)
                    {
                        var ok = true;
                        for (var nx = x + 1;ok && nx < 7;nx++)
                        {
                            if (this._pieces[nx][y] != null ||
                                this.isCellUnderAttack(nx, y, oppcolor))
                            {
                                ok = false;
                            }
                        }
                        if (ok)
                            out.push([6, y]);
                    }
                }
            }
        }
        else
        {
            if (ptype == PAWN)
            {
                test_check_and_add(x, y + fwd, NO_ATTACK);
                if (color == WHITE && y == 1 && this._pieces[x][2] == null)
                {
                    test_check_and_add(x, 3, NO_ATTACK);
                }
                else if (color == BLACK &&
                         y == this._board_height - 2 &&
                         this._pieces[x][this._board_height - 3] == null)
                {
                    test_check_and_add(x, this._board_height - 4, NO_ATTACK);
                }

                // check for attacking opportunities
                // TODO: handle en-passante
                for (var nx = x - 1;nx <= x + 1;nx += 2)
                {
                    test_check_and_add(nx, y + fwd, ONLY_ATTACK);
                }
            }
            else if (ptype == KNIGHT)
            {
                for (var move_index in knight_moves)
                {
                    var move = knight_moves[move_index];
                    test_check_and_add(x + move[0], y + move[1], MAY_ATTACK);
                }
            }
            else if (ptype == BISHOP || ptype == QUEEN)
            {
                // check diagonal moves till empty
                var incs = diag_moves;
                for (var incindex in incs)
                {
                    var xinc = incs[incindex][0];
                    var yinc = incs[incindex][1];
                    var stop = false;
                    for (var inc = 1; ! stop ; inc++)
                    {
                        var nx = x + (inc * xinc);
                        var ny = y + (inc * yinc);
                        test_check_and_add(nx, ny, MAY_ATTACK);
                        if (!this._cell_in_bounds(nx, ny) || this._pieces[nx][ny])
                            stop = true;
                    }
                }
            }
            if (ptype == ROOK || ptype == QUEEN)
            {
                // check diagonal moves till empty
                var incs = horiz_moves;
                for (var incindex in incs)
                {
                    var xinc = incs[incindex][0];
                    var yinc = incs[incindex][1];
                    var stop = false;
                    for (var inc = 1; ! stop ; inc++)
                    {
                        var nx = x + (inc * xinc);
                        var ny = y + (inc * yinc);
                        test_check_and_add(nx, ny, MAY_ATTACK);
                        if (!this._cell_in_bounds(nx, ny) || this._pieces[nx][ny])
                            stop = true;
                    }
                }
            }
        }
        return out;
    };

    this.clearUnitAt = function(x, y)
    {
        var piece = this._pieces[x][y];

        if (piece && piece['ptype'] == KING)
            this._kings[piece['color']] = null;

        this._pieces[x][y] = null;
        this.boardCell(x, y).html("");
        return piece;
    };

    this.setCurrentCellValue = function(color, piece)
    {
        if (this._curr_cell_x >= 0 && this._curr_cell_y >= 0)
        {
            this.setUnitAt(this._curr_cell_x, this._curr_cell_y, make_piece(color, piece));
        }
    };

    this.clearCurrentCell = function()
    {
        if (this._curr_cell_x >= 0 && this._curr_cell_y >= 0)
        {
            this.clearUnitAt(this._curr_cell_x, this._curr_cell_y);
        }
    };

    this.clearAllCells = function()
    {
        // set the kings to null
        this._kings = [null, null];
        for (var x = 0;x < this._board_width;x++)
        {
            for (var y = 0;y < this._board_height;y++)
            {
                this.clearUnitAt(x, y);
            }
        }
    };

    this.onCellClick = function(clickfn)
    {
        this._on_cell_click = clickfn;
    };

    /**
     * Loads board configuration
     */
    this.load = function(board_data)
    {
        this.clearAllCells();
        this.createBoard(board_data['width'], board_data['height']);

        // now the pieces
        var pieces = board_data['pieces'];

        for (var i = 0;i < pieces.length;i++)
        {
            var piece = pieces[i];

            this.setUnitAt(piece['x'], piece['y'], make_piece(piece['color'], piece['ptype']));
        }
    };

    /*
     * Sets whether moves are to be shown or not
     */
    this.setShowMoves = function(show)
    {
        this._show_moves = show;
    };

    /**
     * Tells whether moves are showing or not.
     */
    this.showingMoves = function()
    {
        return this._show_moves;
    };

    /*
     * Sets the current player - ie the player who can move pieces on the
     * board
     */
    this.setCurrentPlayer = function(color)
    {
        this._curr_player = color;
    };

    /*
     * Set the unit at a given (global) location.
     *
     * Returns the old piece.
     */
    this.setUnitAt = function(x, y, piece)
    {
        var data = null;
        var html = "";
        if (piece)
        {
            var color = piece['color'];
            var ptype = piece['ptype'];
            if (piece && piece['ptype'] >= 0 && piece['ptype'] <= KING)
            {
                if (ptype == KING)
                {
                    this._kings[color] = {'x': x, 'y': y};
                }
                data = piece;
                // html = "<img src = '/gfstatic/chess/images/" +
                html = "<img src = '/static/images/" +
                // html = "<img src = '/themes/chess/static/images/" +
                        (color == WHITE ? "white/49/" : "black/49/") +
                        piece_names[ptype] + ".png'>";
            }
        }
        var oldpiece = this._pieces[x][y];
        this._pieces[x][y] = data;
        this.boardCell(x, y).html(html);
        return oldpiece;
    };

    /*
     * Get the unit at a given location
     */
    this.getUnitAt = function(x, y)
    {
        return this._pieces[x][y];
    };

    /**
     * Gets the board's dimensions and the pieces it contains in JSON.
     */
    this.getBoardData = function()
    {
        var boardwidth  = this._board_width;
        var boardheight = this._board_height;
        var board_data  = {'width': boardwidth, 'height': boardheight};
        var piece_data  = [];

        for (var x = 0;x < boardwidth;x++)
        {
            for (var y = 0;y < boardheight;y++)
            {
                if (this._pieces[x][y] != null)
                {
                    piece_data.push({'x': x, 'y': y,
                                     'color': this._pieces[x][y]['color'],
                                     'ptype': this._pieces[x][y]['ptype']});
                }
            }
        }

        board_data['pieces'] = piece_data;
        return board_data;
    };

    /**
     * Gets all cells that have a particular highlight class set.
     */
    this.getCellsByClass = function(classname)
    {
        var out = [];
        if (classname in this._highlights)
        {
            for (var key in this._highlights[classname])
            {
                var val = this._highlights[classname][key];
                if (typeof val !== 'undefined' && val != null)
                {
                    var items = val.split(",");
                    out.append([items[0], items[1]]);
                }
            }
        }

        return out;
    };

    /**
     * Tells if a given cell has the particular class set.
     */
    this.cellHasClass = function(x, y, classname)
    {
        if (this._highlights[classname])
        {
            var cells = this._highlights[classname];
            for (var key in cells)
            {
                var cell = key.split(",");
                var cx = parseInt(cell[0]), cy = parseInt(cell[1]);
                if (x == cx && y == cy)
                    return true;
            }
        }
        return false;
    };

    /**
     * Sets a css class on a certain cell.
     */
    this.setClassForCell = function(x, y, classname)
    {
        if ( ! (classname in this._highlights))
        {
            this._highlights[classname] = {};
        }

        this._highlights[classname][x + "," + y] = true;

        this.boardCell(x, y).addClass(classname);
    };

    /**
     * Set the class for a whole bunch of cells in a single go
     */
    this.setClassForCells = function(cells, classname)
    {
        if ( ! (classname in this._highlights))
        {
            this._highlights[classname] = {};
        }

        if (cells)
        {
            for (var i = cells.length - 1;i >= 0; i--)
            {
                var x = cells[i][0], y = cells[i][1];
                this._highlights[classname][x + "," + y] = true;
                this.boardCell(x, y).addClass(classname);
            }
        }
    };

    /**
     * Remove a given classname on all cells that have it.
     */
    this.removeCellClass = function(classname)
    {
        if (classname in this._highlights)
        {
            var cells                   = this._highlights[classname];
            this._highlights[classname]  = {};

            for (var key in cells)
            {
                var cell = key.split(",");
                var x = parseInt(cell[0]), y = parseInt(cell[1]);
                this.boardCell(x, y).removeClass(classname);
            }
        }
    };

    /**
     * Removes a css class on a certain cell.
     */
    this.removeClassForCell = function(x, y, classname)
    {
        if (classname in this._highlights && ((x + "," + y) in this._highlights[classname]))
        {
            this._highlights[classname][x + "," + y] = null;
        }

        this.boardCell(x, y).removeClass(classname);
    };

    /**
     * Remove the class for a whole bunch of cells in a single go
     */
    this.removeClassForCells = function(cells, classname)
    {
        var hasClass = classname in this._highlights;

        if (cells)
        {
            for (var i = cells.length - 1;i >= 0; i--)
            {
                var x = cells[i][0], y = cells[i][1];
                this.boardCell(x, y).addClass(classname);
                if (hasClass)
                    this._highlights[classname][x + "," + y] = true;
            }
        }
    };

    /**
     * Creates the UI for the board.  This takes into account whether the
     * board is inverted or not (ie showing black in the bottom or white).
     */
    this.createBoard = function(boardwidth, boardheight)
    {
        this._curr_cell_x   = -1;
        this._curr_cell_y   = -1;

        if (!boardwidth)
        {
            alert("Invalid board width: " + boardwidth);
            return ;
        }

        if (!boardheight)
        {
            alert("Invalid board height: " + boardheight);
            return ;
        }

        this._pieces        = [];
        for (var x = 0;x < boardwidth;x++)
        {
            this._pieces.push(new Array());
            for (var y = 0;y < boardheight;y++)
            {
                this._pieces[x].push(null);
            }
        }
    
        this._board_width   = boardwidth;
        this._board_height  = boardheight;

        var html        = "";
        var rowblack    = ((boardheight % 2) != 0);
        var cellsize    = this._configs.cell_size + "px";
        for (var y = 0;y < boardheight;y++)
        {
            html += "<tr>";
            var cellblack = rowblack;
            for (var x = 0;x < boardwidth;x++)
            {
                var id = "cell_" + x + "_" + (boardheight - (y + 1));
                html += "<td id = '" + id + "'" +
                        // " onclick = 'cell_clicked(" + x + ", " + (boardheight - (y + 1)) + ");'" +
                        " class='" + (cellblack ?  "blackcell" : "whitecell") + "'" +
                        " style='width: " + cellsize + ";" +
                        " height: " + cellsize + ";" + "'" +
                        "> </td>"
                cellblack = !cellblack;
            }
            rowblack = !rowblack;
            html += "</tr>";
        }
        $("#" + this._table_id).html(html);
        $("#" + this._chess_board).css("border", "1px black solid");
        $("#" + this._chess_board).css("width", (this._configs.cell_size * boardwidth) + "px");
        $("#" + this._chess_board).css("height", (this._configs.cell_size * boardheight) + "px");

        var theboard = this;
        function handler_maker(a, b)
        {
            return function() {
                        if (theboard._on_cell_click)
                            theboard._on_cell_click(theboard, a, b); }
        }

        for (var y = 0;y < boardheight;y++)
        {
            for (var x = 0;x < boardwidth;x++)
            {
                this.boardCell(x, y).click(handler_maker(x, y));
            }
        }
    };

    /**
     * Gets the UI cell corresponding real world chess coordinates.
     * The real-world chess coordinates are unique.  However, the UI
     * coordinates depends on whether the board is inverted or not!
     */
    this.boardCell = function(x, y)
    {
        var row = ((this._board_height - 1) - y);
        return $("#" + this._table_id + " td:eq(" + ((row * 8) + x)+ ")");
    };









    this._process_configs = function(configs)
    {
        this._configs       = configs;

        if (!this._configs)
            this._configs   = {}

        if ( ! parseInt(this._configs.cell_size ))
            this._configs.cell_size = DEFAULT_CELL_SIZE;
    }

    this._cell_in_bounds = function(x,y)
    {
        return (x >= 0 && y >= 0 &&
                x < this._board_width &&
                y < this._board_height);
    }

    this.reset();
}

