package tc.oc.pgm.goals;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.annotation.Nullable;

import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import tc.oc.api.docs.virtual.MatchDoc;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.bukkit.chat.ComponentRenderers;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.match.Competitor;
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.pgm.events.CompetitorRemoveEvent;
import tc.oc.pgm.goals.events.GoalCompleteEvent;
import tc.oc.pgm.goals.events.GoalTouchEvent;
import tc.oc.pgm.spawns.events.ParticipantDespawnEvent;

/**
 * A {@link Goal} that may be 'touched' by players, meaning the player has
 * made some tangible progress in completing the goal.
 */
@ListenerScope(MatchScope.RUNNING)
public abstract class TouchableGoal<T extends ProximityGoalDefinition> extends ProximityGoal<T> implements Listener {

    public static final ChatColor COLOR_TOUCHED = ChatColor.YELLOW;
    public static final String SYMBOL_TOUCHED = "\u2733"; // ✳

    protected boolean touched;
    protected final Set<Competitor> touchingCompetitors = new HashSet<>();
    protected final Set<ParticipantState> touchingPlayers = new HashSet<>();
    protected final Set<ParticipantState> recentTouchingPlayers = new HashSet<>();

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

    /**
     * Should touches NOT be credited until the goal is completed?
     */
    public boolean getDeferTouches() {
        return false;
    }

    /**
     * Gets a formatted message designed to be broadcast when a player touches the goal.
     *
     * @param toucher The player
     * @param self is the message for the toucher?
     */
    public abstract BaseComponent getTouchMessage(@Nullable ParticipantState toucher, boolean self);

    @Override
    public net.md_5.bungee.api.ChatColor renderProximityColor(Competitor team, Party viewer) {
        return hasTouched(team) ? net.md_5.bungee.api.ChatColor.YELLOW : super.renderProximityColor(team, viewer);
    }

    @Override
    public ChatColor renderSidebarStatusColor(@Nullable Competitor competitor, Party viewer) {
        return shouldShowTouched(competitor, viewer) ? COLOR_TOUCHED
                                                     : super.renderSidebarStatusColor(competitor, viewer);
    }

    @Override
    public String renderSidebarStatusText(@Nullable Competitor competitor, Party viewer) {
        return shouldShowTouched(competitor, viewer) ? SYMBOL_TOUCHED
                                                     : super.renderSidebarStatusText(competitor, viewer);
    }

    public boolean isTouched() {
        return touched;
    }

    /** Gets whether or not the specified team has touched the goal since the last reset. */
    public boolean hasTouched(Competitor team) {
        return touchingCompetitors.contains(team);
    }

    public boolean hasTouched(ParticipantState player) {
        return touchingPlayers.contains(player);
    }

    public ImmutableSet<ParticipantState> getTouchingPlayers() {
        return ImmutableSet.copyOf(touchingPlayers);
    }

    /** Gets whether or not the specified player has recently (in their current lifetime) touched the goal. */
    public boolean hasTouchedRecently(final ParticipantState player) {
        return recentTouchingPlayers.contains(player);
    }

    /**
     * Gets whether or not the specified player touching the goal has any significance at this moment.
     */
    public boolean canTouch(final ParticipantState player) {
        return canComplete(player.getParty()) &&
               !isCompleted(player.getParty()) &&
               !hasTouchedRecently(player);
    }

    public void touch(final @Nullable ParticipantState toucher) {
        // TODO: support playerless touches (deduce which team to give the touch to based on objective owner etc)
        if(toucher == null) return;

        touched = true;

        GoalTouchEvent event;
        if(toucher == null) {
            event = new GoalTouchEvent(this, getMatch().getClock().now().instant);
        } else {
            if(!canTouch(toucher)) return;

            boolean firstForCompetitor = touchingCompetitors.add(toucher.getParty());
            boolean firstForPlayer = touchingPlayers.add(toucher);
            boolean firstForPlayerLife = recentTouchingPlayers.add(toucher);

            event = new GoalTouchEvent(this,
                                       toucher.getParty(), firstForCompetitor,
                                       toucher, firstForPlayer, firstForPlayerLife,
                                       getMatch().getClock().now().instant);
        }

        getMatch().callEvent(event);
        sendTouchMessage(toucher, !event.getCancelToucherMessage());
        playTouchEffects(toucher);
    }

    public void resetTouches() {
        touched = false;
        touchingCompetitors.clear();
        touchingPlayers.clear();
        recentTouchingPlayers.clear();
    }

    public void resetTouches(Competitor team) {
        if(touchingCompetitors.remove(team)) {
            for(Iterator<ParticipantState> iterator = touchingPlayers.iterator(); iterator.hasNext(); ) {
                if(iterator.next().getParty() == team) iterator.remove();;
            }
            for(Iterator<ParticipantState> iterator = recentTouchingPlayers.iterator(); iterator.hasNext(); ) {
                if(iterator.next().getParty() == team) iterator.remove();;
            }
        }
    }

    @Override
    public @Nullable ProximityMetric getProximityMetric(Competitor team) {
        if(hasTouched(team)) {
            return getDefinition().getPostTouchMetric();
        } else {
            return super.getProximityMetric(team);
        }
    }

    public boolean showEnemyTouches() {
        return false;
    }

    public boolean shouldShowTouched(@Nullable Competitor team, Party viewer) {
        return team != null &&
               !isCompleted(team) &&
               hasTouched(team) &&
               (team == viewer || showEnemyTouches() || viewer.isObservingType());
    }

    protected void sendTouchMessage(@Nullable ParticipantState toucher, boolean includeToucher) {
        if(!isVisible()) return;

        BaseComponent message = getTouchMessage(toucher, false);
        ComponentRenderers.send(Bukkit.getConsoleSender(), message);

        if(!showEnemyTouches()) {
            message = new Component(toucher.getParty().getChatPrefix(), message);
        }

        for(MatchPlayer viewer : getMatch().getPlayers()) {
            if(shouldShowTouched(toucher.getParty(), viewer.getParty()) && (toucher == null || !toucher.isPlayer(viewer))) {
                viewer.sendMessage(message);
            }
        }

        if(toucher != null) {
            if(includeToucher) {
                toucher.getAudience().sendMessage(getTouchMessage(toucher, true));
            }

            if(getDeferTouches()) {
                toucher.getAudience().sendMessage(new TranslatableComponent("match.touch.destroyable.deferredNotice"));
            }
        }
    }

    protected void playTouchEffects(@Nullable ParticipantState toucher) {
        if(toucher == null || !isVisible()) return;

        MatchPlayer onlineToucher = toucher.getMatchPlayer();
        if(onlineToucher == null) return;

        onlineToucher.playSparks();
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void onPlayerDeath(ParticipantDespawnEvent event) {
        ParticipantState victim = event.getPlayer().getParticipantState();
        if(victim != null) recentTouchingPlayers.remove(victim);
    }

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

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

    @Override
    public MatchDoc.TouchableGoal getDocument() {
        return new Document();
    }

    public class Document extends OwnedGoal.Document implements MatchDoc.TouchableGoal {
        @Override
        public Collection<? extends MatchDoc.TouchableGoal.Proximity> proximities() {
            return Collections2.transform(
                getMatch().getCompetitors(),
                new Function<Competitor, MatchDoc.TouchableGoal.Proximity>() {
                    @Override
                    public MatchDoc.TouchableGoal.Proximity apply(Competitor competitor) {
                        return new Proximity(competitor);
                    }
                }
            );
        }

        public class Proximity implements MatchDoc.TouchableGoal.Proximity {
            private final Competitor competitor;

            @Override
            public String _id() {
                return competitor.getId();
            }

            public Proximity(Competitor competitor) {
                this.competitor = competitor;
            }

            @Override
            public boolean touched() {
                return hasTouched(competitor);
            }

            @Override
            public Metric metric() {
                final ProximityMetric metric = getProximityMetric(competitor);
                return metric == null ? null : metric.apiValue();
            }

            @Override
            public double distance() {
                return getMinimumDistance(competitor);
            }
        }
    }
}