package tc.oc.pgm.flag;

import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

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.DyeColor;
import org.bukkit.FireworkEffect;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.block.Banner;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Firework;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BannerMeta;
import org.bukkit.util.BlockVector;
import tc.oc.commons.bukkit.chat.BukkitSound;
import tc.oc.commons.bukkit.chat.NameStyle;
import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent;
import tc.oc.commons.bukkit.util.BukkitUtils;
import tc.oc.commons.bukkit.util.Materials;
import tc.oc.commons.bukkit.util.NMSHacks;
import tc.oc.commons.bukkit.util.materials.Banners;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.util.Lazy;
import tc.oc.commons.core.util.Optionals;
import tc.oc.pgm.events.BlockTransformEvent;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.PlayerLeavePartyEvent;
import tc.oc.pgm.filters.query.ILocationQuery;
import tc.oc.pgm.filters.query.IQuery;
import tc.oc.pgm.fireworks.FireworkUtil;
import tc.oc.pgm.flag.event.FlagCaptureEvent;
import tc.oc.pgm.flag.event.FlagStateChangeEvent;
import tc.oc.pgm.flag.state.BaseState;
import tc.oc.pgm.flag.state.Captured;
import tc.oc.pgm.flag.state.Completed;
import tc.oc.pgm.flag.state.Returned;
import tc.oc.pgm.flag.state.Spawned;
import tc.oc.pgm.flag.state.State;
import tc.oc.pgm.goals.TouchableGoal;
import tc.oc.pgm.goals.events.GoalCompleteEvent;
import tc.oc.pgm.goals.events.GoalEvent;
import tc.oc.pgm.goals.events.GoalStatusChangeEvent;
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.module.ModuleLoadException;
import tc.oc.pgm.points.AngleProvider;
import tc.oc.pgm.points.PointProvider;
import tc.oc.pgm.points.StaticAngleProvider;
import tc.oc.pgm.regions.PointRegion;
import tc.oc.pgm.regions.Region;
import tc.oc.pgm.spawns.events.ParticipantDespawnEvent;
import tc.oc.pgm.teams.Team;
import tc.oc.pgm.teams.TeamMatchModule;

@ListenerScope(MatchScope.LOADED)
public class Flag extends TouchableGoal<FlagDefinition> implements Listener {

    public static final String RESPAWNING_SYMBOL = "\u2690"; // ⚐
    public static final String RETURNED_SYMBOL = "\u2691";  // ⚑
    public static final String DROPPED_SYMBOL = "\u2691";  // ⚑
    public static final String CARRIED_SYMBOL = "\u2794";  // ➔

    public static final BukkitSound PICKUP_SOUND_OWN = new BukkitSound(Sound.ENTITY_WITHER_AMBIENT, 0.7f, 1.2f);
    public static final BukkitSound DROP_SOUND_OWN = new BukkitSound(Sound.ENTITY_WITHER_HURT, 0.7f, 1);
    public static final BukkitSound RETURN_SOUND_OWN = new BukkitSound(Sound.ENTITY_ZOMBIE_VILLAGER_CONVERTED, 1.1f, 1.2f);

    public static final BukkitSound PICKUP_SOUND = new BukkitSound(Sound.ENTITY_FIREWORK_LARGE_BLAST_FAR, 1f, 0.7f);
    public static final BukkitSound DROP_SOUND = new BukkitSound(Sound.ENTITY_FIREWORK_TWINKLE_FAR, 1f, 1f);
    public static final BukkitSound RETURN_SOUND = new BukkitSound(Sound.ENTITY_FIREWORK_TWINKLE_FAR, 1f, 1f);

    private final ImmutableSet<Net> nets;
    private final @Nullable Team owner;

    private final Lazy<Set<Team>> capturers;
    private final Lazy<Set<Team>> controllers;
    private final Lazy<Set<Team>> completers;

    private BaseState state;
    private boolean transitioning;

    private final BannerInfo bannerInfo;
    private static class BannerInfo {
        final Location location;
        final BannerMeta meta;
        final ItemStack item;
        final AngleProvider yawProvider;

        private BannerInfo(Location location, BannerMeta meta, ItemStack item, AngleProvider yawProvider) {
            this.location = location;
            this.meta = meta;
            this.item = item;
            this.yawProvider = yawProvider;
        }
    }

    protected Flag(Match match, FlagDefinition definition, ImmutableSet<Net> nets) throws ModuleLoadException {
        super(definition, match);
        this.nets = nets;

        final TeamMatchModule tmm = match.getMatchModule(TeamMatchModule.class);

        this.owner = definition.owner()
                               .map(tmm::team) // Do not use a method ref here, it will NPE if tmm is null
                               .orElse(null);

        this.capturers = Lazy.from(
            () -> Optionals.stream(match.module(TeamMatchModule.class))
                           .flatMap(TeamMatchModule::teams)
                           .filter(team -> getDefinition().canPickup(team) && canCapture(team))
                           .collect(Collectors.toSet())
        );

        this.controllers = Lazy.from(
            () -> nets.stream()
                      .flatMap(net -> Optionals.stream(net.returnPost()
                                                          .flatMap(Post::owner)))
                      .map(tmm::team)
                      .collect(Collectors.toSet())
        );

        this.completers = Lazy.from(
            () -> nets.stream()
                      .flatMap(net -> Optionals.stream(net.returnPost()))
                      .filter(Post::isPermanent)
                      .flatMap(post -> Optionals.stream(post.owner()))
                      .map(tmm::team)
                      .collect(Collectors.toSet())
        );

        Banner banner = null;
        pointLoop: for(PointProvider returnPoint : definition.getDefaultPost().getReturnPoints()) {
            Region region = returnPoint.getRegion();
            if(region instanceof PointRegion) {
                // Do not require PointRegions to be at the exact center of the block.
                // It might make sense to just override PointRegion.getBlockVectors() to
                // always do this, but it does technically violate the contract of that method.
                banner = toBanner(((PointRegion) region).getPosition().toLocation(match.getWorld()).getBlock());
                if(banner != null) break pointLoop;
            } else {
                for(BlockVector pos : returnPoint.getRegion().getBlockVectors()) {
                    banner = toBanner(pos.toLocation(match.getWorld()).getBlock());
                    if(banner != null) break pointLoop;
                }
            }
        }

        if(banner == null) {
            throw new ModuleLoadException("Flag '" + getName() + "' must have a banner at its default post");
        }

        final Location location = Banners.getLocationWithYaw(banner);
        bannerInfo = new BannerInfo(location,
                                    Banners.getItemMeta(banner),
                                    new ItemStack(Material.BANNER),
                                    new StaticAngleProvider(location.getYaw()));
        bannerInfo.item.setItemMeta(bannerInfo.meta);

        match.registerEvents(this);

        this.state = new Returned(this, this.getDefinition().getDefaultPost(), bannerInfo.location);
        this.state.enterState();
    }

    private static Banner toBanner(Block block) {
        if(block == null) return null;
        BlockState state = block.getState();
        return state instanceof Banner ? (Banner) state : null;
    }

    @Override
    public String toString() {
        return "Flag{name=" + this.getName() + " state=" + this.state + "}";
    }

    public DyeColor getDyeColor() {
        DyeColor color = this.getDefinition().getColor();
        if(color == null) color = bannerInfo.meta.getBaseColor();
        return color;
    }

    public net.md_5.bungee.api.ChatColor getChatColor() {
        return BukkitUtils.toChatColor(this.getDyeColor());
    }

    public String getColoredName() {
        return this.getChatColor() + this.getName();
    }

    public Component getComponentName() {
        return new Component(getName()).color(getChatColor());
    }

    public ImmutableSet<Net> getNets() {
        return nets;
    }

    public BannerMeta getBannerMeta() {
        return bannerInfo.meta;
    }

    public ItemStack getBannerItem() {
        return bannerInfo.item;
    }

    public State state() {
        return state;
    }

    /**
     * Owner is defined in XML, and does not change during a match
     */
    public @Nullable Team getOwner() {
        return owner;
    }

    /**
     * Physical location of the flag, if any
     */
    public Optional<Location> getLocation() {
        return state instanceof Spawned ? Optional.of(((Spawned) state).getLocation())
                                        : Optional.empty();
    }

    /**
     * Controller is the owner of the {@link Post} the flag is at, which obviously can change
     */
    public @Nullable Team getController() {
        return this.state.getController();
    }

    public boolean hasMultipleControllers() {
        return !controllers.get().isEmpty();
    }

    public boolean canDropOn(BlockState base) {
        return Materials.isColliding(base.getType()) || (getDefinition().canDropOnWater() && Materials.isWater(base.getType()));
    }

    public boolean canDropAt(Location location) {
        if(!match.getWorld().equals(location.getWorld())) return false;

        Block block = location.getBlock();
        Block below = block.getRelative(BlockFace.DOWN);
        if(!canDropOn(below.getState())) return false;
        if(block.getRelative(BlockFace.UP).getType() != Material.AIR) return false;

        switch(block.getType()) {
            case AIR:
            case LONG_GRASS:
                return true;
            default:
                return false;
        }
    }

    public boolean canDrop(ILocationQuery query) {
        return canDropAt(query.getLocation()) &&
               getDefinition().getDropFilter().query(query).isAllowed();
    }

    public Location getReturnPoint(Post post) {
        return post.getReturnPoint(this, bannerInfo.yawProvider).clone();
    }


    // Touchable

    @Override
    public boolean canTouch(ParticipantState player) {
        MatchPlayer matchPlayer = player.getMatchPlayer();
        return matchPlayer != null && canPickup(matchPlayer, state.getPost());
    }

    @Override
    public boolean showEnemyTouches() {
        return true;
    }

    @Override
    public BaseComponent getTouchMessage(ParticipantState toucher, boolean self) {
        if(self) {
            return new TranslatableComponent("match.flag.pickup.you", getComponentName());
        } else {
            return new TranslatableComponent("match.flag.pickup", getComponentName(), toucher.getStyledName(NameStyle.COLOR));
        }
    }


    // Proximity

    @Override
    public Iterable<Location> getProximityLocations(ParticipantState player) {
        return state.getProximityLocations(player);
    }

    @Override
    public boolean isProximityRelevant(Competitor team) {
        if(hasTouched(team)) {
            return canCapture(team);
        } else {
            return canPickup(team);
        }
    }


    // Misc

    /**
     * Transition to the given state. This happens immediately if not already transitioning.
     * If this is called from within a transition, the state is queued and the transition
     * happens after the current one completes. This allows {@link BaseState#enterState} to
     * immediately transition into another state without nesting the transitions, and keeps
     * the events in the correct order.
     */
    public void transition(BaseState newState) {
        if(this.transitioning) {
            throw new IllegalStateException("Nested flag state transition");
        }

        BaseState oldState = this.state;
        try {
            logger.fine("Transitioning " + getName() + " from " + oldState + " to " + newState);

            this.transitioning = true;
            this.state.leaveState();
            this.state = newState;
            this.state.enterState();
        } finally {
            this.transitioning = false;
        }

        getMatch().callEvent(new FlagStateChangeEvent(this, oldState, this.state));

        // If we are still in the state we just transitioned into, start the countdown, if any.
        // We check this because the FlagStateChangeEvent may have already transitioned into another state.
        if(this.state == newState) {
            this.state.startCountdown();
        }

        // Check again, in case startCountdown transitioned. In that case, the nested
        // transition will have already called these events if necessary.
        if(this.state == newState) {
            getMatch().callEvent(new GoalStatusChangeEvent(this));
            if(isCompleted()) {
                getMatch().callEvent(new GoalCompleteEvent(this,
                                                           true,
                                                           c -> false,
                                                           c -> c.equals(getController())));
            }
        }
    }

    public boolean canPickup(IQuery query, Post post) {
        return getDefinition().getPickupFilter().query(query).isAllowed() &&
               post.getPickupFilter().query(query).isAllowed();
    }

    public boolean canPickup(IQuery query) {
        return canPickup(query, state.getPost());
    }

    public boolean canCapture(IQuery query, Net net) {
        return getDefinition().getCaptureFilter().query(query).isAllowed() &&
               net.getCaptureFilter().query(query).isAllowed();
    }

    public boolean canCapture(IQuery query) {
        return getDefinition().canCapture(query, getNets());
    }

    public boolean isCurrent(Class<? extends State> state) {
        return state.isInstance(this.state);
    }

    public boolean isCurrent(State state) {
        return this.state == state;
    }

    public boolean isCarrying(ParticipantState player) {
        MatchPlayer matchPlayer = player.getMatchPlayer();
        return matchPlayer != null && isCarrying(matchPlayer);
    }

    public boolean isCarrying(MatchPlayer player) {
        return this.state.isCarrying(player);
    }

    public boolean isCarrying(Competitor party) {
        return this.state.isCarrying(party);
    }

    public boolean isAtPost(Post post) {
        return this.state.isAtPost(post);
    }

    public boolean isCompletable() {
        return !completers.get().isEmpty();
    }

    @Override
    public boolean canComplete(Competitor team) {
        return team instanceof Team && capturers.get().contains(team);
    }

    @Override
    public boolean isShared() {
        // Flag is shared if it has multiple capturers or no capturers
        return capturers.get().size() != 1;
    }

    @Override
    public boolean isCompleted() {
        return isCurrent(Completed.class);
    }

    @Override
    public boolean isCompleted(Competitor team) {
        return isCompleted() && getController() == team;
    }

    public boolean isCaptured() {
        return isCompleted() || isCurrent(Captured.class);
    }

    @Override
    public String renderSidebarStatusText(@Nullable Competitor competitor, Party viewer) {
        return this.state.getStatusText(viewer);
    }

    @Override
    public ChatColor renderSidebarStatusColor(@Nullable Competitor competitor, Party viewer) {
        return this.state.getStatusColor(viewer);
    }

    @Override
    public ChatColor renderSidebarLabelColor(@Nullable Competitor competitor, Party viewer) {
        return this.state.getLabelColor(viewer);
    }

    public void playFlareEffect() {
        if(isCurrent(Spawned.class)) {
            Location location = ((Spawned) this.state).getLocation();
            if(location == null) return;
            FireworkEffect effect = FireworkEffect.builder().with(FireworkEffect.Type.BURST).withColor(this.getDyeColor().getColor()).build();
            Firework firework = FireworkUtil.spawnFirework(location, effect, 0);
            NMSHacks.skipFireworksLaunch(firework);
        }
    }

    /**
     * Play one of two status sounds depending on the team of the listener.
     * Owning players hear the first sound, other players hear the second.
     */
    public void playStatusSound(BukkitSound ownerSound, BukkitSound otherSound) {
        for(MatchPlayer listener : getMatch().getPlayers()) {
            if(listener.getParty() != null && (listener.getParty() == this.getOwner() || listener.getParty() == this.getController())) {
                listener.playSound(ownerSound);
            } else {
                listener.playSound(otherSound);
            }
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    private void onPlayerDeath(PlayerDeathEvent event) {
        event.getDrops().removeIf(itemStack -> itemStack.isSimilar(this.getBannerItem()));
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    private void onGoalChange(GoalEvent event) {
        this.state.onEvent(event);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    private void onFlagStateChange(FlagStateChangeEvent event) {
        this.state.onEvent(event);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    private void onFlagCapture(FlagCaptureEvent event) {
        this.state.onEvent(event);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    private void onPlayerMove(PlayerMoveEvent event) {
        if(event.getFrom().getWorld() == event.getTo().getWorld()) { // yes, this can be false
            this.state.onEvent(event);
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    private void onPlayerMove(CoarsePlayerMoveEvent event) {
        this.state.onEvent(event);
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    private void onBlockTransform(BlockTransformEvent event) {
        this.state.onEvent(event);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    private void onItemDrop(PlayerDropItemEvent event) {
        this.state.onEvent(event);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    private void onPlayerDespawn(ParticipantDespawnEvent event) {
        this.state.onEvent(event);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    private void onPlayerDespawn(PlayerLeavePartyEvent event) {
        this.state.onEvent(event);
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    private void onInventoryClick(InventoryClickEvent event) {
        this.state.onEvent(event);
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    private void onProjectileHit(EntityDamageEvent event) {
        this.state.onEvent(event);
    }
}
