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

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

#include <GL/gl.h>

#include "c3ga_util.h"
#include "light_source.h"
#include "ray.h"
#include "scene.h"
#include "surface_point.h"
#include "../shared.src/string_util.h"
#include "../shared.src/intersection_info.h"

using namespace c3ga;

light_source::light_source() :
    m_spot_exponent(0.0f),
    m_spot_cutoff(100.0f),
    m_constant_attenuation(1.0f),
    m_linear_attenuation(0.0f),
    m_quadratic_attenuation(0.0f)  {

}

    light_source::light_source(const EXForm &xf,
        const color &ambient_color,
        const color &diffuse_color,
        const color &specular_color,
        mv::Float spot_exponent,
        mv::Float spot_cutoff,
        mv::Float constant_attenuation,
        mv::Float linear_attenuation,
        mv::Float quadratic_attenuation) : locator(xf),
    m_ambient_color(ambient_color),
    m_diffuse_color(diffuse_color),
    m_specular_color(specular_color),
    m_spot_exponent(spot_exponent),
    m_spot_cutoff(spot_cutoff),
    m_constant_attenuation(constant_attenuation),
    m_linear_attenuation(linear_attenuation),
    m_quadratic_attenuation(quadratic_attenuation) {
}

light_source::light_source(const light_source &l) : locator(l),
    m_ambient_color(l.m_ambient_color),
    m_diffuse_color(l.m_diffuse_color),
    m_specular_color(l.m_specular_color),
    m_spot_exponent(l.m_spot_exponent),
    m_spot_cutoff(l.m_spot_cutoff),
    m_constant_attenuation(l.m_constant_attenuation),
    m_linear_attenuation(l.m_linear_attenuation),
    m_quadratic_attenuation(l.m_quadratic_attenuation) {
}

light_source::~light_source() {
}

light_source &light_source::operator=(const light_source &l) {
    if (this != & l) {
        locator::operator=(l);
        m_ambient_color = l.m_ambient_color;
        m_diffuse_color = l.m_diffuse_color;
        m_specular_color = l.m_specular_color;
        m_spot_exponent = l.m_spot_exponent;
        m_spot_cutoff = l.m_spot_cutoff;
        m_constant_attenuation = l.m_constant_attenuation;
        m_linear_attenuation = l.m_linear_attenuation;
        m_quadratic_attenuation = l.m_quadratic_attenuation;
    }
    return *this;
}

void light_source::to_OpenGL(int OpenGL_light_idx) const {
    // compute position of light, set GL_POSITION
//  flatPoint light_pos = _flatPoint(get_xf() * noni *get_xfi());
    flatPoint light_pos = _flatPoint(apply_om(get_M(), noni));
    GLfloat gl_light_pos[4];
    gl_light_pos[0] = _float(light_pos & e1ni);
    gl_light_pos[1] = _float(light_pos & e2ni);
    gl_light_pos[2] = _float(light_pos & e3ni);
    gl_light_pos[3] = _float(light_pos & noni);
    glLightfv(OpenGL_light_idx, GL_POSITION, gl_light_pos);

    // set GL ambient, diffuse, specular light color:
    get_ambient_color().glLight(OpenGL_light_idx, GL_AMBIENT);
    get_diffuse_color().glLight(OpenGL_light_idx, GL_DIFFUSE);
    get_specular_color().glLight(OpenGL_light_idx, GL_SPECULAR);

    // set atternuation:
    glLightf(OpenGL_light_idx, GL_CONSTANT_ATTENUATION, (float)get_constant_attenuation());
    glLightf(OpenGL_light_idx, GL_LINEAR_ATTENUATION, (float)get_linear_attenuation());
    glLightf(OpenGL_light_idx, GL_QUADRATIC_ATTENUATION, (float)get_quadratic_attenuation());

    // get the direction of the light (it 'shines' along -e3)
//  freeVector light_dir = _freeVector(get_xf() * nie3 *get_xfi());
    freeVector light_dir = _freeVector(apply_om(get_M(), get_xfi()));
    GLfloat gl_light_dir[4];
    gl_light_dir[0] = _float(light_dir & e1ni);
    gl_light_dir[1] = _float(light_dir & e2ni);
    gl_light_dir[2] = _float(light_dir & e3ni);
    gl_light_dir[3] = _float(light_dir & noni); // always 0

    // set spot properties:
    glLightf(OpenGL_light_idx, GL_SPOT_CUTOFF,
        (GLfloat)((get_spot_cutoff() > 95.0f) ? 180.0f :
        (GLfloat)((get_spot_cutoff() > 90.0f) ? 90.0f : get_spot_cutoff())));
    glLightf(OpenGL_light_idx, GL_SPOT_EXPONENT, (GLfloat)get_spot_exponent());
    glLightfv(OpenGL_light_idx, GL_SPOT_DIRECTION, gl_light_dir);

    // done
}

void light_source::to_OpenGL_model() const {
    int i;
    // todo: clean up this dull mess a little if possible

    // multiply current GL matrix with transform
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    apply_transform_OpenGL();

    // set material
    color(0.0f, 0.0f, 0.0f, 1.0f).glMaterial(GL_FRONT_AND_BACK, GL_AMBIENT);
    color(0.0f, 0.0f, 0.0f, 1.0f).glMaterial(GL_FRONT_AND_BACK, GL_DIFFUSE);
    color(0.0f, 0.0f, 0.0f, 1.0f).glMaterial(GL_FRONT_AND_BACK, GL_SPECULAR);
    color(0.6f, 0.6f, 0.6f, 1.0f).glMaterial(GL_FRONT_AND_BACK, GL_EMISSION);
    glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 0.0);

    // coordinates for a little box (yawn):
    const float scale = 0.1f;
    GLfloat vertices[8][3] = {
        {scale *-1.0f, scale *-1.0f, scale *0.0f},
        {scale *1.0f, scale *-1.0f, scale *0.0f},
        {scale *1.0f, scale *1.0f, scale *0.0f},
        {scale *-1.0f, scale *1.0f, scale *0.0f},
        {scale *-1.0f, scale *-1.0f, scale *2.0f},
        {scale *1.0f, scale *-1.0f, scale *2.0f},
        {scale *1.0f, scale *1.0f, scale *2.0f},
        {scale *-1.0f, scale *1.0f, scale *2.0f}
    };
    int vtx_idx[6][4] = {
        {0, 1, 2, 3},
        {1, 5, 6, 2},
        {4, 7, 6, 5},
        {0, 3, 7, 4},
        {0, 4, 5, 1},
        {2, 6, 7, 3}
    };
    GLfloat normals[6][3] = {
        {0.0f, 0.0f, -1.0f},
        {1.0f, 0.0f, 0.0f},
        {0.0f, 0.0f, 1.0f},
        {-1.0f, 0.0f, 0.0f},
        {0.0f, -1.0f, 0.0f},
        {0.0f, 1.0f, 0.0f},
    };

    // draw a little box . . .
    glBegin(GL_QUADS);
    for ( i = 0; i < 6; i++) {
        glNormal3fv(normals[i]);
        for (int j = 3; j >= 0; j--) {
            glVertex3fv(vertices[vtx_idx[i][j]]);
        }
    }
    glEnd();

    mv::Float cutoff = (mv::Float)M_PI * ((get_spot_cutoff() > 90.0f) ? 90.0f : get_spot_cutoff()) / 180.0f;

    // draw flaps that indicate the width of the spotlight:
    float _c = -(float)sin(cutoff);
    float c[4][2] = {
    {0.0f, _c},
    {-_c, 0.0f},
    {0.0f, -_c},
    {_c, 0.0f}
    };

    float s = (float)cos(cutoff);
    const float scale2 = 2.0f * scale;
    glBegin(GL_QUADS);
    for (i = 0; i < 4; i++) {
        int j = (i+1)%4;
        glVertex3fv(vertices[i]);
        glVertex3fv(vertices[j]);
        glVertex3f(vertices[j][0] + scale2 * c[i][0], vertices[j][1] + scale2 * c[i][1], vertices[j][2] - scale2 * s);
        glVertex3f(vertices[i][0] + scale2 * c[i][0], vertices[i][1] + scale2 * c[i][1], vertices[i][2] - scale2 * s);

        glVertex3f(vertices[i][0] + scale2 * c[i][0], vertices[i][1] + scale2 * c[i][1], vertices[i][2] - scale2 * s);
        glVertex3f(vertices[j][0] + scale2 * c[i][0], vertices[j][1] + scale2 * c[i][1], vertices[j][2] - scale2 * s);
        glVertex3fv(vertices[j]);
        glVertex3fv(vertices[i]);
    }
    glEnd();

    // undo the transform:
    glPopMatrix();
}

bool light_source::can_scale() const {
    return false;
}


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

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

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

void light_source::set_color(const std::string colorname, const color &c) {
    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);
}

std::string light_source::to_string() const {
    std::string result;

    result += "light_source_begin\n";

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

    // spot
    result += "spot_exponent " + ::to_string(m_spot_exponent) + "\n";
    result += "spot_cutoff " + ::to_string(m_spot_cutoff) + "\n";

    // attenuation
    result += "constant_attenuation " + ::to_string(m_constant_attenuation) + "\n";
    result += "linear_attenuation " + ::to_string(m_linear_attenuation) + "\n";
    result += "quadratic_attenuation " + ::to_string(m_quadratic_attenuation) + "\n";

    // color
    result += "light_source_ambient_color "+ m_ambient_color.to_string() + "\n";
    result += "light_source_diffuse_color "+ m_diffuse_color.to_string() + "\n";
    result += "light_source_specular_color "+ m_specular_color.to_string() + "\n";

    result += "light_source_end\n";

    return result;
}

void  light_source::set_spot_exponent(mv::Float f) {
    m_spot_exponent = f;
}

void light_source::set_spot_cutoff(mv::Float c) {
    if (c < 0.0f) m_spot_cutoff = 0.0f;
    else if (c > 100.0f) m_spot_cutoff = 100.0f;
    else m_spot_cutoff = c;
}

void  light_source::set_constant_attenuation(mv::Float f) {
    m_constant_attenuation = f;
}

void  light_source::set_linear_attenuation(mv::Float f) {
    m_linear_attenuation = f;
}

void  light_source::set_quadratic_attenuation(mv::Float f) {
    m_quadratic_attenuation = f;
}


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

    // get 'viewing direction' towards the light as a unit free vector
    const freeVector view_dir = _freeVector(-R.get_direction());

    // get direction from surface point to light as unit free vector
    freeVector light_dir = _freeVector(unit_e(get_position() - spt.get_pt()));

    // evaluate the contribution of every light and add it to 'c'
    // is the light visible?
    bool light_visble;
    if (II) light_visble = (II->get_light_visible(S.get_light_index(this)));
    else {
        surface_point tmp_spt;
        // spawn a shadow ray:
        light_visble = !S.find_intersection(ray(spt.get_pt(), light_dir), tmp_spt, false); // false means: don't search for closest point
    }

    color C; // this is where the shading computations are accumulated

    if (light_visble) { // perform diffuse & specular lighting computations
        // compute the diffuse term
        float fd = _float(dual(spt.get_att()) & light_dir);
        if (fd < 0.0f) fd = 0.0f;
        const float terminator_hack_threshold = 0.1f; // hack to limit the 'terminator effect'
        if (fd < terminator_hack_threshold) fd = (fd * fd * fd) / (terminator_hack_threshold * terminator_hack_threshold);
        //printf("fd = %f,\n", fd);

        // add diffuse term to 'C'
        C += fd * get_diffuse_color() * spt.get_diffuse_color();

        // compute the specular term
        freeVector half_dir = _freeVector(unit_e(view_dir + light_dir));
        float fs = _float(dual(spt.get_att()) & half_dir);
        fs = (fs < 0.0f) ? 0.0f : (float)pow(fs, (float)spt.get_shininess());

        // add specular term to 'C'
        C += fs * get_specular_color() * spt.get_specular_color();
    }

    // add ambient term to 'C'
    C += get_ambient_color() * spt.get_ambient_color();

    // spotlight (matches OpenGL: it applies spotlight to ambient too).
    if (get_spot_cutoff() < 95.0) {
        // get cutoff in radians; get cosine of cutoff
        float cutoff = (get_spot_cutoff() < 90.0f) ? (float)get_spot_cutoff() : 90.0f;
        float cutoff_rad = (float)M_PI * cutoff / 180.0f;

        // compute the direction the spot is shining in, as a free vector:
        freeVector spot_dir = _freeVector(apply_om(get_M(), e3ni));

        // compute the cosine of the angle between the light_dir and spot_dir
        float cos_angle = _float(light_dir & spot_dir); // todo: simply use Euclidean version of inner product . . .  (&)
        // the point lies outside the cone
        if (cos_angle < cos(cutoff_rad)) cos_angle = 0.0f;
        // apply spotlight to color
        C *= (float)pow(cos_angle, (float)get_spot_exponent());
    }

    /*
    Attenuation:
    -compute distance of light source to surface point
    -scale 'C' by the attenuation factor
    */
    float distance = (float)flat_point_distance(get_position(), spt.get_pt());
    C *= 1.0f / ((float)get_constant_attenuation() +
        (float)get_linear_attenuation() * distance +
        (float)get_quadratic_attenuation() * distance * distance);
    return C;
}