#include "polygon_model.hpp"

#include <iostream>

namespace endofthejedi {
    PolygonModel::PolygonModel(const std::string &filename)
        : m_filename(filename)
    {
        clear();
    }

    // TODO: add change/set shader function because it stays for all calls of an
    // instance the same.
    void PolygonModel::setup(Shader *shader)
    {
        if (shader == nullptr) {
            std::cout<<"shader is NULL" << std::endl;
            exit(-1);
            return;
        }

        if (shader->program() == 0) {
            std::cout<<"program is invalid" << std::endl;
            exit(-1);
            return;
        }

        //std::cout<<"program: " << shader->program() << std::endl;

        m_shader = shader;

        // TODO error checks
        const char *names[] = {
            "in_vertex",
            "in_normal"
        };

        discardLastGlError();

        for (int i=0; i<2; i++) {
            const char *name = names[i];
            GLint loc = glGetAttribLocation(m_shader->program(), name);
            m_attr_locations[i] = loc;

            checkAndPrintGlError();

            if (m_attr_locations[i] == -1) {
                std::cerr<<"[polygonmodel] warning: attribute location #"
                    << i << " (for '" << name
                    << "') is invalid!" << std::endl;

            } else {
                //std::cout<<"[polygonmodel] attribute location #"
                //    << i << " (for '" << name
                //    << "') is " << m_attr_locations[i] << std::endl;
            }
        }
    }

    bool PolygonModel::bind()
    {
        if (!m_loaded_to_opengl) {
            std::cout<<"[polygonmodel] warning: try to bind model vbo "
                << "which was not uploaded to OpenGL!" << std::endl;

            exit(-1);
            return false;
        }

        // bind position vbo
        for (int i=0; i<2; i++) {
            glEnableVertexAttribArray(m_attr_locations[0]);
            glBindBuffer(GL_ARRAY_BUFFER, m_vbo_id[i]);
            glVertexAttribPointer(m_attr_locations[i], 3, GL_FLOAT, GL_FALSE, 0, NULL);
            glVertexAttribDivisor(m_attr_locations[i], 0); // restore default
        }

        m_binding_active = true;

        return true;
    }

    bool PolygonModel::render()
    {
        if (!m_binding_active || !m_loaded_to_opengl) {
            std::cout<<"[polygonmodel] warning: try to render model without bind()" << std::endl;
            exit(-1);
            return false;
        }

        glDrawArrays(GL_TRIANGLES, 0, m_numVertices);
        return true;
    }

    bool PolygonModel::copyVertices(const aiScene *scene)
    {
        if (scene->mMeshes == 0) {
            std::cout << "[polygonmodel : no meshes loaded for " << m_filename << std::endl;
            return false;
        }

        aiNode *node = scene->mRootNode;
        const aiMesh* mesh = scene->mMeshes[node->mMeshes[0]];

        // 3 vertices per face, 3 floats per vertex
        m_numVertices = mesh->mNumFaces*3;

        m_data_position.reserve(m_numVertices);
        m_data_normal.reserve(m_numVertices);

        size_t t, i;
        for (t=0; t<mesh->mNumFaces; ++t) {
            const aiFace* face = &mesh->mFaces[t];
            if (face->mNumIndices != 3) {
                std::cout << "[polygonmodel] need triangles, got something different with: "
                    << face->mNumIndices  << " vertices" << std::endl;

                return false;
            }

            for (i=0; i<face->mNumIndices; i++) {
                const size_t index = face->mIndices[i];
                m_data_position.push_back(mesh->mVertices[index].x);
                m_data_position.push_back(mesh->mVertices[index].y);
                m_data_position.push_back(mesh->mVertices[index].z);

                m_data_normal.push_back(mesh->mNormals[index].x);
                m_data_normal.push_back(mesh->mNormals[index].y);
                m_data_normal.push_back(mesh->mNormals[index].z);

                //std::cout<<"adding normal: " << mesh->mNormals[index].x  << " " << mesh->mNormals[index].y  << " " << mesh->mNormals[index].z  << std::endl;
            }
        }

        size_t totalBytes = 3*m_numVertices*sizeof(float);
        std::cout<<"[polygonmodel] loaded file " << m_filename
            << " with "  << m_numVertices << " vertices ("
            << totalBytes << " bytes)" << std::endl;

        return true;
    }

    bool PolygonModel::uploadToOpenGl()
    {
        if (!m_loaded_from_file) {
            std::cerr<<"[polygonmodel] warning: try to upload model data "
                << "to OpenGL but no data is loaded!" << std::endl;

            exit(-1);
            return false;
        }

        glGenBuffers(2, m_vbo_id);            // Generate buffer

        glEnableVertexAttribArray(m_attr_locations[0]);
        glBindBuffer(GL_ARRAY_BUFFER, m_vbo_id[0]); // Bind buffer

        glVertexAttribPointer(
                m_attr_locations[0],
                3,         // three floats per vertex
                GL_FLOAT,  // Data is floating point type
                GL_FALSE,  // No fixed point scaling
                0,         // stride: no
                NULL);     // No offset

        // fill buffer with the loaded mesh position data
        glBufferData(
                GL_ARRAY_BUFFER,                    // Buffer target
                3 * m_numVertices * sizeof(float),  // Buffer data size
                m_data_position.data(),             // Buffer data pointer
                GL_STATIC_DRAW);                    // Usage - Data never changes;

        /************************************************************/
        /* Normal data */
        /************************************************************/

        glEnableVertexAttribArray(m_attr_locations[1]);
        glBindBuffer(GL_ARRAY_BUFFER, m_vbo_id[1]); // Bind buffer

        glVertexAttribPointer(
            m_attr_locations[1],
            3,         // three floats per normal
            GL_FLOAT,  // Data is floating point type
            GL_FALSE,  // No fixed point scaling
            0,         // stride: no
            NULL);     // No offset

        // Fill bound buffer
        glBufferData(
            GL_ARRAY_BUFFER,                   // Buffer target
            3 * m_numVertices * sizeof(float), // Buffer data size
            m_data_normal.data(),              // Buffer data pointer
            GL_STATIC_DRAW);                   // Usage - Data never changes;

        m_loaded_to_opengl = true;

        return true;
    }

    bool PolygonModel::import()
    {
        clear();

        // Create an instance of the Importer class
        Assimp::Importer importer;
        // And have it read the given file with some example postprocessing
        // Usually - if speed is not the most important aspect for you - you'll
        // propably to request more postprocessing than we do in this example.
        const aiScene* scene = importer.ReadFile(m_filename,
                aiProcess_CalcTangentSpace       |
                aiProcess_Triangulate            |
                aiProcess_JoinIdenticalVertices  |
                aiProcess_SortByPType);

        // If the import failed, report it
        if (!scene) {
            std::cout<<"[polygonmodel] loading file "
                << m_filename << " failed with: "
                << importer.GetErrorString() << std::endl;

            return false;
        }

        // Now we can access the file's contents.
        copyVertices(scene);

        m_loaded_from_file = true;

        // We're done. Everything will be cleaned up by the importer destructor
        return true;
    }

    void PolygonModel::clear()
    {
        // TODO: delete buffers if there's data

        m_loaded_from_file = false;
        m_loaded_to_opengl = false;

        m_data_position.clear();
        m_data_normal.clear();

        for (int i=0; i<2; i++) {
            m_vbo_id[i] = -1;
            m_attr_locations[i] = -1;
        }

        m_numVertices     = 0;
    }
}
