/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * Copyright (C) 2010-2014 Robert Ancell
 * Copyright (C) 2015-2016 Sahil Sareen
 *
 * 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 3 of the License, or (at your option) any later
 * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
 * license.
 */

public enum CheckState
{
    NONE,
    CHECK,
    CHECKMATE
}

public class ChessState : Object
{
    public int number = 0;
    public ChessPlayer players[2];
    public ChessPlayer current_player;
    public ChessPlayer opponent
    {
        get { return current_player.color == Color.WHITE ? players[Color.BLACK] : players[Color.WHITE]; }
    }
    public bool can_castle_kingside[2];
    public bool can_castle_queenside[2];
    public int en_passant_index = -1;
    public CheckState check_state;
    public int halfmove_clock;

    public ChessPiece board[64];
    public ChessMove? last_move = null;

    /* Bitmap of all the pieces */
    private uint64 piece_masks[2];

    private ChessState.empty ()
    {
    }

    // FIXME Enable or remove these exceptions.
    public ChessState (string fen)
    {
        players[Color.WHITE] = new ChessPlayer (Color.WHITE);
        players[Color.BLACK] = new ChessPlayer (Color.BLACK);
        for (int i = 0; i < 64; i++)
            board[i] = null;

        string[] fields = fen.split (" ");
        //if (fields.length != 6)
        //    throw new Error ("Invalid FEN string");

        /* Field 1: Piece placement */
        string[] ranks = fields[0].split ("/");
        //if (ranks.length != 8)
        //    throw new Error ("Invalid piece placement");
        for (int rank = 0; rank < 8; rank++)
        {
            var rank_string = ranks[7 - rank];
            for (int file = 0, offset = 0; file < 8 && offset < rank_string.length; offset++)
            {
                var c = rank_string[offset];
                if (c >= '1' && c <= '8')
                {
                    file += c - '0';
                    continue;
                }

                PieceType type;
                var color = c.isupper () ? Color.WHITE : Color.BLACK;
                /*if (!*/ decode_piece_type (c.toupper (), out type) //)
                    ;//throw new Error ("");

                int index = get_index (rank, file);
                ChessPiece piece = new ChessPiece (players[color], type);
                board[index] = piece;
                uint64 mask = BitBoard.set_location_masks[index];
                piece_masks[color] |= mask;
                file++;
            }
        }

        /* Field 2: Active color */
        if (fields[1] == "w")
            current_player = players[Color.WHITE];
        else if (fields[1] == "b")
            current_player = players[Color.BLACK];
        //else
        //    throw new Error ("Unknown active color: %s", fields[1]);

        /* Field 3: Castling availability */
        if (fields[2] != "-")
        {
            for (int i = 0; i < fields[2].length; i++)
            {
                var c = fields[2][i];
                if (c == 'K')
                    can_castle_kingside[Color.WHITE] = true;
                else if (c == 'Q')
                    can_castle_queenside[Color.WHITE] = true;
                else if (c == 'k')
                    can_castle_kingside[Color.BLACK] = true;
                else if (c == 'q')
                    can_castle_queenside[Color.BLACK] = true;
                //else
                //    throw new Error ("");
            }
        }

        /* Field 4: En passant target square */
        if (fields[3] != "-")
        {
            //if (fields[3].length != 2)
            //    throw new Error ("");
            en_passant_index = get_index (fields[3][1] - '1', fields[3][0] - 'a');
        }

        /* Field 5: Halfmove clock */
        halfmove_clock = int.parse (fields[4]);

        /* Field 6: Fullmove number */
        number = (int.parse (fields[5]) - 1) * 2;
        if (current_player.color == Color.BLACK)
            number++;

        check_state = get_check_state (current_player);
    }

    public ChessState copy ()
    {
        ChessState state = new ChessState.empty ();

        state.number = number;
        state.players[Color.WHITE] = players[Color.WHITE];
        state.players[Color.BLACK] = players[Color.BLACK];
        state.current_player = current_player;
        state.can_castle_kingside[Color.WHITE] = can_castle_kingside[Color.WHITE];
        state.can_castle_queenside[Color.WHITE] = can_castle_queenside[Color.WHITE];
        state.can_castle_kingside[Color.BLACK] = can_castle_kingside[Color.BLACK];
        state.can_castle_queenside[Color.BLACK] = can_castle_queenside[Color.BLACK];
        state.en_passant_index = en_passant_index;
        state.check_state = check_state;
        if (last_move != null)
            state.last_move = last_move.copy ();
        for (int i = 0; i < 64; i++)
            state.board[i] = board[i];
        state.piece_masks[Color.WHITE] = piece_masks[Color.WHITE];
        state.piece_masks[Color.BLACK] = piece_masks[Color.BLACK];
        state.halfmove_clock = halfmove_clock;

        return state;
    }

    public bool equals (ChessState state)
    {
        /*
         * Check first if there is the same layout of pieces (unlikely),
         * then that the same player is on move, then that the move castling
         * and en-passant state are the same.  This follows the rules for
         * determining threefold repetition:
         *
         * https://en.wikipedia.org/wiki/Threefold_repetition
         */
        if (piece_masks[Color.WHITE] != state.piece_masks[Color.WHITE] ||
            piece_masks[Color.BLACK] != state.piece_masks[Color.BLACK] ||
            current_player.color != state.current_player.color ||
            can_castle_kingside[Color.WHITE] != state.can_castle_kingside[Color.WHITE] ||
            can_castle_queenside[Color.WHITE] != state.can_castle_queenside[Color.WHITE] ||
            can_castle_kingside[Color.BLACK] != state.can_castle_kingside[Color.BLACK] ||
            can_castle_queenside[Color.BLACK] != state.can_castle_queenside[Color.BLACK] ||
            en_passant_index != state.en_passant_index)
            return false;

        /* Finally check the same piece types are present */
        for (int i = 0; i < 64; i++)
        {
            if (board[i] != null && board[i].type != state.board[i].type)
                return false;
        }

        return true;
    }

    public string get_fen ()
    {
        var value = new StringBuilder ();

        for (int rank = 7; rank >= 0; rank--)
        {
            int skip_count = 0;
            for (int file = 0; file < 8; file++)
            {
                var p = board[get_index (rank, file)];
                if (p == null)
                    skip_count++;
                else
                {
                    if (skip_count > 0)
                    {
                        value.append_printf ("%d", skip_count);
                        skip_count = 0;
                    }
                    value.append_printf ("%c", (int) p.symbol);
                }
            }
            if (skip_count > 0)
                value.append_printf ("%d", skip_count);
            if (rank != 0)
                value.append_c ('/');
        }

        value.append_c (' ');
        if (current_player.color == Color.WHITE)
            value.append_c ('w');
        else
            value.append_c ('b');

        value.append_c (' ');
        if (can_castle_kingside[Color.WHITE])
            value.append_c ('K');
        if (can_castle_queenside[Color.WHITE])
            value.append_c ('Q');
        if (can_castle_kingside[Color.BLACK])
            value.append_c ('k');
        if (can_castle_queenside[Color.BLACK])
            value.append_c ('q');
        if (!(can_castle_kingside[Color.WHITE] | can_castle_queenside[Color.WHITE] | can_castle_kingside[Color.BLACK] | can_castle_queenside[Color.BLACK]))
            value.append_c ('-');

        value.append_c (' ');
        if (en_passant_index >= 0)
            value.append_printf ("%c%d", 'a' + get_file (en_passant_index), get_rank (en_passant_index) + 1);
        else
            value.append_c ('-');

        value.append_c (' ');
        value.append_printf ("%d", halfmove_clock);

        value.append_c (' ');
        if (current_player.color == Color.WHITE)
            value.append_printf ("%d", number / 2);
        else
            value.append_printf ("%d", number / 2 + 1);

        return value.str;
    }

    public int get_index (int rank, int file)
    {
        return rank * 8 + file;
    }

    public int get_rank (int index)
    {
        return index / 8;
    }

    public int get_file (int index)
    {
        return index % 8;
    }

    public bool move (string move, bool apply = true)
    {
        int r0, f0, r1, f1;
        PieceType promotion_type;

        if (!decode_move (current_player, move, out r0, out f0, out r1, out f1, out promotion_type))
            return false;

        if (!move_with_coords (current_player, r0, f0, r1, f1, promotion_type, apply))
            return false;

        return true;
    }

    public bool move_with_coords (ChessPlayer player,
                                  int r0, int f0, int r1, int f1,
                                  PieceType promotion_type = PieceType.QUEEN,
                                  bool apply = true, bool test_check = true)
    {
        // FIXME: Make this use indexes to be faster
        var start = get_index (r0, f0);
        var end = get_index (r1, f1);

        var color = player.color;
        var opponent_color = color == Color.WHITE ? Color.BLACK : Color.WHITE;

        /* Must be moving own piece */
        var piece = board[start];
        if (piece == null || piece.player != player)
            return false;

        /* Check valid move */
        uint64 end_mask = BitBoard.set_location_masks[end];
        uint64 move_mask = BitBoard.move_masks[color * 64*6 + piece.type * 64 + start];
        if ((end_mask & move_mask) == 0)
            return false;

        /* Check no pieces in the way */
        uint64 over_mask = BitBoard.over_masks[start * 64 + end];
        if ((over_mask & (piece_masks[Color.WHITE] | piece_masks[Color.BLACK])) != 0)
            return false;

        /* Get victim of move */
        var victim = board[end];
        var victim_index = end;

        /* Can't take own pieces */
        if (victim != null && victim.player == player)
            return false;

        /* Check special moves */
        int rook_start = -1, rook_end = -1;
        bool is_promotion = false;
        bool en_passant = false;
        bool ambiguous_rank = false;
        bool ambiguous_file = false;
        switch (piece.type)
        {
        case PieceType.PAWN:
            /* Check if taking an marched pawn */
            if (victim == null && end == en_passant_index)
            {
                en_passant = true;
                victim_index = get_index (r1 == 2 ? 3 : 4, f1);
                victim = board[victim_index];
            }

            /* If moving diagonally there must be a victim */
            if (f0 != f1)
            {
                if (victim == null)
                    return false;
            }
            else
            {
                /* If moving forward can't take enemy */
                if (victim != null)
                    return false;
            }
            is_promotion = r1 == 0 || r1 == 7;

            /* Always show the file of a pawn capturing */
            if (victim != null)
                ambiguous_file = true;
            break;
        case PieceType.KING:
            /* If moving more than one square must be castling */
            if ((f0 - f1).abs () > 1)
            {
                /* File the rook is on */
                rook_start = get_index (r0, f1 > f0 ? 7 : 0);
                rook_end = get_index (r0, f1 > f0 ? f1 - 1 : f1 + 1);

                /* Check if can castle */
                if (f1 > f0)
                {
                    if (!can_castle_kingside[color])
                        return false;
                }
                else
                {
                    if (!can_castle_queenside[color])
                        return false;
                }

                var rook = board[rook_start];
                if (rook == null || rook.type != PieceType.ROOK || rook.color != color)
                    return false;

                /* Check rook can move */
                uint64 rook_over_mask = BitBoard.over_masks[rook_start * 64 + rook_end];
                if ((rook_over_mask & (piece_masks[Color.WHITE] | piece_masks[Color.BLACK])) != 0)
                    return false;

                /* Can't castle when in check */
                if (check_state == CheckState.CHECK)
                    return false;

                /* Square moved across can't be under attack */
                if (!move_with_coords (player, r0, f0, get_rank (rook_end), get_file (rook_end), PieceType.QUEEN, false, true))
                    return false;
            }
            break;
        default:
            break;
        }

        if (!apply && !test_check)
            return true;

        /* Check if other pieces of the same type can make this move - this is required for SAN notation */
        if (apply)
        {
            for (int i = 0; i < 64; i++)
            {
                /* Ignore our move */
                if (i == start)
                    continue;

                /* Check for a friendly piece of the same type */
                var p = board[i];
                if (p == null || p.player != player || p.type != piece.type)
                    continue;

                /* If more than one piece can move then the rank and/or file are ambiguous */
                var r = get_rank (i);
                var f = get_file (i);
                if (move_with_coords (player, r, f, r1, f1, PieceType.QUEEN, false))
                {
                    if (r != r0)
                        ambiguous_rank = true;
                    if (f != f0)
                        ambiguous_file = true;
                }
            }
        }

        var old_white_mask = piece_masks[Color.WHITE];
        var old_black_mask = piece_masks[Color.BLACK];
        var old_white_can_castle_kingside = can_castle_kingside[Color.WHITE];
        var old_white_can_castle_queenside = can_castle_queenside[Color.WHITE];
        var old_black_can_castle_kingside = can_castle_kingside[Color.BLACK];
        var old_black_can_castle_queenside = can_castle_queenside[Color.BLACK];
        var old_en_passant_index = en_passant_index;
        var old_halfmove_clock = halfmove_clock;

        /* Update board */
        board[start] = null;
        piece_masks[Color.WHITE] &= BitBoard.clear_location_masks[start];
        piece_masks[Color.BLACK] &= BitBoard.clear_location_masks[start];
        if (victim != null)
        {
            board[victim_index] = null;
            piece_masks[Color.WHITE] &= BitBoard.clear_location_masks[victim_index];
            piece_masks[Color.BLACK] &= BitBoard.clear_location_masks[victim_index];
        }
        if (is_promotion)
            board[end] = new ChessPiece (player, promotion_type);
        else
            board[end] = piece;
        piece_masks[color] |= end_mask;
        piece_masks[opponent_color] &= BitBoard.clear_location_masks[end];
        if (rook_start >= 0)
        {
            var rook = board[rook_start];
            board[rook_start] = null;
            piece_masks[color] &= BitBoard.clear_location_masks[rook_start];
            board[rook_end] = rook;
            piece_masks[color] |= BitBoard.set_location_masks[rook_end];
        }

        /* Can't castle once king has moved */
        if (piece.type == PieceType.KING)
        {
            can_castle_kingside[color] = false;
            can_castle_queenside[color] = false;
        }
        /* Can't castle once rooks have moved */
        else if (piece.type == PieceType.ROOK)
        {
            int base_rank = color == Color.WHITE ? 0 : 7;
            if (r0 == base_rank)
            {
                if (f0 == 0)
                    can_castle_queenside[color] = false;
                else if (f0 == 7)
                    can_castle_kingside[color] = false;
            }
        }
        /* Can't castle once the rooks have been captured */
        else if (victim != null && victim.type == PieceType.ROOK)
        {
            int base_rank = opponent_color == Color.WHITE ? 0 : 7;
            if (r1 == base_rank)
            {
                if (f1 == 0)
                    can_castle_queenside[opponent_color] = false;
                else if (f1 == 7)
                    can_castle_kingside[opponent_color] = false;
            }
        }

        /* Pawn square moved over is vulnerable */
        if (piece.type == PieceType.PAWN && over_mask != 0)
            en_passant_index = get_index ((r0 + r1) / 2, f0);
        else
            en_passant_index = -1;

        /* Reset halfmove count when pawn moved or piece taken */
        if (piece.type == PieceType.PAWN || victim != null)
            halfmove_clock = 0;
        else
            halfmove_clock++;

        /* Test if this move would leave that player in check */
        bool result = true;
        if (test_check && is_in_check (player))
            result = false;

        /* Undo move */
        if (!apply || !result)
        {
            board[start] = piece;
            board[end] = null;
            if (victim != null)
                board[victim_index] = victim;
            if (rook_start >= 0)
            {
                var rook = board[rook_end];
                board[rook_start] = rook;
                board[rook_end] = null;
            }
            piece_masks[Color.WHITE] = old_white_mask;
            piece_masks[Color.BLACK] = old_black_mask;
            can_castle_kingside[Color.WHITE] = old_white_can_castle_kingside;
            can_castle_queenside[Color.WHITE] = old_white_can_castle_queenside;
            can_castle_kingside[Color.BLACK] = old_black_can_castle_kingside;
            can_castle_queenside[Color.BLACK] = old_black_can_castle_queenside;
            en_passant_index = old_en_passant_index;
            halfmove_clock = old_halfmove_clock;

            return result;
        }

        current_player = color == Color.WHITE ? players[Color.BLACK] : players[Color.WHITE];
        check_state = get_check_state (current_player);

        last_move = new ChessMove ();
        last_move.number = number;
        last_move.piece = piece;
        if (is_promotion)
            last_move.promotion_piece = board[end];
        last_move.victim = victim;
        if (rook_end >= 0)
            last_move.castling_rook = board[rook_end];
        last_move.r0 = r0;
        last_move.f0 = f0;
        last_move.r1 = r1;
        last_move.f1 = f1;
        last_move.ambiguous_rank = ambiguous_rank;
        last_move.ambiguous_file = ambiguous_file;
        last_move.en_passant = en_passant;
        last_move.check_state = check_state;

        return true;
    }

    public ChessResult get_result (out ChessRule rule)
    {
        rule = ChessRule.CHECKMATE;
        if (check_state == CheckState.CHECKMATE)
        {
            if (current_player.color == Color.WHITE)
            {
                rule = ChessRule.CHECKMATE;
                return ChessResult.BLACK_WON;
            }
            else
            {
                rule = ChessRule.CHECKMATE;
                return ChessResult.WHITE_WON;
            }
        }

        if (!can_move (current_player))
        {
            rule = ChessRule.STALEMATE;
            return ChessResult.DRAW;
        }

        if (last_move != null && last_move.victim != null && !have_sufficient_material ())
        {
            rule = ChessRule.INSUFFICIENT_MATERIAL;
            return ChessResult.DRAW;
        }

        return ChessResult.IN_PROGRESS;
    }

    private CheckState get_check_state (ChessPlayer player)
    {
        if (is_in_check (player))
        {
            if (is_in_checkmate (player))
                return CheckState.CHECKMATE;
            else
                return CheckState.CHECK;
        }
        return CheckState.NONE;
    }

    public bool get_positions_threatening_king (ChessPlayer player, out int[] rank, out int[] file)
    {
        var opponent = player.color == Color.WHITE ? players[Color.BLACK] : players[Color.WHITE];
        bool found = false;

        /* Is in check if any piece can take the king */
        for (int king_index = 0; king_index < 64; king_index++)
        {
            var p = board[king_index];
            if (p != null && p.player == player && p.type == PieceType.KING)
            {
                /* See if any enemy pieces can take the king */
                int[] ranks = {};
                int[] files = {};
                for (int start = 0; start < 64; start++)
                {
                    if (move_with_coords (opponent,
                                          get_rank (start), get_file (start),
                                          get_rank (king_index), get_file (king_index),
                                          PieceType.QUEEN, false, false))
                    {
                        ranks += get_rank (start);
                        files += get_file (start);
                        found = true;
                    }
                }

                rank = ranks;
                file = files;

                return found;
            }
        }

        /* There is no King. (Must be a test rather than a real game!) */
        rank = {};
        file = {};
        return false;
    }

    public bool is_in_check (ChessPlayer player)
    {
        int[] rank, file;
        return get_positions_threatening_king (player, out rank, out file);
    }

    private bool is_in_checkmate (ChessPlayer player)
    {
        /* Is in checkmate if no pieces can move */
        for (int piece_index = 0; piece_index < 64; piece_index++)
        {
            var p = board[piece_index];
            if (p != null && p.player == player)
            {
                for (int end = 0; end < 64; end++)
                {
                    if (move_with_coords (player,
                                          get_rank (piece_index), get_file (piece_index),
                                          get_rank (end), get_file (end),
                                          PieceType.QUEEN, false, true))
                        return false;
                }
            }
        }

        return true;
    }

    public bool can_move (ChessPlayer player)
    {
        bool have_pieces = false;

        for (int start = 0; start < 64; start++)
        {
            var p = board[start];
            if (p != null && p.player == player)
            {
                have_pieces = true;

                /* See if can move anywhere */
                for (int end = 0; end < 64; end++)
                {
                    if (move_with_coords (player,
                                          get_rank (start), get_file (start),
                                          get_rank (end), get_file (end),
                                          PieceType.QUEEN, false, true))
                        return true;
                }
            }
        }

        /* Only mark as stalemate if have at least one piece */
        if (have_pieces)
            return false;
        else
            return true;
    }

    public bool have_sufficient_material ()
    {
        var white_knight_count = 0;
        var white_bishop_count = 0;
        var white_bishop_on_white_square = false;
        var white_bishop_on_black_square = false;
        var black_knight_count = 0;
        var black_bishop_count = 0;
        var black_bishop_on_white_square = false;
        var black_bishop_on_black_square = false;

        for (int i = 0; i < 64; i++)
        {
            var p = board[i];
            if (p == null)
                continue;

            /* Any pawns, rooks or queens can perform checkmate */
            if (p.type == PieceType.PAWN || p.type == PieceType.ROOK || p.type == PieceType.QUEEN)
                return true;

            /* Otherwise, count the minor pieces for each colour... */
            if (p.type == PieceType.KNIGHT)
            {
                if (p.color == Color.WHITE)
                    white_knight_count++;
                else
                    black_knight_count++;
            }

            if (p.type == PieceType.BISHOP)
            {
                var color = Color.BLACK;
                if ((i + i/8) % 2 != 0)
                    color = Color.WHITE;

                if (p.color == Color.WHITE)
                {
                    if (color == Color.WHITE)
                        white_bishop_on_white_square = true;
                    else
                        white_bishop_on_black_square = true;
                    white_bishop_count++;
                }
                else
                {
                    if (color == Color.WHITE)
                        black_bishop_on_white_square = true;
                    else
                        black_bishop_on_black_square = true;
                    black_bishop_count++;
                }
            }

            /*
             * We count the following positions as insufficient:
             *
             * 1) king versus king
             * 2) king and bishop versus king
             * 3) king and knight versus king
             * 4) king and bishop versus king and bishop with the bishops on the same color. (Any
             *    number of additional bishops of either color on the same color of square due to
             *    underpromotion do not affect the situation.)
             *
             * From: https://en.wikipedia.org/wiki/Draw_(chess)#Draws_in_all_games
             *
             * Note also that this follows FIDE rules, not USCF rules. E.g. K+N+N vs. K cannot be
             * forced, so it's not counted as a draw.
             *
             * This is also what CECP engines will be expecting:
             *
             * "Note that (in accordance with FIDE rules) only KK, KNK, KBK and KBKB with all
             * bishops on the same color can be claimed as draws on the basis of insufficient mating
             * material. The end-games KNNK, KBKN, KNKN and KBKB with unlike bishops do have mate
             * positions, and cannot be claimed. Complex draws based on locked Pawn chains will not
             * be recognized as draws by most interfaces, so do not claim in such positions, but
             * just offer a draw or play on."
             *
             * From: http://www.open-aurec.com/wbforum/WinBoard/engine-intf.html
             *
             * (In contrast, UCI seems to expect the interface to handle draws itself.)
             */

            /* Two knights versus king can checkmate (though not against an optimal opponent) */
            if (white_knight_count > 1 || black_knight_count > 1)
                return true;

            /* Bishop and knight versus king can checkmate */
            if (white_bishop_count > 0 && white_knight_count > 0)
                return true;
            if (black_bishop_count > 0 && black_knight_count > 0)
                return true;

            /* King and bishops versus king can checkmate as long as the bishops are on both colours */
            if (white_bishop_on_white_square && white_bishop_on_black_square)
                return true;
            if (black_bishop_on_white_square && black_bishop_on_black_square)
                return true;

            /* King and minor piece vs. King and knight is surprisingly not a draw */
            if ((white_bishop_count > 0 || white_knight_count > 0) && black_knight_count > 0)
                return true;
            if ((black_bishop_count > 0 || black_knight_count > 0) && white_knight_count > 0)
                return true;

            /* King and bishop can checkmate vs. king and bishop if bishops are on opposite colors */
            if (white_bishop_count > 0 && black_bishop_count > 0)
            {
                if (white_bishop_on_white_square && black_bishop_on_black_square)
                    return true;
                else if (white_bishop_on_black_square && black_bishop_on_white_square)
                    return true;
            }
        }

        return false;
    }

    private bool decode_piece_type (unichar c, out PieceType type)
    {
        type = PieceType.PAWN;
        switch (c)
        {
        case 'P':
            type = PieceType.PAWN;
            return true;
        case 'R':
            type = PieceType.ROOK;
            return true;
        case 'N':
            type = PieceType.KNIGHT;
            return true;
        case 'B':
            type = PieceType.BISHOP;
            return true;
        case 'Q':
            type = PieceType.QUEEN;
            return true;
        case 'K':
            type = PieceType.KING;
            return true;
        default:
            return false;
        }
    }

    private bool decode_move (ChessPlayer player, string move, out int r0, out int f0, out int r1, out int f1, out PieceType promotion_type)
    {
        int i = 0;

        promotion_type = PieceType.QUEEN;
        if (move.has_prefix ("O-O-O"))
        {
            if (player.color == Color.WHITE)
                r0 = r1 = 0;
            else
                r0 = r1 = 7;
            f0 = 4;
            f1 = 2;
            i += (int) "O-O-O".length;
        }
        else if (move.has_prefix ("O-O"))
        {
            if (player.color == Color.WHITE)
                r0 = r1 = 0;
            else
                r0 = r1 = 7;
            f0 = 4;
            f1 = 6;
            i += (int) "O-O".length;
        }
        else
        {
            PieceType type = PieceType.PAWN;
            if (decode_piece_type (move[i], out type))
                i++;

            r0 = f0 = r1 = f1 = -1;
            if (move[i] >= 'a' && move[i] <= 'h')
            {
                f1 = (int) (move[i] - 'a');
                i++;
            }
            if (move[i] >= '1' && move[i] <= '8')
            {
                r1 = (int) (move[i] - '1');
                i++;
            }
            if (move[i] == 'x' || move[i] == '-')
                i++;
            if (move[i] >= 'a' && move[i] <= 'h')
            {
                f0 = f1;
                f1 = (int) (move[i] - 'a');
                i++;
            }
            if (move[i] >= '1' && move[i] <= '8')
            {
                r0 = r1;
                r1 = (int) (move[i] - '1');
                i++;
            }
            if (move[i] == '=')
            {
                i++;
                if (decode_piece_type (move[i], out promotion_type))
                    i++;
            }
            else if (move[i] != '\0')
            {
                switch (move[i])
                {
                case 'q':
                case 'Q':
                    promotion_type = PieceType.QUEEN;
                    i++;
                    break;
                case 'n':
                case 'N':
                    promotion_type = PieceType.KNIGHT;
                    i++;
                    break;
                case 'r':
                case 'R':
                    promotion_type = PieceType.ROOK;
                    i++;
                    break;
                case 'b':
                case 'B':
                    promotion_type = PieceType.BISHOP;
                    i++;
                    break;
                }
            }

            /* Don't have a destination to move to */
            if (r1 < 0 || f1 < 0)
            {
                debug ("Move %s missing destination", move);
                return false;
            }

            /* Find source piece */
            if (r0 < 0 || f0 < 0)
            {
                int match_rank = -1, match_file = -1;

                for (int file = 0; file < 8; file++)
                {
                    if (f0 >= 0 && file != f0)
                        continue;

                    for (int rank = 0; rank < 8; rank++)
                    {
                        if (r0 >= 0 && rank != r0)
                            continue;

                        /* Only check this players pieces of the correct type */
                        var piece = board[get_index (rank, file)];
                        if (piece == null || piece.type != type || piece.player != player)
                            continue;

                        /* See if can move here */
                        if (!this.move_with_coords (player, rank, file, r1, f1, PieceType.QUEEN, false))
                            continue;

                        /* Duplicate match */
                        if (match_rank >= 0)
                        {
                            debug ("Move %s is ambiguous", move);
                            return false;
                        }

                        match_rank = rank;
                        match_file = file;
                    }
                }

                if (match_rank < 0)
                {
                    debug ("Move %s has no matches", move);
                    return false;
                }

                r0 = match_rank;
                f0 = match_file;
            }
        }

        if (move[i] == '+')
            i++;
        else if (move[i] == '#')
            i++;

        if (move[i] != '\0')
        {
            debug ("Move %s has unexpected characters", move);
            return false;
        }

        return true;
    }
}
