/// Includes

#include <array>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <limits>
#include <vector>

#define GLM_FORCE_SWIZZLE
#define GLM_FORCE_INTRINSICS // For `GLM_SWIZZLE_OPERATOR`.
#include <glm/glm.hpp>
// #include <glm/gtx/io.hpp> // For `operator<<(std::ostream &)`.

/// Namespaces

using namespace glm;

/// Constants

auto const infinity = std::numeric_limits<float>::infinity();

/// Types

//// Ray
struct Ray
{
    vec3 origin;
    vec3 direction;
};

//// Trace
struct Trace
{
    float distance;
    explicit operator bool() const
    {
        return distance < infinity;
    }
};

//// Sphere
struct Sphere
{
    vec3  center;
    float radius;
    Trace trace(Ray const & ray) const
    {
        auto const offs = center - ray.origin;
        auto const proj = dot(offs, ray.direction);
        if (proj < 0.0F)
            return {infinity}; // Past.
        auto const perp2 = dot(offs, offs) - proj * proj;
        auto const over2 = radius * radius - perp2;
        if (over2 < 0.0F)
            return {infinity}; // Miss.
        auto const dist = proj - sqrt(over2);
        if (dist < 0.0F)
            return {infinity}; // Inside.
        return {dist}; // Hit.
    }
};

//// Shapes
using Shapes = std::vector<Sphere>;

//// Scene
struct Scene
{
    Shapes shapes;
    Trace trace(Ray const & ray) const
    {
        auto nearest = Trace{infinity};
        for (auto const & shape : shapes)
        {
            auto const trace = shape.trace(ray);
            if (trace.distance < nearest.distance)
                nearest = trace;
        }
        return nearest;
    }
};

//// Camera
struct Camera
{
    uvec2 size;
    Ray ray(vec4 const & frag_coord) const
    {
        auto const point = vec3(
            (2.0F * vec2(frag_coord.xy) - vec2(size)) / float(size.y),
            -1.0F
        );
        auto const origin    = vec3(0.0F);
        auto const direction = normalize(point);
        return Ray{origin, direction};
    }
};

//// Framebuffer
struct Framebuffer
{
    uvec2               size;
    std::vector<u8vec4> color;
    explicit Framebuffer(uvec2 const & size_)
    :
        size{size_},
        color((std::size_t)(size_.x * size_.y))
    {}
    template<typename Shader>
    void render(Shader const & shader)
    {
        auto const begin = uvec2(0);
        auto const end   = size;
        for (auto y = begin.y; y < end.y; ++y)
        for (auto x = begin.x; x < end.x; ++x)
        {
            auto const index      = size.x * y + x;
            auto const frag_coord = vec4(vec2(x, y) + 0.5F, 0.0F, 1.0F);
            auto const frag_color = vec4(shader(frag_coord).bgra);
            color[index] = clamp(frag_color, 0.0F, 1.0F) * 255.0F + 0.5F;
        }
    }
    void write_tga(char const * path) const
    {
        auto header = std::array<uint8_t, 18>{
            0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            (uint8_t)(size.x >> 0U),
            (uint8_t)(size.x >> 8U),
            (uint8_t)(size.y >> 0U),
            (uint8_t)(size.y >> 8U),
            32, 0,
        };
        auto ostream = std::ofstream(path, std::ios::binary);
        write(ostream, header);
        write(ostream, color);
    }
    template<typename Collection>
    void write(std::ostream & ostream, Collection const & collection) const
    {
        ostream.write(
            (char const *)            collection.data(),
            (std::streamsize)(sizeof(*collection.data()) * collection.size())
        );
    }
};

/// Main

int main(int argc, char const * argv[])
{
    //// Arguments
    if (argc != 2)
    {
        std::cerr << "Usage: raytracer <path>" << std::endl;
        return EXIT_FAILURE;
    }
    auto const * path = argv[1]; // NOLINT
    //// Configure
    auto size        = uvec2(640, 480);
    auto framebuffer = Framebuffer(size);
    auto camera      = Camera{size};
    auto scene       = Scene{
        { // shapes
            Sphere{{   6.0F,  -4.0F,  -24.0F}, 12.0F},
            Sphere{{   2.0F,   6.0F,  -16.0F},  8.0F},
            Sphere{{  -4.0F,   0.0F,  -10.0F},  4.0F},
            Sphere{{   0.0F,  -4.0F,   -6.0F},  1.0F},
            Sphere{{  -1.1F,   0.8F,   -1.1F},  0.2F},
            Sphere{{-110.0F, -80.0F, -110.0F}, 20.0F},
        },
    };
    //// Render
    framebuffer.render([&](vec4 const & frag_coord)
    {
        auto const ray   = camera.ray(frag_coord);
        auto const trace = scene.trace(ray);
        auto const rgb   = vec3(1.0F / (1.0F + trace.distance));
        auto const alpha = (bool)trace;
        return vec4(rgb, alpha);
    });
    //// Finalize
    framebuffer.write_tga(path);
    return EXIT_SUCCESS;
}