package tc.oc.commons.bukkit.teleport;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.inject.Inject;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.inject.Singleton;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.entity.Player;
import tc.oc.api.docs.Arena;
import tc.oc.api.docs.Game;
import tc.oc.api.docs.Server;
import tc.oc.api.docs.virtual.ServerDoc;
import tc.oc.api.games.ArenaStore;
import tc.oc.api.games.GameStore;
import tc.oc.api.model.ModelDispatcher;
import tc.oc.api.model.ModelListener;
import tc.oc.api.servers.ServerStore;
import tc.oc.commons.bukkit.format.GameFormatter;
import tc.oc.commons.bukkit.ticket.TicketBooth;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.commons.core.util.CacheUtils;
import tc.oc.commons.core.util.Utils;
@Singleton
public class Navigator implements PluginFacet, ModelListener {
private static final char SERVER_SIGIL = '@';
private static final char FAMILY_SIGIL = '.';
private static final char GAME_SIGIL = '!';
private static final char SPECIAL_SIGIL = '$';
private final GameStore games;
private final ArenaStore arenas;
private final ServerStore servers;
private final Teleporter teleporter;
private final TicketBooth ticketBooth;
private final FeaturedServerTracker featuredServerTracker;
private final EmptyConnector emptyConnector = new EmptyConnector();
private final DefaultConnector defaultConnector = new DefaultConnector();
private final LoadingCache<String, SingleServerConnector> serverConnectors = CacheUtils.newCache(SingleServerConnector::new);
private final LoadingCache<String, FeaturedServerConnector> familyConnectors = CacheUtils.newCache(FeaturedServerConnector::new);
private final LoadingCache<String, GameConnector> gameConnectors = CacheUtils.newCache(GameConnector::new);
@Inject Navigator(GameStore games, ArenaStore arenas, ServerStore servers, Teleporter teleporter, TicketBooth ticketBooth, ModelDispatcher modelDispatcher, FeaturedServerTracker featuredServerTracker) {
this.games = games;
this.arenas = arenas;
this.servers = servers;
this.teleporter = teleporter;
this.ticketBooth = ticketBooth;
this.featuredServerTracker = featuredServerTracker;
modelDispatcher.subscribe(this);
}
private String localDatacenter() {
return featuredServerTracker.localDatacenter();
}
public @Nullable Connector parseConnector(Collection<String> tokens) {
final List<Connector> connectors = tokens.stream()
.map(this::parseConnector)
.filter(c -> c != null)
.collect(Collectors.toList());
return connectors.isEmpty() ? null : new MultiConnector(connectors);
}
public @Nullable Connector parseConnector(String token) {
if(token.length() == 0) return null;
final String name = token.substring(1);
switch(token.charAt(0)) {
case SERVER_SIGIL: return serverConnectors.getUnchecked(name);
case FAMILY_SIGIL: return familyConnectors.getUnchecked(name);
case GAME_SIGIL: return gameConnectors.getUnchecked(name);
case SPECIAL_SIGIL:
switch(name) {
case "default": return defaultConnector;
}
break;
}
return null;
}
public Connector combineConnectors(List<Connector> connectors) {
switch(connectors.size()) {
case 0: return emptyConnector;
case 1: return connectors.get(0);
default: return new MultiConnector(connectors);
}
}
@HandleModel
public void serverUpdated(@Nullable Server before, @Nullable Server after, Server latest) {
if(latest.bungee_name() != null) {
final SingleServerConnector serverConnector = serverConnectors.getIfPresent(latest.bungee_name());
if(serverConnector != null) serverConnector.refresh();
}
if(latest.family() != null) {
final FeaturedServerConnector featuredServerConnector = familyConnectors.getIfPresent(latest.family());
if(featuredServerConnector != null) featuredServerConnector.refresh();
}
}
@HandleModel
public void arenaUpdated(@Nullable Arena before, @Nullable Arena after, Arena latest) {
final GameConnector gameConnector = gameConnectors.getIfPresent(latest.game_id());
if(gameConnector != null) gameConnector.refresh();
}
@HandleModel
public void gameUpdated(@Nullable Game before, @Nullable Game after, Game latest) {
final GameConnector gameConnector = gameConnectors.getIfPresent(latest._id());
if(gameConnector != null) gameConnector.refresh();
}
public static final Object DEFAULT_MAPPING = new Object();
public abstract class Connector {
public void startObserving(Consumer<Connector> observer) {}
public void stopObserving(Consumer<Connector> observer) {}
public void release() {}
public abstract @Nullable Object mappedTo();
public boolean isVisible() { return true; }
public boolean isConnectable() { return true; }
public int priority() { return 0; }
public @Nullable BaseComponent description() { return null; }
public abstract void teleport(Player player);
}
public class EmptyConnector extends Connector {
@Override
public Object mappedTo() { return null; }
@Override
public void teleport(Player player) {}
}
public class DefaultConnector extends Connector {
@Override
public String toString() {
return getClass().getSimpleName() + "{}";
}
@Override
public Object mappedTo() {
return DEFAULT_MAPPING;
}
@Override
public void teleport(Player player) {
if(ticketBooth.currentGame(player) != null) {
ticketBooth.leaveGame(player, true);
} else {
teleporter.sendToLobby(player, false);
}
}
}
public abstract class DynamicConnector extends Connector {
private final Set<Consumer<Connector>> observers = new HashSet<>();
@Override
public void startObserving(Consumer<Connector> observer) {
observers.add(observer);
}
@Override
public void stopObserving(Consumer<Connector> observer) {
observers.remove(observer);
}
protected void notifyObservers() {
observers.forEach(o -> o.accept(this));
}
}
public abstract class ServerConnector extends DynamicConnector {
@Nullable protected Server server;
@Override
public void teleport(Player player) {
teleporter.remoteTeleport(player, server);
}
@Override
public Object mappedTo() {
return server;
}
@Override
public BaseComponent description() {
return server != null && server.description() != null ? new TranslatableComponent(server.description())
: super.description();
}
@Override
public boolean isVisible() {
return server != null &&
server.datacenter().equals(localDatacenter()) &&
server.visibility() == ServerDoc.Visibility.PUBLIC &&
server.running();
}
@Override
public boolean isConnectable() {
return server != null &&
server.datacenter().equals(localDatacenter()) &&
server.visibility() != ServerDoc.Visibility.PRIVATE &&
server.online();
}
}
public class SingleServerConnector extends ServerConnector {
private final String bungeeName;
public SingleServerConnector(String bungeeName) {
this.bungeeName = bungeeName;
refresh();
}
@Override
public String toString() {
return getClass().getSimpleName() + "{server=" + bungeeName + "}";
}
protected void refresh() {
server = servers.tryBungeeName(bungeeName);
notifyObservers();
}
}
public class FeaturedServerConnector extends ServerConnector {
private final String familyId;
public FeaturedServerConnector(String familyId) {
this.familyId = familyId;
refresh();
}
@Override
public String toString() {
return getClass().getSimpleName() + "{family=" + familyId + "}";
}
protected void refresh() {
server = featuredServerTracker.featuredServerForFamily(familyId);
notifyObservers();
}
}
public class GameConnector extends DynamicConnector {
private final String gameId;
private @Nullable Game game;
private @Nullable Arena arena;
public GameConnector(String gameId) {
this.gameId = gameId;
refresh();
}
@Override
public String toString() {
return getClass().getSimpleName() + "{game=" + gameId + "}";
}
@Override
public Object mappedTo() {
return arena;
}
@Override
public BaseComponent description() {
return game != null ? new TranslatableComponent(GameFormatter.descriptionKey(game))
: super.description();
}
@Override
public boolean isVisible() {
return arena != null &&
game.visibility() == ServerDoc.Visibility.PUBLIC;
}
@Override
public boolean isConnectable() {
return arena != null &&
game.visibility() != ServerDoc.Visibility.PRIVATE;
}
public void refresh() {
arena = arenas.tryDatacenterAndGameId(localDatacenter(), gameId);
game = arena == null ? null : games.byId(arena.game_id());
notifyObservers();
}
@Override
public void teleport(Player player) {
ticketBooth.playGame(player, arena);
}
}
private class MultiConnector extends DynamicConnector {
private final ImmutableList<Connector> connectors;
private final Consumer<Connector> observer = this::refresh;
private @Nullable Connector mapped;
private MultiConnector(List<Connector> connectors) {
this.connectors = ImmutableList.copyOf(connectors);
for(Connector connector : this.connectors) {
connector.startObserving(observer);
}
refresh(null);
}
@Override
public String toString() {
return getClass().getSimpleName() +
"{connectors=[" +
connectors.stream()
.map(Object::toString)
.collect(Collectors.joining(", ")) +
"]}";
}
@Override
public int hashCode() {
return Objects.hash(connectors);
}
@Override
public boolean equals(Object obj) {
return Utils.equals(MultiConnector.class, this, obj, that -> this.connectors.equals(that.connectors));
}
@Override
public Object mappedTo() {
return mapped == null ? null : mapped.mappedTo();
}
@Override
public BaseComponent description() {
return mapped == null ? null : mapped.description();
}
@Override
public boolean isVisible() {
return mapped != null && mapped.isVisible();
}
@Override
public boolean isConnectable() {
return mapped != null && mapped.isConnectable();
}
@Override
public void release() {
connectors.forEach(c -> c.stopObserving(observer));
}
@Override
public void teleport(Player player) {
if(mapped != null && mapped.isConnectable()) {
mapped.teleport(player);
}
}
private @Nullable Connector choose() {
return connectors.stream()
.filter(Connector::isVisible)
.findFirst()
.orElse(null);
}
private void refresh(@Nullable Connector changed) {
final Connector mapped = choose();
if(!Objects.equals(this.mapped, mapped)) {
this.mapped = mapped;
notifyObservers();
} else if(changed != null && changed.equals(this.mapped)) {
notifyObservers();
}
}
}
}