#include "particle_batch.hpp"

#include <iostream>

// TODO: use VAO's as soon as this is working

int getDivisorForIndex(size_t index)
{
    // 0 or 1?
    return (index == 0) ? 0 : 3;
}

namespace endofthejedi {
    ParticleBatch::ParticleBatch(size_t id, size_t numParticles, float particleSize, float halfAge)
        : m_id(id)
        , m_numParticles(numParticles)
        , m_particleRadius(particleSize)
        , m_age(0.0)
        , m_halfAge(halfAge)

        // XXX this is used in some places in the shader.
        // set it via uniform too
        , m_maxNumHalfAges(5.0)

        , m_maxVelocity(1.0)
        , m_center(glm::vec3(0.0f, 0.0f, 0.0f))

        // 2d quad drawn as a triangle fan.
        //
        // TODO
        // it is transformed before uploading so it looks at the camera
        , m_data_geometry({
                0.5f, -0.5f,
                0.5f,  0.5f,
               -0.5f,  0.5f,
               -0.5f, -0.5f})
    {
        m_num_vertex_buffers = 4;

        //std::cout<<"[ParticleBatch] create for " << numParticles << " num particles" << std::endl;
        m_attr_locations.resize(m_num_vertex_buffers);
        m_data_vbos.resize(m_num_vertex_buffers);

        m_data_position.resize(m_numParticles);
        m_data_velocity.resize(m_numParticles);
        m_data_max_distance.resize(m_numParticles);

        for (size_t i=0; i<m_num_vertex_buffers; i++) {
            m_attr_locations[i] = -1;
            m_data_vbos[i]      =  0;
        }
    }

    ParticleBatch::~ParticleBatch()
    {
        // TODO: find out if stuff must be deallocated
        glDeleteBuffers(m_num_vertex_buffers, m_data_vbos.data());
    }

    void ParticleBatch::setup(Shader *shader)
    {
        static const char *names[] = {
            "in_geometry",
            "in_position",
            "in_velocity",
            "in_maxDist"
        };

        for (size_t i=0; i<m_num_vertex_buffers; i++) {
            const char *name = names[i];
            GLint loc = glGetAttribLocation(shader->program(), name);
            m_attr_locations[i] = loc;
            //std::cout<<"attr location for " << name << " (#"  << i << ") is " << loc << std::endl;
        }
    }

    void ParticleBatch::setCenter(const glm::vec3 &center)
    {
        m_center = center;
    }

    void ParticleBatch::setMaxVelocity(float maxVelocity)
    {
        m_maxVelocity = maxVelocity;
    }

    void ParticleBatch::setParticle(size_t index, const glm::vec3 &p, const glm::vec3 &v, float maxDist)
    {
        if (index >= m_numParticles) {
            return;
        }

        //std::cout<<"[ParticleBatch] setParticle " << index << std::endl;

        m_data_position[index]      = p;
        m_data_velocity[index]      = v;
        m_data_max_distance[index]  = maxDist;
    }

    void ParticleBatch::bind()
    {
        //std::cout<<"[ParticleBatch] bind" << std::endl;

        for (size_t i=0; i<m_num_vertex_buffers; i++) {
            //std::cout<<"vbo #" << i << ": " << m_data_vbos[i] << std::endl;
            glEnableVertexAttribArray(m_attr_locations[i]);
            glBindBuffer(GL_ARRAY_BUFFER, m_data_vbos[i]);

            glVertexAttribDivisor(m_attr_locations[i], getDivisorForIndex(i));

            // TODO: i or index? at first argument?
            glVertexAttribPointer(
                    m_attr_locations[i],
                    dataSizeForIndex(i),
                    GL_FLOAT,  // Data is floating point type
                    GL_FALSE,  // No fixed point scaling
                    0,         // stride: no
                    NULL);     // No offset
        }
    }

    void ParticleBatch::upload()
    {
        //std::cout<<"[ParticleBatch] upload to vbo's " << std::endl;

        glGenBuffers(m_num_vertex_buffers, m_data_vbos.data());            // Generate buffer

        for (size_t i=0; i<m_num_vertex_buffers; i++) {
            size_t bufferDataSize = dataSizeForIndex(i) * m_numParticles * sizeof(float);

            glEnableVertexAttribArray(i);
            glBindBuffer(GL_ARRAY_BUFFER, m_data_vbos[i]);

            // fill buffer with the loaded mesh position data
            glBufferData(
                    GL_ARRAY_BUFFER,        // Buffer target
                    bufferDataSize,         // Buffer data size
                    dataSourceForIndex(i),  // Buffer data pointer
                    GL_STATIC_DRAW);        // Usage - Data never changes;
        }
    }

    void ParticleBatch::render(Shader *shader)
    {
        //std::cout<<"[ParticleBatch] render " << std::endl;
        glUniform1f(shader->location("age"),         m_age);
        glUniform1f(shader->location("maxVelocity"), m_maxVelocity);
        glUniform1f(shader->location("halfAge"),     m_halfAge);
        glUniform1f(shader->location("size"),        m_particleRadius);
        glUniform3f(shader->location("explCenter"),  m_center.x, m_center.y, m_center.z);

        bind();

        //glDrawArrays(GL_TRIANGLE_FAN, 0, 4*m_numParticles);

        //glDrawArraysInstanced( GLenum mode, GLint first, GLsizei count, GLsizei primcount);
        // XXX the 3 is a magical number.
        // I dont know why this works
        // without it, it will render 1/3 of the particles
        glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, 3*m_numParticles);

#if 0
        glBegin(GL_QUADS);
        for (size_t index=0; index<m_numParticles; index++) {
            for (size_t i=0; i<4; i++) {
                glm::vec2 p = m_data_position[index] + triangles[i];
                //glColor3f(1.0, 0.0, 0.0);
                glVertex2f(p.x, p.y);
            }
        }
        glEnd();
#endif

    }

    size_t ParticleBatch::dataSizeForIndex(size_t index)
    {
        switch(index) {
            case 0:
                return 2;

            case 1:
            case 2:
                return 3;

            case 3:
                return 1;

            default:
                std::cerr << "dataSizeForIndex() bad" << std::endl;
                exit(-1);
        }
    }

    void *ParticleBatch::dataSourceForIndex(size_t index)
    {
        switch(index) {
            case 0: return (void *) m_data_geometry.data();
            case 1: return (void *) m_data_position.data();
            case 2: return (void *) m_data_velocity.data();
            case 3: return (void *) m_data_max_distance.data();
            default:
                std::cerr << "dataSourceForIndex() bad" << std::endl;
                exit(-1);
        }
    }

    void ParticleBatch::tick(float dt)
    {
        m_age += dt;
    }

    bool ParticleBatch::done() const
    {
        return m_age >= m_maxNumHalfAges*m_halfAge;
    }
}
