// 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.

#ifdef WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
#include <GL/gl.h>

#include "c3ga_util.h"
#include "model.h"
#include "mesh.h"
#include "ray.h"
#include "surface_point.h"
#include "../shared.src/string_util.h"
#include "../shared.src/fltk_stuff.h"

using namespace c3ga;

model::model() : locator(), m_mesh(NULL),
    m_bumpyness(0.0),
    m_shininess(1.0), m_direct_lighting_factor(0.0),
    m_reflection_factor(0.0), m_refraction_factor(0.0),
    m_refractive_index(1.0) {

    set_default_material_colors();
}

model::model(const model &m) : locator(m), // call superclass constructor
    m_mesh(m.m_mesh),

    m_ambient_color(m.m_ambient_color),
    m_diffuse_color(m.m_diffuse_color),
    m_specular_color(m.m_specular_color),
    m_ambient_texture(m.m_ambient_texture),
    m_diffuse_texture(m.m_diffuse_texture),
    m_specular_texture(m.m_specular_texture),
    m_shininess_texture(m.m_shininess_texture),
    m_bump_map_texture(m.m_bump_map_texture),
    m_shininess(m.m_shininess), m_bumpyness(m.m_bumpyness),
    m_direct_lighting_factor(m.m_direct_lighting_factor),
    m_reflection_factor(m.m_reflection_factor), m_refraction_factor(m.m_refraction_factor),
    m_refractive_index(m.m_refractive_index) {
}

model::~model() {

}


model &model::operator=(const model &m) {
    if (this != &m) {
        locator::operator=(m); // call superclass copy op

        m_mesh = m.m_mesh; // no need to clone the mesh too

        m_ambient_color = m.m_ambient_color;
        m_diffuse_color = m.m_diffuse_color;
        m_specular_color = m.m_specular_color;
        m_ambient_texture = m.m_ambient_texture;
        m_diffuse_texture = m.m_diffuse_texture;
        m_specular_texture = m.m_specular_texture;
        m_shininess_texture = m.m_shininess_texture;
        m_bump_map_texture = m.m_bump_map_texture;

        m_bumpyness = m.m_bumpyness;
        m_shininess = m.m_shininess;

        m_direct_lighting_factor = m.m_direct_lighting_factor;
        m_reflection_factor = m.m_reflection_factor;
        m_refraction_factor = m.m_refraction_factor;
        m_refractive_index = m.m_refractive_index;

    }
    return *this;
}

void model::set_ambient_color(const color &c) {
    m_ambient_color = c;
}

void model::set_diffuse_color(const color &c) {
    m_diffuse_color = c;
}

void model::set_specular_color(const color &c) {
    m_specular_color = c;
}

void model::set_color(const std::string colorname, const color &c) {
//  printf("Set color %s\n", colorname.c_str());
    if ((colorname[0] == 'a') || (colorname[0] == 'A'))
        set_ambient_color(c);
    else if ((colorname[0] == 'd') || (colorname[0] == 'D'))
        set_diffuse_color(c);
    else if ((colorname[0] == 's') || (colorname[0] == 'S'))
        set_specular_color(c);
}

texture &model::get_texture(const std::string &texname) {
    if ((texname[0] == 'a') || (texname[0] == 'A'))
        return m_ambient_texture;
    else if ((texname[0] == 'd') || (texname[0] == 'D'))
        return m_diffuse_texture;
    else if (((texname[0] == 's') || (texname[0] == 'S')) &&
        ((texname[1] == 'p') || (texname[1] == 'P')))
        return m_specular_texture;
    else if (((texname[0] == 's') || (texname[0] == 'S')) &&
        ((texname[1] == 'h') || (texname[1] == 'H')))
        return m_shininess_texture;
    else if ((texname[0] == 'b') || (texname[0] == 'B'))
        return m_bump_map_texture;
    else throw std::string("no texture " + texname);
}

const texture &model::get_texture(const std::string &texname) const {
    return ((model*)this)->get_texture(texname);
}


bool model::set_texture(const std::string texname, const char *filename) {
//  printf("Set texture %s -> %s\n", texname.c_str(), filename);
    try {
        texture &tex(get_texture(texname));
        tex.load(filename);
        tex.set_enabled(tex.valid());
        return tex.valid();
    }
    catch (const std::string &) {
        return false;
    }
}

void model::set_texture_enabled(const std::string texname, bool e) {
//  printf("Set texture %s enable -> %d\n", texname.c_str(), e);
    try {
        texture &tex(get_texture(texname));
        tex.set_enabled(e);
    }
    catch (const std::string &) {
    }
}



void model::set_shininess(mv::Float s) {
    m_shininess = s;
}

void model::set_bumpyness(mv::Float b) {
    m_bumpyness = b;
}

void model::set_direct_lighting_factor(mv::Float dlf) {
    m_direct_lighting_factor = dlf;
}

void model::set_reflection_factor(mv::Float rf) {
    m_reflection_factor = rf;
}

void model::set_refraction_factor(mv::Float rf) {
    m_refraction_factor = rf;
}

void model::set_refractive_index(mv::Float ri) {
    m_refractive_index = ri;
}

void model::set_mesh(const mesh *m) {
    m_mesh = m;
}

void model::set_default_material_colors() {
    m_ambient_color.set(0.5, 0.5, 0.5);
    m_diffuse_color.set(0.5, 0.5, 0.5);
    m_specular_color.set(0.5, 0.5, 0.5);
}

void model::to_OpenGL() const {
    // multiply current GL matrix with transform
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    apply_transform_OpenGL();

    // set material
    get_ambient_color().glMaterial(GL_FRONT_AND_BACK, GL_AMBIENT);
    get_diffuse_color().glMaterial(GL_FRONT_AND_BACK, GL_DIFFUSE);
    get_specular_color().glMaterial(GL_FRONT_AND_BACK, GL_SPECULAR);
    color(0.0, 0.0, 0.0, 1.0).glMaterial(GL_FRONT_AND_BACK, GL_EMISSION);
    glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, (GLfloat)get_shininess());

    // get mesh:
    const mesh *Mesh = get_mesh();

    // draw all faces:
    glBegin(GL_TRIANGLES);

    for (int i = 0; i < Mesh->get_nb_faces(); i++) {

        // get indices of the three faces:
        for (int j = 0; j < 3; j++) {
            // get index of vertex
            int idx = Mesh->get_face(i).get_vtx_idx(j);

            // call vertex to set normal and provide vertex:
            Mesh->get_vertex(idx).glNormal();
            Mesh->get_vertex(idx).glVertex();
        }
    }
    glEnd();

    // undo the transform:
    glPopMatrix();
}

void model::set_ambient_texture(const std::string &filename) {
    m_ambient_texture.load(filename);
}

void model::set_diffuse_texture(const std::string &filename) {
    m_diffuse_texture.load(filename);
}

void model::set_specular_texture(const std::string &filename) {
    m_specular_texture.load(filename);
}

void model::set_shininess_texture(const std::string &filename) {
    m_shininess_texture.load(filename);
}

void model::set_bump_map_texture(const std::string &filename) {
    m_bump_map_texture.load(filename);
}

std::string model::to_string(const std::string &basepath) const {
    std::string result;

    const int l = 1024;
    char rel[l];

    result += "model_begin\n";

    // xf
    result += "model_xf " + locator::to_string() + "\n";

    // mesh
    if (m_mesh) {
        fl_filename_relative_to(rel, l, m_mesh->get_filename().c_str(), basepath.c_str());
        result += std::string("mesh \"") + rel + "\"\n";
    }

    result += "model_ambient_color "+ m_ambient_color.to_string() + "\n";
    result += "model_diffuse_color "+ m_diffuse_color.to_string() + "\n";
    result += "model_specular_color "+ m_specular_color.to_string() + "\n";
    result += "shininess " + ::to_string(get_shininess()) + "\n";
    result += "bumpyness " + ::to_string(get_bumpyness()) + "\n";

    const char *names[] = {
        "ambient",
        "diffuse",
        "specular",
        "shininess",
        "bump_map",
        NULL
    };

    for (int i = 0; names[i]; i++) {
        if (get_texture(names[i]).get_filename().size()) {
            fl_filename_relative_to(rel, l, get_texture(names[i]).get_filename().c_str(), basepath.c_str());

            result += std::string(names[i]) + "_texture \"" + rel + "\"\n";
            result += std::string(names[i]) + std::string("_texture_enabled ") + (get_texture(names[i]).enabled() ? "true" : "false") + "\n";
        }
    }
        /*
    // ambient
    result += "ambient_texture \"" + m_ambient_texture.get_filename() + "\"\n";
    result += std::string("ambient_texture_enabled ") + (m_ambient_texture.enabled() ? "true" : "false") + "\n";

    // diffuse
    result += "diffuse_texture \"" + m_diffuse_texture.get_filename() + "\"\n";
    result += std::string("diffuse_texture_enabled ") + (m_diffuse_texture.enabled() ? "true" : "false") + "\n";

    // specular
    result += "specular_texture \"" + m_specular_texture.get_filename() + "\"\n";
    result += std::string("specular_texture_enabled ") + (m_specular_texture.enabled() ? "true" : "false") + "\n";

    // shininess 
    result += "shininess_texture \"" + m_shininess_texture.get_filename() + "\"\n";
    result += std::string("shininess_texture_enabled ") + (m_shininess_texture.enabled() ? "true" : "false") + "\n";

    // bumpmap
    result += "bump_map_texture \"" + m_bump_map_texture.get_filename() + "\"\n";
    result += std::string("bump_map_texture_enabled ") + (m_bump_map_texture.enabled() ? "true" : "false") + "\n";
*/

    // factors & indices
    result += "direct_lighting_factor " + ::to_string(m_direct_lighting_factor) + "\n";
    result += "reflection_factor " + ::to_string(m_reflection_factor) + "\n";
    result += "refraction_factor " + ::to_string(m_refraction_factor) + "\n";
    result += "refractive_index " + ::to_string(m_refractive_index) + "\n";

    result += "model_end\n";

    return result;
}


sphere model::get_bounding_sphere() const {
    return _sphere(-dual(get_xf() * get_mesh()->get_bounding_dual_sphere() * get_xfi()));
}


bool model::find_intersection(const ray &R, surface_point &spt, bool find_closest) const {
    ray ray_local(_normalizedFlatPoint(apply_om(get_Mi(), R.get_position())),
        _freeVector(apply_om(get_Mi(), R.get_direction())));

    // transform global distance to local frame (apply xf twice, because distance is _squared_)
    spt.set_distance_local(_Float(apply_om(get_Mi(), apply_om(get_Mi(), spt.get_distance() ^ ni)) & ni));

    if (get_mesh()->find_intersection(ray_local, spt, find_closest)) {
        spt.set_model(this);

        // transform local distance to global frame (apply xf twice, because distance is _squared_)
        spt.set_distance(_Float(apply_om(get_M(), apply_om(get_M(), (spt.get_distance_local() ^ ni))) & ni));
        return true;
    }
    else return false;
}


void model::compute_surface_point_details(surface_point &spt) const {
    // retrieve mesh, face, vertices
    const mesh &m(*get_mesh());
    const face &fc(m.get_face(spt.get_face_idx()));

    // let the face compute what it can
    fc.compute_surface_point_details(spt, *this); // NULL will later become pointer to bumpmap?

    // ambient color / texture lookup 
    if (get_ambient_texture().enabled()) {
        spt.set_ambient_color(get_ambient_color() * get_ambient_texture().get_color(spt.get_tex_pt()));
    }
    else spt.set_ambient_color(get_ambient_color());

    // diffuse color / texture lookup 
    if (get_diffuse_texture().enabled())
        spt.set_diffuse_color(get_diffuse_color() * get_diffuse_texture().get_color(spt.get_tex_pt()));
    else spt.set_diffuse_color(get_diffuse_color());

    // specular color / texture lookup 
    if (get_specular_texture().enabled())
        spt.set_specular_color(get_specular_color() * get_specular_texture().get_color(spt.get_tex_pt()));
    else spt.set_specular_color(get_specular_color());

    if (get_shininess_texture().enabled()) {
        spt.set_shininess(get_shininess() * get_shininess_texture().get_grayscale(spt.get_tex_pt()));
    }
    else spt.set_shininess(get_shininess());

    // copy lighting factors
    spt.set_direct_lighting_factor(get_direct_lighting_factor());
    spt.set_reflection_factor(get_reflection_factor());
    spt.set_refraction_factor(get_refraction_factor());
    spt.set_refractive_index(get_refractive_index());

    // transform intersection point and surface attitude into the global frame:
    spt.set_pt(_normalizedFlatPoint(apply_om(get_M(), spt.get_pt_local())));
    spt.set_att(_freeBivector(unit_e(apply_om(get_M(), spt.get_att_local()))));
}