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

#include "Scene.h"
#include "Object.h"
#include "Light.h"
#include "Random.h"

#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

using namespace std;

static Scene * instance = NULL;

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

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

Vec3Df IntersectedTriangle::getFlatNormal(const Object* obj) const
{
    const Vertex& v1 = obj->getMesh().getVertices()[tri.getVertex(0)];
    const Vertex& v2 = obj->getMesh().getVertices()[tri.getVertex(1)];
    const Vertex& v3 = obj->getMesh().getVertices()[tri.getVertex(2)];

    Vec3Df normal = Vec3Df::crossProduct(v2.getPos() - v1.getPos(),v3.getPos() - v1.getPos());
    normal.normalize();
    return normal;
}

Vec3Df IntersectedTriangle::getNormal(const Object* obj) const
{
    const Vec3Df& n1 = obj->getMesh().getVertices()[tri.getVertex(0)].getNormal();
    const Vec3Df& n2 = obj->getMesh().getVertices()[tri.getVertex(1)].getNormal();
    const Vec3Df& n3 = obj->getMesh().getVertices()[tri.getVertex(2)].getNormal();
    Vec3Df normal = n1 + u * (n2 - n1) + v * (n3 - n1);
    normal.normalize();
    return normal;
}

bool Scene::loadScene(const string &filename)
{
    Assimp::Importer importer;

    const aiScene* scene = importer.ReadFile( filename,
        aiProcess_CalcTangentSpace       |
        aiProcess_Triangulate            |
        aiProcess_JoinIdenticalVertices  |
        aiProcess_SortByPType            /*|
        aiProcess_PreTransformVertices*/);

    if( !scene)
    {
        std::cout << "Error loading scene: " << filename << " (" << importer.GetErrorString() << ")\n";
        return false;
    }

    clear();
    int totalV = 0;

    for(unsigned int meshIndex = 0; meshIndex < scene->mNumMeshes; meshIndex++)
    {
        Object* obj = new Object();
        const aiMesh* mesh = scene->mMeshes[meshIndex];
        totalV += mesh->mNumVertices;

        const aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];

        aiColor3D color (0.f,0.f,0.f);
        float shine = 0.0f;
        float refractionIndice = 0.0f;
        material->Get(AI_MATKEY_COLOR_DIFFUSE,color);
        material->Get(AI_MATKEY_SHININESS, shine);
        material->Get(AI_MATKEY_REFRACTI, refractionIndice);

        obj->getMaterial().setColor(Colorf(color.r, color.g, color.b));
        obj->getMaterial().setShininess((int)shine);
        obj->getMaterial().setIndice(refractionIndice);
        obj->getMaterial().setDiffuse(0.8);
        obj->getMaterial().setSpecular(0.2);

        for(unsigned int vertexIndex = 0; vertexIndex < mesh->mNumVertices; vertexIndex++)
        {
            float x = mesh->mVertices[vertexIndex].x;
            float y = mesh->mVertices[vertexIndex].y;
            float z = mesh->mVertices[vertexIndex].z;

            obj->getMesh().addVertex(Vertex(Vec3Df(x, y, z), Vec3Df(1.0, 0.0, 0.0)));
        }

        for(unsigned int faceIndex = 0; faceIndex < mesh->mNumFaces; faceIndex++)
        {
            int i1 = mesh->mFaces[faceIndex].mIndices[0];
            int i2 = mesh->mFaces[faceIndex].mIndices[1];
            int i3 = mesh->mFaces[faceIndex].mIndices[2];

            obj->getMesh().addTriangle(Triangle(i1, i2, i3));
        }

        obj->getMesh().updateBoundingBox();
        obj->getMesh().recomputeSmoothVertexNormals(0);
        obj->getMesh().rebuildTree();

        objects.push_back(obj);
    }


    for(unsigned int lightIndex = 0; lightIndex < scene->mNumLights; lightIndex++)
    {
        const aiLight* light = scene->mLights[lightIndex];

        float x = light->mPosition.x;
        float y = light->mPosition.y;
        float z = light->mPosition.z;

        float r = light->mColorDiffuse.r;
        float g = light->mColorDiffuse.g;
        float b = light->mColorDiffuse.b;

        Light* l = new Light(Vec3Df (x, y, z), Colorf (r, g, b), 4.0f,0);
        lights.push_back(l);
    }

    std::cout << "Loaded scene: " << filename << std::endl;
    return true;
}

Scene::Scene () :
    ambientLight(0.1f),
    shadows(false)
{
    buildDefaultScene ();
    updateBoundingBox ();
}

Scene::~Scene ()
{
    clear();
}

void Scene::clear()
{
    for(unsigned int t = 0; t < objects.size(); t++)
        delete objects[t];
    objects.clear();
    for(unsigned int t = 0; t < lights.size(); t++)
        delete lights[t];
    lights.clear();
}

float Scene::getRadius() const
{
    BoundingBox box = bbox;
    for(unsigned int i = 0; i < lights.size(); i++)
        box.extendTo(lights[i]->getBoundingBox());
    return box.getRadius();
}

void Scene::updateBoundingBox()
{
    if (objects.empty())
        bbox = BoundingBox ();
    else
    {
        bbox = objects[0]->getBoundingBox ();
        for(unsigned int i = 1; i < objects.size (); i++)
            bbox.extendTo(objects[i]->getBoundingBox ());
    }
}

bool Scene::intersect(Ray ray, IntersectionResult& res) const
{
    res.reset();
    IntersectedTriangle tmp;
    float dist = -1.0f;
    Vec3Df orig = ray.getOrigin();

    for(unsigned int k = 0; k < objects.size (); k++)
    {
        // Transform ray
        const Object* obj = objects[k];
        ray.setOrigin(orig - obj->getTrans());

        // Intersect with object
        if(obj->intersect(ray, tmp))
        {
            float d = (tmp.point - ray.getOrigin()).getSquaredLength();
            if(dist < 0.0f || d < dist)
            {
                dist = d;

                res.obj = obj;
                res.i = tmp;
                res.i.point += obj->getTrans();
            }
        }
    }

    return dist >= 0.0f;
}

bool Scene::intersectLights(const Ray& ray, const Light*& light, Vec3Df& point, float& t) const
{
    Vec3Df ipt;
    float dist = -1, tL = 0;

    for(unsigned int k = 0; k < lights.size(); k++)
    {
        const Light* l = lights[k];
        // Punctual lights are not tested (as probability that there is an intersection is very close to 0)
        if(!l->isPunctual()
            && ray.intersect(l->getPos(), l->getExtent() / 2, ipt, tL)
            && (dist < 0.0f || tL < dist))
        {
            dist = tL;
            point = ipt;
            light = l;
            t = tL;
        }
    }

    return dist >= 0.0f;
}

float Scene::getLightIntensity(const Vec3Df& point, const Light& l) const
{
    float pointIntensity = l.getIntensityAt(point);

    // If shadows disabled
    if(!shadows)
        return ambientLight + pointIntensity;

    static const float EPSILON = 0.000001f;
    // Punctual lights
    Vec3Df lPos = l.getPos();

    if(l.isPunctual())
    {
        Vec3Df line = point - lPos;
        Ray ray(lPos, line);
        IntersectionResult intersection;

        if(intersect(ray, intersection) && (intersection.i.point - point).getSquaredLength() > EPSILON)
            return ambientLight;
        else
            return ambientLight + pointIntensity;
    }
    else
    {
        Vec3Df dir = point - lPos;
        Vec3Df u(dir[1], dir[2], dir[0]);
        u = Vec3Df::crossProduct(dir, u);
        Vec3Df v = Vec3Df::crossProduct(u, dir);
        u.normalize();
        v.normalize();
        IntersectionResult intersection;
        Vec3Df pos;
        float res = 0.0f;

        // Zones on the disk
        unsigned int quality = 2; /// TODO: move this in the GUI
        unsigned int nbLayers = 4;//quality * sqrt(l.getExtent());
        unsigned int nbThetas = 2 << (1 + nbLayers);
        float layerSize = l.getExtent() / nbLayers;
        float thetaSize = 2 * PI / nbThetas;

        // Stochastic randomization
        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));
                pos = (cos(theta) * v + sin(theta) * u) * radius;
                Ray ray(lPos + pos, point - lPos - pos);

                if(!intersect(ray, intersection) || (intersection.i.point - point).getSquaredLength() <= EPSILON)
                    res += pointIntensity;
            }

        return ambientLight + res / (nbLayers * nbThetas);
    }
}

// Changer ce code pour creer des scenes originales
void Scene::buildDefaultScene()
{
    // Ground
    Object* ground = new Object();
    ground->getMesh().loadOFF("models/ground.off");
	ground->getMaterial().setTransparency(0.8f);
    ground->getMaterial().setIsMirror(true);
	ground->getMaterial().setGloss(0.45f);
    objects.push_back(ground);
/*
    // Roof
    Object* roof = new Object();
    roof->getMesh().loadOFF("models/ground.off");
    roof->setMaterial(Material());
    roof->setTrans(Vec3Df(0,0,2));
	roof->getMaterial().setTransparency(0.95f);
	roof->setIsMirror(true);
	objects.push_back(roof);
*/
    // Ram
    Object* ram = new Object();
    ram->getMesh().loadOFF("models/ram.off");
    ram->setMaterial(Material(1.f, 1.f, 30, Colorf (1.f, .6f, .2f)));
	ram->setTrans (Vec3Df (1.f, 0.5f, 0.f));
    objects.push_back(ram);

    // Rhino
    Object* rhino = new Object();
    rhino->getMesh().loadOFF("models/rhino.off");
    rhino->setMaterial(Material(1.0f, 0.2f, 30, Colorf (0.6f, 0.6f, 0.7f)));
	rhino->setTrans (Vec3Df (-1.f, -1.0f, 0.4f));
	rhino->getMaterial().setTransparency(0.8f);
    rhino->getMaterial().setIsMirror(false);
	rhino->getMaterial().setIndice(1.5f);
    objects.push_back(rhino);

    // Gargoyle
    Object* garg = new Object();
    garg->getMesh().loadOFF("models/gargoyle.off");
    garg->setMaterial(Material(0.7f, 0.4f, 30, Colorf (0.5f, 0.8f, 0.5f)));
	garg->setTrans (Vec3Df (-1.f, 1.0f, 0.1f));
	objects.push_back(garg);

    Light* l = new Light(Vec3Df (0.0f, 0.0f, 2.0f), Colorf (1.0f, 1.0f, 1.0f), 1.0f, 1);
	lights.push_back(l);
}
