/* JChessBoard -- a chess game
 * Copyright (C) 2000-2004 Claus Divossen <claus.divossen@gmx.de>
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

/* $Id: AI.java,v 1.37 2004/12/26 23:12:14 cdivossen Exp $ */

package jchessboard;

import java.util.HashMap;
import java.util.List;
import java.util.Random;
import java.util.Vector;

class AI implements Runnable {
    private static final int EM = 0; // EMpty.
    private static final int WK = 1; // White King
    private static final int WQ = 2; // White Queen
    private static final int WR = 3; // White Rook
    private static final int WB = 4; // White Bishop
    private static final int WN = 5; // White Knight
    private static final int WP = 6; // White Pawn
    private static final int BK = 7; // Black King
    private static final int BQ = 8; // Black Queen
    private static final int BR = 9; // Black Rook
    private static final int BB = 10; // Black Bishop
    private static final int BN = 11; // Black Knight
    private static final int BP = 12; // Black Pawn
    // As suggested by Shannon 1949:
    private final static int[] figureScore = { 0, 20000, 900, 500, 350, 300, 100, //White
        -20000, -900, -500, -350, -300, -100 //Black
    };
    private boolean isEnabled = true;
    private Thread aiThread;
    private JChessBoard jcb;
    private int maxDepth = 9; // Should be global, used static in recursion.
    private int minDepth = 3;
    private Random random = new Random();
    private int evalCounter, posCounter, getAllMovesCounter, getCapturingMovesCounter;
    private int qSearchCounter;

    private static int TRANSTABLESIZE = 0x2000;
    private static long[][] hashBase = new long[13][64];
    private TransTableEntry[] transTable;
    // History Heuristic Table:
    private int[][][] histHeu; //contains scores [from][to][ply]
    // Killer moves:
    private int[] killer1from = new int[20];
    private int[] killer1to = new int[20];
    private int[] killer1value = new int[20];
    private int[] killer2from = new int[20];
    private int[] killer2to = new int[20];
    private int[] killer2value = new int[20];

    private long startTime, endTime;
    private long timeout = 15000; //millisec.
    private int movesMade = 0;
    private static boolean firstAI = true;
    private int transTableHits, transTableMiss, transTableFaults;
    private static String prefix[] = { "          0 ", "        1 ", "      2 ", "    3 ", "  4 ", "5 " };
    private List visitedPositions;

    public static String getVersion() {
        return "$Id: AI.java,v 1.37 2004/12/26 23:12:14 cdivossen Exp $";
    }

    public void prepareMove() {
        aiThread.interrupt();
    }

    public void prepareMove(long timeout) {
        this.timeout = timeout;
        //        this.timeout = 180000;
        aiThread.interrupt();
    }

    public int getHash(VirtualBoard vb) {
        int hash = 0;
        for (int n = 0; n < 64; n++)
            if (vb.field[n] != EM)
                //Empty fields don't need to change the hash.
                hash ^= hashBase[vb.field[n]][n];
        return hash;
    }

    class TransTableEntry {
        int depth = 0;
        int bestFrom = 64;
        int bestTo = 64;
        boolean isSet = false;
        public VirtualBoard virtualBoard;
        int score = 0;

        public TransTableEntry() {
        };
        boolean isWhiteTurn;

        public void set(VirtualBoard vb, int score, int dep, int bF, int bT) {
            virtualBoard = vb.clonedBoard();
            this.score = score;
            depth = dep;
            bestFrom = bF;
            bestTo = bT;
            isSet = true;
            isWhiteTurn = vb.isWhiteTurn;
        }

        public void clear() {
            virtualBoard = null;
            depth = 0;
            bestFrom = 64;
            bestTo = 64;
            isSet = false;
            score = 0;
        }

    }

    public void newGame() {
        // Needed to reset game specific values.
        movesMade = 0;
    }

    public static int evaluateMaterial(VirtualBoard vb) {
        int score = 0;
        for (int n = 0; n < 64; n++)
            score += figureScore[vb.field[n]];
        return (vb.isWhiteTurn) ? score : -score;
    }

    private static int countWhiteAttackedFields(VirtualBoard vb) {
        int result = 0;
        long board = vb.getWhiteAttackBoard();
        for (int x = 0; x < 64; x++) {
            if ((board & (1l << x)) != 0)
                result++;
        }
        return result;
    }

    private static int countWhiteAttackedCenterFields(VirtualBoard vb) {
        int result = 0;
        long board = vb.getWhiteAttackBoard();
        if ((board & (1l << 26)) != 0)
            result++;
        if ((board & (1l << 27)) != 0)
            result++;
        if ((board & (1l << 28)) != 0)
            result++;
        if ((board & (1l << 29)) != 0)
            result++;

        if ((board & (1l << 34)) != 0)
            result++;
        if ((board & (1l << 35)) != 0)
            result++;
        if ((board & (1l << 36)) != 0)
            result++;
        if ((board & (1l << 37)) != 0)
            result++;
        return result;
    }

    private static int countBlackAttackedFields(VirtualBoard vb) {
        int result = 0;
        long board = vb.getBlackAttackBoard();
        for (int x = 0; x < 64; x++) {
            if ((board & (1l << x)) != 0)
                result++;
        }
        return result;
    }

    private static int countBlackAttackedCenterFields(VirtualBoard vb) {
        int result = 0;
        long board = vb.getBlackAttackBoard();
        if ((board & (1l << 26)) != 0)
            result++;
        if ((board & (1l << 27)) != 0)
            result++;
        if ((board & (1l << 28)) != 0)
            result++;
        if ((board & (1l << 29)) != 0)
            result++;

        if ((board & (1l << 34)) != 0)
            result++;
        if ((board & (1l << 35)) != 0)
            result++;
        if ((board & (1l << 36)) != 0)
            result++;
        if ((board & (1l << 37)) != 0)
            result++;
        return result;
    }

    public static int evaluate(VirtualBoard vb) {
        int score = 0;
        int pieceCount = 0;

        if (vb.blackKingPos == 64)
            return (vb.isWhiteTurn) ? 20001 : -20001;
        if (vb.whiteKingPos == 64)
            return (!vb.isWhiteTurn) ? 20001 : -20001;

        // Material evaluation
        for (int n = 0; n < 64; n++) {
            if (vb.field[n] != VirtualBoard.EMPTY_FIELD) {
                score += figureScore[vb.field[n]];
                pieceCount++;
            }
        }

        // Positional evaluations 

        // Attack/Defense-Map
        score += countWhiteAttackedFields(vb) * 2;
        score -= countBlackAttackedFields(vb) * 2;

        // Center control in start and middle game:
        if (pieceCount > 12) {
            score += countWhiteAttackedCenterFields(vb) * 2;
            score -= countBlackAttackedCenterFields(vb) * 2;
        }

        // TODO: King safety (As suggested by Turing):
        /*        int kingRank = vb.blackKingPos >> 3;
                int kingFile = vb.blackKingPos & 7;
                for (int toRank = 0; toRank < 8; toRank++)
                    for (int toFile = 0; toFile < 8; toFile++)
                        if (vb.BQmightMove(kingRank, kingFile, toRank, toFile))
                            score++;
                kingRank = vb.whiteKingPos >> 3;
                kingFile = vb.whiteKingPos & 7;
                for (int toRank = 0; toRank < 8; toRank++)
                    for (int toFile = 0; toFile < 8; toFile++)
                        if (vb.WQmightMove(kingRank, kingFile, toRank, toFile))
                            score--;
        */
        // King attack:
        if (vb.isWhiteTurn)
            if (vb.isAttackedByBlack(vb.whiteKingPos))
                score -= 20;
            else if (vb.isAttackedByWhite(vb.blackKingPos))
                score += 20;

        // Ability to castle:
        if (vb.whiteKingHasMoved)
            score -= 10;
        else {
            if (vb.leftWhiteRookHasMoved)
                score -= 5;
            if (vb.rightWhiteRookHasMoved)
                score -= 5;
        }
        if (vb.blackKingHasMoved)
            score += 10;
        else {
            if (vb.leftBlackRookHasMoved)
                score += 5;
            if (vb.rightBlackRookHasMoved)
                score += 5;
        }

        // Pending pawn promotions:
        for (int n = 48; n < 56; n++)
            if (vb.field[n] == WP)
                score += 20;
        for (int n = 40; n < 48; n++)
            if (vb.field[n] == WP)
                score += 10;
        for (int n = 8; n < 16; n++)
            if (vb.field[n] == BP)
                score -= 20;
        for (int n = 16; n < 24; n++)
            if (vb.field[n] == BP)
                score -= 10;

        return (vb.isWhiteTurn) ? score : -score;
    }

    public int alphabeta(VirtualBoard vb, int ply, int depth, int qdepth, int alpha, int beta) {

        if (depth == 0) {
            evalCounter++;
            return evaluate(vb);
        }

		if ((System.currentTimeMillis() >= endTime) || !isEnabled) {
			return Integer.MIN_VALUE;
		}

        VirtualBoard pos, bestPos;
        Move bestMove = null;
        int best_score = -10000000;
        TransTableEntry tbEntry;
        int hash = getHash(vb);

        //        		if (maxDepth - depth >= 2) {
        if ((tbEntry = transTable[hash]).isSet) {
            if (tbEntry.virtualBoard.equals(vb) && tbEntry.depth >= depth) {
                transTableHits++;
                return tbEntry.score;
            }
        } else {
            transTableMiss++;
        }
        //        		}

        best_score = -10000000;
        bestPos = null;
        getAllMovesCounter++;
        List moves = vb.getAllMoves();

        if (qdepth > 0) {
            bestPos = vb;
            evalCounter++;
            best_score = evaluate(vb);
            if (qdepth > 1)
                return best_score;
        }

        if (moves.size() == 0) {
            // No more moves from this node:
            if (vb.isWhiteTurn) {
                if (vb.isAttackedByBlack(vb.whiteKingPos)) {
                    return -20001 - depth * 100; // Mate (Lost)
                } else
                    return 10001; // Stalemate. Always avoid this!
            } else {
                if (vb.isAttackedByWhite(vb.blackKingPos)) {
                    return -20001 - depth * 100; // Mate (Lost)
                } else
                    return 10001; // Stalemate. Always avoid this!
            }
            // TODO Other drawish situations like vb.onlyKingsLeft (slow) should be considered too.
        }

        // Move ordering:
        List positions = new Vector();
        for (int x = 0; x < moves.size(); x++) {
            Move move = (Move) moves.get(x);
            pos = vb.clonedBoard();
            pos.makeAnyMove(move);
            if (depth > 1) {
                pos.score = -evaluateMaterial(pos);
                // Is killer move?
                if (pos.lastFrom == killer1from[depth] && pos.lastTo == killer1to[depth])
                    pos.score += 500;
                else if (pos.lastFrom == killer2from[depth] && pos.lastTo == killer2to[depth])
                    pos.score += 450;
                pos.score += histHeu[pos.lastFrom][pos.lastTo][ply + 1] * 10; // ply+1?
            }
            positions.add(pos);
        }
        if (depth > 1) {
            java.util.Collections.sort(positions, new java.util.Comparator() {
                public int compare(Object o1, Object o2) {
                    return ((VirtualBoard) o1).score - ((VirtualBoard) o2).score;
                }
            });
        }

        //
        // Main loop
        //
        for (int x = positions.size() - 1; x >= 0; x--) {
            pos = (VirtualBoard) positions.get(x);
            boolean isCapturingMove = false;
            if (vb.field[pos.lastTo] != VirtualBoard.EMPTY_FIELD) {
                isCapturingMove = true;
            }
            int score = -10000000;
            if (isCapturingMove && depth == 1) { // Perform a quiescence search
                qSearchCounter++;
                score = -alphabeta(pos, ply + 1, 1, qdepth + 1, -beta, -alpha);
            } else {
                score = -alphabeta(pos, ply + 1, depth - 1, 0, -beta, -alpha);
            }

            if (score > best_score) {
                best_score = score;
                bestPos = pos;
            }
            if (best_score > alpha)
                alpha = best_score;

            if (alpha >= beta) {
                if (score > killer1value[depth]) {
                    killer2from[depth] = killer1from[depth];
                    killer2to[depth] = killer1to[depth];
                    killer2value[depth] = killer1value[depth];
                    killer1from[depth] = bestPos.lastFrom;
                    killer1to[depth] = bestPos.lastTo;
                    killer1value[depth] = score;
                }
                if (!transTable[hash].isSet || transTable[hash].depth < depth)
                    transTable[hash].set(vb, best_score, depth, bestPos.lastFrom, bestPos.lastTo);
                histHeu[bestPos.lastFrom][bestPos.lastTo][ply]++;
                return alpha;
            }

        }
		if(bestPos!=null) {
			if (!transTable[hash].isSet || transTable[hash].depth < depth)
				transTable[hash].set(vb, best_score, depth, bestPos.lastFrom, bestPos.lastTo);
			if (best_score > killer1value[depth]) {
				killer2from[depth] = killer1from[depth];
				killer2to[depth] = killer1to[depth];
				killer2value[depth] = killer1value[depth];
				killer1from[depth] = bestPos.lastFrom;
				killer1to[depth] = bestPos.lastTo;
				killer1value[depth] = best_score;
			}
			histHeu[bestPos.lastFrom][bestPos.lastTo][ply]++;
		}
        return best_score;
    }

    public void work() {
        VirtualBoard startBoard;
        startBoard = jcb.getCurrentVirtualBoard();
        if (!startBoard.gameIsFinished()) {

            startTime = System.currentTimeMillis();
            endTime = startTime + timeout;

            visitedPositions = jcb.history.getAllBoards();
            evalCounter = 0;
            posCounter = 0;
            getAllMovesCounter = 0;
            getCapturingMovesCounter = 0;
            qSearchCounter = 0;
            transTableHits = 0;
            transTableMiss = 0;
            transTableFaults = 0;
            for (int i = 0; i < TRANSTABLESIZE; i++)
                transTable[i].clear();
            for (int i = 0; i < 20; i++) {
                killer1from[i] = 64;
                killer1to[i] = 64;
                killer1value[i] = -1000000;
                killer2from[i] = 64;
                killer2to[i] = 64;
                killer2value[i] = -1000000;
            }
            histHeu = new int[64][64][20];

            maxDepth = 20;
            int depth = 2;
            int ply = 0;
            long minTime = 500; // At least 500ms to be spent.
            long time = 0;

            Move bestMove = null;
            List moves = startBoard.getAllMoves();

			final HashMap moveScores = new HashMap();
			for (int x = 0; x<moves.size(); x++) {
				moveScores.put(moves.get(x), new Integer(Integer.MIN_VALUE));
			}

            if (moves.size() == 1) { // Only one move possible, no need to think about. 
                bestMove = (Move) moves.get(0);
                depth = maxDepth;
            } else
                do {
                    System.out.println("\nDepth: " + depth);
                    jcb.showMessage("Depth " + depth + "...", "small");
                    final int thisPly = ply;
                    java.util.Collections.sort(moves, new java.util.Comparator() {
                        public int compare(Object o1, Object o2) {
                            Move m1 = (Move) o1;
                            Move m2 = (Move) o2;
							return ((Integer) moveScores.get(m1)).intValue() - ((Integer) moveScores.get(m2)).intValue(); 
                        }
                    });
                    int best_score = -100000000;
                    int alpha = -100000000;
                    for (int x = moves.size() - 1; x >= 0; x--) {
                        Move move = (Move) moves.get(x);
                        VirtualBoard position = startBoard.clonedBoard();
                        position.makeAnyMove(move);
                        boolean visited = false;
                        for (int n = 0; n < visitedPositions.size(); n++)
                            if (((VirtualBoard) visitedPositions.get(n)).isEqualPosition(position))
                                visited = true;
                        int score;
                        if (visited)
                            score = -30000;
                        else {
							score = -alphabeta(position, ply + 1, depth - 1, 0, -10000000, -alpha);
							if ((System.currentTimeMillis() >= endTime) || !isEnabled) {
								score=Integer.MIN_VALUE;
								System.out.print("TIMEOUT");
								break;
							}
                        }
                        if (score > best_score) {
                            best_score = score;
                            bestMove = move;
                        }
                        if (best_score > alpha)
                            alpha = best_score;
						moveScores.put(move,new Integer(score));
                        System.out.print(move + ":" + score + " ");
                    }
                    depth++;
                    time = System.currentTimeMillis();
                } while (
                    time < endTime
                        && (depth <= maxDepth || (time - startTime) < minTime)
                        && ((time - startTime) < (endTime - time)));
            jcb.showMessage(
                evalCounter
                    + " evalutaions, "
                    + getAllMovesCounter
                    + " getAllMoves, "
                    + qSearchCounter
                    + " quiescence searches, "
                    + (System.currentTimeMillis() - startTime)
                    + " ms",
                "small");
            /*            System.out.println("--------------------------");
                        System.out.println(evalCounter + " evaluations");
                        System.out.println("Transition Table Statistics:");
                        System.out.print("Size: " + TRANSTABLESIZE + " entries");
                        System.out.print("  Hits: " + transTableHits);
                        System.out.print("  Misses: " + transTableMiss);
                        System.out.println("  Faults: " + transTableFaults);
                        int setEntryCounter = 0;
                        for (int n = 0; n < TRANSTABLESIZE; n++)
                            if (transTable[n].isSet)
                                setEntryCounter++;
                        System.out.println(setEntryCounter + " entries set"); */
            final Move aiMove = bestMove;
            // TODO: If there are several equally rated moves, one should be picked by random.
            if (isEnabled) {
                if (startBoard.equals(jcb.getCurrentVirtualBoard())) {
                    javax.swing.SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            synchronized (jcb) {
                                jcb.makeAIsMove(aiMove);
                            }
                        }
                    });
                } else {
                    jcb.showMessage("AI: Who changed the board?!");
                }
            }
        }
    }

    public void run() {
        //Initialize hash base table:
        transTable = new TransTableEntry[TRANSTABLESIZE];
        histHeu = new int[20][64][64];
        // Create transposition table:
        for (int i = 0; i < TRANSTABLESIZE; i++)
            transTable[i] = new TransTableEntry();
        newGame();
        jcb.showMessage("AI is ready.");
        while (isEnabled) {
            try {
                synchronized (this) {
                    wait();
                }
            } catch (InterruptedException ie) {
            }
            if (isEnabled)
                work(); // Do your job!
        }
    }

    public void perfTest() {
        // Performance-Check
        VirtualBoard testboard = new VirtualBoard();
        testboard.init();
        for (int n = 0; n < 1000; n++)
            AI.evaluate(testboard);

        long start = System.currentTimeMillis();
        for (int n = 0; n < 1000; n++)
            AI.evaluate(testboard);
        long end = System.currentTimeMillis();
        jcb.showMessage(1000000 / (end - start) + " evaluations/sec.");

        for (int n = 0; n < 1000; n++)
            testboard.getAllMoves();
        start = System.currentTimeMillis();
        for (int n = 0; n < 1000; n++)
            testboard.getAllMoves();
        end = System.currentTimeMillis();
        jcb.showMessage(1000000 / (end - start) + " getAllMoves/sec");

    }

    public void shutdown() {
        isEnabled = false;
        aiThread.interrupt();
        try {
            aiThread.join();
        } catch (InterruptedException e) {
        }
    }

    // The constructor:
    public AI(JChessBoard jb) {
        jcb = jb;
        if (firstAI) {
            for (int i = 0; i < 13; i++)
                for (int j = 0; j < 64; j++)
                    hashBase[i][j] = random.nextInt(TRANSTABLESIZE - 1);
            firstAI = false;
        }
        (aiThread = new Thread(this)).start();
    }
}
