/*
 * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @author GeyserMC
 * @link https://github.com/GeyserMC/Geyser
 */

package org.geysermc.geyser.registry.populator;

import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SortedSetMultimap;
import com.google.gson.reflect.TypeToken;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.nbt.NbtUtils;
import org.cloudburstmc.protocol.bedrock.codec.v1001.Bedrock_v1001;
import org.cloudburstmc.protocol.bedrock.codec.v2168.Bedrock_v2168;
import org.cloudburstmc.protocol.bedrock.codec.v924.Bedrock_v924;
import org.cloudburstmc.protocol.bedrock.codec.v944.Bedrock_v944;
import org.cloudburstmc.protocol.bedrock.codec.v975.Bedrock_v975;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.definitions.SimpleItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.inventory.CreativeItemCategory;
import org.cloudburstmc.protocol.bedrock.data.inventory.CreativeItemData;
import org.cloudburstmc.protocol.bedrock.data.inventory.CreativeItemGroup;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemVersion;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.NonVanillaCustomBlockData;
import org.geysermc.geyser.api.item.custom.v2.CustomItemBedrockOptions;
import org.geysermc.geyser.api.item.custom.v2.CustomItemDefinition;
import org.geysermc.geyser.api.item.custom.v2.NonVanillaCustomItemDefinition;
import org.geysermc.geyser.api.item.custom.v2.component.geyser.GeyserBlockPlacer;
import org.geysermc.geyser.api.item.custom.v2.component.geyser.GeyserItemDataComponents;
import org.geysermc.geyser.api.predicate.MinecraftPredicate;
import org.geysermc.geyser.api.predicate.context.item.ItemPredicateContext;
import org.geysermc.geyser.api.util.CreativeCategory;
import org.geysermc.geyser.api.util.Identifier;
import org.geysermc.geyser.inventory.item.StoredItemMappings;
import org.geysermc.geyser.item.GeyserCustomMappingData;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.TooltipOptions;
import org.geysermc.geyser.item.custom.GeyserCustomItemDefinition;
import org.geysermc.geyser.item.custom.impl.predicates.GeyserRangeDispatchPredicate;
import org.geysermc.geyser.item.exception.InvalidItemComponentsException;
import org.geysermc.geyser.item.type.BlockItem;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.level.block.property.Properties;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.populator.conversion.ChaosCubedConverter;
import org.geysermc.geyser.registry.populator.conversion.GoldenDandelionConverter;
import org.geysermc.geyser.registry.type.BlockMappings;
import org.geysermc.geyser.registry.type.CustomSkull;
import org.geysermc.geyser.registry.type.GeyserBedrockBlock;
import org.geysermc.geyser.registry.type.GeyserMappingItem;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.registry.type.NonVanillaItemRegistration;
import org.geysermc.geyser.registry.type.PaletteItem;
import org.geysermc.geyser.translator.item.BedrockItemBuilder;
import org.geysermc.geyser.util.JsonUtils;
import org.geysermc.geyser.util.MinecraftKey;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;

import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Populates the item registries.
 */
public class ItemRegistryPopulator {

    @SuppressWarnings("unused") // kept here for convenience when updating
    record PaletteVersion(String version, int protocolVersion, String creativeVersion, Map<Item, Item> javaOnlyItems, Remapper remapper) {

        public PaletteVersion(String version, int protocolVersion) {
            this(version, protocolVersion, null, Collections.emptyMap(), (item, mapping) -> mapping);
        }

        public PaletteVersion(String version, int protocolVersion, Map<Item, Item> javaOnlyItems) {
            this(version, protocolVersion, null, javaOnlyItems, (item, mapping) -> mapping);
        }

        public PaletteVersion(String version, int protocolVersion, Remapper remapper) {
            this(version, protocolVersion, null, Collections.emptyMap(), remapper);
        }

        public PaletteVersion(String version, int protocolVersion, String creativeVersion) {
            this(version, protocolVersion, creativeVersion, Collections.emptyMap(), (item, mapping) -> mapping);
        }

        public PaletteVersion(String version, int protocolVersion, Map<Item, Item> javaOnlyItems, String creativeVersion) {
            this(version, protocolVersion, creativeVersion, javaOnlyItems, (item, mapping) -> mapping);
        }

        public String creativeVersion() {
            return creativeVersion == null ? version : creativeVersion;
        }
    }

    @FunctionalInterface
    interface Remapper {
        @NonNull
        GeyserMappingItem remap(Item item, GeyserMappingItem mapping);
    }

    public static void populate() {
        List<PaletteVersion> paletteVersions = new ArrayList<>(4);
        paletteVersions.add(new PaletteVersion("26_0", Bedrock_v924.CODEC.getProtocolVersion(), GoldenDandelionConverter.convertItem()));
        paletteVersions.add(new PaletteVersion("26_10", Bedrock_v944.CODEC.getProtocolVersion(), ChaosCubedConverter.convertItem()));
        paletteVersions.add(new PaletteVersion("26_20", Bedrock_v975.CODEC.getProtocolVersion(), ChaosCubedConverter.convertItem(), "26_10"));
        paletteVersions.add(new PaletteVersion("26_30", Bedrock_v1001.CODEC.getProtocolVersion()));
        paletteVersions.add(new PaletteVersion("26_40", Bedrock_v2168.CODEC.getProtocolVersion()));

        GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap();

        Type mappingItemsType = new TypeToken<Map<String, GeyserMappingItem>>() { }.getType();

        Map<String, GeyserMappingItem> items;
        try (InputStream stream = bootstrap.getResourceOrThrow("mappings/items.json")) {
            // Load item mappings from Java Edition to Bedrock Edition
            items = JsonUtils.fromJson(stream, mappingItemsType);
        } catch (Exception e) {
            throw new AssertionError("Unable to load Java runtime item IDs", e);
        }

        Multimap<Identifier, CustomItemDefinition> customItems = MultimapBuilder.hashKeys().arrayListValues().build();
        Multimap<Identifier, NonVanillaCustomItemDefinition> nonVanillaCustomItems = MultimapBuilder.hashKeys().arrayListValues().build();

        CustomItemRegistryPopulator.populate(items, customItems, nonVanillaCustomItems);

        // We can reduce some operations as Java information is the same across all palette versions
        boolean firstMappingsPass = true;

        /* Load item palette */
        for (PaletteVersion palette : paletteVersions) {
            Type paletteEntriesType = new TypeToken<List<PaletteItem>>() { }.getType();

            List<PaletteItem> itemEntries;
            try (InputStream stream = bootstrap.getResourceOrThrow(String.format("bedrock/runtime_item_states.%s.json", palette.version()))) {
                itemEntries = JsonUtils.fromJson(stream, paletteEntriesType);
            } catch (Exception e) {
                throw new AssertionError("Unable to load Bedrock runtime item IDs", e);
            }

            NbtMap vanillaComponents;
            try (InputStream stream = bootstrap.getResourceOrThrow("bedrock/item_components.%s.nbt".formatted(palette.version()))) {
                vanillaComponents = (NbtMap) NbtUtils.createGZIPReader(stream, true, true).readTag();
            } catch (Exception e) {
                throw new AssertionError("Unable to load Bedrock item components", e);
            }

            // Used for custom items
            int nextFreeBedrockId = 0;
            Int2ObjectMap<ItemDefinition> registry = new Int2ObjectOpenHashMap<>();
            Map<String, ItemDefinition> definitions = new Object2ObjectLinkedOpenHashMap<>();

            for (PaletteItem entry : itemEntries) {
                int id = entry.getId();
                if (id >= nextFreeBedrockId) {
                    nextFreeBedrockId = id + 1;
                }

                // Some items, e.g. food, are not component based but still have components
                NbtMap components = vanillaComponents.getCompound(entry.getName());
                if (components == null && entry.isComponentBased()) {
                    // FIXME needs a proper item components file update
                    if (!entry.getName().contains("lava_chicken")) {
                        throw new RuntimeException("Could not find vanilla components for vanilla component based item! " + entry.getName());
                    } else {
                        components = NbtMap.EMPTY;
                    }
                }

                ItemDefinition definition = new SimpleItemDefinition(entry.getName().intern(), id, ItemVersion.from(entry.getVersion()), entry.isComponentBased(), components);

                // Some item on Bedrock Edition have a different stack size, so we're changing that through the component.
                // This resolve https://github.com/GeyserMC/Geyser/issues/5612 and https://github.com/GeyserMC/Geyser/issues/4905
                if (definition.getIdentifier().equals("minecraft:cake")) {
                    definition = new SimpleItemDefinition(entry.getName().intern(), id, ItemVersion.from(entry.getVersion()), true, fromItemDefinitionToDataDriven(definition, 1, null, null, false));
                } else if (definition.getIdentifier().equals("minecraft:armor_stand")) {
                    // You have to change the item version to data driven for armor stand else this won't work.
                    definition = new SimpleItemDefinition(entry.getName().intern(), id, ItemVersion.DATA_DRIVEN, true, fromItemDefinitionToDataDriven(definition, 16, "armor_stand", "item.armor_stand.name", false));
                } else if (definition.getIdentifier().equals("minecraft:firework_rocket")) {
                    // For fireworks rocket, we purposely make this item data driven so now bedrock won't do client-sided boosting
                    // and now we can control fireworks boost ourselves! This resolve https://github.com/GeyserMC/Geyser/issues/5409
                    definition = new SimpleItemDefinition(entry.getName().intern(), id, ItemVersion.DATA_DRIVEN, true, fromItemDefinitionToDataDriven(definition, 64, "fireworks", "item.fireworks.name", true));
                }

                definitions.put(entry.getName(), definition);
                registry.put(definition.getRuntimeId(), definition);
            }

            Object2ObjectMap<String, BlockDefinition> bedrockBlockIdOverrides = new Object2ObjectOpenHashMap<>();
            Object2IntMap<String> blacklistedIdentifiers = new Object2IntOpenHashMap<>();

            Object2ObjectMap<CustomBlockData, ItemDefinition> customBlockItemDefinitions = new Object2ObjectOpenHashMap<>();

            List<ItemDefinition> buckets = new ObjectArrayList<>();

            List<ItemMapping> mappings = new ObjectArrayList<>(Registries.JAVA_ITEMS.get().size());
            while (Registries.JAVA_ITEMS.get().size() >= mappings.size()) {
                mappings.add(ItemMapping.AIR);
            }
            // Temporary mapping to create stored items
            Map<Item, ItemMapping> javaItemToMapping = new Object2ObjectOpenHashMap<>();

            List<CreativeItemData> creativeItems = new ArrayList<>();
            Set<String> noBlockDefinitions = new ObjectOpenHashSet<>();

            // Fix: Usage of structure blocks/voids in recipes
            // https://github.com/GeyserMC/Geyser/issues/2890
            noBlockDefinitions.add("minecraft:structure_block");
            noBlockDefinitions.add("minecraft:structure_void");

            AtomicInteger creativeNetId = new AtomicInteger();
            CreativeItemRegistryPopulator.populate(palette, definitions, items, (itemBuilder, groupId) -> {
                ItemData item = itemBuilder.netId(creativeNetId.incrementAndGet()).build();
                creativeItems.add(new CreativeItemData(item, item.getNetId(), groupId));

                if (item.getBlockDefinition() != null) {
                    String identifier = item.getDefinition().getIdentifier();

                    // Add override for item mapping, unless it already exists... then we know multiple states can exist
                    if (!blacklistedIdentifiers.containsKey(identifier)) {
                        if (bedrockBlockIdOverrides.containsKey(identifier)) {
                            bedrockBlockIdOverrides.remove(identifier);
                            // Save this as a blacklist, but also as knowledge of what the block state name should be
                            blacklistedIdentifiers.put(identifier, item.getBlockDefinition().getRuntimeId());
                        } else {
                            // Unless there's multiple possibilities for this one state, let this be
                            bedrockBlockIdOverrides.put(identifier, item.getBlockDefinition());
                        }
                    }
                } else {
                    // Item mappings should also NOT have a block definition for these.
                    noBlockDefinitions.add(item.getDefinition().getIdentifier());
                }
            });

            List<CreativeItemGroup> creativeItemGroups = new ObjectArrayList<>();
            Map<String, Integer> creativeGroupIds = new Object2IntOpenHashMap<>();
            Map<CreativeItemCategory, Integer> lastCreativeGroupIds = new Object2IntOpenHashMap<>();
            CreativeItemRegistryPopulator.readCreativeItemGroups(palette, creativeItems, creativeItemGroups, creativeGroupIds, lastCreativeGroupIds);

            BlockMappings blockMappings = BlockRegistries.BLOCKS.forVersion(palette.protocolVersion());

            Set<Item> javaOnlyItems = new ObjectOpenHashSet<>();
            Collections.addAll(javaOnlyItems, Items.SPECTRAL_ARROW, Items.TIPPED_ARROW, Items.KNOWLEDGE_BOOK, Items.DEBUG_STICK);
            if (!GeyserImpl.getInstance().config().gameplay().enableCustomContent()) {
                javaOnlyItems.add(Items.FURNACE_MINECART);
                if (!GeyserImpl.getInstance().config().gameplay().enableIntegratedPack()) {
                    Collections.addAll(javaOnlyItems, Items.TEST_BLOCK, Items.TEST_INSTANCE_BLOCK);
                }
            }
            // Java-only items for this version
            javaOnlyItems.addAll(palette.javaOnlyItems().keySet());

            Int2ObjectMap<String> customIdMappings = new Int2ObjectOpenHashMap<>();
            Set<Identifier> registeredCustomItems = new ObjectOpenHashSet<>(); // This is used to check for duplicate item names

            for (Map.Entry<String, GeyserMappingItem> entry : items.entrySet()) {
                Item javaItem = Registries.JAVA_ITEM_IDENTIFIERS.get(entry.getKey());
                if (javaItem == null) {
                    throw new RuntimeException("Extra item in mappings? " + entry.getKey());
                }
                GeyserMappingItem mappingItem;
                Item replacementItem = palette.javaOnlyItems().get(javaItem);
                if (replacementItem != null) {
                    mappingItem = items.get(replacementItem.javaIdentifier()); // java only item, a java id fallback has been provided
                } else {
                    // check if any mapping changes need to be made on this version
                    mappingItem = palette.remapper().remap(javaItem, entry.getValue());
                }

                String bedrockIdentifier = mappingItem.getBedrockIdentifier();
                ItemDefinition definition = definitions.get(bedrockIdentifier);
                if (definition == null) {
                    throw new RuntimeException("Missing Bedrock ItemDefinition in version " + palette.version() + " for mapping: " + mappingItem);
                }

                BlockDefinition bedrockBlock = null;
                Integer firstBlockRuntimeId = entry.getValue().getFirstBlockRuntimeId();
                BlockDefinition customBlockItemOverride = null;
                if (firstBlockRuntimeId != null) {
                    BlockDefinition blockOverride = bedrockBlockIdOverrides.get(bedrockIdentifier);

                    // We'll do this here for custom blocks we want in the creative inventory so we can piggyback off the existing logic to find these
                    // blocks in creativeItems
                    CustomBlockData customBlockData = BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.getOrDefault(javaItem.javaIdentifier(), null);
                    if (customBlockData != null) {
                        // this block has a custom item override and thus we should use its runtime ID for the ItemMapping
                        if (customBlockData.includedInCreativeInventory()) {
                            CustomBlockState customBlockState = customBlockData.defaultBlockState();
                            customBlockItemOverride = blockMappings.getCustomBlockStateDefinitions().getOrDefault(customBlockState, null);
                        }
                    }

                    // If it' s a custom block we can't do this because we need to make sure we find the creative item
                    if (blockOverride != null && customBlockItemOverride == null) {
                        // Straight from BDS is our best chance of getting an item that doesn't run into issues
                        bedrockBlock = blockOverride;
                    } else {
                        // Try to get an example block runtime ID from the creative contents packet, for Bedrock identifier obtaining
                        int aValidBedrockBlockId = blacklistedIdentifiers.getOrDefault(bedrockIdentifier, customBlockItemOverride != null ? customBlockItemOverride.getRuntimeId() : -1);
                        if (aValidBedrockBlockId == -1 && customBlockItemOverride == null) {
                            // Fallback
                            if (!noBlockDefinitions.contains(entry.getValue().getBedrockIdentifier())) {
                                bedrockBlock = blockMappings.getBedrockBlock(firstBlockRuntimeId);
                            }
                        } else {
                            // As of 1.16.220, every item requires a block runtime ID attached to it.
                            // This is mostly for identifying different blocks with the same item ID - wool, slabs, some walls.
                            // However, in order for some visuals and crafting to work, we need to send the first matching block state
                            // as indexed by Bedrock's block palette
                            // There are exceptions! But, ideally, the block ID override should take care of those.
                            NbtMapBuilder requiredBlockStatesBuilder = NbtMap.builder();
                            String correctBedrockIdentifier = blockMappings.getDefinition(aValidBedrockBlockId).getState().getString("name");
                            boolean firstPass = true;
                            // Block states are all grouped together. In the mappings, we store the first block runtime ID in order,
                            // and the last, if relevant. We then iterate over all those values and get their Bedrock equivalents
                            int lastBlockRuntimeId = entry.getValue().getLastBlockRuntimeId() == null ? firstBlockRuntimeId : entry.getValue().getLastBlockRuntimeId();
                            for (int i = firstBlockRuntimeId; i <= lastBlockRuntimeId; i++) {
                                GeyserBedrockBlock bedrockBlockRuntimeId = blockMappings.getVanillaBedrockBlock(i);
                                NbtMap blockTag = bedrockBlockRuntimeId.getState();
                                String bedrockName = blockTag.getString("name");
                                if (!bedrockName.equals(correctBedrockIdentifier)) {
                                    continue;
                                }
                                NbtMap states = blockTag.getCompound("states");

                                if (firstPass) {
                                    firstPass = false;
                                    if (states.isEmpty()) {
                                        // No need to iterate and find all block states - this is the one, as there can't be any others
                                        bedrockBlock = bedrockBlockRuntimeId;
                                        break;
                                    }
                                    requiredBlockStatesBuilder.putAll(states);
                                    continue;
                                }
                                for (Map.Entry<String, Object> nbtEntry : states.entrySet()) {
                                    Object value = requiredBlockStatesBuilder.get(nbtEntry.getKey());
                                    if (value != null && !nbtEntry.getValue().equals(value)) { // Null means this value has already been removed/deemed as unneeded
                                        // This state can change between different block states, and therefore is not required
                                        // to build a successful block state of this
                                        requiredBlockStatesBuilder.remove(nbtEntry.getKey());
                                    }
                                }
                                if (requiredBlockStatesBuilder.isEmpty()) {
                                    // There are no required block states
                                    // E.G. there was only a direction property that is no longer in play
                                    // (States that are important include color for glass)
                                    break;
                                }
                            }

                            NbtMap requiredBlockStates = requiredBlockStatesBuilder.build();
                            if (bedrockBlock == null) {
                                // We need to loop around again (we can't cache the block tags above) because Bedrock can include states that we don't have a pairing for
                                // in it's "preferred" block state - I.E. the first matching block state in the list
                                for (GeyserBedrockBlock block : blockMappings.getBedrockRuntimeMap()) {
                                    if (block == null) {
                                        continue;
                                    }
                                    NbtMap blockTag = block.getState();
                                    if (blockTag.getString("name").equals(correctBedrockIdentifier)) {
                                        NbtMap states = blockTag.getCompound("states");
                                        boolean valid = true;
                                        for (Map.Entry<String, Object> nbtEntry : requiredBlockStates.entrySet()) {
                                            if (!states.get(nbtEntry.getKey()).equals(nbtEntry.getValue())) {
                                                // A required block state doesn't match - this one is not valid
                                                valid = false;
                                                break;
                                            }
                                        }
                                        if (valid) {
                                            bedrockBlock = block;
                                            break;
                                        }
                                    }
                                }
                                if (bedrockBlock == null) {
                                    throw new RuntimeException("Could not find a block match for " + entry.getKey());
                                }
                            }

                            // Because we have replaced the Bedrock block ID, we also need to replace the creative contents block runtime ID
                            // That way, creative items work correctly for these blocks

                            // Set our custom block override now if there is one
                            if (customBlockItemOverride != null) {
                                bedrockBlock = customBlockItemOverride;
                            }

                            for (int j = 0; j < creativeItems.size(); j++) {
                                CreativeItemData itemData = creativeItems.get(j);
                                if (itemData.getItem().getDefinition().equals(definition)) {
                                    if (itemData.getItem().getDamage() != 0) {
                                        break;
                                    }

                                    NbtMap states = ((GeyserBedrockBlock) itemData.getItem().getBlockDefinition()).getState().getCompound("states");

                                    boolean valid = true;
                                    for (Map.Entry<String, Object> nbtEntry : requiredBlockStates.entrySet()) {
                                        if (!Objects.equals(states.get(nbtEntry.getKey()), nbtEntry.getValue())) {
                                            // A required block state doesn't match - this one is not valid
                                            valid = false;
                                            break;
                                        }
                                    }
                                    if (valid) {
                                        if (customBlockItemOverride != null && customBlockData != null) {
                                            // Assuming this is a valid custom block override we'll just register it now while we have the creative item
                                            int customProtocolId = nextFreeBedrockId++;
                                            mappingItem = mappingItem.withBedrockData(customProtocolId);
                                            bedrockIdentifier = customBlockData.identifier();
                                            definition = new SimpleItemDefinition(bedrockIdentifier, customProtocolId, ItemVersion.DATA_DRIVEN, true, NbtMap.EMPTY);
                                            registry.put(customProtocolId, definition);
                                            customBlockItemDefinitions.put(customBlockData, definition);
                                            customIdMappings.put(customProtocolId, bedrockIdentifier);

                                            CreativeItemCategory category = customBlockData.creativeCategory() == null ? CreativeItemCategory.UNDEFINED :
                                                CreativeItemCategory.values()[customBlockData.creativeCategory().ordinal()];

                                            CreativeItemData newData = new CreativeItemData(itemData.getItem().toBuilder()
                                                .definition(definition)
                                                .blockDefinition(bedrockBlock)
                                                .netId(itemData.getNetId())
                                                .count(1)
                                                .build(), itemData.getNetId(), getCreativeIndex(
                                                    customBlockData.creativeGroup(),
                                                    category,
                                                    creativeGroupIds,
                                                    lastCreativeGroupIds, creativeItemGroups)
                                                );

                                            creativeItems.set(j, newData);
                                        } else {
                                            CreativeItemData creativeItemData = new CreativeItemData(itemData.getItem().toBuilder()
                                                .blockDefinition(bedrockBlock)
                                                .build(), itemData.getNetId(), itemData.getGroupId());

                                            creativeItems.set(j, creativeItemData);
                                        }
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }

                ItemMapping.ItemMappingBuilder mappingBuilder = ItemMapping.builder()
                        .bedrockIdentifier(bedrockIdentifier.intern())
                        .bedrockDefinition(definition)
                        .bedrockData(mappingItem.getBedrockData())
                        .bedrockBlockDefinition(bedrockBlock)
                        .javaItem(javaItem);

                if (javaOnlyItems.contains(javaItem)) {
                    // These items don't exist on Bedrock, so set up a variable that indicates they should have custom names
                    mappingBuilder = mappingBuilder.translationString((javaItem instanceof BlockItem ? "block." : "item.") + entry.getKey().replace(":", "."));
                    GeyserImpl.getInstance().getLogger().debug("Adding " + entry.getKey() + " as an item that needs to be translated.");
                }

                // Add the custom item properties, if applicable
                boolean containsOldMappings = false;
                SortedSetMultimap<Key, GeyserCustomMappingData> customItemDefinitions;
                Collection<CustomItemDefinition> customItemsToLoad = customItems.get(Identifier.of(javaItem.javaIdentifier()));
                if (!customItemsToLoad.isEmpty()) {
                    customItemDefinitions = MultimapBuilder.hashKeys(customItemsToLoad.size()).treeSetValues(new CustomItemDefinitionComparator()).build();

                    for (CustomItemDefinition customItem : customItemsToLoad) {
                        int customProtocolId = nextFreeBedrockId++;

                        Identifier customItemIdentifier = customItem.bedrockIdentifier();
                        if (!registeredCustomItems.add(customItemIdentifier)) {
                            if (firstMappingsPass) {
                                GeyserImpl.getInstance().getLogger().error("Custom item '" + customItemIdentifier + "' already exists and was registered again! Skipping...");
                            }
                            continue;
                        }

                        try {
                            GeyserCustomMappingData customMapping = CustomItemRegistryPopulator.registerCustomItem(javaItem, mappingItem, customItem, customProtocolId, palette.protocolVersion, firstMappingsPass);

                            if (customItem.bedrockOptions().creativeCategory() != CreativeCategory.NONE) {
                                CreativeItemData creativeItemData = new CreativeItemData(ItemData.builder()
                                    .netId(creativeNetId.incrementAndGet())
                                    .definition(customMapping.itemDefinition())
                                    .blockDefinition(null)
                                    .count(1)
                                    .build(), creativeNetId.get(), getCreativeIndex(
                                        customItem.bedrockOptions().creativeGroup(),
                                        CreativeItemCategory.values()[customItem.bedrockOptions().creativeCategory().id()],
                                        creativeGroupIds,
                                        lastCreativeGroupIds,
                                        creativeItemGroups)
                                    );
                                creativeItems.add(creativeItemData);
                            }

                            // See CustomItemTranslator for a full reason; but essentially, some servers have v1 mappings AND send item_models
                            // Converted v1 mappings assume a vanilla model is used... tadaa
                            if (customItem instanceof GeyserCustomItemDefinition customItemDefinition && customItemDefinition.isOldConvertedItem()) {
                                containsOldMappings = true;
                            }

                            // ComponentItemData - used to register some custom properties
                            customItemDefinitions.put(MinecraftKey.identifierToKey(customItem.model()), customMapping);
                            registry.put(customMapping.integerId(), customMapping.itemDefinition());

                            customIdMappings.put(customMapping.integerId(), customItemIdentifier.toString());
                        } catch (InvalidItemComponentsException exception) {
                            if (firstMappingsPass) {
                                GeyserImpl.getInstance().getLogger().error("Not registering custom item (bedrock identifier=" + customItem.bedrockIdentifier() + ")!", exception);
                            }
                        }
                    }
                } else {
                    customItemDefinitions = null;
                }
                mappingBuilder.customItemDefinitions(customItemDefinitions);
                mappingBuilder.containsV1Mappings(containsOldMappings);

                ItemMapping mapping = mappingBuilder.build();

                if (javaItem.javaIdentifier().contains("bucket") && !javaItem.javaIdentifier().contains("milk")) {
                    buckets.add(definition);
                }

                mappings.set(javaItem.javaId(), mapping);
                javaItemToMapping.put(javaItem, mapping);
            }

            // Add the light block level since it doesn't exist on java but we need it for item conversion
            Int2ObjectMap<ItemMapping> lightBlocks = new Int2ObjectOpenHashMap<>();

            for (int i = 0; i <= Properties.LEVEL.high(); i++) {
                ItemDefinition lightBlock = definitions.get("minecraft:light_block_" + i);
                if (lightBlock == null) {
                    break;
                }

                ItemMapping lightBlockEntry = ItemMapping.builder()
                    .javaItem(Items.LIGHT)
                    .bedrockIdentifier("minecraft:light_block_" + i)
                    .bedrockDefinition(lightBlock)
                    .bedrockData(0)
                    .bedrockBlockDefinition(null)
                    .customItemDefinitions(null)
                    .build();
                lightBlocks.put(lightBlock.getRuntimeId(), lightBlockEntry);
            }

            ItemDefinition lodestoneCompass = definitions.get("minecraft:lodestone_compass");
            if (lodestoneCompass == null) {
                throw new RuntimeException("Lodestone compass not found in item palette!");
            }

            // Add the lodestone compass since it doesn't exist on java but we need it for item conversion
            ItemMapping lodestoneEntry = ItemMapping.builder()
                    .javaItem(Items.COMPASS)
                    .bedrockIdentifier("minecraft:lodestone_compass")
                    .bedrockDefinition(lodestoneCompass)
                    .bedrockData(0)
                    .bedrockBlockDefinition(null)
                    .customItemDefinitions(null)
                    .build();

            final IntSet nonVanillaCustomItemIds = new IntOpenHashSet();

            // Register any completely custom items given to us
            IntSet registeredJavaIds = new IntOpenHashSet(); // Used to check for duplicate item java ids
            for (NonVanillaCustomItemDefinition customItem : nonVanillaCustomItems.values()) {
                if (!registeredJavaIds.add(customItem.javaId())) {
                    // This should never happen since we validate for this in the CustomItemRegistryPopulator
                    throw new IllegalStateException("Custom item java id " + customItem.javaId() + " already exists and was registered again!");
                }

                int customItemId = nextFreeBedrockId++;
                try {
                    NonVanillaItemRegistration registration = CustomItemRegistryPopulator.registerCustomItem(customItem, customItemId, palette.protocolVersion, firstMappingsPass);

                    ItemMapping mapping = registration.mapping();
                    Item javaItem = registration.javaItem();
                    while (javaItem.javaId() >= mappings.size()) {
                        // Fill with empty to get to the correct size
                        mappings.add(ItemMapping.AIR);
                    }
                    mappings.set(javaItem.javaId(), mapping);
                    registry.put(customItemId, mapping.getBedrockDefinition());

                    nonVanillaCustomItemIds.add(javaItem.javaId());

                    if (customItem.bedrockOptions().creativeCategory() != CreativeCategory.NONE) {
                        CreativeItemData creativeItemData = new CreativeItemData(ItemData.builder()
                            .definition(registration.mapping().getBedrockDefinition())
                            .netId(creativeNetId.incrementAndGet())
                            .count(1)
                            .build(), creativeNetId.get(),
                            getCreativeIndex(customItem.bedrockOptions().creativeGroup(),
                                CreativeItemCategory.values()[customItem.bedrockOptions().creativeCategory().id()],
                                creativeGroupIds,lastCreativeGroupIds,
                                creativeItemGroups)
                    );

                        creativeItems.add(creativeItemData);
                    }
                } catch (InvalidItemComponentsException exception) {
                    GeyserImpl.getInstance().getLogger().error("Not registering non-vanilla custom item (identifier=" + customItem.identifier() + ")!", exception);
                }
            }

            Set<CustomBlockData> skullBlocks = new ObjectOpenHashSet<>();
            for (CustomSkull skull : BlockRegistries.CUSTOM_SKULLS.get().values()) {
                skullBlocks.add(skull.getCustomBlockData());
            }

            // Register the item forms of custom blocks
            for (CustomBlockData customBlock : BlockRegistries.CUSTOM_BLOCKS.get()) {
                // We might've registered it already with the vanilla blocks so check first
                if (customBlockItemDefinitions.containsKey(customBlock)) {
                    continue;
                }

                // Non-vanilla custom blocks will be handled in the item
                // registry, so we don't need to do anything here.
                if (customBlock instanceof NonVanillaCustomBlockData) {
                    continue;
                }

                int customProtocolId = nextFreeBedrockId++;
                String identifier = customBlock.identifier();

                // Bedrock only renders the icon of a block item once it has loaded that block, which leaves custom skulls
                // showing as the vanilla player head until then. Registering them as custom items lets them carry their own icon.
                ItemDefinition definition = null;
                if (skullBlocks.contains(customBlock)) {
                    try {
                        definition = registerSkullItem(identifier, items.get("minecraft:player_head"), customProtocolId, palette.protocolVersion, firstMappingsPass);
                    } catch (InvalidItemComponentsException exception) {
                        GeyserImpl.getInstance().getLogger().error("Failed to register the item form of custom skull " + identifier + "!", exception);
                    }
                }
                if (definition == null) {
                    definition = new SimpleItemDefinition(identifier, customProtocolId, ItemVersion.NONE, false, null);
                }
                registry.put(customProtocolId, definition);
                customBlockItemDefinitions.put(customBlock, definition);
                customIdMappings.put(customProtocolId, identifier);

                GeyserBedrockBlock bedrockBlock = blockMappings.getCustomBlockStateDefinitions().getOrDefault(customBlock.defaultBlockState(), null);

                if (bedrockBlock != null && customBlock.includedInCreativeInventory()) {
                    CreativeItemCategory category = customBlock.creativeCategory() == null ? CreativeItemCategory.UNDEFINED :
                        CreativeItemCategory.values()[customBlock.creativeCategory().ordinal()];

                    CreativeItemData creativeItemData = new CreativeItemData(ItemData.builder()
                        .definition(definition)
                        .blockDefinition(bedrockBlock)
                        .netId(creativeNetId.incrementAndGet())
                        .count(1)
                        .build(), creativeNetId.get(), getCreativeIndex(
                            customBlock.creativeGroup(),
                            category,
                            creativeGroupIds,
                            lastCreativeGroupIds, creativeItemGroups)
                        );
                    creativeItems.add(creativeItemData);
                }
            }

            // Since the fireworks tag now won't show up due to this is being a data driven item, we have to translate it to lore ourselves
            for (int j = 0; j < creativeItems.size(); j++) {
                CreativeItemData itemData = creativeItems.get(j);
                if (!itemData.getItem().getDefinition().getIdentifier().equals("minecraft:firework_rocket")) {
                    continue;
                }

                NbtMap tag = null;
                if (itemData.getItem().getTag() != null) {
                    final DataComponents components = new DataComponents(new HashMap<>());
                    Items.FIREWORK_ROCKET.translateNbtToJava(null, itemData.getItem().getTag(), components, null);
                    final BedrockItemBuilder builder = new BedrockItemBuilder();
                    Items.FIREWORK_ROCKET.translateComponentsToBedrock(null, components, TooltipOptions.ALL_SHOWN, builder);

                    tag = builder.build();
                }

                creativeItems.set(j, new CreativeItemData(ItemData.builder()
                    .usingNetId(true)
                    .netId(itemData.getItem().getNetId())
                    .definition(itemData.getItem().getDefinition())
                    .tag(tag)
                    .count(itemData.getItem().getCount())
                    .build(), itemData.getNetId(), itemData.getGroupId()));
            }

            ItemMappings itemMappings = ItemMappings.builder()
                    .items(mappings.toArray(new ItemMapping[0]))
                    .zeroBlockDefinitionRuntimeId(mappings.stream()
                        .filter(entry -> entry.isBlock() && entry.getBedrockBlockDefinition().getRuntimeId() == 0)
                        .map(itemMapping -> itemMapping.getBedrockDefinition().getRuntimeId())
                        .toArray(Integer[]::new))
                    .creativeItems(creativeItems)
                    .creativeItemGroups(creativeItemGroups)
                    .itemDefinitions(registry)
                    .storedItems(new StoredItemMappings(javaItemToMapping))
                    .javaOnlyItems(javaOnlyItems)
                    .buckets(buckets)
                    .lightBlocks(lightBlocks)
                    .lodestoneCompass(lodestoneEntry)
                    .customIdMappings(customIdMappings)
                    .nonVanillaCustomItemIds(nonVanillaCustomItemIds)
                    .customBlockItemDefinitions(customBlockItemDefinitions)
                    .build();

            Registries.ITEMS.register(palette.protocolVersion(), itemMappings);

            firstMappingsPass = false;
        }
    }
  
    /**
     * Registers the item form of a custom skull as a custom item, giving it the icon of the block it places,
     * wearability, and the player head stack size. The latter two are inherited from the vanilla player head components.
     */
    private static ItemDefinition registerSkullItem(String identifier, GeyserMappingItem playerHeadMapping, int protocolId,
                                                    int protocolVersion, boolean firstMappingsPass) throws InvalidItemComponentsException {
        CustomItemDefinition definition = new GeyserCustomItemDefinition.Builder(Identifier.of(identifier), Identifier.of("minecraft", "player_head"))
            .geyserComponent(GeyserItemDataComponents.BLOCK_PLACER, GeyserBlockPlacer.of(Identifier.of(identifier), true))
            .displayName("item.player_head.name")
            .bedrockOptions(CustomItemBedrockOptions.builder().creativeCategory(CreativeCategory.EQUIPMENT))
            .build();
        return CustomItemRegistryPopulator.registerCustomItem(Items.PLAYER_HEAD, playerHeadMapping, definition, protocolId, protocolVersion, firstMappingsPass).itemDefinition();
    }

    private static NbtMap fromItemDefinitionToDataDriven(ItemDefinition definition, int maxStackSize, String texture, String displayName, boolean swing) {
        NbtMapBuilder builder = NbtMap.builder();
        builder.putString("name", definition.getIdentifier()).putInt("id", definition.getRuntimeId());

        NbtMapBuilder itemProperties = NbtMap.builder();

        NbtMapBuilder componentBuilder = NbtMap.builder();

        if (texture != null) {
            NbtMap iconMap = NbtMap.builder()
                .putCompound("textures", NbtMap.builder()
                    .putString("default", texture)
                    .build())
                .build();
            itemProperties.putCompound("minecraft:icon", iconMap);
        }

        if (displayName != null) {
            componentBuilder.putCompound("minecraft:display_name", NbtMap.builder().putString("value", displayName).build());
        }

        itemProperties.putBoolean("allow_off_hand", true);
        itemProperties.putInt("max_stack_size", maxStackSize);

        componentBuilder.putCompound("item_properties", itemProperties.build());

        if (swing) {
            componentBuilder.putCompound("minecraft:throwable", NbtMap.builder().putBoolean("do_swing_animation", true).build());
            componentBuilder.putCompound("minecraft:projectile", NbtMap.builder().putString("projectile_entity", "minecraft:snowball").build());
        }

        builder.putCompound("components", componentBuilder.build());
        return builder.build();
    }

    public static int getCreativeIndex(String creativeGroup, CreativeItemCategory creativeItemCategory, Map<String, Integer> groupIndexes, Map<CreativeItemCategory, Integer> fallBacks, List<CreativeItemGroup> creativeItemGroups) {
        if (fallBacks.isEmpty()) {
            // Not post rewrite, this index won't be used either way
            return 0;
        }

        if (creativeGroup != null) {
            if (groupIndexes.containsKey(creativeGroup)) {
                return groupIndexes.get(creativeGroup);
            }
        }

        if (creativeItemCategory != null) {
            if (fallBacks.containsKey(creativeItemCategory)) {
                return fallBacks.get(creativeItemCategory);
            }

            // Must "register" this category first
            // Sending a creative item group with no items in it crashes the client. Fun.
            creativeItemGroups.add(new CreativeItemGroup(creativeItemCategory, "", ItemData.AIR));
            fallBacks.put(creativeItemCategory, creativeItemGroups.size() - 1);

            return fallBacks.get(creativeItemCategory);
        }

        return fallBacks.get(CreativeItemCategory.UNDEFINED);
    }

    /**
     * Compares custom item definitions based on their predicates:
     *
     * <ol>
     *     <li>First by checking their priority values, higher priority values going first.</li>
     *     <li>Then by checking if they both have a similar range dispatch predicate, the one with the highest threshold going first.</li>
     *     <li>Lastly by the amount of predicates, from most to least.</li>
     * </ol>
     *
     * <p>If two definitions are the same, 0 is returned. If the model of two definitions differs, they are compared using their bedrock identifier, since
     * predicates won't matter there. If two definitions have the same model, same priority, same amount of predicates and no range dispatch preference, they are compared
     * using their bedrock identifier.</p>
     *
     * <p>As such, it is important that bedrock identifiers are always unique when using this comparator. This comparator can regard 2 definitions as equal if their
     * bedrock identifier is equal.</p>
     *
     * <p>Please note! The range dispatch predicate sorting only works when two definitions both only have one range dispatch predicate, both of which have the same properties
     * (property, index, normalized, negated). If there's a more complicated setup with e.g. multiple range dispatch predicates, priority values should be used to ensure
     * proper sorting.</p>
     */
    private static class CustomItemDefinitionComparator implements Comparator<GeyserCustomMappingData> {

        @Override
        public int compare(GeyserCustomMappingData firstData, GeyserCustomMappingData secondData) {
            // Important to remember when reading through this code: returning a positive number means the second definition will be checked before the first.
            // Returning a negative number means the first definition will be checked before the second.
            // Returning zero means the definitions are equal.

            CustomItemDefinition first = firstData.definition();
            CustomItemDefinition second = secondData.definition();
            if (first.equals(second)) {
                // If equal, return 0
                return 0;
            } else if (!first.model().equals(second.model())) {
                // If models differ, compare by bedrock identifier, which should always be unique
                return first.bedrockIdentifier().toString().compareTo(second.bedrockIdentifier().toString());
            }

            if (first.priority() != second.priority()) {
                // If priority differs, prefer the higher priority value
                return second.priority() - first.priority();
            }

            if (first.predicates().size() != second.predicates().size()) {
                // If predicate size differs, there's no need to check for range dispatch predicates.
                // Prefer the one with the most amount of predicates
                return second.predicates().size() - first.predicates().size();
            }

            // Loop through the predicates of the first definition and look for a range dispatch predicate
            for (MinecraftPredicate<? super ItemPredicateContext> predicate : first.predicates()) {
                if (predicate instanceof GeyserRangeDispatchPredicate(GeyserRangeDispatchPredicate.GeyserRangeDispatchProperty rangeProperty, double threshold, int index, boolean normalized, boolean negated)) {
                    // Look for a similar predicate in the other definition's predicates
                    Optional<GeyserRangeDispatchPredicate> other = second.predicates().stream()
                        .filter(GeyserRangeDispatchPredicate.class::isInstance)
                        .map(GeyserRangeDispatchPredicate.class::cast)
                        .filter(otherPredicate ->
                            otherPredicate.rangeProperty() == rangeProperty
                            && otherPredicate.index() == index
                            && otherPredicate.normalized() == normalized
                            && otherPredicate.negated() == negated)
                        .findFirst();

                    if (other.isPresent()) {
                        // If a similar predicate was found, check if they're negated
                        // If they are, prefer the one with the lowest threshold
                        // If they aren't, prefer the one with the highest threshold
                        double thresholdComparison = negated ? threshold - other.get().threshold() : other.get().threshold() - threshold;
                        if (thresholdComparison != 0.0) {
                            return thresholdComparison < 0 ? -1 : 1;
                        }
                    }
                }
            }

            if (first.predicates().size() == second.predicates().size()) {
                // If there's no preferred range predicate order and they both have the same amount of predicates, compare by bedrock identifier
                return first.bedrockIdentifier().toString().compareTo(second.bedrockIdentifier().toString());
            }

            // Prefer the one with the most amount of predicates
            return second.predicates().size() - first.predicates().size();
        }
    }
}
