foreign fn ppm_start(width: i32, height: i32);
foreign fn ppm_pixel(red: i32, green: i32, blue: i32);
foreign fn ppm_finish();

foreign fn sqrtf(num: f32) -> f32;
foreign fn tanf(num: f32) -> f32;
foreign fn randf() -> f32;

struct Vec3 {
    x: f32,
    y: f32,
    z: f32
}

struct Ray {
    origin: Vec3,
    dir: Vec3
}

// Added material type (0=Diffuse/Normal, 1=Glass, 2=Metal) and Refractive Index
struct Sphere {
    center: Vec3,
    radius: f32,
    color: Vec3,
    mat_type: i32,
    ref_idx: f32,
    fuzz: f32
}

struct Hit {
    hit: bool,
    t: f32,
    normal: Vec3,
    color: Vec3,
    mat_type: i32,
    ref_idx: f32,
    fuzz: f32
}

// Wrapper for refraction results
struct RefractResult {
    success: bool,
    dir: Vec3
}

fn vec3_add(a: Vec3, b: Vec3) -> Vec3 {
    return Vec3 { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
}

fn vec3_sub(a: Vec3, b: Vec3) -> Vec3 {
    return Vec3 { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
}

fn vec3_mul(a: Vec3, t: f32) -> Vec3 {
    return Vec3 { x: a.x * t, y: a.y * t, z: a.z * t };
}

fn vec3_dot(a: Vec3, b: Vec3) -> f32 {
    return a.x * b.x + a.y * b.y + a.z * b.z;
}

fn vec3_normalize(v: Vec3) -> Vec3 {
    let len = sqrtf(vec3_dot(v, v));
    if len == 0.0 {
        return Vec3 { x: 0.0, y: 0.0, z: 0.0 };
    }
    return vec3_mul(v, 1.0 / len);
}

// V - 2 * dot(V, N) * N
fn vec3_reflect(v: Vec3, n: Vec3) -> Vec3 {
    return vec3_sub(v, vec3_mul(n, 2.0 * vec3_dot(v, n)));
}

// Snell's Law for calculating the refracted ray direction
fn vec3_refract(v: Vec3, n: Vec3, ni_over_nt: f32) -> RefractResult {
    let uv = vec3_normalize(v);
    let dt = vec3_dot(uv, n);
    let discriminant = 1.0 - ni_over_nt * ni_over_nt * (1.0 - dt * dt);

    if discriminant > 0.0 {
        let temp1 = vec3_mul(vec3_sub(uv, vec3_mul(n, dt)), ni_over_nt);
        let temp2 = vec3_mul(n, sqrtf(discriminant));
        return RefractResult {
            success: true,
            dir: vec3_sub(temp1, temp2)
        };
    }

    // Total internal reflection occurred
    return RefractResult {
        success: false,
        dir: Vec3 { x: 0.0, y: 0.0, z: 0.0 }
    };
}

// The Cross Product generates a perpendicular vector to a and b
fn vec3_cross(a: Vec3, b: Vec3) -> Vec3 {
    return Vec3 {
        x: a.y * b.z - a.z * b.y,
        y: a.z * b.x - a.x * b.z,
        z: a.x * b.y - a.y * b.x
    };
}

// Generates a random point inside a 2D unit disk (used for depth of field blur)
fn random_in_unit_disk() -> Vec3 {
    let p = Vec3 { x: 1.0, y: 1.0, z: 0.0 };
    let found = false;
    while !found {
        p = Vec3 {
            x: randf() * 2.0 - 1.0,
            y: randf() * 2.0 - 1.0,
            z: 0.0
        };
        // Check if the point is inside the radius of 1.0
        if vec3_dot(p, p) < 1.0 {
            found = true;
        }
    }
    return p;
}

fn intersect_sphere(ray: *Ray, sphere: *Sphere, t_min: f32, t_max: f32) -> Hit {
    let oc = vec3_sub((*sphere).center, (*ray).origin);
    let a = vec3_dot((*ray).dir, (*ray).dir);
    let h = vec3_dot((*ray).dir, oc);
    let c = vec3_dot(oc, oc) - (*sphere).radius * (*sphere).radius;
    let discriminant = h * h - a * c;

    let miss = Hit {
        hit: false, t: 0.0, normal: Vec3 { x: 0.0, y: 0.0, z: 0.0 },
        color: Vec3 { x: 0.0, y: 0.0, z: 0.0 }, mat_type: 0, ref_idx: 0.0,
        fuzz: 0.0 // Ensure this is present
    };

    if discriminant < 0.0 { return miss; }

    let sqrtd = sqrtf(discriminant);
    let root = (h - sqrtd) / a;

    if root <= t_min {
        root = (h + sqrtd) / a;
        if root <= t_min { return miss; }
    }
    if root >= t_max { return miss; }

    let hit_point = vec3_add((*ray).origin, vec3_mul((*ray).dir, root));
    let normal = vec3_mul(vec3_sub(hit_point, (*sphere).center), 1.0 / (*sphere).radius);

    return Hit {
        hit: true, t: root, normal: normal,
        color: (*sphere).color, mat_type: (*sphere).mat_type,
        ref_idx: (*sphere).ref_idx,
        fuzz: (*sphere).fuzz // Copy fuzz from the sphere
    };
}

fn schlick(cosine: f32, ref_idx: f32) -> f32 {
    let r0 = (1.0 - ref_idx) / (1.0 + ref_idx);
    let r0_sq = r0 * r0;
    let comp = 1.0 - cosine;
    // Approximating (1 - cosine)^5
    return r0_sq + (1.0 - r0_sq) * comp * comp * comp * comp * comp;
}

fn random_unit_vector() -> Vec3 {
    // Generate a random vector in a cube
    let v = Vec3 {
        x: randf() * 2.0 - 1.0,
        y: randf() * 2.0 - 1.0,
        z: randf() * 2.0 - 1.0
    };
    // Normalize it to project it onto the unit sphere
    return vec3_normalize(v);
}

fn ray_color(ray: *Ray, spheres: *[Sphere; 500], sphere_count: i32, depth: i32) -> Vec3 {
    if depth <= 0 {
        return Vec3 { x: 0.0, y: 0.0, z: 0.0 };
    }

    let closest_so_far = 100000.0; // Increased to ensure far spheres render
    let hit_anything = false;
    let final_hit = Hit {
        hit: false, t: 0.0, normal: Vec3 { x: 0.0, y: 0.0, z: 0.0 },
        color: Vec3 { x: 0.0, y: 0.0, z: 0.0 }, mat_type: 0, ref_idx: 0.0, fuzz: 0.0
    };

    // Iterate up to the active sphere_count, not a hardcoded 4
    let i = 0;
    while i < sphere_count {
        let hit = intersect_sphere(ray, &(*spheres)[i], 0.001, closest_so_far);
        if hit.hit {
            hit_anything = true;
            closest_so_far = hit.t;
            final_hit = hit;
        }
        i = i + 1;
    }

    if hit_anything {
        let hit_point = vec3_add((*ray).origin, vec3_mul((*ray).dir, final_hit.t));
        let unit_dir = vec3_normalize((*ray).dir);

        if final_hit.mat_type == 1 { // Glass
            let outward_normal: Vec3;
            let ni_over_nt: f32;
            let dir_dot_normal = vec3_dot(unit_dir, final_hit.normal);

            if dir_dot_normal > 0.0 {
                outward_normal = vec3_mul(final_hit.normal, -1.0);
                ni_over_nt = final_hit.ref_idx;
            } else {
                outward_normal = final_hit.normal;
                ni_over_nt = 1.0 / final_hit.ref_idx;
            }

            let cos_theta = vec3_dot(vec3_mul(unit_dir, -1.0), outward_normal);
            let refract_result = vec3_refract((*ray).dir, outward_normal, ni_over_nt);

            let reflect_prob = 1.0;
            if refract_result.success {
                reflect_prob = schlick(cos_theta, final_hit.ref_idx);
            }

            let scattered_ray: Ray;
            if randf() < reflect_prob {
                // Use normalized unit_dir to prevent self-intersection
                let reflected = vec3_reflect(unit_dir, final_hit.normal);
                scattered_ray = Ray { origin: hit_point, dir: reflected };
            } else {
                scattered_ray = Ray { origin: hit_point, dir: refract_result.dir };
            }

            let bounce_color = ray_color(&scattered_ray, spheres, sphere_count, depth - 1);
            return Vec3 {
                x: final_hit.color.x * bounce_color.x,
                y: final_hit.color.y * bounce_color.y,
                z: final_hit.color.z * bounce_color.z
            };
        }

        if final_hit.mat_type == 2 { // Metal
            let reflected = vec3_reflect(unit_dir, final_hit.normal);

            // Apply fuzz by randomly perturbing the reflected vector
            let fuzzed_dir = vec3_add(reflected, vec3_mul(random_unit_vector(), final_hit.fuzz));
            let scattered_ray = Ray { origin: hit_point, dir: vec3_normalize(fuzzed_dir) };

            // Only scatter if the fuzzed ray doesn't bounce inward
            if vec3_dot(scattered_ray.dir, final_hit.normal) > 0.0 {
                let bounce_color = ray_color(&scattered_ray, spheres, sphere_count, depth - 1);
                return Vec3 {
                    x: final_hit.color.x * bounce_color.x,
                    y: final_hit.color.y * bounce_color.y,
                    z: final_hit.color.z * bounce_color.z
                };
            }
            return Vec3 { x: 0.0, y: 0.0, z: 0.0 };
        }

        // True Lambertian Diffuse
        let scatter_dir = vec3_add(final_hit.normal, random_unit_vector());

        // Catch degenerate scatter direction (if random vector exactly opposes normal)
        if vec3_dot(scatter_dir, scatter_dir) < 0.001 {
            scatter_dir = final_hit.normal;
        }

        // Ensure the outbound ray is perfectly normalized
        let scattered_ray = Ray { origin: hit_point, dir: vec3_normalize(scatter_dir) };
        let bounce_color = ray_color(&scattered_ray, spheres, sphere_count, depth - 1);

        return Vec3 {
            x: final_hit.color.x * bounce_color.x,
            y: final_hit.color.y * bounce_color.y,
            z: final_hit.color.z * bounce_color.z
        };
    }

    // Skybox gradient
    let unit_dir = vec3_normalize((*ray).dir);
    let t = 0.5 * (unit_dir.y + 1.0);
    return vec3_add(
        vec3_mul(Vec3 { x: 1.0, y: 1.0, z: 1.0 }, 1.0 - t),
        vec3_mul(Vec3 { x: 0.5, y: 0.7, z: 1.0 }, t)
    );
}

fn main() -> i32 {
    let aspect_ratio = 16.0 / 9.0;
    let image_width = 1920;
    let image_height = ((image_width as f32) / aspect_ratio) as i32;

    let samples_per_pixel = 10; // Increase this for higher quality
    let max_depth = 32;

    // Allocate our static maximum array and track how many we've placed
    let spheres: [Sphere; 500];
    let sphere_count = 0;

    // Ground sphere
    spheres[sphere_count] = Sphere {
        center: Vec3 { x: 0.0, y: -1000.0, z: 0.0 }, radius: 1000.0,
        color: Vec3 { x: 0.5, y: 0.5, z: 0.5 }, mat_type: 0, ref_idx: 0.0, fuzz: 0.0
    };
    sphere_count = sphere_count + 1;

    // Generate random tiny spheres
    let a = -11;
    while a < 11 {
        let b = -11;
        while b < 11 {
            let choose_mat = randf();
            let center = Vec3 {
                x: (a as f32) + 0.9 * randf(),
                y: 0.2,
                z: (b as f32) + 0.9 * randf()
            };

            // Avoid spawning inside the giant main spheres
            let center_offset = vec3_sub(center, Vec3 { x: 4.0, y: 0.2, z: 0.0 });
            let dist_sq = vec3_dot(center_offset, center_offset);

            if dist_sq > (0.9 * 0.9) {
                if choose_mat < 0.8 {
                    // Diffuse (albedo = color::random() * color::random())
                    spheres[sphere_count] = Sphere {
                        center: center, radius: 0.2,
                        color: Vec3 { x: randf() * randf(), y: randf() * randf(), z: randf() * randf() },
                        mat_type: 0, ref_idx: 0.0, fuzz: 0.0
                    };
                } else if choose_mat < 0.95 {
                    // Metal (albedo = random(0.5, 1), fuzz = random(0, 0.5))
                    spheres[sphere_count] = Sphere {
                        center: center, radius: 0.2,
                        color: Vec3 { x: 0.5 + 0.5 * randf(), y: 0.5 + 0.5 * randf(), z: 0.5 + 0.5 * randf() },
                        mat_type: 2, ref_idx: 0.0, fuzz: randf() * 0.5
                    };
                } else {
                    // Glass
                    spheres[sphere_count] = Sphere {
                        center: center, radius: 0.2,
                        color: Vec3 { x: 1.0, y: 1.0, z: 1.0 }, // Clear
                        mat_type: 1, ref_idx: 1.5, fuzz: 0.0
                    };
                }
                sphere_count = sphere_count + 1;
            }
            b = b + 1;
        }
        a = a + 1;
    }

    // Material 1: Giant Central Glass Sphere
    spheres[sphere_count] = Sphere {
        center: Vec3 { x: 0.0, y: 1.0, z: 0.0 }, radius: 1.0,
        color: Vec3 { x: 1.0, y: 1.0, z: 1.0 }, mat_type: 1, ref_idx: 1.5, fuzz: 0.0
    };
    sphere_count = sphere_count + 1;

    // Material 2: Giant Left Diffuse Sphere
    spheres[sphere_count] = Sphere {
        center: Vec3 { x: -4.0, y: 1.0, z: 0.0 }, radius: 1.0,
        color: Vec3 { x: 0.4, y: 0.2, z: 0.1 }, mat_type: 0, ref_idx: 0.0, fuzz: 0.0
    };
    sphere_count = sphere_count + 1;

    // Material 3: Giant Right Metal Sphere
    spheres[sphere_count] = Sphere {
        center: Vec3 { x: 4.0, y: 1.0, z: 0.0 }, radius: 1.0,
        color: Vec3 { x: 0.7, y: 0.6, z: 0.5 }, mat_type: 2, ref_idx: 0.0, fuzz: 0.0
    };
    sphere_count = sphere_count + 1;

    ppm_start(image_width, image_height);

    // --- Camera Settings ---
    let vfov = 20.0;
    let lookfrom = Vec3 { x: 13.0, y: 2.0, z: 3.0 };
    let lookat = Vec3 { x: 0.0, y: 0.0, z: 0.0 };
    let vup = Vec3 { x: 0.0, y: 1.0, z: 0.0 };
    let defocus_angle = 0.6;
    let focus_dist = 10.0;

    let pi = 3.14159265;
    let theta = vfov * pi / 180.0;
    let h = tanf(theta / 2.0);

    let viewport_height = 2.0 * h * focus_dist;
    let viewport_width = viewport_height * ((image_width as f32) / (image_height as f32));

    // Calculate the u,v,w unit basis vectors for the camera coordinate frame.
    let w = vec3_normalize(vec3_sub(lookfrom, lookat));
    let u = vec3_normalize(vec3_cross(vup, w));
    let v = vec3_cross(w, u);

    // Calculate the vectors across the horizontal and down the vertical viewport edges.
    let viewport_u = vec3_mul(u, viewport_width);
    let viewport_v = vec3_mul(v, -viewport_height); // Negative to point down the image

    // Calculate the horizontal and vertical delta vectors from pixel to pixel.
    let pixel_delta_u = vec3_mul(viewport_u, 1.0 / (image_width as f32));
    let pixel_delta_v = vec3_mul(viewport_v, 1.0 / (image_height as f32));

    // Calculate the location of the upper left pixel.
    let half_u = vec3_mul(viewport_u, 0.5);
    let half_v = vec3_mul(viewport_v, 0.5);
    let focus_w = vec3_mul(w, focus_dist);
    let viewport_upper_left = vec3_sub(vec3_sub(vec3_sub(lookfrom, focus_w), half_u), half_v);

    let pixel00_loc = vec3_add(viewport_upper_left, vec3_mul(vec3_add(pixel_delta_u, pixel_delta_v), 0.5));

    // Calculate the camera defocus disk basis vectors.
    let defocus_radius = focus_dist * tanf((defocus_angle / 2.0) * pi / 180.0);
    let defocus_disk_u = vec3_mul(u, defocus_radius);
    let defocus_disk_v = vec3_mul(v, defocus_radius);

    // --- Render ---
    let j = 0;
    while j < image_height {
        let i = 0;
        while i < image_width {
            let pixel_color = Vec3 { x: 0.0, y: 0.0, z: 0.0 };

            let s = 0;
            while s < samples_per_pixel {
                let px = -0.5 + randf();
                let py = -0.5 + randf();

                let pixel_center = vec3_add(pixel00_loc, vec3_add(vec3_mul(pixel_delta_u, i as f32), vec3_mul(pixel_delta_v, j as f32)));
                let sample_offset = vec3_add(vec3_mul(pixel_delta_u, px), vec3_mul(pixel_delta_v, py));
                let sample_loc = vec3_add(pixel_center, sample_offset);

                // Apply Depth of Field logic
                let ray_origin = lookfrom;
                if defocus_angle > 0.0 {
                    let p = random_in_unit_disk();
                    let defocus_offset = vec3_add(vec3_mul(defocus_disk_u, p.x), vec3_mul(defocus_disk_v, p.y));
                    ray_origin = vec3_add(lookfrom, defocus_offset);
                }

                let ray_dir = vec3_sub(sample_loc, ray_origin);
                let ray = Ray { origin: ray_origin, dir: ray_dir };

                let sample_color = ray_color(&ray, &spheres, sphere_count, max_depth);
                pixel_color = vec3_add(pixel_color, sample_color);

                s = s + 1;
            }

            let scale = 1.0 / (samples_per_pixel as f32);
            pixel_color = vec3_mul(pixel_color, scale);

            pixel_color.x = sqrtf(pixel_color.x);
            pixel_color.y = sqrtf(pixel_color.y);
            pixel_color.z = sqrtf(pixel_color.z);

            let ir = (255.999 * pixel_color.x) as i32;
            let ig = (255.999 * pixel_color.y) as i32;
            let ib = (255.999 * pixel_color.z) as i32;

            ppm_pixel(ir, ig, ib);

            i = i + 1;
        }
        j = j + 1;
    }

    ppm_finish();

    return 0;
}
