package com.tiedup.remake.client.gltf;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.nio.ByteBuffer;
import java.util.List;
import org.joml.Quaternionf;
import org.joml.Vector3f;

/**
 * Shared stateless utilities for parsing binary glTF (.glb) files.
 *
 * <p>These methods are used by both {@link GlbParser} (single-armature bondage meshes)
 * and {@link com.tiedup.remake.v2.furniture.client.FurnitureGlbParser FurnitureGlbParser}
 * (multi-armature furniture meshes). Extracted to eliminate ~160 lines of verbatim
 * duplication between the two parsers.</p>
 *
 * <p>All methods are pure functions (no state, no side effects).</p>
 */
public final class GlbParserUtils {

    // glTF component type constants
    public static final int BYTE = 5120;
    public static final int UNSIGNED_BYTE = 5121;
    public static final int SHORT = 5122;
    public static final int UNSIGNED_SHORT = 5123;
    public static final int UNSIGNED_INT = 5125;
    public static final int FLOAT = 5126;

    private GlbParserUtils() {}

    // ---- Material name parsing ----

    /**
     * Parse the root "materials" array and extract each material's "name" field.
     * Returns an empty array if no materials are present.
     */
    public static String[] parseMaterialNames(JsonObject root) {
        if (!root.has("materials") || !root.get("materials").isJsonArray()) {
            return new String[0];
        }
        JsonArray materials = root.getAsJsonArray("materials");
        String[] names = new String[materials.size()];
        for (int i = 0; i < materials.size(); i++) {
            JsonObject mat = materials.get(i).getAsJsonObject();
            names[i] = mat.has("name") ? mat.get("name").getAsString() : null;
        }
        return names;
    }

    // ---- Array flattening utilities ----

    public static float[] flattenFloats(List<float[]> arrays) {
        int total = 0;
        for (float[] a : arrays) total += a.length;
        float[] result = new float[total];
        int offset = 0;
        for (float[] a : arrays) {
            System.arraycopy(a, 0, result, offset, a.length);
            offset += a.length;
        }
        return result;
    }

    public static int[] flattenInts(List<int[]> arrays) {
        int total = 0;
        for (int[] a : arrays) total += a.length;
        int[] result = new int[total];
        int offset = 0;
        for (int[] a : arrays) {
            System.arraycopy(a, 0, result, offset, a.length);
            offset += a.length;
        }
        return result;
    }

    // ---- Accessor reading utilities ----

    public static float[] readFloatAccessor(
        JsonArray accessors,
        JsonArray bufferViews,
        ByteBuffer binData,
        int accessorIdx
    ) {
        JsonObject accessor = accessors.get(accessorIdx).getAsJsonObject();
        int count = accessor.get("count").getAsInt();
        int componentType = accessor.get("componentType").getAsInt();
        String type = accessor.get("type").getAsString();
        int components = typeComponents(type);

        int bvIdx = accessor.get("bufferView").getAsInt();
        JsonObject bv = bufferViews.get(bvIdx).getAsJsonObject();
        int byteOffset =
            (bv.has("byteOffset") ? bv.get("byteOffset").getAsInt() : 0) +
            (accessor.has("byteOffset")
                ? accessor.get("byteOffset").getAsInt()
                : 0);
        int byteStride = bv.has("byteStride")
            ? bv.get("byteStride").getAsInt()
            : 0;

        int totalElements = count * components;
        float[] result = new float[totalElements];

        int componentSize = componentByteSize(componentType);
        int stride = byteStride > 0 ? byteStride : components * componentSize;

        for (int i = 0; i < count; i++) {
            int pos = byteOffset + i * stride;
            for (int c = 0; c < components; c++) {
                binData.position(pos + c * componentSize);
                result[i * components + c] = readComponentAsFloat(
                    binData,
                    componentType
                );
            }
        }

        return result;
    }

    public static int[] readIntAccessor(
        JsonArray accessors,
        JsonArray bufferViews,
        ByteBuffer binData,
        int accessorIdx
    ) {
        JsonObject accessor = accessors.get(accessorIdx).getAsJsonObject();
        int count = accessor.get("count").getAsInt();
        int componentType = accessor.get("componentType").getAsInt();
        String type = accessor.get("type").getAsString();
        int components = typeComponents(type);

        int bvIdx = accessor.get("bufferView").getAsInt();
        JsonObject bv = bufferViews.get(bvIdx).getAsJsonObject();
        int byteOffset =
            (bv.has("byteOffset") ? bv.get("byteOffset").getAsInt() : 0) +
            (accessor.has("byteOffset")
                ? accessor.get("byteOffset").getAsInt()
                : 0);
        int byteStride = bv.has("byteStride")
            ? bv.get("byteStride").getAsInt()
            : 0;

        int totalElements = count * components;
        int[] result = new int[totalElements];

        int componentSize = componentByteSize(componentType);
        int stride = byteStride > 0 ? byteStride : components * componentSize;

        for (int i = 0; i < count; i++) {
            int pos = byteOffset + i * stride;
            for (int c = 0; c < components; c++) {
                binData.position(pos + c * componentSize);
                result[i * components + c] = readComponentAsInt(
                    binData,
                    componentType
                );
            }
        }

        return result;
    }

    public static float readComponentAsFloat(
        ByteBuffer buf,
        int componentType
    ) {
        return switch (componentType) {
            case FLOAT -> buf.getFloat();
            case BYTE -> buf.get() / 127.0f;
            case UNSIGNED_BYTE -> (buf.get() & 0xFF) / 255.0f;
            case SHORT -> buf.getShort() / 32767.0f;
            case UNSIGNED_SHORT -> (buf.getShort() & 0xFFFF) / 65535.0f;
            case UNSIGNED_INT -> (buf.getInt() & 0xFFFFFFFFL) /
            (float) 0xFFFFFFFFL;
            default -> throw new IllegalArgumentException(
                "Unknown component type: " + componentType
            );
        };
    }

    public static int readComponentAsInt(ByteBuffer buf, int componentType) {
        return switch (componentType) {
            case BYTE -> buf.get();
            case UNSIGNED_BYTE -> buf.get() & 0xFF;
            case SHORT -> buf.getShort();
            case UNSIGNED_SHORT -> buf.getShort() & 0xFFFF;
            case UNSIGNED_INT -> buf.getInt();
            case FLOAT -> (int) buf.getFloat();
            default -> throw new IllegalArgumentException(
                "Unknown component type: " + componentType
            );
        };
    }

    public static int typeComponents(String type) {
        return switch (type) {
            case "SCALAR" -> 1;
            case "VEC2" -> 2;
            case "VEC3" -> 3;
            case "VEC4" -> 4;
            case "MAT4" -> 16;
            default -> throw new IllegalArgumentException(
                "Unknown accessor type: " + type
            );
        };
    }

    public static int componentByteSize(int componentType) {
        return switch (componentType) {
            case BYTE, UNSIGNED_BYTE -> 1;
            case SHORT, UNSIGNED_SHORT -> 2;
            case UNSIGNED_INT, FLOAT -> 4;
            default -> throw new IllegalArgumentException(
                "Unknown component type: " + componentType
            );
        };
    }

    // ---- Deep-copy utility ----

    /**
     * Deep-copy an AnimationClip (preserves original data before MC conversion).
     */
    public static GltfData.AnimationClip deepCopyClip(
        GltfData.AnimationClip clip
    ) {
        Quaternionf[][] rawRotations =
            new Quaternionf[clip.rotations().length][];
        for (int j = 0; j < clip.rotations().length; j++) {
            if (clip.rotations()[j] != null) {
                rawRotations[j] = new Quaternionf[clip.rotations()[j].length];
                for (int f = 0; f < clip.rotations()[j].length; f++) {
                    rawRotations[j][f] = new Quaternionf(
                        clip.rotations()[j][f]
                    );
                }
            }
        }
        Vector3f[][] rawTranslations = null;
        if (clip.translations() != null) {
            rawTranslations = new Vector3f[clip.translations().length][];
            for (int j = 0; j < clip.translations().length; j++) {
                if (clip.translations()[j] != null) {
                    rawTranslations[j] =
                        new Vector3f[clip.translations()[j].length];
                    for (int f = 0; f < clip.translations()[j].length; f++) {
                        rawTranslations[j][f] = new Vector3f(
                            clip.translations()[j][f]
                        );
                    }
                }
            }
        }
        return new GltfData.AnimationClip(
            clip.timestamps().clone(),
            rawRotations,
            rawTranslations,
            clip.frameCount()
        );
    }

    // ---- Coordinate system conversion ----

    /**
     * Convert an animation clip's rotations and translations to MC space.
     * Negate qx/qy for rotations and negate tx/ty for translations.
     */
    public static void convertAnimationToMinecraftSpace(
        GltfData.AnimationClip clip,
        int jointCount
    ) {
        if (clip == null) return;

        Quaternionf[][] rotations = clip.rotations();
        for (int j = 0; j < jointCount; j++) {
            if (j < rotations.length && rotations[j] != null) {
                for (Quaternionf q : rotations[j]) {
                    q.x = -q.x;
                    q.y = -q.y;
                }
            }
        }

        Vector3f[][] translations = clip.translations();
        if (translations != null) {
            for (int j = 0; j < jointCount; j++) {
                if (j < translations.length && translations[j] != null) {
                    for (Vector3f t : translations[j]) {
                        t.x = -t.x;
                        t.y = -t.y;
                    }
                }
            }
        }
    }
}
