package tc.oc.commons.bukkit.respack;

import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;

import com.google.common.eventbus.Subscribe;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerResourcePackStatusEvent;
import org.bukkit.plugin.Plugin;
import tc.oc.api.bukkit.users.Users;
import tc.oc.api.docs.virtual.UserDoc;
import tc.oc.api.minecraft.MinecraftService;
import tc.oc.api.minecraft.servers.LocalServerReconfigureEvent;
import tc.oc.api.users.UserService;
import tc.oc.commons.bukkit.util.OnlinePlayerMapAdapter;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.plugin.PluginFacet;

@Singleton
public class ResourcePackListener implements ResourcePackManager, Listener, PluginFacet {

    // Minimum time a player must be connected before sending them a res pack.
    // If we send it too soon, the client behaves badly.
    private static final Duration JOIN_DELAY = Duration.ofSeconds(1);

    private final Logger logger;
    private final Plugin plugin;
    private final MinecraftService minecraftService;
    private final UserService userService;

    private boolean enabled = true;
    private final OnlinePlayerMapAdapter<String> lastSentSha1;
    private final OnlinePlayerMapAdapter<Instant> joinTime;

    @Inject ResourcePackListener(Loggers loggers, Plugin plugin, MinecraftService minecraftService, UserService userService, OnlinePlayerMapAdapter<String> lastSentSha1, OnlinePlayerMapAdapter<Instant> joinTime) {
        this.logger = loggers.get(getClass());
        this.userService = userService;
        this.lastSentSha1 = lastSentSha1;
        this.joinTime = joinTime;
        this.minecraftService = minecraftService;
        this.plugin = plugin;
    }

    @Override
    public void enable() {
        lastSentSha1.enable();
        joinTime.enable();
    }

    @Override
    public void disable() {
        joinTime.disable();
        lastSentSha1.disable();
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    @Override
    public void setEnabled(boolean enabled) {
        if(this.enabled != enabled) {
            this.enabled = enabled;
            if(enabled && isFastUpdate()) {
                refreshAll();
            }
        }
    }

    @Override
    public boolean isFastUpdate() {
        return minecraftService.getLocalServer().resource_pack_fast_update();
    }

    @Override
    public @Nullable String getUrl() {
        return minecraftService.getLocalServer().resource_pack_url();
    }

    @Override
    public @Nullable String getSha1() {
        return minecraftService.getLocalServer().resource_pack_sha1();
    }

    @Override
    public void refreshPlayer(final Player player) {
        if(!enabled) return;
        if(!player.isOnline()) return;

        String url = getUrl();
        String sha1 = getSha1();
        if(url == null || sha1 == null) return;

        if(!Objects.equals(lastSentSha1.get(player), sha1)) {
            Instant joined = joinTime.get(player);
            if(joined == null) return;
            long delayMillis = Duration.between(Instant.now(), joined.plus(JOIN_DELAY)).toMillis();

            if(delayMillis <= 0) {
                logger.fine("Sending resource pack " + url + " with SHA1 " + sha1 + " to player " + player.getName());
                lastSentSha1.put(player, sha1);
                player.setResourcePack(url, sha1);
            } else {
                plugin.getServer().getScheduler().runTaskLater(plugin, new Runnable() {
                    @Override
                    public void run() {
                        refreshPlayer(player);
                    }
                }, delayMillis / 50 + 1);
            }
        }
    }

    @Override
    public void refreshAll() {
        for(Player player : plugin.getServer().getOnlinePlayers()) {
            refreshPlayer(player);
        }
    }

    @EventHandler
    public void join(PlayerJoinEvent event) {
        this.joinTime.put(event.getPlayer(), Instant.now());
        refreshPlayer(event.getPlayer());
    }

    @Subscribe
    public void reconfigure(LocalServerReconfigureEvent event) {
        if(event.getNewConfig().resource_pack_fast_update()) {
            String oldSha1 = event.getOldConfig() == null ? null : event.getOldConfig().resource_pack_sha1();
            String newSha1 = event.getNewConfig().resource_pack_sha1();
            if(!Objects.equals(oldSha1, newSha1)) refreshAll();
        }
    }

    @EventHandler
    public void confirm(final PlayerResourcePackStatusEvent event) {
        logger.fine("Player " + event.getPlayer().getName() + " sent res pack status " + event.getStatus());
        final UserDoc.ResourcePackStatus status;
        switch(event.getStatus()) {
            case ACCEPTED: status = UserDoc.ResourcePackStatus.ACCEPTED; break;
            case DECLINED: status = UserDoc.ResourcePackStatus.DECLINED; break;
            case SUCCESSFULLY_LOADED: status = UserDoc.ResourcePackStatus.LOADED; break;
            case FAILED_DOWNLOAD: status = UserDoc.ResourcePackStatus.FAILED; break;
            default: throw new IllegalStateException("Unknown status " + event.getStatus());
        }
        userService.update(
            Users.playerId(event.getPlayer()),
            (UserDoc.ResourcePackResponse) () -> status
        );
    }
}
