package engine;

import com.codingame.gameengine.module.entities.GraphicEntityModule;

import java.io.Console;
import java.util.*;

import com.codingame.game.Player;

public class Board {
    private final int WIDTH = 11;
    private final int HEIGHT = 11;

    private List<Player> players;
    private Integer[][] gameBoard;
    private final int[][] moveDirections = new int[][] { { 1, 0 }, { 0, -1 }, { -1, -1 }, { -1, 0 }, { 0, 1 }, { 1, 1 } };
    private MoveLog moveLog;
    private Move lastMove;

    /*
     * Starting board:
     * 
     *     O O O O O 
     *    O O O O O O 
     *   + + O O O + + 
     *  + + + + + + + + 
     * + + + + + + + + + 
     *  + + + + + + + + 
     *   + + @ @ @ + +
     *    @ @ @ @ @ @
     *     @ @ @ @ @
     * 
     * Directions: 
     *     2 1 
     *    3 X 0 
     *     4 5
     * 
     *    2 1 
     *    3 X 0 
     *      4 5
     */

    public Board(List<Player> players, GraphicEntityModule graphicEntityModule) {
        this.players = players;

        Integer[][] transposedGameBoard = new Integer[][] { 
                { null, null, null, null, null, null, null, null, null, null, null },
                { null, 1, 1, 1, 1, 1, null, null, null, null, null },
                { null, 1, 1, 1, 1, 1, 1, null, null, null, null }, 
                { null, 0, 0, 1, 1, 1, 0, 0, null, null, null },
                { null, 0, 0, 0, 0, 0, 0, 0, 0, null, null }, 
                { null, 0, 0, 0, 0, 0, 0, 0, 0, 0, null },
                { null, null, 0, 0, 0, 0, 0, 0, 0, 0, null }, 
                { null, null, null, 0, 0, 2, 2, 2, 0, 0, null },
                { null, null, null, null, 2, 2, 2, 2, 2, 2, null },
                { null, null, null, null, null, 2, 2, 2, 2, 2, null },
                { null, null, null, null, null, null, null, null, null, null, null } };

        gameBoard = new Integer[WIDTH][HEIGHT];

        for (int x = 0; x < WIDTH; ++x) {
            for (int y = 0; y < HEIGHT; ++y) {
                gameBoard[x][y] = transposedGameBoard[y][x];
            }
        }

        moveLog = new MoveLog();
        lastMove = null;
    }

    public Board(Board board) {
        players = board.players;
        gameBoard = new Integer[WIDTH][HEIGHT];

        for (int x = 0; x < WIDTH; ++x) {
            for (int y = 0; y < HEIGHT; ++y) {
                gameBoard[x][y] = board.gameBoard[x][y];
            }
        }
    }

    private void copyGameBoard(Board board) {
        for (int x = 0; x < WIDTH; ++x) {
            for (int y = 0; y < HEIGHT; ++y) {
                gameBoard[x][y] = board.gameBoard[x][y];
            }
        }
    }

    // Rule Coords - in the rules, Map Coords - indices in the gameBoard

    static public int[] mapToRuleCoords(int x, int y) {
        --x;
        --y;

        if (y > 4) {
            x += (4 - y);
        }

        return new int[] { x, y };
    }

    static public int[] ruleToMapCoords(int x, int y) {

        if (y > 4) {
            x += y - 4;
        }

        ++x;
        ++y;

        return new int[] { x, y };
    }

    static public int[] mapToRuleCoords(int[] coords) {
        return mapToRuleCoords(coords[0], coords[1]);
    }

    static public int[] ruleToMapCoords(int[] coords) {
        return ruleToMapCoords(coords[0], coords[1]);
    }

    public boolean checkEnd() {
        for (Player player : players) {
            if (player.hasWon() || player.getScore() == -1) {
                return true;
            }
        }

        return false;
    }

    public MoveLog getMoveLog() {
        return moveLog;
    }

    public boolean applyMove(Player player, final Move move) throws Exception {
        this.lastMove = move;

        if (checkEnd()) {
            return true;
        }

        moveLog = new MoveLog();
        int playerID = player.getIndex() + 1;
        int enemyID = (playerID % 2) + 1;

        // check if selected marbles are valid
        int dx = move.lastMarble[0] - move.firstMarble[0];
        int dy = move.lastMarble[1] - move.firstMarble[1];

        boolean on_line = false;

        for (int d = 0; d < 6; ++d) {
            if ((dx == 0 && dy == 0) ||
                (dx == moveDirections[d][0] && dy == moveDirections[d][1]) || 
                (dx == (moveDirections[d][0] * 2) && dy == (moveDirections[d][1] * 2))) {
                    on_line = true;
            }   
        }
        
        if (!on_line) {
            throw new Exception("Chosen marbles must lie on a straight line!");
        }

        List <Integer> selectedMarblesX = new ArrayList<>();
        List <Integer> selectedMarblesY = new ArrayList<>();
        int tempX = move.firstMarble[0];
        int tempY = move.firstMarble[1];

        while(tempX != move.lastMarble[0] || tempY != move.lastMarble[1]) {
            
            if(gameBoard[tempX][tempY] == null || gameBoard[tempX][tempY] != playerID) {
                throw new Exception("You may choose only your own marbles!");
            }

            selectedMarblesX.add(tempX);
            selectedMarblesY.add(tempY);

            if (dx > 0) {
                tempX++;
            } else if (dx < 0) {
                tempX--;
            }

            if (dy > 0) {
                tempY++;
            } else if (dy < 0) {
                tempY--;
            }
        }

        if(gameBoard[move.lastMarble[0]][move.lastMarble[1]] == null || 
            gameBoard[move.lastMarble[0]][move.lastMarble[1]] != playerID) {
                throw new Exception("You may choose only your own marbles!");
        }

        selectedMarblesX.add(move.lastMarble[0]);
        selectedMarblesY.add(move.lastMarble[1]);

        if (selectedMarblesX.size() > 3) {
            throw new Exception("You may choose up to 3 marbles!");
        }
        // -------------------------------------------------------------

        // System.out.println(String.format("Tries to move %d balls", selectedMarblesX.size()));

        // determine if the move is arrow-like
        boolean arrowLike = false;
        int headX = -1;
        int headY = -1;

        if (selectedMarblesX.size() > 1) {
            int diffX = selectedMarblesX.get(1) - selectedMarblesX.get(0);
            int diffY = selectedMarblesY.get(1) - selectedMarblesY.get(0);

            if (diffX == moveDirections[move.direction][0] && 
                    diffY == moveDirections[move.direction][1]) {
                arrowLike = true;
                headX = selectedMarblesX.get(selectedMarblesX.size() - 1);
                headY = selectedMarblesY.get(selectedMarblesY.size() - 1);
            }

            if (diffX == moveDirections[(move.direction + 3) % 6][0] && 
                    diffY == moveDirections[(move.direction + 3) % 6][1]) {
                arrowLike = true;
                headX = selectedMarblesX.get(0);
                headY = selectedMarblesY.get(0);
            }
        }

        // -------------------------------------------------------------

        // check and apply normal move    
        boolean emptyCells = true;

        for (int i = 0; i < selectedMarblesX.size(); ++i) {
            gameBoard[selectedMarblesX.get(i)][selectedMarblesY.get(i)] = 0;
        }

        for (int i = 0; i < selectedMarblesX.size(); ++i) {
            Integer temp = gameBoard[selectedMarblesX.get(i) + moveDirections[move.direction][0]]
            [selectedMarblesY.get(i) + moveDirections[move.direction][1]];

            if (temp == null || temp != 0) {
                emptyCells = false;
            }
        }

        if (!arrowLike && !emptyCells) {

            for (int i = 0; i < selectedMarblesX.size(); ++i) {
                gameBoard[selectedMarblesX.get(i)][selectedMarblesY.get(i)] = playerID;
            }

            throw new Exception("Tried to move marble into blocked cell.");
        }

        if (emptyCells) {
            for (int i = 0; i < selectedMarblesX.size(); ++i) {
                gameBoard[selectedMarblesX.get(i) + moveDirections[move.direction][0]]
                        [selectedMarblesY.get(i) + moveDirections[move.direction][1]] = playerID;
                
                // log marble movement
                moveLog.add(new MoveLogEntry(
                    selectedMarblesX.get(i), 
                    selectedMarblesY.get(i),
                    selectedMarblesX.get(i) + moveDirections[move.direction][0],
                    selectedMarblesY.get(i) + moveDirections[move.direction][1],
                    playerID
                    )
                );
            }

            return false;
        }
        // -------------------------------------------------------------
 
        // check and apply sumito move

        // calculate enemy marbles
        List <Integer> enemyMarblesX = new ArrayList<>();
        List <Integer> enemyMarblesY = new ArrayList<>();

        tempX = headX + moveDirections[move.direction][0];
        tempY = headY + moveDirections[move.direction][1];

        while(gameBoard[tempX][tempY] != null && gameBoard[tempX][tempY] == enemyID) {
            enemyMarblesX.add(tempX);
            enemyMarblesY.add(tempY);

            tempX += moveDirections[move.direction][0];
            tempY += moveDirections[move.direction][1];
        }

        if ((gameBoard[tempX][tempY] != null && gameBoard[tempX][tempY] == playerID) || 
            enemyMarblesX.size() == 0 || 
            enemyMarblesX.size() >= selectedMarblesX.size()) {

            for (int i = 0; i < selectedMarblesX.size(); ++i) {
                gameBoard[selectedMarblesX.get(i)][selectedMarblesY.get(i)] = playerID;
            }

            throw new Exception("Tried to move marble into blocked cell.");
        }

        // push enemy marbles
        for (int i = 0; i < enemyMarblesX.size(); ++i) {
            gameBoard[enemyMarblesX.get(i)][enemyMarblesY.get(i)] = 0;
        }

        for (int i = 0; i < enemyMarblesX.size(); ++i) {

            if (gameBoard[enemyMarblesX.get(i) + moveDirections[move.direction][0]]
                    [enemyMarblesY.get(i) + moveDirections[move.direction][1]] == null) {
                player.setScore(player.getScore() + 1);
            } else {
                gameBoard[enemyMarblesX.get(i) + moveDirections[move.direction][0]]
                        [enemyMarblesY.get(i) + moveDirections[move.direction][1]] = enemyID;
            }
            // log marble movement
            moveLog.add(new MoveLogEntry(
                enemyMarblesX.get(i), 
                enemyMarblesY.get(i),
                enemyMarblesX.get(i) + moveDirections[move.direction][0],
                enemyMarblesY.get(i) + moveDirections[move.direction][1],
                enemyID
                )
            );
        }

        // place player's marbles
        for (int i = 0; i < selectedMarblesX.size(); ++i) {
            gameBoard[selectedMarblesX.get(i) + moveDirections[move.direction][0]]
                    [selectedMarblesY.get(i) + moveDirections[move.direction][1]] = playerID;
            
            // log marble movement
            moveLog.add(new MoveLogEntry(
                selectedMarblesX.get(i), 
                selectedMarblesY.get(i),
                selectedMarblesX.get(i) + moveDirections[move.direction][0],
                selectedMarblesY.get(i) + moveDirections[move.direction][1],
                playerID
                )
            );
        }

        return checkEnd();
    }

    private List<Move> getLegalMoves(Player player) {
        List<Move> legalMoves = new ArrayList<Move>();
        Board temp = new Board(this);

        for (int x1 = 1; x1 < 10; ++x1) {
            for (int y1 = 1; y1 < 10; ++y1) {
                for (int x2 = x1; x2 < Math.min(x1 + 3, 10); ++x2)  {
                    for (int y2 = y1; y2 <  Math.min(y1 + 3, 10); ++y2) {
                        for (int d = 0; d < 6; ++d) {
                            if (gameBoard[x1][y1] == null || gameBoard[x2][y2] == null) {
                                continue;
                            }

                            if (x2 < x1 || (x2 == x1 && y2 < y1)) {
                                continue;
                            }

                            int oldScore = player.getScore();
                            int[] ruleCoordsFirst = mapToRuleCoords(x1, y1);
                            int[] ruleCoordsLast = mapToRuleCoords(x2, y2);
                            Move move = new Move(ruleCoordsFirst[0], ruleCoordsFirst[1], ruleCoordsLast[0], ruleCoordsLast[1], d);

                            temp.copyGameBoard(this);

                            try {
                                temp.applyMove(player, move);
                                // System.out.println("good move");
                                legalMoves.add(move);
                            } catch (Exception e) {
                                // System.out.println(String.format("%s %d %d %d %d %d", e.getMessage(), ruleCoordsFirst[0], ruleCoordsFirst[1], ruleCoordsLast[0], ruleCoordsLast[1], d));
                            }

                            player.setScore(oldScore);
                        }
                    }
                }
            }
        }
        
        return legalMoves;
    }

    public int getWidth() {
        return WIDTH;
    }

    public int getHeight() {
        return HEIGHT;
    }

    public Integer getBoardValue(int[] coords) {
        return getBoardValue(coords[0], coords[1]);
    }

    public Integer getBoardValue(int x, int y) {
        return gameBoard[x][y];
    }

    private void sentBoardInput(Player player) {
        for (int y = 1; y < HEIGHT - 1; ++y) {
            StringBuilder rowString = new StringBuilder("");

            for (int x = 1; x < WIDTH - 1; ++x) {
                if (gameBoard[x][y] != null) {
                    rowString.append(gameBoard[x][y].toString());
                }
            }

            player.sendInputLine(rowString.toString());
        }
    }

    /*
     * First turn: {Player ID}
     * 
     * Every turn: {Player points} {Enemy points} 
     * {Strings representation of the board}
     * {Enemy move}
     */
    public void sendInputToPlayer(Player player, Player enemy, boolean firstTurn) {
        if (firstTurn) {
            player.sendInputLine(String.format("%d", player.getIndex() + 1));
        }

        player.sendInputLine(String.format("%d %d", player.getScore(), enemy.getScore()));
        sentBoardInput(player);
        if (lastMove == null) {
            player.sendInputLine("-1 -1 -1 -1 -1");
        } else {

            int[] firstMarble = mapToRuleCoords(lastMove.firstMarble);
            int[] lastMarble = mapToRuleCoords(lastMove.lastMarble);

            player.sendInputLine(
                String.format("%d %d %d %d %d", 
                firstMarble[0], 
                firstMarble[1], 
                lastMarble[0], 
                lastMarble[1], 
                lastMove.direction
                )
            );
        }

        List<Move> legalMoves = getLegalMoves(player);
        player.sendInputLine(String.format("%d", legalMoves.size()));
        
        for (Move move : legalMoves) {
            int[] firstMarble = mapToRuleCoords(move.firstMarble);
            int[] lastMarble = mapToRuleCoords(move.lastMarble);

            player.sendInputLine(
                String.format("%d %d %d %d %d", 
                firstMarble[0], 
                firstMarble[1], 
                lastMarble[0], 
                lastMarble[1], 
                move.direction
                )
            );
        }
    }

    public List<Player> getPlayers() {
        return players;
    }
}
