package net.minecraft.world.level.block.entity;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.yggdrasil.ProfileResult;
import java.time.Duration;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BooleanSupplier;
import javax.annotation.Nullable;
import net.minecraft.Util;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.component.DataComponentGetter;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.ComponentSerialization;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.Services;
import net.minecraft.util.StringUtil;
import net.minecraft.world.item.component.ResolvableProfile;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.SkullBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;

public class SkullBlockEntity extends BlockEntity {
    private static final String TAG_PROFILE = "profile";
    private static final String TAG_NOTE_BLOCK_SOUND = "note_block_sound";
    private static final String TAG_CUSTOM_NAME = "custom_name";
    @Nullable
    private static Executor mainThreadExecutor;
    @Nullable
    private static LoadingCache<String, CompletableFuture<Optional<GameProfile>>> profileCacheByName;
    @Nullable
    private static LoadingCache<com.mojang.datafixers.util.Pair<java.util.UUID,  @org.jetbrains.annotations.Nullable GameProfile>, CompletableFuture<Optional<GameProfile>>> profileCacheById; // Paper - player profile events
    public static final Executor CHECKED_MAIN_THREAD_EXECUTOR = runnable -> {
        Executor executor = mainThreadExecutor;
        if (executor != null) {
            executor.execute(runnable);
        }
    };
    @Nullable
    public ResolvableProfile owner;
    @Nullable
    public ResourceLocation noteBlockSound;
    private int animationTickCount;
    private boolean isAnimating;
    @Nullable
    public Component customName;

    public SkullBlockEntity(BlockPos pos, BlockState blockState) {
        super(BlockEntityType.SKULL, pos, blockState);
    }

    public static void setup(final Services services, Executor mainThreadExecutor) {
        SkullBlockEntity.mainThreadExecutor = mainThreadExecutor;
        final BooleanSupplier booleanSupplier = () -> profileCacheById == null;
        profileCacheByName = CacheBuilder.newBuilder()
            .expireAfterAccess(Duration.ofMinutes(10L))
            .maximumSize(256L)
            .build(new CacheLoader<String, CompletableFuture<Optional<GameProfile>>>() {
                @Override
                public CompletableFuture<Optional<GameProfile>> load(String username) {
                    return SkullBlockEntity.fetchProfileByName(username, services);
                }
            });
        profileCacheById = CacheBuilder.newBuilder()
            .expireAfterAccess(Duration.ofMinutes(10L))
            .maximumSize(256L)
            .build(new CacheLoader<>() { // Paper - player profile events
                @Override
                public CompletableFuture<Optional<GameProfile>> load(com.mojang.datafixers.util.Pair<java.util.UUID, @org.jetbrains.annotations.Nullable GameProfile> id) { // Paper - player profile events
                    return SkullBlockEntity.fetchProfileById(id, services, booleanSupplier);
                }
            });
    }

    static CompletableFuture<Optional<GameProfile>> fetchProfileByName(String name, Services services) {
        return services.profileCache()
            .getAsync(name)
            .thenCompose(
                optional -> {
                    LoadingCache<com.mojang.datafixers.util.Pair<java.util.UUID, @org.jetbrains.annotations.Nullable GameProfile>, CompletableFuture<Optional<GameProfile>>> loadingCache = profileCacheById; // Paper - player profile events
                    return loadingCache != null && !optional.isEmpty()
                        ? loadingCache.getUnchecked(new com.mojang.datafixers.util.Pair<>(optional.get().getId(), optional.get())).thenApply(optional1 -> optional1.or(() -> optional)) // Paper - player profile events
                        : CompletableFuture.completedFuture(Optional.empty());
                }
            );
    }

    static CompletableFuture<Optional<GameProfile>> fetchProfileById(com.mojang.datafixers.util.Pair<java.util.UUID, @org.jetbrains.annotations.Nullable GameProfile> id, Services services, BooleanSupplier cacheUninitialized) { // Paper
        return CompletableFuture.supplyAsync(() -> {
            if (cacheUninitialized.getAsBoolean()) {
                return Optional.empty();
            } else {
                // Paper start - fill player profile events
                if (services.sessionService() instanceof com.destroystokyo.paper.profile.PaperMinecraftSessionService paperService) {
                    final GameProfile profile = id.getSecond() != null ? id.getSecond() : new com.mojang.authlib.GameProfile(id.getFirst(), "");
                    return Optional.ofNullable(paperService.fetchProfile(profile, true)).map(ProfileResult::profile);
                }
                ProfileResult profileResult = services.sessionService().fetchProfile(id.getFirst(), true);
                // Paper end - fill player profile events
                return Optional.ofNullable(profileResult).map(ProfileResult::profile);
            }
        }, Util.PROFILE_EXECUTOR); // Paper - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread
    }

    public static void clear() {
        mainThreadExecutor = null;
        profileCacheByName = null;
        profileCacheById = null;
    }

    @Override
    protected void saveAdditional(ValueOutput output) {
        super.saveAdditional(output);
        output.storeNullable("profile", ResolvableProfile.CODEC, this.owner);
        output.storeNullable("note_block_sound", ResourceLocation.CODEC, this.noteBlockSound);
        output.storeNullable("custom_name", ComponentSerialization.CODEC, this.customName);
    }

    @Override
    protected void loadAdditional(ValueInput input) {
        super.loadAdditional(input);
        this.setOwner(input.read("profile", ResolvableProfile.CODEC).orElse(null));
        this.noteBlockSound = input.read("note_block_sound", ResourceLocation.CODEC).orElse(null);
        this.customName = parseCustomNameSafe(input, "custom_name");
    }

    public static void animation(Level level, BlockPos pos, BlockState state, SkullBlockEntity blockEntity) {
        if (state.hasProperty(SkullBlock.POWERED) && state.getValue(SkullBlock.POWERED)) {
            blockEntity.isAnimating = true;
            blockEntity.animationTickCount++;
        } else {
            blockEntity.isAnimating = false;
        }
    }

    public float getAnimation(float partialTick) {
        return this.isAnimating ? this.animationTickCount + partialTick : this.animationTickCount;
    }

    @Nullable
    public ResolvableProfile getOwnerProfile() {
        return this.owner;
    }

    @Nullable
    public ResourceLocation getNoteBlockSound() {
        return this.noteBlockSound;
    }

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

    @Override
    public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
        return this.saveCustomOnly(registries);
    }

    public void setOwner(@Nullable ResolvableProfile owner) {
        synchronized (this) {
            this.owner = owner;
        }

        this.updateOwnerProfile();
    }

    private void updateOwnerProfile() {
        if (this.owner != null && !this.owner.isResolved()) {
            this.owner.resolve().thenAcceptAsync(resolvableProfile -> {
                this.owner = resolvableProfile;
                this.setChanged();
            }, CHECKED_MAIN_THREAD_EXECUTOR);
        } else {
            this.setChanged();
        }
    }

    public static CompletableFuture<Optional<GameProfile>> fetchGameProfile(String profileName) {
        LoadingCache<String, CompletableFuture<Optional<GameProfile>>> loadingCache = profileCacheByName;
        return loadingCache != null && StringUtil.isValidPlayerName(profileName)
            ? loadingCache.getUnchecked(profileName)
            : CompletableFuture.completedFuture(Optional.empty());
    }

    // Paper start - player profile events
    public static CompletableFuture<Optional<GameProfile>> fetchGameProfile(UUID profileUuid, @Nullable String name) {
        LoadingCache<com.mojang.datafixers.util.Pair<java.util.UUID,  @org.jetbrains.annotations.Nullable GameProfile>, CompletableFuture<Optional<GameProfile>>> loadingCache = profileCacheById;
        return loadingCache != null ? loadingCache.getUnchecked(new com.mojang.datafixers.util.Pair<>(profileUuid, name != null ? new com.mojang.authlib.GameProfile(profileUuid, name) : null)) : CompletableFuture.completedFuture(Optional.empty());
        // Paper end - player profile events
    }

    @Override
    protected void applyImplicitComponents(DataComponentGetter componentGetter) {
        super.applyImplicitComponents(componentGetter);
        this.setOwner(componentGetter.get(DataComponents.PROFILE));
        this.noteBlockSound = componentGetter.get(DataComponents.NOTE_BLOCK_SOUND);
        this.customName = componentGetter.get(DataComponents.CUSTOM_NAME);
    }

    @Override
    protected void collectImplicitComponents(DataComponentMap.Builder components) {
        super.collectImplicitComponents(components);
        components.set(DataComponents.PROFILE, this.owner);
        components.set(DataComponents.NOTE_BLOCK_SOUND, this.noteBlockSound);
        components.set(DataComponents.CUSTOM_NAME, this.customName);
    }

    @Override
    public void removeComponentsFromTag(ValueOutput output) {
        super.removeComponentsFromTag(output);
        output.discard("profile");
        output.discard("note_block_sound");
        output.discard("custom_name");
    }
}
