#ifndef CONE_GLSL_
#define CONE_GLSL_

#include "svo.glsl"
#include "blockTint.glsl"
#include "nodeSamples.glsl"

struct Cone
{
    uvec3 coords;
    vec3 offset;
    vec3 dir;
    vec3 radiance;
    vec3 tau;
    vec3 filterValue;
    float distance;
    uint steps;
    Order order;
    float tanAngle;
};

Cone coneNew(uvec3 coords, vec3 offset, vec3 dir, float angle)
{
    Cone cone;
    cone.coords = coords;
    cone.offset = offset;
    cone.dir = dir;
    cone.radiance = vec3(0.0);
    cone.tau = vec3(0.0);
    cone.filterValue = vec3(0.0);
    cone.distance = 0.0;
    cone.steps = 0;
    cone.order = 0;
    cone.tanAngle = tan(angle);
    return cone;
}

bool coneIsSaturated(Cone cone)
{
    float minTau = min(cone.tau.x, min(cone.tau.y, cone.tau.z));
    float maxFilter = max(cone.filterValue.x, max(cone.filterValue.y, cone.filterValue.z));
    return minTau > 3.0 || maxFilter > 0.9;
}

vec3 coneGetThroughput(Cone cone)
{
    if (coneIsSaturated(cone))
    {
        return vec3(0.0);
    }

    return exp(-cone.tau);
}

LightSample coneSampleVoxel(inout Cone cone, uint face, Order minOrder, uvec3 coords)
{
    if (minOrder > 3)
    {
        return LightSample(vec3(0.0), vec3(0.0));
    }

    Order currentOrder = GLSL_SVO_MAX_ORDER;
    NodeID currentNode = 0;

    while ((currentNode & GLSL_SVO_VOXEL_BIT) == 0 && currentOrder != minOrder)
    {
        currentNode = svoDescend(currentNode, currentOrder--, coords);
    }

    if (coords == cone.coords)
    {
        cone.order = currentOrder;
    }

    return fetchFaceSample(currentNode, face);
}

LightSample coneSampleMip(inout Cone cone, uint face, Order minOrder, uvec3 traversal)
{
    if (minOrder < 0)
    {
        return coneSampleVoxel(cone, face, 0, cone.coords);
    }

    vec3 nodeOffset = cone.offset + vec3(cone.coords & uvec3((1 << minOrder) - 1));
    float nodeSize = float(1 << minOrder);
    nodeOffset /= nodeSize;

    uvec3 u = traversal.yzx;
    uvec3 v = traversal.zxy;

    bvec3 lowerHalf = lessThan(nodeOffset, vec3(0.5));

    uvec3 nodeSizeInteger = uvec3(1 << minOrder);

    uvec3 base = cone.coords - uvec3(lowerHalf) * (uvec3(1) - traversal) * nodeSizeInteger;
    vec3 baseParameters = nodeOffset + vec3(lowerHalf) - vec3(0.5);

    float up = clamp(dot(baseParameters * vec3(u), vec3(1.0)), 0.0, 1.0);
    float vp = clamp(dot(baseParameters * vec3(v), vec3(1.0)), 0.0, 1.0);

    float w00 = (1 - up) * (1 - vp);
    float w10 = (up)     * (1 - vp);
    float w01 = (1 - up) * (vp);
    float w11 = (up)     * (vp);

    LightSample s00 = coneSampleVoxel(cone, face, minOrder, base);
    LightSample s10 = coneSampleVoxel(cone, face, minOrder, base + u * nodeSizeInteger);
    LightSample s01 = coneSampleVoxel(cone, face, minOrder, base + v * nodeSizeInteger);
    LightSample s11 = coneSampleVoxel(cone, face, minOrder, base + u * nodeSizeInteger + v * nodeSizeInteger);

    LightSample result;
    result.radiance = s00.radiance * w00 + s01.radiance * w01 + s10.radiance * w10 + s11.radiance * w11;
    result.filterValue = s00.filterValue * w00 + s01.filterValue * w01 + s10.filterValue * w10 + s11.filterValue * w11;

    return result;
}

void coneAddSample(inout Cone cone, uint face, uvec3 traversal)
{
    float lod = min(3.0, log2(max(0.5, 2.0 * cone.distance * cone.tanAngle)));
    Order minOrder = Order(floor(lod));
    LightSample sampleAbove = coneSampleMip(cone, face, Order(floor(lod)) + 1, traversal);
    LightSample sampleBelow = coneSampleMip(cone, face, Order(floor(lod)), traversal);

    LightSample sampleFace = mix(sampleBelow, sampleAbove, fract(lod));
    cone.radiance += sampleFace.radiance * coneGetThroughput(cone);
    cone.filterValue = sampleFace.filterValue;
}

void coneAdvance(inout Cone cone, Order order)
{
    cone.steps++;
    uint offsetMask = getOrderOffsetMask(order);

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

    float nodeSize = float(1 << order);

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

    stepSize /= abs(cone.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);
    uint axis = isY * 1 + isZ * 2;
    uint orientation = (int(sign(cone.dir * vec3(traversal))) + 1) >> 1;
    uint face = (axis << 1) | orientation;

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

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

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

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

    cone.distance += minDistanceToNextNode;
    cone.steps++;

    cone.tau += cone.filterValue * minDistanceToNextNode;
    coneAddSample(cone, face, traversal);
}

void coneMarch(inout Cone cone, float maxDistance)
{
    while (cone.steps < 256 && cone.distance < maxDistance && !coneIsSaturated(cone))
    {
        coneAdvance(cone, cone.order);
    }
}

#endif