package com.tiedup.remake.v2.client;

import com.tiedup.remake.blocks.entity.ModBlockEntities;
import com.tiedup.remake.client.model.CellCoreBakedModel;
import com.tiedup.remake.client.renderer.CellCoreRenderer;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.V2BlockEntities;
import com.tiedup.remake.v2.bondage.client.V2BondageRenderLayer;
import com.tiedup.remake.v2.bondage.client.diagnostic.GlbValidationReloadListener;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemReloadListener;
import com.tiedup.remake.v2.furniture.FurnitureDefinition;
import com.tiedup.remake.v2.furniture.FurnitureRegistry;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import net.minecraft.client.renderer.entity.player.PlayerRenderer;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.EntityRenderersEvent;
import net.minecraftforge.client.event.ModelEvent;
import net.minecraftforge.client.event.RegisterClientReloadListenersEvent;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.registries.ForgeRegistries;

/**
 * V2 Client-side setup.
 * Registers block entity renderers, model replacements, and icon model overrides.
 *
 * <p>The icon override system allows data-driven bondage items and furniture placers
 * to display per-variant inventory sprites. Each JSON definition can specify an
 * optional {@code icon} field pointing to a model ResourceLocation. The model
 * is resolved at render time from the baked model registry.</p>
 *
 * <p>Icon models that correspond to existing registered items (e.g., "tiedup:item/armbinder")
 * are automatically available. For custom icon models that don't correspond to a registered
 * item, place the model JSON under {@code assets/<namespace>/models/item/icons/} and it
 * will be registered for baking via {@link ModelEvent.RegisterAdditional}.</p>
 */
@Mod.EventBusSubscriber(
    modid = TiedUpMod.MOD_ID,
    bus = Mod.EventBusSubscriber.Bus.MOD,
    value = Dist.CLIENT
)
public class V2ClientSetup {

    @SubscribeEvent
    public static void registerRenderers(
        EntityRenderersEvent.RegisterRenderers event
    ) {
        // Register OBJ block renderers
        event.registerBlockEntityRenderer(
            V2BlockEntities.PET_BOWL.get(),
            ObjBlockRenderer::new
        );
        event.registerBlockEntityRenderer(
            V2BlockEntities.PET_BED.get(),
            ObjBlockRenderer::new
        );
        event.registerBlockEntityRenderer(
            V2BlockEntities.PET_CAGE.get(),
            ObjBlockRenderer::new
        );

        // Register Cell Core pulsing indicator renderer
        event.registerBlockEntityRenderer(
            ModBlockEntities.CELL_CORE.get(),
            CellCoreRenderer::new
        );

        TiedUpMod.LOGGER.info(
            "[V2ClientSetup] Registered block entity renderers"
        );
    }

    /**
     * Attach the V2 bondage item render layer to both player model variants.
     * The layer queries the equipment capability on every player render, so
     * registering it once covers all players (local + remote).
     */
    @SuppressWarnings("unchecked")
    @SubscribeEvent
    public static void onAddLayers(EntityRenderersEvent.AddLayers event) {
        var defaultRenderer = event.getSkin("default");
        if (defaultRenderer instanceof PlayerRenderer playerRenderer) {
            playerRenderer.addLayer(
                new V2BondageRenderLayer<>(playerRenderer)
            );
        }
        var slimRenderer = event.getSkin("slim");
        if (slimRenderer instanceof PlayerRenderer playerRenderer) {
            playerRenderer.addLayer(
                new V2BondageRenderLayer<>(playerRenderer)
            );
        }
        TiedUpMod.LOGGER.info(
            "[V2ClientSetup] V2 bondage render layer attached to player renderers"
        );
    }

    /**
     * Register bondage-specific reload listeners. Uses {@code LOW} priority so
     * it fires after {@link com.tiedup.remake.client.gltf.GltfClientSetup}
     * registers the generic GLB cache clear — item definitions must not load
     * against a stale GLB cache, and the validator must run against a warm
     * item registry.
     */
    @SubscribeEvent(priority = EventPriority.LOW)
    public static void onRegisterReloadListeners(
        RegisterClientReloadListenersEvent event
    ) {
        event.registerReloadListener(new DataDrivenItemReloadListener());
        event.registerReloadListener(new GlbValidationReloadListener());
        event.registerReloadListener(
            new com.tiedup.remake.rig.anim.client.property.JointMaskReloadListener()
        );
        TiedUpMod.LOGGER.info(
            "[V2ClientSetup] Data-driven item + GLB validation + joint mask reload listeners registered"
        );
    }

    /**
     * Register custom icon models for baking.
     *
     * <p>When a data-driven item or furniture JSON declares an {@code icon}
     * ResourceLocation, the mod loads the corresponding model from
     * {@code assets/<namespace>/models/item/<path>.json} (or
     * {@code .../models/item/icons/<path>.json} by convention). Forge's
     * model baker only auto-discovers models referenced by registered items;
     * icons that aren't the model of a registered item are invisible unless
     * explicitly registered here.</p>
     *
     * <p>We scan both registries at this phase and register every non-null
     * icon. If an icon already matches a registered item's model, Forge
     * deduplicates internally — safe to register unconditionally.</p>
     *
     * <p>Timing caveat: if {@code DataDrivenItemRegistry} hasn't finished
     * loading JSON on first bootstrap, {@link #getAllIconLocations()}
     * returns fewer entries than expected. The subsequent resource reload
     * (triggered by {@code DataDrivenItemReloadListener}) refires this
     * event, so icons register correctly on the second pass. In practice,
     * inventory icons are visible from the first render after login.</p>
     */
    @SubscribeEvent
    public static void onRegisterAdditionalModels(
        ModelEvent.RegisterAdditional event
    ) {
        Set<ResourceLocation> icons = collectCustomIconLocations();
        int registered = 0;
        for (ResourceLocation icon : icons) {
            event.register(icon);
            registered++;
        }
        TiedUpMod.LOGGER.info(
            "[V2ClientSetup] Registered {} custom icon model(s) for baking",
            registered
        );
    }

    /**
     * Gather every icon ResourceLocation declared by a data-driven item or
     * furniture definition that doesn't correspond to an already-registered
     * Forge item. Icons pointing at registered items are auto-loaded by the
     * baker and don't need explicit registration.
     */
    private static Set<ResourceLocation> collectCustomIconLocations() {
        Set<ResourceLocation> icons = new HashSet<>();
        for (DataDrivenItemDefinition def : DataDrivenItemRegistry.getAll()) {
            if (def.icon() != null && !isRegisteredItemModel(def.icon())) {
                icons.add(def.icon());
            }
        }
        for (FurnitureDefinition def : FurnitureRegistry.getAll()) {
            if (def.icon() != null && !isRegisteredItemModel(def.icon())) {
                icons.add(def.icon());
            }
        }
        return icons;
    }

    /**
     * True when {@code modelLoc} follows the {@code <ns>:item/<path>} convention
     * and {@code <ns>:<path>} is a registered Forge item. Those icons are
     * auto-baked — re-registering is harmless but noisy.
     */
    private static boolean isRegisteredItemModel(ResourceLocation modelLoc) {
        String path = modelLoc.getPath();
        if (!path.startsWith("item/")) return false;
        String itemPath = path.substring("item/".length());
        ResourceLocation itemRL = ResourceLocation.fromNamespaceAndPath(
            modelLoc.getNamespace(),
            itemPath
        );
        return ForgeRegistries.ITEMS.containsKey(itemRL);
    }

    @SubscribeEvent
    public static void onModifyBakingResult(
        ModelEvent.ModifyBakingResult event
    ) {
        // Block model path key (used in model JSON references)
        ResourceLocation blockModelLoc = ResourceLocation.fromNamespaceAndPath(
            TiedUpMod.MOD_ID,
            "block/cell_core"
        );
        // Blockstate variant key (used by the block renderer to look up models)
        ModelResourceLocation stateModelLoc = new ModelResourceLocation(
            ResourceLocation.fromNamespaceAndPath(
                TiedUpMod.MOD_ID,
                "cell_core"
            ),
            ""
        );

        // Find the original model from either key
        BakedModel original = event.getModels().get(stateModelLoc);
        if (original == null) {
            original = event.getModels().get(blockModelLoc);
        }

        if (original != null) {
            CellCoreBakedModel wrapper = new CellCoreBakedModel(original);
            event.getModels().put(blockModelLoc, wrapper);
            event.getModels().put(stateModelLoc, wrapper);
            TiedUpMod.LOGGER.info(
                "[V2ClientSetup] Replaced cell_core BakedModel at both model keys"
            );
        }

        // ===== Data-driven item icon overrides =====
        wrapItemModelWithIconOverrides(
            event.getModels(),
            "data_driven_item",
            DataDrivenIconOverrides.Mode.BONDAGE_ITEM
        );

        wrapItemModelWithIconOverrides(
            event.getModels(),
            "furniture_placer",
            DataDrivenIconOverrides.Mode.FURNITURE_PLACER
        );
    }

    /**
     * Wrap an item's baked model with a {@link DataDrivenIconBakedModel} that
     * switches the rendered model based on NBT icon definitions.
     *
     * @param models   the mutable model registry from {@link ModelEvent.ModifyBakingResult}
     * @param itemName the item registry name (e.g., "data_driven_item")
     * @param mode     the icon override mode (determines which NBT key to read)
     */
    private static void wrapItemModelWithIconOverrides(
        Map<ResourceLocation, BakedModel> models,
        String itemName,
        DataDrivenIconOverrides.Mode mode
    ) {
        ModelResourceLocation itemModelLoc = new ModelResourceLocation(
            ResourceLocation.fromNamespaceAndPath(TiedUpMod.MOD_ID, itemName),
            "inventory"
        );

        BakedModel originalItemModel = models.get(itemModelLoc);
        if (originalItemModel == null) {
            TiedUpMod.LOGGER.warn(
                "[V2ClientSetup] Could not find baked model for {} — icon overrides not applied",
                itemModelLoc
            );
            return;
        }

        DataDrivenIconOverrides overrides = new DataDrivenIconOverrides(mode);
        DataDrivenIconBakedModel wrapped = new DataDrivenIconBakedModel(
            originalItemModel,
            overrides
        );
        models.put(itemModelLoc, wrapped);

        TiedUpMod.LOGGER.info(
            "[V2ClientSetup] Wrapped {} model with icon overrides (mode={})",
            itemName,
            mode
        );
    }
}
