/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2020 Oleg Endo <olegendo@gcc.gnu.org>
 * Copyright (C) 2015-2020 Mario Luzeiro <mrluzeiro@ua.pt>
 * Copyright (C) 2015-2020 KiCad Developers, see AUTHORS.txt for contributors.
 *
 * 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, you may find one here:
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * or you may search the http://www.gnu.org website for the version 2 license,
 * or you may write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

/**
 * @file  c_ogl_3dmodel.cpp
 * @brief
 */
#include <stdexcept>
#include <gal/opengl/kiglew.h>    // Must be included first

#include "3d_model.h"
#include "ogl_legacy_utils.h"
#include "../common_ogl/ogl_utils.h"
#include "../3d_math.h"
#include <wx/debug.h>
#include <wx/log.h>
#include <chrono>
#include <memory>


/*
 * Flag to enable connectivity profiling.
 *
 * @ingroup trace_env_vars
 */
const wxChar* MODEL_3D::m_logTrace = wxT( "KI_TRACE_EDA_OGL_3DMODEL" );


void MODEL_3D::MakeBbox( const BBOX_3D& aBox, unsigned int aIdxOffset, VERTEX* aVtxOut,
                         GLuint* aIdxOut, const glm::vec4& aColor )
{
    aVtxOut[0].m_pos = { aBox.Min().x, aBox.Min().y, aBox.Min().z };
    aVtxOut[1].m_pos = { aBox.Max().x, aBox.Min().y, aBox.Min().z };
    aVtxOut[2].m_pos = { aBox.Max().x, aBox.Max().y, aBox.Min().z };
    aVtxOut[3].m_pos = { aBox.Min().x, aBox.Max().y, aBox.Min().z };

    aVtxOut[4].m_pos = { aBox.Min().x, aBox.Min().y, aBox.Max().z };
    aVtxOut[5].m_pos = { aBox.Max().x, aBox.Min().y, aBox.Max().z };
    aVtxOut[6].m_pos = { aBox.Max().x, aBox.Max().y, aBox.Max().z };
    aVtxOut[7].m_pos = { aBox.Min().x, aBox.Max().y, aBox.Max().z };

    for( unsigned int i = 0; i < 8; ++i )
        aVtxOut[i].m_color = aVtxOut[i].m_cad_color = glm::clamp( aColor * 255.0f, 0.0f, 255.0f );

    #define bbox_line( vtx_a, vtx_b )\
         do { *aIdxOut++ = vtx_a + aIdxOffset; \
              *aIdxOut++ = vtx_b + aIdxOffset; } while( 0 )

    bbox_line( 0, 1 );
    bbox_line( 1, 2 );
    bbox_line( 2, 3 );
    bbox_line( 3, 0 );

    bbox_line( 4, 5 );
    bbox_line( 5, 6 );
    bbox_line( 6, 7 );
    bbox_line( 7, 4 );

    bbox_line( 0, 4 );
    bbox_line( 1, 5 );
    bbox_line( 2, 6 );
    bbox_line( 3, 7 );

    #undef bbox_line
}


MODEL_3D::MODEL_3D( const S3DMODEL& a3DModel, MATERIAL_MODE aMaterialMode )
{
    wxLogTrace( m_logTrace, wxT( "MODEL_3D::MODEL_3D %u meshes %u materials" ),
                static_cast<unsigned int>( a3DModel.m_MeshesSize ),
                static_cast<unsigned int>( a3DModel.m_MaterialsSize ) );

    auto start_time = std::chrono::high_resolution_clock::now();

    // Validate a3DModel pointers
    wxASSERT( a3DModel.m_Materials != nullptr );
    wxASSERT( a3DModel.m_Meshes != nullptr );
    wxASSERT( a3DModel.m_MaterialsSize > 0 );
    wxASSERT( a3DModel.m_MeshesSize > 0 );

    m_materialMode = aMaterialMode;

    if( a3DModel.m_Materials == nullptr || a3DModel.m_Meshes == nullptr
      || a3DModel.m_MaterialsSize == 0 || a3DModel.m_MeshesSize == 0 )
        return;

    // create empty bbox for each mesh.  it will be updated when the vertices are copied.
    m_meshes_bbox.resize( a3DModel.m_MeshesSize );

    // copy materials for later use during rendering.
    m_materials.reserve( a3DModel.m_MaterialsSize );

    for( unsigned int i = 0; i < a3DModel.m_MaterialsSize; ++i )
        m_materials.emplace_back( a3DModel.m_Materials[i] );

    // build temporary vertex and index buffers for bounding boxes.
    // the first box is the outer box.
    std::vector<VERTEX> bbox_tmp_vertices( ( m_meshes_bbox.size() + 1 ) * bbox_vtx_count );
    std::vector<GLuint> bbox_tmp_indices( ( m_meshes_bbox.size() + 1 ) * bbox_idx_count );

    // group all meshes by material.
    // for each material create a combined vertex and index buffer.
    // some models might have many sub-meshes.  so iterate over the
    // input meshes only once.
    struct MESH_GROUP
    {
        std::vector<VERTEX> m_vertices;
        std::vector<GLuint> m_indices;
    };

    std::vector<MESH_GROUP> mesh_groups( m_materials.size() );

    for( unsigned int mesh_i = 0; mesh_i < a3DModel.m_MeshesSize; ++mesh_i )
    {
        const auto& mesh = a3DModel.m_Meshes[mesh_i];

        // silently ignore meshes that have invalid material references or invalid geometry.
        if( mesh.m_MaterialIdx >= m_materials.size()
          || mesh.m_Positions == nullptr
          || mesh.m_FaceIdx == nullptr
          || mesh.m_Normals == nullptr
          || mesh.m_FaceIdxSize == 0
          || mesh.m_VertexSize == 0 )
          continue;

        auto& mesh_group = mesh_groups[mesh.m_MaterialIdx];
        auto& material = m_materials[mesh.m_MaterialIdx];

        if( material.IsTransparent()
                && m_materialMode != MATERIAL_MODE::DIFFUSE_ONLY )
            m_have_transparent_meshes = true;
         else
            m_have_opaque_meshes = true;

        const unsigned int vtx_offset = mesh_group.m_vertices.size();
        mesh_group.m_vertices.resize( mesh_group.m_vertices.size() + mesh.m_VertexSize );

        // copy vertex data and update the bounding box.
        // use material color for mesh bounding box or some sort of average vertex color.
        glm::vec3 avg_color = material.m_Diffuse;

        for( unsigned int vtx_i = 0; vtx_i < mesh.m_VertexSize; ++vtx_i )
        {
            m_meshes_bbox[mesh_i].Union( mesh.m_Positions[vtx_i] );

            auto& vtx_out = mesh_group.m_vertices[vtx_offset + vtx_i];

            vtx_out.m_pos = mesh.m_Positions[vtx_i];
            vtx_out.m_nrm = glm::clamp( glm::vec4( mesh.m_Normals[vtx_i], 1.0f ) * 127.0f,
                                        -127.0f, 127.0f );

            vtx_out.m_tex_uv = mesh.m_Texcoords != nullptr
                               ? mesh.m_Texcoords[vtx_i]
                               : glm::vec2 (0);

            if( mesh.m_Color != nullptr )
            {
              avg_color = ( avg_color + mesh.m_Color[vtx_i] ) * 0.5f;

              vtx_out.m_color =
                  glm::clamp( glm::vec4( mesh.m_Color[vtx_i],
                                         1 - material.m_Transparency ) * 255.0f,
                              0.0f, 255.0f );

              vtx_out.m_cad_color =
                  glm::clamp( glm::vec4( MaterialDiffuseToColorCAD( mesh.m_Color[vtx_i] ),
                                         1 ) * 255.0f, 0.0f, 255.0f );
            }
            else
            {
                // the mesh will be rendered with other meshes that might have
                // vertex colors.  thus, we can't enable/disable vertex colors
                // for individual meshes during rendering.

                // if there are no vertex colors, use material color instead.
                vtx_out.m_color =
                    glm::clamp( glm::vec4( material.m_Diffuse,
                                           1 - material.m_Transparency ) * 255.0f,
                                0.0f, 255.0f );

                vtx_out.m_cad_color =
                    glm::clamp( glm::vec4 ( MaterialDiffuseToColorCAD( material.m_Diffuse ),
                                            1 ) * 255.0f,
                                0.0f, 255.0f );
            }
        }

        if( m_meshes_bbox[mesh_i].IsInitialized() )
        {
            // generate geometry for the bounding box
            MakeBbox( m_meshes_bbox[mesh_i], ( mesh_i + 1 ) * bbox_vtx_count,
                      &bbox_tmp_vertices[( mesh_i + 1 ) * bbox_vtx_count],
                      &bbox_tmp_indices[( mesh_i + 1 ) * bbox_idx_count],
                      { avg_color, 1.0f } );

            // bump the outer bounding box
            m_model_bbox.Union( m_meshes_bbox[mesh_i] );
        }


        // append indices of this mesh to the mesh group.
        const unsigned int idx_offset = mesh_group.m_indices.size();
        unsigned int use_idx_count = mesh.m_FaceIdxSize;

        if( use_idx_count % 3 != 0 )
        {
            wxLogTrace( m_logTrace, wxT( "  index count %u not multiple of 3, truncating" ),
                        static_cast<unsigned int>( use_idx_count ) );
            use_idx_count = ( use_idx_count / 3 ) * 3;
        }

        mesh_group.m_indices.resize( mesh_group.m_indices.size() + use_idx_count );

        for( unsigned int idx_i = 0; idx_i < use_idx_count; ++idx_i )
        {
            if( mesh.m_FaceIdx[idx_i] >= mesh.m_VertexSize )
            {
                wxLogTrace( m_logTrace, wxT( " index %u out of range (%u)" ),
                            static_cast<unsigned int>( mesh.m_FaceIdx[idx_i] ),
                            static_cast<unsigned int>( mesh.m_VertexSize ) );

                // FIXME: should skip this triangle
            }

            mesh_group.m_indices[idx_offset + idx_i] = mesh.m_FaceIdx[idx_i] + vtx_offset;
        }
    }

    // generate geometry for the outer bounding box
    if( m_model_bbox.IsInitialized() )
        MakeBbox( m_model_bbox, 0, &bbox_tmp_vertices[0], &bbox_tmp_indices[0],
                  { 0.0f, 1.0f, 0.0f, 1.0f } );

    // create bounding box buffers
    glGenBuffers( 1, &m_bbox_vertex_buffer );
    glBindBuffer( GL_ARRAY_BUFFER, m_bbox_vertex_buffer );
    glBufferData( GL_ARRAY_BUFFER, sizeof( VERTEX ) * bbox_tmp_vertices.size(),
                  bbox_tmp_vertices.data(), GL_STATIC_DRAW );

    glGenBuffers( 1, &m_bbox_index_buffer );
    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, m_bbox_index_buffer );

    if( bbox_tmp_vertices.size() <= std::numeric_limits<GLushort>::max() )
    {
        m_bbox_index_buffer_type = GL_UNSIGNED_SHORT;

        auto u16buf = std::make_unique<GLushort[]>( bbox_tmp_indices.size() );

        for( unsigned int i = 0; i < bbox_tmp_indices.size(); ++i )
          u16buf[i] = static_cast<GLushort>( bbox_tmp_indices[i] );

        glBufferData( GL_ELEMENT_ARRAY_BUFFER, sizeof( GLushort ) * bbox_tmp_indices.size(),
                      u16buf.get(), GL_STATIC_DRAW );
    }
    else
    {
        m_bbox_index_buffer_type = GL_UNSIGNED_INT;
        glBufferData( GL_ELEMENT_ARRAY_BUFFER, sizeof( GLuint ) * bbox_tmp_indices.size(),
                      bbox_tmp_indices.data(), GL_STATIC_DRAW );
    }

    // merge the mesh group geometry data.
    unsigned int total_vertex_count = 0;
    unsigned int total_index_count = 0;

    for( auto& mg : mesh_groups )
    {
        total_vertex_count += mg.m_vertices.size();
        total_index_count += mg.m_indices.size();
    }

    wxLogTrace( m_logTrace, wxT( "  total %u vertices, %u indices" ),
                total_vertex_count, total_index_count );

    glGenBuffers( 1, &m_vertex_buffer );
    glBindBuffer( GL_ARRAY_BUFFER, m_vertex_buffer );
    glBufferData( GL_ARRAY_BUFFER, sizeof( VERTEX ) * total_vertex_count,
                  nullptr, GL_STATIC_DRAW );

    unsigned int idx_size = 0;

    if( total_vertex_count <= std::numeric_limits<GLushort>::max() )
    {
        m_index_buffer_type = GL_UNSIGNED_SHORT;
        idx_size = sizeof( GLushort );
    }
    else
    {
        m_index_buffer_type = GL_UNSIGNED_INT;
        idx_size = sizeof( GLuint );
    }

    // temporary index buffer which will contain either GLushort or GLuint
    // type indices.  allocate with a bit of meadow at the end.
    auto tmp_idx = std::make_unique<GLuint[]>(
                      ( idx_size * total_index_count + 8 ) / sizeof( GLuint ) );

    unsigned int prev_vtx_count = 0;
    unsigned int idx_offset = 0;
    unsigned int vtx_offset = 0;

    for( unsigned int mg_i = 0; mg_i < mesh_groups.size (); ++mg_i )
    {
        auto& mg = mesh_groups[mg_i];
        auto& mat = m_materials[mg_i];

        if( m_index_buffer_type == GL_UNSIGNED_SHORT )
        {
            auto idx_out = reinterpret_cast<GLushort*>(
                reinterpret_cast<uintptr_t>( tmp_idx.get() ) + idx_offset );

            for( auto idx : mg.m_indices )
                *idx_out++ = static_cast<GLushort>( idx + prev_vtx_count );
        }
        else if( m_index_buffer_type == GL_UNSIGNED_INT )
        {
            auto idx_out = reinterpret_cast<GLuint*>(
                reinterpret_cast<uintptr_t>( tmp_idx.get() ) + idx_offset );

            for( auto idx : mg.m_indices )
                *idx_out++ = static_cast<GLuint>( idx + prev_vtx_count );
        }

        glBufferSubData( GL_ARRAY_BUFFER, vtx_offset, mg.m_vertices.size() * sizeof( VERTEX ),
                         mg.m_vertices.data() );

        mat.m_render_idx_buffer_offset = idx_offset;
        mat.m_render_idx_count = mg.m_indices.size();

        prev_vtx_count += mg.m_vertices.size();
        idx_offset += mg.m_indices.size() * idx_size;
        vtx_offset += mg.m_vertices.size() * sizeof( VERTEX );
    }

    glGenBuffers( 1, &m_index_buffer );
    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, m_index_buffer );
    glBufferData( GL_ELEMENT_ARRAY_BUFFER, idx_size * total_index_count, tmp_idx.get(),
                  GL_STATIC_DRAW );

    glBindBuffer( GL_ARRAY_BUFFER, 0 );
    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );

    auto end_time = std::chrono::high_resolution_clock::now();

    wxLogTrace( m_logTrace, wxT( "  loaded in %u ms\n" ),
               (unsigned int)std::chrono::duration_cast<std::chrono::milliseconds> (
                   end_time - start_time).count() );
}


void MODEL_3D::BeginDrawMulti( bool aUseColorInformation )
{
    glEnableClientState( GL_VERTEX_ARRAY );
    glEnableClientState( GL_NORMAL_ARRAY );

    if( aUseColorInformation )
    {
        glEnableClientState( GL_COLOR_ARRAY );
        glEnableClientState( GL_TEXTURE_COORD_ARRAY );
        glEnable( GL_COLOR_MATERIAL );
    }

    glColorMaterial( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE );
}


void MODEL_3D::EndDrawMulti()
{
    glDisable( GL_COLOR_MATERIAL );
    glDisableClientState( GL_VERTEX_ARRAY );
    glDisableClientState( GL_NORMAL_ARRAY );
    glDisableClientState( GL_COLOR_ARRAY );
    glDisableClientState( GL_TEXTURE_COORD_ARRAY );

    glBindBuffer( GL_ARRAY_BUFFER, 0 );
    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );
}


void MODEL_3D::Draw( bool aTransparent, float aOpacity, bool aUseSelectedMaterial,
                     SFVEC3F& aSelectionColor ) const
{
    if( aOpacity <= FLT_EPSILON )
        return;

    if( !glBindBuffer )
        throw std::runtime_error( "The OpenGL context no longer exists: unable to draw" );

    glBindBuffer( GL_ARRAY_BUFFER, m_vertex_buffer );
    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, m_index_buffer );

    glVertexPointer( 3, GL_FLOAT, sizeof( VERTEX ),
                     reinterpret_cast<const void*>( offsetof( VERTEX, m_pos ) ) );

    glNormalPointer( GL_BYTE, sizeof( VERTEX ),
                     reinterpret_cast<const void*>( offsetof( VERTEX, m_nrm ) ) );

    glColorPointer( 4, GL_UNSIGNED_BYTE, sizeof( VERTEX ),
                    reinterpret_cast<const void*>(
                                     m_materialMode == MATERIAL_MODE::CAD_MODE
                                     ? offsetof( VERTEX, m_cad_color )
                                     : offsetof( VERTEX, m_color ) ) );

    glTexCoordPointer( 2, GL_FLOAT, sizeof( VERTEX ),
                       reinterpret_cast<const void*>( offsetof( VERTEX, m_tex_uv ) ) );

    const SFVEC4F param = SFVEC4F( 1.0f, 1.0f, 1.0f, aOpacity );

    glTexEnvfv( GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, (const float*)&param.x );

    // BeginDrawMulti();
    for( const MODEL_3D::MATERIAL& mat : m_materials )
    {
        if( ( mat.IsTransparent() != aTransparent )
                && ( aOpacity >= 1.0f )
                && m_materialMode != MATERIAL_MODE::DIFFUSE_ONLY )
        {
            continue;
        }

        switch( m_materialMode )
        {
        case MATERIAL_MODE::NORMAL:
            OglSetMaterial( mat, aOpacity, aUseSelectedMaterial, aSelectionColor );
            break;

        case MATERIAL_MODE::DIFFUSE_ONLY:
            OglSetDiffuseMaterial( mat.m_Diffuse, aOpacity );
            break;

        case MATERIAL_MODE::CAD_MODE:
            OglSetDiffuseMaterial( MaterialDiffuseToColorCAD( mat.m_Diffuse ), aOpacity );
            break;

        default:
            break;
        }

        glDrawElements( GL_TRIANGLES, mat.m_render_idx_count, m_index_buffer_type,
                        reinterpret_cast<const void*>(
                                static_cast<uintptr_t>( mat.m_render_idx_buffer_offset ) ) );
    }
}


MODEL_3D::~MODEL_3D()
{
    if( glDeleteBuffers )
    {
        glDeleteBuffers( 1, &m_vertex_buffer );
        glDeleteBuffers( 1, &m_index_buffer );
        glDeleteBuffers( 1, &m_bbox_vertex_buffer );
        glDeleteBuffers( 1, &m_bbox_index_buffer );
    }
}


void MODEL_3D::DrawBbox() const
{
    if( !glBindBuffer )
        throw std::runtime_error( "The OpenGL context no longer exists: unable to draw bbox" );

    glBindBuffer( GL_ARRAY_BUFFER, m_bbox_vertex_buffer );
    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, m_bbox_index_buffer );

    glVertexPointer( 3, GL_FLOAT, sizeof( VERTEX ),
                     reinterpret_cast<const void*>( offsetof( VERTEX, m_pos ) ) );

    glColorPointer( 4, GL_UNSIGNED_BYTE, sizeof( VERTEX ),
                    reinterpret_cast<const void*>( offsetof( VERTEX, m_color ) ) );

    glDrawElements( GL_LINES, bbox_idx_count, m_bbox_index_buffer_type,
                    reinterpret_cast<const void*>( 0 ) );
}


void MODEL_3D::DrawBboxes() const
{
    if( !glBindBuffer )
        throw std::runtime_error( "The OpenGL context no longer exists: unable to draw bboxes" );

    glBindBuffer( GL_ARRAY_BUFFER, m_bbox_vertex_buffer );
    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, m_bbox_index_buffer );

    glVertexPointer( 3, GL_FLOAT, sizeof( VERTEX ),
                     reinterpret_cast<const void*>( offsetof( VERTEX, m_pos ) ) );

    glColorPointer( 4, GL_UNSIGNED_BYTE, sizeof( VERTEX ),
                    reinterpret_cast<const void*>( offsetof( VERTEX, m_color ) ) );

    unsigned int idx_size = m_bbox_index_buffer_type == GL_UNSIGNED_SHORT
                            ? sizeof( GLushort ) : sizeof( GLuint );

    glDrawElements( GL_LINES, bbox_idx_count * m_meshes_bbox.size(), m_bbox_index_buffer_type,
                    reinterpret_cast<const void*>( 
                        static_cast<uintptr_t>( bbox_idx_count * idx_size ) ) );
}

