package chess4j.board;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import chess4j.App;
import chess4j.Color;
import chess4j.hash.Zobrist;
import chess4j.moves.Move;
import chess4j.pieces.Bishop;
import chess4j.pieces.King;
import chess4j.pieces.Knight;
import chess4j.pieces.Pawn;
import chess4j.pieces.Piece;
import chess4j.pieces.Queen;
import chess4j.pieces.Rook;

public class BoardImpl implements Board {
	private List<Undo> _undoStack = new ArrayList<Undo>();
	private Map<Square,Piece> _pieceMap = new HashMap<Square,Piece>();
	private Map<Piece, List<Square>> _squaresMap = new HashMap<Piece,List<Square>>();
	private Set<CastlingRights> _castlingRights = new HashSet<CastlingRights>();
	private Color _playerToMove;
	private Square _epSquare;
	private int _fiftyCounter;
	
	public BoardImpl() {
		_squaresMap.put(Rook.BLACK_ROOK, new ArrayList<Square>());
		_squaresMap.put(Rook.WHITE_ROOK, new ArrayList<Square>());
		_squaresMap.put(Knight.BLACK_KNIGHT, new ArrayList<Square>());
		_squaresMap.put(Knight.WHITE_KNIGHT, new ArrayList<Square>());
		_squaresMap.put(Bishop.BLACK_BISHOP, new ArrayList<Square>());
		_squaresMap.put(Bishop.WHITE_BISHOP, new ArrayList<Square>());
		_squaresMap.put(Queen.BLACK_QUEEN, new ArrayList<Square>());
		_squaresMap.put(Queen.WHITE_QUEEN, new ArrayList<Square>());
		_squaresMap.put(Pawn.BLACK_PAWN, new ArrayList<Square>());
		_squaresMap.put(Pawn.WHITE_PAWN, new ArrayList<Square>());
		_squaresMap.put(King.BLACK_KING, new ArrayList<Square>());
		_squaresMap.put(King.WHITE_KING, new ArrayList<Square>());
		resetBoard();
	}
		
	public void addCastlingRight(CastlingRights castlingRight) {
		_castlingRights.add(castlingRight);
	}

	public void addPiece(Piece p,Square s) {
		_pieceMap.put(s, p);
		if (p != null)
			_squaresMap.get(p).add(s);
	}
	
	private void applyKingSpecialCases(Move m) {
		if (_playerToMove.isWhite()) {
			_castlingRights.remove(CastlingRights.WHITE_KINGSIDE);
			_castlingRights.remove(CastlingRights.WHITE_QUEENSIDE);
			if (m.from().equals(Square.valueOf(File.FILE_E, Rank.RANK_1))) {
				if (m.to().equals(Square.valueOf(File.FILE_G, Rank.RANK_1))) {
					_fiftyCounter=0;
					movePiece(Square.valueOf(File.FILE_H, Rank.RANK_1),
							Square.valueOf(File.FILE_F,Rank.RANK_1));
				} else if (m.to().equals(Square.valueOf(File.FILE_C, Rank.RANK_1))) {
					_fiftyCounter=0;
					movePiece(Square.valueOf(File.FILE_A, Rank.RANK_1),
							Square.valueOf(File.FILE_D,Rank.RANK_1));
				}
			}
		} else {
			_castlingRights.remove(CastlingRights.BLACK_KINGSIDE);
			_castlingRights.remove(CastlingRights.BLACK_QUEENSIDE);
			if (m.from().equals(Square.valueOf(File.FILE_E, Rank.RANK_8))) {
				if (m.to().equals(Square.valueOf(File.FILE_G, Rank.RANK_8))) {
					_fiftyCounter=0;
					movePiece(Square.valueOf(File.FILE_H, Rank.RANK_8),
							Square.valueOf(File.FILE_F,Rank.RANK_8));
				} else if (m.to().equals(Square.valueOf(File.FILE_C, Rank.RANK_8))) {
					_fiftyCounter=0;
					movePiece(Square.valueOf(File.FILE_A, Rank.RANK_8),
							Square.valueOf(File.FILE_D,Rank.RANK_8));
				}
			}
		}
	}
	
	public void applyMove(Move m) {
		assert(BoardVerifier.verifyBoard(this));
		_undoStack.add(new Undo(m,_fiftyCounter,_castlingRights,_epSquare));

		if (m.captured()==null)
			_fiftyCounter++; // may still get set to 0 
		else {
			_fiftyCounter=0;
			if (m.to().equals(Square.valueOf(File.FILE_H, Rank.RANK_1))) {
				_castlingRights.remove(CastlingRights.WHITE_KINGSIDE);
			} else if (m.to().equals(Square.valueOf(File.FILE_A, Rank.RANK_1))) {
				_castlingRights.remove(CastlingRights.WHITE_QUEENSIDE);
			} if (m.to().equals(Square.valueOf(File.FILE_H, Rank.RANK_8))) {
				_castlingRights.remove(CastlingRights.BLACK_KINGSIDE);
			} else if (m.to().equals(Square.valueOf(File.FILE_A, Rank.RANK_8))) {
				_castlingRights.remove(CastlingRights.BLACK_QUEENSIDE);
			}
		}

		Piece p = removePiece(m.from());
		if (p instanceof Pawn) {
			applyPawnMove(p,m);
		} else {
			if (m.captured() != null)
				removePiece(m.to());
			addPiece(p,m.to());
			_epSquare=null;
			if (p instanceof King) 
				applyKingSpecialCases(m);
			else if (p instanceof Rook)
				applyRookSpecialCases(m);
		}
		swapPlayer();
		assert(BoardVerifier.verifyBoard(this));
	}

	private void applyPawnMove(Piece p,Move m) {
		_fiftyCounter=0;
		if (p.isWhite()) {
			if (m.from().rank().equals(Rank.RANK_2) && m.to().rank().equals(Rank.RANK_4)) {
				addPiece(p,m.to());
				_epSquare = Square.valueOf(m.to().file(), m.to().rank().south());
			} else if (m.to().equals(_epSquare)) {
				removePiece(Square.valueOf(_epSquare.file(), _epSquare.rank().south()));
				addPiece(p,m.to());
				_epSquare = null;
			} else if (m.to().rank().equals(Rank.RANK_8)) {
				removePiece(m.to());
				addPiece(m.promotion(),m.to());
				_epSquare = null;
			} else {
				removePiece(m.to());
				addPiece(p,m.to());
				_epSquare = null;
			}
		} else {
			if (m.from().rank().equals(Rank.RANK_7) && m.to().rank().equals(Rank.RANK_5)) {
				addPiece(p,m.to());
				_epSquare = Square.valueOf(m.to().file(), m.to().rank().north());
			} else if (m.to().equals(_epSquare)) {
				removePiece(Square.valueOf(_epSquare.file(), _epSquare.rank().north()));
				addPiece(p,m.to());
				_epSquare = null;
			} else if (m.to().rank().equals(Rank.RANK_1)) {
				removePiece(m.to());
				addPiece(m.promotion(),m.to());
				_epSquare = null;
			} else {
				removePiece(m.to());
				addPiece(p,m.to());
				_epSquare = null;
			}
		}
	}

	private void applyRookSpecialCases(Move m) {
		if (_playerToMove.isWhite()) {
			if (m.from().equals(Square.valueOf(File.FILE_H, Rank.RANK_1))) {
				_fiftyCounter=0;
				_castlingRights.remove(CastlingRights.WHITE_KINGSIDE);
			} else if (m.from().equals(Square.valueOf(File.FILE_A, Rank.RANK_1))) {
				_fiftyCounter=0;
				_castlingRights.remove(CastlingRights.WHITE_QUEENSIDE);
			}
		} else {
			if (m.from().equals(Square.valueOf(File.FILE_H, Rank.RANK_8))) {
				_fiftyCounter=0;
				_castlingRights.remove(CastlingRights.BLACK_KINGSIDE);
			} else if (m.from().equals(Square.valueOf(File.FILE_A, Rank.RANK_8))) {
				_fiftyCounter=0;
				_castlingRights.remove(CastlingRights.BLACK_QUEENSIDE);
			}			
		}
	}

	private boolean blackCanCastleKingSide() {
		Color opponent = Color.swap(_playerToMove);
		return hasCastlingRight(CastlingRights.BLACK_KINGSIDE)
			&& isEmpty(Square.valueOf(File.FILE_F,Rank.RANK_8)) 
			&& isEmpty(Square.valueOf(File.FILE_G,Rank.RANK_8))
			&& !App.getAttack().attacked(this,Square.valueOf(File.FILE_E,Rank.RANK_8),opponent)
			&& !App.getAttack().attacked(this,Square.valueOf(File.FILE_F,Rank.RANK_8),opponent)
			;
	}

	private boolean blackCanCastleQueenSide() {
		Color opponent = Color.swap(_playerToMove);
		return hasCastlingRight(CastlingRights.BLACK_QUEENSIDE)
			&& isEmpty(Square.valueOf(File.FILE_D,Rank.RANK_8)) 
			&& isEmpty(Square.valueOf(File.FILE_C,Rank.RANK_8)) 
			&& isEmpty(Square.valueOf(File.FILE_B,Rank.RANK_8))
			&& !App.getAttack().attacked(this,Square.valueOf(File.FILE_E,Rank.RANK_8),opponent)
			&& !App.getAttack().attacked(this,Square.valueOf(File.FILE_D,Rank.RANK_8),opponent)
			;
	}
	
	public boolean canCastle(CastlingRights cr) {
		if (cr.equals(CastlingRights.WHITE_KINGSIDE))
			return whiteCanCastleKingSide();
		else if (cr.equals(CastlingRights.WHITE_QUEENSIDE))
			return whiteCanCastleQueenSide();
		else if (cr.equals(CastlingRights.BLACK_KINGSIDE))
			return blackCanCastleKingSide();
		else return blackCanCastleQueenSide();
	}
	
	public void clearBoard() {
		List<Square> squares = Square.allSquares();
		for (Square sq : squares) {
			Piece p = getPiece(sq);
			if (p != null) {
				removePiece(sq);
			}
		}
		
		_epSquare = null;
		_castlingRights.clear();
		_fiftyCounter = 0;
		_undoStack.clear();
	}

	public synchronized Board deepCopy() {
		BoardImpl b = new BoardImpl();
		b._undoStack.clear();
		b._undoStack.addAll(_undoStack);
		b._pieceMap.clear();
		for (Square sq : _pieceMap.keySet()) {
			b._pieceMap.put(sq, _pieceMap.get(sq));
		}
		b._squaresMap.clear();
		for (Piece p : _squaresMap.keySet()) {
			List<Square> squares = new ArrayList<Square>(_squaresMap.get(p));
			b._squaresMap.put(p, squares);
		}
		b._castlingRights.clear();
		b._castlingRights.addAll(_castlingRights);
		b._playerToMove=_playerToMove;
		b._epSquare=_epSquare;
		b._fiftyCounter=_fiftyCounter;
		
		return b;
	}
	
	public List<Square> getSquares(Piece p) {
		return Collections.unmodifiableList(_squaresMap.get(p));
	}
	
	private Square getBlackKingSquare() {
		List<Square> kingSquares = _squaresMap.get(King.BLACK_KING);
		assert(kingSquares.size()==1);
		return kingSquares.get(0);
	}
	
	public Square getEPSquare() {
		return _epSquare;
	}
	
	public int getFiftyCounter() {
		return _fiftyCounter;
	}

	public Square getKingSquare(Color player) {
		return player.isWhite() ? getWhiteKingSquare() : getBlackKingSquare();
	}
	
	public int getMoveCounter() {
		return _undoStack.size();
	}

	public Piece getPiece(Square square) {
		return _pieceMap.get(square);
	}
	
	public Color getPlayerToMove() {
		return _playerToMove;
	}

	private Square getWhiteKingSquare() {
		List<Square> kingSquares = _squaresMap.get(King.WHITE_KING);
		assert(kingSquares.size()==1);
		return kingSquares.get(0);
	}

	private boolean hasCastlingRight(CastlingRights cr) {
		return _castlingRights.contains(cr);
	}

	public boolean isEmpty(Square square) {
		return _pieceMap.get(square)==null;
	}

	public boolean isOpponentInCheck() {
		return App.getAttack().attacked(this,getKingSquare(Color.swap(_playerToMove)),_playerToMove);
	}

	public boolean isPlayerInCheck() {
		return App.getAttack().attacked(this,getKingSquare(_playerToMove),Color.swap(_playerToMove));
	}
	
	public void movePiece(Square from,Square to) {
		if (from.equals(to))
			return;
		addPiece(getPiece(from),to);
		removePiece(from);
	}

	private Piece removePiece(Square s) {
		Piece p = getPiece(s);
		if (p==null) return null;
		_pieceMap.remove(s);
		_squaresMap.get(p).remove(s);
		return p;
	}

	public void resetBoard() {
		_undoStack.clear();
		_pieceMap.clear();
		_squaresMap.get(Rook.WHITE_ROOK).clear();
		_squaresMap.get(Rook.BLACK_ROOK).clear();
		_squaresMap.get(Knight.WHITE_KNIGHT).clear();
		_squaresMap.get(Knight.BLACK_KNIGHT).clear();
		_squaresMap.get(Bishop.WHITE_BISHOP).clear();
		_squaresMap.get(Bishop.BLACK_BISHOP).clear();
		_squaresMap.get(Queen.WHITE_QUEEN).clear();
		_squaresMap.get(Queen.BLACK_QUEEN).clear();
		_squaresMap.get(Pawn.WHITE_PAWN).clear();
		_squaresMap.get(Pawn.BLACK_PAWN).clear();
		_squaresMap.get(King.WHITE_KING).clear();
		_squaresMap.get(King.BLACK_KING).clear();

		addPiece(Rook.BLACK_ROOK,Square.valueOf(File.FILE_A, Rank.RANK_8));
		addPiece(Knight.BLACK_KNIGHT,Square.valueOf(File.FILE_B, Rank.RANK_8));
		addPiece(Bishop.BLACK_BISHOP,Square.valueOf(File.FILE_C, Rank.RANK_8));
		addPiece(Queen.BLACK_QUEEN,Square.valueOf(File.FILE_D, Rank.RANK_8));
		addPiece(King.BLACK_KING,Square.valueOf(File.FILE_E, Rank.RANK_8));
		addPiece(Bishop.BLACK_BISHOP,Square.valueOf(File.FILE_F, Rank.RANK_8));
		addPiece(Knight.BLACK_KNIGHT,Square.valueOf(File.FILE_G, Rank.RANK_8));
		addPiece(Rook.BLACK_ROOK,Square.valueOf(File.FILE_H, Rank.RANK_8));
		List<Square> squares = Square.rankSquares(Rank.RANK_7);
		for (Square sq : squares) {
			addPiece(Pawn.BLACK_PAWN,sq);
		}
		squares = Square.rankSquares(Rank.RANK_2);
		for (Square sq : squares) {
			addPiece(Pawn.WHITE_PAWN,sq);
		}
		addPiece(Rook.WHITE_ROOK,Square.valueOf(File.FILE_A, Rank.RANK_1));
		addPiece(Knight.WHITE_KNIGHT,Square.valueOf(File.FILE_B, Rank.RANK_1));
		addPiece(Bishop.WHITE_BISHOP,Square.valueOf(File.FILE_C, Rank.RANK_1));
		addPiece(Queen.WHITE_QUEEN,Square.valueOf(File.FILE_D, Rank.RANK_1));
		addPiece(King.WHITE_KING,Square.valueOf(File.FILE_E, Rank.RANK_1));
		addPiece(Bishop.WHITE_BISHOP,Square.valueOf(File.FILE_F, Rank.RANK_1));
		addPiece(Knight.WHITE_KNIGHT,Square.valueOf(File.FILE_G, Rank.RANK_1));
		addPiece(Rook.WHITE_ROOK,Square.valueOf(File.FILE_H, Rank.RANK_1));
	
		_castlingRights.clear();
		_castlingRights.addAll(EnumSet.allOf(CastlingRights.class));
		_playerToMove = Color.WHITE;
		_epSquare = null;
		_fiftyCounter = 0;
	}
	
	public void setEP(Square ep) {
		_epSquare = ep;
	}
	
	public void swapPlayer() {
		_playerToMove = Color.swap(_playerToMove);
	}

	private void undoCastle(Undo u,Piece p) {
		if (p.isWhite()) {
			if (u.getMove().from().equals(Square.valueOf(File.FILE_E, Rank.RANK_1))) {
				if (u.getMove().to().equals(Square.valueOf(File.FILE_G, Rank.RANK_1))) {
					movePiece(Square.valueOf(File.FILE_F, Rank.RANK_1),
							Square.valueOf(File.FILE_H,Rank.RANK_1));
				} else if (u.getMove().to().equals(Square.valueOf(File.FILE_C, Rank.RANK_1))) {
					movePiece(Square.valueOf(File.FILE_D, Rank.RANK_1),
							Square.valueOf(File.FILE_A,Rank.RANK_1));
				}
			}
		} else {
			if (u.getMove().from().equals(Square.valueOf(File.FILE_E, Rank.RANK_8))) {
				if (u.getMove().to().equals(Square.valueOf(File.FILE_G, Rank.RANK_8))) {
					movePiece(Square.valueOf(File.FILE_F, Rank.RANK_8),
							Square.valueOf(File.FILE_H,Rank.RANK_8));
				} else if (u.getMove().to().equals(Square.valueOf(File.FILE_C, Rank.RANK_8))) {
					movePiece(Square.valueOf(File.FILE_D, Rank.RANK_8),
							Square.valueOf(File.FILE_A,Rank.RANK_8));
				}
			}
		} 
	}
	
	private void undoPawnMove(Undo u,Piece p) {
		if (p.isWhite()) {
			if (u.getMove().to().equals(_epSquare)) {
				addPiece(u.getMove().captured(),Square.valueOf(_epSquare.file(), _epSquare.rank().south()));
				addPiece(p,u.getMove().from());
			} else {
				addPiece(u.getMove().captured(),u.getMove().to());
				addPiece(p,u.getMove().from());
			}
		} else {
			if (u.getMove().to().equals(_epSquare)) {
				addPiece(u.getMove().captured(),Square.valueOf(_epSquare.file(), _epSquare.rank().north()));
				addPiece(p,u.getMove().from());
			} else {
				addPiece(u.getMove().captured(),u.getMove().to());
				addPiece(p,u.getMove().from());
			}
		}			
	}

	private void undoPromotion(Undo u) {
		addPiece(u.getMove().captured(),u.getMove().to());
		addPiece(
			_playerToMove.equals(Color.WHITE)?Pawn.WHITE_PAWN:Pawn.BLACK_PAWN,
			u.getMove().from());
	}
	
	public void undoLastMove() {
		assert(BoardVerifier.verifyBoard(this));
		int ind = _undoStack.size()-1;
		Undo u = _undoStack.remove(ind);
		swapPlayer();
		_epSquare = u.getEpSquare();
		_fiftyCounter = u.getFiftyCounter();
		_castlingRights.clear();
		_castlingRights.addAll(u.getCastlingRights());
		
		Piece p = removePiece(u.getMove().to());
		if (u.getMove().promotion() != null) {
			undoPromotion(u);
		} else if (p instanceof Pawn) {
			undoPawnMove(u,p);
		} else { 
			if (p instanceof King) {
				undoCastle(u,p);
			}
			addPiece(u.getMove().captured(),u.getMove().to());
			addPiece(p,u.getMove().from());
		}
		assert(BoardVerifier.verifyBoard(this));
	}
	
	private boolean whiteCanCastleKingSide() {
		Color opponent = Color.swap(_playerToMove);
		return hasCastlingRight(CastlingRights.WHITE_KINGSIDE)
			&& isEmpty(Square.valueOf(File.FILE_F,Rank.RANK_1)) 
			&& isEmpty(Square.valueOf(File.FILE_G,Rank.RANK_1))
			&& !App.getAttack().attacked(this,Square.valueOf(File.FILE_E,Rank.RANK_1),opponent)
			&& !App.getAttack().attacked(this,Square.valueOf(File.FILE_F,Rank.RANK_1),opponent)
			;
	}

	private boolean whiteCanCastleQueenSide() {
		Color opponent = Color.swap(_playerToMove);
		return hasCastlingRight(CastlingRights.WHITE_QUEENSIDE)
			&& isEmpty(Square.valueOf(File.FILE_D,Rank.RANK_1)) 
			&& isEmpty(Square.valueOf(File.FILE_C,Rank.RANK_1)) 
			&& isEmpty(Square.valueOf(File.FILE_B,Rank.RANK_1))
			&& !App.getAttack().attacked(this,Square.valueOf(File.FILE_E,Rank.RANK_1),opponent)
			&& !App.getAttack().attacked(this,Square.valueOf(File.FILE_D,Rank.RANK_1),opponent)
			;
	}
	
	@Override 
	public int hashCode() {
		int h=0;
		for (Square sq : Square.allSquares()) {
			Piece p = getPiece(sq);
			if (p!=null) {
				h ^= Zobrist.getPieceKey(sq, p);
			}
		}
		h ^= Zobrist.getPlayerKey(getPlayerToMove());
		Square ep = getEPSquare();
		if (ep != null) {
			h ^= Zobrist.getEnPassantKey(ep);
		}
		for (CastlingRights cr : _castlingRights) {
			h ^= Zobrist.getCastlingKey(cr);
		}
		return h;
	}
}
