package tc.oc.pgm.lane;

import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;

import com.google.common.collect.ObjectArrays;
import com.google.common.collect.Sets;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.ChatColor;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.bukkit.util.Vector;
import tc.oc.api.docs.PlayerId;
import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent;
import tc.oc.commons.bukkit.util.BlockUtils;
import tc.oc.commons.bukkit.util.Materials;
import tc.oc.commons.core.stream.BiStream;
import tc.oc.commons.core.stream.Collectors;
import tc.oc.pgm.PGMTranslations;
import tc.oc.pgm.events.BlockTransformEvent;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.PlayerBlockTransformEvent;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.regions.Region;
import tc.oc.pgm.regions.Union;
import tc.oc.pgm.spawns.events.ParticipantDespawnEvent;
import tc.oc.pgm.teams.Team;
import tc.oc.pgm.teams.TeamMatchModule;

@ListenerScope(MatchScope.RUNNING)
public class LaneMatchModule extends MatchModule implements Listener {
    private final Map<Team, Region> lanes;
    private final Set<PlayerId> voidPlayers = Sets.newHashSet();

    @Inject private LaneMatchModule(List<Lane> lanes, TeamMatchModule teams) {
        this.lanes = BiStream.fromValues(lanes.stream(), lane -> teams.team(lane.team()))
                             .mapValues(lane -> Union.of(lane.regions()))
                             .collect(Collectors.toImmutableMap());
    }

    @Override
    public void disable() {
        this.voidPlayers.clear();
    }

    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
    public void checkLaneMovement(final CoarsePlayerMoveEvent event) {
        MatchPlayer player = this.match.getPlayer(event.getPlayer());
        if(player == null ||
           !player.canInteract() ||
           !(player.getParty() instanceof Team) ||
           player.getBukkit().getGameMode() == GameMode.CREATIVE ||
           event.getTo().getY() <= 0) return;

        Region laneRegion = this.lanes.get(player.getParty());
        if(laneRegion == null) return;

        boolean containsFrom = laneRegion.contains(event.getBlockFrom().toVector());
        boolean containsTo = laneRegion.contains(event.getBlockTo().toVector());

        // prevent ender pearling to the other lane
        if(!containsTo && event.getCause() instanceof PlayerTeleportEvent) {
            if(((PlayerTeleportEvent) event.getCause()).getCause() == TeleportCause.ENDER_PEARL) {
                event.setCancelled(true, new TranslatableComponent("match.lane.enderPearl.disabled"));
                return;
            }
        }

        if(this.voidPlayers.contains(player.getPlayerId())) {
            event.getPlayer().setFallDistance(0);
            // they have been marked as "out of lane"
            if(containsTo && !containsFrom) {
                // prevent the player from re-entering the lane
                event.setCancelled(true, new TranslatableComponent("match.lane.reEntry.disabled"));
            } else {
                // if they are going to land on something, teleport them underneith it
                Block under = event.getTo().clone().add(new Vector(0, -1, 0)).getBlock();
                if(under != null && under.getType() != Material.AIR) {
                    // teleport them to the lowest block
                    Vector safe = getSafeLocationUnder(under);
                    EntityLocation safeLocation = event.getPlayer().getEntityLocation();
                    safeLocation.setPosition(safe);
                    event.setTo(safeLocation);
                }
            }
        } else {
            if(!containsFrom && !containsTo) {
                // they are outside of the lane
                if(isIllegallyOutsideLane(laneRegion, event.getTo())) {
                    this.voidPlayers.add(player.getPlayerId());
                    event.getPlayer().sendMessage(ChatColor.RED + PGMTranslations.t("match.lane.exit", player));
                }
            }
        }
    }

    @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
    public void preventBlockPlaceByVoidPlayer(final BlockTransformEvent event) {
        if(event instanceof PlayerBlockTransformEvent) {
            event.setCancelled(this.voidPlayers.contains(((PlayerBlockTransformEvent) event).getPlayerState().getPlayerId()));
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void clearLaneStatus(final ParticipantDespawnEvent event) {
        this.voidPlayers.remove(event.getPlayer().getPlayerId());
    }

    private static BlockFace[] CARDINAL_DIRECTIONS = { BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST };
    private static BlockFace[] DIAGONAL_DIRECTIONS = { BlockFace.NORTH_EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH_WEST, BlockFace.NORTH_WEST };

    private static Block getAdjacentRegionBlock(Region region, Block origin) {
        for(BlockFace face : ObjectArrays.concat(CARDINAL_DIRECTIONS, DIAGONAL_DIRECTIONS, BlockFace.class)) {
            Block adjacent = origin.getRelative(face);
            if(region.contains(BlockUtils.center(adjacent).toVector())) {
                return adjacent;
            }
        }
        return null;
    }

    private static boolean isIllegalBlock(Region region, Block block) {
        Block adjacent = getAdjacentRegionBlock(region, block);
        return adjacent == null || Materials.isColliding(adjacent.getType());
    }

    private static boolean isIllegallyOutsideLane(Region lane, Location loc) {
        Block feet = loc.getBlock();
        if(feet == null) return false;

        if(isIllegalBlock(lane, feet)) {
            return true;
        }

        Block head = feet.getRelative(BlockFace.UP);
        if(head == null) return false;

        if(isIllegalBlock(lane, head)) {
            return true;
        }

        return false;
    }

    private static Vector getSafeLocationUnder(Block block) {
        World world = block.getWorld();
        for(int y = block.getY() - 2; y >= 0; y--) {
            Block feet = world.getBlockAt(block.getX(), y, block.getZ());
            Block head = world.getBlockAt(block.getX(), y + 1, block.getZ());
            if(feet.getType() == Material.AIR && head.getType() == Material.AIR) {
                return new Vector(block.getX() + 0.5, y, block.getZ() + 0.5);
            }
        }
        return new Vector(block.getX() + 0.5, -2, block.getZ() + 0.5);
    }
}
