
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;

precision mediump float;
layout(std430) buffer;

layout(binding = 0) restrict readonly buffer elementBufferSSBO
{
    oc_gl_path_elt elements[];
}

elementBuffer;

layout(binding = 1) coherent restrict buffer segmentCountBufferSSBO
{
    int elements[];
}

segmentCountBuffer;

layout(binding = 2) restrict buffer segmentBufferSSBO
{
    oc_gl_segment elements[];
}

segmentBuffer;

layout(binding = 3) restrict buffer pathQueueBufferSSBO
{
    oc_gl_path_queue elements[];
}

pathQueueBuffer;

layout(binding = 4) coherent restrict buffer tileQueueBufferSSBO
{
    oc_gl_tile_queue elements[];
}

tileQueueBuffer;

layout(binding = 5) coherent restrict buffer tileOpCountBufferSSBO
{
    int elements[];
}

tileOpCountBuffer;

layout(binding = 6) restrict buffer tileOpBufferSSBO
{
    oc_gl_tile_op elements[];
}

tileOpBuffer;

layout(location = 0) uniform float scale;
layout(location = 1) uniform uint tileSize;
layout(location = 2) uniform int elementBufferStart;

void bin_to_tiles(int segIndex)
{
    //NOTE: add segment index to the queues of tiles it overlaps with
    const oc_gl_segment seg = segmentBuffer.elements[segIndex];
    const oc_gl_path_queue pathQueue = pathQueueBuffer.elements[seg.pathIndex];

    ivec4 pathArea = pathQueue.area;
    ivec4 coveredTiles = ivec4(seg.box) / int(tileSize);
    int xMin = max(0, coveredTiles.x - pathArea.x);
    int yMin = max(0, coveredTiles.y - pathArea.y);
    int xMax = min(coveredTiles.z - pathArea.x, pathArea.z - 1);
    int yMax = min(coveredTiles.w - pathArea.y, pathArea.w - 1);

    for(int y = yMin; y <= yMax; y++)
    {
        for(int x = xMin; x <= xMax; x++)
        {
            vec4 tileBox = vec4(float(x + pathArea.x),
                                float(y + pathArea.y),
                                float(x + pathArea.x + 1),
                                float(y + pathArea.y + 1))
                         * float(tileSize);

            vec2 bl = { tileBox.x, tileBox.y };
            vec2 br = { tileBox.z, tileBox.y };
            vec2 tr = { tileBox.z, tileBox.w };
            vec2 tl = { tileBox.x, tileBox.w };

            int sbl = side_of_segment(bl, seg);
            int sbr = side_of_segment(br, seg);
            int str = side_of_segment(tr, seg);
            int stl = side_of_segment(tl, seg);

            bool crossL = (stl * sbl < 0);
            bool crossR = (str * sbr < 0);
            bool crossT = (stl * str < 0);
            bool crossB = (sbl * sbr < 0);

            vec2 s0, s1;
            if(seg.config == OC_GL_TL || seg.config == OC_GL_BR)
            {
                s0 = seg.box.xy;
                s1 = seg.box.zw;
            }
            else
            {
                s0 = seg.box.xw;
                s1 = seg.box.zy;
            }
            bool s0Inside = s0.x >= tileBox.x
                         && s0.x < tileBox.z
                         && s0.y >= tileBox.y
                         && s0.y < tileBox.w;

            bool s1Inside = s1.x >= tileBox.x
                         && s1.x < tileBox.z
                         && s1.y >= tileBox.y
                         && s1.y < tileBox.w;

            if(crossL || crossR || crossT || crossB || s0Inside || s1Inside)
            {
                int tileOpIndex = atomicAdd(tileOpCountBuffer.elements[0], 1);

                if(tileOpIndex < tileOpBuffer.elements.length())
                {
                    tileOpBuffer.elements[tileOpIndex].kind = OC_GL_OP_SEGMENT;
                    tileOpBuffer.elements[tileOpIndex].index = segIndex;
                    tileOpBuffer.elements[tileOpIndex].windingOffsetOrCrossRight = 0;
                    tileOpBuffer.elements[tileOpIndex].next = -1;

                    int tileQueueIndex = pathQueue.tileQueues + y * pathArea.z + x;

                    tileOpBuffer.elements[tileOpIndex].next = atomicExchange(tileQueueBuffer.elements[tileQueueIndex].first,
                                                                             tileOpIndex);
                    if(tileOpBuffer.elements[tileOpIndex].next == -1)
                    {
                        tileQueueBuffer.elements[tileQueueIndex].last = tileOpIndex;
                    }

                    //NOTE: if the segment crosses the tile's bottom boundary, update the tile's winding offset
                    if(crossB)
                    {
                        atomicAdd(tileQueueBuffer.elements[tileQueueIndex].windingOffset, seg.windingIncrement);
                    }

                    //NOTE: if the segment crosses the right boundary, mark it.
                    if(crossR)
                    {
                        tileOpBuffer.elements[tileOpIndex].windingOffsetOrCrossRight = 1;
                    }
                }
            }
        }
    }
}

int push_segment(in vec2 p[4], int kind, int pathIndex)
{
    int segIndex = atomicAdd(segmentCountBuffer.elements[0], 1);

    if(segIndex < segmentBuffer.elements.length())
    {
        vec2 s, c, e;

        switch(kind)
        {
            case OC_GL_LINE:
                s = p[0];
                c = p[0];
                e = p[1];
                break;

            case OC_GL_QUADRATIC:
                s = p[0];
                c = p[1];
                e = p[2];
                break;

            case OC_GL_CUBIC:
            {
                s = p[0];
                float sqrNorm0 = dot(p[1] - p[0], p[1] - p[0]);
                float sqrNorm1 = dot(p[3] - p[2], p[3] - p[2]);
                if(sqrNorm0 < sqrNorm1)
                {
                    c = p[2];
                }
                else
                {
                    c = p[1];
                }
                e = p[3];
            }
            break;
        }

        bool goingUp = e.y >= s.y;
        bool goingRight = e.x >= s.x;

        vec4 box = vec4(min(s.x, e.x),
                        min(s.y, e.y),
                        max(s.x, e.x),
                        max(s.y, e.y));

        segmentBuffer.elements[segIndex].kind = kind;
        segmentBuffer.elements[segIndex].pathIndex = pathIndex;
        segmentBuffer.elements[segIndex].windingIncrement = goingUp ? 1 : -1;
        segmentBuffer.elements[segIndex].box = box;

        float dx = c.x - box.x;
        float dy = c.y - box.y;
        float alpha = (box.w - box.y) / (box.z - box.x);
        float ofs = box.w - box.y;

        if(goingUp == goingRight)
        {
            if(kind == OC_GL_LINE)
            {
                segmentBuffer.elements[segIndex].config = OC_GL_BR;
            }
            else if(dy > alpha * dx)
            {
                segmentBuffer.elements[segIndex].config = OC_GL_TL;
            }
            else
            {
                segmentBuffer.elements[segIndex].config = OC_GL_BR;
            }
        }
        else
        {
            if(kind == OC_GL_LINE)
            {
                segmentBuffer.elements[segIndex].config = OC_GL_TR;
            }
            else if(dy < ofs - alpha * dx)
            {
                segmentBuffer.elements[segIndex].config = OC_GL_BL;
            }
            else
            {
                segmentBuffer.elements[segIndex].config = OC_GL_TR;
            }
        }
    }
    return (segIndex);
}

#define square(x) ((x) * (x))
#define cube(x) ((x) * (x) * (x))

void line_setup(vec2 p[4], int pathIndex)
{
    int segIndex = push_segment(p, OC_GL_LINE, pathIndex);
    if(segIndex < segmentBuffer.elements.length())
    {
        segmentBuffer.elements[segIndex].hullVertex = p[0];
        bin_to_tiles(segIndex);
    }
}

vec2 quadratic_blossom(vec2 p[4], float u, float v)
{
    vec2 b10 = u * p[1] + (1 - u) * p[0];
    vec2 b11 = u * p[2] + (1 - u) * p[1];
    vec2 b20 = v * b11 + (1 - v) * b10;
    return (b20);
}

void quadratic_slice(vec2 p[4], float s0, float s1, out vec2 sp[4])
{
    /*NOTE: using blossoms to compute sub-curve control points ensure that the fourth point
	        of sub-curve (s0, s1) and the first point of sub-curve (s1, s3) match.
	        However, due to numerical errors, the evaluation of B(s=0) might not be equal to
	        p[0] (and likewise, B(s=1) might not equal p[3]).
	        We handle that case explicitly to ensure that we don't create gaps in the paths.
	*/
    sp[0] = (s0 == 0) ? p[0] : quadratic_blossom(p, s0, s0);
    sp[1] = quadratic_blossom(p, s0, s1);
    sp[2] = (s1 == 1) ? p[2] : quadratic_blossom(p, s1, s1);
}

int quadratic_monotonize(vec2 p[4], out float splits[4])
{
    //NOTE: compute split points
    int count = 0;
    splits[0] = 0;
    count++;

    vec2 r = (p[0] - p[1]) / (p[2] - 2 * p[1] + p[0]);
    if(r.x > r.y)
    {
        float tmp = r.x;
        r.x = r.y;
        r.y = tmp;
    }
    if(r.x > 0 && r.x < 1)
    {
        splits[count] = r.x;
        count++;
    }
    if(r.y > 0 && r.y < 1)
    {
        splits[count] = r.y;
        count++;
    }
    splits[count] = 1;
    count++;
    return (count);
}

mat3 barycentric_matrix(vec2 v0, vec2 v1, vec2 v2)
{
    float det = v0.x * (v1.y - v2.y) + v1.x * (v2.y - v0.y) + v2.x * (v0.y - v1.y);
    mat3 B = { { v1.y - v2.y, v2.y - v0.y, v0.y - v1.y },
               { v2.x - v1.x, v0.x - v2.x, v1.x - v0.x },
               { v1.x * v2.y - v2.x * v1.y, v2.x * v0.y - v0.x * v2.y, v0.x * v1.y - v1.x * v0.y } };
    B *= (1 / det);
    return (B);
}

void quadratic_emit(vec2 p[4], int pathIndex)
{
    int segIndex = push_segment(p, OC_GL_QUADRATIC, pathIndex);

    if(segIndex < segmentBuffer.elements.length())
    {
        //NOTE: compute implicit equation matrix
        float det = p[0].x * (p[1].y - p[2].y) + p[1].x * (p[2].y - p[0].y) + p[2].x * (p[0].y - p[1].y);

        float a = p[0].y - p[1].y + 0.5 * (p[2].y - p[0].y);
        float b = p[1].x - p[0].x + 0.5 * (p[0].x - p[2].x);
        float c = p[0].x * p[1].y - p[1].x * p[0].y + 0.5 * (p[2].x * p[0].y - p[0].x * p[2].y);
        float d = p[0].y - p[1].y;
        float e = p[1].x - p[0].x;
        float f = p[0].x * p[1].y - p[1].x * p[0].y;

        float flip = (segmentBuffer.elements[segIndex].config == OC_GL_TL
                      || segmentBuffer.elements[segIndex].config == OC_GL_BL)
                       ? -1
                       : 1;

        float g = flip * (p[2].x * (p[0].y - p[1].y) + p[0].x * (p[1].y - p[2].y) + p[1].x * (p[2].y - p[0].y));

        segmentBuffer.elements[segIndex].implicitMatrix = (1 / det) * mat3(a, d, 0., b, e, 0., c, f, g);
        segmentBuffer.elements[segIndex].hullVertex = p[1];

        bin_to_tiles(segIndex);
    }
}

void quadratic_setup(vec2 p[4], int pathIndex)
{
    float splits[4];
    int splitCount = quadratic_monotonize(p, splits);

    //NOTE: produce bézier curve for each consecutive pair of roots
    for(int sliceIndex = 0; sliceIndex < splitCount - 1; sliceIndex++)
    {
        vec2 sp[4];
        quadratic_slice(p, splits[sliceIndex], splits[sliceIndex + 1], sp);
        quadratic_emit(sp, pathIndex);
    }
}

int quadratic_roots_with_det(float a, float b, float c, float det, out float r[2])
{
    int count = 0;

    if(a == 0)
    {
        if(b != 0)
        {
            count = 1;
            r[0] = -c / b;
        }
    }
    else
    {
        b /= 2.0;

        if(det >= 0)
        {
            count = (det == 0) ? 1 : 2;

            if(b > 0)
            {
                float q = b + sqrt(det);
                r[0] = -c / q;
                r[1] = -q / a;
            }
            else if(b < 0)
            {
                float q = -b + sqrt(det);
                r[0] = q / a;
                r[1] = c / q;
            }
            else
            {
                float q = sqrt(-a * c);
                if(abs(a) >= abs(c))
                {
                    r[0] = q / a;
                    r[1] = -q / a;
                }
                else
                {
                    r[0] = -c / q;
                    r[1] = c / q;
                }
            }
        }
    }
    if(count > 1 && r[0] > r[1])
    {
        float tmp = r[0];
        r[0] = r[1];
        r[1] = tmp;
    }
    return (count);
}

int quadratic_roots(float a, float b, float c, out float r[2])
{
    float det = square(b) / 4. - a * c;
    return (quadratic_roots_with_det(a, b, c, det, r));
}

vec2 cubic_blossom(vec2 p[4], float u, float v, float w)
{
    vec2 b10 = u * p[1] + (1 - u) * p[0];
    vec2 b11 = u * p[2] + (1 - u) * p[1];
    vec2 b12 = u * p[3] + (1 - u) * p[2];
    vec2 b20 = v * b11 + (1 - v) * b10;
    vec2 b21 = v * b12 + (1 - v) * b11;
    vec2 b30 = w * b21 + (1 - w) * b20;
    return (b30);
}

void cubic_slice(vec2 p[4], float s0, float s1, out vec2 sp[4])
{
    /*NOTE: using blossoms to compute sub-curve control points ensure that the fourth point
	        of sub-curve (s0, s1) and the first point of sub-curve (s1, s3) match.
	        However, due to numerical errors, the evaluation of B(s=0) might not be equal to
	        p[0] (and likewise, B(s=1) might not equal p[3]).
	        We handle that case explicitly to ensure that we don't create gaps in the paths.
	*/
    sp[0] = (s0 == 0) ? p[0] : cubic_blossom(p, s0, s0, s0);
    sp[1] = cubic_blossom(p, s0, s0, s1);
    sp[2] = cubic_blossom(p, s0, s1, s1);
    sp[3] = (s1 == 1) ? p[3] : cubic_blossom(p, s1, s1, s1);
}

#define CUBIC_ERROR 0
#define CUBIC_SERPENTINE 1
#define CUBIC_CUSP 2
#define CUBIC_CUSP_INFINITY 3
#define CUBIC_LOOP 4
#define CUBIC_DEGENERATE_QUADRATIC 5
#define CUBIC_DEGENERATE_LINE 6

struct cubic_info
{
    int kind;
    mat4 K;
    vec2 ts[2];
    float d1;
    float d2;
    float d3;
};

cubic_info cubic_classify(vec2 c[4])
{
    cubic_info result;
    result.kind = CUBIC_ERROR;
    mat4 F;

    /*NOTE(martin):
		now, compute determinants d0, d1, d2, d3, which gives the coefficients of the
	        inflection points polynomial:

		I(t, s) = d0*t^3 - 3*d1*t^2*s + 3*d2*t*s^2 - d3*s^3

		The roots of this polynomial are the inflection points of the parametric curve, in homogeneous
		coordinates (ie we can have an inflection point at inifinity with s=0).

		         |x3 y3 w3|              |x3 y3 w3|             |x3 y3 w3|              |x2 y2 w2|
		d0 = det |x2 y2 w2|    d1 = -det |x2 y2 w2|    d2 = det |x1 y1 w1|    d3 = -det |x1 y1 w1|
		         |x1 y1 w1|              |x0 y0 w0|             |x0 y0 w0|              |x0 y0 w0|

		In our case, the pi.w equal 1 (no point at infinity), so _in_the_power_basis_, w1 = w2 = w3 = 0 and w0 = 1
		(which also means d0 = 0)

		//WARN: there seems to be a mismatch between the signs of the d_i and the orientation test in the Loop-Blinn paper?
		//      flipping the sign of the d_i doesn't change the roots (and the implicit matrix), but it does change the orientation.
		//      Keeping the signs of the paper puts the interior on the left of parametric travel, unlike what's stated in the paper.
		//      this may very well be an error on my part that's cancelled by flipping the signs of the d_i though!
	*/

    float d1 = -(c[3].y * c[2].x - c[3].x * c[2].y);
    float d2 = -(c[3].x * c[1].y - c[3].y * c[1].x);
    float d3 = -(c[2].y * c[1].x - c[2].x * c[1].y);

    result.d1 = d1;
    result.d2 = d2;
    result.d3 = d3;

    //NOTE(martin): compute the second factor of the discriminant discr(I) = d1^2*(3*d2^2 - 4*d3*d1)
    float discrFactor2 = 3.0 * square(d2) - 4.0 * d3 * d1;

    //NOTE(martin): each following case gives the number of roots, hence the category of the parametric curve
    if(abs(d1) <= 1e-6 && abs(d2) <= 1e-6 && abs(d3) > 1e-6)
    {
        //NOTE(martin): quadratic degenerate case
        //NOTE(martin): compute quadratic curve control point, which is at p0 + 1.5*(p1-p0) = 1.5*p1 - 0.5*p0
        result.kind = CUBIC_DEGENERATE_QUADRATIC;
    }
    else if((discrFactor2 > 0 && abs(d1) > 1e-6)
            || (discrFactor2 == 0 && abs(d1) > 1e-6))
    {
        //NOTE(martin): serpentine curve or cusp with inflection at infinity
        //              (these two cases are handled the same way).
        //NOTE(martin): compute the solutions (tl, sl), (tm, sm), and (tn, sn) of the inflection point equation
        float tmtl[2];
        quadratic_roots_with_det(1, -2 * d2, (4. / 3. * d1 * d3), (1. / 3.) * discrFactor2, tmtl);

        float tm = tmtl[0];
        float sm = 2 * d1;
        float tl = tmtl[1];
        float sl = 2 * d1;

        float invNorm = 1 / sqrt(square(tm) + square(sm));
        tm *= invNorm;
        sm *= invNorm;

        invNorm = 1 / sqrt(square(tl) + square(sl));
        tl *= invNorm;
        sl *= invNorm;

        /*NOTE(martin):
			the power basis coefficients of points k,l,m,n are collected into the rows of the 4x4 matrix F:

				| tl*tm            tl^3        tm^3        1 |
				| -sm*tl - sl*tm   -3sl*tl^2   -3*sm*tm^2  0 |
				| sl*sm            3*sl^2*tl   3*sm^2*tm   0 |
				| 0                -sl^3       -sm^3       0 |
		*/
        result.kind = (discrFactor2 > 0 && d1 != 0) ? CUBIC_SERPENTINE : CUBIC_CUSP;

        F = mat4(tl * tm, -sm * tl - sl * tm, sl * sm, 0,
                 cube(tl), -3 * sl * square(tl), 3 * square(sl) * tl, -cube(sl),
                 cube(tm), -3 * sm * square(tm), 3 * square(sm) * tm, -cube(sm),
                 1, 0, 0, 0);

        result.ts[0] = vec2(tm, sm);
        result.ts[1] = vec2(tl, sl);
    }
    else if(discrFactor2 < 0 && abs(d1) > 1e-6)
    {
        //NOTE(martin): loop curve
        result.kind = CUBIC_LOOP;

        float tetd[2];
        quadratic_roots_with_det(1, -2 * d2, 4 * (square(d2) - d1 * d3), -discrFactor2, tetd);

        float td = tetd[1];
        float sd = 2 * d1;
        float te = tetd[0];
        float se = 2 * d1;

        float invNorm = 1 / sqrt(square(td) + square(sd));
        td *= invNorm;
        sd *= invNorm;

        invNorm = 1 / sqrt(square(te) + square(se));
        te *= invNorm;
        se *= invNorm;

        /*NOTE(martin):
			the power basis coefficients of points k,l,m,n are collected into the rows of the 4x4 matrix F:

				| td*te            td^2*te                 td*te^2                1 |
				| -se*td - sd*te   -se*td^2 - 2sd*te*td    -sd*te^2 - 2*se*td*te  0 |
				| sd*se            te*sd^2 + 2*se*td*sd    td*se^2 + 2*sd*te*se   0 |
				| 0                -sd^2*se                -sd*se^2               0 |
		*/
        F = mat4(td * te, -se * td - sd * te, sd * se, 0,
                 square(td) * te, -se * square(td) - 2 * sd * te * td, te * square(sd) + 2 * se * td * sd, -square(sd) * se,
                 td * square(te), -sd * square(te) - 2 * se * td * te, td * square(se) + 2 * sd * te * se, -sd * square(se),
                 1, 0, 0, 0);

        result.ts[0] = vec2(td, sd);
        result.ts[1] = vec2(te, se);
    }
    else if(d2 != 0)
    {
        //NOTE(martin): cusp with cusp at infinity
        float tl = d3;
        float sl = 3 * d2;

        float invNorm = 1 / sqrt(square(tl) + square(sl));
        tl *= invNorm;
        sl *= invNorm;

        /*NOTE(martin):
			the power basis coefficients of points k,l,m,n are collected into the rows of the 4x4 matrix F:

				| tl    tl^3        1  1 |
				| -sl   -3sl*tl^2   0  0 |
				| 0     3*sl^2*tl   0  0 |
				| 0     -sl^3       0  0 |
		*/
        result.kind = CUBIC_CUSP_INFINITY;

        F = mat4(tl, -sl, 0, 0,
                 cube(tl), -3 * sl * square(tl), 3 * square(sl) * tl, -cube(sl),
                 1, 0, 0, 0,
                 1, 0, 0, 0);

        result.ts[0] = vec2(tl, sl);
        result.ts[1] = vec2(0, 0);
    }
    else
    {
        //NOTE(martin): line or point degenerate case
        result.kind = CUBIC_DEGENERATE_LINE;
    }

    /*
			F is then multiplied by M3^(-1) on the left which yelds the bezier coefficients k, l, m, n
			at the control points.

			               | 1  0   0   0 |
				M3^(-1) =  | 1  1/3 0   0 |
				           | 1  2/3 1/3 0 |
					       | 1  1   1   1 |
	*/
    mat4 invM3 = mat4(1, 1, 1, 1,
                      0, 1. / 3., 2. / 3., 1,
                      0, 0, 1. / 3., 1,
                      0, 0, 0, 1);

    result.K = transpose(invM3 * F);

    return (result);
}

vec2 select_hull_vertex(vec2 p0, vec2 p1, vec2 p2, vec2 p3)
{
    /*NOTE: check intersection of lines (p1-p0) and (p3-p2)
		P = p0 + u(p1-p0)
		P = p2 + w(p3-p2)

		control points are inside a right triangle so we should always find an intersection
	*/
    vec2 pm;

    float det = (p1.x - p0.x) * (p3.y - p2.y) - (p1.y - p0.y) * (p3.x - p2.x);
    float sqrNorm0 = dot(p1 - p0, p1 - p0);
    float sqrNorm1 = dot(p2 - p3, p2 - p3);

    if(abs(det) < 1e-3 || sqrNorm0 < 0.1 || sqrNorm1 < 0.1)
    {
        if(sqrNorm0 < sqrNorm1)
        {
            pm = p2;
        }
        else
        {
            pm = p1;
        }
    }
    else
    {
        float u = ((p0.x - p2.x) * (p2.y - p3.y) - (p0.y - p2.y) * (p2.x - p3.x)) / det;
        pm = p0 + u * (p1 - p0);
    }
    return (pm);
}

void cubic_emit(cubic_info curve, vec2 p[4], float s0, float s1, vec2 sp[4], int pathIndex)
{
    int segIndex = push_segment(sp, OC_GL_CUBIC, pathIndex);

    if(segIndex < segmentBuffer.elements.length())
    {
        vec2 v0 = p[0];
        vec2 v1 = p[3];
        vec2 v2;
        mat3 K;

        //TODO: haul that up in caller
        float sqrNorm0 = dot(p[1] - p[0], p[1] - p[0]);
        float sqrNorm1 = dot(p[2] - p[3], p[2] - p[3]);

        if(dot(p[0] - p[3], p[0] - p[3]) > 1e-5)
        {
            if(sqrNorm0 >= sqrNorm1)
            {
                v2 = p[1];
                K = mat3(curve.K[0].xyz, curve.K[3].xyz, curve.K[1].xyz);
            }
            else
            {
                v2 = p[2];
                K = mat3(curve.K[0].xyz, curve.K[3].xyz, curve.K[2].xyz);
            }
        }
        else
        {
            v1 = p[1];
            v2 = p[2];
            K = mat3(curve.K[0].xyz, curve.K[1].xyz, curve.K[2].xyz);
        }
        //NOTE: set matrices

        //TODO: should we compute matrix relative to a base point to avoid loss of precision
        //      when computing barycentric matrix?

        mat3 B = barycentric_matrix(v0, v1, v2);

        segmentBuffer.elements[segIndex].implicitMatrix = K * B;
        segmentBuffer.elements[segIndex].hullVertex = select_hull_vertex(sp[0], sp[1], sp[2], sp[3]);

        //NOTE: compute sign flip
        segmentBuffer.elements[segIndex].sign = 1;

        if(curve.kind == CUBIC_SERPENTINE
           || curve.kind == CUBIC_CUSP)
        {
            segmentBuffer.elements[segIndex].sign = (curve.d1 < 0) ? -1 : 1;
        }
        else if(curve.kind == CUBIC_LOOP)
        {
            float d1 = curve.d1;
            float d2 = curve.d2;
            float d3 = curve.d3;

            float H0 = d3 * d1 - square(d2) + d1 * d2 * s0 - square(d1) * square(s0);
            float H1 = d3 * d1 - square(d2) + d1 * d2 * s1 - square(d1) * square(s1);
            float H = (abs(H0) > abs(H1)) ? H0 : H1;
            segmentBuffer.elements[segIndex].sign = (H * d1 > 0) ? -1 : 1;
        }

        if(sp[3].y > sp[0].y)
        {
            segmentBuffer.elements[segIndex].sign *= -1;
        }

        //NOTE: bin to tiles
        bin_to_tiles(segIndex);
    }
}

void cubic_setup(vec2 p[4], int pathIndex)
{
    /*NOTE(martin): first convert the control points to power basis, multiplying by M3

		     | 1  0  0  0|      |p0|      |c0|
		M3 = |-3  3  0  0|, B = |p1|, C = |c1| = M3*B
		     | 3 -6  3  0|      |p2|      |c2|
		     |-1  3 -3  1|      |p3|      |c3|
	*/
    vec2 c[4] = {
        p[0],
        3.0 * (p[1] - p[0]),
        3.0 * (p[0] + p[2] - 2 * p[1]),
        3.0 * (p[1] - p[2]) + p[3] - p[0]
    };

    //NOTE: get classification, implicit matrix, double points and inflection points
    cubic_info curve = cubic_classify(c);

    if(curve.kind == CUBIC_DEGENERATE_LINE)
    {
        vec2 l[4] = { p[0], p[3], vec2(0), vec2(0) };
        line_setup(l, pathIndex);
        return;
    }
    else if(curve.kind == CUBIC_DEGENERATE_QUADRATIC)
    {
        vec2 quadPoint = vec2(1.5 * p[1].x - 0.5 * p[0].x, 1.5 * p[1].y - 0.5 * p[0].y);
        vec2 q[4] = { p[0], quadPoint, p[3], vec2(0) };
        quadratic_setup(q, pathIndex);
        return;
    }

    //NOTE: get the roots of B'(s) = 3.c3.s^2 + 2.c2.s + c1
    float rootsX[2];
    int rootCountX = quadratic_roots(3 * c[3].x, 2 * c[2].x, c[1].x, rootsX);

    float rootsY[2];
    int rootCountY = quadratic_roots(3 * c[3].y, 2 * c[2].y, c[1].y, rootsY);

    float roots[6];
    for(int i = 0; i < rootCountX; i++)
    {
        roots[i] = rootsX[i];
    }
    for(int i = 0; i < rootCountY; i++)
    {
        roots[i + rootCountX] = rootsY[i];
    }

    //NOTE: add double points and inflection points to roots if finite
    int rootCount = rootCountX + rootCountY;
    for(int i = 0; i < 2; i++)
    {
        if(curve.ts[i].y != 0)
        {
            roots[rootCount] = curve.ts[i].x / curve.ts[i].y;
            rootCount++;
        }
    }

    //NOTE: sort roots
    for(int i = 1; i < rootCount; i++)
    {
        float tmp = roots[i];
        int j = i - 1;
        while(j >= 0 && roots[j] > tmp)
        {
            roots[j + 1] = roots[j];
            j--;
        }
        roots[j + 1] = tmp;
    }

    //NOTE: compute split points
    float splits[8];
    int splitCount = 0;
    splits[0] = 0;
    splitCount++;
    for(int i = 0; i < rootCount; i++)
    {
        if(roots[i] > 0 && roots[i] < 1)
        {
            splits[splitCount] = roots[i];
            splitCount++;
        }
    }
    splits[splitCount] = 1;
    splitCount++;

    //NOTE: for each monotonic segment, compute hull matrix and sign, and emit segment
    for(int sliceIndex = 0; sliceIndex < splitCount - 1; sliceIndex++)
    {
        float s0 = splits[sliceIndex];
        float s1 = splits[sliceIndex + 1];
        vec2 sp[4];
        cubic_slice(p, s0, s1, sp);
        cubic_emit(curve, p, s0, s1, sp, pathIndex);
    }
}

void main()
{
    int eltIndex = int(gl_WorkGroupID.x);

    oc_gl_path_elt elt = elementBuffer.elements[elementBufferStart + eltIndex];

    switch(elt.kind)
    {
        case OC_GL_LINE:
        {
            vec2 p[4] = { elt.p[0] * scale, elt.p[1] * scale, vec2(0), vec2(0) };
            line_setup(p, elt.pathIndex);
        }
        break;

        case OC_GL_QUADRATIC:
        {
            vec2 p[4] = { elt.p[0] * scale, elt.p[1] * scale, elt.p[2] * scale, vec2(0) };
            quadratic_setup(p, elt.pathIndex);
        }
        break;

        case OC_GL_CUBIC:
        {
            vec2 p[4] = { elt.p[0] * scale, elt.p[1] * scale, elt.p[2] * scale, elt.p[3] * scale };
            cubic_setup(p, elt.pathIndex);
        }
        break;

        default:
            break;
    }
}
