package tc.oc.commons.bukkit.listeners;

import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;

import org.bukkit.ChatColor;
import org.bukkit.Sound;
import org.bukkit.configuration.Configuration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import java.time.Duration;
import java.time.Instant;
import tc.oc.commons.bukkit.configuration.ConfigUtils;
import tc.oc.commons.bukkit.localization.CommonsTranslations;
import tc.oc.commons.bukkit.teleport.PlayerServerChanger;
import tc.oc.commons.bukkit.util.OnlinePlayerMapAdapter;
import tc.oc.commons.core.exception.ExceptionHandler;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.minecraft.api.scheduler.Tickable;

@Singleton
public class InactivePlayerListener implements Listener, PluginFacet, Tickable {

    public static class Config {
        private final Configuration config;
        private final ExceptionHandler<Throwable> exceptionHandler;

        @Inject Config(Configuration config, ExceptionHandler<Throwable> exceptionHandler) {
            this.config = config;
            this.exceptionHandler = exceptionHandler;
        }

        public boolean enabled() {
            return timeout() != null;
        }

        public Duration timeout() {
            return ConfigUtils.getDuration(config, "afk.timeout");
        }

        public Duration warning() {
            return ConfigUtils.getDuration(config, "afk.warning");
        }

        public java.time.Duration interval() {
            return exceptionHandler.flatGet(() -> config.duration("afk.interval"))
                                   .orElse(java.time.Duration.ofSeconds(10));
        }
    }

    private static final String AFK_FOREVER_PERM = "afk.forever";
    private final Config config;
    private final PlayerServerChanger playerServerChanger;

    private final OnlinePlayerMapAdapter<Instant> lastActivity;
    private @Nullable Instant lastCheck;

    @Inject InactivePlayerListener(Config config, PlayerServerChanger playerServerChanger, OnlinePlayerMapAdapter<Instant> lastActivity) {
        this.config = config;
        this.playerServerChanger = playerServerChanger;
        this.lastActivity = lastActivity;
    }

    @Override
    public boolean isActive() {
        return config.enabled();
    }

    @Override
    public java.time.Duration tickPeriod() {
        return config.interval();
    }

    @Override
    public void enable() {
        lastActivity.enable();
    }

    @Override
    public void disable() {
        lastActivity.disable();
    }

    private void activity(Player player) {
        if(player.hasPermission(AFK_FOREVER_PERM)) {
            this.lastActivity.remove(player);
        } else {
            this.lastActivity.put(player, Instant.now());
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void join(PlayerJoinEvent event) {
        this.activity(event.getPlayer());
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void move(PlayerMoveEvent event) {
        if(!Objects.equals(event.getFrom(), event.getTo())) {
            this.activity(event.getPlayer());
        }
    }

    @Override
    public void tick() {
        final Duration timeout = config.timeout();
        final Duration warning = config.warning();

        Instant now = Instant.now();
        Instant kickTime = now.minus(timeout);
        Instant warnTime = warning == null ? null : now.minus(warning);
        Instant lastWarnTime = warning == null || lastCheck == null ? null : lastCheck.minus(warning);

        // Iterate over a copy, because kicking players while iterating the original
        // OnlinePlayerMapAdapter throws a ConcurrentModificationException
        for(Map.Entry<Player, Instant> entry : lastActivity.entrySetCopy()) {
            Player player = entry.getKey();
            Instant time = entry.getValue();

            if(time.isBefore(kickTime)) {
                playerServerChanger.kickPlayer(player, CommonsTranslations.get().t("afk.kick", player));
            } else if(warnTime != null && time.isAfter(lastWarnTime) && !time.isAfter(warnTime)) {
                player.playSound(player.getLocation(), Sound.BLOCK_NOTE_PLING, 1, 1);
                player.sendMessage(ChatColor.RED.toString() + ChatColor.BOLD + CommonsTranslations.get().t(
                    "afk.warn", player,
                    ChatColor.AQUA.toString() + ChatColor.BOLD +
                        timeout.minus(warning).getSeconds() +
                        ChatColor.RED + ChatColor.BOLD
                ));
            }
        }

        lastCheck = now;
    }
}