/*
 * (C) Copyright Ian D. Romanick 2007
 * All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * on the rights to use, copy, modify, merge, publish, distribute, sub
 * license, and/or sell copies of the Software, and to permit persons to whom
 * the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.  IN NO EVENT SHALL
 * AUTHORS, COPYRIGHT HOLDERS, AND/OR THEIR SUPPLIERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
 * USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# define _USE_MATH_DEFINES
# include <windows.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <GL/glew.h>

#include "vector.h"
#include "primitives.h"


/**
 * Create buffer objects for the vertex and element data in the primitive.
 */
void idr_primitive::create_vbos()
{
    if (GLEW_ARB_vertex_buffer_object) {
        GLuint buffs[2];

        glGenBuffersARB(2, buffs);
        data_vbo = buffs[0];
        index_vbo = buffs[1];

        glBindBufferARB(GL_ARRAY_BUFFER_ARB, data_vbo);
        glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_vbo);

        glBufferData(GL_ARRAY_BUFFER_ARB, stride * total_verts, data,
                     GL_STATIC_DRAW);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER_ARB,
                     element_size * total_elements, elements, GL_STATIC_DRAW);

        delete data;
        delete elements;
        data = NULL;
        index_16 = NULL;
        index_32 = NULL;
        elements = NULL;

        glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
        glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
    }
}


/**
 * Create a torus.
 *
 * \param inner_radius  Radius of the inside of the torus.
 * \param circle_radius Radius of the solid part of the torus.
 * \param sides         Number of sides for each radial section.
 * \param rings         Number of radial sections.
 * \param tanget_attribute  Generic vertex attribute to be used for the
 *                          per-vertex tangent vector.  If 0, generation of
 *                          tangent vectors is disabled.
 */
idr_torus::idr_torus(GLdouble ir, GLdouble cr, GLint sides, GLint rings)
{
    init();

    if (sides < 3) {
        sides = 3;
    }

    if (rings < 3) {
        rings = 3;
    }


    inner_radius = ir;
    circle_radius = cr;


    /* The center of solid part of the torus is located at the offset of the
     * inner radius plus the radius of the solid part.
     */
    const double center = inner_radius + circle_radius;


    /* Total number of unique vertices.
     */
    total_verts = rings * sides;
    stride = sizeof(GLfloat) * (3 + 3 + 3);
    data = new GLfloat[(3 + 3 + 3) * total_verts];


    /* The 2D circles only have two coordinates (X and Y) per vertex, and
     * there are "sides" vertexes.
     */
    const unsigned circle_2d_size = sides * 2;


    /* Allocate enough memory to store a basic 2D circle.
     */
    float *base_circle = new float[circle_2d_size];
    if (base_circle == NULL) {
        return;
    }

    unsigned i;
    unsigned j;
    double theta;
    double delta_theta;



    /* A torus is a surface of revolution.  This means that it is generated
     * rotating some base shape, in this case a circle, around an axis.  The
     * first step is to generate the coordinates of the base shape.
     */
    theta = 0.0;
    delta_theta = 2.0 * M_PI / (double) sides;
    for (i = 0; i < (unsigned) sides; i++) {
        base_circle[(i * 2) + 0] = (float)sin(theta);
        base_circle[(i * 2) + 1] = (float)cos(theta);

        theta += delta_theta;
    }


    /* To draw a set of quads, we need a pair of 3D circles.  To save work,
     * we calculate the ith segment and reuse the (i-1)th segment.  This is
     * why there are two of each 3D array (see above).  next_segement is the
     * index of the array to be calculated.  This toggles between 0 and 1 on
     * successive passes.
     */

    GLfloat *vert = data;
    theta = 0.0;
    delta_theta = 2.0 * M_PI / (double) rings;
    for (i = 0; i < (unsigned) rings; i++) {
        const double sin_t = sin(theta);
        const double cos_t = cos(theta);

        idr_vec4 bitangent((float)sin(theta - M_PI_2), 0.0f, (float)cos(theta - M_PI_2), 0.0f);

        for (j = 0; j < (unsigned) sides; j++) {
            double r = (base_circle[(j * 2) + 0] * circle_radius) + center;

            vert[0] = float(sin_t * r);
            vert[1] = base_circle[(j * 2) + 1] * float(circle_radius);
            vert[2] = float(cos_t * r);

            vert[3] = float(sin_t) * base_circle[(j * 2) + 0];
            vert[4] = base_circle[(j * 2) + 1];
            vert[5] = float(cos_t) * base_circle[(j * 2) + 0];

            const idr_vec4 n(& vert[3]);
            const idr_vec4 t(n.cross(bitangent));

            vert[6] = t[0];
            vert[7] = t[1];
            vert[8] = t[2];

            vert += 9;
        }

        theta += delta_theta;
    }

    delete base_circle;

    draw_calls = rings;
    elements_per_draw_call = 2 * (sides + 1);

    total_elements = draw_calls * elements_per_draw_call;

    if (total_verts < ((1 << 16) - 1)) {
        index_16 = new GLushort[total_elements];

        element_size = sizeof(GLushort);
        element_enum = GL_UNSIGNED_SHORT;
        elements = (GLubyte *)index_16;

        unsigned idx_offset = 0;
        for (i = 0; i < draw_calls; i++) {
            const unsigned k = i * sides;
            const unsigned next_k = ((i + 1) % rings) * sides;

            for (j = 0; j < (unsigned) sides; j++) {
                index_16[idx_offset++] = j + next_k;
                index_16[idx_offset++] = j + k;
            }

            index_16[idx_offset++] = next_k;
            index_16[idx_offset++] = k;
        }
    }
    else {
        index_32 = new GLuint[total_elements];

        element_size = sizeof(GLuint);
        element_enum = GL_UNSIGNED_INT;
        elements = (GLubyte *)index_32;

        unsigned idx_offset = 0;
        for (i = 0; i < draw_calls; i++) {
            const unsigned k = i * sides;
            const unsigned next_k = ((i + 1) % rings) * sides;

            for (j = 0; j < (unsigned) sides; j++) {
                index_32[idx_offset++] = j + next_k;
                index_32[idx_offset++] = j + k;
            }

            index_32[idx_offset++] = next_k;
            index_32[idx_offset++] = k;
        }
    }

    position_offset = 0;
    normal_offset = 3;
    tangent_offset = 6;

    create_vbos();
}


void idr_torus::draw(GLuint tangent_attribute)
{
    if (data_vbo != 0) {
        glBindBufferARB(GL_ARRAY_BUFFER_ARB, data_vbo);
        glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_vbo);
    }

    glVertexPointer(3, GL_FLOAT, stride, data + position_offset);
    glNormalPointer(GL_FLOAT, stride, data + normal_offset);
    if (tangent_attribute) {
        glVertexAttribPointerARB(tangent_attribute, 3, GL_FLOAT, GL_FALSE,
                                 stride, data + tangent_offset);
    }

    for (unsigned i = 0; i < draw_calls; i++) {
        glDrawRangeElements(GL_QUAD_STRIP, 0, total_verts,
                            elements_per_draw_call, element_enum,
                            & elements[element_size * i * elements_per_draw_call]);
    }

    if (data_vbo != 0) {
        glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
        glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
    }
}


void idr_torus::bounding_box(idr_vec4 *c1, idr_vec4 *c2)
{
    *c1 = idr_vec4(float(inner_radius + circle_radius),
                   float(inner_radius + circle_radius),
                   float(circle_radius),
                   0.0f);
    *c2 = -c1[0];
}


/**
 * Draw a solid sphere.
 *
 * \param radius            Radius of the sphere.
 * \param slices            Number of radial sections.
 * \param stacks            Number of sides for each radial section.
 * \param tanget_attribute  Generic vertex attribute to be used for the
 *                          per-vertex tangent vector.  If 0, generation of
 *                          tangent vectors is disabled.
 */
idr_sphere::idr_sphere(GLdouble r, GLint slices, GLint stacks)
{
    init();

    if (stacks < 4) {
        stacks = 4;
    }
    
    if (slices < 4) {
        slices = 4;
    }

    radius = r;

    /* The 2D circles only have two coordinates (X and Y) per vertex, and
     * there are "stacks" vertexes.
     */
    const unsigned circle_2d_size = (stacks + 1) * 2;


    /* Total number of unique vertices.
     */
    total_verts = (stacks + 1) * slices;
    stride = sizeof(GLfloat) * (3 + 3 + 3);
    data = new GLfloat[(3 + 3 + 3) * total_verts];


    /* Allocate enough memory to store a 2D circle.
     */
    float *base_circle = new float[circle_2d_size];
    if (base_circle == NULL) {
        return;
    }


    int i;
    int j;
    double theta;
    double delta_theta;



    /* A sphere is a surface of revolution.  This means that it is generated
     * rotating some base shape, in this case a half-circle, around an axis.
     * The first step is to generate the coordinates of the base shape.
     */
    theta = 0.0;
    delta_theta = M_PI / (double) stacks;
    for (i = 0; i < (stacks + 1); i++) {
        base_circle[(i * 2) + 0] = float(sin(theta));
        base_circle[(i * 2) + 1] = float(cos(theta));

        theta += delta_theta;
    }


    /* To draw a set of quads, we need a pair of 3D circles.  To save work,
     * we calculate the ith segment and reuse the (i-1)th segment.  This is
     * why there are two of each 3D array (see above).  next_segement is the
     * index of the array to be calculated.  This toggles between 0 and 1 on
     * successive passes.
     */

    GLfloat *vert = data;
    theta = 0.0;
    delta_theta = 2.0 * M_PI / (double) slices;
    for (i = 0; i < slices; i++) {
        const double sin_t = sin(theta);
        const double cos_t = cos(theta);

        idr_vec4 bitangent(float(sin(theta - M_PI_2)), 0.0f, float(cos(theta - M_PI_2)), 0.0f);


        for (j = 0; j < stacks + 1; j++) {
            double r = (base_circle[(j * 2) + 0] * radius);

            vert[0] = float(sin_t * r);
            vert[1] = base_circle[(j * 2) + 1] * float(radius);
            vert[2] = float(cos_t * r);

            vert[3] = float(sin_t) * base_circle[(j * 2) + 0];
            vert[4] = base_circle[(j * 2) + 1];
            vert[5] = float(cos_t) * base_circle[(j * 2) + 0];

            const idr_vec4 n(& vert[3]);
            const idr_vec4 t(n.cross(bitangent));

            vert[6] = t[0];
            vert[7] = t[1];
            vert[8] = t[2];

            vert += 9;
        }

        theta += delta_theta;
    }

    delete base_circle;


    quad_draw_calls = slices;
    elements_per_quad_draw_call = (stacks - 1) * 2;

    elements_per_tri_draw_call = 2 * 3 * slices;


    total_elements = (quad_draw_calls * elements_per_quad_draw_call)
        + elements_per_tri_draw_call;

    if (total_verts < ((1 << 16) - 1)) {
        index_16 = new GLushort[total_elements];

        element_size = sizeof(GLushort);
        element_enum = GL_UNSIGNED_SHORT;
        elements = (GLubyte *)index_16;

        unsigned idx_offset = 0;
        for (i = 0; i < (int) quad_draw_calls; i++) {
            const unsigned k =        i                * (stacks + 1);
            const unsigned next_k = ((i + 1) % slices) * (stacks + 1);

            for (j = 1; j < stacks; j++) {
                index_16[idx_offset++] = j + next_k;
                index_16[idx_offset++] = j + k;
            }
        }

        for (j = 0; j < slices; j++) {
            const unsigned k =        j                * (stacks + 1);
            const unsigned next_k = ((j + 1) % slices) * (stacks + 1);

            index_16[idx_offset++] = k;
            index_16[idx_offset++] = k + 1;
            index_16[idx_offset++] = next_k + 1;

            index_16[idx_offset++] = next_k + (stacks - 1);
            index_16[idx_offset++] = k + (stacks - 1);
            index_16[idx_offset++] = k + (stacks);
        }
    }
    else {
        index_32 = new GLuint[total_elements];

        element_size = sizeof(GLuint);
        element_enum = GL_UNSIGNED_INT;
        elements = (GLubyte *)index_32;

        unsigned idx_offset = 0;
        for (i = 0; i < (int) quad_draw_calls; i++) {
            const unsigned k =        i                * (stacks + 1);
            const unsigned next_k = ((i + 1) % slices) * (stacks + 1);

            for (j = 1; j < stacks; j++) {
                index_32[idx_offset++] = j + next_k;
                index_32[idx_offset++] = j + k;
            }
        }

        for (j = 0; j < slices; j++) {
            const unsigned k =        j                * (stacks + 1);
            const unsigned next_k = ((j + 1) % slices) * (stacks + 1);

            index_32[idx_offset++] = k;
            index_32[idx_offset++] = k + 1;
            index_32[idx_offset++] = next_k + 1;

            index_32[idx_offset++] = next_k + (stacks - 1);
            index_32[idx_offset++] = k + (stacks - 1);
            index_32[idx_offset++] = k + (stacks);
        }
    }

    position_offset = 0;
    normal_offset = 3;
    tangent_offset = 6;

    create_vbos();
}


void idr_sphere::draw(GLuint tangent_attribute)
{
    if (data_vbo != 0) {
        glBindBufferARB(GL_ARRAY_BUFFER_ARB, data_vbo);
        glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_vbo);
    }

    glVertexPointer(3, GL_FLOAT, stride, data + position_offset);
    glNormalPointer(GL_FLOAT, stride, data + normal_offset);
    if (tangent_attribute) {
        glVertexAttribPointerARB(tangent_attribute, 3, GL_FLOAT, GL_FALSE,
                                 stride, data + tangent_offset);
    }

    for (unsigned i = 0; i < quad_draw_calls; i++) {
        glDrawRangeElements(GL_QUAD_STRIP, 0, total_verts,
                            elements_per_quad_draw_call, element_enum,
                            & elements[element_size * i * elements_per_quad_draw_call]);
    }

    glDrawRangeElements(GL_TRIANGLES, 0, total_verts,
                        elements_per_tri_draw_call, element_enum,
                        & elements[element_size * quad_draw_calls * elements_per_quad_draw_call]);

    if (data_vbo != 0) {
        glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
        glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
    }
}


void idr_sphere::bounding_box(idr_vec4 *c1, idr_vec4 *c2)
{
    *c1 = idr_vec4(float(radius), float(radius), float(radius), 0.0f);
    *c2 = -c1[0];
}


idr_plane::idr_plane(const GLfloat *point, const GLfloat *normal,
                     const GLfloat *tangent)
{
    init();

    p = idr_vec4(point[0], point[1], point[2], 0.0);
    n = idr_vec4(normal[0], normal[1], normal[2], 0.0);
    t = idr_vec4(tangent[0], tangent[1], tangent[2], 0.0);
    b = n.cross(t);
}


idr_plane::idr_plane(const idr_vec4 &point, const idr_vec4 &normal,
                     const idr_vec4 &tangent)
{
    init();

    p = point;
    n = normal;
    t = tangent;
    b = n.cross(t);
}


void idr_plane::projection_matrix(const idr_vec4 &l, GLfloat factor, 
                                  GLfloat *m)
{
    const float d = -n.dot4(p) - factor;
    const float n_dot_l = n.dot4(l);


    m[0] = n_dot_l + d - l[0] * n[0];
    m[1] = -l[1] * n[0];
    m[2] = -l[2] * n[0];
    m[3] = -n[0];

    m[4] = -l[0] * n[1];
    m[5] = n_dot_l + d - l[1] * n[1];
    m[6] = -l[2] * n[1];
    m[7] = -n[1];

    m[8]  = -l[0] * n[2];
    m[9]  = -l[1] * n[2];
    m[10] =  n_dot_l + d - l[2] * n[2];
    m[11] = -n[2];

    m[12] = -l[0] * d;
    m[13] = -l[1] * d;
    m[14] = -l[2] * d;
    m[15] = n_dot_l;
}


void idr_plane::projection_matrix(const GLfloat *light, GLfloat factor, 
                                  GLfloat *m)
{
    projection_matrix(idr_vec4(light[0], light[1], light[2], 0.0),
                      factor, m);
}


void idr_plane::draw(GLuint tangent_attribute)
{
    glNormal3f(n[0], n[1], n[2]);

    if (tangent_attribute != 0) {
        glVertexAttrib3f(tangent_attribute, t[0], t[1], t[2]);
    }

    idr_vec4 A(-t + b);
    idr_vec4 B(-t - b);
    idr_vec4 C(t - b);
    idr_vec4 D(t + b);

    A *= 100000.0;
    B *= 100000.0;
    C *= 100000.0;
    D *= 100000.0;
    
    A += p;
    B += p;
    C += p;
    D += p;

    glBegin(GL_QUADS);
    glVertex4f(A[0], A[1], A[2], 1);
    glVertex4f(B[0], B[1], B[2], 1);
    glVertex4f(C[0], C[1], C[2], 1);
    glVertex4f(D[0], D[1], D[2], 1);
    glEnd();
}


void idr_plane::bounding_box(idr_vec4 *c1, idr_vec4 *c2)
{
    *c1 = idr_vec4(0.0, 0.0, 0.0, 0.0);
    *c2 = -c1[0];
}
