package com.christophdietze.jack.shared.pgn;
import java.util.HashMap;
import java.util.Map;
import com.christophdietze.jack.shared.board.IllegalPositionException;
import com.christophdietze.jack.shared.board.Piece;
import com.christophdietze.jack.shared.board.PieceType;
import com.christophdietze.jack.shared.board.Position;
import com.christophdietze.jack.shared.board.PositionChecker;
/**
* @see {@link http://en.wikipedia.org/wiki/Forsyth-Edwards_Notation}
*/
public class FenParser {
private static Map<Piece, Character> squareToFenCharMap = new HashMap<Piece, Character>();
private static Map<Character, Piece> fenCharToSquareMap = new HashMap<Character, Piece>();
static {
initMaps();
}
private PositionChecker positionChecker = new PositionChecker();
private Position.Builder builder;
private Position position;
private String fenString;
private int curIndex;
private char curChar;
public FenParser() {
}
public void parse(String fenString) throws FenParsingException, IllegalPositionException {
this.fenString = fenString;
curIndex = -1;
nextChar();
position = null;
builder = new Position.Builder();
parsePosition();
parseWhitespace();
parseActiveColor();
parseWhitespace();
parseCastlingAvailability();
parseWhitespace();
parseEnPassantAvailability();
parseWhitespace();
parseHalfmoveClock();
parseWhitespace();
parseFullmoveNumber();
if (hasMoreChars()) {
throw new FenParsingException(fenString, curIndex, "end of input expected, found '" + curChar + "'");
}
position = builder.build();
builder = null;
positionChecker.checkForSensiblePosition(position);
}
public Position getPosition() {
return position;
}
private void parsePosition() throws FenParsingException {
int fenPosIndex = 0;
while (curChar != ' ') {
if (Character.isDigit(curChar)) {
int skipCount = curChar - '0';
fenPosIndex += skipCount;
} else if (curChar == '/') {
if (fenPosIndex % 8 != 0) {
throw new FenParsingException(fenString, curIndex, "unexpected '/'");
}
} else {
Piece square = toSquare(curChar);
if (square == null) {
throw new FenParsingException(fenString, curIndex, "unknown piece code '" + curChar + "'");
}
int boardPosIndex = (7 - fenPosIndex / 8) * 8 + fenPosIndex % 8;
builder.piece(boardPosIndex, toSquare(curChar));
fenPosIndex++;
}
nextChar();
}
}
private void parseActiveColor() throws FenParsingException {
if (curChar == 'w') {
builder.whiteToMove(true);
} else if (curChar == 'b') {
builder.whiteToMove(false);
} else {
throw new FenParsingException(fenString, curIndex, "expected 'w' or 'b' indicating the active color '");
}
nextChar();
}
private void parseCastlingAvailability() throws FenParsingException {
builder.canWhiteCastleKingside(false);
builder.canWhiteCastleQueenside(false);
builder.canBlackCastleKingside(false);
builder.canBlackCastleQueenside(false);
if (curChar == '-') {
nextChar();
return;
}
while (curChar != ' ') {
if (curChar == 'K') {
builder.canWhiteCastleKingside(true);
} else if (curChar == 'Q') {
builder.canWhiteCastleQueenside(true);
} else if (curChar == 'k') {
builder.canBlackCastleKingside(true);
} else if (curChar == 'q') {
builder.canBlackCastleQueenside(true);
}
nextChar();
}
}
private void parseEnPassantAvailability() throws FenParsingException {
if (curChar == '-') {
nextChar();
return;
}
int file = curChar - 'a';
nextChar();
int rank = builder.isWhiteToMove() ? 5 : 2;
char expectedRank = Character.forDigit(rank + 1, 10);
if (curChar != expectedRank) {
throw new FenParsingException(fenString, curIndex, "expected rank " + expectedRank);
}
int squareIndex = file + 8 * rank;
builder.enPassantPawnIndex(squareIndex, !builder.isWhiteToMove());
nextChar();
}
private void parseHalfmoveClock() throws FenParsingException {
int startIndex = curIndex;
while (Character.isDigit(curChar)) {
nextChar();
}
int halfmoveClock = Integer.parseInt(fenString.substring(startIndex, curIndex));
builder.halfmoveClock(halfmoveClock);
}
private void parseFullmoveNumber() throws FenParsingException {
int startIndex = curIndex;
while (hasMoreChars() && Character.isDigit(curChar)) {
nextChar();
}
int fullmoveNumber = Integer.parseInt(fenString.substring(startIndex, curIndex));
builder.fullmoveNumber(fullmoveNumber);
}
private void parseWhitespace() throws FenParsingException {
if (curChar != ' ') {
throw new FenParsingException(fenString, curIndex, "whitespace expected, found '" + curChar + "'");
}
nextChar();
}
private void nextChar() throws FenParsingException {
curIndex++;
if (curIndex > fenString.length()) {
throw new FenParsingException(fenString, curIndex, "unexpected end of string");
} else if (curIndex == fenString.length()) {
curChar = '\0';
} else {
curChar = fenString.charAt(curIndex);
}
}
private boolean hasMoreChars() {
return curIndex < fenString.length();
}
private static Piece toSquare(char fenChar) {
return fenCharToSquareMap.get(fenChar);
}
private static void initMaps() {
for (PieceType piece : PieceType.values()) {
Piece whiteSquare = Piece.getFromColorAndPieceType(true, piece);
Piece blackSquare = Piece.getFromColorAndPieceType(false, piece);
putMapping(whiteSquare, Character.toUpperCase(piece.getSymbol()));
putMapping(blackSquare, Character.toLowerCase(piece.getSymbol()));
}
}
private static void putMapping(Piece square, char c) {
squareToFenCharMap.put(square, c);
fenCharToSquareMap.put(c, square);
}
}