/*
    CuckooChess - A java chess program.
    Copyright (C) 2011  Peter Österlund, peterosterlund2@gmail.com

    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.

    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, see <http://www.gnu.org/licenses/>.
*/

package chess;

import java.util.Locale;

public class TextIO {
    static public final String startPosFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";

    /** Parse a FEN string and return a chess Position object. */
    public static Position readFEN(String fen) throws ChessParseError {
        Position pos = new Position();
        String[] words = fen.split(" ");
        if (words.length < 2) {
            throw new ChessParseError("Too few spaces");
        }
        
        // Piece placement
        int row = 7;
        int col = 0;
        for (int i = 0; i < words[0].length(); i++) {
            char c = words[0].charAt(i);
            switch (c) {
                case '1': col += 1; break;
                case '2': col += 2; break;
                case '3': col += 3; break;
                case '4': col += 4; break;
                case '5': col += 5; break;
                case '6': col += 6; break;
                case '7': col += 7; break;
                case '8': col += 8; break;
                case '/': row--; col = 0; break;
                case 'P': safeSetPiece(pos, col, row, Piece.WPAWN);   col++; break;
                case 'N': safeSetPiece(pos, col, row, Piece.WKNIGHT); col++; break;
        case 'B': safeSetPiece(pos, col, row, Piece.WBISHOP); col++; break;
        case 'R': safeSetPiece(pos, col, row, Piece.WROOK);   col++; break;
        case 'Q': safeSetPiece(pos, col, row, Piece.WQUEEN);  col++; break;
        case 'K': safeSetPiece(pos, col, row, Piece.WKING);   col++; break;
        case 'p': safeSetPiece(pos, col, row, Piece.BPAWN);   col++; break;
        case 'n': safeSetPiece(pos, col, row, Piece.BKNIGHT); col++; break;
        case 'b': safeSetPiece(pos, col, row, Piece.BBISHOP); col++; break;
        case 'r': safeSetPiece(pos, col, row, Piece.BROOK);   col++; break;
        case 'q': safeSetPiece(pos, col, row, Piece.BQUEEN);  col++; break;
        case 'k': safeSetPiece(pos, col, row, Piece.BKING);   col++; break;
                default: throw new ChessParseError("Invalid piece");
            }
        }
        if (words[1].length() == 0) {
            throw new ChessParseError("Invalid side");
        }
        pos.setWhiteMove(words[1].charAt(0) == 'w');

        // Castling rights
        int castleMask = 0;
        if (words.length > 2) {
            for (int i = 0; i < words[2].length(); i++) {
                char c = words[2].charAt(i);
                switch (c) {
                    case 'K':
                        castleMask |= (1 << Position.H1_CASTLE);
                        break;
                    case 'Q':
                        castleMask |= (1 << Position.A1_CASTLE);
                        break;
                    case 'k':
                        castleMask |= (1 << Position.H8_CASTLE);
                        break;
                    case 'q':
                        castleMask |= (1 << Position.A8_CASTLE);
                        break;
                    case '-':
                        break;
                    default:
                        throw new ChessParseError("Invalid castling flags");
                }
            }
        }
        pos.setCastleMask(castleMask);

        if (words.length > 3) {
            // En passant target square
            String epString = words[3];
            if (!epString.equals("-")) {
                if (epString.length() < 2) {
                    throw new ChessParseError("Invalid en passant square");
                }
                pos.setEpSquare(getSquare(epString));
            }
        }

        try {
            if (words.length > 4) {
                pos.halfMoveClock = Integer.parseInt(words[4]);
            }
            if (words.length > 5) {
                pos.fullMoveCounter = Integer.parseInt(words[5]);
            }
        } catch (NumberFormatException nfe) {
            // Ignore errors here, since the fields are optional
        }

        // Each side must have exactly one king
        int wKings = 0;
        int bKings = 0;
        for (int x = 0; x < 8; x++) {
            for (int y = 0; y < 8; y++) {
                int p = pos.getPiece(Position.getSquare(x, y));
                if (p == Piece.WKING) {
                    wKings++;
                } else if (p == Piece.BKING) {
                    bKings++;
                }
            }
        }
        if (wKings != 1) {
            throw new ChessParseError("White must have exactly one king");
        }
        if (bKings != 1) {
            throw new ChessParseError("Black must have exactly one king");
        }

        // Make sure king can not be captured
        Position pos2 = new Position(pos);
        pos2.setWhiteMove(!pos.whiteMove);
        if (MoveGen.inCheck(pos2)) {
            throw new ChessParseError("King capture possible");
        }

        fixupEPSquare(pos);

        return pos;
    }

    /** Remove pseudo-legal EP square if it is not legal, ie would leave king in check. */
    public static void fixupEPSquare(Position pos) {
        int epSquare = pos.getEpSquare();
        if (epSquare >= 0) {
            MoveGen.MoveList moves = MoveGen.instance.pseudoLegalMoves(pos);
            MoveGen.removeIllegal(pos, moves);
            boolean epValid = false;
            for (int mi = 0; mi < moves.size; mi++) {
                Move m = moves.m[mi];
                if (m.to == epSquare) {
                    if (pos.getPiece(m.from) == (pos.whiteMove ? Piece.WPAWN : Piece.BPAWN)) {
                        epValid = true;
                        break;
                    }
                }
            }
            if (!epValid) {
                pos.setEpSquare(-1);
            }
        }
    }

    private static void safeSetPiece(Position pos, int col, int row, int p) throws ChessParseError {
        if (row < 0) throw new ChessParseError("Too many rows");
        if (col > 7) throw new ChessParseError("Too many columns");
        if ((p == Piece.WPAWN) || (p == Piece.BPAWN)) {
            if ((row == 0) || (row == 7))
                throw new ChessParseError("Pawn on first/last rank");
        }
        pos.setPiece(Position.getSquare(col, row), p);
    }
    
    /** Return a FEN string corresponding to a chess Position object. */
    public static String toFEN(Position pos) {
        StringBuilder ret = new StringBuilder();
        // Piece placement
        for (int r = 7; r >=0; r--) {
            int numEmpty = 0;
            for (int c = 0; c < 8; c++) {
                int p = pos.getPiece(Position.getSquare(c, r));
                if (p == Piece.EMPTY) {
                    numEmpty++;
                } else {
                    if (numEmpty > 0) {
                        ret.append(numEmpty);
                        numEmpty = 0;
                    }
                    switch (p) {
                        case Piece.WKING:   ret.append('K'); break;
                        case Piece.WQUEEN:  ret.append('Q'); break;
                        case Piece.WROOK:   ret.append('R'); break;
                        case Piece.WBISHOP: ret.append('B'); break;
                        case Piece.WKNIGHT: ret.append('N'); break;
                        case Piece.WPAWN:   ret.append('P'); break;
                        case Piece.BKING:   ret.append('k'); break;
                        case Piece.BQUEEN:  ret.append('q'); break;
                        case Piece.BROOK:   ret.append('r'); break;
                        case Piece.BBISHOP: ret.append('b'); break;
                        case Piece.BKNIGHT: ret.append('n'); break;
                        case Piece.BPAWN:   ret.append('p'); break;
                        default: throw new RuntimeException();
                    }
                }
            }
            if (numEmpty > 0) {
                ret.append(numEmpty);
            }
            if (r > 0) {
                ret.append('/');
            }
        }
        ret.append(pos.whiteMove ? " w " : " b ");

        // Castling rights
        boolean anyCastle = false;
        if (pos.h1Castle()) {
            ret.append('K');
            anyCastle = true;
        }
        if (pos.a1Castle()) {
            ret.append('Q');
            anyCastle = true;
        }
        if (pos.h8Castle()) {
            ret.append('k');
            anyCastle = true;
        }
        if (pos.a8Castle()) {
            ret.append('q');
            anyCastle = true;
        }
        if (!anyCastle) {
            ret.append('-');
        }
        
        // En passant target square
        {
            ret.append(' ');
            if (pos.getEpSquare() >= 0) {
                int x = Position.getX(pos.getEpSquare());
                int y = Position.getY(pos.getEpSquare());
                ret.append((char)(x + 'a'));
                ret.append((char)(y + '1'));
            } else {
                ret.append('-');
            }
        }

        // Move counters
        ret.append(' ');
        ret.append(pos.halfMoveClock);
        ret.append(' ');
        ret.append(pos.fullMoveCounter);

        return ret.toString();
    }
    
    /**
     * Convert a chess move to human readable form.
     * @param pos      The chess position.
     * @param move     The executed move.
     * @param longForm If true, use long notation, eg Ng1-f3.
     *                 Otherwise, use short notation, eg Nf3
     */
    public static String moveToString(Position pos, Move move, boolean longForm) {
        MoveGen.MoveList moves = MoveGen.instance.pseudoLegalMoves(pos);
        MoveGen.removeIllegal(pos, moves);
        return moveToString(pos, move, longForm, moves);
    }
    private static String moveToString(Position pos, Move move, boolean longForm, MoveGen.MoveList moves) {
        StringBuilder ret = new StringBuilder();
        int wKingOrigPos = Position.getSquare(4, 0);
        int bKingOrigPos = Position.getSquare(4, 7);
        if (move.from == wKingOrigPos && pos.getPiece(wKingOrigPos) == Piece.WKING) {
            // Check white castle
            if (move.to == Position.getSquare(6, 0)) {
                    ret.append("O-O");
            } else if (move.to == Position.getSquare(2, 0)) {
                ret.append("O-O-O");
            }
        } else if (move.from == bKingOrigPos && pos.getPiece(bKingOrigPos) == Piece.BKING) {
            // Check black castle
            if (move.to == Position.getSquare(6, 7)) {
                ret.append("O-O");
            } else if (move.to == Position.getSquare(2, 7)) {
                ret.append("O-O-O");
            }
        }
        if (ret.length() == 0) {
            int p = pos.getPiece(move.from);
            ret.append(pieceToChar(p));
            int x1 = Position.getX(move.from);
            int y1 = Position.getY(move.from);
            int x2 = Position.getX(move.to);
            int y2 = Position.getY(move.to);
            if (longForm) {
                ret.append((char)(x1 + 'a'));
                ret.append((char) (y1 + '1'));
                ret.append(isCapture(pos, move) ? 'x' : '-');
            } else {
                if (p == (pos.whiteMove ? Piece.WPAWN : Piece.BPAWN)) {
                    if (isCapture(pos, move)) {
                        ret.append((char) (x1 + 'a'));
                    }
                } else {
                    int numSameTarget = 0;
                    int numSameFile = 0;
                    int numSameRow = 0;
                    for (int mi = 0; mi < moves.size; mi++) {
                        Move m = moves.m[mi];
                        if (m == null)
                            break;
                        if ((pos.getPiece(m.from) == p) && (m.to == move.to)) {
                            numSameTarget++;
                            if (Position.getX(m.from) == x1)
                                numSameFile++;
                            if (Position.getY(m.from) == y1)
                                numSameRow++;
                        }
                    }
                    if (numSameTarget < 2) {
                        // No file/row info needed
                    } else if (numSameFile < 2) {
                        ret.append((char) (x1 + 'a'));   // Only file info needed
                    } else if (numSameRow < 2) {
                        ret.append((char) (y1 + '1'));   // Only row info needed
                    } else {
                        ret.append((char) (x1 + 'a'));   // File and row info needed
                        ret.append((char) (y1 + '1'));
                    }
                }
                if (isCapture(pos, move)) {
                    ret.append('x');
                }
            }
            ret.append((char) (x2 + 'a'));
            ret.append((char) (y2 + '1'));
            if (move.promoteTo != Piece.EMPTY) {
                ret.append(pieceToChar(move.promoteTo));
            }
        }
        UndoInfo ui = new UndoInfo();
        if (MoveGen.givesCheck(pos, move)) {
            pos.makeMove(move, ui);
            MoveGen.MoveList nextMoves = MoveGen.instance.pseudoLegalMoves(pos);
            MoveGen.removeIllegal(pos, nextMoves);
            if (nextMoves.size == 0) {
                ret.append('#');
            } else {
                ret.append('+');
            }
            pos.unMakeMove(move, ui);
        }

        return ret.toString();
    }

    /** Convert a move object to UCI string format. */
    public static String moveToUCIString(Move m) {
        String ret = squareToString(m.from);
        ret += squareToString(m.to);
        switch (m.promoteTo) {
            case Piece.WQUEEN:
            case Piece.BQUEEN:
                ret += "q";
                break;
            case Piece.WROOK:
            case Piece.BROOK:
                ret += "r";
                break;
            case Piece.WBISHOP:
            case Piece.BBISHOP:
                ret += "b";
                break;
            case Piece.WKNIGHT:
            case Piece.BKNIGHT:
                ret += "n";
                break;
            default:
                break;
        }
        return ret;
    }

    /**
     * Convert a string to a Move object.
     * @return A move object, or null if move has invalid syntax
     */
    public static Move uciStringToMove(String move) {
        if ((move.length() < 4) || (move.length() > 5))
            return null;
        int fromSq = TextIO.getSquare(move.substring(0, 2));
        int toSq   = TextIO.getSquare(move.substring(2, 4));
        if ((fromSq < 0) || (toSq < 0)) {
            return null;
        }
        char prom = ' ';
        boolean white = true;
        if (move.length() == 5) {
            prom = move.charAt(4);
            if (Position.getY(toSq) == 7) {
                white = true;
            } else if (Position.getY(toSq) == 0) {
                white = false;
            } else {
                return null;
            }
        }
        int promoteTo;
        switch (prom) {
            case ' ':
                promoteTo = Piece.EMPTY;
                break;
            case 'q':
                promoteTo = white ? Piece.WQUEEN : Piece.BQUEEN;
                break;
            case 'r':
                promoteTo = white ? Piece.WROOK : Piece.BROOK;
                break;
            case 'b':
                promoteTo = white ? Piece.WBISHOP : Piece.BBISHOP;
                break;
            case 'n':
                promoteTo = white ? Piece.WKNIGHT : Piece.BKNIGHT;
                break;
            default:
                return null;
        }
        return new Move(fromSq, toSq, promoteTo);
    }

    private static boolean isCapture(Position pos, Move move) {
        if (pos.getPiece(move.to) == Piece.EMPTY) {
            int p = pos.getPiece(move.from);
            return (p == (pos.whiteMove ? Piece.WPAWN : Piece.BPAWN)) && (move.to == pos.getEpSquare());
        } else {
            return true;
        }
    }

    /**
     * Convert a chess move string to a Move object.
     * Any prefix of the string representation of a valid move counts as a legal move string,
     * as long as the string only matches one valid move.
     */
    public static Move stringToMove(Position pos, String strMove) {
        strMove = strMove.replaceAll("=", "");
        if (strMove.length() == 0)
            return null;
        MoveGen.MoveList moves = MoveGen.instance.pseudoLegalMoves(pos);
        MoveGen.removeIllegal(pos, moves);
        {
            char lastChar = strMove.charAt(strMove.length() - 1);
            if ((lastChar == '#') || (lastChar == '+')) {
                MoveGen.MoveList subMoves = new MoveGen.MoveList();
                int len = 0;
                for (int mi = 0; mi < moves.size; mi++) {
                    Move m = moves.m[mi];
                    String str1 = TextIO.moveToString(pos, m, true, moves);
                    if (str1.charAt(str1.length() - 1) == lastChar) {
                        subMoves.m[len++] = m;
                    }
                }
                subMoves.size = len;
                moves = subMoves;
                strMove = normalizeMoveString(strMove);
            }
        }

        for (int i = 0; i < 2; i++) {
            // Search for full match
            for (int mi = 0; mi < moves.size; mi++) {
                Move m = moves.m[mi];
                String str1 = normalizeMoveString(TextIO.moveToString(pos, m, true, moves));
                String str2 = normalizeMoveString(TextIO.moveToString(pos, m, false, moves));
                if (i == 0) {
                    if (strMove.equals(str1) || strMove.equals(str2)) {
                        return m;
                    }
                } else {
                    if (strMove.toLowerCase().equals(str1.toLowerCase()) ||
                            strMove.toLowerCase().equals(str2.toLowerCase())) {
                        return m;
                    }
                }
            }
        }

        Move move = null;
        for (int i = 0; i < 2; i++) {
            // Search for unique substring match
            for (int mi = 0; mi < moves.size; mi++) {
                Move m = moves.m[mi];
                String str1 = normalizeMoveString(TextIO.moveToString(pos, m, true));
                String str2 = normalizeMoveString(TextIO.moveToString(pos, m, false));
                boolean match;
                if (i == 0) {
                    match = (str1.startsWith(strMove) || str2.startsWith(strMove));
                } else {
                    match = (str1.toLowerCase().startsWith(strMove.toLowerCase()) ||
                            str2.toLowerCase().startsWith(strMove.toLowerCase()));
                }
                if (match) {
                    if (move != null) {
                        return null; // More than one match, not ok
                    } else {
                        move = m;
                    }
                }
            }
            if (move != null)
                return move;
        }
        return null;
    }

    /**
     * Convert a string, such as "e4" to a square number.
     * @return The square number, or -1 if not a legal square.
     */
    public static int getSquare(String s) {
        int x = s.charAt(0) - 'a';
        int y = s.charAt(1) - '1';
        if ((x < 0) || (x > 7) || (y < 0) || (y > 7))
            return -1;
        return Position.getSquare(x, y);
    }

    /**
     * Convert a square number to a string, such as "e4".
     */
    public static String squareToString(int square) {
        StringBuilder ret = new StringBuilder();
        int x = Position.getX(square);
        int y = Position.getY(square);
        ret.append((char) (x + 'a'));
        ret.append((char) (y + '1'));
        return ret.toString();
    }

    /**
     * Create an ascii representation of a position.
     */
    public static String asciiBoard(Position pos) {
        StringBuilder ret = new StringBuilder(400);
        String nl = String.format(Locale.US, "%n");
        ret.append("    +----+----+----+----+----+----+----+----+"); ret.append(nl);
        for (int y = 7; y >= 0; y--) {
            ret.append("    |");
            for (int x = 0; x < 8; x++) {
                ret.append(' ');
                int p = pos.getPiece(Position.getSquare(x, y));
                if (p == Piece.EMPTY) {
                    boolean dark = Position.darkSquare(x, y);
                    ret.append(dark ? ".. |" : "   |");
                } else {
                    ret.append(Piece.isWhite(p) ? ' ' : '*');
                    String pieceName = pieceToChar(p);
                    if (pieceName.length() == 0)
                        pieceName = "P";
                    ret.append(pieceName);
                    ret.append(" |");
                }
            }
            ret.append(nl);
            ret.append("    +----+----+----+----+----+----+----+----+");
            ret.append(nl);
        }
        return ret.toString();
    }

    /**
     * Convert move string to lower case and remove special check/mate symbols.
     */
    private static String normalizeMoveString(String str) {
        if (str.length() > 0) {
            char lastChar = str.charAt(str.length() - 1);
            if ((lastChar == '#') || (lastChar == '+')) {
                str = str.substring(0, str.length() - 1);
            }
        }
        return str;
    }
    
    private static String pieceToChar(int p) {
        switch (p) {
            case Piece.WQUEEN:  case Piece.BQUEEN:  return "Q";
            case Piece.WROOK:   case Piece.BROOK:   return "R";
            case Piece.WBISHOP: case Piece.BBISHOP: return "B";
            case Piece.WKNIGHT: case Piece.BKNIGHT: return "N";
            case Piece.WKING:   case Piece.BKING:   return "K";
        }
        return "";
    }
}
