/*
 * MINIMAL 3D RENDERER
 * ===================
 * 
 * This renders a spinning cube in 3D. The core idea is transforming points
 * from 3D space onto a 2D screen, then filling triangles with pixels.
 * 
 * The rendering pipeline transforms each vertex through several spaces:
 * - Object space (the cube's original coordinates)
 * - World space (after moving/rotating the cube)  
 * - Camera space (relative to where the camera is looking)
 * - Screen space (final 2D pixel coordinates)
 * 
 * Backface culling skips triangles facing away from the camera, which both
 * optimizes performance and prevents drawing the hidden back faces of the cube.
 */

#include 
#include 
#include 
#include 

/* Basic 3D vector representing a point in space */
typedef struct { float x, y, z; } Vec3;

/* Triangle defined by three vertices and a color */
typedef struct {
    Vec3 v[3];
    unsigned int color;
} Triangle;

/* 
 * Cross product computes a vector perpendicular to two input vectors using
 * the determinant formula. Given vectors a=(ax,ay,az) and b=(bx,by,bz), the
 * cross product aÃb produces a vector whose components are:
 * 
 * x = ay*bz - az*by
 * y = az*bx - ax*bz  
 * z = ax*by - ay*bx
 * 
 * Geometrically, the magnitude |aÃb| equals the area of the parallelogram
 * formed by a and b. The direction follows the right-hand rule: if you curl
 * your right hand from a toward b, your thumb points in the direction of aÃb.
 * 
 * For a triangle with vertices v0,v1,v2, computing (v1-v0)Ã(v2-v0) gives the
 * surface normal vector that points outward from the front face. This is
 * fundamental for lighting calculations and determining which side of a surface
 * we're looking at.
 */
Vec3 cross(Vec3 a, Vec3 b) {
    return (Vec3){a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x};
}

/* 
 * Dot product computes the scalar a·b = ax*bx + ay*by + az*bz. This measures
 * the projection of one vector onto another. Mathematically, a·b = |a||b|cos(θ)
 * where θ is the angle between the vectors and |a| denotes the magnitude
 * (length) of vector a computed as sqrt(ax²+ay²+az²).
 * 
 * From the cosine relationship, we can determine angles without computing them:
 * - If a·b > 0: angle is acute (< 90°), vectors point somewhat together
 * - If a·b = 0: angle is exactly 90°, vectors are perpendicular
 * - If a·b < 0: angle is obtuse (> 90°), vectors point somewhat apart
 * 
 * For backface culling, we compute normal·view_direction. If this is negative,
 * the surface normal points away from the camera (angle > 90°), meaning we're
 * looking at the back of the triangle. The mathematical property dot(a,b) < 0
 * precisely captures "pointing in opposite directions."
 */
float dot(Vec3 a, Vec3 b) {
    return a.x*b.x + a.y*b.y + a.z*b.z;
}

/* Vector subtraction gives the direction from b to a */
Vec3 sub(Vec3 a, Vec3 b) {
    return (Vec3){a.x-b.x, a.y-b.y, a.z-b.z};
}

/*
 * Rotation around the Y axis applies the 2D rotation matrix to the X-Z plane
 * while leaving Y unchanged. The standard 2D rotation matrix is:
 * 
 * [cos(θ)  -sin(θ)] [x]   [x*cos(θ) - z*sin(θ)]
 * [sin(θ)   cos(θ)] [z] = [x*sin(θ) + z*cos(θ)]
 * 
 * In 3D, this becomes:
 * x' = x*cos(θ) + z*sin(θ)
 * y' = y
 * z' = -x*sin(θ) + z*cos(θ)
 * 
 * The trigonometric functions encode circular motion. As θ increases from 0
 * to 2Ï, the point (x,z) traces a circle. At θ=0, cos(0)=1 and sin(0)=0, so
 * x'=x and z'=z (no rotation). At θ=Ï/2 (90°), cos(Ï/2)=0 and sin(Ï/2)=1,
 * so x'=z and z'=-x (90° rotation). The negative sign in the z' equation
 * determines the rotation direction (clockwise vs counter-clockwise when
 * viewed from above).
 */
Vec3 rotate_y(Vec3 v, float angle) {
    float c = cosf(angle), s = sinf(angle);
    return (Vec3){v.x*c + v.z*s, v.y, -v.x*s + v.z*c};
}

/*
 * Rotation around the X axis applies 2D rotation to the Y-Z plane while X
 * remains constant. The transformation matrix operates on the Y and Z
 * components:
 * 
 * x' = x
 * y' = y*cos(θ) - z*sin(θ)
 * z' = y*sin(θ) + z*cos(θ)
 * 
 * This rotates points around the X axis like a wheel spinning. Combined with
 * Y-axis rotation, we can achieve arbitrary 3D orientations through Euler
 * angles, though the order of rotations matters due to the non-commutative
 * property of matrix multiplication (R_x·R_y â  R_y·R_x).
 */
Vec3 rotate_x(Vec3 v, float angle) {
    float c = cosf(angle), s = sinf(angle);
    return (Vec3){v.x, v.y*c - v.z*s, v.y*s + v.z*c};
}

/*
 * Perspective projection transforms 3D points to 2D screen coordinates using
 * the pinhole camera model. A point (x,y,z) in camera space projects onto a
 * 2D image plane at distance f (focal length) from the origin.
 * 
 * By similar triangles, a point at distance z projects to position:
 * x_screen = f * (x/z)
 * y_screen = f * (y/z)
 * 
 * The division by z creates the perspective effect: larger z (farther away)
 * produces smaller screen coordinates, making distant objects appear smaller.
 * This is realistic (how cameras and eyes work) but means the cube appears
 * distorted - the front face looks larger than the back face because it's
 * closer to the camera.
 * 
 * The focal length f controls the field of view - larger f zooms in (telephoto
 * lens), smaller f zooms out (wide-angle lens). At f=600 pixels with an 800px
 * wide screen, we get approximately a 53° horizontal field of view, which is
 * moderate (not wide-angle, not telephoto).
 * 
 * We add 8 to z before dividing to move the cube away from the camera (which
 * sits at z=0). We then add 400 and 300 to x and y to translate the origin
 * from the top-left corner to the center of our 800Ã600 screen. Without this
 * translation, the cube would render in the top-left corner since screen
 * coordinates typically have (0,0) there rather than at the center.
 * 
 * Note: For a geometrically perfect cube (parallel edges, equal face sizes),
 * you would need orthographic projection where x_screen = x and y_screen = y
 * without any division by z. Orthographic projection is used in CAD and
 * technical drawings, but it lacks depth perception since all objects appear
 * the same size regardless of distance.
 */
Vec3 project(Vec3 v, float focal_length) {
    float scale = focal_length / (v.z + 8.0f);
    return (Vec3){v.x * scale + 400, v.y * scale + 300, v.z};
}

/*
 * The edge function computes the signed area of the triangle formed by points
 * a, b, and p using the 2D cross product (the z-component of a 3D cross product
 * where z=0). Mathematically:
 * 
 * edge(a,b,p) = (p-a) Ã (b-a) = (px-ax)(by-ay) - (py-ay)(bx-ax)
 * 
 * This is the z-component of the cross product and equals twice the signed area
 * of triangle (a,b,p). The sign indicates which side of line segment ab the
 * point p lies on:
 * - Positive: p is to the left of ab (counter-clockwise)
 * - Zero: p is exactly on line ab
 * - Negative: p is to the right of ab (clockwise)
 * 
 * For a triangle with vertices v0,v1,v2 in counter-clockwise order, a point p
 * is inside the triangle if and only if:
 * edge(v0,v1,p) ⥠0 AND edge(v1,v2,p) ⥠0 AND edge(v2,v0,p) ⥠0
 * 
 * This works because being on the "left" side of all three edges means being
 * inside the triangle. The three edge function values also give us barycentric
 * coordinates (w0,w1,w2) which can interpolate vertex attributes across the
 * triangle's surface, though we don't use that feature here.
 */
float edge_func(Vec3 a, Vec3 b, Vec3 p) {
    return (p.x - a.x)*(b.y - a.y) - (p.y - a.y)*(b.x - a.x);
}

/*
 * Rasterization converts a triangle from three vertex coordinates into a set
 * of pixels. The algorithm tests each pixel in a bounding rectangle to determine
 * if it lies inside the triangle.
 * 
 * The bounding box is computed by taking the minimum and maximum x and y
 * coordinates among the three vertices, giving us a tight rectangular region
 * that fully contains the triangle. This is an optimization - instead of testing
 * all 480,000 pixels in an 800Ã600 image, we only test pixels that could
 * possibly be inside.
 * 
 * We clamp the bounding box to screen dimensions [0,width)Ã[0,height) to avoid
 * accessing memory outside the framebuffer array, which would cause crashes or
 * undefined behavior.
 * 
 * For each pixel (x,y) in the bounding box, we test the point (x+0.5, y+0.5)
 * rather than (x,y) because pixel centers are at half-integer coordinates. A
 * pixel at array index (x,y) covers the square [x,x+1)Ã[y,y+1), so its center
 * is at (x+0.5, y+0.5). Testing at the center gives more accurate coverage.
 * 
 * We compute three edge function values w0, w1, w2 for the three edges of the
 * triangle. By the mathematical property of the edge function, if all three
 * values are non-negative (â¥0), the point lies inside the triangle (including
 * on the edges). We then write the triangle's color to the framebuffer at
 * index [y*width + x], which maps 2D coordinates to the 1D array.
 */
void rasterize(Triangle t, unsigned int* pixels, int width, int height) {
    int min_x = fminf(fminf(t.v[0].x, t.v[1].x), t.v[2].x);
    int max_x = fmaxf(fmaxf(t.v[0].x, t.v[1].x), t.v[2].x);
    int min_y = fminf(fminf(t.v[0].y, t.v[1].y), t.v[2].y);
    int max_y = fmaxf(fmaxf(t.v[0].y, t.v[1].y), t.v[2].y);
    
    if (min_x < 0) min_x = 0;
    if (max_x >= width) max_x = width - 1;
    if (min_y < 0) min_y = 0;
    if (max_y >= height) max_y = height - 1;
    
    for (int y = min_y; y <= max_y; y++) {
        for (int x = min_x; x <= max_x; x++) {
            Vec3 p = {x + 0.5f, y + 0.5f, 0};
            float w0 = edge_func(t.v[1], t.v[2], p);
            float w1 = edge_func(t.v[2], t.v[0], p);
            float w2 = edge_func(t.v[0], t.v[1], p);
            
            if (w0 >= 0 && w1 >= 0 && w2 >= 0) {
                pixels[y * width + x] = t.color;
            }
        }
    }
}

/*
 * Backface culling determines whether a triangle faces toward or away from the
 * camera using the mathematical relationship between the surface normal and
 * view direction.
 * 
 * We compute the surface normal n as the cross product of two edges: n = e1Ãe2
 * where e1 = v1-v0 and e2 = v2-v0. For counter-clockwise vertex ordering, this
 * normal points outward from the front face of the triangle due to the right-
 * hand rule property of cross products.
 * 
 * The view direction vector v points from the surface toward the camera. In our
 * simplified camera model where the camera looks down the -Z axis, v = (0,0,-1).
 * 
 * The dot product n·v tells us the angle between the normal and view direction:
 * - If n·v > 0: angle < 90°, normal points toward camera (front face)
 * - If n·v = 0: angle = 90°, viewing edge-on
 * - If n·v < 0: angle > 90°, normal points away from camera (back face)
 * 
 * Mathematically, n·v < 0 ⟺ cos(θ) < 0 ⟺ θ > 90°, which means the surface
 * is facing away. We return true (should cull) in this case. This optimization
 * skips rendering roughly half the triangles in a closed mesh, since we can
 * never see the back faces of an opaque object.
 */
int should_cull(Vec3 v0, Vec3 v1, Vec3 v2) {
    Vec3 edge1 = sub(v1, v0);
    Vec3 edge2 = sub(v2, v0);
    Vec3 normal = cross(edge1, edge2);
    Vec3 view_dir = {0, 0, -1};
    return dot(normal, view_dir) < 0;
}

/*
 * Save the framebuffer as a PPM image file. PPM is an uncompressed format
 * that's trivial to write: a simple header followed by raw RGB bytes. Most
 * image viewers can open PPM files directly.
 */
void save_ppm(const char* filename, unsigned int* pixels, int w, int h) {
    FILE* f = fopen(filename, "wb");
    fprintf(f, "P6\n%d %d\n255\n", w, h);
    for (int i = 0; i < w * h; i++) {
        unsigned char rgb[3] = {
            (pixels[i] >> 16) & 0xFF,
            (pixels[i] >> 8) & 0xFF,
            pixels[i] & 0xFF
        };
        fwrite(rgb, 1, 3, f);
    }
    fclose(f);
}

int main(void) {
    const int W = 800, H = 600;
    unsigned int* pixels = calloc(W * H, sizeof(unsigned int));
    
    /* 
     * A cube has 8 vertices at the corners. These define the geometry in
     * object space, centered at the origin with sides of length 2.
     */
    Vec3 cube_verts[8] = {
        {-1,-1,-1}, {1,-1,-1}, {1,1,-1}, {-1,1,-1},
        {-1,-1,1}, {1,-1,1}, {1,1,1}, {-1,1,1}
    };
    
    /*
     * The 12 triangles that make up the 6 faces of the cube. Each face uses
     * two triangles, and each triangle references three vertices by index.
     * The order matters for backface culling: vertices are listed counter-
     * clockwise when viewing the front face.
     */
    int cube_faces[12][3] = {
        {0,1,2}, {0,2,3}, {4,7,6}, {4,6,5}, {0,4,5}, {0,5,1},
        {1,5,6}, {1,6,2}, {2,6,7}, {2,7,3}, {3,7,4}, {3,4,0}
    };
    
    /* Face colors alternate between purple, cyan, and yellow */
    unsigned int colors[6] = {
        0xFF00FF, 0xFF00FF, 0x00FFFF, 0x00FFFF, 0xFFFF00, 0xFFFF00
    };
    
    /* Render 60 frames of animation */
    for (int frame = 0; frame < 60; frame++) {
        memset(pixels, 0, W * H * sizeof(unsigned int));
        
        float angle = frame * 0.05f;
        
        /* Transform and project all vertices once */
        Vec3 transformed[8];
        for (int i = 0; i < 8; i++) {
            Vec3 v = cube_verts[i];
            v = rotate_y(v, angle);
            v = rotate_x(v, angle * 0.7f);
            transformed[i] = project(v, 600);
        }
        
        /* Render each triangle if it's front-facing */
        for (int i = 0; i < 12; i++) {
            int i0 = cube_faces[i][0];
            int i1 = cube_faces[i][1];
            int i2 = cube_faces[i][2];
            
            Vec3 v0 = transformed[i0];
            Vec3 v1 = transformed[i1];
            Vec3 v2 = transformed[i2];
            
            /* Skip triangles facing away from camera */
            if (should_cull(v0, v1, v2)) continue;
            
            Triangle tri = {{v0, v1, v2}, colors[i/2]};
            rasterize(tri, pixels, W, H);
        }
        
        char filename[32];
        sprintf(filename, "frame_%03d.ppm", frame);
        save_ppm(filename, pixels, W, H);
        printf("Rendered frame %d/60\n", frame + 1);
    }
    
    free(pixels);
    printf("Done! Convert to video: ffmpeg -i frame_%%03d.ppm output.mp4\n");
    return 0;
}