/***********************************************/
/*           Copyright (c) 2025 Belmu          */
/*             All Rights Reserved             */
/***********************************************/

uniform sampler2D modelData;

#define ALPHA_TEST

struct VoxelIntersection {
    bool intersect;
    vec2 dists;
    vec3 position;
    vec3 normal;

    Voxel voxel;
    ivec2 textureCoords;

    bool inside;
};

struct Box {
    vec3 size;
    vec3 offset;
    vec2 modelRotation;

    vec3 pivot;
    vec3 boxRotation;

    bool uvLock;
};

vec3 rotateYX(in vec3 vector, vec2 rotation) {
    if (rotation == vec2(0.0)) return vector;

    vector = rotate(vector, vec3(0.0, 1.0, 0.0), rotation.y);
    vector = rotate(vector, vec3(1.0, 0.0, 0.0), rotation.x);
    return vector;
}

vec3 rotateXY(in vec3 vector, vec2 rotation) {
    if (rotation == vec2(0.0)) return vector;

    vector = rotate(vector, vec3(1.0, 0.0, 0.0), rotation.x);
    vector = rotate(vector, vec3(0.0, 1.0, 0.0), rotation.y);
    return vector;
}

vec3 rotateYXZ(in vec3 vector, vec3 rotation) {
    if (rotation == vec3(0.0)) return vector;

    vector = rotate(vector, vec3(0.0, 1.0, 0.0), rotation.y);
    vector = rotate(vector, vec3(1.0, 0.0, 0.0), rotation.x);
    vector = rotate(vector, vec3(0.0, 0.0, 1.0), rotation.z);
    return vector;
}

void intersectBox(inout VoxelIntersection hit, vec3 origin, vec3 direction, ivec3 index, int id, Box box) {
    bool translucent = isTranslucent(id) || id == 974;

    vec3 boxSize = translucent ? box.size - 1e-5 : box.size;

    vec3 invDir = 1.0 / direction;
    vec3 n      = invDir * origin;
    vec3 k      = abs(invDir) * boxSize;

    vec3 minDists = -n - k;
    vec3 maxDists = -n + k;

    float distNear = maxOf(minDists);
    float distFar  = minOf(maxDists);

    hit.dists = vec2(-1.0);

    if (distNear > distFar || distFar < 0.0) return;

    const float eps = 1e-4;

    hit.inside = distNear <= eps && distFar > -eps && translucent;
    vec4 dists = hit.inside ? vec4(maxDists, distFar) : vec4(minDists, distNear);

    hit.normal   = vec3(equal(dists.xyz, dists.www)) * sign(direction) * (hit.inside ? 1.0 : -1.0);
    hit.position = origin + direction * dists.w + box.offset;

    vec3 localOffset = index + vec3(0.5); 

    vec3 rotatedNormal   = rotateXY(hit.normal,   -box.modelRotation);
    vec3 rotatedPosition = rotateXY(hit.position, -box.modelRotation) + localOffset;

    if (box.uvLock) {
        hit.normal   = rotatedNormal;
        hit.position = rotatedPosition;

        hit.textureCoords = calculateTextureCoords(hit.voxel.packedTileData, hit.position, hit.normal);
    } else {
        hit.textureCoords = calculateTextureCoords(hit.voxel.packedTileData, hit.position + localOffset, hit.normal);

        hit.normal   = rotatedNormal;
        hit.position = rotatedPosition;
    }

    #if defined ALPHA_TEST
        bool alphaTest = texelFetch(albedoAtlas, hit.textureCoords, 0).a > 0.102;
             alphaTest = alphaTest || id == GLASS;

        if (!alphaTest) return;
    #endif

    hit.dists = vec2(dists.w, distFar);
}

vec3 fetchModelData(ivec2 coords) {
    return texelFetch(modelData, ivec2(coords), 0).rgb;
}

Box decodeBox(in int x, in int y) {
    Box box;

    vec3 size = fetchModelData(ivec2(x, y));
    box.size  = round(size * 64.0) / 64.0;
    box.size  = max(box.size, vec3(EPS));
    x++;

    vec3 offset = fetchModelData(ivec2(x, y));
    box.offset  = (round(offset * 64.0) / 64.0) * 2.0 - 1.0;
    x++;

    vec3 modelRotation = fetchModelData(ivec2(x, y));
    box.modelRotation  = modelRotation.rg * 270;
    box.uvLock         = bool(modelRotation.b);
    x++;

    vec3 pivot = fetchModelData(ivec2(x, y));
    box.pivot  = pivot * 2.0 - 1.0;
    x++;

    vec3 boxRotation = fetchModelData(ivec2(x, y));
    box.boxRotation  = round(boxRotation * 180 - 90);

    return box;
}

const Box cube = Box(vec3(0.5), vec3(0.0), vec2(0.0), vec3(0.0), vec3(0.0), false);

void intersectVoxelBlockModel(inout VoxelIntersection hit, vec3 origin, vec3 direction, ivec3 index, int blockId) {
    #if SPHERES_MODE == 1
        /* Spheres */
        if (blockId == SPHERE_0 || blockId == SPHERE_1) {
            vec2 sphere   = intersectSphere(origin, direction, 0.4995);
            hit.dists.x   = sphere.x <= 1e-3 && sphere.y > -1e-3 ? sphere.y : sphere.x;
            hit.position  = origin + direction * hit.dists.x;
            hit.normal    = normalize(hit.position);
            hit.position += index + vec3(0.5);
            return;
        }
    #endif

    if (blockId == GLASS) {
        intersectBox(hit, origin, direction, index, blockId, cube);
        return;
    }

    int id    = blockId - 1;
    int count = int(fetchModelData(ivec2(0, id)).r * 255.0);

    for (int i = 0; i < count; i++) {
        Box box = decodeBox(1 + i * 5, id);

        if (blockId == WATER || blockId == LAVA) {
            uint topId = unpackId(readVoxel(index + ivec3(0, 1, 0)).packedData);

            if (topId == WATER || topId == LAVA) {
                box.size = vec3(0.5); box.offset = vec3(0.0);
            }
        }

        vec3 rotOrigin   = origin, rotDirection = direction;
        vec3 pivotOffset = box.pivot - box.offset;

        rotOrigin    = rotateYX(rotOrigin   , box.modelRotation) - box.offset - pivotOffset;
        rotDirection = rotateYX(rotDirection, box.modelRotation);

        rotOrigin    = rotateYXZ(rotOrigin   , -box.boxRotation) + pivotOffset;
        rotDirection = rotateYXZ(rotDirection, -box.boxRotation);

        VoxelIntersection boxHit; boxHit.voxel = hit.voxel;
        intersectBox(boxHit, rotOrigin, rotDirection, index, blockId, box);
        
        if (boxHit.dists.x >= 0.0 && (hit.dists.x < 0.0 || boxHit.dists.x < hit.dists.x)) hit = boxHit;
    }
}
