#include "adt_loader.h"
#include "wow_chunk_reader.h"

#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/variant/vector3.hpp>

#include <fstream>
#include <cstring>
#include <cmath>
#include <algorithm>

using namespace godot;

// ─────────────────────────────────────────────────────────────────────────────
// ADT constants
// ─────────────────────────────────────────────────────────────────────────────
static constexpr float TILE_SIZE  = 533.33333f;   // yards per map tile
static constexpr float CHUNK_SIZE = TILE_SIZE / 16.f;   // 33.333 yards
static constexpr float UNIT_SIZE  = CHUNK_SIZE  / 8.f;   //  4.166 yards (between outer verts)

// ─────────────────────────────────────────────────────────────────────────────
// Raw structures
// ─────────────────────────────────────────────────────────────────────────────
#pragma pack(push, 1)

struct MDDFEntry {          // M2 doodad placement
    uint32_t nameId;
    uint32_t uniqueId;
    float    pos[3];        // WoW world coords
    float    rot[3];        // degrees
    uint16_t scale;         // 1024 = 1.0
    uint16_t flags;
};

struct MODFEntry {          // WMO placement
    uint32_t nameId;
    uint32_t uniqueId;
    float    pos[3];
    float    rot[3];
    float    bboxMin[3];
    float    bboxMax[3];
    uint16_t flags;
    uint16_t doodadSet;
    uint16_t nameSet;
    uint16_t padding;
};

struct MCNKHeader {
    uint32_t flags;
    uint32_t indexX;
    uint32_t indexY;
    uint32_t nLayers;
    uint32_t nDoodadRefs;
    uint32_t ofsMCVT;
    uint32_t ofsMCNR;
    uint32_t ofsMCLY;
    uint32_t ofsMCRF;
    uint32_t ofsMCAL;
    uint32_t sizeMCAL;
    uint32_t ofsMCSH;
    uint32_t sizeMCSH;
    uint32_t areaId;
    uint32_t nMapObjRefs;
    uint32_t holes; // low 16 bits are the classic 4x4 hole mask
    uint16_t doodadMapping[8];
    uint8_t  doodadStencil[8];
    uint32_t ofsMCSE;
    uint32_t nSoundEmitters;
    uint32_t ofsMCLQ;
    uint32_t sizeMCLQ;
    float    zpos;
    float    xpos;
    float    ypos;
    uint32_t ofsMCCV;
    uint32_t unused1;
    uint32_t unused2;
};

struct MCLYEntry {
    uint32_t textureId;
    uint32_t flags;
    uint32_t ofsMCAL;
    int32_t  effectId;
};

struct MH2OHeader {
    uint32_t ofsInformation;
    uint32_t nLayers;
    uint32_t ofsAttributes;
};

struct MH2OInformation {
    uint16_t liquidId;
    uint16_t liquidVertexFormat;
    float    minHeight;
    float    maxHeight;
    uint8_t  xOffset;
    uint8_t  yOffset;
    uint8_t  width;
    uint8_t  height;
    uint32_t ofsInfoMask;
    uint32_t ofsHeightMap;
};

#pragma pack(pop)

// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
std::vector<uint8_t> ADTLoader::read_file(const std::string &path) {
    std::ifstream f(path, std::ios::binary | std::ios::ate);
    if (!f) return {};
    auto sz = f.tellg(); f.seekg(0);
    std::vector<uint8_t> buf(sz);
    f.read(reinterpret_cast<char *>(buf.data()), sz);
    return buf;
}

std::string ADTLoader::to_std(const String &s) {
    return std::string(s.utf8().get_data());
}

String ADTLoader::to_godot(const std::string &s) {
    return String(s.c_str());
}

PackedStringArray ADTLoader::parse_names_with_offsets(
    const WoWChunk &names_chunk, const WoWChunk &ids_chunk)
{
    PackedStringArray result;
    if (names_chunk.size == 0 || ids_chunk.size == 0) return result;

    const char     *ndata = reinterpret_cast<const char *>(names_chunk.data);
    const uint32_t *offsets = ids_chunk.array<uint32_t>();
    uint32_t n = ids_chunk.size / 4;

    for (uint32_t i = 0; i < n; ++i) {
        uint32_t ofs = offsets[i];
        if (ofs >= names_chunk.size) { result.push_back(""); continue; }
        result.push_back(String(ndata + ofs));
    }
    return result;
}

namespace {

static constexpr uint32_t MCNK_FLAG_DO_NOT_FIX_ALPHA_MAP = 0x8000;
static constexpr uint32_t MCLY_FLAG_ALPHA_COMPRESSED     = 0x200;
static constexpr size_t   MCNK_CHUNK_HEADER_SIZE         = 8;

bool mcnk_subchunk_fits(uint32_t offset_from_chunk_start, size_t bytes_needed, size_t mcnk_payload_size) {
    if (offset_from_chunk_start < MCNK_CHUNK_HEADER_SIZE) {
        return false;
    }
    const size_t offset_in_payload = (size_t)offset_from_chunk_start - MCNK_CHUNK_HEADER_SIZE;
    return offset_in_payload + bytes_needed <= mcnk_payload_size;
}

bool mh2o_region_fits(uint32_t offset_in_payload, size_t bytes_needed, size_t mh2o_payload_size) {
    const size_t offset = (size_t)offset_in_payload;
    return offset <= mh2o_payload_size && bytes_needed <= mh2o_payload_size - offset;
}

const uint8_t *mcnk_subchunk_ptr(const uint8_t *mcnk_payload, uint32_t offset_from_chunk_start) {
    if (offset_from_chunk_start < MCNK_CHUNK_HEADER_SIZE) {
        return nullptr;
    }
    return mcnk_payload + ((size_t)offset_from_chunk_start - MCNK_CHUNK_HEADER_SIZE);
}

PackedByteArray decode_mcal_big_alpha(const uint8_t *src, size_t available_bytes) {
    PackedByteArray out;
    if (available_bytes < 64 * 64) {
        return out;
    }

    out.resize(64 * 64);
    std::memcpy(out.ptrw(), src, 64 * 64);
    return out;
}

PackedByteArray decode_mcal_old_alpha(const uint8_t *src, size_t available_bytes, bool fix_edges) {
    PackedByteArray out;
    if (available_bytes < 64 * 32) {
        return out;
    }

    out.resize(64 * 64);
    uint8_t *dst = out.ptrw();

    // Legacy MCAL uses the old 4-bit layout.
    // Noggit decodes it into a transposed linear buffer (x-major, not row-major)
    // and uploads that buffer directly to the GPU. Reproducing that layout keeps
    // layer placement aligned with Noggit / the original client for classic ADTs.
    size_t pos = 0;
    for (int x = 0; x < 64; ++x) {
        for (int y = 0; y < 64; y += 2) {
            if (pos >= available_bytes) {
                return PackedByteArray();
            }

            const uint8_t value = src[pos++];
            const uint8_t low   = value & 0x0F;
            const uint8_t high  = (value >> 4) & 0x0F;

            dst[x * 64 + (y + 0)] = (low << 4) | low;
            dst[x * 64 + (y + 1)] = (high << 4) | high;
        }
    }

    if (fix_edges) {
        for (int i = 0; i < 64; ++i) {
            dst[i * 64 + 63] = dst[i * 64 + 62];
            dst[63 * 64 + i] = dst[62 * 64 + i];
        }
        dst[63 * 64 + 63] = dst[62 * 64 + 62];
    }

    return out;
}

PackedByteArray decode_mcal_compressed_alpha(const uint8_t *src, size_t available_bytes) {
    PackedByteArray out;
    out.resize(64 * 64);
    uint8_t *dst = out.ptrw();

    size_t in_pos  = 0;
    size_t out_pos = 0;
    while (in_pos < available_bytes && out_pos < 64 * 64) {
        const uint8_t header = src[in_pos++];
        const size_t  count  = header & 0x7F;
        const bool    fill   = (header & 0x80) != 0;

        if (count == 0) {
            continue;
        }

        const size_t write_count = std::min<size_t>(count, 64 * 64 - out_pos);
        if (fill) {
            if (in_pos >= available_bytes) {
                return PackedByteArray();
            }
            std::memset(dst + out_pos, src[in_pos++], write_count);
        } else {
            if (in_pos + count > available_bytes) {
                return PackedByteArray();
            }
            std::memcpy(dst + out_pos, src + in_pos, write_count);
            in_pos += count;
        }

        out_pos += write_count;
    }

    if (out_pos != 64 * 64) {
        return PackedByteArray();
    }

    return out;
}

// ADT placement tables (MDDF/MODF) do not use the same world-space basis as
// MCNK origins. In 3.3.5a they are already expressed in tile/world space that
// matches the streamed terrain layout, with the vector stored as X, Z, Y.
inline void adt_placement_pos_to_godot(float px, float pz, float py,
                                       float &gx, float &gy, float &gz) {
    gx = px;
    gy = pz;
    gz = py;
}

// MDDF/MODF Euler angles are stored in degrees in WoW render space (Y-up):
//   rot[0] — pitch around X, rot[1] — yaw around Y, rot[2] — roll around Z.
// WoW's "0 yaw" faces a different direction than Godot's, so rot[1] needs
// a fixed +90° offset (matches Blender wow.export importer). Other axes
// map straight through.
inline void adt_placement_rot_to_godot(float rx, float ry_yaw, float rz,
                                       float &gx, float &gy, float &gz) {
    constexpr float D2R = (float)(M_PI / 180.0);
    gx =  rx              * D2R;
    gy = (ry_yaw - 90.0f) * D2R;
    gz =  rz              * D2R;
}

} // namespace

// ─────────────────────────────────────────────────────────────────────────────
// Main loader
// ─────────────────────────────────────────────────────────────────────────────
Dictionary ADTLoader::load_adt_from_bytes(const PackedByteArray &bytes) {
    if (bytes.is_empty()) return Dictionary();
    // Wrap in a vector for the shared parse path
    std::vector<uint8_t> buf(bytes.size());
    std::memcpy(buf.data(), bytes.ptr(), bytes.size());
    // Reuse the body of load_adt by calling the internal parse
    // (inline here to avoid duplicating the reader setup)
    const uint8_t *raw = buf.data();
    size_t         len = buf.size();
    Dictionary result;
    _parse_adt(raw, len, result);
    return result;
}

Dictionary ADTLoader::load_adt(const String &path) {
    Dictionary result;
    auto buf = read_file(to_std(path));
    if (buf.empty()) {
        UtilityFunctions::push_error("ADTLoader: cannot read ", path);
        return result;
    }
    _parse_adt(buf.data(), buf.size(), result);
    return result;
}

void ADTLoader::_parse_adt(const uint8_t *raw, size_t len, Dictionary &result) {
    ChunkReader reader(raw, len);
    WoWChunk chunk;

    // ── Pass 1: collect named chunks ──────────────────────────────────────────
    WoWChunk cMTEX{}, cMMDX{}, cMMID{}, cMWMO{}, cMWID{}, cMDDF{}, cMODF{}, cMH2O{};
    std::vector<WoWChunk> mcnk_chunks;
    mcnk_chunks.reserve(256);

    while (reader.next(chunk)) {
        if      (chunk.is("MTEX")) cMTEX = chunk;
        else if (chunk.is("MMDX")) cMMDX = chunk;
        else if (chunk.is("MMID")) cMMID = chunk;
        else if (chunk.is("MWMO")) cMWMO = chunk;
        else if (chunk.is("MWID")) cMWID = chunk;
        else if (chunk.is("MDDF")) cMDDF = chunk;
        else if (chunk.is("MODF")) cMODF = chunk;
        else if (chunk.is("MH2O")) cMH2O = chunk;
        else if (chunk.is("MCNK")) mcnk_chunks.push_back(chunk);
    }

    // ── Textures ──────────────────────────────────────────────────────────────
    PackedStringArray textures;
    if (cMTEX.size > 0) {
        const char *p   = reinterpret_cast<const char *>(cMTEX.data);
        const char *end = p + cMTEX.size;
        while (p < end) {
            textures.push_back(String(p));
            p += strlen(p) + 1;
        }
    }
    result["textures"] = textures;

    // ── M2 / WMO names ────────────────────────────────────────────────────────
    result["m2_names"]  = (cMMDX.size && cMMID.size)
                          ? parse_names_with_offsets(cMMDX, cMMID)
                          : PackedStringArray();
    result["wmo_names"] = (cMWMO.size && cMWID.size)
                          ? parse_names_with_offsets(cMWMO, cMWID)
                          : PackedStringArray();

    // ── M2 placements ─────────────────────────────────────────────────────────
    Array m2_placements;
    if (cMDDF.size > 0) {
        uint32_t n  = cMDDF.count_of(sizeof(MDDFEntry));
        auto    *dd = cMDDF.array<MDDFEntry>();
        for (uint32_t i = 0; i < n; ++i) {
            float gx, gy, gz, rx, ry, rz;
            adt_placement_pos_to_godot(dd[i].pos[0], dd[i].pos[1], dd[i].pos[2], gx, gy, gz);
            adt_placement_rot_to_godot(dd[i].rot[0], dd[i].rot[1], dd[i].rot[2], rx, ry, rz);

            Dictionary p;
            p["name_id"]   = (int)dd[i].nameId;
            p["unique_id"] = (int)dd[i].uniqueId;
            p["pos"]       = Vector3(gx, gy, gz);
            p["rot"]       = Vector3(rx, ry, rz);
            p["scale"]     = dd[i].scale / 1024.f;
            m2_placements.push_back(p);
        }
    }
    result["m2_placements"] = m2_placements;

    // ── WMO placements ────────────────────────────────────────────────────────
    Array wmo_placements;
    if (cMODF.size > 0) {
        uint32_t n  = cMODF.count_of(sizeof(MODFEntry));
        auto    *od = cMODF.array<MODFEntry>();
        for (uint32_t i = 0; i < n; ++i) {
            float gx, gy, gz, rx, ry, rz;
            adt_placement_pos_to_godot(od[i].pos[0], od[i].pos[1], od[i].pos[2], gx, gy, gz);
            adt_placement_rot_to_godot(od[i].rot[0], od[i].rot[1], od[i].rot[2], rx, ry, rz);
            // MODF (WMO) needs an additional +90° yaw on top of the M2 offset
            // (so total WMO yaw offset = 0°). WMO local-forward differs from M2.
            ry -= (float)(M_PI * 0.5);

            Dictionary p;
            p["name_id"]    = (int)od[i].nameId;
            p["unique_id"]  = (int)od[i].uniqueId;
            p["pos"]        = Vector3(gx, gy, gz);
            p["rot"]        = Vector3(rx, ry, rz);
            p["scale"]      = 1.0f;
            p["flags"]      = (int)od[i].flags;
            p["doodad_set"] = (int)od[i].doodadSet;
            p["name_set"]   = (int)od[i].nameSet;
            wmo_placements.push_back(p);
        }
    }
    result["wmo_placements"] = wmo_placements;

    // ── MCNK chunks ───────────────────────────────────────────────────────────
    Array chunks_array;
    chunks_array.resize((int)mcnk_chunks.size());

    for (int ci = 0; ci < (int)mcnk_chunks.size(); ++ci) {
        const WoWChunk &mc = mcnk_chunks[ci];
        if (mc.size < sizeof(MCNKHeader)) { chunks_array[ci] = Dictionary(); continue; }

        const MCNKHeader &hdr = *reinterpret_cast<const MCNKHeader *>(mc.data);
        const uint8_t *base   = mc.data;

        // World origin of this chunk (MCNK stores z/x/y, convert to Godot)
        float ox, oy, oz;
        wow_to_godot(hdr.zpos, hdr.xpos, hdr.ypos, ox, oy, oz);

        // ── Heights (MCVT) ───────────────────────────────────────────────
        PackedFloat32Array heights;
        PackedVector3Array normals_arr;

        if (hdr.ofsMCVT && mcnk_subchunk_fits(hdr.ofsMCVT, 8 + 145 * 4, mc.size)) {
            // MCVT sub-chunk: 8-byte header + 145 floats
            const uint8_t *p     = mcnk_subchunk_ptr(base, hdr.ofsMCVT);
            // skip sub-chunk magic(4) + size(4)
            const float   *hdata = reinterpret_cast<const float *>(p + 8);
            heights.resize(145);
            for (int j = 0; j < 145; ++j)
                heights[j] = hdata[j];
        }

        if (hdr.ofsMCNR && mcnk_subchunk_fits(hdr.ofsMCNR, 8 + 145 * 3, mc.size)) {
            const uint8_t *p = mcnk_subchunk_ptr(base, hdr.ofsMCNR) + 8; // skip sub-chunk header
            normals_arr.resize(145);
            for (int j = 0; j < 145; ++j) {
                // MCNR stores signed bytes in WoW-axis order X, Z, Y.
                // Terrain positions are converted with wow_to_godot(wx, wy, wz),
                // so normals must be transformed with the same basis change:
                // Godot = (-WoW.Y, WoW.Z, -WoW.X).
                float wx = (int8_t)p[j * 3 + 0] / 127.f;
                float wz = (int8_t)p[j * 3 + 1] / 127.f;
                float wy = (int8_t)p[j * 3 + 2] / 127.f;
                normals_arr[j] = Vector3(-wy, wz, -wx).normalized();
            }
        }

        // ── Texture layers (MCLY) ────────────────────────────────────────
        Array layers;
        if (hdr.ofsMCLY && hdr.nLayers > 0 &&
            mcnk_subchunk_fits(hdr.ofsMCLY, 8 + hdr.nLayers * sizeof(MCLYEntry), mc.size))
        {
            const MCLYEntry *ly = reinterpret_cast<const MCLYEntry *>(
                mcnk_subchunk_ptr(base, hdr.ofsMCLY) + 8);
            for (uint32_t li = 0; li < hdr.nLayers; ++li) {
                Dictionary ld;
                ld["texture_id"]   = (int)ly[li].textureId;
                ld["flags"]        = (int)ly[li].flags;
                ld["effect_id"]    = (int)ly[li].effectId;
                ld["alpha_offset"] = (int)ly[li].ofsMCAL;
                layers.push_back(ld);
            }
        }

        Array alpha_maps;
        if (hdr.ofsMCAL && hdr.sizeMCAL >= 8 &&
            mcnk_subchunk_fits(hdr.ofsMCAL, hdr.sizeMCAL, mc.size) &&
            layers.size() > 1)
        {
            const uint8_t *mcal_data     = mcnk_subchunk_ptr(base, hdr.ofsMCAL) + 8;
            const size_t   mcal_size     = hdr.sizeMCAL - 8;
            const bool     fix_old_edges = (hdr.flags & MCNK_FLAG_DO_NOT_FIX_ALPHA_MAP) == 0;

            bool has_compressed_alpha = false;
            for (int li = 1; li < layers.size(); ++li) {
                Dictionary layer = layers[li];
                if (((int)layer.get("flags", 0) & (int)MCLY_FLAG_ALPHA_COMPRESSED) != 0) {
                    has_compressed_alpha = true;
                    break;
                }
            }

            const int  alpha_layer_count = layers.size() - 1;
            const bool use_big_alpha     = has_compressed_alpha ||
                                           mcal_size >= (size_t)(alpha_layer_count * 4096);

            for (int li = 1; li < layers.size(); ++li) {
                Dictionary        layer      = layers[li];
                const int         flags      = (int)layer.get("flags", 0);
                const size_t      start      = (size_t)(int)layer.get("alpha_offset", 0);
                size_t            end        = mcal_size;
                PackedByteArray   alpha_data;

                for (int next_li = li + 1; next_li < layers.size(); ++next_li) {
                    Dictionary next_layer = layers[next_li];
                    const size_t next_start = (size_t)(int)next_layer.get("alpha_offset", 0);
                    if (next_start > start) {
                        end = std::min(end, next_start);
                        break;
                    }
                }

                if (start >= mcal_size || end <= start) {
                    alpha_maps.push_back(PackedByteArray());
                    continue;
                }

                const size_t available = end - start;
                const uint8_t *src     = mcal_data + start;

                if ((flags & (int)MCLY_FLAG_ALPHA_COMPRESSED) != 0) {
                    alpha_data = decode_mcal_compressed_alpha(src, available);
                } else if (use_big_alpha) {
                    alpha_data = decode_mcal_big_alpha(src, available);
                } else {
                    alpha_data = decode_mcal_old_alpha(src, available, fix_old_edges);
                }

                alpha_maps.push_back(alpha_data);
            }
        }

        // ── Build chunk dictionary ───────────────────────────────────────
        Array liquids;
        if (cMH2O.size >= 256 * sizeof(MH2OHeader)) {
            const size_t header_index = (size_t)hdr.indexY * 16 + hdr.indexX;
            const size_t header_offset = header_index * sizeof(MH2OHeader);

            if (header_index < 256 && header_offset + sizeof(MH2OHeader) <= cMH2O.size) {
                const MH2OHeader &water_header =
                    *reinterpret_cast<const MH2OHeader *>(cMH2O.data + header_offset);

                if (water_header.nLayers > 0 &&
                    water_header.ofsInformation > 0 &&
                    mh2o_region_fits(
                        water_header.ofsInformation,
                        (size_t)water_header.nLayers * sizeof(MH2OInformation),
                        cMH2O.size))
                {
                    for (uint32_t li = 0; li < water_header.nLayers; ++li) {
                        const uint32_t info_offset =
                            water_header.ofsInformation + li * sizeof(MH2OInformation);
                        const MH2OInformation &info =
                            *reinterpret_cast<const MH2OInformation *>(cMH2O.data + info_offset);

                        if (info.width == 0 || info.height == 0 ||
                            info.xOffset > 8 || info.yOffset > 8 ||
                            info.xOffset + info.width > 8 ||
                            info.yOffset + info.height > 8)
                        {
                            continue;
                        }

                        PackedByteArray liquid_mask;
                        liquid_mask.resize(8 * 8);
                        uint8_t *mask_ptr = liquid_mask.ptrw();
                        std::memset(mask_ptr, 0, 8 * 8);

                        uint64_t info_mask = 0xFFFFFFFFFFFFFFFFull;
                        const size_t mask_bits  = (size_t)info.width * info.height;
                        const size_t mask_bytes = (mask_bits + 7) / 8;
                        if (info.ofsInfoMask > 0) {
                            if (!mh2o_region_fits(info.ofsInfoMask, mask_bytes, cMH2O.size)) {
                                continue;
                            }
                            info_mask = 0;
                            std::memcpy(&info_mask, cMH2O.data + info.ofsInfoMask, mask_bytes);
                        }

                        size_t bit_index = 0;
                        for (int z = 0; z < info.height; ++z) {
                            for (int x = 0; x < info.width; ++x, ++bit_index) {
                                if (((info_mask >> bit_index) & 1ull) == 0) {
                                    continue;
                                }
                                mask_ptr[(info.yOffset + z) * 8 + (info.xOffset + x)] = 255;
                            }
                        }

                        PackedFloat32Array liquid_heights;
                        liquid_heights.resize(9 * 9);
                        float *height_ptr = liquid_heights.ptrw();
                        for (int i = 0; i < 9 * 9; ++i) {
                            height_ptr[i] = info.minHeight;
                        }

                        const bool has_height_values =
                            info.ofsHeightMap > 0 &&
                            (info.liquidVertexFormat == 0 ||
                             info.liquidVertexFormat == 1 ||
                             info.liquidVertexFormat == 3);

                        if (has_height_values) {
                            const size_t vertex_count = (size_t)(info.width + 1) * (info.height + 1);
                            const size_t height_bytes = vertex_count * sizeof(float);
                            if (!mh2o_region_fits(info.ofsHeightMap, height_bytes, cMH2O.size)) {
                                continue;
                            }

                            const float *src_heights =
                                reinterpret_cast<const float *>(cMH2O.data + info.ofsHeightMap);
                            for (int z = 0; z <= info.height; ++z) {
                                for (int x = 0; x <= info.width; ++x) {
                                    const size_t src_index = (size_t)z * (info.width + 1) + x;
                                    const size_t dst_index =
                                        (size_t)(info.yOffset + z) * 9 + (info.xOffset + x);
                                    height_ptr[dst_index] =
                                        std::clamp(src_heights[src_index], info.minHeight, info.maxHeight);
                                }
                            }
                        }

                        Dictionary liquid;
                        liquid["liquid_id"]     = (int)info.liquidId;
                        liquid["vertex_format"] = (int)info.liquidVertexFormat;
                        liquid["min_height"]    = info.minHeight;
                        liquid["max_height"]    = info.maxHeight;
                        liquid["x_offset"]      = (int)info.xOffset;
                        liquid["y_offset"]      = (int)info.yOffset;
                        liquid["width"]         = (int)info.width;
                        liquid["height"]        = (int)info.height;
                        liquid["mask"]          = liquid_mask;
                        liquid["heights"]       = liquid_heights;
                        liquids.push_back(liquid);
                    }
                }
            }
        }

        Dictionary cd;
        cd["index_x"]   = (int)hdr.indexX;
        cd["index_y"]   = (int)hdr.indexY;
        cd["origin"]    = Vector3(ox, oy, oz);
        cd["heights"]   = heights;
        cd["normals"]   = normals_arr;
        cd["holes"]     = (int)(hdr.holes & 0xFFFF);
        cd["layers"]    = layers;
        cd["alpha_maps"] = alpha_maps;
        cd["liquids"]   = liquids;

        chunks_array[ci] = cd;
    }

    result["chunks"] = chunks_array;
}

// ─────────────────────────────────────────────────────────────────────────────
void ADTLoader::_bind_methods() {
    ClassDB::bind_method(D_METHOD("load_adt",            "path"),  &ADTLoader::load_adt);
    ClassDB::bind_method(D_METHOD("load_adt_from_bytes", "bytes"), &ADTLoader::load_adt_from_bytes);
}
