package tc.oc.pgm.tracker.trackers;

import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;

import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityExplodeEvent;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.events.ParticipantBlockTransformEvent;
import tc.oc.pgm.tracker.EntityResolver;
import tc.oc.pgm.tracker.damage.DamageInfo;
import tc.oc.pgm.tracker.damage.ExplosionInfo;
import tc.oc.pgm.tracker.damage.PhysicalInfo;
import tc.oc.pgm.tracker.damage.PlayerInfo;
import tc.oc.pgm.tracker.damage.SpleefInfo;
import tc.oc.pgm.tracker.event.PlayerSpleefEvent;
import tc.oc.commons.bukkit.util.Materials;

/**
 * Tracks blocks broken by players and fires a {@link PlayerSpleefEvent}
 * when it appears to cause a player to leave the ground.
 */
@ListenerScope(MatchScope.RUNNING)
public class SpleefTracker implements Listener {

    private static final float PLAYER_WIDTH = 0.6f;
    private static final float PLAYER_RADIUS = PLAYER_WIDTH / 2.0f;

    // A player must leave the ground within this many ticks of a block being broken
    // under them for the fall to be caused by a spleef from that block
    public static final long MAX_SPLEEF_TICKS = 20;

    private final EntityResolver entityResolver;
    private final Match match;
    private final Map<Block, SpleefInfo> brokenBlocks = new HashMap<>();

    @Inject SpleefTracker(EntityResolver entityResolver, Match match) {
        this.entityResolver = entityResolver;
        this.match = match;
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void onBlockBreak(final ParticipantBlockTransformEvent event) {
        if(!event.isBreak()) return;
        if(!Materials.isColliding(event.getOldState())) return;

        final Block block = event.getBlock();
        DamageInfo breaker = null;

        if(event.getCause() instanceof EntityExplodeEvent) {
            PhysicalInfo explosive = entityResolver.resolveInfo(((EntityExplodeEvent) event.getCause()).getEntity(),
                                                                PhysicalInfo.class);
            if(explosive != null) {
                breaker = new ExplosionInfo(explosive);
            }
        }

        if(breaker == null) {
            breaker = new PlayerInfo(event.getPlayerState());
        }

        final SpleefInfo info = new SpleefInfo(breaker, match.getClock().now());
        brokenBlocks.put(block, info);

        match.getScheduler(MatchScope.RUNNING).createDelayedTask(MAX_SPLEEF_TICKS + 1, () -> {
            // Only remove the BrokenBlock if it's the same one we added. It may have been replaced since then.
            if(info == brokenBlocks.get(block)) {
                brokenBlocks.remove(block);
            }
        });
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void onPlayerOnGroundChanged(final PlayerOnGroundEvent event) {
        MatchPlayer player = match.getParticipant(event.getPlayer());
        if(player == null) return;

        Block block = this.lastBlockBrokenUnderPlayer(player);
        if(block != null) {
            SpleefInfo info = brokenBlocks.get(block);
            if(match.getClock().now().tick - info.getTime().tick <= MAX_SPLEEF_TICKS) {
                match.callEvent(new PlayerSpleefEvent(player, block, info));
            }
        }
    }

    public Block lastBlockBrokenUnderPlayer(MatchPlayer player) {
        Location playerLocation = player.getBukkit().getLocation();

        int y = (int) Math.floor(playerLocation.getY() - 0.1);

        int x1 = (int) Math.floor(playerLocation.getX() - PLAYER_RADIUS);
        int z1 = (int) Math.floor(playerLocation.getZ() - PLAYER_RADIUS);

        int x2 = (int) Math.floor(playerLocation.getX() + PLAYER_RADIUS);
        int z2 = (int) Math.floor(playerLocation.getZ() + PLAYER_RADIUS);

        long latestTick = Long.MIN_VALUE;
        Block latestBlock = null;

        for(int x = x1; x <= x2; ++x) {
            for(int z = z1; z <= z2; ++z) {
                Block block = playerLocation.getBlock();
                SpleefInfo info = this.brokenBlocks.get(block);
                if(info != null) {
                    long tick = info.getTime().tick;
                    if(tick > latestTick) {
                        latestTick = tick;
                        latestBlock = block;
                    }
                }
            }
        }

        return latestBlock;
    }
}
