/// Includes

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

#define GLM_FORCE_SWIZZLE
#include <glm/glm.hpp>
#include <glm/fwd.hpp>
#include <glm/ext/matrix_transform.hpp>  // https://glm.g-truc.net/0.9.9/api/a00247.html
#include <glm/ext/matrix_clip_space.hpp> // https://glm.g-truc.net/0.9.9/api/a00243.html
#include <glm/ext/matrix_projection.hpp> // https://glm.g-truc.net/0.9.9/api/a00245.html
#include <glm/gtc/color_space.hpp>       // https://glm.g-truc.net/0.9.9/api/a00289.html
// #include <glm/gtx/io.hpp>                // https://glm.g-truc.net/0.9.9/api/a00332.html


/// Namespaces

using namespace glm;


/// Constants

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


/// Types

//// Ray
struct Ray
{
    vec3 origin;
    vec3 direction;
    vec3 point(float distance) const
    {
        return origin + direction * distance;
    }
    Ray resumed(float distance)
    {
        return {point(distance), direction};
    }
};

//// Material
struct Material
{
    vec4 color;
};

//// Trace
struct Trace
{
    float    distance;
    Material material;
    vec3     miss;
};

//// Circle
struct Circle
{
    vec3     center;
    float    radius;
    Material material;
    Trace trace(Ray const & ray) const
    {
        auto const offs = center - ray.origin;
        auto const proj = dot(offs, ray.direction);
        if (proj < 0.0F) // Past.
            return {infinity, {}, {}};
        auto const perp2 = dot(offs, offs) - proj * proj;
        auto const pene2 = radius * radius - perp2;
        if (pene2 < 0.0F) // Miss.
        {
            auto const poin = ray.point(proj);
            auto const rati = 1.0F - radius * inversesqrt(perp2);
            auto const miss = rati * (center - poin);
            return {proj, material, miss};
        }
        auto const dist = proj - sqrt(pene2);
        if (dist < 0.0F) // Inside.
            return {infinity, {}, {}};
        return {dist, material, vec3(0.0F)};
    }
};

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

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

//// Camera
struct Camera
{
    vec3  position;
    vec3  target;
    float fovy;
    float near;
    float far;
    Ray ray(uvec2 const & size, uvec2 const & pixel) const
    {
        auto const aspect    = (float)size.x / (float)size.y;
        auto const viewport  = uvec4(0, 0, size);
        auto const window    = vec3(vec2(pixel) + 0.5F, 0.0F);
        auto const up        = vec3(0.0F, 1.0F, 0.0F);
        auto const view      = glm::lookAt(position, target, up);
        auto const proj      = glm::perspective(fovy, aspect, near, far);
        auto const world     = glm::unProject(window, view, proj, viewport);
        auto const direction = normalize(world.xyz() - position);
        return Ray{position, direction};
    }
    vec2 project(uvec2 const & size, vec3 const & point) const
    {
        auto const aspect   = (float)size.x / (float)size.y;
        auto const viewport = uvec4(0, 0, size);
        auto const up       = vec3(0.0F, 1.0F, 0.0F);
        auto const view     = glm::lookAt(position, target, up);
        auto const proj     = glm::perspective(fovy, aspect, near, far);
        auto const window   = glm::project(point, view, proj, viewport);
        return window.xy();
    }
};

//// ACES
struct ACES
{
    vec3 whitepoint;
    vec3 transform(vec3 color) const
    {
        color /= whitepoint;
        return
            (color * (2.51F * color + 0.03F)) /
            (color * (2.43F * color + 0.59F) + 0.14F);
    }
};

//// TGA
struct TGA
{
    uvec2       size;
    std::string path;
    template<typename ColorFunc>
    void render(ColorFunc const & color_func) const
    {
        auto frame_size = size.x * size.y;
        auto frame      = std::vector<u8vec4>(frame_size);
        auto header     = std::array<u8, 18>{
            0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            (u8)(size.x >> 0U),
            (u8)(size.x >> 8U),
            (u8)(size.y >> 0U),
            (u8)(size.y >> 8U),
            32, 0,
        };
        for (auto y = 0U; y < size.y; ++y)
        {
            for (auto x = 0U; x < size.x; ++x)
            {
                auto const color = color_func(size, uvec2(x, y));
                auto const index = y * size.x + x;
                frame[index] = clamp(color.bgra(), 0.0F, 1.0F) * 255.0F + 0.5F;
            }
        }
        auto ostream = std::ofstream(path, std::ios::binary);
        write(ostream, header);
        write(ostream, frame);
    }
    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: raytrace <path>" << std::endl;
        return 0;
    }
    auto const * path = argv[1]; // NOLINT
    //// Configure
    auto const camera = Camera{
        vec3(0.0F, 0.0F,  0.0F), // position
        vec3(0.0F, 0.0F, -1.0F), // target
        radians(45.0F),          // fovy
        0.01F,                   // near
        100.0F,                  // far
    };
    auto const scene = Scene{
        // background
        {vec4(0.2F, 0.2F, 0.5F, 0.5F)},
        // shapes
        {
            {vec3( 2.0F, -1.0F, -10.0F), 3.0F, {vec4(1.0F, 0.0F, 0.0F, 1.0F)}},
            {vec3( 1.0F,  2.0F,  -7.0F), 2.0F, {vec4(0.0F, 1.0F, 0.0F, 1.0F)}},
            {vec3(-1.0F,  0.0F,  -4.0F), 1.0F, {vec4(0.0F, 0.0F, 1.0F, 1.0F)}},
            {vec3( 0.0F, -1.0F,  -3.0F), 0.2F, {vec4(0.0F, 1.0F, 1.0F, 0.5F)}},
        },
    };
    auto const tonemap = ACES{vec3(1.0F)};
    auto const output  = TGA{uvec2(640, 480), path};
    //// Render
    output.render([&](uvec2 const & size, uvec2 const & pixel) {
        auto rgb   = vec3(0.0F); // Pre-multiplied alpha (opacity).
        auto trans = 1.0F;       // 1 - alpha (opacity).
        auto ray   = camera.ray(size, pixel);
        while (true)
        {
            auto trace = scene.trace(ray);
            auto color = trace.material.color;
            if (trace.miss != vec3(0.0F))
            {
                auto const point = ray.point(trace.distance);
                auto const dist  = distance(
                    camera.project(size, point),
                    camera.project(size, point + trace.miss)
                );
                color.a *= 1.0F - clamp(dist, 0.0F, 1.0F);
            }
            rgb   += color.rgb() * color.a * trans;
            trans *= 1.0F - color.a;
            if (trace.distance == infinity || trans == 0.0F)
                break;
            ray = ray.resumed(trace.distance + delta);
        }
        auto const alpha = 1.0F - trans;
        auto const srgb  = convertLinearToSRGB(tonemap.transform(rgb / alpha));
        return vec4(srgb, alpha);
    });
}