package tc.oc.pgm.flag.state;

import javax.annotation.Nullable;
import net.kyori.text.TranslatableComponent;
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.pgm.api.event.BlockTransformEvent;
import tc.oc.pgm.api.party.Party;
import tc.oc.pgm.api.player.MatchPlayer;
import tc.oc.pgm.flag.Flag;
import tc.oc.pgm.flag.FlagMatchModule;
import tc.oc.pgm.flag.Post;
import tc.oc.pgm.flag.event.FlagPickupEvent;
import tc.oc.pgm.util.block.BlockStates;
import tc.oc.pgm.util.material.Materials;

/** 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());

    Block block = this.location.getBlock();
    if (block.getType() == Material.STANDING_BANNER) {
      // Banner may already be here at match start
      this.oldBlock = BlockStates.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);
    }

    Materials.placeStanding(this.location, this.flag.getBannerMeta());

    this.labelEntity =
        this.location.getWorld().spawn(this.location.clone().add(0, 1.7, 0), ArmorStand.class);
    this.labelEntity.setVisible(false);
    this.labelEntity.setMarker(true);
    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);
  }

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

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

  @Override
  public void leaveState() {
    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().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

    for (Flag flag : this.flag.getMatch().getModule(FlagMatchModule.class).getFlags()) {
      if (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, TranslatableComponent.of("flag.cannotBreakFlag"));
    } else if (block.equals(flagBlock.getRelative(BlockFace.DOWN))) {
      event.setCancelled(true, TranslatableComponent.of("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();
      }
    }
  }
}