package io.papermc.paper.adventure;

import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.DynamicOps;
import com.mojang.serialization.JavaOps;
import com.mojang.serialization.JsonOps;
import io.papermc.paper.util.MethodParameterSource;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Stream;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.BlockNBTComponent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.ByteTag;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.Tag;
import net.minecraft.network.chat.ComponentSerialization;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import org.apache.commons.lang3.RandomStringUtils;
import org.bukkit.support.RegistryHelper;
import org.bukkit.support.environment.VanillaFeature;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junitpioneer.jupiter.cartesian.CartesianTest;

import static io.papermc.paper.adventure.AdventureCodecs.CLICK_EVENT_CODEC;
import static io.papermc.paper.adventure.AdventureCodecs.COMPONENT_CODEC;
import static io.papermc.paper.adventure.AdventureCodecs.HOVER_EVENT_CODEC;
import static io.papermc.paper.adventure.AdventureCodecs.KEY_CODEC;
import static io.papermc.paper.adventure.AdventureCodecs.STYLE_MAP_CODEC;
import static io.papermc.paper.adventure.AdventureCodecs.TEXT_COLOR_CODEC;
import static java.util.Objects.requireNonNull;
import static net.kyori.adventure.key.Key.key;
import static net.kyori.adventure.text.Component.blockNBT;
import static net.kyori.adventure.text.Component.entityNBT;
import static net.kyori.adventure.text.Component.keybind;
import static net.kyori.adventure.text.Component.score;
import static net.kyori.adventure.text.Component.selector;
import static net.kyori.adventure.text.Component.storageNBT;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.Component.translatable;
import static net.kyori.adventure.text.TranslationArgument.numeric;
import static net.kyori.adventure.text.event.ClickEvent.openUrl;
import static net.kyori.adventure.text.event.ClickEvent.suggestCommand;
import static net.kyori.adventure.text.event.HoverEvent.showEntity;
import static net.kyori.adventure.text.format.Style.style;
import static net.kyori.adventure.text.format.TextColor.color;
import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

@VanillaFeature
class AdventureCodecsTest {

    static final String PARAMETERIZED_NAME = "[{index}] {displayName}: {arguments}";

    @Test
    void testTextColor() {
        final TextColor color = color(0x1d38df);
        final Tag result = TEXT_COLOR_CODEC.encodeStart(NbtOps.INSTANCE, color).result().orElseThrow();
        assertEquals(color.asHexString(), result.getAsString());
        final net.minecraft.network.chat.TextColor nms = net.minecraft.network.chat.TextColor.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst();
        assertEquals(color.value(), nms.getValue());
    }

    @Test
    void testNamedTextColor() {
        final NamedTextColor color = NamedTextColor.BLUE;
        final Tag result = TEXT_COLOR_CODEC.encodeStart(NbtOps.INSTANCE, color).result().orElseThrow();
        assertEquals(NamedTextColor.NAMES.keyOrThrow(color), result.getAsString());
        final net.minecraft.network.chat.TextColor nms = net.minecraft.network.chat.TextColor.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst();
        assertEquals(color.value(), nms.getValue());
    }

    @Test
    void testKey() {
        final Key key = key("hello", "there");
        final Tag result = KEY_CODEC.encodeStart(NbtOps.INSTANCE, key).result().orElseThrow();
        assertEquals(key.asString(), result.getAsString());
        final ResourceLocation location = ResourceLocation.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst();
        assertEquals(key.asString(), location.toString());
    }

    @ParameterizedTest(name = PARAMETERIZED_NAME)
    @EnumSource(value = ClickEvent.Action.class, mode = EnumSource.Mode.EXCLUDE, names = {"OPEN_FILE"})
    void testClickEvent(final ClickEvent.Action action) {
        final ClickEvent event = ClickEvent.clickEvent(action, RandomStringUtils.randomAlphanumeric(20));
        final Tag result = CLICK_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, event).result().orElseThrow();
        final net.minecraft.network.chat.ClickEvent nms = net.minecraft.network.chat.ClickEvent.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst();
        assertEquals(event.action().toString(), nms.getAction().getSerializedName());
        assertEquals(event.value(), nms.getValue());
    }

    @Test
    void testShowTextHoverEvent() {
        final HoverEvent<Component> hoverEvent = HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, text("hello"));
        final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow();
        final net.minecraft.network.chat.HoverEvent nms = net.minecraft.network.chat.HoverEvent.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst();
        assertEquals(hoverEvent.action().toString(), nms.getAction().getSerializedName());
        assertNotNull(nms.getValue(net.minecraft.network.chat.HoverEvent.Action.SHOW_TEXT));
    }

    @Test
    void testShowItemHoverEvent() throws IOException {
        final ItemStack stack = new ItemStack(Items.PUMPKIN, 3);
        stack.set(DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal("NAME"));
        final HoverEvent<HoverEvent.ShowItem> hoverEvent = HoverEvent.showItem(key("minecraft:pumpkin"), 3, PaperAdventure.asAdventure(stack.getComponentsPatch()));
        final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow();
        final DataResult<Pair<net.minecraft.network.chat.HoverEvent, Tag>> dataResult = net.minecraft.network.chat.HoverEvent.CODEC.decode(NbtOps.INSTANCE, result);
        assertTrue(dataResult.result().isPresent(), () -> dataResult + " result is not present");
        final net.minecraft.network.chat.HoverEvent nms = dataResult.result().orElseThrow().getFirst();
        assertEquals(hoverEvent.action().toString(), nms.getAction().getSerializedName());
        final net.minecraft.network.chat.HoverEvent.ItemStackInfo value = nms.getValue(net.minecraft.network.chat.HoverEvent.Action.SHOW_ITEM);
        assertNotNull(value);
        assertEquals(hoverEvent.value().count(), value.count);
        assertEquals(hoverEvent.value().item().asString(), value.item.unwrapKey().orElseThrow().location().toString());
        assertEquals(stack.getComponentsPatch(), value.components);
    }

    @Test
    void testShowEntityHoverEvent() {
        UUID uuid = UUID.randomUUID();
        final HoverEvent<HoverEvent.ShowEntity> hoverEvent = showEntity(key("minecraft:wolf"), uuid, text("NAME"));
        final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow();
        final DataResult<Pair<net.minecraft.network.chat.HoverEvent, Tag>> dataResult = net.minecraft.network.chat.HoverEvent.CODEC.decode(NbtOps.INSTANCE, result);
        assertTrue(dataResult.result().isPresent(), () -> dataResult + " result is not present");
        final net.minecraft.network.chat.HoverEvent nms = dataResult.result().orElseThrow().getFirst();
        assertEquals(hoverEvent.action().toString(), nms.getAction().getSerializedName());
        final net.minecraft.network.chat.HoverEvent.EntityTooltipInfo value = nms.getValue(net.minecraft.network.chat.HoverEvent.Action.SHOW_ENTITY);
        assertNotNull(value);
        assertEquals(hoverEvent.value().type().asString(), BuiltInRegistries.ENTITY_TYPE.getKey(value.type).toString());
        assertEquals(hoverEvent.value().id(), value.id);
        assertEquals("NAME", value.name.orElseThrow().getString());
    }

    @Test
    void testSimpleStyle() {
        final Style style = style().decorate(TextDecoration.BOLD).color(NamedTextColor.RED).build();
        final Tag result = STYLE_MAP_CODEC.codec().encodeStart(NbtOps.INSTANCE, style).result().orElseThrow();
        final DataResult<Pair<net.minecraft.network.chat.Style, Tag>> dataResult = net.minecraft.network.chat.Style.Serializer.CODEC.decode(NbtOps.INSTANCE, result);
        assertTrue(dataResult.result().isPresent(), () -> dataResult + " result is not present");
        final net.minecraft.network.chat.Style nms = dataResult.result().get().getFirst();
        assertTrue(nms.isBold());
        assertEquals(requireNonNull(style.color()).value(), requireNonNull(nms.getColor()).getValue());
    }

    @CartesianTest(name = PARAMETERIZED_NAME)
    void testDirectRoundTripStyle(
        @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps,
        @MethodParameterSource("testStyles") final Style style
    ) {
        testDirectRoundTrip(dynamicOps, STYLE_MAP_CODEC.codec(), style);
    }

    @CartesianTest(name = PARAMETERIZED_NAME)
    void testMinecraftRoundTripStyle(
        @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps,
        @MethodParameterSource("testStyles") final Style style
    ) {
        testMinecraftRoundTrip(dynamicOps, STYLE_MAP_CODEC.codec(), net.minecraft.network.chat.Style.Serializer.CODEC, style);
    }

    @CartesianTest(name = PARAMETERIZED_NAME)
    void testDirectRoundTripComponent(
        @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps,
        @TestComponents final Component component
    ) {
        testDirectRoundTrip(dynamicOps, COMPONENT_CODEC, component);
    }

    @CartesianTest(name = PARAMETERIZED_NAME)
    void testMinecraftRoundTripComponent(
        @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps,
        @TestComponents final Component component
    ) {
        testMinecraftRoundTrip(dynamicOps, COMPONENT_CODEC, ComponentSerialization.CODEC, component);
    }

    static List<? extends DynamicOps<?>> dynamicOps() {
        return Stream.of(
            NbtOps.INSTANCE,
            JavaOps.INSTANCE,
            JsonOps.INSTANCE
        )
            .map(ops -> RegistryHelper.getRegistry().createSerializationContext(ops))
            .toList();
    }

    @ParameterizedTest(name = PARAMETERIZED_NAME)
    @MethodSource({"invalidData"})
    void invalidThrows(final Tag input) {
        assertThrows(RuntimeException.class, () -> {
            require(
                COMPONENT_CODEC.decode(NbtOps.INSTANCE, input),
                msg -> "Failed to decode " + input + ": " + msg
            );
        });
    }

    static <A, O> void testDirectRoundTrip(final DynamicOps<O> ops, final Codec<A> codec, final A adventure) {
        final O encoded = require(
            codec.encodeStart(ops, adventure),
            msg -> "Failed to encode " + adventure + ": " + msg
        );
        final Pair<A, O> roundTripResult = require(
            codec.decode(ops, encoded),
            msg -> "Failed to decode " + encoded + ": " + msg
        );
        assertEquals(adventure, roundTripResult.getFirst());
    }

    static <A, M, O> void testMinecraftRoundTrip(final DynamicOps<O> ops, final Codec<A> adventureCodec, final Codec<M> minecraftCodec, final A adventure) {
        final O encoded = require(
            adventureCodec.encodeStart(ops, adventure),
            msg -> "Failed to encode " + adventure + ": " + msg
        );
        final M minecraftResult = require(
            minecraftCodec.decode(ops, encoded),
            msg -> "Failed to decode to Minecraft: " + encoded + "; " + msg
        ).getFirst();
        final O minecraftReEncoded = require(
            minecraftCodec.encodeStart(ops, minecraftResult),
            msg -> "Failed to re-encode Minecraft: " + minecraftResult + "; " + msg
        );
        final Pair<A, O> roundTripResult = require(
            adventureCodec.decode(ops, minecraftReEncoded),
            msg -> "Failed to decode " + minecraftReEncoded + ": " + msg
        );
        assertEquals(adventure, roundTripResult.getFirst());
    }

    static <R> R require(final DataResult<R> result, final Function<String, String> errorMessage) {
        return result.getOrThrow(s -> new RuntimeException(errorMessage.apply(s)));
    }

    static List<Tag> invalidData() {
        return List.of(
            IntTag.valueOf(-1),
            ByteTag.ZERO,
            new CompoundTag(),
            new ListTag()
        );
    }

    static List<Style> testStyles() {
        return List.of(
            Style.empty(),
            style(color(0x0a1ab9)),
            style(NamedTextColor.LIGHT_PURPLE),
            style(TextDecoration.BOLD),
            style(TextDecoration.BOLD.withState(false)),
            style(TextDecoration.BOLD.withState(TextDecoration.State.NOT_SET)),
            style()
                .font(key("kyori", "kittens"))
                .color(NamedTextColor.RED)
                .decoration(TextDecoration.BOLD, true)
                .clickEvent(openUrl("https://github.com"))
                .build(),
            style()
                .hoverEvent(HoverEvent.showEntity(HoverEvent.ShowEntity.showEntity(
                    Key.key(Key.MINECRAFT_NAMESPACE, "pig"),
                    UUID.randomUUID(),
                    Component.text("Dolores", TextColor.color(0x0a1ab9))
                )))
                .build()
        );
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
    @MethodParameterSource({
        "testTexts", "testTranslatables", "testKeybinds", "testScores",
        "testSelectors", "testBlockNbts", "testEntityNbts", "testStorageNbts"
    })
    @interface TestComponents {
    }

    static List<Component> testTexts() {
        return List.of(
            Component.empty(),
            text("Hello, world."),
            text().content("c")
                .color(NamedTextColor.GOLD)
                .append(text("o", NamedTextColor.DARK_AQUA))
                .append(text("l", NamedTextColor.LIGHT_PURPLE))
                .append(text("o", NamedTextColor.DARK_PURPLE))
                .append(text("u", NamedTextColor.BLUE))
                .append(text("r", NamedTextColor.DARK_GREEN))
                .append(text("s", NamedTextColor.RED))
                .build(),
            text().content("This is a test.")
                .color(NamedTextColor.DARK_PURPLE)
                .hoverEvent(HoverEvent.showText(text("A test.")))
                .append(text(" "))
                .append(text("A what?", NamedTextColor.DARK_AQUA))
                .build(),
            text().append(text("Hello")).build(),
            miniMessage().deserialize("<rainbow>|||||||||||||||||||||||<bold>|||||||||||||</bold>|||||||||")
        );
    }

    static List<Component> testTranslatables() {
        final String key = "multiplayer.player.left";
        final UUID id = UUID.fromString("eb121687-8b1a-4944-bd4d-e0a818d9dfe2");
        final String name = "kashike";
        final String command = String.format("/msg %s ", name);

        return List.of(
            translatable(key),
            translatable()
                .key("thisIsA")
                .fallback("This is a test.")
                .build(),
            translatable(key, numeric(Integer.MAX_VALUE), text("HEY")), // boolean doesn't work in vanilla, can't test here
            translatable(
                key,
                text().content(name)
                    .clickEvent(suggestCommand(command))
                    .hoverEvent(showEntity(HoverEvent.ShowEntity.showEntity(
                        key("minecraft", "player"),
                        id,
                        text(name)
                    )))
                    .build()
            ).color(NamedTextColor.YELLOW)
        );
    }

    static List<Component> testKeybinds() {
        return List.of(keybind("key.jump"));
    }

    static List<Component> testScores() {
        final String name = "abc";
        final String objective = "def";

        return List.of(score(name, objective));
    }

    static List<Component> testSelectors() {
        final String selector = "@p";

        return List.of(
            selector(selector),
            selector(selector, text(','))
        );
    }

    static List<Component> testBlockNbts() {
        return List.of(
            blockNBT().nbtPath("abc").localPos(1.23d, 2.0d, 3.89d).build(),
            blockNBT().nbtPath("xyz").absoluteWorldPos(4, 5, 6).interpret(true).build(),
            blockNBT().nbtPath("eeee").relativeWorldPos(7, 83, 900)
                .separator(text(';'))
                .build(),
            blockNBT().nbtPath("qwert").worldPos(
                BlockNBTComponent.WorldPos.Coordinate.absolute(12),
                BlockNBTComponent.WorldPos.Coordinate.relative(3),
                BlockNBTComponent.WorldPos.Coordinate.absolute(1200)
            ).build()
        );
    }

    static List<Component> testEntityNbts() {
        return List.of(
            entityNBT().nbtPath("abc").selector("test").build(),
            entityNBT().nbtPath("abc").selector("test").separator(text(',')).build(),
            entityNBT().nbtPath("abc").selector("test").interpret(true).build()
        );
    }

    static List<Component> testStorageNbts() {
        return List.of(
            storageNBT().nbtPath("abc").storage(key("doom:apple")).build(),
            storageNBT().nbtPath("abc").storage(key("doom:apple")).separator(text(", ")).build(),
            storageNBT().nbtPath("abc").storage(key("doom:apple")).interpret(true).build()
        );
    }
}
