#ifndef RAY_GLSL_
#define RAY_GLSL_

#include "palette.glsl"
#include "svo.glsl"
#include "blockTint.glsl"

struct Ray
{
    uvec3 coords;
    vec3 offset;
    vec3 dir;
    vec3 normal;
    vec4 color;
    vec4 light;
    vec3 tau;
    vec4 tint;
    bool hit;
    float distance;
    NodeID currentNode;
    Order currentOrder;
    float refractiveIndex;
    uint steps;
    bool allowRefract;
};

uint getOrderOffsetMask(Order order)
{
    return (1u << order) - 1u;
}

void rayReset(inout Ray ray, uvec3 coords, vec3 offset, vec3 dir)
{
    ray.coords = coords;
    ray.offset = offset;
    ray.dir = dir;
    ray.steps = 0;
}

void rayDescendToDetail(inout Ray ray)
{
    // Move down levels until full detail is reached.
    while ((ray.currentNode & GLSL_SVO_VOXEL_BIT) == 0)
    {
        NodeID subnode = svoDescend(ray.currentNode, ray.currentOrder, ray.coords);
        ray.currentNode = subnode;
        ray.currentOrder--;
    }
}

Ray rayNew(uvec3 coords, vec3 offset, vec3 dir)
{
    Ray ray;
    rayReset(ray, coords, offset, dir);

    ray.currentNode = 0;   // node 0 is the root node
    ray.currentOrder = GLSL_SVO_MAX_ORDER;
    rayDescendToDetail(ray);

    ray.hit = false;
    ray.tau = vec3(0.0);
    ray.distance = 0.0;
    ray.tint = vec4(0.0);
    ray.allowRefract = true;

    Voxel voxel = readVoxel(ray.currentNode);
    vec4 refractProps = decodeXYZW8(voxel.refract);
    ray.refractiveIndex = refractProps.y * 10.0;
    ray.color = decodeSRGBA8(voxel.color);

    return ray;
}

Ray rayNew(Ray prevSegment, vec3 dir)
{
    Ray ray;
    rayReset(ray, prevSegment.coords, prevSegment.offset, dir);

    ray.hit = false;
    ray.tau = vec3(0.0);
    ray.distance = 0.0;
    ray.tint = vec4(0.0);
    ray.allowRefract = true;

    ray.currentOrder = prevSegment.currentOrder;
    ray.currentNode = prevSegment.currentNode;
    ray.refractiveIndex = prevSegment.refractiveIndex;
    ray.color = prevSegment.color;

    return ray;
}

float rayGetDistance(Ray ray)
{
    return ray.distance;
}

void rayAdvance(inout Ray ray, Order order)
{
    ray.steps++;
    uint offsetMask = getOrderOffsetMask(order);

    ray.offset += vec3(ray.coords & offsetMask);
    ray.coords &= ~offsetMask;

    float nodeSize = float(1 << order);

    vec3 stepSize = vec3(
        ray.dir.x > 0 ? nodeSize - ray.offset.x : ray.offset.x,
        ray.dir.y > 0 ? nodeSize - ray.offset.y : ray.offset.y,
        ray.dir.z > 0 ? nodeSize - ray.offset.z : ray.offset.z
    );

    stepSize /= abs(ray.dir);
    float minDistanceToNextNode = min(stepSize.x, min(stepSize.y, stepSize.z));

    uint isX = uint(minDistanceToNextNode == stepSize.x);
    uint isY = uint(minDistanceToNextNode == stepSize.y) * (1 - isX);
    uint isZ = (1 - isX) * (1 - isY);
    uvec3 traversal = uvec3(isX, isY, isZ);
    ray.normal = -normalize(ray.dir * vec3(traversal));

    ray.offset += minDistanceToNextNode * ray.dir;
    ray.offset *= vec3(uvec3(1) - traversal);

    uvec3 directionMask = uvec3(greaterThan(ray.dir, vec3(0.0)));
    uvec3 borderCoords = ray.coords + directionMask * (1 << order) - uvec3(1) * (1 - directionMask);
    vec3 borderOffsets = step(ray.dir, vec3(0.0)) * vec3(0.9999);

    uvec3 newCoords = ray.coords + uvec3(ivec3(floor(ray.offset)));
    newCoords *= uvec3(1) - traversal;
    newCoords += borderCoords * traversal;

    ray.coords = newCoords;
    ray.offset = fract(ray.offset);
    ray.offset += borderOffsets * vec3(traversal);

    ray.distance += minDistanceToNextNode;

    vec3 thisFilter = (1.0 - ray.color.rgb) * ray.color.a;
    ray.tau += thisFilter * minDistanceToNextNode * 0.333;
}

void rayAdvance(inout Ray ray)
{
    rayAdvance(ray, ray.currentOrder);
}

vec3 rayGetFilterColor(Ray ray)
{
    return exp(-ray.tau);
}

void rayMarch(inout Ray ray, float maxDistance)
{
    const float epsilon = 1e-5;

    while (ray.steps < 256)
    {
        rayAdvance(ray);
        if (ray.distance >= maxDistance)
        {
            break;
        }

        ray.currentOrder = GLSL_SVO_MAX_ORDER;
        ray.currentNode = 0;

        rayDescendToDetail(ray);

        VoxelRef newVoxel = ray.currentNode;

        Voxel voxel = readVoxel(newVoxel);
        ray.color = decodeSRGBA8(voxel.color);
        ray.light = decodeXYZW8(voxel.lightSpec);

        vec3 bias = decodeXYZW8(voxel.bias).xyz;
        vec3 noise = calculateNoise(ivec3(ray.coords));
        ray.color = vec4(hsv2rgb(rgb2hsv(ray.color.xyz) + bias * noise), ray.color.a);

        vec4 refractProps = decodeXYZW8(voxel.refract);

        const float wavyness = refractProps.x;
        float materialRefractiveIndex = refractProps.y * 10.0;
        const float materialNormalNoise = refractProps.z;

        if ((voxel.color & 0xFF) != 0x00 && (uFrameData.flags & GLSL_FRAMEDATA_DEBUG_REFRACT) != 0)
        {
            materialRefractiveIndex = uFrameData.debugRefractValue * 10.0;
        }

        float ftime = float(uFrameData.ticks) / 200.0;
        float factor = (float(int(ray.coords.x & 0xFFF)) + ray.offset.x) / 4.0 + ftime;
        ray.normal += wavyness * ray.normal.yzx * vec3(sin(factor), cos(factor), sin(factor));
        factor = (float(int(ray.coords.y & 0xFFF)) + ray.offset.y) / 4.0 + ftime;
        ray.normal += wavyness * ray.normal.zxy * vec3(sin(factor), cos(factor), sin(factor));
        ray.normal += materialNormalNoise * noise;
        ray.normal = normalize(ray.normal);

        vec4 layerTint = getTint((ray.coords & getOrderOffsetMask(GLSL_SVO_MAX_ORDER + 1)) >> 3);
        if (layerTint.a > epsilon && ray.color.a > epsilon)
        {
            ray.tint = layerTint;
        }

        if (ray.allowRefract && (voxel.color & 0xFF) != 0xFF)
        {
            bool reflected;
            ray.dir = normalize(refract(ray.dir, ray.normal, ray.refractiveIndex, materialRefractiveIndex, reflected));
            ray.refractiveIndex = materialRefractiveIndex * float(!reflected) + ray.refractiveIndex * float(reflected);
        }
        else
        {
            ray.refractiveIndex = materialRefractiveIndex;
        }

        if ((voxel.color & 0xFF) == 0xFF) {
            ray.color *= vec4(rayGetFilterColor(ray), 1.0);
            ray.hit = true;
            break;
        }
    }
}

#endif