package com.mediamonks.googleflip.pages.game.management;

import android.util.Log;

import com.mediamonks.googleflip.GoogleFlipGameApplication;
import com.mediamonks.googleflip.data.constants.LevelColor;
import com.mediamonks.googleflip.data.constants.LevelDifficulty;
import com.mediamonks.googleflip.data.constants.PlayerState;
import com.mediamonks.googleflip.data.vo.ClientVO;
import com.mediamonks.googleflip.data.vo.LevelResultVO;
import com.mediamonks.googleflip.data.vo.LevelVO;
import com.mediamonks.googleflip.data.vo.PlayerScoreVO;
import com.mediamonks.googleflip.pages.game.management.gamemessages.GameMessage;
import com.mediamonks.googleflip.pages.game.management.gamemessages.GameMessageConverter;
import com.mediamonks.googleflip.pages.game.management.gamemessages.c2s.C2SClientNameMessage;
import com.mediamonks.googleflip.pages.game.management.gamemessages.c2s.C2SRoundFinishedMessage;
import com.mediamonks.googleflip.pages.game.management.gamemessages.s2c.S2CClientAckMessage;
import com.mediamonks.googleflip.pages.game.management.gamemessages.s2c.S2CClientsScoreChangedMessage;
import com.mediamonks.googleflip.pages.game.management.gamemessages.s2c.S2CConnectedClientsChangedMessage;
import com.mediamonks.googleflip.pages.game.management.gamemessages.s2c.S2CGameFinishedMessage;
import com.mediamonks.googleflip.pages.game.management.gamemessages.s2c.S2CRoundFinishedMessage;
import com.mediamonks.googleflip.pages.game.management.gamemessages.s2c.S2CRoundStartedMessage;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import temple.multiplayer.net.common.connection.Connection;

/**
 * Implementation of the GameServer interface
 */
public class GameServerImpl implements GameServer {
    private static final String TAG = GameServerImpl.class.getSimpleName();

    private static final int MAX_PLAYER_COUNT = 4;

    private static int sCurrentId = 0;

    private final List<Player> _players = new ArrayList<>();
    private boolean _debug;
    private List<Long> _rounds;
    private int _roundIndex;
    private List<LevelColor> _backgroundColors = new ArrayList<>();
    private HashMap<Integer, List<LevelVO>> _levelDifficultyMap;

    public GameServerImpl() {
    }

    public void initBackgroundColors() {
        LevelColor[] colors = {LevelColor.BLUE, LevelColor.CYAN, LevelColor.PINK, LevelColor.PURPLE};
        List<LevelColor> colorList = Arrays.asList(colors);
        Collections.shuffle(colorList);

        _backgroundColors = new LinkedList<>();
        _backgroundColors.addAll(colorList);
    }

    @Override
    public boolean hasRoomForMorePlayers() {
        return _players.size() < MAX_PLAYER_COUNT;
    }

    @Override
    public void addPlayer(final Player player) {
        if (_debug) Log.d(TAG, "addPlayer: ");

        // initialize player with unique ID & background color
        player.setPlayerId(getUniqueId());
        player.setPlayerLevelColor(getLevelColor());

        player.getConnection().setMessageHandler(
                new Connection.MessageHandler() {
                    @Override
                    public void onMessageReceived(String message) {
                        GameServerImpl.this.onMessageReceived(player, message);
                    }
                }
        );
        player.getConnection().setConnectionHandler(
                new Connection.ConnectionHandler() {
                    @Override
                    public void onConnectionLost() {
                        removePlayer(player);
                    }
                }
        );

        _players.add(player);

        // send this now, even though the client may not be ready to receive it
        sendClientAck(player);
    }

    private void sendClientAck(Player player) {
        ClientVO clientVO = player.getClientVO();
        player.getConnection().writeMessage(GameMessageConverter.writeMessage(new S2CClientAckMessage(clientVO.id, clientVO.levelColor, clientVO.name)));
    }

    private LevelColor getLevelColor() {
        if (_backgroundColors.size() == 0) {
            initBackgroundColors();
        }

        return _backgroundColors.remove(0);
    }

    private void onConnectedClientsChanged() {
        List<ClientVO> clients = new ArrayList<>();
        for (Player player : _players) {
            clients.add(player.getClientVO());
        }

        broadcastMessage(new S2CConnectedClientsChangedMessage(clients));
    }

    @Override
    public void removePlayer(Player player) {
        if (_debug) Log.d(TAG, "removePlayer: player = " + player);

        _backgroundColors.add(player.getClientVO().levelColor);

        disconnectPlayer(player);

        _players.remove(player);

        onConnectedClientsChanged();
    }

    private void disconnectPlayer(Player player) {
        player.getConnection().setMessageHandler(null);
        player.getConnection().setConnectionHandler(null);
        player.getConnection().disconnect();
    }

    @Override
    public void removePlayer(String deviceAddress) {
        if (_debug) Log.d(TAG, "removePlayer: deviceAddress = " + deviceAddress);

        for (Player player : _players) {
            if (player.getConnection().getDeviceAddress().equals(deviceAddress)) {
                removePlayer(player);

                return;
            }
        }
    }

    private void onMessageReceived(Player player, String message) {
        GameMessage gameMessage = GameMessageConverter.readMessage(message);
        if (gameMessage == null) {
            Log.e(TAG, "onMessageReceived: couldn't convert message " + message);
            return;
        }

        switch (gameMessage.getType()) {
            case C2S_CLIENT_NAME:
                onClientNameMessageReceived(player, (C2SClientNameMessage) gameMessage);
                break;
            case C2S_ROUND_FINISHED:
                onRoundFinishedMessageReceived(player, (C2SRoundFinishedMessage) gameMessage);
                break;
            default:
                if (_debug) Log.d(TAG, "onMessageReceived: unhandled message type " + gameMessage.getType());
                break;
        }
    }

    private void onRoundFinishedMessageReceived(Player player, C2SRoundFinishedMessage message) {
        player.addLevelResult(message.levelResultVO);

        player.setPlayerState(PlayerState.FINISHED);

        updateScores();

        checkRoundFinished();
    }

    /**
     * Check if all players are done with the current round
     */
    private void checkRoundFinished() {
        for (Player player : _players) {
            if (player.getClientVO().playerState.equals(PlayerState.PLAYING)) {
                return;
            }
        }

        _roundIndex++;

        if (_debug) Log.d(TAG, "checkRoundFinished: " + _roundIndex);

        // check if round is finished, or whole game is finished
        if (_roundIndex < _rounds.size()) {
            broadcastMessage(new S2CRoundFinishedMessage());
        } else {
            broadcastMessage(new S2CGameFinishedMessage(getWinnerId()));
        }
    }

    /**
     * Determine the ID of the winner
     */
    private int getWinnerId() {
        int id = 0;
        float minTime = Float.MAX_VALUE;

        for (Player player : _players) {
            float totalTime = 0;
            for (LevelResultVO resultVO : player.getLevelResults()) {
                totalTime += resultVO.seconds;
            }

            if (totalTime < minTime) {
                minTime = totalTime;
                id = player.getClientVO().id;
            }
        }

        return id;
    }

    private void clearScores() {
        for (Player player : _players) {
            player.clearLevelResults();
        }
    }

    private void updateScores() {
        List<PlayerScoreVO> playerScores = new ArrayList<>();

        for (Player player : _players) {
            ClientVO clientVO = player.getClientVO();
            PlayerScoreVO scoreVO = new PlayerScoreVO(clientVO, _roundIndex);

            float totalTime = 0;
            for (LevelResultVO resultVO : player.getLevelResults()) {
                scoreVO.roundScores.add(resultVO.seconds);

                totalTime += resultVO.seconds;
            }
            scoreVO.totalTime = totalTime;

            scoreVO.isPlaying = (player.getClientVO().playerState.equals(PlayerState.PLAYING));

            playerScores.add(scoreVO);
        }

        // update order
        Collections.sort(playerScores);
        for (int i = 0; i < playerScores.size(); i++) {
            playerScores.get(i).order = i + 1;
        }

        broadcastMessage(new S2CClientsScoreChangedMessage(playerScores));
    }

    private void onClientNameMessageReceived(Player player, C2SClientNameMessage message) {
        String playerName = player.getClientVO().name;
        if (playerName != null && playerName.equals(message.name) && player.getClientVO().id == message.id) {
            // onClientNameMessageReceived: player already known
        } else {
            player.setPlayerName(message.name);

            sendClientAck(player);

            onConnectedClientsChanged();
        }
    }

    private static int getUniqueId() {
        return sCurrentId++;
    }

    public void setDebug(boolean debug) {
        _debug = debug;
    }

    @Override
    public void setLevels(List<LevelVO> levels) {
        // create map with shuffled lists of levels per difficulty
        _levelDifficultyMap = new HashMap<>();
        for (int difficulty : LevelDifficulty.DIFFICULTIES) {
            _levelDifficultyMap.put(difficulty, new ArrayList<LevelVO>());
        }
        for (LevelVO levelVO : levels) {
            _levelDifficultyMap.get(levelVO.difficulty).add(levelVO);
        }
        for (int difficulty : LevelDifficulty.DIFFICULTIES) {
            Collections.shuffle(_levelDifficultyMap.get(difficulty));
        }
    }

    @Override
    public void stop() {
        while (_players.size() > 0) {
            Player player = _players.remove(0);

            disconnectPlayer(player);
        }
    }

    @Override
    public void startGame() {
        clearScores();

        initRounds();

        _roundIndex = 0;

        startRound();
    }

    private void initRounds() {
        _rounds = new ArrayList<>();

        for (int difficulty : LevelDifficulty.DIFFICULTIES) {
            List<LevelVO> levels = _levelDifficultyMap.get(difficulty);
            int index = (int)(Math.floor(levels.size() * Math.random()));
            _rounds.add(levels.get(index).id);
        }

        Log.d(TAG, "initRounds: " + _rounds);
    }

    public void startRound() {
        Long currentLevel = _rounds.get(_roundIndex);

        for (Player player : _players) {
            player.setPlayerState(PlayerState.PLAYING);
        }

        GoogleFlipGameApplication.getUserModel().selectLevelById(currentLevel);
        broadcastMessage(new S2CRoundStartedMessage(currentLevel, _roundIndex));
    }

    private void broadcastMessage(GameMessage gameMessage) {
        String message = GameMessageConverter.writeMessage(gameMessage);

        for (Player player : _players) {
            player.getConnection().writeMessage(message);
        }
    }
}
