package tc.oc.pgm.events;

import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockIgniteEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.ExplosionPrimeByEntityEvent;
import org.bukkit.event.player.PlayerBucketEmptyEvent;
import org.bukkit.event.player.PlayerBucketFillEvent;
import org.bukkit.material.MaterialData;
import tc.oc.commons.bukkit.event.GeneralizingEvent;
import tc.oc.commons.bukkit.util.BlockStateUtils;
import tc.oc.commons.core.reflect.Types;
import tc.oc.pgm.blockdrops.BlockDrops;
import tc.oc.pgm.utils.MaterialPattern;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Event called when a block transforms from one state to another.
 */
public class BlockTransformEvent extends GeneralizingEvent {

    protected final Block block;
    protected final BlockState oldState;
    protected final BlockState newState;
    protected BlockDrops drops;

    protected BlockTransformEvent(Event cause, Block block, BlockState oldState, BlockState newState) {
        super(checkNotNull(cause));
        this.block = checkNotNull(block);
        this.oldState = checkNotNull(oldState);
        this.newState = checkNotNull(newState);
    }

    public BlockTransformEvent(Event cause, BlockState oldState, BlockState newState) {
        this(cause, oldState.getBlock(), oldState, newState);
    }

    public BlockTransformEvent(Event cause, Block block, MaterialData newMaterial) {
        this(cause, block, block.getState(), BlockStateUtils.cloneWithMaterial(block, newMaterial));
    }

    public BlockTransformEvent(Event cause, Block block, Material newMaterial) {
        this(cause, block, block.getState(), BlockStateUtils.cloneWithMaterial(block, newMaterial));
    }

    public World getWorld() {
        return this.oldState.getWorld();
    }

    public Block getBlock() {
        return block;
    }

    public BlockState getOldState() {
        return this.oldState;
    }

    public BlockState getNewState() {
        if(this.drops == null || this.drops.replacement == null) {
            return this.newState;
        } else {
            BlockState state = this.newState.getBlock().getState();
            state.setType(this.drops.replacement.getItemType());
            state.setData(this.drops.replacement);
            return state;
        }
    }

    public boolean changedFrom(Material material) {
        return this.oldState.getType() == material && this.newState.getType() != material;
    }

    public boolean changedFrom(MaterialData material) {
        return this.oldState.getData().equals(material) && !this.newState.getData().equals(material);
    }

    public boolean changedFrom(MaterialPattern material) {
        return material.matches(this.oldState.getData()) && !material.matches(this.newState.getData());
    }

    public boolean changedTo(Material material) {
        return this.oldState.getType() != material && this.newState.getType() == material;
    }

    public boolean changedTo(MaterialData material) {
        return !this.oldState.getData().equals(material) && this.newState.getData().equals(material);
    }

    public boolean changedTo(MaterialPattern material) {
        return !material.matches(this.oldState.getData()) && material.matches(this.newState.getData());
    }

    public BlockDrops getDrops() {
        return drops;
    }

    public void setDrops(BlockDrops drops) {
        this.drops = drops;
    }

    /**
     * Return true if this is a "place" i.e. there is a non-air block here after the transform.
     * Note that a place can also be a break, if the new block replaced an existing one.
     */
    public boolean isPlace() {
        return this.newState.getType() != Material.AIR;
    }

    /**
     * Return true if this is a "break" i.e. there was a non-air block here before the transform.
     * Note that a break can also be a place, if the existing block was replaced by a new one.
     */
    public boolean isBreak() {
        return this.oldState.getType() != Material.AIR;
    }

    /**
     * Return true if the block transformation was performed "by hand".
     *
     * Handled:
     *  - place
     *  - mine
     *  - bucket fill/empty
     *  - flint & steel fire/tnt
     *
     * Not handled:
     *  - bonemeal
     *  - probably lots of other things
     */
    public boolean isManual() {
        final Event event = getCause();

        if(Types.instanceOfAny(
            event,
            BlockPlaceEvent.class,
            BlockBreakEvent.class,
            PlayerBucketEmptyEvent.class,
            PlayerBucketFillEvent.class
        )) return true;

        if(event instanceof BlockIgniteEvent) {
            BlockIgniteEvent igniteEvent = (BlockIgniteEvent) event;
            if(igniteEvent.getCause() == BlockIgniteEvent.IgniteCause.FLINT_AND_STEEL && igniteEvent.getIgnitingEntity() != null) {
                return true;
            }
        }

        if(event instanceof ExplosionPrimeByEntityEvent && ((ExplosionPrimeByEntityEvent) event).getPrimer() instanceof Player) {
            return true;
        }

        return false;
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName() +
               "{pos=" + this.getOldState().getLocation().toVector() +
               " oldState=" + this.getOldState().getData() +
               " newState=" + this.getNewState().getData() +
               " drops=" + this.getDrops() +
               " cancelled=" + this.isCancelled() +
               " cause=" + (this.getCause() == null ? "null" : this.getCause().getEventName()) +
               "}";
    }

    private static final HandlerList handlers = new HandlerList();

    @Override
    public HandlerList getHandlers() {
        return handlers;
    }

    public static HandlerList getHandlerList() {
        return handlers;
    }
}