package tc.oc.pgm.goals;

import java.util.Map;
import javax.annotation.Nullable;

import com.google.common.collect.ImmutableSet;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Location;
import org.bukkit.block.BlockState;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import tc.oc.commons.bukkit.util.BlockUtils;
import tc.oc.commons.core.chat.ChatUtils;
import tc.oc.commons.core.localization.Locales;
import tc.oc.commons.core.util.DefaultMapAdapter;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.Config;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.match.ParticipantState;
import tc.oc.pgm.match.Party;
import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent;
import tc.oc.pgm.events.CompetitorRemoveEvent;
import tc.oc.pgm.events.MatchPlayerDeathEvent;
import tc.oc.pgm.events.ParticipantBlockTransformEvent;
import tc.oc.pgm.goals.events.GoalCompleteEvent;
import tc.oc.pgm.goals.events.GoalProximityChangeEvent;
import tc.oc.pgm.goals.events.GoalTouchEvent;

@ListenerScope(MatchScope.RUNNING)
public abstract class ProximityGoal<T extends ProximityGoalDefinition> extends OwnedGoal<T> implements Listener {

    private final Map<Competitor, Integer> proximity = new DefaultMapAdapter<>(Integer.MAX_VALUE);

    public ProximityGoal(T definition, Match match) {
        super(definition, match);
        match.registerEvents(this);
    }

    /**
     * Get the locations from which proximity can be measured relative to.
     * The shortest measurement will be used.
     */
    public abstract Iterable<Location> getProximityLocations(ParticipantState player);

    public @Nullable ProximityMetric getProximityMetric(Competitor team) {
        return getDefinition().getPreTouchMetric();
    }

    public @Nullable ProximityMetric.Type getProximityMetricType(Competitor team) {
        ProximityMetric metric = getProximityMetric(team);
        return metric == null ? null : metric.type;
    }

    /**
     * Is proximity relevant at the present moment for the given team?
     * That is, can it be measured and affect the outcome of te match?
     */
    public boolean isProximityRelevant(Competitor team) {
        return canComplete(team) && !isCompleted() && getProximityMetric(team) != null;
    }

    protected boolean canPlayerUpdateProximity(ParticipantState player) {
        return canComplete(player.getParty());
    }

    protected boolean canBlockUpdateProximity(BlockState oldState, BlockState newState) {
        return true;
    }

    private static double distanceFromDistanceSquared(int squared) {
        return squared == Integer.MAX_VALUE ? Double.POSITIVE_INFINITY : Math.sqrt(squared);
    }

    public int getProximity(Competitor team) {
        return this.proximity.get(team);
    }

    /**
     * Get the minimum distance the given team has been from the objective at
     * any time during the match (which is +Inf at the start of the match).
     * The given metric determines exactly how this is measured.
     */
    public double getMinimumDistance(Competitor team) {
        return distanceFromDistanceSquared(this.getProximity(team));
    }

    public void resetProximity(Competitor team) {
        Integer oldProximity = proximity.remove(team);
        if(oldProximity != null) {
            getMatch().callEvent(new GoalProximityChangeEvent(this, team, null, distanceFromDistanceSquared(oldProximity), Double.POSITIVE_INFINITY));
        }
    }

    public void resetProximity() {
        for(Competitor team : ImmutableSet.copyOf(proximity.keySet())) {
            resetProximity(team);
        }
    }

    public int getProximityFrom(ParticipantState player, Location location) {
        if(Double.isInfinite(location.lengthSquared())) return Integer.MAX_VALUE;

        ProximityMetric metric = getProximityMetric(player.getParty());
        if(metric == null) return Integer.MAX_VALUE;

        int minimumDistance = Integer.MAX_VALUE;
        for(Location v : getProximityLocations(player)) {
            // If either point is at infinity, the distance is infinite
            if(Double.isInfinite(v.lengthSquared())) continue;

            int dx = location.getBlockX() - v.getBlockX();
            int dy = location.getBlockY() - v.getBlockY();
            int dz = location.getBlockZ() - v.getBlockZ();

            // Note: distances stay squared as long as possible
            int distance;
            if(metric.horizontal) {
                distance = dx*dx + dz*dz;
            } else {
                distance = dx*dx + dy*dy + dz*dz;
            }

            if(distance < minimumDistance) {
                minimumDistance = distance;
            }
        }

        return minimumDistance;
    }

    public boolean updateProximity(ParticipantState player, Location location) {
        if(isProximityRelevant(player.getParty()) && canPlayerUpdateProximity(player)) {
            int oldProximity = proximity.get(player.getParty());
            int newProximity = getProximityFrom(player, location);
            if(newProximity < oldProximity) {
                proximity.put(player.getParty(), newProximity);
                getMatch().callEvent(
                    new GoalProximityChangeEvent(this, player.getParty(), location,
                                                 distanceFromDistanceSquared(oldProximity),
                                                 distanceFromDistanceSquared(newProximity))
                );
                return true;
            }
        }
        return false;
    }

    public boolean shouldShowProximity(@Nullable Competitor team, Party viewer) {
        return team != null &&
               Config.Scoreboard.showProximity() &&
               isProximityRelevant(team) &&
               (viewer == team || viewer.isObservingType());
    }

    public ChatColor renderProximityColor(Competitor team, Party viewer) {
        return ChatColor.GRAY;
    }

    public String renderProximity(@Nullable Competitor team, Party viewer) {
        if(!shouldShowProximity(team, viewer)) return "";

        String text;
        double distance = this.getMinimumDistance(team);
        if(distance == Double.POSITIVE_INFINITY) {
            text = "\u221e"; // ∞
        } else {
            text = ChatUtils.tiny(String.format(Locales.DEFAULT_LOCALE, "%.1f", distance));
        }

        return renderProximityColor(team, viewer) + text;
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    private void onPlayerMove(CoarsePlayerMoveEvent event) {
        MatchPlayer player = getMatch().getParticipant(event.getPlayer());
        if(player != null &&
           getProximityMetricType(player.getCompetitor()) == ProximityMetric.Type.CLOSEST_PLAYER) {

            updateProximity(player.getParticipantState(), event.getBlockTo());
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    private void onPlayerPlaceBlock(ParticipantBlockTransformEvent event) {
        if(getProximityMetricType(event.getPlayerState().getParty()) == ProximityMetric.Type.CLOSEST_BLOCK &&
           canBlockUpdateProximity(event.getOldState(), event.getNewState())) {

            updateProximity(event.getPlayerState(), BlockUtils.center(event.getNewState()));
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    private void onPlayerKill(MatchPlayerDeathEvent event) {
        if(event.getKiller() != null &&
           event.isChallengeKill() &&
           getProximityMetricType(event.getKiller().getParty()) == ProximityMetric.Type.CLOSEST_KILL) {

            updateProximity(event.getKiller(), event.getKiller().getLocation());
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    private void onTouch(GoalTouchEvent event) {
        if(this == event.getGoal() && event.isFirstForCompetitor()) {
            resetProximity(event.getCompetitor());
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    private void onComplete(GoalCompleteEvent event) {
        if(this == event.getGoal()) {
            resetProximity();
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    private void onCompetitorRemove(CompetitorRemoveEvent event) {
        resetProximity(event.getCompetitor());
    }
}
