const float aabb_max = 1.0;

uniform float uFrames;
uniform bool uIsFullSphere;
uniform float uRadius;
uniform vec3 uOffset;

// = object.matrixWorld
uniform mat4 modelMatrix;

// = camera.matrixWorldInverse * object.matrixWorld
uniform mat4 modelViewMatrix;

// = camera.projectionMatrix
uniform mat4 projectionMatrix;

// = camera.matrixWorldInverse
uniform mat4 viewMatrix;

// = inverse transpose of modelViewMatrix
uniform mat3 normalMatrix;

// = camera position in world space
uniform vec3 cameraPosition;

in vec2 uv;

out vec2 uv_frame1;
out vec2 xy_frame1;
flat out  vec2  frame1;
flat out  vec3 frame1_normal;
out vec2 uv_frame2;
out vec2 xy_frame2;
flat out  vec2 frame2;
flat out  vec3 frame2_normal;
out vec2 uv_frame3;
out vec2 xy_frame3;
flat out  vec2 frame3;
flat out  vec3 frame3_normal;
out vec4 quad_blend_weights;

vec2 VecToSphereOct(vec3 pivotToCamera)
{
    vec3 octant = sign(pivotToCamera);

    //  |x| + |y| + |z| = 1
    float sum = dot(pivotToCamera, octant);
    vec3 octahedron = pivotToCamera / sum;

    if (octahedron.y < 0.0){
        vec3 absolute = abs(octahedron);
        octahedron.xz = octant.xz * vec2(1.0 - absolute.z, 1.0 - absolute.x);
    }
    return octahedron.xz;
}

//for hemisphere
vec2 VecToHemiSphereOct(vec3 vec)
{
    vec.y = max(vec.y, 0.001);
    vec = normalize(vec);
    vec3 octant = sign(vec);

    //  |x| + |y| + |z| = 1
    float sum = dot(vec, octant);
    vec3 octahedron = vec / sum;

    return vec2(
    octahedron.x + octahedron.z,
    octahedron.z - octahedron.x
    );
}

vec2 VectorToGrid(vec3 vec)
{
    if (uIsFullSphere)
    {
        return VecToSphereOct(vec);
    }
    else
    {
        return VecToHemiSphereOct(vec);
    }
}
//for sphere
vec3 OctaSphereEnc(vec2 coord)
{
    coord = (coord - 0.5) * 2.0;
    vec3 position = vec3(coord.x, 0.0, coord.y);
    vec2 absolute = abs(position.xz);
    position.y = 1.0 - absolute.x - absolute.y;

    if (position.y < 0.0)
    {
        position.xz = sign(position.xz) * vec2(1.0 - absolute.y, 1.0 - absolute.x);
    }

    return position;
}

//for hemisphere
vec3 OctaHemiSphereEnc(vec2 coord)
{
    //coord = (0, 0.27)
    //pos = -0.27, 0, -0.63

    vec3 position = vec3(coord.x - coord.y, 0.0, -1.0 + coord.x + coord.y);
    vec2 absolute = abs(position.xz);
    position.y = 1.0 - absolute.x - absolute.y;
    return position;
}

vec3 GridToVector(vec2 coord)
{
    if (uIsFullSphere)
    {
        return OctaSphereEnc(coord);
    }
    else
    {
        return OctaHemiSphereEnc(coord);
    }
}

vec3 FrameXYToRay(vec2 frame, vec2 frameCountMinusOne)
{
    //divide frame x y by framecount minus one to get 0-1
    vec2 f = (frame.xy/ frameCountMinusOne);
    //bias and scale to -1 to 1

    vec3 vec = GridToVector(f);
    vec = normalize(vec);
    return vec;
}

vec3 SpriteProjection(vec3 pivotToCameraRayLocal, vec2 size, vec2 loc_uv)
{
    vec3 z = normalize(pivotToCameraRayLocal);
    vec3 x, y;
    vec3 up = vec3(0, 1, 0);
    //cross product doesnt work if we look directly from bottom
    if (abs(z.y) > 0.999f)
    {
        up = vec3(0, 0, -1);
    }
    x = normalize(cross(up, z));
    y = normalize(cross(x, z));

    loc_uv -= vec2(0.5, 0.5);
    vec2 uv = (loc_uv) * 2.0;//-1 to 1

    vec3 newX = x * uv.x;
    vec3 newY = y * uv.y;

    vec2 vecSize = size * 0.5;

    newX *= vecSize.x;
    newY *= vecSize.y;

    return newX + newY;
}

vec4 quadBlendWieghts(vec2 coords)
{
    vec4 res;
    /* 0 0 0
    0 0 0
    1 0 0 */
    res.x = min(1.0 - coords.x, 1.0 - coords.y);
    /* 1 0 0
    0 0 0
    0 0 1 */
    res.y = abs(coords.x - coords.y);
    /* 0 0 1
    0 0 0
    0 0 0 */
    res.z = min(coords.x, coords.y);
    /* 0 0 0
    0 0 1
    0 1 1 */
    res.w = ceil(coords.x - coords.y);
    //res.xyz /= (res.x + res.y + res.z);
    return res;
}

//this function works well in orthogonal projection. It works okeyish with further distances of perspective projection
vec2 virtualPlaneUV(vec3 plane_normal, vec3  plane_x, vec3  plane_y, vec3 pivotToCameraRay, vec3 vertexToCameraRay, float size)
{
    plane_normal = normalize(plane_normal);
    plane_x = normalize(plane_x);
    plane_y = normalize(plane_y);

    // plane_normal is normalized but pivotToCameraRay & vertexToCameraRay are NOT
    // so what are we doing here ?
    // We are calculating length of pivotToCameraRay projected onto plane_normal
    // so pivotToCameraRay is vector to camera from CENTER OF object
    // we are recalculting this distance taking into account new plane normal
    float projectedNormalRayLength = dot(plane_normal, pivotToCameraRay);
    // tihs is direction is almost the same as origin, but its to individual vertex
    // not sure this is correct for perspective projection
    float projectedVertexRayLength = dot(plane_normal, vertexToCameraRay);
    // basically its length difference betwen center and vertex - "not precise"
    // projectedVertexRayLength is bigger than projectedNormalRayLength when vertex is
    // further than "main front facing billboard"
    // so offsetLength is getting smaller, otherwise is getting bigger
    float offsetLength = projectedNormalRayLength/projectedVertexRayLength;

    // ok so offsetLength is just a length
    // we want a vector so we multiply it by vertexToCameraRay to get this offset
    // now what are we REALY doing is calculuating distance difference
    // se are SUBSTRACTING pivotToCameraRay vector
    // we would get difference between center of plane and vertex rotated
    vec3 offsetVector = vertexToCameraRay * offsetLength - pivotToCameraRay;

    // we got the offset of rotated vertex, but we need to offset it from correct plane axis
    // so again we projecting length of intersection (offset of rotated vertex) onto plane_x
    // and plane_y
    vec2 duv = vec2(
    dot(plane_x, offsetVector),
    dot(plane_y, offsetVector)
    );

    //we are in space -1 to 1
    duv /= 2.0 * size;
    duv += 0.5;
    return duv;
}
void calcuateXYbasis(vec3 plane_normal, out vec3 plane_x, out vec3 plane_y)
{
    vec3 up = vec3(0, 1, 0);
    //cross product doesnt work if we look directly from bottom
    if (abs(plane_normal.y) > 0.999f)
    {
        up = vec3(0, 0, 1);
    }
    plane_x = normalize(cross(plane_normal, up));
    plane_y = normalize(cross(plane_x, plane_normal));
}
vec3 projectOnPlaneBasis(vec3 ray, vec3 plane_normal, vec3 plane_x, vec3 plane_y)
{
    //reproject plane normal onto planeXY basos
    return normalize(vec3(
    dot(plane_x, ray),
    dot(plane_y, ray),
    dot(plane_normal, ray)
    ));
}

void main()
{
    vec2 framesMinusOne =  vec2(uFrames-1.0);
    vec3 cameraPos_WS = cameraPosition;
    vec3 cameraPos_OS = (viewMatrix* vec4(cameraPos_WS, 1.0)).xyz;

    //radius * 2
    vec2 size = vec2(2.0) * uRadius;

    mat4 m4 = modelViewMatrix;

    m4[0][0] = 1.0;
    m4[0][1] = 0.0;
    m4[0][2] = 0.0;

    m4[1][0] = 0.0;
    m4[1][1] = 1.0;
    m4[1][2] = 0.0;

    m4[2][0] = 0.0;
    m4[2][1] = 0.0;
    m4[2][2] = 1.0;

    vec3 object_scale = vec3(
    length(modelViewMatrix[0].xyz),
    length(modelViewMatrix[1].xyz),
    length(modelViewMatrix[2].xyz)
    );

    vec4 mvPosition = m4 * vec4(object_scale*(vec3(uv.x -0.5, uv.y - 0.5, 0.0) + uOffset/ (uRadius*2.0)), 1.0);
    gl_Position = projectionMatrix * mvPosition;
    vec2 pos = gl_Position.xy/gl_Position.w;


    mat4 inverse_matrix = inverse(projectionMatrix * modelViewMatrix);
    //compute ray's start and end as inversion of this coordinates
    //in near and far clip planes
    vec4 near_4 = inverse_matrix * (vec4(pos, -1.0, 1.0));
    vec4 far_4 = inverse_matrix * (vec4(pos, 1.0, 1.0));

    vec3 local_ray_near = near_4.xyz / near_4.w;
    vec3 local_ray_far = far_4.xyz/ far_4.w;

    //TODO: check if this is correct. We are using orho projected images, so
    // camera far away
    //vec3 pivotToCameraRay = (cameraPos_OS) * 10.0;
    //vec3 pivotToCameraDir = normalize(cameraPos_OS);

    vec3 ray_direction = local_ray_far-local_ray_near;
    ray_direction *= -1.0;

    vec2 grid = VectorToGrid(ray_direction);
    //bias and scale to 0 to 1
    grid = clamp((grid + 1.0) * 0.5, 0.0, 1.0);
    grid *= framesMinusOne;
    grid = clamp(grid, vec2(0.0), framesMinusOne);
    vec2 gridFloor = min(floor(grid), framesMinusOne);
    vec2 gridFract = fract(grid);

    //vec3 projected = SpriteProjection(pivotToCameraDir, size, uv);
    //vec3 vertexToCameraRay = (pivotToCameraRay - (projected));
    //vec3 vertexToCameraDir = normalize(vertexToCameraRay);

    frame1 = gridFloor;
    quad_blend_weights = quadBlendWieghts(gridFract);
    //convert frame coordinate to octahedron direction
    vec3 projectedQuadADir = FrameXYToRay(frame1, framesMinusOne);

    frame2 = clamp(frame1 + mix(vec2(0, 1), vec2(1, 0), quad_blend_weights.w), vec2(0, 0), framesMinusOne);
    vec3 projectedQuadBDir = FrameXYToRay(frame2, framesMinusOne);

    frame3 = clamp(frame1 + vec2(1.0), vec2(0, 0), framesMinusOne);
    vec3 projectedQuadCDir = FrameXYToRay(frame3, framesMinusOne);

    frame1_normal = (modelViewMatrix *vec4(projectedQuadADir, 0)).xyz;
    frame2_normal = (modelViewMatrix *vec4(projectedQuadBDir, 0)).xyz;
    frame3_normal = (modelViewMatrix *vec4(projectedQuadCDir, 0)).xyz;

    //calcute virtual planes projections
    vec3 plane_x1, plane_y1, plane_x2, plane_y2, plane_x3, plane_y3;
    calcuateXYbasis(projectedQuadADir, plane_x1, plane_y1);
    uv_frame1 = virtualPlaneUV(projectedQuadADir, plane_x1, plane_y1, local_ray_near, local_ray_far, uRadius);
    xy_frame1 = projectOnPlaneBasis(-ray_direction, projectedQuadADir, plane_x1, plane_y1).xy;

    calcuateXYbasis(projectedQuadBDir, plane_x2, plane_y2);
    uv_frame2 = virtualPlaneUV(projectedQuadBDir, plane_x2, plane_y2, local_ray_near, local_ray_far, uRadius);
    xy_frame2 = projectOnPlaneBasis(-ray_direction, projectedQuadBDir, plane_x2, plane_y2).xy;

    calcuateXYbasis(projectedQuadCDir, plane_x3, plane_y3);
    uv_frame3 = virtualPlaneUV(projectedQuadCDir, plane_x3, plane_y3, local_ray_near, local_ray_far, uRadius);
    xy_frame3 = projectOnPlaneBasis(-ray_direction, projectedQuadCDir, plane_x3, plane_y3).xy;

    //to fragment shader
    //to fragment shader
    //    vec3 p = (projected+uOffset)+pivotToCameraDir*aabb_max;

    //    gl_Position = projectionMatrix  * vec4(p,1.0);

    //    NORMAL = normalize(pivotToCameraDir);
    //    TANGENT= normalize(cross(NORMAL, vec3(0.0, 1.0, 0.0)));
    //    BINORMAL = normalize(cross(TANGENT, NORMAL));
}
