package com.minelittlepony.unicopia.block;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jetbrains.annotations.Nullable;

import com.minelittlepony.unicopia.EquineContext;
import com.minelittlepony.unicopia.EquinePredicates;
import com.minelittlepony.unicopia.Race;
import com.minelittlepony.unicopia.USounds;
import com.minelittlepony.unicopia.entity.player.Pony;
import com.minelittlepony.unicopia.particle.ParticleUtils;
import com.minelittlepony.unicopia.util.NbtSerialisable;
import com.minelittlepony.unicopia.util.NbtSerialisable.Serializer;
import com.minelittlepony.unicopia.util.PosHelper;

import net.minecraft.block.Block;
import net.minecraft.block.BlockEntityProvider;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.block.ConnectingBlock;
import net.minecraft.block.ShapeContext;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.BlockEntityTicker;
import net.minecraft.block.entity.BlockEntityType;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtElement;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundCategory;
import net.minecraft.state.StateManager;
import net.minecraft.state.property.BooleanProperty;
import net.minecraft.state.property.Property;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.math.random.Random;
import net.minecraft.util.shape.VoxelShape;
import net.minecraft.util.shape.VoxelShapes;
import net.minecraft.world.BlockView;
import net.minecraft.world.World;
import net.minecraft.world.WorldAccess;
import net.minecraft.world.event.BlockPositionSource;
import net.minecraft.world.event.GameEvent;
import net.minecraft.world.event.GameEvent.Emitter;
import net.minecraft.world.event.PositionSource;
import net.minecraft.world.event.listener.GameEventListener;

public class HiveBlock extends ConnectingBlock implements BlockEntityProvider {
    static final BooleanProperty AWAKE = BooleanProperty.of("awake");
    static final BooleanProperty CONSUMING = BooleanProperty.of("consuming");
    static final Collection<BooleanProperty> PROPERTIES = FACING_PROPERTIES.values();

    public HiveBlock(Settings settings) {
        super(0.5F, settings);
        setDefaultState(getDefaultState().with(AWAKE, false).with(CONSUMING, false));
        PROPERTIES.forEach(property -> {
            setDefaultState(getDefaultState().with(property, true));
        });
    }

    @Override
    protected void appendProperties(StateManager.Builder<Block, BlockState> builder) {
        builder.add(PROPERTIES.toArray(Property[]::new));
        builder.add(AWAKE, CONSUMING);
    }

    @Override
    public void randomDisplayTick(BlockState state, World world, BlockPos pos, Random random) {
        super.randomDisplayTick(state, world, pos, random);

        if (random.nextInt(250) == 0) {
            world.playSoundAtBlockCenter(pos, USounds.BLOCK_CHITIN_AMBIENCE, SoundCategory.BLOCKS, 0.13F, 0.2F, true);

            for (int i = 0; i < 9; i++) {
                world.addParticle(random.nextInt(2) == 0 ? ParticleTypes.SPORE_BLOSSOM_AIR : ParticleTypes.CRIMSON_SPORE,
                        pos.getX() + random.nextDouble(),
                        pos.getY() + 1.1, pos.getZ() + random.nextDouble(),
                        random.nextDouble() - 0.5, 0, random.nextDouble() - 0.5
                );
            }
        }
    }

    @Deprecated
    @Override
    public BlockState getStateForNeighborUpdate(BlockState state, Direction direction, BlockState neighborState, WorldAccess world, BlockPos pos, BlockPos neighborPos) {
        if (state.get(CONSUMING)) {
            return state;
        }
        boolean connected = !neighborState.isAir();
        state = state.with(FACING_PROPERTIES.get(direction), connected);

        if (!connected) {
            return state.with(AWAKE, true);
        }

        return state;
    }

    @Override
    public void scheduledTick(BlockState state, ServerWorld world, BlockPos pos, Random random) {
        if (state.get(CONSUMING) || !state.get(AWAKE)) {
            return;
        }

        for (var property : FACING_PROPERTIES.entrySet()) {
            if (!state.get(property.getValue())) {
                BlockPos neighborPos = pos.offset(property.getKey());
                //world.setBlockState(neighborPos, getDefaultState());
                world.setBlockState(neighborPos, UBlocks.CHITIN.getDefaultState());

                world.playSound(null, neighborPos, USounds.Vanilla.BLOCK_SLIME_BLOCK_PLACE, SoundCategory.BLOCKS);
                world.playSound(null, neighborPos, USounds.BLOCK_CHITIN_AMBIENCE, SoundCategory.BLOCKS, 0.13F, 0.2F);

                for (int i = 0; i < 9; i++) {
                    ParticleUtils.spawnParticle(world, random.nextInt(2) == 0 ? ParticleTypes.SPORE_BLOSSOM_AIR : ParticleTypes.CRIMSON_SPORE,
                            neighborPos.getX() + random.nextDouble(),
                            neighborPos.getY() + 1.1, pos.getZ() + random.nextDouble(),
                            random.nextDouble() - 0.5, 0, random.nextDouble() - 0.5
                    );
                }
            }
        }
    }

    @Override
    public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {
        if (EquineContext.of(player).getCompositeRace().includes(Race.CHANGELING)) {
            world.setBlockState(pos, state.with(CONSUMING, true));
            if (!world.isClient) {
                BlockEntityUtil.getOrCreateBlockEntity(world, pos, UBlockEntities.HIVE_STORAGE).ifPresent(data -> {

                    if (data.opening) {
                        data.opening = false;
                        data.closing = true;
                    } else {
                        data.opening = true;
                        data.closing = false;
                    }
                    data.tickNow = true;
                    data.markDirty();
                });
            }
            return ActionResult.SUCCESS;
        }
        return ActionResult.PASS;
    }

    @Deprecated
    @Override
    public void neighborUpdate(BlockState state, World world, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) {
        world.scheduleBlockTick(pos, this, 15);
        super.neighborUpdate(state, world, pos, sourceBlock, sourcePos, notify);
    }

    @Deprecated
    @Override
    public VoxelShape getCollisionShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) {
        return EquineContext.of(context).getSpecies() == Race.CHANGELING ? VoxelShapes.empty() : super.getCollisionShape(state, world, pos, context);
    }

    @Deprecated
    @Override
    public float calcBlockBreakingDelta(BlockState state, PlayerEntity player, BlockView world, BlockPos pos) {
        float delta = super.calcBlockBreakingDelta(state, player, world, pos);
        delta *= Pony.of(player).getSpecies() == Race.CHANGELING ? 2 : 1;
        return delta;
    }

    @Nullable
    @Override
    public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
        return state.get(CONSUMING) ? new TileData(pos, state) : null;
    }

    @Override
    public <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) {
        if (!state.get(CONSUMING)) {
            return null;
        }
        return BlockEntityUtil.checkType(type, UBlockEntities.HIVE_STORAGE, TileData::tick);
    }

    static class TileData extends BlockEntity implements GameEventListener.Holder<TileData.Listener> {
        private final Map<BlockPos, Entry> storedBlocks = new HashMap<>();
        private final List<Set<BlockPos>> lastConsumed = new ArrayList<>();
        private boolean opening;
        private boolean closing;
        private boolean tickNow;
        private long lastTick;
        private final Listener listener;

        public TileData(BlockPos pos, BlockState state) {
            super(UBlockEntities.HIVE_STORAGE, pos, state);
            listener = new Listener(pos);
        }

        @Override
        public void readNbt(NbtCompound nbt) {
            opening = nbt.getBoolean("opening");
            closing = nbt.getBoolean("closing");
            storedBlocks.clear();
            if (nbt.contains("storedBlocks", NbtElement.LIST_TYPE)) {
                Entry.SERIALIZER.readAll(nbt.getList("storedBlocks", NbtElement.COMPOUND_TYPE)).forEach(entry -> {
                    storedBlocks.put(entry.pos(), entry);
                });
            }
        }

        @Override
        protected void writeNbt(NbtCompound nbt) {
            nbt.putBoolean("opening", opening);
            nbt.putBoolean("closing", closing);
            nbt.put("storedBlocks", Entry.SERIALIZER.writeAll(storedBlocks.values()));
        }

        static void tick(World world, BlockPos pos, BlockState state, TileData data) {
            if (data.tickNow || (world.getTime() > data.lastTick + 2)) {
                data.tickNow = false;
                data.lastTick = world.getTime();
                if (data.closing) {
                    data.stepClosing(world, pos);

                    if (data.lastConsumed.isEmpty() && state.get(CONSUMING)) {
                        world.setBlockState(pos, state.with(CONSUMING, false));
                    }
                } else if (data.opening) {
                    data.stepOpening(world, pos);
                }
            }
        }

        private void stepOpening(World world, BlockPos pos) {
            if (lastConsumed.size() >= 4) {
                return;
            }

            Set<BlockPos> consumed = new HashSet<>();
            for (BlockPos neighbor : lastConsumed.isEmpty() ? Set.of(pos) : lastConsumed.get(lastConsumed.size() - 1)) {
                if (!(neighbor.equals(pos) || (storedBlocks.containsKey(neighbor) && storedBlocks.get(neighbor).state().isOf(UBlocks.CHITIN)))) {
                    continue;
                }
                PosHelper.adjacentNeighbours(neighbor).forEach(adjacent -> {
                    BlockState s = world.getBlockState(adjacent);

                    if (consumed.add(adjacent.toImmutable()) && !storedBlocks.containsKey(adjacent)) {
                        BlockEntity data = world.getBlockEntity(adjacent);
                        storedBlocks.put(adjacent.toImmutable(), new Entry(adjacent.toImmutable(), s, data instanceof TileData ? null : data));

                        if (s.isOf(UBlocks.CHITIN)) {
                            world.breakBlock(adjacent, false);
                        } else if (s.isOf(UBlocks.HIVE)) {
                            world.setBlockState(adjacent, s.with(CONSUMING, true));
                            TileData next = BlockEntityUtil.getOrCreateBlockEntity(world, adjacent.toImmutable(), UBlockEntities.HIVE_STORAGE).orElse(null);
                            if (next != null) {
                                next.opening = opening;
                                next.closing = closing;
                                next.tickNow = true;
                                next.markDirty();
                            }
                        }
                    }
                });
            }
            lastConsumed.add(consumed);
            markDirty();
        }

        private void stepClosing(World world, BlockPos pos) {
            if (lastConsumed.isEmpty()) {
                closing = false;
                markDirty();
                return;
            }

            for (BlockPos neighbor : lastConsumed.remove(lastConsumed.size() - 1)) {
                @Nullable
                Entry entry = storedBlocks.remove(neighbor);
                if (entry != null && !entry.state().isAir()) {
                    if (world.getBlockState(entry.pos()).isOf(UBlocks.HIVE)) {
                        BlockEntityUtil.getOrCreateBlockEntity(world, entry.pos(), UBlockEntities.HIVE_STORAGE).ifPresent(data -> {
                            data.closing = closing;
                            data.opening = opening;
                            data.tickNow = true;
                            data.markDirty();
                        });
                    } else {
                        entry.restore(world);
                    }
                }
            }

            markDirty();
        }

        record Entry (BlockPos pos, BlockState state, @Nullable BlockEntity data) {
            public static final Serializer<Entry> SERIALIZER = Serializer.of(compound -> new Entry(
                NbtSerialisable.BLOCK_POS.read(compound.getCompound("pos")),
                NbtSerialisable.decode(BlockState.CODEC, compound.get("state")).orElse(Blocks.AIR.getDefaultState()),
                compound.getCompound("data")
            ), entry -> {
                NbtCompound compound = new NbtCompound();
                compound.put("pos", NbtSerialisable.BLOCK_POS.write(entry.pos()));
                compound.put("state", NbtSerialisable.encode(BlockState.CODEC, entry.state()));
                if (entry.data() != null) {
                    compound.put("data", entry.data().createNbtWithId());
                }
                return compound;
            });

            Entry(BlockPos pos, BlockState state, NbtCompound nbt) {
                this(pos, state, BlockEntity.createFromNbt(pos, state, nbt));
            }

            @SuppressWarnings("deprecation")
            public void restore(World world) {
                if (world.isAir(pos)) {
                    world.setBlockState(pos, state);
                    if (data != null) {
                        data.setCachedState(state);
                        world.addBlockEntity(data);
                    }
                }
            }
        }

        @Override
        public Listener getEventListener() {
            return listener;
        }

        class Listener implements GameEventListener {
            private final PositionSource position;

            Listener(BlockPos pos) {
                this.position = new BlockPositionSource(pos);
            }

            @Override
            public PositionSource getPositionSource() {
                return position;
            }

            @Override
            public int getRange() {
                return 15;
            }

            @Override
            public boolean listen(ServerWorld world, GameEvent event, Emitter emitter, Vec3d emitterPos) {
                if (isImportant(event) || (EquinePredicates.IS_PLAYER.test(emitter.sourceEntity()) && !EquinePredicates.CHANGELING.test(emitter.sourceEntity()))) {
                    closing = true;
                    markDirty();
                    return true;
                }
                return false;
            }

            private boolean isImportant(GameEvent event) {
                return event == GameEvent.EXPLODE
                    || event == GameEvent.ENTITY_ROAR
                    || event == GameEvent.SHRIEK;
            }
        }
    }
}
