package com.tiedup.remake.v2.furniture.client;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.tiedup.remake.client.gltf.GlbParserUtils;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import org.joml.Quaternionf;
import org.joml.Vector3f;

/**
 * First pass of {@link FurnitureGlbParser}: discover {@code Player_*} armature
 * roots, classify skins (furniture vs. per-seat), and extract per-seat root
 * transforms.
 *
 * <p>Extracted from {@code FurnitureGlbParser.parse} to limit the god-class
 * size and keep the scan/classify/extract logic testable in isolation.
 * Pure-functional — no state on the class, every method returns a fresh
 * result.</p>
 */
@OnlyIn(Dist.CLIENT)
final class PlayerArmatureScanner {

    private static final Logger LOGGER = LogManager.getLogger("FurnitureGltf");
    private static final String PLAYER_PREFIX = "Player_";

    private PlayerArmatureScanner() {}

    /**
     * Result of {@link #scan}: maps seat ID → node index for every
     * {@code Player_<seatId>} root node, plus the set of root node indices
     * (used later to classify skins).
     */
    static final class ArmatureScan {

        final Map<String, Integer> seatIdToRootNode;
        final Set<Integer> playerRootNodes;

        ArmatureScan(
            Map<String, Integer> seatIdToRootNode,
            Set<Integer> playerRootNodes
        ) {
            this.seatIdToRootNode = seatIdToRootNode;
            this.playerRootNodes = playerRootNodes;
        }
    }

    /**
     * Walk every node, match names against {@code "Player_<seatId>"}, and
     * return the (seatId → nodeIdx) map plus the set of those node indices.
     * A node named exactly {@code "Player_"} (empty seat ID) is skipped.
     */
    static ArmatureScan scan(@Nullable JsonArray nodes) {
        Map<String, Integer> seatIdToRootNode = new LinkedHashMap<>();
        Set<Integer> playerRootNodes = new HashSet<>();

        if (nodes == null) return new ArmatureScan(
            seatIdToRootNode,
            playerRootNodes
        );

        for (int ni = 0; ni < nodes.size(); ni++) {
            JsonObject node = nodes.get(ni).getAsJsonObject();
            String name = node.has("name")
                ? node.get("name").getAsString()
                : "";
            if (
                name.startsWith(PLAYER_PREFIX) &&
                name.length() > PLAYER_PREFIX.length()
            ) {
                String seatId = name.substring(PLAYER_PREFIX.length());
                seatIdToRootNode.put(seatId, ni);
                playerRootNodes.add(ni);
                LOGGER.debug(
                    "[FurnitureGltf] Found Player armature: '{}' -> seat '{}'",
                    name,
                    seatId
                );
            }
        }
        return new ArmatureScan(seatIdToRootNode, playerRootNodes);
    }

    /**
     * Result of {@link #classifySkins}: the first non-seat skin index (the
     * "furniture" skin, or -1 if none) and a {@code seatId → skinIdx} map.
     */
    static final class SkinClassification {

        final int furnitureSkinIdx;
        final Map<String, Integer> seatIdToSkinIdx;

        SkinClassification(
            int furnitureSkinIdx,
            Map<String, Integer> seatIdToSkinIdx
        ) {
            this.furnitureSkinIdx = furnitureSkinIdx;
            this.seatIdToSkinIdx = seatIdToSkinIdx;
        }
    }

    /**
     * Classify each skin as either "furniture" or "seat N":
     * <ol>
     *   <li>If the skin's {@code skeleton} field points at a known Player_*
     *       root → it's that seat.</li>
     *   <li>Fallback: if any joint in the skin is a descendant of a Player_*
     *       root → that seat.</li>
     *   <li>Otherwise: the first such skin is the "furniture" skin.</li>
     * </ol>
     * Extra non-matching skins are logged and ignored.
     */
    static SkinClassification classifySkins(
        @Nullable JsonArray skins,
        @Nullable JsonArray nodes,
        ArmatureScan scan,
        String debugName
    ) {
        int furnitureSkinIdx = -1;
        Map<String, Integer> seatIdToSkinIdx = new LinkedHashMap<>();
        if (skins == null || nodes == null) return new SkinClassification(
            furnitureSkinIdx,
            seatIdToSkinIdx
        );

        for (int si = 0; si < skins.size(); si++) {
            JsonObject skin = skins.get(si).getAsJsonObject();
            int skeletonNode = skin.has("skeleton")
                ? skin.get("skeleton").getAsInt()
                : -1;

            String matchedSeatId = null;
            if (
                skeletonNode >= 0 &&
                scan.playerRootNodes.contains(skeletonNode)
            ) {
                for (Map.Entry<
                    String,
                    Integer
                > entry : scan.seatIdToRootNode.entrySet()) {
                    if (entry.getValue() == skeletonNode) {
                        matchedSeatId = entry.getKey();
                        break;
                    }
                }
            }

            if (matchedSeatId == null && skin.has("joints")) {
                matchedSeatId = matchSkinToPlayerArmature(
                    skin.getAsJsonArray("joints"),
                    nodes,
                    scan.seatIdToRootNode
                );
            }

            if (matchedSeatId != null) {
                seatIdToSkinIdx.put(matchedSeatId, si);
                LOGGER.debug(
                    "[FurnitureGltf] Skin {} -> seat '{}'",
                    si,
                    matchedSeatId
                );
            } else if (furnitureSkinIdx < 0) {
                furnitureSkinIdx = si;
                LOGGER.debug("[FurnitureGltf] Skin {} -> furniture", si);
            } else {
                LOGGER.warn(
                    "[FurnitureGltf] Extra non-Player skin {} ignored in '{}'",
                    si,
                    debugName
                );
            }
        }
        return new SkinClassification(furnitureSkinIdx, seatIdToSkinIdx);
    }

    /**
     * Build per-seat root transforms from the {@code Player_*} node rest pose.
     * Position and rotation are read from the node's {@code translation} /
     * {@code rotation} fields (identity defaults if absent) and then converted
     * from raw glTF space to Minecraft model-def space by negating X/Y on the
     * translation and qx/qy on the quaternion — matching the 180°-around-Z
     * rotation applied to the furniture mesh by
     * {@link com.tiedup.remake.client.gltf.GlbParserUtils#convertMeshToMinecraftSpace}.
     */
    static Map<String, FurnitureGltfData.SeatTransform> extractTransforms(
        @Nullable JsonArray nodes,
        Map<String, Integer> seatIdToRootNode
    ) {
        Map<String, FurnitureGltfData.SeatTransform> seatTransforms =
            new LinkedHashMap<>();
        if (nodes == null) return seatTransforms;
        for (Map.Entry<String, Integer> entry : seatIdToRootNode.entrySet()) {
            String seatId = entry.getKey();
            int nodeIdx = entry.getValue();
            JsonObject node = nodes.get(nodeIdx).getAsJsonObject();
            Vector3f position = GlbParserUtils.readRestTranslation(node);
            Quaternionf rotation = GlbParserUtils.readRestRotation(node);

            // glTF space → MC model-def space (180° around Z), matching the
            // same transform GlbParserUtils.convertMeshToMinecraftSpace applies
            // to the furniture mesh. Must stay consistent with the mesh or the
            // passenger renders on the opposite side of asymmetric furniture.
            position.x = -position.x;
            position.y = -position.y;
            rotation.x = -rotation.x;
            rotation.y = -rotation.y;

            seatTransforms.put(
                seatId,
                new FurnitureGltfData.SeatTransform(seatId, position, rotation)
            );
            LOGGER.debug(
                "[FurnitureGltf] Seat '{}' transform (MC space): pos=({},{},{}), rot=({},{},{},{})",
                seatId,
                position.x,
                position.y,
                position.z,
                rotation.x,
                rotation.y,
                rotation.z,
                rotation.w
            );
        }
        return seatTransforms;
    }

    /**
     * Walk a skin's joint list to see if any joint is a descendant of one of
     * the {@code Player_*} root nodes. Used as the fallback classification
     * when the skin's {@code skeleton} field isn't set.
     */
    @Nullable
    private static String matchSkinToPlayerArmature(
        JsonArray skinJoints,
        JsonArray nodes,
        Map<String, Integer> seatIdToRootNode
    ) {
        for (JsonElement jointElem : skinJoints) {
            int jointNodeIdx = jointElem.getAsInt();
            for (Map.Entry<
                String,
                Integer
            > entry : seatIdToRootNode.entrySet()) {
                if (isDescendantOf(jointNodeIdx, entry.getValue(), nodes)) {
                    return entry.getKey();
                }
            }
        }
        return null;
    }

    /**
     * Check if {@code nodeIdx} is {@code ancestorIdx} itself or any descendant
     * of {@code ancestorIdx} via the node children hierarchy.
     */
    private static boolean isDescendantOf(
        int nodeIdx,
        int ancestorIdx,
        JsonArray nodes
    ) {
        if (nodeIdx == ancestorIdx) return true;
        JsonObject ancestor = nodes.get(ancestorIdx).getAsJsonObject();
        if (!ancestor.has("children")) return false;
        for (JsonElement child : ancestor.getAsJsonArray("children")) {
            if (isDescendantOf(nodeIdx, child.getAsInt(), nodes)) return true;
        }
        return false;
    }
}
