package tc.oc.pgm.goals;

import java.util.Map;
import javax.annotation.Nullable;
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.pgm.api.PGM;
import tc.oc.pgm.api.event.CoarsePlayerMoveEvent;
import tc.oc.pgm.api.match.Match;
import tc.oc.pgm.api.party.Competitor;
import tc.oc.pgm.api.party.Party;
import tc.oc.pgm.api.party.event.CompetitorRemoveEvent;
import tc.oc.pgm.api.player.MatchPlayer;
import tc.oc.pgm.api.player.ParticipantState;
import tc.oc.pgm.api.player.event.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;
import tc.oc.pgm.util.LegacyFormatUtils;
import tc.oc.pgm.util.block.BlockVectors;
import tc.oc.pgm.util.collection.DefaultMapAdapter;

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);
  }

  /**
   * 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 : 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
        && PGM.get().getConfiguration().showProximity()
        && isProximityRelevant(team)
        && (viewer == team || viewer.isObserving());
  }

  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 = LegacyFormatUtils.tiny(String.format("%.1f", distance));
    }

    return renderProximityColor(team, viewer) + text;
  }

  @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
  public 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)
  public void onPlayerPlaceBlock(ParticipantBlockTransformEvent event) {
    if (getProximityMetricType(event.getPlayerState().getParty())
            == ProximityMetric.Type.CLOSEST_BLOCK
        && canBlockUpdateProximity(event.getOldState(), event.getNewState())) {

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

  @EventHandler(priority = EventPriority.MONITOR)
  public 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)
  public void onTouch(GoalTouchEvent event) {
    if (this == event.getGoal() && event.isFirstForCompetitor()) {
      resetProximity(event.getCompetitor());
    }
  }

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

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