// *********************************************************
// Material Class
// Author : Tamy Boubekeur (boubek@gmail.com).
// Copyright (C) 2010 Tamy Boubekeur.
// All rights reserved.
// *********************************************************

#include "Material.h"
#include "Scene.h"
#include "Light.h"
#include "Object.h"
#include "Ray.h"
#include "RayTracer.h"
#include "Random.h"

Colorf Material::getPhongDiffuse(const Vec3Df& normal, const IntersectionResult& intersection, const Scene& scene) const
{
    Colorf pixelColor;

    for(unsigned int i = 0; i < scene.getLightNb(); i++)
    {
        // Light
        const Light& light = scene.getLightRef(i);
        Vec3Df lightDir = light.getPos() - intersection.i.point;
        lightDir.normalize();
        float lightIntensity = scene.getLightIntensity(intersection.i.point, light);

        // Diffuse
        float lamberFactor = std::max(0.0f, Vec3Df::dotProduct(normal, lightDir));
        pixelColor += color * light.getColor() * lightIntensity * diffuse * lamberFactor;

        // Specular
        Vec3Df eye = intersection.i.point;
        eye.normalize();
        Vec3Df R = -lightDir - 2.0f * Vec3Df::dotProduct(normal, -lightDir) * normal;
        float spec = std::pow(std::max(Vec3Df::dotProduct(R, eye), 0.0f), shininess);
        pixelColor += color * light.getColor() * light.getIntensity() * spec * specular;
    }

    return pixelColor;
}

Colorf Material::getTransparent(const Vec3Df& incident, const IntersectionResult& intersection, const Scene& scene, unsigned int depth) const
{
	RayTracer* renderer = RayTracer::getInstance();
	Vec3Df normal = intersection.getNormal(); //intersection.getFlatNormal();
	Colorf transparentColor;

	if(renderer->canCastRay(depth) && transparency > 0.01f)
	{
		float n1 = 1, n2 = indice;
		if(Vec3Df::dotProduct(incident, normal) > 0.0f)
			// Ray is going out
			std::swap(n1, n2);

		bool refracted = false;
		Vec3Df reflectDir = reflect(incident, normal);
		Vec3Df refractDir = refract(incident, normal, n1, n2, refracted);

		// Single ray if no gloss filtering (or if no gloss)
		if(!filterGloss || gloss <= 0.0f)
		{
			if(gloss > 0.0f)
			{
				reflectDir = addGloss(reflectDir, normal);
				if(refracted)
					refractDir = addGloss(refractDir, normal);
			}

			if(!isMirror && refracted)
			{
				float refRatio = reflectionRatio(incident, normal, n1, n2);
				Ray reflectRay(intersection.i.point + reflectDir * 0.0001f, reflectDir);
				Ray refractRay(intersection.i.point + refractDir * 0.0001f, refractDir);

				transparentColor = refRatio * renderer->computeRayColor(reflectRay, &scene, depth + 1)
						   + (1 - refRatio) * renderer->computeRayColor(refractRay, &scene, depth + 1);
			}
			else
			{
				Ray reflectRay(intersection.i.point + reflectDir * 0.0001f, reflectDir);

				transparentColor = renderer->computeRayColor(reflectRay, &scene, depth + 1);
			}
		}
		else // Cast multiple ray to filter gloss
		{
			// Prepare stochastic disk subdivision
			unsigned int nbLayers = 1;
			unsigned int nbThetas = 2 << (1 + nbLayers);
			float layerSize = gloss / nbLayers;
			float thetaSize = 2 * PI / nbThetas;

			if(!isMirror && refracted)
			{
				// Base
				Vec3Df uReflect = Vec3Df::crossProduct(reflectDir, normal);
				Vec3Df vReflect = Vec3Df::crossProduct(uReflect, reflectDir);
				Vec3Df uRefract = Vec3Df::crossProduct(refractDir, normal);
				Vec3Df vRefract = Vec3Df::crossProduct(uRefract, refractDir);
				Vec3Df posReflect, posRefract;
				uReflect.normalize();
				uRefract.normalize();
				vReflect.normalize();
				vRefract.normalize();
				float refRatio = reflectionRatio(incident, normal, n1, n2);

				// Averaging
				for(unsigned int r = 0; r < nbLayers; r++)
				{
					for(unsigned int th = 0; th < nbThetas; th++)
					{
						float radius = rnd::random(layerSize * r, layerSize * (r + 1));
						float theta = rnd::random(thetaSize * th, thetaSize * (th + 1));
						posReflect = reflectDir + (cos(theta) * vReflect + sin(theta) * uReflect) * radius;
						posRefract = refractDir + (cos(theta) * vRefract + sin(theta) * uRefract) * radius;

						Ray reflectRay(intersection.i.point + posReflect * 0.0001f, posReflect);
						Ray refractRay(intersection.i.point + posRefract * 0.0001f, posRefract);
						transparentColor.addNoClamp(refRatio * renderer->computeRayColor(reflectRay, &scene, depth + 1)
											+ (1 - refRatio) * renderer->computeRayColor(refractRay, &scene, depth + 1));
					}
				}
				transparentColor *= 1.0f / (nbLayers * nbThetas);
			}
			else
			{
				// Base
				Vec3Df uReflect = Vec3Df::crossProduct(reflectDir, normal);
				Vec3Df vReflect = Vec3Df::crossProduct(uReflect, reflectDir);
				Vec3Df posReflect;
				uReflect.normalize();
				vReflect.normalize();

				// Averaging
				for(unsigned int r = 0; r < nbLayers; r++)
				{
					for(unsigned int th = 0; th < nbThetas; th++)
					{
						float radius = rnd::random(layerSize * r, layerSize * (r + 1));
						float theta = rnd::random(thetaSize * th, thetaSize * (th + 1));
						posReflect = reflectDir + (cos(theta) * vReflect + sin(theta) * uReflect) * radius;

						Ray reflectRay(intersection.i.point + posReflect * 0.0001f, posReflect);
						transparentColor.addNoClamp(renderer->computeRayColor(reflectRay, &scene, depth + 1));
					}
				}
				transparentColor *= 1.0f / (nbLayers * nbThetas);
			}
		}
	}

	return transparentColor;
}

Colorf Material::pathTracedBrdfAt(const Ray& ray, const IntersectionResult& intersection, const Scene& scene, unsigned int depth) const
{
	RayTracer* renderer = RayTracer::getInstance();
	if(!renderer->canCastRay(depth))
		return renderer->getBackgroundColor(); //color; //getPhongDiffuse(normal, intersection, scene);

	// *************************************************************
	// Triangle normal
	// *************************************************************
	Vec3Df normal = intersection.getNormal();

	// *************************************************************
    // Local base
	// *************************************************************
    Vec3Df u = Vec3Df(normal[1], normal[0], 0);
    if(normal[1] == normal[0]) u[0] = 2 * normal[1];
    u = Vec3Df::crossProduct(u, normal);
    Vec3Df v = Vec3Df::crossProduct(normal, u);
    u.normalize();
    v.normalize();

	// *************************************************************
	// Stochastic hemisphere division parameters
	// *************************************************************
	unsigned int nbPhis = 2;
	unsigned int nbThetas = 2 << (1 + nbPhis);
	float phiSize = 0.5f * PI / nbPhis;
	float thetaSize = 2 * PI / nbThetas;

	// *************************************************************
	// Cast rays
	// *************************************************************
	Ray ptRay(intersection.i.point + normal * 0.00001f,Vec3Df());
	Colorf resColor;
	for(unsigned int p = 0; p < nbPhis; p++)
		for(unsigned int t = 0; t < nbThetas; t++)
		{
			float phi = rnd::random(p * phiSize, (p + 1) * phiSize);
			float theta = rnd::random(t * thetaSize, (t + 1) * thetaSize);
			ptRay.setDirection((cos(theta) * u + sin(theta) * v) * sin(phi) + normal * cos(phi));

			resColor.addNoClamp(cos(phi) * renderer->computeRayColor(ptRay, &scene, depth + 1));
		}
	resColor = color * (resColor / float(nbThetas * nbPhis));

	// *************************************************************
	// Transparency
	// *************************************************************
	Vec3Df incident = ray.getDirection();
	incident.normalize();
	Colorf transparentColor = getTransparent(incident, intersection, scene, depth);

	// *************************************************************
	// Blend color
	// *************************************************************
	return (1 - transparency) * resColor + transparency * transparentColor * transparencyColor;
}

Colorf Material::brdfAt(const Ray& ray, const IntersectionResult& intersection, const Scene& scene, unsigned int depth) const
{
    // *************************************************************
    // Triangle normal
    // *************************************************************
    Vec3Df normal = intersection.getNormal();
    Vec3Df incident = ray.getDirection();
    incident.normalize();

    // *************************************************************
    // Diffuse component
    // *************************************************************
    Colorf matColor = getPhongDiffuse(normal, intersection, scene);

    // *************************************************************
    // Reflexion / refraction for transparent materials
    // *************************************************************
	Colorf transparentColor = getTransparent(incident, intersection, scene, depth);

    // *************************************************************
    // Color blending
    // *************************************************************
    //return (1 - transparency) * matColor + transparency * transparentColor;
    return (1 - transparency) * matColor + transparency * transparentColor * transparencyColor;
}

Vec3Df Material::addGloss(const Vec3Df& dir, const Vec3Df& baseGen) const
{
    Vec3Df u = Vec3Df::crossProduct(dir, baseGen);
    Vec3Df v = Vec3Df::crossProduct(u, dir);
    u.normalize();
    v.normalize();

    float radius = rnd::random(0.0f, gloss);
    float theta = rnd::random(0.0f, 2 * PI);

    return dir + (cos(theta) * v + sin(theta) * u) * radius;
}

Vec3Df Material::reflect(const Vec3Df& v, const Vec3Df& normal) const
{
    return v - 2 * Vec3Df::dotProduct(v,normal) * normal;
}

Vec3Df Material::refract(const Vec3Df& incident, const Vec3Df& normal, float n1, float n2, bool& success) const
{
    // Angles
    float n = n1 / n2;
    float cos1 = -Vec3Df::dotProduct(normal, incident);
    float cos2sq = 1 - n * n * (1 - cos1 * cos1);

    if(cos2sq < 0.0f)
    {
        // Total reflexion
        success = false;
        return Vec3Df();
    }
    else
    {
        // Refraction
        success = true;
        if(cos1 >= 0.0f)
            return n * incident + (n * cos1 - sqrt(cos2sq)) * normal;
        else
            return n * incident + (n * cos1 + sqrt(cos2sq)) * normal;
    }
}

float Material::reflectionRatio(const Vec3Df& incident, const Vec3Df& normal, float n1, float n2) const
{
    // Schlick approximation
    float r0 = (n1 - n2) / (n1 + n2);
    r0 *= r0;
    float cosI = -Vec3Df::dotProduct(normal, incident);

    if(n1 > n2)
    {
        const float n = n1 / n2;
        const float cosTsq = 1 - n * n * (1 - cosI * cosI);
        if(cosTsq < 0.0f)
            return 1.0f; // Total reflexion
        else
            cosI = sqrt(cosTsq);
    }

    const float x = 1 - cosI;
    return r0 + (1 - r0) * x * x * x * x * x;
}
