// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

#include <math.h>
#include <time.h>

#include "../shared.src/string_util.h"
#include "../shared.src/mt19937-2.h"
#include "../shared.src/intersection_info.h"

#include "scene.h"
#include "ui.h"
#include "ray.h"
#include "c3ga_util.h"
#include "surface_point.h"

using namespace c3ga;

scene::scene() : m_current_camera(0), m_random_seed(0), m_display_lights(true) {
    // set render camera
    m_camera.push_back(camera(_EXForm(1.0), 1.0)); // first 1.0 is identify transform, second 1.0 is fov_width

    // set model camera
    m_camera.push_back(camera(_EXForm(1.0), 1.0)); // first 1.0 is identify transform, second 1.0 is fov_width

    // add a default light source:
    add_light_source(
        light_source(
            _EXForm(1.0), // xf
            color(0.8f, 0.8f, 0.8f), // ambient_color
            color(0.7f, 0.7f, 0.7f), // diffuse_color
            color(0.1f, 0.1f, 0.1f), // specular_color,
            20.0, // spot_exponent
            180.0, // spot_cutoff,
            1.0, // constant_attenuation,
            0.0, // linear_attenuation,
            0.0 // quadratic_attenuation
            ));

    //load_rsn("../files/my_third_scene.rsn");
}

scene::scene(const scene &s) :
    m_light_source(s.m_light_source),
//  m_mesh(s.m_mesh), 
    m_camera(s.m_camera),
    m_model(s.m_model),
    m_rtm_model_cache(s.m_rtm_model_cache),
    m_rtm_mesh_cache(s.m_rtm_mesh_cache),
    m_current_camera(s.m_current_camera),
    m_random_seed(s.m_random_seed),
    m_display_lights(s.m_display_lights) {

    transfer_mesh_pointers();

}

scene::scene(const std::string &filename) : m_random_seed(0) {
    load_rsn(filename);
}


scene::~scene() {
}

scene &scene::operator=(const scene &s) {
    if (this != &s) {
        m_light_source = s.m_light_source;
        //m_mesh = s.m_mesh;
        m_camera = s.m_camera;
        m_model = s.m_model;
        m_rtm_model_cache = s.m_rtm_model_cache;
        m_rtm_mesh_cache = s.m_rtm_mesh_cache;
        m_current_camera = s.m_current_camera;
        m_random_seed = s.m_random_seed;
        m_display_lights = s.m_display_lights;

        transfer_mesh_pointers();
    }
    return *this;
}

const model &scene::get_rtm(const std::string &filename) {
    if (m_rtm_model_cache.find(filename) == m_rtm_model_cache.end())
        load_rtm(filename);
    return m_rtm_model_cache[filename];
}

void scene::add_model(const std::string &filename) {
    // load model
    model m(get_rtm(filename));

    // get camera
    const camera &c(get_current_camera());

    // set transform of the model (so that it is in front of the current camera)
    // todo: 3.0 -> bounding sphere size . . .
    m.set_xf(_EXForm(c.get_xf() * exp(-3.0 * nie3)));

//  printf("%s\n", m.get_xf().string());

    // add model
    m_model.push_back(m);
}

void scene::add_model(const model &M) {
    m_model.push_back(M);
}

/// duplicates a model \param M
void scene::duplicate_model(const model &_M) {
    // copy model
    model M(_M);

    /**
    We want to keep the orientation of the model, but position it
    in front of the camera.
    So first we compute where we want the model to be (pos).
    Then we compute that position in the frame of the model,
    and create a translator to that position, also in the model frame.
    */
    EXForm in_front_of_camera_xf = _EXForm(scene::get_current_camera().get_xf() * exp(-3.0 * nie3));
    // todo: 3.0 -> bounding sphere size . . .
    flatPoint pos = _flatPoint(in_front_of_camera_xf * noni * inverse(in_front_of_camera_xf));
    flatPoint pos_in_model_frame = _flatPoint(M.apply_xfi_to(pos));
    translator T = _translator(exp(-0.5 * (pos_in_model_frame - (noni))));

    // apply transform to model
    M.post_mul_xf(T);

    // turn of selection because that might be on (if the model _M passed to this function was selected!)
    M.set_selected(false);

    // add model to scene
    add_model(M);
}



const camera &scene::get_camera(int idx) const {
    try {
        return m_camera.at(idx);
    }
    catch (...) {
        throw std::string("scene::get_camera(): index out of range");
    }
}

camera &scene::get_camera(int idx) {
    try {
        return m_camera.at(idx);
    }
    catch (...) {
        throw std::string("scene::get_camera(): index out of range");
    }
}

void scene::set_camera(int idx, const camera &C) {
    if ((idx < 0) || (idx >= (int)m_camera.size()))
        throw std::string("scene::set_camera(): index out of range");
    m_camera[idx] = C;
}

const camera &scene::get_current_camera() const {
    return get_camera(m_current_camera);
}

camera &scene::get_current_camera() {
    return get_camera(m_current_camera);
}

int scene::get_current_camera_idx() const {
    return m_current_camera;
}


/// Sets the currently active camera ('model' or 'render'); throws exception if index out of range
void scene::set_current_camera(int idx) {
    if ((idx < 0) || (idx >= (int)m_camera.size()))
        throw std::string("scene::set_current_camera(): index out of range");
    if (m_current_camera == idx) return;


    m_current_camera = idx;

    // update the user interface (menu widgets)
    if (g_ui_state)
        g_ui_state->update_current_camera();
}


void scene::add_light_source(const light_source &L) {
    m_light_source.push_back(L);
}

void scene::reset_selected_object() {
    unsigned int i;
    // set all models & light source to 'not selected'
    for (i = 0; i < m_model.size(); i++)
        m_model[i].set_selected(false);
    for (i = 0; i < m_light_source.size(); i++)
        m_light_source[i].set_selected(false);

    if (g_ui_state) g_ui_state->update_selected_none();
}

void scene::set_selected_object(int idx) {
    // Goal: set the specified object to selected

    // check if already selected; if so: simply return
    if (idx < 0) {

    }
    else if (idx < (int)m_model.size()) {
        if (m_model[idx].get_selected()) {
            return;
        }
    }
    else if (idx < (int)(m_model.size() + m_light_source.size())) {
        if (m_light_source[idx - m_model.size()].get_selected()) {
            return;
        }
    }

    // set all models & light source to 'not selected'
    reset_selected_object();

    // set the specified object to selected
    if (idx < 0) {
        if (g_ui_state) g_ui_state->update_selected_none();
    }
    else if (idx < (int)m_model.size()) {
        m_model[idx].set_selected(true);

        // tell it to the UI state so it can update the properties window:
        if (g_ui_state)
            g_ui_state->update_selected_model();
    }
    else {
        idx -= (int)m_model.size();
        if (idx < (int)m_light_source.size()) {
            m_light_source[idx].set_selected(true);
            // tell it to the UI state so it can update the properties window:
            if (g_ui_state)
                g_ui_state->update_selected_light_source();
        }
    }

}

locator &scene::get_selected_object() {
    unsigned int i;
    for (i = 0; i < m_model.size(); i++)
        if (m_model[i].get_selected())
            return m_model[i];
    for (i = 0; i < m_light_source.size(); i++)
        if (m_light_source[i].get_selected())
            return m_light_source[i];
    throw std::string("scene::get_selected_object(): no object selected");
}

light_source &scene::get_selected_light_source() {
    for (unsigned int i = 0; i < m_light_source.size(); i++)
        if (m_light_source[i].get_selected())
            return m_light_source[i];
    throw std::string("scene::get_selected_light_source(): no light source selected");
}

model &scene::get_selected_model() {
    for (unsigned int i = 0; i < m_model.size(); i++)
        if (m_model[i].get_selected())
            return m_model[i];
    throw std::string("scene::get_selected_model(): no model selected");
}

/// Turns this scene into a string (e.g., for storing a scene to file)
std::string scene::to_string(const std::string &basepath) const {
    std::string result;
    unsigned int i;

    // models 
    for (i = 0; i < m_model.size(); i++)
        result += m_model[i].to_string(basepath) + "\n";

    // light sources
    for (i = 0; i < m_light_source.size(); i++)
        result += m_light_source[i].to_string() + "\n";

    result += "display_lights " + ::to_string((int)display_lights()) + "\n";

    // cameras
    for (i = 0; i < m_camera.size(); i++)
        result += m_camera[i].to_string() + "\n";
    result += "current_camera " + ::to_string(m_current_camera) + "\n";

    return result;
}

void scene::transfer_mesh_pointers() {
    /*
    After a copy, all pointers to meshes have to be
    'transfered' from the old object to the new object
    */
    for (unsigned int i = 0; i < m_model.size(); i++)
        m_model[i].set_mesh(&(m_rtm_mesh_cache[m_model[i].get_mesh()->get_filename()]));

    for (std::map<std::string, model>::iterator I = m_rtm_model_cache.begin();
        I != m_rtm_model_cache.end(); I++)
        I->second.set_mesh(&(m_rtm_mesh_cache[I->second.get_mesh()->get_filename()]));

}

void scene::remove_selected_object() {
    unsigned int i;

    for (i = 0; i < m_model.size(); i++)
        if (m_model[i].get_selected())
            m_model.erase(m_model.begin() + i);

    // light sources
    for (i = 0; i < m_light_source.size(); i++)
        if (m_light_source[i].get_selected())
            m_light_source.erase(m_light_source.begin() + i);

    // tell it to the UI state so it can update the properties window:
    if (g_ui_state)
        g_ui_state->update_selected_none();

}

void scene::set_random_seed(unsigned long seed) {
    sgenrand(m_random_seed = seed);
}

void scene::reset_random_seed() const {
    sgenrand(m_random_seed);
}

void scene::render_init() {
    int i;
    for (i = 0; i < get_nb_models(); i++)
        get_model(i).compute_position();

    for (i = 0; i < get_nb_light_sources(); i++)
        get_light_source(i).compute_position();

    get_current_camera().compute_position();
}


void scene::render(int camera_idx, unsigned char *image_buffer,
                   int image_width, int image_height, int multisample,
                   int verbosity, const intersection_info *II, int max_recursion, int scanline) {

    /*
    If precomputed intersection info is provided, reset the random seed for the 
    mersenne twister to the value it had before intersection info
    was precomputed. This is required in order to reproduce sequence
    of events as they happened during precomputation.

    If no precomputed intersection info is provided,
    just set the seed to the current time.
    */
    if (scanline <= 0) {
        if (II) reset_random_seed();
        else set_random_seed(time(NULL));
    }

    if (multisample < 0) multisample = 1;

    // get camera
    const camera &C(get_camera(camera_idx));

    // initialize per pixel/line adds
    mv::Float pixel_width = C.get_fov_width() / image_width;
    mv::Float pixel_height = -pixel_width;

    /*
    We 'recycle' the initial ray for all samples/pixels
    Here we set the position of the ray, later we set
    its direction.
    */
    ray R;
    R.set_position(C.get_position());

    int startline = 0;
    int endline = image_height;
    if ((scanline >= 0) && (scanline < image_height))
        startline = scanline, endline = startline + 1;

    int idx = startline * image_width; // index in buffer where the final color values go

    for (int y = startline; y < endline; y++) {
        if (verbosity > 0) printf("scanline %d\n", y);
        for (int x = 0; x < image_width; x++) {

            /*
            Compute the 'base direction' of the ray. It has to 
            go through sensor pixel [x, y]. Later we add small pertubations
            for multisampling.
            */
            freeVector base_direction = _freeVector(
                                    ((x * pixel_width - 0.5 * image_width * pixel_width) * e1 +
                                    (y * pixel_height - 0.5 * image_height * pixel_height) * e2 -
                                    e3) ^ ni);

            // sample multiple times, accumulate the result in pix_color:
            color pixel_color;
            for (int s = 0; s < multisample; s++) {
                /*
                Add a small perturbation within the pixel.
                To generate random numbers, we call genrand(),
                the mersenne twister random number generator.
                */
                freeVector sample_direction;
                if (multisample > 1)
                    sample_direction = _freeVector(
                        unit_e(base_direction +
                        (((genrand() - 0.5) * e1 * pixel_width +
                        (genrand() - 0.5) * e2 * pixel_height) ^ ni)));
                else sample_direction = _freeVector(unit_e(base_direction));

                /*
                Sample direction is in the camera frame, so transform it to the 
                global frame, and set the ray direction
                */
                R.set_direction(_freeVector(apply_om(C.get_M(), sample_direction)));

                // trace the ray
                pixel_color += trace(R, max_recursion, (II) ? II + idx : NULL);
            }

            pixel_color /= (float)multisample;

            // clamp color to range of image
            pixel_color.clamp(0.0, 1.0);

            // set color
            image_buffer[idx * 3 + 0] = (unsigned char)(pixel_color.r() * 255.0f + 0.5f);
            image_buffer[idx * 3 + 1] = (unsigned char)(pixel_color.g() * 255.0f + 0.5f);
            image_buffer[idx * 3 + 2] = (unsigned char)(pixel_color.b() * 255.0f + 0.5f);

            // update index into image_buffer
            idx++;
        }
    }
}


color scene::trace(const ray &R, int max_recursion, const intersection_info *II) const {
    surface_point spt;

    // find the closest intersection point
    if (II) {
    /*  if (info->m_model == NULL) return 0;
        if (info->m_model->lineFaceIntersect(ln, startPt, spt, info->m_fIdx) == 0) {
            cr.set(0.0, 0.0, 0.0);
            return 0;
        }*/
    }
    else {
        if (!find_intersection(R, spt, true)) return color(); // no intersection found
    }

    // get all info for the surface point in 'spt':
    spt.compute_details();


    // this is where the resulting color will go:
    color C;

    // do local shading computations
    if (spt.get_direct_lighting_factor() > 0.0)
        C = shade(R, II, spt);

    // reflection required? This will recursively call trace() again
    if ((max_recursion > 0) && (spt.get_reflection_factor() > 0.0))
        C += reflect(R, max_recursion, II, spt);

    // refraction required? This will also recursively call trace() again
    if ((max_recursion > 0) && (spt.get_refraction_factor() > 0.0))
        C += refract(R, max_recursion, II, spt);

    return C;
}


color scene::shade(const ray &R, const intersection_info *II, const surface_point &spt) const {
    // perform basic 'OpenGL-style' lighting computations (+ a shadow check)...

    // this is where the lighting computations are accumulated:
    color C;

    // evaluate the contribution of every light and add it to 'c'
    for (int i = 0; i < get_nb_light_sources(); i++) {
        C += get_light_source(i).shade(R, *this, II, spt);
    }

    // global ambient in scene (TODO TODO TODO: look this up in OpenGL manual)
    //C.mul(m_ambientColor, spt.m_ambientColor);

    // apply direct lighting factor & return
    return (float)spt.get_direct_lighting_factor() * C;
}


color scene::reflect(const ray &R, int max_recursion, const intersection_info *II, const surface_point &spt) const {
    /*
    Reflect the ray direction in the surface attitude,
    and spawn a new ray with that direction at the surface point.
    */
    ray reflected_ray(
        spt.get_pt(),
        _freeVector(-((spt.get_att() ^ no) * R.get_direction() * reverse(spt.get_att() ^ no)))
        );

    return trace(reflected_ray, max_recursion - 1, II);
}


color scene::refract(const ray &R, int max_recursion, const intersection_info *II, const surface_point &spt) const {
    vectorE3GA surface_normal = _vectorE3GA(no << dual(spt.get_att()));
    vectorE3GA ray_direction = _vectorE3GA(no << R.get_direction());

    //printf("surface_normal = %s,\n", surface_normal.string());
    //printf("ray_direction = %s,\n", ray_direction.string());

    mv::Float nu2 = _Float(surface_normal % ray_direction);
    mv::Float r, r2, f, sd;

    if (nu2 > 0.0) {
        sd = 1.0;
        r = spt.get_refractive_index() / get_refractive_index();
    }
    else if (nu2 < 0.0) {
        sd = -1.0;
        r = get_refractive_index() / spt.get_refractive_index();
    }
    else return color();

    r2 = r * r;

    f = (1 - r2) + nu2 * nu2 * r2;
    if (f < 0) return color(); // total internal reflection

    ray refracted_ray(
        spt.get_pt(),
        _freeVector(((sd * (mv::Float)sqrt(f) - nu2 * r) * surface_normal + r * ray_direction) ^ ni)
        );

    //printf("ray_direction = %s,\n\n", refracted_ray.get_direction().string());
//  printf("Rp = %s,\n", c3ga::string(refracted_ray.get_position(), "%f"));
//  printf("Rd = %s,\n", c3ga::string(refracted_ray.get_direction(), "%f"));
    return trace(refracted_ray, max_recursion - 1, II);
}


bool scene::find_intersection(const ray &R, surface_point &spt, bool find_closest) const {
    /*
    Find the (closest) intersection point with any model
    */
    spt.set_valid(false);
//  surface_point tmp_spt; // temporary surface point
    for (unsigned int i = 0; i < m_model.size(); i++) {
        // for each model, seach for the intersection
        if (m_model[i].find_intersection(R, spt, find_closest) &&
            (!find_closest)) {
            // an intersection was found and the caller doesn't care about the closest
            return true;
            }
    }

    // set distance to true value (required?):
    if (spt.get_valid()) {
        spt.set_distance(sqrt(spt.get_distance()));
        return true;
    }
    else return false;
}

bool scene::display_lights() const {
    return m_display_lights;
}

void scene::set_display_lights(bool l) {
    m_display_lights = l;
}