// *********************************************************
// Ray Tracer Class
// Author : Tamy Boubekeur (boubek@gmail.com).
// Copyright (C) 2012 Tamy Boubekeur.
// All rights reserved.
// *********************************************************

#include "RayTracer.h"
#include "Ray.h"
#include "Scene.h"
#include "Color.h"
#include "Light.h"
#include "Material.h"
#include "Object.h"
#include "Random.h"
#include <QProgressDialog>
#include <QMap>
#include <cmath>

static RayTracer * instance = NULL;

RayTracer * RayTracer::getInstance () {
    if (instance == NULL)
        instance = new RayTracer ();
    return instance;
}

void RayTracer::destroyInstance () {
    if (instance != NULL) {
        delete instance;
        instance = NULL;
    }
}

inline int clamp (float f, int inf, int sup) {
    int v = static_cast<int> (f);
    return (v < inf ? inf : (v > sup ? sup : v));
}

// POINT D'ENTREE DU PROJET.
void RayTracer::render(QImage& image,
                       const SRenderParameters &rp,
                       QWidget *parent)
{
    // Progress dialog
    QProgressDialog progressDialog("Raytracing...", "Cancel", 0, 100, parent);
    progressDialog.show();

    // buffer + scene
    Scene* scene = Scene::getInstance();
    Color* img = (Color*)image.bits();

    // near plane (plane at distance 1.0 following camDir)
    float nearSizeY = 2 * tan(rp.cam.fov / 2);
    float nearSizeX = nearSizeY * rp.cam.aspect;
    Vec3Df nearOrigin = rp.cam.dir + rp.cam.up * nearSizeY / 2.0f - rp.cam.right * nearSizeX / 2.0f;

    // Anti-aliasing (stochastic)
    Vec3Df aaX = rp.cam.right * (nearSizeX / rp.screen.width);
    Vec3Df aaY = -rp.cam.up * (nearSizeY / rp.screen.height);
    float invAa = 1.0f / (rp.screen.aaX * rp.screen.aaY);
    float aaSizeX = 1.0f / rp.screen.aaX, aaSizeY = 1.0f / rp.screen.aaY;

    // Depth of Field parameters
    float aperture = rp.dof.aperture;
    float focusDistance = rp.dof.focusDistance;
    float sensorWidth = 0.036f;
    float sensorHeight = sensorWidth * rp.cam.aspect;//2.4f;
    float focalLength = 0.5f * std::sqrt(sensorWidth*sensorWidth + sensorHeight * sensorHeight) / tanf (/*float(PI/180) * */0.5f * rp.cam.fov);
    float lensWidth = focalLength / aperture;

    // Zones on the disk
    unsigned int dofQuality = rp.dof.quality;
    unsigned int nbDofLayers = std::floor(std::log((float)rp.dof.quality)/std::log(2.0f) + 1);
    unsigned int nbDofThetas = 2 << (1 + nbDofLayers);
    float dofLayerSize = lensWidth / nbDofLayers;
    float dofThetaSize = 2 * PI / nbDofThetas;
    unsigned int nbDoFRay = std::pow(2.0f,(int)dofQuality); // Number of ray per Quadrant (except the original one)
    float invDoF = 1.0f / (nbDoFRay*nbDofLayers*nbDofThetas);

    // Loop for each pixel
    for (unsigned int y = 0; y < rp.screen.height; y++)
    {
        float yRatio = y / float(rp.screen.height);
        progressDialog.setValue(100 * yRatio);
        printf("\rRaytracing... %i%%       ",int(100 * yRatio));
        fflush(stdout);

        #pragma omp parallel for
        for (int x = 0; x < (int)rp.screen.width; x++)
        {
            float xRatio = x / float(rp.screen.width);
            Ray ray;
            ray.setOrigin(rp.cam.pos);
            Vec3Df rayDir = nearOrigin + xRatio * nearSizeX * rp.cam.right - yRatio * nearSizeY * rp.cam.up;
            Colorf pixelColor;

            if(!rp.dof.enabled)
            {
                // Anti-aliasing
                if(rp.screen.aaEnabled)
                {
                    for(unsigned int xAa = 0; xAa < rp.screen.aaX; xAa++)
                        for(unsigned int yAa = 0; yAa < rp.screen.aaY; yAa++)
                        {
                            // Build ray
                            if(rp.screen.aaX == 1 && rp.screen.aaY == 1)
                                ray.setDirection(rayDir);
                            else
                                ray.setDirection(rayDir
                                             + rnd::random(xAa * aaSizeX, (xAa + 1) * aaSizeX) * aaX
                                             + rnd::random(yAa * aaSizeY, (yAa + 1) * aaSizeY) * aaY);

                            // Compute pixel color (background if no intersection)
                            pixelColor.addNoClamp(computeRayColor(ray, scene, 0));
                        }

                    pixelColor *= invAa;
                }
                else
                {
                    // No aa
                    ray.setDirection(rayDir);
                    pixelColor = computeRayColor(ray, scene, 0);
                }
            }
            else
            {
                // Depth of Field
                rayDir.normalize();
                Vec3Df pixFocalPoint = rp.cam.pos + rayDir * focusDistance/Vec3Df::dotProduct(rayDir, rp.cam.dir);

                // Stochastic randomization on a disc with multiple layers
                for(unsigned int r = 0; r < nbDofLayers; r++)
                {
                     for(unsigned int th = 0; th < nbDofThetas; th++)
                     {
                         for(unsigned int nDoFRay = 0; nDoFRay < nbDoFRay; nDoFRay++)
                         {
                             float radius = rnd::random(dofLayerSize * r, dofLayerSize * (r + 1));
                             float theta = rnd::random(dofThetaSize * th, dofThetaSize * (th + 1));
                             Vec3Df originPoint = rp.cam.pos + (cos(theta) * rp.cam.right + sin(theta) * rp.cam.up) * radius;
                             ray.setOrigin(originPoint);
                             ray.setDirection(pixFocalPoint - originPoint);
                             pixelColor.addNoClamp(computeRayColor(ray, scene, 0));
                         }
                     }
                }

                pixelColor *= invDoF;
            }

            // Write pixel
            img[x + y * image.width()] = pixelColor;
        }
    }

    printf("\rRaytracing... 100%%\n");
    progressDialog.setValue(100);
}

Colorf RayTracer::computeRayColor(const Ray& ray, const Scene* scene, unsigned int depth)
{
    // Test if an object or a light is intersecting
    IntersectionResult intersection;
    const Light* light = 0;
    Vec3Df ipoint;
    float tLight = 0;
    bool useObject = scene->intersect(ray, intersection);
    bool useLight = false;

    // Avoid colored disks on the final image (only if lights are raytraced)
    if(raytraceLights && depth > 0)
        useLight = scene->intersectLights(ray, light, ipoint, tLight);

    // Use the closest item
    if(useObject && useLight)
    {
        useObject = intersection.i.t < tLight;
        useLight = !useObject;
    }

    // Compute color
    if(useObject)
    {
        const Material& mat = overrideMaterial ? *overrideMaterial : intersection.obj->getMaterial();
        if(pathTracing)
            return mat.pathTracedBrdfAt(ray, intersection, *scene, depth);
        else
            return mat.brdfAt(ray, intersection, *scene, depth);
    }
    else if(useLight)
        return light->getColor() * light->getIntensity(); // TODO: intensityAt(ray.origin)
    else
        return backgroundColor;
}

void RayTracer::renderDofPreview(QImage& image,
                      const SRenderParameters& rp)
{

    // buffer + scene
    Scene* scene = Scene::getInstance();
    Color* img = (Color*)image.bits();

    // near plane (plane at distance 1.0 following camDir)
    float nearSizeY = 2 * tan(rp.cam.fov / 2);
    float nearSizeX = nearSizeY * rp.cam.aspect;
    Vec3Df nearOrigin = rp.cam.dir + rp.cam.up * nearSizeY / 2.0f - rp.cam.right * nearSizeX / 2.0f;

	// DoF
	float sensorWidth = 0.036f;
	float sensorHeight = sensorWidth * rp.cam.aspect;//2.4f;
	float focalLength = 0.5f * std::sqrt(sensorWidth*sensorWidth + sensorHeight * sensorHeight) / tanf (/*float(PI/180) * */0.5f * rp.cam.fov);
	float confusionCircle = 0.0001f;
	float dn = 1.0f*rp.dof.focusDistance*focalLength*focalLength /
			(focalLength*focalLength + rp.dof.aperture*confusionCircle*(rp.dof.focusDistance-focalLength));
	float df = 1.0f*rp.dof.focusDistance*focalLength*focalLength /
			(focalLength*focalLength - rp.dof.aperture*confusionCircle*(rp.dof.focusDistance-focalLength));

    // Loop for each pixel
    for (unsigned int y = 0; y < rp.screen.height; y++)
    {
        float yRatio = y / float(rp.screen.height);
        printf("\rRaytracing... %i%%       ",int(100 * yRatio));
        fflush(stdout);

        #pragma omp parallel for
        for (int x = 0; x < (int)rp.screen.width; x++)
        {
            float xRatio = x / float(rp.screen.width);
            Ray ray;
            ray.setOrigin(rp.cam.pos);
            Vec3Df rayDir = nearOrigin + xRatio * nearSizeX * rp.cam.right - yRatio * nearSizeY * rp.cam.up;
            Colorf pixelColor;

            rayDir.normalize();
            Vec3Df pixFocalPoint = rp.cam.pos + rayDir * rp.dof.focusDistance/Vec3Df::dotProduct(rayDir, rp.cam.dir);
            ray.setDirection(pixFocalPoint - rp.cam.pos);


            IntersectionResult res;
            if(scene->intersect(ray, res))
            {
                float focusShift = Vec3Df::dotProduct((res.i.point - rp.cam.pos), rp.cam.dir) - Vec3Df::dotProduct((pixFocalPoint - rp.cam.pos), rp.cam.dir);

                pixelColor = computeRayColor(ray, scene, 0);

                if( !(focusShift > (df - rp.dof.focusDistance)) && !(focusShift < (dn - rp.dof.focusDistance)) )
                {
                    pixelColor += Colorf(1.0f, 0.0f, 0.0f);
                    pixelColor *= 0.5f;
                }
            }
            else
                pixelColor = Colorf();

            img[x + y * image.width()] = pixelColor;
        }
    }

    printf("\rRaytracing... 100%%\n");
}
