package tc.oc.commons.bukkit.ticket;

import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;

import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import tc.oc.api.bukkit.users.BukkitUserStore;
import tc.oc.api.docs.Arena;
import tc.oc.api.docs.Game;
import tc.oc.api.docs.PlayerId;
import tc.oc.api.docs.Server;
import tc.oc.api.docs.Ticket;
import tc.oc.api.docs.virtual.MatchDoc;
import tc.oc.api.docs.virtual.ServerDoc;
import tc.oc.api.games.ArenaStore;
import tc.oc.api.games.GameStore;
import tc.oc.api.games.TicketService;
import tc.oc.api.games.TicketStore;
import tc.oc.api.message.types.PlayGameRequest;
import tc.oc.api.message.types.Reply;
import tc.oc.minecraft.scheduler.SyncExecutor;
import tc.oc.api.servers.ServerStore;
import tc.oc.commons.bukkit.chat.Audiences;
import tc.oc.commons.bukkit.chat.WarningComponent;
import tc.oc.commons.bukkit.format.GameFormatter;
import tc.oc.commons.bukkit.teleport.PlayerServerChanger;
import tc.oc.commons.bukkit.util.PlayerStates;
import tc.oc.commons.core.chat.Audience;
import tc.oc.commons.core.commands.CommandFutureCallback;

/**
 * User actions for querying, joining, and leaving {@link Game}s
 */
@Singleton
public class TicketBooth {

    private final SyncExecutor syncExecutor;
    private final BukkitUserStore userStore;
    private final PlayerStates playerStates;
    private final Audiences audiences;
    private final TicketService ticketService;
    private final GameFormatter gameFormatter;
    private final GameStore games;
    private final ArenaStore arenas;
    private final TicketStore tickets;
    private final ServerStore servers;
    private final PlayerServerChanger serverChanger;
    private final Server localServer;

    private @Nullable PlayHandler playHandler;

    @Inject TicketBooth(SyncExecutor syncExecutor,
                        BukkitUserStore userStore,
                        PlayerStates playerStates,
                        Audiences audiences,
                        TicketService ticketService,
                        GameFormatter gameFormatter,
                        GameStore games,
                        ArenaStore arenas,
                        TicketStore tickets,
                        ServerStore servers,
                        PlayerServerChanger serverChanger,
                        Server localServer) {

        this.syncExecutor = syncExecutor;
        this.userStore = userStore;
        this.playerStates = playerStates;
        this.audiences = audiences;
        this.ticketService = ticketService;
        this.gameFormatter = gameFormatter;
        this.games = games;
        this.arenas = arenas;
        this.tickets = tickets;
        this.servers = servers;
        this.serverChanger = serverChanger;
        this.localServer = localServer;
    }

    @FunctionalInterface
    public interface PlayHandler {
        boolean requestPlay(Player player);
    }

    public PlayHandler playHandler() {
        return playHandler;
    }

    public void setPlayHandler(PlayHandler handler) {
        playHandler = handler;
    }

    public void removePlayHandler(PlayHandler handler) {
        if(handler.equals(playHandler)) {
            playHandler = null;
        }
    }

    public Set<Game> allGames(CommandSender viewer) {
        return Sets.filter(games.set(), game -> game.visibility() == ServerDoc.Visibility.PUBLIC);
    }

    public void showGames(CommandSender sender) {
        gameFormatter.sendList(audiences.get(sender), allGames(sender));
    }

    public @Nullable Arena localArena() {
        final String id = localServer.arena_id();
        return id == null ? null : arenas.byId(id);
    }

    public @Nullable Game localGame() {
        final String id = localServer.game_id();
        return id == null ? null : games.byId(id);
    }

    public @Nullable Arena currentArena(Player player) {
        return currentArena(userStore.playerId(player));
    }

    public @Nullable Game currentGame(Player player) {
        return currentGame(userStore.playerId(player));
    }

    public @Nullable Arena currentArena(PlayerId playerId) {
        final Ticket ticket = tickets.tryUser(playerId);
        return ticket == null ? null : arenas.byId(ticket.arena_id());
    }

    public @Nullable Game currentGame(PlayerId playerId) {
        final Arena arena = currentArena(playerId);
        return arena == null ? null : games.byId(arena.game_id());
    }

    public @Nullable Game findGame(CommandSender sender, String name) {
        name = name.trim().toLowerCase();
        for(Game game : games.set()) {
            if(game.visibility() != ServerDoc.Visibility.PRIVATE &&
               name.equals(game.name().toLowerCase())) {
                return game;
            }
        }
        audiences.get(sender).sendMessage(new WarningComponent("game.unknown", name));
        showGames(sender);
        return null;
    }

    public @Nullable Arena findArena(CommandSender sender, @Nullable String name) {
        final Arena arena;
        if(name == null || name.length() == 0) {
            arena = localArena();
            if(arena == null) {
                showGames(sender);
            }
        } else {
            final Game game = findGame(sender, name);
            if(game == null) return null;

            arena = arenas.tryDatacenterAndGameId(localServer.datacenter(), game._id());
            if(arena == null) {
                audiences.get(sender).sendMessage(new WarningComponent("game.offline",  gameFormatter.name(game)));
            }
        }
        return arena;
    }

    private ListenableFuture<Reply> sendPlayRequest(Player player, @Nullable Arena arena) {
        return sendPlayRequest(userStore.playerId(player), arena);
    }

    private ListenableFuture<Reply> sendPlayRequest(PlayerId playerId, @Nullable Arena arena) {
        return sendPlayRequest(playerId, arena, false);
    }

    private ListenableFuture<Reply> sendPlayRequest(PlayerId playerId, @Nullable Arena arena, boolean force) {
        final Arena playing = currentArena(playerId);
        if(!force && Objects.equals(playing, arena)) {
            return Futures.immediateFuture(Reply.SUCCESS);
        } else {
            return ticketService.requestPlay(new PlayGameRequest() {
                @Override public String user_id() { return playerId._id(); }
                @Override public @Nullable String arena_id() { return arena == null ? null : arena._id(); }
            });
        }
    }

    public void leaveGame(Player player, boolean returnToLobby) {
        final Audience audience = audiences.get(player);

        final PlayerId playerId = userStore.playerId(player);
        final Game game = currentGame(playerId);
        if(game == null) {
            audience.sendMessage(gameFormatter.notPlaying());
            return;
        }

        syncExecutor.callback(
            sendPlayRequest(playerId, null),
            CommandFutureCallback.onSuccess(player, reply -> {
                audience.sendMessage(gameFormatter.left(game));
                if(returnToLobby) {
                    serverChanger.sendPlayerToLobby(player, true);
                }
            })
        );
    }

    public void playLocalGame(Player player) {
        final Game game = localGame();
        if(game != null) {
            final Arena arena = arenas.tryDatacenterAndGameId(localServer.datacenter(), game._id());
            if(arena != null) {
                playGame(player, arena);
            }
        } else if(playHandler != null) {
            playHandler.requestPlay(player);
        } else {
            showGames(player);
        }
    }

    public void playGame(Player player, @Nullable String name) {
        final Arena arena = findArena(player, name);
        if(arena != null) {
            playGame(player, arena);
        }
    }

    public void playGame(Player player, Arena arena) {
        final PlayerId playerId = userStore.playerId(player);
        boolean forceRequest = false;
        if(arena.equals(currentArena(playerId))) {
            final Game game = games.byId(arena.game_id());
            final Audience audience = audiences.get(player);
            final MatchDoc match = localServer.current_match();
            if(match != null && match.join_mid_match()) {
                audience.sendMessage(gameFormatter.alreadyPlaying(game));
                return;
            } else {
                audience.sendMessage(gameFormatter.replay(game));
                forceRequest = playerStates.isObserving(player);
            }
        }
        sendPlayRequest(playerId, arena, forceRequest);
    }

    public void watchGame(Player player, @Nullable String name) {
        final Arena arena = findArena(player, name);
        if(arena != null) {
            watchGame(player, arena);
        }
    }

    public void watchGame(Player player, Arena arena) {
        final Audience audience = audiences.get(player);
        final Game game = games.byId(arena.game_id());
        final Optional<Server> fullest = servers.byArena(arena)
                                                .stream()
                                                .filter(Server::online)
                                                .max(Comparator.comparing(Server::num_participating));
        if(!fullest.isPresent()) {
            audience.sendWarning(new TranslatableComponent("game.offline", gameFormatter.name(game)), false);
            return;
        } else if(fullest.get().num_participating() < 2) {
            audience.sendWarning(new TranslatableComponent("game.empty", gameFormatter.name(game)), false);
            return;
        }

        syncExecutor.callback(
            sendPlayRequest(player, null),
            reply -> {
                serverChanger.sendPlayerToServer(player, fullest.get(), false);
            }
        );
    }
}
