package com.trunksbomb.minetriad.blockentity;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import com.trunksbomb.minetriad.game.BoardCell;
import com.trunksbomb.minetriad.game.DuelSession;
import com.trunksbomb.minetriad.game.DuelSessionManager;
import com.trunksbomb.minetriad.game.MoveResult;
import com.trunksbomb.minetriad.registry.TriadBlockEntities;
import com.trunksbomb.minetriad.registry.TriadItems;

import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.NonNullList;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.Interaction;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;

import com.trunksbomb.minetriad.game.BoardLocalSpace;
import com.trunksbomb.minetriad.world.DuelTableBlock;

public class DuelTableBlockEntity extends BlockEntity {
    public static final String REWARD_PROXY_TAG = "minetriad.reward_proxy";
    public static final String REWARD_PROXY_POS_PREFIX = "minetriad.reward_pos:";
    public static final String REWARD_PROXY_SLOT_PREFIX = "minetriad.reward_slot:";
    public static final int SLOT_COUNT = 9;
    public static final int REWARD_SLOT_COUNT = 5;
    public static final int OWNER_NONE = 0;
    public static final int OWNER_FIRST = 1;
    public static final int OWNER_SECOND = 2;
    private static final int PLACEMENT_DURATION = 10;
    private static final int CAPTURE_DURATION = 10;
    private static final int OVERVIEW_SWEEP_DURATION = 18;
    private static final int CLEAR_WAVE_DURATION = 10;
    private static final int REWARD_TAKE_DURATION = 14;

    private final NonNullList<ItemStack> boardCards = NonNullList.withSize(SLOT_COUNT, ItemStack.EMPTY);
    private final NonNullList<ItemStack> rewardCards = NonNullList.withSize(REWARD_SLOT_COUNT, ItemStack.EMPTY);
    private final List<RewardTakeAnimation> rewardTakeAnimations = new ArrayList<>();
    private final int[] ownerSlots = new int[SLOT_COUNT];
    private final int[] slotAges = new int[SLOT_COUNT];
    private final ArrayDeque<AnimationStep> pendingAnimations = new ArrayDeque<>();
    private UUID firstParticipantId;
    private UUID secondParticipantId;
    private AnimationType animationType = AnimationType.NONE;
    private int[] animationSlots = new int[0];
    private int animationTargetOwner = OWNER_NONE;
    private int animationAge;
    private int animationDuration;
    private boolean animationOwnerApplied;
    private TableAnimationType tableAnimationType = TableAnimationType.NONE;
    private int tableAnimationAge;
    private int tableAnimationDuration;
    private boolean hudActive;
    private int hudP1HandCount;
    private int hudP2HandCount;
    private int hudP1Score;
    private int hudP2Score;
    private String hudRules = "";
    private String hudGameState = "";
    private String hudTurn = "";
    private String hudInstruction = "";

    public DuelTableBlockEntity(BlockPos pos, BlockState blockState) {
        super(TriadBlockEntities.DUEL_TABLE.get(), pos, blockState);
    }

    public ItemStack getCard(int slot) {
        return boardCards.get(slot);
    }

    public ItemStack getRewardCard(int slot) {
        return slot >= 0 && slot < rewardCards.size() ? rewardCards.get(slot) : ItemStack.EMPTY;
    }

    public int rewardCardCount() {
        int count = 0;
        for (ItemStack rewardCard : rewardCards) {
            if (!rewardCard.isEmpty()) {
                count++;
            }
        }
        return count;
    }

    public List<RewardTakeAnimation> rewardTakeAnimations() {
        return List.copyOf(rewardTakeAnimations);
    }

    public int ownerAt(int slot) {
        return ownerSlots[slot];
    }

    public int slotAge(int slot) {
        return slot >= 0 && slot < slotAges.length ? slotAges[slot] : 0;
    }

    public Optional<UUID> firstParticipantId() {
        return Optional.ofNullable(firstParticipantId);
    }

    public Optional<UUID> secondParticipantId() {
        return Optional.ofNullable(secondParticipantId);
    }

    public boolean hudActive() {
        return hudActive;
    }

    public int hudP1HandCount() {
        return hudP1HandCount;
    }

    public int hudP2HandCount() {
        return hudP2HandCount;
    }

    public int hudP1Score() {
        return hudP1Score;
    }

    public int hudP2Score() {
        return hudP2Score;
    }

    public String hudRules() {
        return hudRules;
    }

    public String hudGameState() {
        return hudGameState;
    }

    public String hudTurn() {
        return hudTurn;
    }

    public String hudInstruction() {
        return hudInstruction;
    }

    public boolean hasActiveAnimation() {
        return animationType != AnimationType.NONE || tableAnimationType != TableAnimationType.NONE;
    }

    public String animationTypeName() {
        return animationType.name();
    }

    public boolean isPlacementAnimation() {
        return animationType == AnimationType.PLACEMENT;
    }

    public boolean isCaptureAnimation() {
        return animationType == AnimationType.CAPTURE;
    }

    public int animationSlot() {
        return animationSlots.length > 0 ? animationSlots[0] : -1;
    }

    public boolean isAnimatingSlot(int slot) {
        for (int animatedSlot : animationSlots) {
            if (animatedSlot == slot) {
                return true;
            }
        }
        return false;
    }

    public float animationProgress(float partialTick) {
        if (!hasActiveAnimation() || animationDuration <= 0) {
            return 0.0F;
        }
        return Math.clamp((animationAge + partialTick) / animationDuration, 0.0F, 1.0F);
    }

    public boolean hasOverviewSweepAnimation() {
        return tableAnimationType == TableAnimationType.OVERVIEW_SWEEP;
    }

    public boolean hasClearWaveAnimation() {
        return tableAnimationType == TableAnimationType.CLEARING;
    }

    public float tableAnimationProgress(float partialTick) {
        if (tableAnimationType == TableAnimationType.NONE || tableAnimationDuration <= 0) {
            return 0.0F;
        }
        return Math.clamp((tableAnimationAge + partialTick) / tableAnimationDuration, 0.0F, 1.0F);
    }

    public boolean animationOwnerApplied() {
        return animationOwnerApplied;
    }

    public void setParticipants(UUID firstParticipantId, UUID secondParticipantId) {
        this.firstParticipantId = firstParticipantId;
        this.secondParticipantId = secondParticipantId;
        sync();
    }

    public void updateHudState(DuelSession session) {
        hudActive = true;
        hudP1HandCount = session.playerHandCount();
        hudP2HandCount = session.opponentHandCount();
        hudP1Score = session.playerScore();
        hudP2Score = session.opponentScore();
        hudRules = session.rulesSummary();
        hudGameState = session.gameStateLabel();
        hudTurn = session.turnIndicator();
        hudInstruction = session.isInOverview()
                ? rewardCardCount() > 0 ? "P1 choosing 1 cards to keep" : "Ready for next game"
                : session.nextStepInstruction();
        sync();
    }

    public void clearHudState() {
        hudActive = false;
        hudP1HandCount = 0;
        hudP2HandCount = 0;
        hudP1Score = 0;
        hudP2Score = 0;
        hudRules = "";
        hudGameState = "";
        hudTurn = "";
        hudInstruction = "";
        sync();
    }

    public boolean setCard(int slot, ItemStack stack, int owner) {
        if (slot < 0 || slot >= boardCards.size() || !boardCards.get(slot).isEmpty() || !stack.is(TriadItems.TRIAD_CARD.get())) {
            return false;
        }

        boardCards.set(slot, stack.copyWithCount(1));
        ownerSlots[slot] = owner;
        slotAges[slot] = 0;
        sync();
        return true;
    }

    public void setOwner(int slot, int owner) {
        if (slot < 0 || slot >= ownerSlots.length || boardCards.get(slot).isEmpty()) {
            return;
        }
        ownerSlots[slot] = owner;
        sync();
    }

    public void clearBoard() {
        for (int index = 0; index < boardCards.size(); index++) {
            boardCards.set(index, ItemStack.EMPTY);
            ownerSlots[index] = OWNER_NONE;
            slotAges[index] = 0;
        }
        pendingAnimations.clear();
        clearAnimationState();
        clearRewardCards();
        firstParticipantId = null;
        secondParticipantId = null;
        clearHudState();
        sync();
    }

    public void removeBoardCardsOwnedBy(int owner) {
        boolean changed = false;
        for (int index = 0; index < boardCards.size(); index++) {
            if (ownerSlots[index] != owner || boardCards.get(index).isEmpty()) {
                continue;
            }
            boardCards.set(index, ItemStack.EMPTY);
            ownerSlots[index] = OWNER_NONE;
            slotAges[index] = 0;
            changed = true;
        }
        if (changed) {
            sync();
        }
    }

    public void dropBoardCards(Level level, BlockPos pos) {
        for (int slot = 0; slot < boardCards.size(); slot++) {
            ItemStack stack = boardCards.get(slot);
            if (!stack.isEmpty()) {
                Block.popResource(level, pos, stack);
                boardCards.set(slot, ItemStack.EMPTY);
                ownerSlots[slot] = OWNER_NONE;
                slotAges[slot] = 0;
            }
        }
        pendingAnimations.clear();
        clearAnimationState();
        dropRewardCards(level, pos);
        firstParticipantId = null;
        secondParticipantId = null;
        clearHudState();
        sync();
    }

    public void setRewardCards(List<ItemStack> stacks) {
        clearRewardCards();
        for (int index = 0; index < rewardCards.size() && index < stacks.size(); index++) {
            ItemStack stack = stacks.get(index);
            if (!stack.isEmpty()) {
                rewardCards.set(index, stack.copyWithCount(1));
            }
        }
        refreshRewardInteractions();
        DuelSession session = level == null ? null : DuelSessionManager.getAt(worldPosition);
        if (session != null) {
            updateHudState(session);
        }
        sync();
    }

    public ItemStack takeRewardCard(int slot) {
        if (slot < 0 || slot >= rewardCards.size()) {
            return ItemStack.EMPTY;
        }
        ItemStack taken = rewardCards.get(slot);
        if (taken.isEmpty()) {
            return ItemStack.EMPTY;
        }
        int visibleIndex = visibleRewardIndex(slot);
        int rewardCountBeforeTake = rewardCardCount();
        if (visibleIndex >= 0 && rewardCountBeforeTake > 0) {
            BoardLocalSpace.SlotCenter center = BoardLocalSpace.rewardCenter(
                    visibleIndex,
                    getBlockState().getValue(DuelTableBlock.FACING),
                    rewardCountBeforeTake);
            rewardTakeAnimations.add(new RewardTakeAnimation(taken.copyWithCount(1), center.x(), center.z(), 0, REWARD_TAKE_DURATION));
        }
        rewardCards.set(slot, ItemStack.EMPTY);
        refreshRewardInteractions();
        DuelSession session = level == null ? null : DuelSessionManager.getAt(worldPosition);
        if (session != null) {
            updateHudState(session);
        }
        sync();
        return taken;
    }

    public void clearRewardCards() {
        for (int index = 0; index < rewardCards.size(); index++) {
            rewardCards.set(index, ItemStack.EMPTY);
        }
        rewardTakeAnimations.clear();
        removeRewardInteractions();
    }

    public void dropRewardCards(Level level, BlockPos pos) {
        for (int slot = 0; slot < rewardCards.size(); slot++) {
            ItemStack stack = rewardCards.get(slot);
            if (!stack.isEmpty()) {
                Block.popResource(level, pos, stack);
                rewardCards.set(slot, ItemStack.EMPTY);
            }
        }
        rewardTakeAnimations.clear();
        removeRewardInteractions();
    }

    public void refreshRewardInteractions() {
        if (level == null || level.isClientSide) {
            return;
        }
        removeRewardInteractions();
        int visibleIndex = 0;
        var facing = getBlockState().getValue(DuelTableBlock.FACING);
        for (int slot = 0; slot < rewardCards.size(); slot++) {
            if (rewardCards.get(slot).isEmpty()) {
                continue;
            }
            BoardLocalSpace.SlotCenter center = BoardLocalSpace.rewardCenter(visibleIndex, facing, rewardCardCount());
            Interaction interaction = new Interaction(EntityType.INTERACTION, level);
            interaction.setPos(worldPosition.getX() + center.x(), worldPosition.getY() + 1.12D, worldPosition.getZ() + center.z());
            interaction.setNoGravity(true);
            interaction.addTag(REWARD_PROXY_TAG);
            interaction.addTag(REWARD_PROXY_POS_PREFIX + worldPosition.getX() + "," + worldPosition.getY() + "," + worldPosition.getZ());
            interaction.addTag(REWARD_PROXY_SLOT_PREFIX + slot);
            level.addFreshEntity(interaction);
            visibleIndex++;
        }
    }

    public void removeRewardInteractions() {
        if (level == null || level.isClientSide) {
            return;
        }
        AABB searchBox = new AABB(worldPosition).inflate(2.0D, 2.0D, 2.0D);
        for (Interaction interaction : level.getEntitiesOfClass(Interaction.class, searchBox, DuelTableBlockEntity::isRewardProxy)) {
            if (worldPosition.equals(rewardProxyPos(interaction))) {
                interaction.discard();
            }
        }
    }

    public static boolean isRewardProxy(Interaction interaction) {
        return interaction.getTags().contains(REWARD_PROXY_TAG);
    }

    public static BlockPos rewardProxyPos(Interaction interaction) {
        for (String tag : interaction.getTags()) {
            if (!tag.startsWith(REWARD_PROXY_POS_PREFIX)) {
                continue;
            }
            String[] parts = tag.substring(REWARD_PROXY_POS_PREFIX.length()).split(",");
            if (parts.length == 3) {
                try {
                    return new BlockPos(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2]));
                } catch (NumberFormatException ignored) {
                    return null;
                }
            }
        }
        return null;
    }

    public static int rewardProxySlot(Interaction interaction) {
        for (String tag : interaction.getTags()) {
            if (tag.startsWith(REWARD_PROXY_SLOT_PREFIX)) {
                try {
                    return Integer.parseInt(tag.substring(REWARD_PROXY_SLOT_PREFIX.length()));
                } catch (NumberFormatException ignored) {
                    return -1;
                }
            }
        }
        return -1;
    }

    public void startMoveAnimation(MoveResult moveResult, int owner) {
        pendingAnimations.clear();
        clearAnimationState();
        clearTableAnimationState();
        if (moveResult.playedCell() == null) {
            return;
        }

        pendingAnimations.addLast(new AnimationStep(AnimationType.PLACEMENT, new int[] {moveResult.playedCell().index()}, owner));
        for (List<BoardCell> captureWave : moveResult.captureWaves()) {
            int[] captureSlots = captureWave.stream()
                    .mapToInt(BoardCell::index)
                    .toArray();
            if (captureSlots.length > 0) {
                pendingAnimations.addLast(new AnimationStep(AnimationType.CAPTURE, captureSlots, owner));
            }
        }
        advanceAnimationStep();
    }

    public void startOverviewSweepAnimation() {
        clearAnimationState();
        tableAnimationType = TableAnimationType.OVERVIEW_SWEEP;
        tableAnimationAge = 0;
        tableAnimationDuration = OVERVIEW_SWEEP_DURATION;
        sync();
    }

    public void startClearWaveAnimation() {
        clearAnimationState();
        tableAnimationType = TableAnimationType.CLEARING;
        tableAnimationAge = 0;
        tableAnimationDuration = CLEAR_WAVE_DURATION;
        sync();
    }

    public static void tick(Level level, BlockPos pos, BlockState state, DuelTableBlockEntity blockEntity) {
        if (blockEntity.hasActiveAnimation()) {
            if (blockEntity.animationType != AnimationType.NONE) {
                blockEntity.animationAge++;
                if (!level.isClientSide
                        && blockEntity.animationType == AnimationType.CAPTURE
                        && !blockEntity.animationOwnerApplied
                        && blockEntity.animationAge >= Math.max(1, blockEntity.animationDuration / 2)) {
                    for (int slot : blockEntity.animationSlots) {
                        blockEntity.setOwner(slot, blockEntity.animationTargetOwner);
                    }
                    blockEntity.animationOwnerApplied = true;
                    blockEntity.sync();
                }

                if (blockEntity.animationAge >= blockEntity.animationDuration) {
                    if (!level.isClientSide) {
                        blockEntity.advanceAnimationStep();
                        if (blockEntity.animationType == AnimationType.NONE && blockEntity.tableAnimationType == TableAnimationType.NONE) {
                            blockEntity.resolvePostAnimation(level, pos);
                        }
                    } else {
                        blockEntity.clearAnimationState();
                    }
                }
            }

            if (blockEntity.tableAnimationType != TableAnimationType.NONE) {
                blockEntity.tableAnimationAge++;
                if (!level.isClientSide
                        && blockEntity.tableAnimationType == TableAnimationType.CLEARING) {
                    blockEntity.updateClearWaveState(level, pos);
                }
                if (blockEntity.tableAnimationAge >= blockEntity.tableAnimationDuration) {
                    if (!level.isClientSide) {
                        blockEntity.clearTableAnimationState();
                        blockEntity.resolvePostAnimation(level, pos);
                    } else {
                        blockEntity.clearTableAnimationState();
                    }
                }
            }
        }

        for (int slot = 0; slot < SLOT_COUNT; slot++) {
            if (!blockEntity.boardCards.get(slot).isEmpty()) {
                blockEntity.slotAges[slot]++;
            } else {
                blockEntity.slotAges[slot] = 0;
            }
        }

        if (!blockEntity.rewardTakeAnimations.isEmpty()) {
            boolean changed = false;
            for (int index = blockEntity.rewardTakeAnimations.size() - 1; index >= 0; index--) {
                RewardTakeAnimation animation = blockEntity.rewardTakeAnimations.get(index);
                animation.age++;
                if (animation.age >= animation.duration) {
                    blockEntity.rewardTakeAnimations.remove(index);
                    changed = true;
                }
            }
            if (!level.isClientSide && changed) {
                blockEntity.sync();
            }
        }
    }

    @Override
    protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
        super.saveAdditional(tag, registries);
        ContainerHelper.saveAllItems(tag, boardCards, registries);
        CompoundTag rewardTag = new CompoundTag();
        ContainerHelper.saveAllItems(rewardTag, rewardCards, registries);
        tag.put("RewardCards", rewardTag);
        saveRewardTakeAnimations(tag, registries);
        saveHudState(tag);
        tag.putIntArray("OwnerSlots", ownerSlots);
        tag.putIntArray("SlotAges", slotAges);
        saveAnimation(tag);
        saveTableAnimation(tag);
        if (firstParticipantId != null) {
            tag.putUUID("FirstParticipantId", firstParticipantId);
        }
        if (secondParticipantId != null) {
            tag.putUUID("SecondParticipantId", secondParticipantId);
        }
    }

    @Override
    protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
        super.loadAdditional(tag, registries);
        clearBoardContents();
        ContainerHelper.loadAllItems(tag, boardCards, registries);
        if (tag.contains("RewardCards", Tag.TAG_COMPOUND)) {
            ContainerHelper.loadAllItems(tag.getCompound("RewardCards"), rewardCards, registries);
        }
        loadRewardTakeAnimations(tag, registries);
        loadHudState(tag);
        loadOwnerData(tag);
        loadAnimation(tag);
        loadTableAnimation(tag);
    }

    @Override
    public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
        CompoundTag tag = super.getUpdateTag(registries);
        ContainerHelper.saveAllItems(tag, boardCards, registries);
        CompoundTag rewardTag = new CompoundTag();
        ContainerHelper.saveAllItems(rewardTag, rewardCards, registries);
        tag.put("RewardCards", rewardTag);
        saveRewardTakeAnimations(tag, registries);
        saveHudState(tag);
        tag.putIntArray("OwnerSlots", ownerSlots);
        tag.putIntArray("SlotAges", slotAges);
        saveAnimation(tag);
        saveTableAnimation(tag);
        if (firstParticipantId != null) {
            tag.putUUID("FirstParticipantId", firstParticipantId);
        }
        if (secondParticipantId != null) {
            tag.putUUID("SecondParticipantId", secondParticipantId);
        }
        return tag;
    }

    @Override
    public ClientboundBlockEntityDataPacket getUpdatePacket() {
        return ClientboundBlockEntityDataPacket.create(this);
    }

    @Override
    public void onDataPacket(Connection connection, ClientboundBlockEntityDataPacket packet, HolderLookup.Provider registries) {
        CompoundTag tag = packet.getTag();
        if (tag != null) {
            clearBoardContents();
            ContainerHelper.loadAllItems(tag, boardCards, registries);
            if (tag.contains("RewardCards", Tag.TAG_COMPOUND)) {
                ContainerHelper.loadAllItems(tag.getCompound("RewardCards"), rewardCards, registries);
            }
            loadRewardTakeAnimations(tag, registries);
            loadHudState(tag);
            loadOwnerData(tag);
            loadAnimation(tag);
            loadTableAnimation(tag);
        }
    }

    private void clearBoardContents() {
        for (int index = 0; index < boardCards.size(); index++) {
            boardCards.set(index, ItemStack.EMPTY);
            ownerSlots[index] = OWNER_NONE;
            slotAges[index] = 0;
        }
        pendingAnimations.clear();
        clearAnimationState();
        clearRewardCards();
        firstParticipantId = null;
        secondParticipantId = null;
        hudActive = false;
        hudP1HandCount = 0;
        hudP2HandCount = 0;
        hudP1Score = 0;
        hudP2Score = 0;
        hudRules = "";
        hudGameState = "";
        hudTurn = "";
        hudInstruction = "";
    }

    private void loadOwnerData(CompoundTag tag) {
        int[] loadedOwners = tag.getIntArray("OwnerSlots");
        for (int index = 0; index < ownerSlots.length; index++) {
            ownerSlots[index] = index < loadedOwners.length ? loadedOwners[index] : OWNER_NONE;
        }
        int[] loadedAges = tag.getIntArray("SlotAges");
        for (int index = 0; index < slotAges.length; index++) {
            slotAges[index] = index < loadedAges.length ? loadedAges[index] : 0;
        }
        firstParticipantId = tag.hasUUID("FirstParticipantId") ? tag.getUUID("FirstParticipantId") : null;
        secondParticipantId = tag.hasUUID("SecondParticipantId") ? tag.getUUID("SecondParticipantId") : null;
    }

    private void saveRewardTakeAnimations(CompoundTag tag, HolderLookup.Provider registries) {
        CompoundTag animationsTag = new CompoundTag();
        for (int index = 0; index < rewardTakeAnimations.size(); index++) {
            RewardTakeAnimation animation = rewardTakeAnimations.get(index);
            CompoundTag animationTag = new CompoundTag();
            animationTag.put("Stack", animation.stack.saveOptional(registries));
            animationTag.putFloat("LocalX", animation.localX);
            animationTag.putFloat("LocalZ", animation.localZ);
            animationTag.putInt("Age", animation.age);
            animationTag.putInt("Duration", animation.duration);
            animationsTag.put("Animation" + index, animationTag);
        }
        animationsTag.putInt("Count", rewardTakeAnimations.size());
        tag.put("RewardTakeAnimations", animationsTag);
    }

    private void saveHudState(CompoundTag tag) {
        tag.putBoolean("HudActive", hudActive);
        tag.putInt("HudP1HandCount", hudP1HandCount);
        tag.putInt("HudP2HandCount", hudP2HandCount);
        tag.putInt("HudP1Score", hudP1Score);
        tag.putInt("HudP2Score", hudP2Score);
        tag.putString("HudRules", hudRules);
        tag.putString("HudGameState", hudGameState);
        tag.putString("HudTurn", hudTurn);
        tag.putString("HudInstruction", hudInstruction);
    }

    private void loadHudState(CompoundTag tag) {
        hudActive = tag.getBoolean("HudActive");
        hudP1HandCount = tag.getInt("HudP1HandCount");
        hudP2HandCount = tag.getInt("HudP2HandCount");
        hudP1Score = tag.getInt("HudP1Score");
        hudP2Score = tag.getInt("HudP2Score");
        hudRules = tag.getString("HudRules");
        hudGameState = tag.getString("HudGameState");
        hudTurn = tag.getString("HudTurn");
        hudInstruction = tag.getString("HudInstruction");
    }

    private void loadRewardTakeAnimations(CompoundTag tag, HolderLookup.Provider registries) {
        rewardTakeAnimations.clear();
        if (!tag.contains("RewardTakeAnimations", Tag.TAG_COMPOUND)) {
            return;
        }
        CompoundTag animationsTag = tag.getCompound("RewardTakeAnimations");
        int count = animationsTag.getInt("Count");
        for (int index = 0; index < count; index++) {
            CompoundTag animationTag = animationsTag.getCompound("Animation" + index);
            ItemStack stack = ItemStack.parseOptional(registries, animationTag.getCompound("Stack"));
            if (stack.isEmpty()) {
                continue;
            }
            rewardTakeAnimations.add(new RewardTakeAnimation(
                    stack,
                    animationTag.getFloat("LocalX"),
                    animationTag.getFloat("LocalZ"),
                    animationTag.getInt("Age"),
                    animationTag.getInt("Duration")));
        }
    }

    private void saveAnimation(CompoundTag tag) {
        tag.putString("AnimationType", animationType.name());
        tag.putIntArray("AnimationSlots", animationSlots);
        tag.putInt("AnimationTargetOwner", animationTargetOwner);
        tag.putInt("AnimationAge", animationAge);
        tag.putInt("AnimationDuration", animationDuration);
        tag.putBoolean("AnimationOwnerApplied", animationOwnerApplied);
    }

    private void loadAnimation(CompoundTag tag) {
        animationType = AnimationType.valueOf(tag.getString("AnimationType").isEmpty() ? AnimationType.NONE.name() : tag.getString("AnimationType"));
        animationSlots = tag.getIntArray("AnimationSlots");
        animationTargetOwner = tag.getInt("AnimationTargetOwner");
        animationAge = tag.getInt("AnimationAge");
        animationDuration = tag.getInt("AnimationDuration");
        animationOwnerApplied = tag.getBoolean("AnimationOwnerApplied");
    }

    private void saveTableAnimation(CompoundTag tag) {
        tag.putString("TableAnimationType", tableAnimationType.name());
        tag.putInt("TableAnimationAge", tableAnimationAge);
        tag.putInt("TableAnimationDuration", tableAnimationDuration);
    }

    private void loadTableAnimation(CompoundTag tag) {
        tableAnimationType = TableAnimationType.valueOf(tag.getString("TableAnimationType").isEmpty() ? TableAnimationType.NONE.name() : tag.getString("TableAnimationType"));
        tableAnimationAge = tag.getInt("TableAnimationAge");
        tableAnimationDuration = tag.getInt("TableAnimationDuration");
    }

    private void advanceAnimationStep() {
        AnimationStep nextStep = pendingAnimations.pollFirst();
        if (nextStep == null) {
            clearAnimationState();
            sync();
            return;
        }

        animationType = nextStep.type();
        animationSlots = nextStep.slots();
        animationTargetOwner = nextStep.targetOwner();
        animationAge = 0;
        animationDuration = nextStep.type() == AnimationType.PLACEMENT ? PLACEMENT_DURATION : CAPTURE_DURATION;
        animationOwnerApplied = nextStep.type() != AnimationType.CAPTURE;
        sync();
    }

    private void clearAnimationState() {
        animationType = AnimationType.NONE;
        animationSlots = new int[0];
        animationTargetOwner = OWNER_NONE;
        animationAge = 0;
        animationDuration = 0;
        animationOwnerApplied = false;
    }

    private void clearTableAnimationState() {
        tableAnimationType = TableAnimationType.NONE;
        tableAnimationAge = 0;
        tableAnimationDuration = 0;
    }

    private void resolvePostAnimation(Level level, BlockPos pos) {
        DuelSession session = DuelSessionManager.getAt(pos);
        if (session == null) {
            return;
        }

        if (session.isClosingAnimation()) {
            clearBoard();
            var player = level.getServer() == null ? null : level.getServer().getPlayerList().getPlayer(session.playerParticipantId());
            if (player != null) {
                DuelSessionManager.end(player, true);
                player.displayClientMessage(Component.literal("Duel finished. All played cards have been returned.").withStyle(ChatFormatting.GREEN), false);
            }
            return;
        }

        if (session.isAwaitingPlayerAnimation()) {
            DuelTableAnimationResolver.handleAfterPlayerAnimation(level, pos, this, session);
            return;
        }

        if (session.isAwaitingOpponentAnimation()) {
            DuelTableAnimationResolver.handleAfterOpponentAnimation(level, pos, this, session);
        }
    }

    private void updateClearWaveState(Level level, BlockPos pos) {
        for (int slot = 0; slot < SLOT_COUNT; slot++) {
            if (boardCards.get(slot).isEmpty()) {
                continue;
            }
            if (clearWaveSlotProgress(slot, 0.0F) >= 1.0F) {
                boardCards.set(slot, ItemStack.EMPTY);
                ownerSlots[slot] = OWNER_NONE;
                slotAges[slot] = 0;
            }
        }
        sync();
    }

    public float overviewSweepSlotProgress(int slot, float partialTick) {
        return waveSlotProgress(slot, partialTick, SLOT_COUNT, tableAnimationAge, tableAnimationDuration);
    }

    public float clearWaveSlotProgress(int slot, float partialTick) {
        return waveSlotProgress(slot, partialTick, SLOT_COUNT, tableAnimationAge, tableAnimationDuration);
    }

    private float waveSlotProgress(int slot, float partialTick, int slotCount, int age, int duration) {
        if (slot < 0 || slot >= slotCount || duration <= 0) {
            return 0.0F;
        }
        float global = Math.clamp((age + partialTick) / duration, 0.0F, 1.0F);
        float delay = slot / (float) slotCount;
        float window = 0.28F;
        return Math.clamp((global - delay) / window, 0.0F, 1.0F);
    }

    private void sync() {
        setChanged();
        if (level != null) {
            level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), Block.UPDATE_ALL);
        }
    }

    private enum AnimationType {
        NONE,
        PLACEMENT,
        CAPTURE
    }

    private enum TableAnimationType {
        NONE,
        OVERVIEW_SWEEP,
        CLEARING
    }

    private record AnimationStep(AnimationType type, int[] slots, int targetOwner) {
    }

    public static final class RewardTakeAnimation {
        private final ItemStack stack;
        private final float localX;
        private final float localZ;
        private int age;
        private final int duration;

        private RewardTakeAnimation(ItemStack stack, float localX, float localZ, int age, int duration) {
            this.stack = stack;
            this.localX = localX;
            this.localZ = localZ;
            this.age = age;
            this.duration = duration;
        }

        public ItemStack stack() {
            return stack;
        }

        public float localX() {
            return localX;
        }

        public float localZ() {
            return localZ;
        }

        public int age() {
            return age;
        }

        public int duration() {
            return duration;
        }
    }

    private int visibleRewardIndex(int slot) {
        int visibleIndex = 0;
        for (int index = 0; index < rewardCards.size(); index++) {
            if (rewardCards.get(index).isEmpty()) {
                continue;
            }
            if (index == slot) {
                return visibleIndex;
            }
            visibleIndex++;
        }
        return -1;
    }
}
