#include "logcategories.h"
#include "mesh.h"

Mesh::SubdivEntry::SubdivEntry()
{
    VB_handle = 0xFFFFFFFF;
    VB_handle_irregular = 0xFFFFFFFF;
    IB_irregular_handle = 0xFFFFFFFF;
    IB_regular_handle = 0xFFFFFFFF;
}

Mesh::SubdivEntry::~SubdivEntry()
{
    qCDebug(log_mesh) << "FIXME: Delete Called: num vertex:" << vertices.size();
    /*f->glDeleteBuffers(1, &VB_handle);
    f->glDeleteBuffers(1, &IB_irregular_handle);
    f->glDeleteBuffers(1, &IB_regular_handle);
    */
}

void Mesh::SubdivEntry::updateIndices() {
    f->glDeleteBuffers(1, &IB_irregular_handle);
    f->glGenBuffers(1, &IB_irregular_handle);
    f->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IB_irregular_handle);
    if(indices_irregular.size() != 0)
        f->glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int) * indices_irregular.size(), &indices_irregular[0], GL_STATIC_DRAW);

    f->glDeleteBuffers(1, &IB_regular_handle);
    f->glGenBuffers(1, &IB_regular_handle);
    f->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IB_regular_handle);
    if(indices_regular.size() != 0)
        f->glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int) * indices_regular.size(), &indices_regular[0], GL_STATIC_DRAW);
}

void Mesh::SubdivEntry::init(QOpenGLFunctions_4_3_Core *f,QVector<Vertex>& Vertices,
                             QVector<unsigned int>& Indices_irregular){

    f->glGenBuffers(1, &VB_handle);
    f->glBindBuffer(GL_ARRAY_BUFFER, VB_handle);
    f->glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * Vertices.size(), &Vertices[0], GL_STATIC_DRAW);

    QVector<unsigned int> patches;
    this->init(f, VB_handle, Vertices, Vertices, Indices_irregular, patches);
}

void Mesh::SubdivEntry::init(QOpenGLFunctions_4_3_Core *f, GLuint VB_handle, QVector<Vertex>& Vertices, QVector<Vertex>& Vertices_irregular,
                             QVector<unsigned int>& Indices_irregular, QVector<unsigned int>& patches){
    this->f = f;
    this->VB_handle = VB_handle;

    vertices = Vertices;
    indices_irregular = Indices_irregular;
    indices_regular = patches;

    qCDebug(log_mesh) << "Vertices" << vertices.length();
    qCDebug(log_mesh) << "Vertices Irregular" << Vertices_irregular.length();
    qCDebug(log_mesh) << "Indices irregular" << indices_irregular.length();
    qCDebug(log_mesh) << "Indices patches" << patches.length();

    f->glGenBuffers(1, &VB_handle_irregular);
    f->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, VB_handle_irregular);
    if(Vertices_irregular.size() != 0)
        f->glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Vertex) * Vertices_irregular.size(), &Vertices_irregular[0], GL_STATIC_DRAW);

    f->glGenBuffers(1, &IB_irregular_handle);
    f->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IB_irregular_handle);
    if(indices_irregular.size() != 0)
        f->glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int) * indices_irregular.size(), &indices_irregular[0], GL_STATIC_DRAW);

    f->glGenBuffers(1, &IB_regular_handle);
    f->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IB_regular_handle);
    if(patches.size() != 0)
        f->glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int) * patches.size(), &patches[0], GL_STATIC_DRAW);
}


Mesh::MeshEntry::MeshEntry()
{
    materialIndex = 0xFFFFFFFF;
    double amax = std::numeric_limits<float>::max();
    min = QVector3D(amax,amax,amax);
    max = QVector3D(-amax,-amax,-amax);
}

void Mesh::MeshEntry::init(QOpenGLFunctions_4_3_Core *f,QVector<Vertex>& Vertices,
                           QVector<unsigned int>& Indices){
    this->f = f;

    buffers.resize(1);
    buffers[0].reset(new SubdivEntry);
    buffers[0]->init(f,Vertices,Indices);

    //calc AABB
    for (int i = 0; i < Vertices.size(); ++i) {
        Vertex v = Vertices[i];
        min.setX(qMin(min.x(),v.pos.x()));
        min.setY(qMin(min.y(),v.pos.y()));
        min.setZ(qMin(min.z(),v.pos.z()));

        max.setX(qMax(max.x(),v.pos.x()));
        max.setY(qMax(max.y(),v.pos.y()));
        max.setZ(qMax(max.z(),v.pos.z()));
    }
}

void Mesh::MeshEntry::update(GLuint VB_handle, QVector<Vertex>& Vertices,QVector<Vertex>& Vertices_Irregular, QVector<unsigned int>& Indices_irregular, QVector<unsigned int>& patches) {
    SubdivEntry *entry = new SubdivEntry;
    buffers.push_back(std::shared_ptr<SubdivEntry>(entry));
    entry->init(f, VB_handle, Vertices, Vertices_Irregular,Indices_irregular, patches);
}

Mesh::MeshEntry::~MeshEntry()
{
    //moved to subdiventry
}

Mesh::MaterialInfo::MaterialInfo()
{
    Name = QString("Default");
    Diffuse = QVector3D();
    Specular= QVector3D();
    hasTexture = false;
    texture = Texture();
    Shininess = 30;
}

Mesh::Node::Node()
{
    name = QString("DefaultNode");
    transformation.setToIdentity();
    meshes.clear();
    children.clear();
}

bool Mesh::Node::getFirstMeshIndex(int &index){
    if (meshes.length()>0){
        index = meshes[0];
        return true;
    }
    for (int i = 0; i < children.length(); i++){
        if (children[i].getFirstMeshIndex(index)){
            return true;
        }
    }
    return false;
}

Mesh::Mesh(QOpenGLFunctions_4_3_Core *f,QString fileName)
{
    loaded = false;
    this->f =f;

    scene = NULL;
    scene= importer.ReadFile(fileName.toStdString(),
                             //aiProcess_CalcTangentSpace |
                             aiProcess_GenSmoothNormals |
                             //aiProcess_JoinIdenticalVertices |
                             //aiProcess_SortByPType |
                             // ? aiProcess_FlipUVs |
                             aiProcess_JoinIdenticalVertices  |
                             aiProcess_Triangulate
                             );

    qCDebug(log_mesh) << "File Read";
    if( !scene)
    {
        qCDebug(log_mesh) << importer.GetErrorString();
    }else {
        QString dir = QFileInfo(fileName).path();

        //Init Materials
        materials.resize(scene->mNumMaterials);
        if(scene->HasMaterials()){
            //Init Materials
            for (unsigned int i = 0 ; i < scene->mNumMaterials ; i++) {
                const aiMaterial* material = scene->mMaterials[i];
                aiString mname;
                material->Get(AI_MATKEY_NAME, mname);
                initMaterial(dir, i, material);

                qCDebug(log_mesh) << "Loaded Material" << (i+1) << "/" << scene->mNumMaterials << ":" << mname.C_Str() << "Shininess" << materials[i].Shininess;
            }
        }
        qCDebug(log_mesh) << "Materials Read";

        //Init Mesh
        if(scene->HasMeshes())
        {
            entries.resize(scene->mNumMeshes);
            for (int i = 0 ; i < entries.size() ; i++) {
                aiMesh* meshEntry = scene->mMeshes[i];
                initMeshEntry(i, meshEntry);

            }
        } else{
            qCWarning(log_mesh) << "No Mesh found";
            return;
        }

        qCDebug(log_mesh) << "Mesh Read";

        //init Nodes
        if(scene->mRootNode != NULL)
        {
            initNode(scene, scene->mRootNode, rootNode, QString());
        } else {
            rootNode.transformation.setToIdentity();
            rootNode.children.resize(0);
            rootNode.meshes.resize(entries.length());

            for(int i = 0; i < entries.length(); ++i)
            {
                rootNode.meshes[i] = i;
            }
            qCDebug(log_mesh) << "No Root Node";
        }
        qCDebug(log_mesh) << "Nodes Read";

        globalInverseTransform = rootNode.transformation.inverted();

        double amax = std::numeric_limits<float>::max();
        QVector3D min = QVector3D(amax,amax,amax);
        QVector3D max = QVector3D(-amax,-amax,-amax);

        findObjectDimension(rootNode,QMatrix4x4(),min,max);
        qCDebug(log_mesh) << "AABB" << min << max;

        float dist = qMax(max.x() - min.x(), qMax(max.y()-min.y(), max.z() - min.z()));
        float sc = 100.0/dist;
        QVector3D center = (max - min)/2;
        QVector3D trans = -(max - center);

        // Apply the scale and translation to a matrix
        screenTransform.setToIdentity();
        screenTransform.scale(sc);
        screenTransform.translate(trans);

        loaded = true;
    }
}

Mesh::Mesh(QOpenGLFunctions_4_3_Core *f, Mesh *mesh, QVector<Vertex> &vertex_buffer, QVector<GLuint> &index_buffer)
{
    loaded = false;
    this->f = mesh->f;

    scene = mesh->scene;

    //Init Materials
    for (int i = 0; i < mesh->materials.size(); i++) {
        materials.push_back(mesh->materials[i]);
    }

    entries.resize(1);
    entries[0].name = mesh->entries[0].name;
    entries[0].materialIndex = mesh->entries[0].materialIndex;
    entries[0].init(f, vertex_buffer, index_buffer);

    rootNode.name = mesh->rootNode.name;
    rootNode.transformation = mesh->rootNode.transformation;
    rootNode.meshes.resize(1);
    rootNode.meshes[0] = 0;

    globalInverseTransform = rootNode.transformation.inverted();

    double amax = std::numeric_limits<float>::max();
    QVector3D min = QVector3D(amax,amax,amax);
    QVector3D max = QVector3D(-amax,-amax,-amax);

    findObjectDimension(rootNode,QMatrix4x4(),min,max);
    qCDebug(log_mesh) << "AABB" << min << max;

    float dist = qMax(max.x() - min.x(), qMax(max.y()-min.y(), max.z() - min.z()));
    float sc = 100.0/dist;
    QVector3D center = (max - min)/2;
    QVector3D trans = -(max - center);

    // Apply the scale and translation to a matrix
    screenTransform.setToIdentity();
    screenTransform.scale(sc);
    screenTransform.translate(trans);

    loaded = true;
}

Mesh::~Mesh()
{
    entries.clear();
    materials.clear();
}

Mesh::Node Mesh::getRootNode(){
    return rootNode;
}

Mesh::MeshEntry *Mesh::getMeshEntry(int index){
    return &entries[index];
}

void Mesh::initNode(const aiScene *scene, aiNode *node, Node &newNode,QString debugoffset){
    newNode.name = node->mName.length != 0 ? node->mName.C_Str() : "";
    newNode.transformation = QMatrix4x4(node->mTransformation[0]);

    newNode.meshes.resize(node->mNumMeshes);
    if(debug) qCDebug(log_mesh) << debugoffset.toStdString().c_str() << "NodeName" << newNode.name;
    if(debug) qCDebug(log_mesh) << debugoffset.toStdString().c_str() << "  NumMeshes" << newNode.meshes.size();

    for(uint i = 0; i < node->mNumMeshes; ++i)
    {
        newNode.meshes[i] = node->mMeshes[i];
        if(debug) qCDebug(log_mesh) << debugoffset.toStdString().c_str() << "    MeshName" << entries[newNode.meshes[i]].name;
    }

    if(debug) qCDebug(log_mesh) << debugoffset.toStdString().c_str() << "  NumChildren" << node->mNumChildren;
    QString newDebug = QString(debugoffset).append("    ");
    for(uint i = 0; i < node->mNumChildren; ++i)
    {
        newNode.children.push_back(Node());
        initNode(scene, node->mChildren[i], newNode.children[i],newDebug);
    }
}

void Mesh::initMaterial(QString dir, unsigned int i, const aiMaterial* material)
{
    aiString mname;
    material->Get(AI_MATKEY_NAME, mname);
    if (mname.length > 0)
        materials[i].Name = QString(mname.C_Str());

    aiColor3D dif(0.f,0.f,0.f);
    aiColor3D amb(0.f,0.f,0.f);
    aiColor3D spec(0.f,0.f,0.f);
    float shine = 0.0;

    material->Get(AI_MATKEY_COLOR_AMBIENT, amb);
    material->Get(AI_MATKEY_COLOR_DIFFUSE, dif);
    material->Get(AI_MATKEY_COLOR_SPECULAR, spec);
    material->Get(AI_MATKEY_SHININESS, shine);

    materials[i].Diffuse = QVector3D(dif.r, dif.g, dif.b);
    materials[i].Specular = QVector3D(spec.r, spec.g, spec.b);
    materials[i].Shininess = shine;

    if (materials[i].Shininess == 0.0){
        materials[i].Shininess = 2;
    }

    if (material->GetTextureCount(aiTextureType_DIFFUSE) > 0) {
        aiString Path;

        if (material->GetTexture(aiTextureType_DIFFUSE, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS) {
            QString temp = QString(Path.data);
            temp = temp.split("\\").last();
            temp = temp.split("/").last();
            std::string FullPath = dir.toStdString() + "/" + temp.toStdString();
            materials[i].texture.Load(GL_TEXTURE_2D, QString(FullPath.c_str()));
            materials[i].hasTexture = true;
        } else{
            qCWarning(log_mesh) << "Warning No Texture";
            materials[i].hasTexture = false;
        }
    }
}

void Mesh::initMeshEntry(int index, aiMesh * entry) {
    QVector<Vertex> Vertices;
    QVector<unsigned int> Indices;

    const aiVector3D Zero3D(0.0f, 0.0f, 0.0f);

    for (unsigned int i = 0; i < entry->mNumVertices; i++) {
        const aiVector3D* pPos      = &(entry->mVertices[i]);
        const aiVector3D* pNormal   = &(entry->mNormals[i]);
        const aiVector3D* pTexCoord = entry->HasTextureCoords(0) ? &(entry->mTextureCoords[0][i]) : &Zero3D;

        //possible: nuuv Channels, TAngents and Bitangents

        Vertex v(QVector3D(pPos->x, pPos->y, pPos->z),
                 QVector3D(pNormal->x, pNormal->y, pNormal->z),
                 QVector2D(pTexCoord->x, pTexCoord->y));

        Vertices.push_back(v);
    }

    for (unsigned int i = 0; i < entry->mNumFaces; i++) {
        const aiFace& face = entry->mFaces[i];
        assert(face.mNumIndices == 3);
        Indices.push_back(face.mIndices[0]);
        Indices.push_back(face.mIndices[1]);
        Indices.push_back(face.mIndices[2]);
    }

    entries[index].name = entry->mName.length != 0 ? entry->mName.C_Str() : "";
    entries[index].materialIndex = entry->mMaterialIndex;

    qCDebug(log_mesh) << "Loaded Mesh:" << entries[index].name << "HasBones:" << entry->HasBones();

    entries[index].init(f,Vertices, Indices);
}

void Mesh::render(QOpenGLShaderProgram *shader, QMatrix4x4 V,QMatrix4x4 P, int subdivision, bool regular){
    if(!loaded)
        return;
    if(f == NULL){
        qCCritical(log_mesh) << "OpenGL functions == null";
        return;
    }

    f->glEnableVertexAttribArray(positionIndex);
    f->glEnableVertexAttribArray(normalIndex);
    f->glEnableVertexAttribArray(uvIndex);

    renderNode(shader,rootNode,V*screenTransform,P,QMatrix4x4(),subdivision, regular);

    f->glDisableVertexAttribArray(positionIndex);
    f->glDisableVertexAttribArray(normalIndex);
    f->glDisableVertexAttribArray(uvIndex);
}

void Mesh::renderNode(QOpenGLShaderProgram *shader, Node &node, QMatrix4x4 V,QMatrix4x4 P,QMatrix4x4 M, int subdivision, bool regular){
    M *= node.transformation;

    QMatrix4x4 MV = V*M;
    QMatrix4x4 MVP = P*MV;

    shader->setUniformValue("MVP",MVP);
    shader->setUniformValue("MV",MV);

    for (int i = 0 ; i < node.meshes.size() ; i++) {
        int index = node.meshes[i];
        //load Material
        renderMesh(shader, index,subdivision, regular);
    }

    for (int i = 0 ; i < node.children.size() ; i++) {
        renderNode(shader,node.children[i],V,P,M, subdivision,regular);
    }
}

void Mesh::renderMesh(QOpenGLShaderProgram *shader, int index, int subdivision, bool regular)
{
    int MaterialIndex = entries[index].materialIndex;
    if (MaterialIndex < materials.size()) {
        // qCDebug(log_mesh) << materials[MaterialIndex].Diffuse << materials[MaterialIndex].Specular << materials[MaterialIndex].Shininess << materials[MaterialIndex].hasTexture;

        if (regular ) {
            // shader->setUniformValue("materialInfo.Diffuse", QVector3D(0.0f, 0.0f, 1.0f));
            shader->setUniformValue("materialInfo.Diffuse", QVector3D(243.0, 156.0, 18.0)/255.0);
            // shader->setUniformValue("materialInfo.Diffuse", QVector/3D(1.0f, 1.0f, 1.0f));
        } else {
            shader->setUniformValue("materialInfo.Diffuse", materials[MaterialIndex].Diffuse);
        }
        shader->setUniformValue("materialInfo.Specular",materials[MaterialIndex].Specular);
        shader->setUniformValue("materialInfo.Shininess",materials[MaterialIndex].Shininess);
        shader->setUniformValue("materialInfo.hasTexture",materials[MaterialIndex].hasTexture);

        if(materials[MaterialIndex].hasTexture)
            materials[MaterialIndex].texture.bind(f,GL_TEXTURE0);
    }

    if(entries[index].buffers.size() <= subdivision){
        subdivision = entries[index].buffers.size()-1;
    }

    std::shared_ptr<SubdivEntry> entry = entries[index].buffers[subdivision];

    // Draw Vertex Array
    if(regular){
        for (int depth = 0; depth < subdivision+1; ++depth) {
            std::shared_ptr<SubdivEntry> depthEntry = entries[index].buffers[depth];
            if (!depthEntry->indices_regular.isEmpty()) {

                std::shared_ptr<SubdivEntry> depthEntryMinusOne = entries[index].buffers[depth];
                if(depth > 0){
                  //  depthEntryMinusOne = entries[index].buffers[depth-1];
                }

                shader->setUniformValue("depth",depth);

                f->glBindBuffer(GL_ARRAY_BUFFER, depthEntryMinusOne->VB_handle);
                f->glVertexAttribPointer(positionIndex, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);

                f->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, depthEntry->IB_regular_handle);
                f->glPatchParameteri(GL_PATCH_VERTICES, 12);
                f->glDrawElements(GL_PATCHES, depthEntry->indices_regular.size(), GL_UNSIGNED_INT, 0);
               // qCDebug(log_mesh_render)<<"Patches "<<entry->indices_regular.size()/12<<" Subdivision Level: "<< subdivision;
            }
        }
    } else {
        if (!entry->indices_irregular.isEmpty()) {
            f->glBindBuffer(GL_ARRAY_BUFFER, entry->VB_handle_irregular);
            f->glVertexAttribPointer(positionIndex, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);

            f->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, entry->IB_irregular_handle);
            f->glPatchParameteri(GL_PATCH_VERTICES, 3);
            f->glDrawElements(GL_PATCHES, entry->indices_irregular.size(), GL_UNSIGNED_INT, 0);
        }
    }
}

void Mesh::findObjectDimension(Node node, QMatrix4x4 transform, QVector3D &min, QVector3D &max){
    transform *= node.transformation;

    for (int i = 0; i < node.meshes.size(); i++) {
        QVector4D temp = transform*QVector4D(entries[node.meshes[i]].min,1.0);
        min.setX(qMin(min.x(),temp.x()));
        min.setY(qMin(min.y(),temp.y()));
        min.setZ(qMin(min.z(),temp.z()));
        max.setX(qMax(max.x(),temp.x()));
        max.setY(qMax(max.y(),temp.y()));
        max.setZ(qMax(max.z(),temp.z()));

        temp = transform*QVector4D(entries[node.meshes[i]].max,1.0);
        min.setX(qMin(min.x(),temp.x()));
        min.setY(qMin(min.y(),temp.y()));
        min.setZ(qMin(min.z(),temp.z()));
        max.setX(qMax(max.x(),temp.x()));
        max.setY(qMax(max.y(),temp.y()));
        max.setZ(qMax(max.z(),temp.z()));
    }

    for (int i = 0; i < node.children.size(); i++) {
        findObjectDimension(node.children[i], transform, min, max);
    }
}
