#include "raycast.h"

#include <algorithm>
#include <cmath>

namespace {

int floorInt(float v) {
    return static_cast<int>(std::floor(v));
}

void pointToBlockAndVoxel(float px, float py, float pz, int& bx, int& by, int& bz, int& vx, int& vy, int& vz) {
    bx = floorInt(px);
    by = floorInt(py);
    bz = floorInt(pz);

    const float localX = px - static_cast<float>(bx);
    const float localY = py - static_cast<float>(by);
    const float localZ = pz - static_cast<float>(bz);

    vx = std::clamp(static_cast<int>(localX * static_cast<float>(MicroVoxelGrid::SIZE)), 0, MicroVoxelGrid::SIZE - 1);
    vy = std::clamp(static_cast<int>(localY * static_cast<float>(MicroVoxelGrid::SIZE)), 0, MicroVoxelGrid::SIZE - 1);
    vz = std::clamp(static_cast<int>(localZ * static_cast<float>(MicroVoxelGrid::SIZE)), 0, MicroVoxelGrid::SIZE - 1);
}

BlockType sampleSolid(const World& world, int bx, int by, int bz, int vx, int vy, int vz) {
    const BlockType block = world.getBlock(bx, by, bz);

    if (world.isBlockSubdivided(bx, by, bz)) {
        return world.getVoxel(bx, by, bz, vx, vy, vz);
    }

    if (block == BlockType::Air || block == BlockType::Water) {
        return BlockType::Air;
    }

    return block;
}

}

RaycastHit Raycast::cast(
    const World& world,
    const Camera& camera,
    float maxDistance,
    float stepSize
) {
    RaycastHit result{};

    const Vec3 origin = camera.position;
    const Vec3 dir = camera.forward();

    float prevPx = origin.x;
    float prevPy = origin.y;
    float prevPz = origin.z;

    for (float t = 0.0f; t <= maxDistance; t += stepSize) {
        const float px = origin.x + dir.x * t;
        const float py = origin.y + dir.y * t;
        const float pz = origin.z + dir.z * t;

        int bx = 0, by = 0, bz = 0;
        int vx = 0, vy = 0, vz = 0;
        pointToBlockAndVoxel(px, py, pz, bx, by, bz, vx, vy, vz);

        if (sampleSolid(world, bx, by, bz, vx, vy, vz) != BlockType::Air) {
            result.hit = true;
            result.blockX = bx;
            result.blockY = by;
            result.blockZ = bz;
            result.voxelX = vx;
            result.voxelY = vy;
            result.voxelZ = vz;

            int pbx = 0, pby = 0, pbz = 0;
            int pvx = 0, pvy = 0, pvz = 0;
            pointToBlockAndVoxel(prevPx, prevPy, prevPz, pbx, pby, pbz, pvx, pvy, pvz);
            result.placeX = pbx;
            result.placeY = pby;
            result.placeZ = pbz;
            result.placeVoxelX = pvx;
            result.placeVoxelY = pvy;
            result.placeVoxelZ = pvz;
            return result;
        }

        prevPx = px;
        prevPy = py;
        prevPz = pz;
    }

    return result;
}
