package featurecat.lizzie.rules;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import featurecat.lizzie.analysis.MoveData;
import org.eclipse.collections.api.set.MutableSet;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class BoardData {
    private ImmutablePair<Integer, Integer> boardSize;

    private Stone[] stonesOnBoard;
    private int[] lastMove;
    private Stone lastMoveColor;
    private boolean blackToPlay;
    private Zobrist zobrist;
    private int moveNumber;
    private int[] moveNumberListOnBoard;
    private MutableSet<Coordinates> removedEnemyStoneIndexes;
    private int blackPrisonersCount;
    private int whitePrisonersCount;

    private List<VariationData> variationDataList;

    public BoardData(ImmutablePair<Integer, Integer> boardSize, Stone[] stonesOnBoard, int[] lastMove, Stone lastMoveColor, boolean blackToPlay, Zobrist zobrist, int moveNumber, int[] moveNumberListOnBoard, List<VariationData> variationDataList, MutableSet<Coordinates> removedEnemyStoneIndexes, int blackPrisonersCount, int whitePrisonersCount) {
        this.boardSize = boardSize;
        this.stonesOnBoard = stonesOnBoard;
        this.lastMove = lastMove;
        this.lastMoveColor = lastMoveColor;
        this.blackToPlay = blackToPlay;
        this.zobrist = zobrist;
        this.moveNumber = moveNumber;
        this.moveNumberListOnBoard = moveNumberListOnBoard;
        this.variationDataList = variationDataList;
        this.removedEnemyStoneIndexes = removedEnemyStoneIndexes;
        this.blackPrisonersCount = blackPrisonersCount;
        this.whitePrisonersCount = whitePrisonersCount;
    }

    public BoardData(ImmutablePair<Integer, Integer> boardSize, Stone[] stonesOnBoard, int[] lastMove, Stone lastMoveColor, boolean blackToPlay, Zobrist zobrist, int moveNumber, int[] moveNumberListOnBoard, MutableSet<Coordinates> removedEnemyStoneIndexes, int blackPrisonersCount, int whitePrisonersCount) {
        this(boardSize, stonesOnBoard, lastMove, lastMoveColor, blackToPlay, zobrist, moveNumber, moveNumberListOnBoard, null, removedEnemyStoneIndexes, blackPrisonersCount, whitePrisonersCount);
    }

    public BoardData(Stone[] stonesOnBoard, int[] lastMove, Stone lastMoveColor, boolean blackToPlay, Zobrist zobrist, int moveNumber, int[] moveNumberListOnBoard, MutableSet<Coordinates> removedEnemyStoneIndexes, int blackPrisonersCount, int whitePrisonersCount) {
        this(ImmutablePair.of(19, 19), stonesOnBoard, lastMove, lastMoveColor, blackToPlay, zobrist, moveNumber, moveNumberListOnBoard, null, removedEnemyStoneIndexes, blackPrisonersCount, whitePrisonersCount);
    }

    public ImmutablePair<Integer, Integer> getBoardSize() {
        return boardSize;
    }

    public Stone[] getStonesOnBoard() {
        return stonesOnBoard;
    }

    public void setStonesOnBoard(Stone[] stonesOnBoard) {
        this.stonesOnBoard = stonesOnBoard;
    }

    public int[] getLastMove() {
        return lastMove;
    }

    public void setLastMove(int[] lastMove) {
        this.lastMove = lastMove;
    }

    public Stone getLastMoveColor() {
        return lastMoveColor;
    }

    public void setLastMoveColor(Stone lastMoveColor) {
        this.lastMoveColor = lastMoveColor;
    }

    public boolean isBlackToPlay() {
        return blackToPlay;
    }

    public void setBlackToPlay(boolean blackToPlay) {
        this.blackToPlay = blackToPlay;
    }

    public Zobrist getZobrist() {
        return zobrist;
    }

    public void setZobrist(Zobrist zobrist) {
        this.zobrist = zobrist;
    }

    public int getMoveNumber() {
        return moveNumber;
    }

    public void setMoveNumber(int moveNumber) {
        this.moveNumber = moveNumber;
    }

    public int[] getMoveNumberListOnBoard() {
        return moveNumberListOnBoard;
    }

    public void setMoveNumberListOnBoard(int[] moveNumberListOnBoard) {
        this.moveNumberListOnBoard = moveNumberListOnBoard;
    }

    public List<VariationData> getVariationDataList() {
        return variationDataList;
    }

    public void setVariationDataList(List<VariationData> variationDataList) {
        this.variationDataList = variationDataList;
    }

    public Optional<VariationData> getFirstVariation() {
        if (CollectionUtils.isEmpty(variationDataList)) {
            return Optional.empty();
        } else {
            return Optional.ofNullable(variationDataList.get(0));
        }
    }

    public double getBlackWinrate() {
        Optional<VariationData> variationData = getFirstVariation();
        double winrate = variationData.map(VariationData::getWinrate).orElse(50.0);
        if (blackToPlay) {
            return winrate;
        } else {
            return 100.0 - winrate;
        }
    }

    public double getWhiteWinrate() {
        return 100.0 - getBlackWinrate();
    }

    public MutableSet<Coordinates> getRemovedEnemyStoneIndexes() {
        return removedEnemyStoneIndexes;
    }

    public void setRemovedEnemyStoneIndexes(MutableSet<Coordinates> removedEnemyStoneIndexes) {
        this.removedEnemyStoneIndexes = removedEnemyStoneIndexes;
    }

    public int getBlackPrisonersCount() {
        return blackPrisonersCount;
    }

    public void setBlackPrisonersCount(int blackPrisonersCount) {
        this.blackPrisonersCount = blackPrisonersCount;
    }

    public int getWhitePrisonersCount() {
        return whitePrisonersCount;
    }

    public void setWhitePrisonersCount(int whitePrisonersCount) {
        this.whitePrisonersCount = whitePrisonersCount;
    }

    public int getCalculationCount() {
        Optional<VariationData> variationData = getFirstVariation();
        return variationData.map(VariationData::getPlayouts).orElse(0);
    }

    public int getTotalCalculationCount() {
        return CollectionUtils.isEmpty(variationDataList)
                ? 0
                : variationDataList.stream().mapToInt(VariationData::getPlayouts).sum();
    }

    public Optional<Boolean> isBlack() {
        switch (lastMoveColor) {
            case BLACK:
                return Optional.of(true);
            case WHITE:
                return Optional.of(false);
            default:
                return Optional.empty();
        }
    }

    public Optional<Boolean> isWhite() {
        switch (lastMoveColor) {
            case BLACK:
                return Optional.of(false);
            case WHITE:
                return Optional.of(true);
            default:
                return Optional.empty();
        }
    }

    public boolean isPass() {
        return lastMove == null || lastMove[0] < 0 || lastMove[0] >= boardSize.getLeft() || lastMove[1] < 0 || lastMove[1] >= boardSize.getRight();
    }

    public void tryUpdateVariationInfo(List<MoveData> newMoveDataList) {
        if (CollectionUtils.isEmpty(newMoveDataList)) {
            return;
        }
        // FIXME: Due to boardStateCount cannot sync with bestMoves, we may receive previous move's variations
//        if (CollectionUtils.isEmpty(variationDataList)) {
//            variationDataList = newMoveDataList.stream().map(VariationData::new).collect(Collectors.toList());
//        } else {
//            int totalCalculation = variationDataList.stream().mapToInt(VariationData::getPlayouts).sum();
//            int newTotalCalculation = newMoveDataList.stream().mapToInt(MoveData::getPlayouts).sum();
//            if (newTotalCalculation > totalCalculation) {
//                variationDataList = newMoveDataList.stream().map(VariationData::new).collect(Collectors.toList());
//            }
//        }
        variationDataList = newMoveDataList.stream().map(VariationData::new).collect(Collectors.toList());
    }

    public int coordsToIndex(int row, int col) {
        return row * boardSize.getLeft() + col;
    }

    public Stone getStoneOnBoard(int row, int col) {
        int index = coordsToIndex(row, col);
        try {
            return stonesOnBoard[index];
        } catch (ArrayIndexOutOfBoundsException e) {
            return null;
        }
    }

    public int getMoveNumberOnBoard(int row, int col) {
        int index = coordsToIndex(row, col);
        try {
            return moveNumberListOnBoard[index];
        } catch (ArrayIndexOutOfBoundsException e) {
            return 0;
        }
    }

    public Optional<int[]> getBestMove() {
        if (CollectionUtils.isEmpty(variationDataList)) {
            return Optional.empty();
        }

        VariationData bestVariation = variationDataList.get(0);
        if (CollectionUtils.isEmpty(bestVariation.getVariation())) {
            return Optional.empty();
        } else {
            return Optional.ofNullable(bestVariation.getVariation().get(0));
        }
    }
}