package fi.dy.masa.enderutilities.util;

import java.lang.invoke.MethodHandle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Optional;
import net.minecraft.block.Block;
import net.minecraft.block.SoundType;
import net.minecraft.block.properties.IProperty;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.init.Blocks;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.server.management.PlayerInteractionManager;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.SoundCategory;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.fml.common.registry.ForgeRegistries;
import net.minecraftforge.items.CapabilityItemHandler;
import net.minecraftforge.items.IItemHandler;
import fi.dy.masa.enderutilities.EnderUtilities;
import fi.dy.masa.enderutilities.event.EntityEventHandler;
import fi.dy.masa.enderutilities.util.MethodHandleUtils.UnableToFindMethodHandleException;

public class BlockUtils
{
    public static final Pattern PATTERN_BLOCK_STATE_STRING = Pattern.compile("(?<name>([a-z0-9_]+:)?[a-z0-9\\._]+)\\[(?<props>[a-z0-9_]+=[a-z0-9_]+(,[a-z0-9_]+=[a-z0-9_]+)*)\\]");
    private static MethodHandle methodHandle_Block_getSilkTouchDrop;

    static
    {
        try
        {
            methodHandle_Block_getSilkTouchDrop = MethodHandleUtils.getMethodHandleVirtual(
                    Block.class, new String[] { "func_180643_i", "getSilkTouchDrop" }, IBlockState.class);
        }
        catch (UnableToFindMethodHandleException e)
        {
            EnderUtilities.logger.error("BlockUtils: Failed to get a MethodHandle for Block#getSilkTouchDrop()", e);
        }
    }

    public static Set<IBlockState> getMatchingBlockStatesForString(String blockStateString)
    {
        Set<IBlockState> validStates = new HashSet<>();
        ResourceLocation air = new ResourceLocation("minecraft:air");
        int index = blockStateString.indexOf('[');
        String name = index > 0 ? blockStateString.substring(0, index) : blockStateString;
        ResourceLocation key = new ResourceLocation(name);
        Block block = ForgeRegistries.BLOCKS.getValue(key);

        if (block != null && (block != Blocks.AIR || key.equals(air)))
        {
            // First get all valid states for this block
            Collection<IBlockState> statesTmp = block.getBlockState().getValidStates();
            // Then get the list of properties and their values in the given name (if any)
            List<Pair<String, String>> props = getBlockStatePropertiesFromString(blockStateString);

            // ... and then filter the list of all valid states by the provided properties and their values
            if (props.isEmpty() == false)
            {
                for (Pair<String, String> pair : props)
                {
                    statesTmp = getFilteredStates(statesTmp, pair.getLeft(), pair.getRight());
                }
            }

            validStates.addAll(statesTmp);
        }
        else
        {
            EnderUtilities.logger.warn("BlockUtils.getMatchingBlockStatesForString(): Invalid block state string '{}'", blockStateString);
        }

        return validStates;
    }

    public static List<Pair<String, String>> getBlockStatePropertiesFromString(String blockStateString)
    {
        Matcher matcherNameProps = PATTERN_BLOCK_STATE_STRING.matcher(blockStateString);

        if (matcherNameProps.matches())
        {
            List<Pair<String, String>> props = new ArrayList<>();
            // name[props]
            //String name = matcherNameProps.group("name");
            String propStr = matcherNameProps.group("props");
            String[] propParts = propStr.split(",");
            Pattern patternProp = Pattern.compile("(?<prop>[a-zA-Z0-9\\._-]+)=(?<value>[a-zA-Z0-9\\._-]+)");

            for (int i = 0; i < propParts.length; i++)
            {
                Matcher matcherProp = patternProp.matcher(propParts[i]);

                if (matcherProp.matches())
                {
                    props.add(Pair.of(matcherProp.group("prop"), matcherProp.group("value")));
                }
                else
                {
                    EnderUtilities.logger.warn("BlockUtils.getBlockStatePropertiesFromString(): Invalid block property '{}'", propParts[i]);
                }
            }

            Collections.sort(props); // the properties need to be in alphabetical order
            //System.out.printf("name: %s, props: %s (propStr: %s)\n", name, String.join(",", props), propStr);
            return props;
        }

        return Collections.emptyList();
    }

    public static <T extends Comparable<T>> List<IBlockState> getFilteredStates(Collection<IBlockState> initialStates, String propName, String propValue)
    {
        List<IBlockState> list = new ArrayList<>();

        for (IBlockState state : initialStates)
        {
            @SuppressWarnings("unchecked")
            IProperty<T> prop = (IProperty<T>) state.getBlock().getBlockState().getProperty(propName);

            if (prop != null)
            {
                Optional<T> value = prop.parseValue(propValue);

                if (value.isPresent() && state.getValue(prop).equals(value.get()))
                {
                    list.add(state);
                }
            }
        }

        return list;
    }

    /**
     * Breaks the block as a player, and thus drops the item(s) from it
     */
    public static void breakBlockAsPlayer(World world, BlockPos pos, EntityPlayerMP playerMP, ItemStack toolStack)
    {
        PlayerInteractionManager manager = playerMP.interactionManager;
        int exp = ForgeHooks.onBlockBreakEvent(world, manager.getGameType(), playerMP, pos);

        if (exp != -1)
        {
            IBlockState stateExisting = world.getBlockState(pos);
            Block blockExisting = stateExisting.getBlock();

            blockExisting.onBlockHarvested(world, pos, stateExisting, playerMP);
            boolean harvest = blockExisting.removedByPlayer(stateExisting, world, pos, playerMP, true);

            if (harvest)
            {
                blockExisting.onPlayerDestroy(world, pos, stateExisting);
                blockExisting.harvestBlock(world, playerMP, pos, stateExisting, world.getTileEntity(pos), toolStack);
            }
        }
    }

    /**
     * Checks if the player is allowed to change this block. Creative mode players are always allowed to change blocks.
     */
    public static boolean canChangeBlock(World world, BlockPos pos, EntityPlayer player, boolean allowTileEntities, float maxHardness)
    {
        if (player.capabilities.isCreativeMode)
        {
            return true;
        }

        IBlockState state = world.getBlockState(pos);

        if (state.getBlock().isAir(state, world, pos))
        {
            return true;
        }

        float hardness = state.getBlockHardness(world, pos);

        return world.isBlockModifiable(player, pos) && hardness >= 0 && (hardness <= maxHardness || state.getMaterial().isLiquid()) &&
               (allowTileEntities || world.getTileEntity(pos) == null);
    }

    public static void getDropAndSetToAir(World world, EntityPlayer player, BlockPos pos, EnumFacing side, boolean addToInventory)
    {
        if (player.capabilities.isCreativeMode == false)
        {
            ItemStack stack = BlockUtils.getPickBlockItemStack(world, pos, player, side);

            if (stack.isEmpty() == false)
            {
                if (addToInventory)
                {
                    IItemHandler inv = player.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, null);

                    if (inv != null)
                    {
                        stack = InventoryUtils.tryInsertItemStackToInventory(inv, stack);
                    }
                }

                if (stack.isEmpty() == false)
                {
                    EntityUtils.dropItemStacksInWorld(world, pos, stack, -1, true);
                }
            }
        }

        setBlockToAirWithBreakSound(world, pos);
    }

    public static void setBlockToAirWithBreakSound(World world, BlockPos pos)
    {
        playBlockBreakSound(world, pos);
        world.setBlockToAir(pos);
    }

    public static void playBlockBreakSound(World world, BlockPos pos)
    {
        IBlockState state = world.getBlockState(pos);
        SoundType soundtype = state.getBlock().getSoundType(state, world, pos, null);

        world.playSound(null, pos, soundtype.getBreakSound(), SoundCategory.BLOCKS, soundtype.getVolume(), soundtype.getPitch());
    }

    public static void setBlockToAirWithoutSpillingContents(World world, BlockPos pos)
    {
        setBlockToAirWithoutSpillingContents(world, pos, 3);
    }

    public static void setBlockToAirWithoutSpillingContents(World world, BlockPos pos, int flags)
    {
        EntityEventHandler.setPreventEntitySpawning(true);
        world.restoringBlockSnapshots = true;

        world.setBlockState(pos, Blocks.AIR.getDefaultState(), flags);

        world.restoringBlockSnapshots = false;
        EntityEventHandler.setPreventEntitySpawning(false);
    }

    /**
     * Sets the block state in the world and plays the placement sound.
     * @return true if setting the block state succeeded
     */
    public static boolean setBlockStateWithPlaceSound(World world, BlockPos pos, IBlockState newState, int setBlockStateFlags)
    {
        boolean success = world.setBlockState(pos, newState, setBlockStateFlags);

        if (success)
        {
            SoundType soundtype = newState.getBlock().getSoundType(newState, world, pos, null);
            world.playSound(null, pos, soundtype.getPlaceSound(), SoundCategory.BLOCKS,
                    (soundtype.getVolume() + 1.0F) / 2.0F, soundtype.getPitch() * 0.8F);
        }

        return success;
    }

    public static ItemStack getPickBlockItemStack(World world, BlockPos pos, EntityPlayer player, EnumFacing side)
    {
        return getPickBlockItemStack(world, pos, world.getBlockState(pos), player, side);
    }

    /**
     * Gets a pick-blocked ItemStack for the given IBlockState <b>state</b>.
     * If the block currently in the world is different, the it will be replaced with <b>state</b>
     * for the duration of the pick-block operation.
     */
    public static ItemStack getPickBlockItemStack(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumFacing side)
    {
        IBlockState existingState = world.getBlockState(pos);
        NBTTagCompound nbt = null;
        boolean replaced = false;

        if (existingState.getBlock() != state.getBlock())
        {
            TileEntity te = world.getTileEntity(pos);

            if (te != null)
            {
                nbt = te.writeToNBT(new NBTTagCompound());
                te.onChunkUnload();
            }

            setBlockToAirWithoutSpillingContents(world, pos, 4);
            world.setBlockState(pos, state, 4);

            replaced = true;
        }

        RayTraceResult trace = new RayTraceResult(new Vec3d(pos.getX() + 0.5, pos.getY() + 1, pos.getZ() + 0.5), side, pos);
        ItemStack stack = state.getBlock().getPickBlock(state, trace, world, pos, player);

        if (replaced)
        {
            setBlockToAirWithoutSpillingContents(world, pos, 4);
            world.setBlockState(pos, existingState, 4);

            if (nbt != null)
            {
                TileUtils.createAndAddTileEntity(world, pos, nbt);
            }
        }

        return stack;
    }

    public static ItemStack getSilkTouchDrop(World world, BlockPos pos)
    {
        return getSilkTouchDrop(world.getBlockState(pos));
    }

    public static ItemStack getSilkTouchDrop(IBlockState state)
    {
        Block block = state.getBlock();
        ItemStack stack = ItemStack.EMPTY;

        try
        {
            stack = (ItemStack) methodHandle_Block_getSilkTouchDrop.invokeExact(block, state);
        }
        catch (Throwable t)
        {
            EnderUtilities.logger.warn("Error while trying invoke Block#getSilkTouchDrop() from {} via a MethodHandle", block.getClass().getName(), t);
        }

        return stack;
    }

    /**
     * Check if the given block can be placed in the given position.
     * Note: This method is a functional copy of ItemBlock.func_150936_a() which is client side only.
     */
    public static boolean checkCanPlaceBlockAt(World world, BlockPos pos, EnumFacing side, Block blockNew)
    {
        Block blockExisting = world.getBlockState(pos).getBlock();

        if (blockExisting == Blocks.SNOW_LAYER && blockExisting.isReplaceable(world, pos))
        {
            side = EnumFacing.UP;
        }
        else if (blockExisting.isReplaceable(world, pos) == false)
        {
            pos = pos.offset(side);
        }

        return world.mayPlace(blockNew, pos, false, side, (Entity) null);
    }
}