/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2015-2017 Cirilo Bernardo <cirilo.bernardo@gmail.com>
 *
 * 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
 */

#include <iostream>
#include <sstream>
#include <fstream>
#include <wx/filename.h>
#include <wx/log.h>
#include "plugins/3dapi/ifsg_api.h"
#include "plugins/3dapi/sg_version.h"
#include "streamwrapper.h"
#include "3d_cache/sg/sg_node.h"
#include "3d_cache/sg/scenegraph.h"
#include "3d_cache/sg/sg_appearance.h"
#include "3d_cache/sg/sg_shape.h"
#include "3d_cache/sg/sg_helpers.h"


#ifdef DEBUG
static char BadNode[] = " * [BUG] NULL pointer passed for aNode\n";
#endif

// version format of the cache file
#define SG_VERSION_TAG "VERSION:2"


static void formatMaterial( SMATERIAL& mat, SGAPPEARANCE const* app )
{
    float v0, v1, v2;

    app->ambient.GetColor( v0, v1, v2 );
    mat.m_Ambient.x = v0;
    mat.m_Ambient.y = v1;
    mat.m_Ambient.z = v2;

    app->diffuse.GetColor( v0, v1, v2 );
    mat.m_Diffuse.x = v0;
    mat.m_Diffuse.y = v1;
    mat.m_Diffuse.z = v2;
    mat.m_Ambient.x *= v0;
    mat.m_Ambient.y *= v1;
    mat.m_Ambient.z *= v2;

    app->emissive.GetColor( v0, v1, v2 );
    mat.m_Emissive.x = v0;
    mat.m_Emissive.y = v1;
    mat.m_Emissive.z = v2;

    app->specular.GetColor( v0, v1, v2 );
    mat.m_Specular.x = v0;
    mat.m_Specular.y = v1;
    mat.m_Specular.z = v2;

    mat.m_Shininess = app->shininess;
    mat.m_Transparency = app->transparency;

    return;
}


bool S3D::WriteVRML( const char* filename, bool overwrite, SGNODE* aTopNode,
    bool reuse, bool renameNodes )
{
    if( NULL == filename || filename[0] == 0 )
        return false;

    wxString ofile = wxString::FromUTF8Unchecked( filename );

    if( wxFileName::Exists( ofile ) )
    {
        if( !overwrite )
            return false;

        // make sure we make no attempt to write a directory
        if( !wxFileName::FileExists( ofile ) )
            return false;
    }

    if( NULL == aTopNode )
    {
        #ifdef DEBUG
        do {
            std::ostringstream ostr;
            ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
            ostr << " * [BUG] NULL pointer passed for aTopNode";
            wxLogTrace( MASK_3D_SG, "%s\n", ostr.str().c_str() );
        } while( 0 );
        #endif

        return false;
    }

    if( S3D::SGTYPE_TRANSFORM != aTopNode->GetNodeType() )
    {
        #ifdef DEBUG
        do {
            std::ostringstream ostr;
            ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
            ostr << " * [BUG] aTopNode is not a SCENEGRAPH object";
            wxLogTrace( MASK_3D_SG, "%s\n", ostr.str().c_str() );
        } while( 0 );
        #endif

        return false;
    }

    OPEN_OSTREAM( op, filename );

    if( op.fail() )
    {
        wxString errmsg;
        errmsg << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
        errmsg << " * [INFO] " << "failed to open file" << " '" << filename << "'";
        wxLogTrace( MASK_3D_SG, errmsg );
        return false;
    }

    op.imbue( std::locale( "C" ) );
    op << "#VRML V2.0 utf8\n";

    if( renameNodes )
    {
        aTopNode->ResetNodeIndex();
        aTopNode->ReNameNodes();
    }

    aTopNode->WriteVRML( op, reuse );

    if( !op.fail() )
    {
        CLOSE_STREAM( op );
        return true;
    }

    CLOSE_STREAM( op );

    wxString errmsg;
    errmsg << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
    errmsg << " * [INFO] " << "problems encountered writing file" << " '" << filename << "'";
    wxLogTrace( MASK_3D_SG, errmsg );

    return false;
}


void S3D::ResetNodeIndex( SGNODE* aNode )
{
    if( NULL == aNode )
    {
        #ifdef DEBUG
        do {
            std::ostringstream ostr;
            ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
            ostr << BadNode;
            wxLogTrace( MASK_3D_SG, "%s", ostr.str().c_str() );
        } while( 0 );
        #endif

        return;
    }

    aNode->ResetNodeIndex();

    return;
}


void S3D::RenameNodes( SGNODE* aNode )
{
    if( NULL == aNode )
    {
        #ifdef DEBUG
        do {
            std::ostringstream ostr;
            ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
            ostr << BadNode;
            wxLogTrace( MASK_3D_SG, "%s", ostr.str().c_str() );
        } while( 0 );
        #endif

        return;
    }

    aNode->ReNameNodes();

    return;
}


void S3D::DestroyNode( SGNODE* aNode )
{
    if( NULL == aNode )
    {
        #ifdef DEBUG
        do {
            std::ostringstream ostr;
            ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
            ostr << BadNode;
            wxLogTrace( MASK_3D_SG, "%s", ostr.str().c_str() );
        } while( 0 );
        #endif

        return;
    }

    delete aNode;

    return;
}


bool S3D::WriteCache( const char* aFileName, bool overwrite, SGNODE* aNode,
    const char* aPluginInfo )
{
    if( NULL == aFileName || aFileName[0] == 0 )
        return false;

    wxString ofile = wxString::FromUTF8Unchecked( aFileName );

    if( NULL == aNode )
    {
        #ifdef DEBUG
        do {
            std::ostringstream ostr;
            ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
            ostr << BadNode;
            wxLogTrace( MASK_3D_SG, "%s", ostr.str().c_str() );
        } while( 0 );
        #endif

        return false;
    }


    if( wxFileName::Exists( ofile ) )
    {
        if( !overwrite )
        {
            wxString errmsg;
            errmsg << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
            errmsg << " * [INFO] " << "file exists; not overwriting" << " '";
            errmsg << aFileName << "'";
            wxLogTrace( MASK_3D_SG, errmsg );

            return false;
        }

        // make sure we make no attempt to write a directory
        if( !wxFileName::FileExists( aFileName ) )
        {
            wxString errmsg;
            errmsg << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
            errmsg << " * [INFO] " << "specified path is a directory" << " '";
            errmsg << aFileName << "'";
            wxLogTrace( MASK_3D_SG, errmsg );
            return false;
        }
    }

    OPEN_OSTREAM( output, aFileName );

    if( output.fail() )
    {
        wxString errmsg;
        errmsg << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
        errmsg << " * [INFO] " << "failed to open file" << " '" << aFileName << "'";
        wxLogTrace( MASK_3D_SG, errmsg );
        return false;
    }

    output << "(" << SG_VERSION_TAG << ")";

    if( NULL != aPluginInfo && aPluginInfo[0] != 0 )
        output << "(" << aPluginInfo << ")";
    else
        output << "(INTERNAL:0.0.0.0)";

    bool rval = aNode->WriteCache( output, NULL );
    CLOSE_STREAM( output );

    if( !rval )
    {
        #ifdef DEBUG
        do {
            std::ostringstream ostr;
            ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
            ostr << " * [INFO] problems encountered writing cache file '";
            ostr << aFileName << "'";
            wxLogTrace( MASK_3D_SG, "%s\n", ostr.str().c_str() );
        } while( 0 );
        #endif

        // delete the defective file
        wxRemoveFile( ofile );
    }

    return rval;
}


SGNODE* S3D::ReadCache( const char* aFileName, void* aPluginMgr,
        bool (*aTagCheck)( const char*, void* ) )
{
    if( NULL == aFileName || aFileName[0] == 0 )
        return NULL;

    wxString ofile = wxString::FromUTF8Unchecked( aFileName );

    if( !wxFileName::FileExists( aFileName ) )
    {
        std::ostringstream ostr;
        ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
        wxString errmsg = _( "no such file" );
        ostr << " * [INFO] " << errmsg.ToUTF8() << " '";
        ostr << aFileName << "'";
        wxLogTrace( MASK_3D_SG, "%s\n", ostr.str().c_str() );

        return NULL;
    }

    SGNODE* np = new SCENEGRAPH( NULL );

    if( NULL == np )
    {
        #ifdef DEBUG
        do {
            std::ostringstream ostr;
            ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
            ostr << " * [INFO] failed to instantiate SCENEGRAPH";
            wxLogTrace( MASK_3D_SG, "%s\n", ostr.str().c_str() );
        } while( 0 );
        #endif

        return NULL;
    }

    OPEN_ISTREAM( file, aFileName );

    if( file.fail() )
    {
        delete np;
        std::ostringstream ostr;
        ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
        wxString errmsg = _( "failed to open file" );
        ostr << " * [INFO] " << errmsg.ToUTF8() << " '";
        ostr << aFileName << "'";
        wxLogTrace( MASK_3D_SG, "%s\n", ostr.str().c_str() );
        return NULL;
    }

    // from SG_VERSION_TAG 1, read the version tag; if it's not the expected tag
    // then we fail to read the cache file
    do
    {
        std::string name;
        char schar;
        file.get( schar );

        if( '(' != schar )
        {
            #ifdef DEBUG
            do {
                std::ostringstream ostr;
                ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
                ostr << " * [INFO] corrupt data; missing left parenthesis at position ";
                ostr << file.tellg();
                wxLogTrace( MASK_3D_SG, "%s\n", ostr.str().c_str() );
            } while( 0 );
            #endif

            CLOSE_STREAM( file );
            return NULL;
        }

        file.get( schar );

        while( ')' != schar && file.good() )
        {
            name.push_back( schar );
            file.get( schar );
        }

        if( name.compare( SG_VERSION_TAG ) )
        {
            CLOSE_STREAM( file );
            return NULL;
        }

    } while( 0 );

    // from SG_VERSION_TAG 2, read the PluginInfo string and check that it matches
    // version tag; if it's not the expected tag then we fail to read the file
    do
    {
        std::string name;
        char schar;
        file.get( schar );

        if( '(' != schar )
        {
            #ifdef DEBUG
            do {
                std::ostringstream ostr;
                ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
                ostr << " * [INFO] corrupt data; missing left parenthesis at position ";
                ostr << file.tellg();
                wxLogTrace( MASK_3D_SG, "%s\n", ostr.str().c_str() );
            } while( 0 );
            #endif

            CLOSE_STREAM( file );
            return NULL;
        }

        file.get( schar );

        while( ')' != schar && file.good() )
        {
            name.push_back( schar );
            file.get( schar );
        }

        // check the plugin tag
        if( NULL != aTagCheck && NULL != aPluginMgr && !aTagCheck( name.c_str(), aPluginMgr ) )
        {
            CLOSE_STREAM( file );
            return NULL;
        }

    } while( 0 );

    bool rval = np->ReadCache( file, NULL );
    CLOSE_STREAM( file );

    if( !rval )
    {
        delete np;
        std::ostringstream ostr;
        ostr << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << "\n";
        wxString errmsg = "problems encountered reading cache file";
        ostr << " * [INFO] " << errmsg.ToUTF8() << " '";
        ostr << aFileName << "'";
        wxLogTrace( MASK_3D_SG, "%s\n", ostr.str().c_str() );
        return NULL;
    }

    return np;
}


S3DMODEL* S3D::GetModel( SCENEGRAPH* aNode )
{
    if( NULL == aNode )
        return NULL;

    if( aNode->GetNodeType() != S3D::SGTYPE_TRANSFORM )
        return NULL;

    S3D::MATLIST materials;
    std::vector< SMESH > meshes;

    // the materials list shall have a default color; although the VRML
    // default is an opaque black, the default used here shall be a median
    // gray in hopes that it may help highlight faulty models; this color is
    // also typical of MCAD applications. When a model has no associated
    // material color it shall be assigned the index 0.
    SGAPPEARANCE app( NULL );
    app.ambient = SGCOLOR( 0.6f, 0.6f, 0.6f );
    app.diffuse = SGCOLOR( 0.6f, 0.6f, 0.6f );
    app.specular = app.diffuse;
    app.shininess = 0.05f;
    app.transparency = 0.0f;

    materials.matorder.push_back( &app );
    materials.matmap.insert( std::pair< SGAPPEARANCE const*, int >( &app, 0 ) );

    if( aNode->Prepare( NULL, materials, meshes ) )
    {
        if( meshes.empty() )
            return NULL;

        S3DMODEL* model = S3D::New3DModel();

        // add all the materials
        size_t j = materials.matorder.size();
        SMATERIAL* lmat = new SMATERIAL[j];

        for( size_t i = 0; i < j; ++i )
            formatMaterial( lmat[i], materials.matorder[i] );

        model->m_Materials = lmat;
        model->m_MaterialsSize = j;

        // add all the meshes
        j = meshes.size();
        SMESH* lmesh = new SMESH[j];

        for( size_t i = 0; i < j; ++i )
            lmesh[i] = meshes[i];

        model->m_Meshes = lmesh;
        model->m_MeshesSize = j;

        return model;
    }

    size_t j = meshes.size();

    for( size_t i = 0; i < j; ++i )
        S3D::Free3DMesh( meshes[i] );

    return NULL;
}


void S3D::Destroy3DModel( S3DMODEL** aModel )
{
    if( NULL == aModel || NULL == *aModel )
        return;

    S3DMODEL* m = *aModel;
    S3D::FREE_S3DMODEL( *m );
    delete m;
    *aModel = NULL;

    return;
}


void Free3DModel( S3DMODEL& aModel )
{
    S3D::FREE_S3DMODEL( aModel );
    return;
}


void S3D::Free3DMesh( SMESH& aMesh )
{
    S3D::FREE_SMESH( aMesh );
    return;
}


S3DMODEL* S3D::New3DModel( void )
{
    S3DMODEL* mp = new S3DMODEL;
    S3D::INIT_S3DMODEL( *mp );
    return mp;
}


void S3D::Init3DMaterial( SMATERIAL& aMat )
{
    S3D::INIT_SMATERIAL( aMat );
    return;
}


void S3D::Init3DMesh( SMESH& aMesh )
{
    S3D::INIT_SMESH( aMesh );

    return;
}


void S3D::GetLibVersion( unsigned char* Major, unsigned char* Minor,
    unsigned char* Patch, unsigned char* Revision )
{
    if( Major )
        *Major = KICADSG_VERSION_MAJOR;

    if( Minor )
        *Minor = KICADSG_VERSION_MINOR;

    if( Revision )
        *Revision = KICADSG_VERSION_REVISION;

    if( Patch )
        *Patch = KICADSG_VERSION_PATCH;

    return;
}


SGVECTOR S3D::CalcTriNorm( const SGPOINT& p1, const SGPOINT& p2, const SGPOINT& p3 )
{
    glm::dvec3 tri = glm::dvec3( 0.0, 0.0, 0.0 );
    glm::dvec3 pts[3];

    pts[0] = glm::dvec3( p1.x, p1.y, p1.z );
    pts[1] = glm::dvec3( p2.x, p2.y, p2.z );
    pts[2] = glm::dvec3( p3.x, p3.y, p3.z );

    // degenerate points are given a default 0, 0, 1 normal
    if( S3D::degenerate( pts ) )
        return SGVECTOR( 0.0, 0.0, 1.0 );

    // normal
    tri = glm::cross( pts[1] - pts[0], pts[2] - pts[0] );
    glm::normalize( tri );

    return SGVECTOR( tri.x, tri.y, tri.z );
}


S3D::SGTYPES S3D::GetSGNodeType( SGNODE* aNode )
{
    if( NULL == aNode )
        return SGTYPE_END;

    return aNode->GetNodeType();
}


SGNODE* S3D::GetSGNodeParent( SGNODE* aNode )
{
    if( NULL == aNode )
        return NULL;

    return aNode->GetParent();
}


bool S3D::AddSGNodeRef( SGNODE* aParent, SGNODE* aChild )
{
    if( NULL == aParent || NULL == aChild )
        return false;

    return aParent->AddRefNode( aChild );
}


bool S3D::AddSGNodeChild( SGNODE* aParent, SGNODE* aChild )
{
    if( NULL == aParent || NULL == aChild )
        return false;

    return aParent->AddChildNode( aChild );
}


void S3D::AssociateSGNodeWrapper( SGNODE* aObject, SGNODE** aRefPtr )
{
    if( NULL == aObject || NULL == aRefPtr || aObject != *aRefPtr )
        return;

    aObject->AssociateWrapper( aRefPtr );

    return;
}
