package org.allaymc.server.block.component.plant;

import it.unimi.dsi.fastutil.longs.Long2LongMap;
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
import org.allaymc.api.block.BlockBehavior;
import org.allaymc.api.block.data.BlockFace;
import org.allaymc.api.block.data.BlockTags;
import org.allaymc.api.block.dto.Block;
import org.allaymc.api.block.property.type.BlockPropertyTypes;
import org.allaymc.api.block.type.BlockState;
import org.allaymc.api.block.type.BlockType;
import org.allaymc.api.block.type.BlockTypes;
import org.allaymc.api.entity.Entity;
import org.allaymc.api.eventbus.event.block.BlockFadeEvent;
import org.allaymc.api.item.ItemStack;
import org.allaymc.api.item.type.ItemType;
import org.allaymc.api.item.type.ItemTypes;
import org.allaymc.server.block.FortuneDropHelper;
import org.allaymc.server.block.component.BlockBaseComponentImpl;
import org.allaymc.server.item.data.ItemId;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Supplier;

import static org.allaymc.api.block.property.type.BlockPropertyTypes.UPDATE_BIT;

/**
 * @author daoge_cmd
 */
public class BlockLeavesBaseComponentImpl extends BlockBaseComponentImpl {

    protected static final BlockFace[] LOG_FINDING_ORDER = new BlockFace[]{
            BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.DOWN, BlockFace.UP
    };

    protected final Supplier<ItemType<?>> saplingType; // Nullable
    protected final boolean canDropApple;
    protected final boolean dropMoreSaplings;

    public BlockLeavesBaseComponentImpl(BlockType<? extends BlockBehavior> blockType, Supplier<ItemType<?>> saplingTypeSupplier, boolean canDropApple, boolean dropMoreSaplings) {
        super(blockType);
        this.saplingType = saplingTypeSupplier;
        this.canDropApple = canDropApple;
        this.dropMoreSaplings = dropMoreSaplings;
    }

    public BlockLeavesBaseComponentImpl(BlockType<? extends BlockBehavior> blockType, ItemId saplingId, boolean canDropApple, boolean dropMoreSaplings) {
        this(blockType, saplingId == null ? null : saplingId::getItemType, canDropApple, dropMoreSaplings);
    }

    protected static long hashBlockXYZ(int x, int y, int z) {
        return ((long) y << 52) + (((long) z & 0x3ffffff) << 26) + ((long) x & 0x3ffffff);
    }

    @Override
    public void onNeighborUpdate(Block block, Block neighbor, BlockFace face, BlockState oldNeighborState) {
        super.onNeighborUpdate(block, neighbor, face, oldNeighborState);
        onNeighborOrScheduledUpdate(block);
    }

    @Override
    public void onScheduledUpdate(Block block) {
        onNeighborOrScheduledUpdate(block);
    }

    protected void onNeighborOrScheduledUpdate(Block current) {
        var pos = current.getPosition();
        if (!current.getPropertyValue(UPDATE_BIT)) {
            current = current.setPropertyValue(UPDATE_BIT, true);
            pos.dimension().setBlockState(pos, current.getBlockState(), 0, true, false, false);
        }

        // Slowly propagates the need to update instead of peaking down the TPS for huge trees
        for (var face : BlockFace.VALUES) {
            var sideBlockState = current.offsetPos(face);
            if (sideBlockState.getBlockType().hasBlockTag(BlockTags.LEAVES)) {
                if (!sideBlockState.getPropertyValue(UPDATE_BIT)) {
                    pos.dimension().getBlockUpdateManager().scheduleBlockUpdateInDelay(sideBlockState.getPosition(), 2);
                }
            }
        }
    }

    @Override
    public void onRandomUpdate(Block block) {
        super.onRandomUpdate(block);
        if (!block.getPropertyValue(UPDATE_BIT)) {
            return;
        }

        var pos = block.getPosition();
        if (block.getPropertyValue(BlockPropertyTypes.PERSISTENT_BIT) || findLog(block, 7, null)) {
            block = block.setPropertyValue(UPDATE_BIT, false);
            pos.dimension().setBlockState(pos, block.getBlockState(), 0, true, false, false);
        } else {
            if (new BlockFadeEvent(block, BlockTypes.AIR.getDefaultState()).call()) {
                block.breakBlock();
            }
        }
    }

    protected boolean findLog(Block current, int distance, Long2LongMap visited) {
        if (visited == null) {
            visited = new Long2LongOpenHashMap();
            visited.defaultReturnValue(-1);
        }

        if (current.getBlockType().hasBlockTag(BlockTags.LEAVES_SUPPORTER)) {
            return true;
        }
        if (distance == 0 || !current.getBlockType().hasBlockTag(BlockTags.LEAVES)) {
            return false;
        }

        var pos = current.getPosition();
        long hash = hashBlockXYZ(pos.x(), pos.y(), pos.z());
        if (visited.get(hash) >= distance) {
            return false;
        }

        visited.put(hash, distance);
        for (BlockFace face : LOG_FINDING_ORDER) {
            var nextBlockStateWithPos = current.offsetPos(face);
            if (findLog(nextBlockStateWithPos, distance - 1, visited)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean canRandomUpdate() {
        return true;
    }

    @Override
    public Set<ItemStack> getDrops(Block block, ItemStack usedItem, Entity entity) {
        if (usedItem != null && usedItem.getItemType() == ItemTypes.SHEARS) {
            return super.getDrops(block, usedItem, entity);
        }

        Set<ItemStack> drops = new HashSet<>();
        if (canDropApple && FortuneDropHelper.bonusChanceDivisor(usedItem, 200, 20)) {
            drops.add(ItemTypes.APPLE.createItemStack());
        }

        if (FortuneDropHelper.bonusChanceDivisor(usedItem, 50, 5)) {
            drops.add(ItemTypes.STICK.createItemStack(ThreadLocalRandom.current().nextInt(1, 3)));
        }

        var saplingItemType = saplingType == null ? null : saplingType.get();
        if (saplingItemType != null && FortuneDropHelper.bonusChanceDivisor(usedItem, dropMoreSaplings ? 40 : 20, 4)) {
            drops.add(saplingItemType.createItemStack());
        }

        return drops;
    }
}
