/*
 * 3D Engine 
 * triangle.c: 
 * Based on pikuma.com 3D software renderer in C
 * Copyright (c) 2021 986-Studio. All rights reserved.
 *
 * Created by Manoël Trapier on 04/03/2021.
 */

#include <stdint.h>
#include <stdbool.h>

#include <display.h>
#include <triangle.h>
#include <math.h>

bool doPerpectiveCorrection = true;

static vec3_t barycentricWeights(vec2_t a, vec2_t b, vec2_t c, vec2_t p)
{
    vec2_t ab = vec2SubVectors(b, a);
    vec2_t bc = vec2SubVectors(c, b);
    vec2_t ac = vec2SubVectors(c, a);
    vec2_t ap = vec2SubVectors(p, a);
    vec2_t bp = vec2SubVectors(p, b);
    vec3_t ret;

    double areaTriangleABC = (ab.x * ac.y) - (ab.y * ac.x);

    double alpha = ((bc.x * bp.y) - (bp.x * bc.y)) / areaTriangleABC;
    double beta = ((ap.x * ac.y) - (ac.x * ap.y)) / areaTriangleABC;
    double gamma = 1 - alpha - beta;

    ret.x = alpha;
    ret.y = beta;
    ret.z = gamma;

    return ret;
}

void drawTriangle(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t x2, int32_t y2, colour_t colour)
{
    drawLine(x0, y0, x1, y1, colour);
    drawLine(x1, y1, x2, y2, colour);
    drawLine(x2, y2, x0, y0, colour);
}

/* ----------------------------------- Filled triangles ----------------------------------- */
static void drawZPixel(int32_t x, int32_t y, vec4_t a, vec4_t b, vec4_t c, colour_t colour)
{
    vec2_t pointP = { x, y };
    vec2_t a2 = vec2FromVec4(a);
    vec2_t b2 = vec2FromVec4(b);
    vec2_t c2 = vec2FromVec4(c);

    if ((x < 0) || (y < 0) || (x > windowWidth) ||(y > windowHeight))
    {
        return;
    }

    vec3_t weights = barycentricWeights(a2, b2, c2, pointP);

    double alpha = weights.x;
    double beta = weights.y;
    double gamma = weights.z;
    double interpolatedReciprocalW;

    interpolatedReciprocalW = (1 / a.w) * alpha + (1 / b.w) * beta + (1 / c.w) * gamma;

    /* Inverse to get logical W invrementing going further to us */
    interpolatedReciprocalW = 1 - interpolatedReciprocalW;
    if (interpolatedReciprocalW < zBuffer[(windowWidth * y) + x])
    {
        drawPixel(x, y, colour);

        /* Update Z Buffer */
        zBuffer[(windowWidth * y) + x] = interpolatedReciprocalW;
    }
}

void drawFilledTriangle(struct triangle_t *t)
{
    int32_t x, y;

    if (t->points[0].y > t->points[1].y)
    {
        doubleSwap(&t->points[0].x, &t->points[1].x); doubleSwap(&t->points[0].y, &t->points[1].y);
        doubleSwap(&t->points[0].z, &t->points[1].z); doubleSwap(&t->points[0].w, &t->points[1].w);
    }
    if (t->points[1].y > t->points[2].y)
    {
        doubleSwap(&t->points[1].x, &t->points[2].x); doubleSwap(&t->points[1].y, &t->points[2].y);
        doubleSwap(&t->points[1].z, &t->points[2].z); doubleSwap(&t->points[1].w, &t->points[2].w);
    }
    if (t->points[0].y > t->points[1].y)
    {
        doubleSwap(&t->points[0].x, &t->points[1].x); doubleSwap(&t->points[0].y, &t->points[1].y);
        doubleSwap(&t->points[0].z, &t->points[1].z); doubleSwap(&t->points[0].w, &t->points[1].w);
    }

    vec4_t a = t->points[0];
    vec4_t b = t->points[1];
    vec4_t c = t->points[2];

    /* Render the top part */
    double inverseSlope1 = 0;
    double inverseSlope2 = 0;
    int32_t x0 = a.x, y0 = a.y;
    int32_t x1 = b.x, y1 = b.y;
    int32_t x2 = c.x, y2 = c.y;

    if ((y1 - y0) != 0) { inverseSlope1 = (double)(x1 - x0) / abs(y1 - y0); }
    if ((y2 - y0) != 0) { inverseSlope2 = (double)(x2 - x0) / abs(y2 - y0); }

    if ((y1 - y0) != 0)
    {
        for (y = y0 ; y <= y1 ; y++)
        {
            int32_t xStart = x1 + (y - y1) * inverseSlope1;
            int32_t xEnd = x0 + (y - y0) * inverseSlope2;

            if (doZBuffer)
            {
                if (xEnd < xStart)
                {
                    intSwap(&xStart, &xEnd);
                }
                for (x = xStart ; x <= xEnd ; x++)
                {
                    drawZPixel(x, y, a, b, c,t->colour);
                }
            }
            else
            {
                drawHLine(xStart, y, xEnd, t->colour);
            }
        }
    }

    /* Render the bottom part */
    inverseSlope1 = 0;
    if ((y2 - y1) != 0) { inverseSlope1 = (double)(x2 - x1) / abs(y2 - y1); }

    if ((y2 - y1) != 0)
    {
        for (y = y1 ; y <= y2 ; y++)
        {
            int32_t xStart = x2 + (y - y2) * inverseSlope1;
            int32_t xEnd = x0 + (y - y0) * inverseSlope2;

            if (doZBuffer)
            {
                if (xEnd < xStart)
                {
                    intSwap(&xStart, &xEnd);
                }
                for (x = xStart ; x <= xEnd ; x++)
                {
                    drawZPixel(x, y, a, b, c,t->colour);
                }
            }
            else
            {
                drawHLine(xStart, y, xEnd, t->colour);
            }
        }
    }

}

/* ----------------------------------- Textured triangles ----------------------------------- */
static void drawTexel(int32_t x, int32_t y, vec4_t a, vec4_t b, vec4_t c, tex2_t ta, tex2_t tb, tex2_t tc, colour_t *texture)
{
    vec2_t pointP = { x, y };
    vec2_t a2 = vec2FromVec4(a);
    vec2_t b2 = vec2FromVec4(b);
    vec2_t c2 = vec2FromVec4(c);

    if ((x < 0) || (y < 0) || (x > windowWidth) ||(y > windowHeight))
    {
        return;
    }

    vec3_t weights = barycentricWeights(a2, b2, c2, pointP);

    double alpha = weights.x;
    double beta = weights.y;
    double gamma = weights.z;
    double interpolatedU, interpolatedV, interpolatedReciprocalW;
    int32_t texX, texY;

    interpolatedReciprocalW = (1 / a.w) * alpha + (1 / b.w) * beta + (1 / c.w) * gamma;

    if (doPerpectiveCorrection)
    {
        interpolatedU = (ta.u / a.w) * alpha + (tb.u / b.w) * beta + (tc.u / c.w) * gamma;
        interpolatedV = ((1 - ta.v) / a.w) * alpha + ((1 - tb.v) / b.w) * beta + ((1 - tc.v) / c.w) * gamma;


        interpolatedU /= interpolatedReciprocalW;
        interpolatedV /= interpolatedReciprocalW;
    }
    else
    {
        interpolatedU = ta.u * alpha + tb.u * beta + tc.u * gamma;
        interpolatedV = (1 - ta.v) * alpha + (1 - tb.v) * beta + (1 - tc.v) * gamma;
    }

    texX = abs((int32_t)(interpolatedU * textureWidth));
    texY = abs((int32_t)(interpolatedV * textureHeight));

    texX = texX % textureWidth;
    texY = texY % textureWidth;

    if (doZBuffer)
    {
        /* Inverse to get logical W invrementing going further to us */
        interpolatedReciprocalW = 1 - interpolatedReciprocalW;
        if (interpolatedReciprocalW < zBuffer[(windowWidth * y) + x])
        {
            drawPixel(x, y, texture[(texY * textureWidth) + texX]);

            /* Update Z Buffer */
            zBuffer[(windowWidth * y) + x] = interpolatedReciprocalW;
        }
    }
    else
    {
        drawPixel(x, y, texture[(texY * textureWidth) + texX]);
    }
}


void drawTextureTriangle(struct triangle_t *t)
{
    int32_t x, y;

    if (t->points[0].y > t->points[1].y)
    {
        doubleSwap(&t->points[0].x, &t->points[1].x); doubleSwap(&t->points[0].y, &t->points[1].y);
        doubleSwap(&t->points[0].z, &t->points[1].z); doubleSwap(&t->points[0].w, &t->points[1].w);
        doubleSwap(&t->textureCoordinates[0].u, &t->textureCoordinates[1].u);
        doubleSwap(&t->textureCoordinates[0].v, &t->textureCoordinates[1].v);
    }
    if (t->points[1].y > t->points[2].y)
    {
        doubleSwap(&t->points[1].x, &t->points[2].x); doubleSwap(&t->points[1].y, &t->points[2].y);
        doubleSwap(&t->points[1].z, &t->points[2].z); doubleSwap(&t->points[1].w, &t->points[2].w);
        doubleSwap(&t->textureCoordinates[1].u, &t->textureCoordinates[2].u);
        doubleSwap(&t->textureCoordinates[1].v, &t->textureCoordinates[2].v);
    }
    if (t->points[0].y > t->points[1].y)
    {
        doubleSwap(&t->points[0].x, &t->points[1].x); doubleSwap(&t->points[0].y, &t->points[1].y);
        doubleSwap(&t->points[0].z, &t->points[1].z); doubleSwap(&t->points[0].w, &t->points[1].w);
        doubleSwap(&t->textureCoordinates[0].u, &t->textureCoordinates[1].u);
        doubleSwap(&t->textureCoordinates[0].v, &t->textureCoordinates[1].v);
    }

    vec4_t a = t->points[0];
    vec4_t b = t->points[1];
    vec4_t c = t->points[2];

    /* Render the top part */
    double inverseSlope1 = 0;
    double inverseSlope2 = 0;
    int32_t x0 = a.x, y0 = a.y;
    int32_t x1 = b.x, y1 = b.y;
    int32_t x2 = c.x, y2 = c.y;

    if ((y1 - y0) != 0) { inverseSlope1 = (double)(x1 - x0) / abs(y1 - y0); }
    if ((y2 - y0) != 0) { inverseSlope2 = (double)(x2 - x0) / abs(y2 - y0); }

    if ((y1 - y0) != 0)
    {
        for (y = y0 ; y <= y1 ; y++)
        {
            int32_t xStart = x1 + (y - y1) * inverseSlope1;
            int32_t xEnd = x0 + (y - y0) * inverseSlope2;

            if (xEnd < xStart)
            {
                intSwap(&xStart, &xEnd);
            }

            for (x = xStart ; x <= xEnd ; x++)
            {
                drawTexel(x, y, a, b, c,
                          t->textureCoordinates[0],
                          t->textureCoordinates[1],
                          t->textureCoordinates[2],
                          t->texture);
            }
        }
    }

    /* Render the bottom part */
    inverseSlope1 = 0;
    if ((y2 - y1) != 0) { inverseSlope1 = (double)(x2 - x1) / abs(y2 - y1); }

    if ((y2 - y1) != 0)
    {
        for (y = y1 ; y <= y2 ; y++)
        {
            int32_t xStart = x2 + (y - y2) * inverseSlope1;
            int32_t xEnd = x0 + (y - y0) * inverseSlope2;

            if (xEnd < xStart)
            {
                intSwap(&xStart, &xEnd);
            }

            for (x = xStart ; x <= xEnd ; x++)
            {
                drawTexel(x, y, a, b, c,
                          t->textureCoordinates[0],
                          t->textureCoordinates[1],
                          t->textureCoordinates[2],
                          t->texture);
            }
        }
    }

}

/* ---- Utility ---- */

int compareTrianglesZOrder(const void *p1, const void *p2)
{
    triangle_t *t1 = (struct triangle_t *)p1;
    triangle_t *t2 = (struct triangle_t *)p2;

    if (t1->averageDepth > t2->averageDepth)
    {
        return -1;
    }
    else if (t1->averageDepth < t2->averageDepth)
    {
        return  1;
    }

    return 0;
}