package tc.oc.commons.bukkit.commands;

import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;

import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.sk89q.minecraft.util.commands.CommandContext;
import com.sk89q.minecraft.util.commands.CommandException;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import tc.oc.api.bukkit.users.BukkitUserStore;
import tc.oc.api.bukkit.users.OnlinePlayers;
import tc.oc.api.docs.User;
import tc.oc.api.exceptions.NotFound;
import tc.oc.api.minecraft.MinecraftService;
import tc.oc.api.users.UserSearchRequest;
import tc.oc.api.users.UserSearchResponse;
import tc.oc.api.users.UserService;
import tc.oc.commons.bukkit.nick.IdentityProvider;
import tc.oc.minecraft.scheduler.MainThreadExecutor;
import tc.oc.commons.bukkit.users.PlayerSearchResponse;
import tc.oc.commons.core.commands.TranslatableCommandException;
import tc.oc.commons.core.util.Orderable;

@Singleton
public class UserFinder {

    public enum Default implements Orderable<Default> { NULL, THROW, SENDER }
    public enum Scope implements Orderable<Scope> { LOCAL, ONLINE, ALL }

    private final MinecraftService minecraftService;
    private final UserService userService;
    private final BukkitUserStore userStore;
    private final IdentityProvider identityProvider;
    private final MainThreadExecutor mainThreadExecutor;
    private final OnlinePlayers onlinePlayers;

    @Inject UserFinder(MinecraftService minecraftService, UserService userService, BukkitUserStore userStore, IdentityProvider identityProvider, MainThreadExecutor mainThreadExecutor, OnlinePlayers onlinePlayers) {
        this.minecraftService = minecraftService;
        this.userService = userService;
        this.userStore = userStore;
        this.identityProvider = identityProvider;
        this.mainThreadExecutor = mainThreadExecutor;
        this.onlinePlayers = onlinePlayers;
    }

    public Player senderToPlayer(CommandSender sender) throws CommandException {
        if(sender instanceof Player) {
            return (Player) sender;
        } else {
            throw new TranslatableCommandException("command.onlyPlayers");
        }
    }

    public @Nullable User getLocalUser(CommandSender sender) {
        if(sender instanceof Player) {
            return userStore.getUser((Player) sender);
        }
        return null;
    }

    public @Nullable Player getLocalPlayer(CommandSender sender, String username) {
        return sender.getServer().getPlayerExact(username, sender);
    }

    public UserSearchResponse localUserResponse(CommandSender viewer, Player player) {
        return new UserSearchResponse(
            userStore.getUser(player),
            true,
            identityProvider.currentIdentity(player).isDisguised(viewer),
            userStore.getSession(player),
            minecraftService.getLocalServer()
        );
    }

    public PlayerSearchResponse localPlayerResponse(CommandSender viewer, Player player) {
        return new PlayerSearchResponse(
            localUserResponse(viewer, player),
            player
        );
    }

    public ListenableFuture<UserSearchResponse> findUser(final CommandSender sender, @Nullable String name, Scope scope, Default def) {
        try {
            if(name != null) {
                final Player player = getLocalPlayer(sender, name);
                if(player != null) {
                    return Futures.immediateFuture(localUserResponse(sender, player));
                }

                if(scope.noGreaterThan(Scope.LOCAL)) {
                    throw new TranslatableCommandException("command.playerNotFound");
                }

                final SettableFuture<UserSearchResponse> result = SettableFuture.create();
                Futures.addCallback(
                    userService.search(new UserSearchRequest(name, getLocalUser(sender))),
                    new FutureCallback<UserSearchResponse>() {
                        @Override
                        public void onSuccess(@Nullable UserSearchResponse response) {
                            if(!response.online && scope.noGreaterThan(Scope.ONLINE)) {
                                result.setException(new TranslatableCommandException("command.playerNotFound"));
                            } else {
                                result.set(response);
                            }
                        }

                        @Override
                        public void onFailure(Throwable e) {
                            if(e instanceof NotFound) {
                                result.setException(new TranslatableCommandException("command.playerNotFound"));
                            } else {
                                result.setException(e);
                            }
                        }
                    }
                );

                return result;
            } else {
                switch(def) {
                    case NULL:
                        return Futures.immediateFuture(null);

                    case SENDER:
                        return Futures.immediateFuture(localUserResponse(sender, senderToPlayer(sender)));

                    default:
                        throw new TranslatableCommandException("command.specifyPlayer");
                }
            }
        } catch(CommandException e) {
            return Futures.immediateFailedFuture(e);
        }
    }

    public ListenableFuture<PlayerSearchResponse> findPlayer(CommandSender sender, @Nullable String name, Scope scope, Default def) {
        try {
            final Player player = getLocalPlayer(sender, name);
            if(player != null) {
                return Futures.immediateFuture(localPlayerResponse(sender, player));
            }

            if(scope.noGreaterThan(Scope.LOCAL)) {
                throw new TranslatableCommandException("command.playerNotFound");
            }

            final SettableFuture<PlayerSearchResponse> playerResult = SettableFuture.create();
            mainThreadExecutor.callback(
                findUser(sender, name, scope, def),
                new FutureCallback<UserSearchResponse>() {
                    @Override
                    public void onSuccess(@Nullable UserSearchResponse userResult) {
                        playerResult.set(new PlayerSearchResponse(userResult, onlinePlayers.find(userResult.user)));
                    }

                    @Override
                    public void onFailure(Throwable t) {
                        playerResult.setException(t);
                    }
                }
            );

            return playerResult;
        } catch(CommandException e) {
            return Futures.immediateFailedFuture(e);
        }
    }


    // findUser overloads

    public ListenableFuture<UserSearchResponse> findUser(final CommandSender sender, CommandContext args, int index, Scope scope, Default def) {
        return findUser(sender, args.getString(index, null), scope, def);
    }

    public ListenableFuture<UserSearchResponse> findUser(final CommandSender sender, CommandContext args, int index, Default def) {
        return findUser(sender, args, index, Scope.ALL, def);
    }

    public ListenableFuture<UserSearchResponse> findUser(final CommandSender sender, CommandContext args, int index, Scope scope) {
        return findUser(sender, args.getString(index, null), scope, Default.THROW);
    }

    public ListenableFuture<UserSearchResponse> findUser(final CommandSender sender, CommandContext args, int index) {
        return findUser(sender, args, index, Scope.ALL, Default.THROW);
    }


    // findPlayer overloads

    public ListenableFuture<PlayerSearchResponse> findPlayer(CommandSender sender, CommandContext args, int index, Scope scope, Default def) {
        return findPlayer(sender, args.getString(index, null), scope, def);
    }

    public ListenableFuture<PlayerSearchResponse> findPlayer(CommandSender sender, CommandContext args, int index, Scope scope) {
        return findPlayer(sender, args, index, scope, Default.THROW);
    }

    public ListenableFuture<PlayerSearchResponse> findPlayer(CommandSender sender, CommandContext args, int index, Default def) {
        return findPlayer(sender, args, index, Scope.ALL, def);
    }

    public ListenableFuture<PlayerSearchResponse> findPlayer(CommandSender sender, CommandContext args, int index) {
        return findPlayer(sender, args, index, Scope.ALL, Default.THROW);
    }


    // Special cases

    public ListenableFuture<PlayerSearchResponse> findLocalPlayer(CommandSender sender, CommandContext args, int index, Default def) {
        return findPlayer(sender, args, index, Scope.LOCAL, def);
    }

    public ListenableFuture<PlayerSearchResponse> findLocalPlayer(CommandSender sender, CommandContext args, int index) {
        return findLocalPlayer(sender, args, index, Default.THROW);
    }

    public ListenableFuture<PlayerSearchResponse> findLocalPlayerOrSender(CommandSender sender, CommandContext args, int index) {
        return findLocalPlayer(sender, args, index, Default.SENDER);
    }
}