package tc.oc.pgm.flag.state;

import javax.annotation.Nullable;

import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.Effect;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Projectile;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import tc.oc.commons.bukkit.util.BlockStateUtils;
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.pgm.PGM;
import tc.oc.pgm.events.BlockTransformEvent;
import tc.oc.pgm.flag.Flag;
import tc.oc.pgm.flag.Post;
import tc.oc.pgm.flag.event.FlagPickupEvent;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.Party;

/**
 * Base class for flag states in which the banner is placed
 * on the ground somewhere as a block
 */
public abstract class Uncarried extends Spawned {

    protected final Location location;
    protected final BlockState oldBlock;
    protected final BlockState oldBase;
    protected ArmorStand labelEntity;
    private @Nullable MatchPlayer pickingUp;

    public Uncarried(Flag flag, Post post, @Nullable Location location) {
        super(flag, post);
        if(location == null) location = flag.getReturnPoint(post);
        this.location = new Location(location.getWorld(),
                                     location.getBlockX() + 0.5,
                                     location.getBlockY(),
                                     location.getBlockZ() + 0.5,
                                     location.getYaw(),
                                     location.getPitch());

        if(!flag.getMatch().getWorld().equals(this.location.getWorld())) {
            throw new IllegalStateException("Tried to place flag in the wrong world");
        }

        Block block = this.location.getBlock();
        if(block.getType() == Material.STANDING_BANNER) {
            // Banner may already be here at match start
            this.oldBlock = BlockStateUtils.cloneWithMaterial(block, Material.AIR);
        } else {
            this.oldBlock = block.getState();
        }
        this.oldBase = block.getRelative(BlockFace.DOWN).getState();
    }

    @Override
    public Location getLocation() {
        return this.location;
    }

    protected void placeBanner() {
        if(!this.flag.canDropOn(oldBase)) {
            oldBase.getBlock().setType(Material.SEA_LANTERN, false);
        } else if(Materials.isWater(oldBase.getType())) {
            oldBase.getBlock().setType(Material.ICE, false);
        }

        if(!Banners.placeStanding(this.location, this.flag.getBannerMeta())) {
            PGM.get().getRootMapLogger().severe("Failed to place flag at " + location);
            forceRecover();
            return;
        }

        this.labelEntity = this.location.getWorld().spawn(this.location.clone().add(0, 0.7, 0), ArmorStand.class);
        this.labelEntity.setVisible(false);
        this.labelEntity.setGravity(false);
        this.labelEntity.setRemoveWhenFarAway(false);
        this.labelEntity.setSmall(true);
        this.labelEntity.setArms(false);
        this.labelEntity.setBasePlate(false);
        this.labelEntity.setCustomName(this.flag.getColoredName());
        this.labelEntity.setCustomNameVisible(true);
        NMSHacks.enableArmorSlots(this.labelEntity, false);
    }

    protected void breakBanner() {
        this.labelEntity.remove();
        oldBase.update(true, false);
        oldBlock.update(true, false);
    }

    @Override
    public void enterState() {
        super.enterState();
        this.placeBanner();
        this.flag.playFlareEffect();
    }

    @Override
    public void leaveState() {
        this.flag.playFlareEffect();
        this.breakBanner();
        super.leaveState();
    }

    @Override
    public boolean isCarrying(MatchPlayer player) {
        // This allows CarryingFlagFilter to match and cancel the pickup before it actually happens
        return player == this.pickingUp || super.isCarrying(player);
    }

    @Override
    public boolean isCarrying(Party party) {
        return (this.pickingUp != null && party == this.pickingUp.getParty()) || super.isCarrying(party);
    }

    protected boolean pickupFlag(MatchPlayer carrier) {
        try {
            this.pickingUp = carrier;
            FlagPickupEvent event = new FlagPickupEvent(this.flag, carrier, this.location);
            this.flag.getMatch().getPluginManager().callEvent(event);
            if(event.isCancelled()) return false;
        }
        finally {
            this.pickingUp = null;
        }

        this.flag.playStatusSound(Flag.PICKUP_SOUND_OWN, Flag.PICKUP_SOUND);
        this.flag.touch(carrier.getParticipantState());

        this.flag.transition(new Carried(this.flag, this.post, carrier, this.location));

        return true;
    }

    protected boolean inPickupRange(Location playerLoc) {
        Location flagLoc = this.getLocation();
        if(playerLoc.getY() < flagLoc.getY() + 2 && playerLoc.getY() >= flagLoc.getY() - 2) {
            double dx = playerLoc.getX() - flagLoc.getX();
            double dz = playerLoc.getZ() - flagLoc.getZ();

            if(dx * dx + dz * dz <= 1) {
                return true;
            }
        }

        return false;
    }

    protected boolean canPickup(MatchPlayer player) {
        if(this.pickingUp != null) return false; // Prevent infinite recursion

        if(flag.getMatch()
               .features()
               .all(Flag.class)
               .anyMatch(flag -> flag.isCarrying(player))) {

            return false;
        }

        return this.flag.canPickup(player, this.post);
    }

    @Override
    public void onEvent(PlayerMoveEvent event) {
        super.onEvent(event);
        MatchPlayer player = this.flag.getMatch().getPlayer(event.getPlayer());
        if(player == null || !player.canInteract() || player.getBukkit().isDead()) return;

        if(this.inPickupRange(player.getBukkit().getLocation()) && this.canPickup(player)) {
            this.pickupFlag(player);
        }
    }

    @Override
    public void onEvent(BlockTransformEvent event) {
        super.onEvent(event);

        Block block = event.getOldState().getBlock();
        Block flagBlock = this.location.getBlock();

        if(block.equals(flagBlock) || block.equals(flagBlock.getRelative(BlockFace.UP))) {
            event.setCancelled(true, new TranslatableComponent("match.flag.cannotBreak"));
        } else if(block.equals(flagBlock.getRelative(BlockFace.DOWN))) {
            event.setCancelled(true, new TranslatableComponent("match.flag.cannotBreakBlockUnder"));
        }
    }

    @Override
    public void onEvent(EntityDamageEvent event) {
        super.onEvent(event);

        if(event.getEntity() == this.labelEntity) {
            event.setCancelled(true);

            if(event instanceof EntityDamageByEntityEvent && ((EntityDamageByEntityEvent) event).getDamager() instanceof Projectile) {
                ((EntityDamageByEntityEvent) event).getDamager().remove();
            }
        }
    }

    @Override
    public void tickLoaded() {
        super.tickLoaded();

        if(this.particleClock % 10 == 0) {
            this.flag.getMatch().getWorld().playEffect(this.getLocation().clone().add(0, 1, 0),
                                                       Effect.PORTAL,
                                                       0, 0,
                                                       0, 0, 0,
                                                       1, 8, 64);
        }
    }
}