package tc.oc.pgm.eventrules;

import java.util.Optional;

import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Hanging;
import org.bukkit.entity.ItemFrame;
import org.bukkit.entity.LeashHitch;
import org.bukkit.entity.Painting;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockDamageEvent;
import org.bukkit.event.block.BlockPhysicsEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.hanging.HangingBreakByEntityEvent;
import org.bukkit.event.hanging.HangingPlaceEvent;
import org.bukkit.event.player.PlayerBucketEmptyEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;
import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent;
import tc.oc.commons.bukkit.event.GeneralizingEvent;
import tc.oc.commons.bukkit.util.BlockStateUtils;
import tc.oc.commons.bukkit.util.BlockUtils;
import tc.oc.pgm.events.BlockTransformEvent;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.ParticipantBlockTransformEvent;
import tc.oc.pgm.filters.Filter.QueryResponse;
import tc.oc.pgm.filters.query.BlockEventQuery;
import tc.oc.pgm.filters.query.IBlockQuery;
import tc.oc.pgm.filters.query.IEventQuery;
import tc.oc.pgm.filters.query.IPlayerQuery;
import tc.oc.pgm.filters.query.IQuery;
import tc.oc.pgm.filters.query.PlayerBlockEventQuery;
import tc.oc.pgm.flag.event.FlagPickupEvent;
import tc.oc.pgm.kits.KitPlayerFacet;
import tc.oc.pgm.map.ProtoVersions;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.match.ParticipantState;
import tc.oc.pgm.utils.MatchPlayers;

import static tc.oc.pgm.map.ProtoVersions.REGION_PRIORITY_VERSION;

@ListenerScope(MatchScope.LOADED)
public class EventRuleMatchModule extends MatchModule implements Listener {

    protected final EventRuleContext ruleContext;
    protected final boolean useRegionPriority;

    public EventRuleMatchModule(Match match, EventRuleContext ruleContext) {
        super(match);
        this.ruleContext = ruleContext;
        this.useRegionPriority = this.getMatch().getMapInfo().proto.isNoOlderThan(REGION_PRIORITY_VERSION);
    }

    protected void checkEnterLeave(Event event, MatchPlayer player, Optional<BlockVector> from, BlockVector to) {
        if(player == null || !player.canInteract()) return;

        if(this.useRegionPriority) {
            // We need to handle both scopes in the same loop, because the priority order can interleave them
            for(EventRule rule : this.ruleContext.getAll()) {
                if((rule.scope() == EventRuleScope.PLAYER_ENTER && rule.region().enters(from, to)) ||
                   (rule.scope() == EventRuleScope.PLAYER_LEAVE && rule.region().exits(from, to))) {

                    if(processQuery(event, rule, player)) {
                        break; // Stop after the first non-abstaining filter
                    }
                }
            }
        } else {
            // To preserve legacy behavior exactly, these need to be in seperate loops
            for(EventRule rule : this.ruleContext.get(EventRuleScope.PLAYER_ENTER)) {
                if(rule.region().enters(from, to)) {
                    processQuery(event, rule, player);
                }
            }

            for(EventRule rule : this.ruleContext.get(EventRuleScope.PLAYER_LEAVE)) {
                if(rule.region().exits(from, to)) {
                    processQuery(event, rule, player);
                }
            }
        }
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void checkEnterLeave(final CoarsePlayerMoveEvent event) {
        this.checkEnterLeave(event, match.getPlayer(event.getPlayer()), Optional.of(event.getBlockFrom().toBlockVector()), event.getBlockTo().toBlockVector());
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void checkFlagPickup(final FlagPickupEvent event) {
        this.checkEnterLeave(event, event.getCarrier(), Optional.empty(), event.getCarrier().getBukkit().getLocation().toBlockVector());
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void applyEffects(final CoarsePlayerMoveEvent event) {
        MatchPlayer player = this.match.getPlayer(event.getPlayer());
        if(player == null) return;

        final BlockVector from = event.getBlockFrom().toBlockVector();
        final BlockVector to = event.getBlockTo().toBlockVector();

        for(EventRule rule : this.ruleContext.get(EventRuleScope.EFFECT)) {
            if(rule.velocity() == null && rule.kit() == null) continue;

            boolean enters = rule.region().enters(from, to);
            boolean exits = rule.region().exits(from, to);
            if(!enters && !exits) continue;

            if(!player.canInteract() || rule.filter() == null || rule.filter().query(player) != QueryResponse.DENY) {
                // Note: works on observers
                if(enters && rule.velocity() != null) {
                    event.getPlayer().setVelocity(rule.velocity());
                }

                if(rule.kit() != null && player.canInteract()) {
                    if(enters) {
                        player.facet(KitPlayerFacet.class).applyKit(rule.kit(), false);
                    }

                    if(exits && rule.lendKit()) {
                        rule.kit().remove(player);
                    }
                }
            }
        }
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void checkBlockTransform(final BlockTransformEvent event) {
        final BlockVector pos = BlockUtils.center(event.getNewState()).toBlockVector();
        final Optional<ParticipantState> actor = getActor(event);

        BlockState againstBlock = null;
        if(event.getCause() instanceof BlockPlaceEvent) {
            againstBlock = ((BlockPlaceEvent) event.getCause()).getBlockAgainst().getState();
        } else if(event.getCause() instanceof PlayerBucketEmptyEvent) {
            againstBlock = ((PlayerBucketEmptyEvent) event.getCause()).getBlockClicked().getState();
        }

        final IEventQuery breakQuery = PlayerBlockEventQuery.of(event.getOldState(), event, actor);
        final IEventQuery placeQuery = PlayerBlockEventQuery.of(event.getNewState(), event, actor);
        final IEventQuery againstQuery = againstBlock == null ? null : PlayerBlockEventQuery.of(againstBlock, event, actor);

        if(this.useRegionPriority) {
            // Note that the event may be in multiple scopes, which is why they must all be handled in the same pass
            ruleLoop: for(EventRule rule : this.ruleContext.getAll()) {
                switch(rule.scope()) {
                    case BLOCK_BREAK:
                        if(event.isBreak() && rule.region().contains(event.getOldState())) {
                            if(processQuery(rule, breakQuery)) {
                                break ruleLoop;
                            }
                        }
                        break;

                    case BLOCK_PLACE:
                        if(event.isPlace() && rule.region().contains(event.getNewState())) {
                            if(processQuery(rule, placeQuery)) {
                                break ruleLoop;
                            }
                        }
                        break;

                    case BLOCK_PLACE_AGAINST:
                        if(againstQuery != null) {
                            if(rule.region().contains(((IBlockQuery) againstQuery).getBlock())) {
                                if(processQuery(rule, againstQuery)) {
                                    break ruleLoop;
                                }
                            }
                        }
                        break;
                }
            }
        } else {
            // Legacy behavior
            if(event.isPlace()) {
                for(EventRule rule : this.ruleContext.get(EventRuleScope.BLOCK_PLACE)) {
                    if(rule.region().contains(pos)) {
                        processQuery(rule, placeQuery);
                    }
                }
            } else {
                for(EventRule rule : this.ruleContext.get(EventRuleScope.BLOCK_BREAK)) {
                    if(rule.region().contains(pos)) {
                        processQuery(rule, breakQuery);
                    }
                }
            }
        }
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void checkBlockPhysics(final BlockPhysicsEvent event) {
        BlockEventQuery query = new BlockEventQuery(event, event.getBlock().getState());
        for(EventRule rule : this.ruleContext.get(EventRuleScope.BLOCK_PHYSICS)) {
            if(rule.region().contains(event.getBlock()) && processQuery(rule, query)) break;
        }
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void checkBlockDamage(final BlockDamageEvent event) {
        MatchPlayer player = this.match.getParticipant(event.getPlayer());
        if(player == null) return;

        PlayerBlockEventQuery query = new PlayerBlockEventQuery(player, event, event.getBlock().getState());

        for(EventRule rule : this.ruleContext.get(EventRuleScope.BLOCK_BREAK)) {
            if(rule.earlyWarning() && rule.region().contains(event.getBlock())) {
                if(processQuery(rule, query)) {
                    if(event.isCancelled() && rule.message() != null) {
                        player.sendWarning(rule.message(), true);
                    }
                    if(this.useRegionPriority) {
                        break;
                    }
                }
            }
        }
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void checkUse(final PlayerInteractEvent event) {
        if(event.getAction() == Action.RIGHT_CLICK_BLOCK) {
            MatchPlayer player = this.match.getParticipant(event.getPlayer());
            if(player == null) return;

            Block block = event.getClickedBlock();
            if(block == null) return;

            this.handleUse(event, block.getState(), player);
        }
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void checkHangingPlace(final HangingPlaceEvent event) {
        this.handleHangingPlace(event, getHangingBlockState(event.getEntity()), event.getPlayer());
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void checkHangingBreak(final HangingBreakByEntityEvent event) {
        this.handleHangingBreak(event, event.getEntity(), event.getRemover());
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void checkItemFrameItemRemove(EntityDamageByEntityEvent event) {
        // This event is fired when popping an item out of an item frame, without breaking the frame itself
        if(event.getEntity() instanceof ItemFrame && ((ItemFrame) event.getEntity()).getItem() != null) {
            this.handleHangingBreak(event, (Hanging) event.getEntity(), event.getDamager());
        }
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void checkItemFrameRotate(PlayerInteractEntityEvent event) {
        if(event.getRightClicked() instanceof ItemFrame) {
            ItemFrame itemFrame = (ItemFrame) event.getRightClicked();
            if(itemFrame.getItem() != null) {
                // If frame contains an item, right-click will rotate it, which is handled as a "use" event
                this.handleUse(event, getHangingBlockState(itemFrame), this.match.getParticipant(event.getPlayer()));
            } else if(event.getPlayer().getItemInHand() != null) {
                // If the frame is empty and it's right clicked with an item, this will place the item in the frame,
                // which is handled as a "place" event, with the placed item as the block material
                BlockState blockState = BlockStateUtils.cloneWithMaterial(itemFrame.getLocation().getBlock(),
                                                                          event.getPlayer().getItemInHand().getData());
                this.handleHangingPlace(event, blockState, event.getPlayer());
            }
        }
    }

    private void handleUse(Event event, BlockState blockState, MatchPlayer player) {
        if(!player.canInteract()) return;

        PlayerBlockEventQuery query = new PlayerBlockEventQuery(player, event, blockState);

        for(EventRule rule : this.ruleContext.get(EventRuleScope.USE)) {
            if(rule.region().contains(blockState)) {
                if(processQuery(rule, query)) {
                    if(query.getEvent() instanceof PlayerInteractEvent && ((PlayerInteractEvent) query.getEvent()).isCancelled()) {
                        PlayerInteractEvent pie = (PlayerInteractEvent) query.getEvent();
                        pie.setCancelled(false);
                        pie.setUseItemInHand(Event.Result.ALLOW);
                        pie.setUseInteractedBlock(Event.Result.DENY);

                        if(rule.message() != null) {
                            player.sendWarning(rule.message(), false);
                        }
                    }
                    if(this.useRegionPriority) {
                        break;
                    }
                }
            }
        }
    }

    private void handleHangingPlace(Event event, BlockState blockState, Entity placer) {
        IEventQuery query = makeBlockQuery(event, placer, blockState);

        for(EventRule rule : this.ruleContext.get(EventRuleScope.BLOCK_PLACE)) {
            if(rule.region().contains(blockState)) {
                if(processQuery(rule, query)) {
                    sendCancelMessage(rule, query);
                    if(this.useRegionPriority) break;
                }
            }
        }
    }

    private void handleHangingBreak(Event event, Hanging hanging, Entity breaker) {
        BlockState blockState = getHangingBlockState(hanging);
        if(blockState == null) return;

        IEventQuery query = makeBlockQuery(event, breaker, blockState);

        for(EventRule rule : this.ruleContext.get(EventRuleScope.BLOCK_BREAK)) {
            if(rule.region().contains(blockState)) {
                if(processQuery(rule, query)) {
                    sendCancelMessage(rule, query);
                    if(this.useRegionPriority) break;
                }
            }
        }
    }

    private void sendCancelMessage(EventRule rule, IEventQuery query) {
        if(rule.message() != null &&
           query.getEvent() instanceof Cancellable &&
           ((Cancellable) query.getEvent()).isCancelled() &&
           query instanceof IPlayerQuery) {

            MatchPlayer player = getMatch().getPlayer(((IPlayerQuery) query).getPlayerId());
            if(player != null) player.sendWarning(rule.message(), false);
        }
    }

    private IEventQuery makeBlockQuery(Event event, Entity entity, BlockState block) {
        if(entity instanceof Player) {
            MatchPlayer player = this.match.getPlayer((Player) entity);
            if(MatchPlayers.canInteract(player)) {
                return new PlayerBlockEventQuery(player, event, block);
            }
        }
        return new BlockEventQuery(event, block);
    }

    private Optional<ParticipantState> getActor(BlockTransformEvent event) {
        // Legacy maps assume that all TNT damage is done by "world"
        if(getMatch().getMapInfo().proto.isOlderThan(ProtoVersions.FILTER_OWNED_TNT) &&
           event.getCause() instanceof EntityExplodeEvent) {
            return Optional.empty();
        }

        return Optional.ofNullable(ParticipantBlockTransformEvent.getPlayerState(event));
    }

    private static BlockState getHangingBlockState(Hanging hanging) {
        Block block = hanging.getLocation().getBlock();
        Material type = getHangingType(hanging);
        return type == null ? null : BlockStateUtils.cloneWithMaterial(block, type);
    }

    private static Material getHangingType(Hanging hanging) {
        if(hanging instanceof Painting) {
            return Material.PAINTING;
        } else if(hanging instanceof ItemFrame) {
            return Material.ITEM_FRAME;
        } else if(hanging instanceof LeashHitch) {
            return Material.LEASH;
        } else {
            return null;
        }
    }

    protected static boolean processQuery(EventRule rule, IEventQuery query) {
        return processQuery(query.getEvent(), rule, query);
    }

    /**
     * Query the rule's filter with the given objects.
     * If the query is denied, cancel the event and set the deny message.
     * If the query is allowed, un-cancel the event.
     * If the query abstains, do nothing.
     * @return false if the query abstained, otherwise true
     */
    protected static boolean processQuery(Event event, EventRule rule, IQuery query) {
        if(rule.filter() == null) {
            return false;
        }

        switch(rule.filter().query(query)) {
            case ALLOW:
                if(event instanceof Cancellable) {
                    ((Cancellable) event).setCancelled(false);
                }
                return true;

            case DENY:
                if(event instanceof GeneralizingEvent) {
                    ((GeneralizingEvent) event).setCancelled(true, rule.message());
                } else if(event instanceof Cancellable) {
                    ((Cancellable) event).setCancelled(true);
                }
                return true;

            default:
                return false;
        }
    }
}