/// 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;
    float range;
};

//// 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 < min(nearest.distance, ray.range))
                nearest = trace;
        }
        return nearest;
    }
};

//// Camera
struct Camera
{
    mat4 camera_inverse;
    explicit Camera(
        mat4 const & view_,
        mat4 const & projection_,
        mat4 const & window_
    )
    :
        camera_inverse{inverse(window_ * projection_ * view_)}
    {}
    vec3 unproject(vec3 const & frag_coord) const
    {
            auto const world_coord = camera_inverse * vec4(frag_coord, 1.0F);
            return vec3(world_coord) / world_coord.w;
    }
    Ray ray(vec4 const & frag_coord, vec2 const & depth_range) const
    {
        auto const origin    = unproject(vec3(frag_coord.xy, depth_range[0]));
        auto const stop      = unproject(vec3(frag_coord.xy, depth_range[1]));
        auto const range     = length(stop - origin);
        auto const direction = (stop - origin) / range;
        return Ray{origin, direction, range};
    }
};

//// 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(uvec4 viewport, vec2 depth_range, Shader const & shader)
    {
        auto const begin = max(uvec2(0),    uvec2(viewport.xy));
        auto const end   = min(uvec2(size), uvec2(viewport.xy + viewport.zw));
        auto const z     = 0.5F * (depth_range[0] + depth_range[1]);
        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, z, 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())
        );
    }
};

/// Transformations

//// window
mat4 window(uvec4 viewport, vec2 depth_range)
{
    auto const size = vec3(uvec2(viewport.zw), depth_range[1]-depth_range[0]);
    auto const offs = vec3(uvec2(viewport.xy),                depth_range[0]);
    auto const s = 0.5F * size;
    auto const t = 0.5F * size + offs;
    auto const S = matrixCompMult(mat3(1.0F), outerProduct(vec3(1.0F), s));
    return mat4(mat4x3(S[0], S[1], S[2], t));
}

//// ortho
mat4 ortho(float left, float right, float bottom, float top, float near, float far)
{
    auto const size = vec3(right-left, top-bottom, far-near);
    auto const offs = vec3(     -left,    -bottom,    +near);
    auto const s = vec3(2.0F, 2.0F, -2.0F) / size;
    auto const t = s * offs - 1.0F;
    auto const S = matrixCompMult(mat3(1.0F), outerProduct(vec3(1.0F), s));
    return mat4(mat4x3(S[0], S[1], S[2], t));
}

//// frustum
mat4 frustum(float left, float right, float bottom, float top, float near, float far)
{
    auto const z = vec4(0.0F, 0.0F, near+far, -1.0F);
    auto const w = vec4(0.0F, 0.0F, near*far,  0.0F);
    auto const S = mat4(near);
    return ortho(left, right, bottom, top, near, far) * mat4(S[0], S[1], z, w);
}

//// perspective
mat4 perspective(float fovy, float aspect, float near, float far)
{
    auto const y = near * tan(0.5F * radians(fovy));
    auto const x = y * aspect;
    return frustum(-x, +x, -y, +y, near, far);
}

//// lookat
mat4 lookat(vec3 position, vec3 target, vec3 up)
{
    auto const z = normalize(position - target);
    auto const x = normalize(cross(up, z));
    auto const y = cross(z, x);
    auto const R_inv = transpose(mat3(x, y, z));
    auto const t_inv = -position;
    return mat4(mat4x3(R_inv[0], R_inv[1], R_inv[2], R_inv * t_inv));
}

/// 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 aspect      = (float)size.x / (float)size.y;
    auto viewport    = uvec4(uvec2(0), size);
    auto depth_range = vec2(0.0F, 1.0F);
    auto framebuffer = Framebuffer(size);
    auto camera = Camera(
        lookat( // view
            {0.0F, 0.0F,  0.0F}, // position
            {0.0F, 0.0F, -1.0F}, // target
            {0.0F, 1.0F,  0.0F}  // up
        ),
        perspective( // projection
            90.0F,  // fovy
            aspect, // aspect
            1.0F,   // near
            100.0F  // far
        ),
        window(viewport, depth_range)
    );
    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(viewport, depth_range, [&](vec4 const & frag_coord)
    {
        auto const ray   = camera.ray(frag_coord, depth_range);
        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;
}
