
#include <osg/Notify>
#include <osg/Group>
#include <osg/Geode>
#include <osg/Geometry>
#include <osg/Texture2D>
#include <osg/Material>
#include <osg/TexEnv>
#include <osg/ref_ptr>
#include <osg/MatrixTransform>
#include <osg/BlendFunc>
#include <osg/TexEnvCombine>
#include <osg/CullFace>

#include <osgDB/Registry>
#include <osgDB/FileUtils>
#include <osgDB/FileNameUtils>
#include <osgDB/ReadFile>

#include <osgUtil/TriStripVisitor>

//MIKEC debug only for PrintVisitor
#include <osg/NodeVisitor>

#include "WriterNodeVisitor.h"
#include "lib3ds/lib3ds.h"
#include <stdlib.h>
#include <string.h>

#include <set>
#include <map>
#include <iostream>
#include <sstream>
#include <limits>
#include <assert.h>

using namespace std;
using namespace osg;

/// Implementation borrowed from boost and slightly modified
template<class T> class scoped_array // noncopyable
{
private:
    T * px;
    scoped_array(scoped_array const &);
    scoped_array & operator=(scoped_array const &);
    typedef scoped_array<T> this_type;

    void operator==( scoped_array const& ) const;
    void operator!=( scoped_array const& ) const;

public:
    typedef T element_type;
    explicit scoped_array( T * p = 0 ) : px( p ) {}

    ~scoped_array() { delete[] px; }

    void reset(T * p = 0) {
        assert( p == 0 || p != px ); // catch self-reset errors
        this_type(p).swap(*this);
    }

    T & operator[](std::ptrdiff_t i) const // never throws
    {
        assert( px != 0 );
        assert( i >= 0 );
        return px[i];
    }

    T * get() const // never throws
    {
        return px;
    }

    void swap(scoped_array & b) // never throws
    {
        T * tmp = b.px;
        b.px = px;
        px = tmp;
    }
};



void copyLib3dsMatrixToOsgMatrix(osg::Matrix& osg_matrix, const Lib3dsMatrix lib3ds_matrix)
{
    osg_matrix.set(
        lib3ds_matrix[0][0],lib3ds_matrix[0][1],lib3ds_matrix[0][2],lib3ds_matrix[0][3],
        lib3ds_matrix[1][0],lib3ds_matrix[1][1],lib3ds_matrix[1][2],lib3ds_matrix[1][3],
        lib3ds_matrix[2][0],lib3ds_matrix[2][1],lib3ds_matrix[2][2],lib3ds_matrix[2][3],
        lib3ds_matrix[3][0],lib3ds_matrix[3][1],lib3ds_matrix[3][2],lib3ds_matrix[3][3]);
}

osg::Matrix copyLib3dsMatrixToOsgMatrix(const Lib3dsMatrix mat)
{
    osg::Matrix osgMatrix;
    copyLib3dsMatrixToOsgMatrix(osgMatrix, mat);
    return osgMatrix;
}

void copyLib3dsVec3ToOsgVec3(osg::Vec3f osgVec, const float vertices[3])
{
    return osgVec.set(vertices[0], vertices[1], vertices[2]);
}

osg::Vec3f copyLib3dsVec3ToOsgVec3(const float vertices[3])
{
    return osg::Vec3f(vertices[0], vertices[1], vertices[2]);
}

osg::Quat copyLib3dsQuatToOsgQuat(const float quat[4])
{
    return osg::Quat(quat[0], quat[1], quat[2], quat[3]);
}



class PrintVisitor : public NodeVisitor
{
public:
    PrintVisitor(std::ostream& out):
      NodeVisitor(NodeVisitor::TRAVERSE_ALL_CHILDREN),
          _out(out)
      {
          _indent = 0;
          _step = 4;
      }

      inline void moveIn() { _indent += _step; }
      inline void moveOut() { _indent -= _step; }
      inline void writeIndent()
      {
          for(int i=0;i<_indent;++i) _out << " ";
      }

      virtual void apply(Node& node)
      {
          moveIn();
          writeIndent(); _out << node.className() <<std::endl;
          traverse(node);
          moveOut();
      }

      virtual void apply(Geode& node)         { apply((Node&)node); }
      virtual void apply(Billboard& node)     { apply((Geode&)node); }
      virtual void apply(LightSource& node)   { apply((Group&)node); }
      virtual void apply(ClipNode& node)      { apply((Group&)node); }

      virtual void apply(Group& node)         { apply((Node&)node); }
      virtual void apply(Transform& node)     { apply((Group&)node); }
      virtual void apply(Projection& node)    { apply((Group&)node); }
      virtual void apply(Switch& node)        { apply((Group&)node); }
      virtual void apply(LOD& node)           { apply((Group&)node); }

protected:

    PrintVisitor& operator = (const PrintVisitor&) { return *this; }

    std::ostream& _out;
    int _indent;
    int _step;
};

/// Possible options:
///        - noMatrixTransforms: set the plugin to apply matrices into the mesh vertices ("old behaviour") instead of restoring them ("new behaviour"). You may use this option to avoid a few rounding errors.
///        - checkForEspilonIdentityMatrices: if noMatrixTransforms is \b not set, then consider "almost identity" matrices to be identity ones (in case of rounding errors).
///        - restoreMatrixTransformsNoMeshes: makes an exception to the behaviour when 'noMatrixTransforms' is \b not set for mesh instances. When a mesh instance has a transform on it, the reader creates a MatrixTransform above the Geode. If you don't want the hierarchy to be modified, then you can use this option to merge the transform into vertices.
class ReaderWriter3DS : public osgDB::ReaderWriter
{
public:

    ReaderWriter3DS();

    virtual const char* className() const { return "3DS Auto Studio Reader/Writer"; }

    virtual ReadResult readNode(const std::string& file, const osgDB::ReaderWriter::Options* options) const;
    virtual ReadResult readNode(std::istream& fin, const Options* options) const;
    virtual ReadResult doReadNode(std::istream& fin, const Options* options, const std::string & fileNamelib3ds) const;        ///< Subfunction of readNode()s functions.

    virtual WriteResult writeNode(const osg::Node& /*node*/,const std::string& /*fileName*/,const Options* =NULL) const;
    virtual WriteResult writeNode(const osg::Node& /*node*/,std::ostream& /*fout*/,const Options* =NULL) const;
    virtual WriteResult doWriteNode(const osg::Node& /*node*/,std::ostream& /*fout*/,const Options*, const std::string & fileNamelib3ds) const;

protected:
    ReadResult constructFrom3dsFile(Lib3dsFile *f,const std::string& filename, const Options* options) const;

    bool createFileObject(const osg::Node& node, Lib3dsFile * file3ds,const std::string& fileName, const osgDB::ReaderWriter::Options* options) const;

    /// An OSG state set with the original 3DS material attached (used to get info such as UV scaling & offset)
    struct StateSetInfo
    {
        StateSetInfo(osg::StateSet * stateset=NULL, Lib3dsMaterial * lib3dsmat=NULL) : stateset(stateset), lib3dsmat(lib3dsmat) {}
        StateSetInfo(const StateSetInfo & v) : stateset(v.stateset), lib3dsmat(v.lib3dsmat) {}
        StateSetInfo & operator=(const StateSetInfo & v) { stateset=v.stateset; lib3dsmat=v.lib3dsmat; return *this; }

        osg::ref_ptr<osg::StateSet> stateset;
        Lib3dsMaterial * lib3dsmat;
    };

    class ReaderObject
    {
    public:
        ReaderObject(const osgDB::ReaderWriter::Options* options);

        typedef std::vector<StateSetInfo> StateSetMap;
        typedef std::vector<int> FaceList;
        typedef std::map<std::string,osg::StateSet*> GeoStateMap;

        osg::Texture2D* createTexture(Lib3dsTextureMap *texture,const char* label,bool& transparancy);
        StateSetInfo createStateSet(Lib3dsMaterial *materials);
        osg::Drawable* createDrawable(Lib3dsMesh *meshes,FaceList& faceList, const osg::Matrix * matrix, StateSetInfo & ssi, bool smoothVertexNormals);

        std::string _directory;
        bool _useSmoothingGroups;

        // MIKEC
        osg::Node* processMesh(StateSetMap& drawStateMap,osg::Group* parent,Lib3dsMesh* mesh, const osg::Matrix * matrix);
        osg::Node* processNode(StateSetMap& drawStateMap,Lib3dsFile *f,Lib3dsNode *node);
    private:
        const osgDB::ReaderWriter::Options* options;
        bool noMatrixTransforms;            ///< Should the plugin apply matrices into the mesh vertices ("old behaviour"), instead of restoring matrices ("new behaviour")?
        bool checkForEspilonIdentityMatrices;
        bool restoreMatrixTransformsNoMeshes;
        typedef std::map<unsigned int,FaceList> SmoothingFaceMap;
        void addDrawableFromFace(osg::Geode * geode, FaceList & faceList, Lib3dsMesh * mesh, const osg::Matrix * matrix, StateSetInfo & ssi);

        typedef std::map<std::string, osg::ref_ptr<osg::Texture2D> > TexturesMap;        // Should be an unordered map (faster)
        TexturesMap texturesMap;
};
};

// now register with Registry to instantiate the above
// reader/writer.
REGISTER_OSGPLUGIN(3ds, ReaderWriter3DS)

ReaderWriter3DS::ReaderWriter3DS()
{
    supportsExtension("3ds","3D Studio model format");
    //supportsOption("OutputTextureFiles","Write out the texture images to file");
    //supportsOption("flipTexture", "flip texture upside-down");
    supportsOption("extended3dsFilePaths", "(Write option) Keeps long texture filenames (not 8.3) when exporting 3DS, but can lead to compatibility problems.");
    supportsOption("preserveMaterialNames", "(Write option) Preserve original material names, up to 64 characters. This can lead to compatibility problems.");
    supportsOption("noMatrixTransforms", "(Read option) Set the plugin to apply matrices into the mesh vertices (\"old behaviour\") instead of restoring them (\"new behaviour\"). You may use this option to avoid a few rounding errors.");
    supportsOption("checkForEspilonIdentityMatrices", "(Read option) If not set, then consider \"almost identity\" matrices to be identity ones (in case of rounding errors).");
    supportsOption("restoreMatrixTransformsNoMeshes", "(Read option) Makes an exception to the behaviour when 'noMatrixTransforms' is not set for mesh instances. When a mesh instance has a transform on it, the reader creates a MatrixTransform above the Geode. If you don't want the hierarchy to be modified, then you can use this option to merge the transform into vertices.");

#if 0
    OSG_NOTICE<<"3DS reader sizes:"<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsBool)="<<sizeof(Lib3dsBool)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsByte)="<<sizeof(Lib3dsByte)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsWord)="<<sizeof(Lib3dsWord)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsDword)="<<sizeof(Lib3dsDword)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsIntb)="<<sizeof(Lib3dsIntb)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsIntw)="<<sizeof(Lib3dsIntw)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsIntd)="<<sizeof(Lib3dsIntd)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsFloat)="<<sizeof(Lib3dsFloat)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsDouble)="<<sizeof(Lib3dsDouble)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsVector)="<<sizeof(Lib3dsVector)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsTexel)="<<sizeof(Lib3dsTexel)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsQuat)="<<sizeof(Lib3dsQuat)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsMatrix)="<<sizeof(Lib3dsMatrix)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsRgb)="<<sizeof(Lib3dsRgb)<<std::endl;
    OSG_NOTICE<<"  sizeof(Lib3dsRgba)="<<sizeof(Lib3dsRgba)<<std::endl;
#endif

}

ReaderWriter3DS::ReaderObject::ReaderObject(const osgDB::ReaderWriter::Options* options) :
    _useSmoothingGroups(true),
    options(options),
    noMatrixTransforms(false),
    checkForEspilonIdentityMatrices(false),
    restoreMatrixTransformsNoMeshes(false)
{
    if (options)
    {
        std::istringstream iss(options->getOptionString());
        std::string opt;
        while (iss >> opt)
        {
            if (opt == "noMatrixTransforms")
                noMatrixTransforms = true;
            else if (opt == "checkForEspilonIdentityMatrices")
                checkForEspilonIdentityMatrices = true;
            else if (opt == "restoreMatrixTransformsNoMeshes")
                restoreMatrixTransformsNoMeshes = true;
        }
    }
}


/**
    These print methods for 3ds hacking
*/
void pad(int level)
{
    for(int i=0;i<level;i++) std::cout<<"  ";
}
void print(Lib3dsMesh *mesh,int level);
void print(Lib3dsUserData *user,int level);
void print(Lib3dsMeshInstanceNode *object,int level);
void print(Lib3dsNode *node, int level);

void print(Lib3dsMatrix matrix,int level)
{
    pad(level); cout << matrix[0][0] <<" "<< matrix[0][1] <<" "<< matrix[0][2] <<" "<< matrix[0][3] << endl;
    pad(level); cout << matrix[1][0] <<" "<< matrix[1][1] <<" "<< matrix[1][2] <<" "<< matrix[1][3] << endl;
    pad(level); cout << matrix[2][0] <<" "<< matrix[2][1] <<" "<< matrix[2][2] <<" "<< matrix[2][3] << endl;
    pad(level); cout << matrix[3][0] <<" "<< matrix[3][1] <<" "<< matrix[3][2] <<" "<< matrix[3][3] << endl;
}
void print(Lib3dsMesh *mesh,int level)
{
    if (mesh)
    {
        pad(level); cout << "mesh name " << mesh->name  << endl;
        print(mesh->matrix,level);
    }
    else
    {
        pad(level); cout << "no mesh " << endl;
    }
}
void print(Lib3dsUserData *user,int level)
{
    if (user)
    {
        pad(level); cout << "user data" << endl;
        //print(user->mesh,level+1);
    }
    else
    {
        pad(level); cout << "no user data" << endl;
    }
}

void print(Lib3dsMeshInstanceNode *object,int level)
{
    if (object)
    {
        pad(level); cout << "objectdata instance [" << object->instance_name << "]" << endl;
        pad(level); cout << "pivot     " << object->pivot[0] <<" "<< object->pivot[1] <<" "<< object->pivot[2] << endl;
        pad(level); cout << "pos       " << object->pos[0] <<" "<< object->pos[1] <<" "<< object->pos[2] << endl;
        pad(level); cout << "scl       " << object->scl[0] <<" "<< object->scl[1] <<" "<< object->scl[2] << endl;
        pad(level); cout << "rot       " << object->rot[0] <<" "<< object->rot[1] <<" "<< object->rot[2] <<" "<< object->rot[3] << endl;
    }
    else
    {
        pad(level); cout << "no object data" << endl;
    }
}

void print(Lib3dsNode *node, int level)
{

    pad(level); cout << "node name [" << node->name << "]" << endl;
    pad(level); cout << "node id    " << node->user_id << endl;
    pad(level); cout << "node parent id " << (node->parent ? static_cast<int>(node->parent->user_id) : -1) << endl;
    pad(level); cout << "node matrix:" << endl;
    print(node->matrix,level+1);

    if (node->type == LIB3DS_NODE_MESH_INSTANCE)
    {
        pad(level); cout << "mesh instance data:" << endl;
        print(reinterpret_cast<Lib3dsMeshInstanceNode *>(node),level+1);
    }
    else
    {
        pad(level); cout << "node is not a mesh instance (not handled)" << endl;
    }

    print(&node->user_ptr,level);

    for(Lib3dsNode *child=node->childs; child; child=child->next)
    {
        print(child,level+1);
    }

}

void ReaderWriter3DS::ReaderObject::addDrawableFromFace(osg::Geode * geode, FaceList & faceList,
                                                        Lib3dsMesh * mesh,
                                                        const osg::Matrix * matrix,
                                                        StateSetInfo & ssi)
{
    if (_useSmoothingGroups)
    {
        SmoothingFaceMap smoothingFaceMap;
        for (FaceList::iterator flitr=faceList.begin();
            flitr!=faceList.end();
            ++flitr)
        {
            // ChrisD: Worth bearing in mind that this splitting up of
            // faces into smoothing groups is only correct for faces
            // belonging to a single smoothing group. The smoothing group
            // value is actually a bitmask for all the smoothing groups that
            // a face may belong to.
            smoothingFaceMap[mesh->faces[*flitr].smoothing_group].push_back(*flitr);
        }

        for(SmoothingFaceMap::iterator sitr=smoothingFaceMap.begin();
            sitr!=smoothingFaceMap.end();
            ++sitr)
        {
            // We only compute smoothed vertex normals for faces with non-zero smoothing group value.
            const unsigned int smoothing_group = sitr->first;
            bool smoothVertexNormals = (smoothing_group != 0);

            // each smoothing group to have its own geom
            // to ensure the vertices on adjacent groups
            // don't get shared.
            FaceList& smoothFaceList = sitr->second;
            osg::ref_ptr<osg::Drawable> drawable = createDrawable(mesh, smoothFaceList, matrix, ssi, smoothVertexNormals);
            if (drawable.valid())
            {
                if (ssi.stateset)
                    drawable->setStateSet(ssi.stateset.get());
                geode->addDrawable(drawable.get());
            }
        }
    }
    else // ignore smoothing groups.
    {
        // Create drawable with no smoothing of normals.
        bool smoothVertexNormals = false;
        osg::ref_ptr<osg::Drawable> drawable = createDrawable(mesh, faceList, matrix, ssi, smoothVertexNormals);
        if (drawable.valid())
        {
            if (ssi.stateset)
                drawable->setStateSet(ssi.stateset.get());
            geode->addDrawable(drawable.get());
        }
    }
}


// Transforms points by matrix if 'matrix' is not NULL
// Creates a Geode and Geometry (as parent,child) and adds the Geode to 'parent' parameter iff 'parent' is non-NULL
// Returns ptr to the Geode
osg::Node* ReaderWriter3DS::ReaderObject::processMesh(StateSetMap& drawStateMap,osg::Group* parent,Lib3dsMesh* mesh, const osg::Matrix * matrix)
{
    typedef std::vector<FaceList> MaterialFaceMap;
    MaterialFaceMap materialFaceMap;
    unsigned int numMaterials = drawStateMap.size();
    materialFaceMap.insert(materialFaceMap.begin(), numMaterials, FaceList());        // Setup the map
    FaceList defaultMaterialFaceList;
    for (unsigned int i=0; i<mesh->nfaces; ++i)
    {
        if (mesh->faces[i].material>=0)
        {
            materialFaceMap[mesh->faces[i].material].push_back(i);
        }
        else
        {
            defaultMaterialFaceList.push_back(i);
        }
    }
    if (materialFaceMap.empty() && defaultMaterialFaceList.empty())
    {
        OSG_NOTICE<<"Warning : no triangles assigned to mesh '"<<mesh->name<<"'"<< std::endl;
        //OSG_INFO << "No material assigned to mesh '" << mesh->name << "'" << std::endl;
        return NULL;
    }
    else
    {
        osg::Geode* geode = new osg::Geode;
        geode->setName(mesh->name);
        if (!defaultMaterialFaceList.empty())
        {
            StateSetInfo emptySSI;
            addDrawableFromFace(geode, defaultMaterialFaceList, mesh, matrix, emptySSI);
        }
        for(unsigned int imat=0; imat<numMaterials; ++imat)
        {
            addDrawableFromFace(geode, materialFaceMap[imat], mesh, matrix, drawStateMap[imat]);
        }
        if (parent) parent->addChild(geode);
        return geode;
    }
}


/// Returns true if a matrix is 'almost' identity, meaning that the difference between each value and the corresponding identity value is less than an epsilon value.
bool isIdentityEquivalent(const osg::Matrix & mat, osg::Matrix::value_type epsilon=1e-6)
{
    return osg::equivalent(mat(0,0), 1, epsilon) && osg::equivalent(mat(0,1), 0, epsilon) && osg::equivalent(mat(0,2), 0, epsilon) &&  osg::equivalent(mat(0,3), 0, epsilon) &&
           osg::equivalent(mat(1,0), 0, epsilon) && osg::equivalent(mat(1,1), 1, epsilon) && osg::equivalent(mat(1,2), 0, epsilon) &&  osg::equivalent(mat(1,3), 0, epsilon) &&
           osg::equivalent(mat(2,0), 0, epsilon) && osg::equivalent(mat(2,1), 0, epsilon) && osg::equivalent(mat(2,2), 1, epsilon) &&  osg::equivalent(mat(2,3), 0, epsilon) &&
           osg::equivalent(mat(3,0), 0, epsilon) && osg::equivalent(mat(3,1), 0, epsilon) && osg::equivalent(mat(3,2), 0, epsilon) &&  osg::equivalent(mat(3,3), 1, epsilon);
}


/**
How to cope with pivot points in 3ds (short version)

  All object coordinates in 3ds are stored in world space, this is why you can just rip out the meshes and use/draw them without meddeling further
  Unfortunately, this gets a bit wonky with objects with pivot points (conjecture: PP support is retro fitted into the .3ds format and so doesn't fit perfectly?)

  Objects with pivot points have a position relative to their PP, so they have to undergo this transform:

    invert the mesh matrix, apply this matrix to the object. This puts the object back at the origin
    Transform the object by the nodes (negative) pivot point coords, this puts the PP at the origin
    Transform the node by the node matrix, which does the orientation about the pivot point, (and currently) transforms the object back by a translation to the PP.

*/
osg::Node* ReaderWriter3DS::ReaderObject::processNode(StateSetMap& drawStateMap,Lib3dsFile *f,Lib3dsNode *node)
{
    // Get mesh
    Lib3dsMeshInstanceNode * object = (node->type == LIB3DS_NODE_MESH_INSTANCE) ? reinterpret_cast<Lib3dsMeshInstanceNode *>(node) : NULL;
    Lib3dsMesh * mesh = lib3ds_file_mesh_for_node(f,node);
    assert(!(mesh && !object));        // Node must be a LIB3DS_NODE_MESH_INSTANCE if a mesh exists

    // Retrieve LOCAL transform
    static const osg::Matrix::value_type MATRIX_EPSILON = 1e-10;
    osg::Matrix osgWorldToNodeMatrix( copyLib3dsMatrixToOsgMatrix(node->matrix) );
    osg::Matrix osgWorldToParentNodeMatrix;
    if (node->parent)
    {
        // Matrices evaluated by lib3DS are multiplied by parents' ones
        osgWorldToParentNodeMatrix = copyLib3dsMatrixToOsgMatrix(node->parent->matrix);
    }
    osg::Matrix osgNodeMatrix( osgWorldToNodeMatrix * osg::Matrix::inverse(osgWorldToParentNodeMatrix) );

    // Test if we should create an intermediate Group (or MatrixTransform) and which matrix we should apply to the vertices
    osg::Group* group = NULL;

    // Get pivot point
    osg::Vec3 pivot( object ? copyLib3dsVec3ToOsgVec3(object->pivot) : osg::Vec3() );
    bool pivoted = pivot.x()!=0 || pivot.y()!=0 || pivot.z()!=0;

    osg::Matrix meshMat;
    if (mesh)
    {
        if (!noMatrixTransforms)
        {
            // There can be a transform directly on a mesh instance (= as if a osg::MatrixTransform and a osg::Geode were merged together) in object->pos/rot/scl
            if (pivoted) {
                meshMat = osg::Matrix::inverse(copyLib3dsMatrixToOsgMatrix(mesh->matrix)) * osg::Matrix::translate(-pivot);
            } else {
                meshMat = osg::Matrix::inverse(copyLib3dsMatrixToOsgMatrix(mesh->matrix));
            }
        }
        else {
            if (pivoted) {
                meshMat = osg::Matrix::inverse(copyLib3dsMatrixToOsgMatrix(mesh->matrix)) * osg::Matrix::translate(-pivot) * osgWorldToNodeMatrix;
            } else {
                meshMat = osg::Matrix::inverse(copyLib3dsMatrixToOsgMatrix(mesh->matrix)) * osgWorldToNodeMatrix;        // ==Identity when not pivoted?
            }
            osgNodeMatrix = osg::Matrix::identity();        // Not sure it's useful, but it's harmless ;)
        }
    }

    bool isOsgNodeMatrixIdentity = false;
    if (osgNodeMatrix.isIdentity() || (checkForEspilonIdentityMatrices && isIdentityEquivalent(osgNodeMatrix, MATRIX_EPSILON)))
    {
        isOsgNodeMatrixIdentity = true;
    }


    //if (node->childs != NULL || pivoted || (!isOsgNodeMatrixIdentity && !noMatrixTransforms))
    if (node->childs != NULL || (!isOsgNodeMatrixIdentity && !noMatrixTransforms))
    {
        if (isOsgNodeMatrixIdentity || noMatrixTransforms)
        {
            group = new osg::Group;
        }
        else
        {
            group = new osg::MatrixTransform(osgNodeMatrix);
        }
    }

    if (group)
    {
        if (strcmp(node->name, "$$$DUMMY") == 0)
        {
            if (node->type == LIB3DS_NODE_MESH_INSTANCE)
                group->setName(reinterpret_cast<Lib3dsMeshInstanceNode *>(node)->instance_name);
        }
        else if (node->type == LIB3DS_NODE_MESH_INSTANCE && strlen(reinterpret_cast<Lib3dsMeshInstanceNode *>(node)->instance_name) != 0)
            group->setName(reinterpret_cast<Lib3dsMeshInstanceNode *>(node)->instance_name);
        else
            group->setName(node->name);

        // Handle all children of this node for hierarchical assemblies
        for (Lib3dsNode *p=node->childs; p!=NULL; p=p->next)
        {
            group->addChild(processNode(drawStateMap,f,p));
        }
    }
    else
    {
        assert(node->childs == NULL);        // Else we must have a group to put childs into
    }

    // Handle mesh
    if (mesh)
    {
        osg::Matrix * meshAppliedMatPtr = NULL;
        if (!meshMat.isIdentity() && !(checkForEspilonIdentityMatrices && isIdentityEquivalent(meshMat, MATRIX_EPSILON)))
        {
            meshAppliedMatPtr = &meshMat;
        }

        // MIKEC: "group" handles the node transform, siblings, and our child geometry if we don't want an MT node for it
        // if noMatrixTransforms is not set, we create a transform node for the mesh's matrix
        osg::Group* meshTransform=NULL;

        if ((noMatrixTransforms==false) && meshAppliedMatPtr) { // we are allowed to have, and need another matrixtransform
            meshTransform=new osg::MatrixTransform(meshMat);
            meshAppliedMatPtr=NULL; // since meshTransform applies it

            meshTransform->setName("3DSMeshMatrix");
            if (group) group->addChild(meshTransform);
        } else {
            meshTransform=group; // don't need the meshTransform node - note group can be NULL
        }

        if (group)
        {
            // add our geometry to group (where our children already are)
            // creates geometry under modifier node
            processMesh(drawStateMap,meshTransform,mesh,meshAppliedMatPtr);
            return group;
        }
        else
        {
            // didn't use group for children, return a ptr directly to the Geode for this mesh
            // there is no group node but may have a meshTransform node to hold the meshes matrix
            if (meshTransform) {
                processMesh(drawStateMap,meshTransform,mesh,meshAppliedMatPtr);
                return meshTransform;
            } else { // no group or meshTransform nodes - create a new Geode and return that
                return processMesh(drawStateMap,NULL,mesh,meshAppliedMatPtr);
            }
        }

    }
    else
    {
        // no mesh for this node - probably a camera or something of that persuasion
        //cout << "no mesh for object " << node->name << endl;
        return group; // we have no mesh, but we might have children
    }
}


static long filei_seek_func(void *self, long offset, Lib3dsIoSeek origin)
{
    std::istream *f = reinterpret_cast<std::istream*>(self);
    ios_base::seekdir o = ios_base::beg;
    if (origin == LIB3DS_SEEK_CUR) o = ios_base::cur;
    else if (origin == LIB3DS_SEEK_END) o = ios_base::end;

    f->seekg(offset, o);
    return f->fail() ? -1 : 0;
}

static long fileo_seek_func(void *self, long offset, Lib3dsIoSeek origin)
{
    std::ostream *f = reinterpret_cast<std::ostream*>(self);
    ios_base::seekdir o = ios_base::beg;
    if (origin == LIB3DS_SEEK_CUR) o = ios_base::cur;
    else if (origin == LIB3DS_SEEK_END) o = ios_base::end;

    f->seekp(offset, o);
    return f->fail() ? -1 : 0;
}

static long filei_tell_func(void *self)
{
    std::istream *f = reinterpret_cast<std::istream*>(self);
    return f->tellg();
}

static long fileo_tell_func(void *self)
{
    std::ostream *f = reinterpret_cast<std::ostream*>(self);
    return f->tellp();
}


static size_t filei_read_func(void *self, void *buffer, size_t size)
{
    std::istream *f = reinterpret_cast<std::istream*>(self);
    f->read(reinterpret_cast<char*>(buffer), size);
    return f->gcount();
}

static size_t fileo_write_func(void *self, const void *buffer, size_t size)
{
    std::ostream *f = reinterpret_cast<std::ostream*>(self);
    f->write(static_cast<const char*>(buffer), size);
    return f->fail() ? 0 : size;
}

static void fileio_log_func(void *self, Lib3dsLogLevel level, int indent, const char *msg)
{
    osg::NotifySeverity l = osg::INFO;
    // Intentionally NOT mapping 3DS levels with OSG levels
    if (level == LIB3DS_LOG_ERROR) l = osg::WARN;
    else if (level == LIB3DS_LOG_WARN) l = osg::NOTICE;
    else if (level == LIB3DS_LOG_INFO) l = osg::INFO;
    else if (level == LIB3DS_LOG_DEBUG) l = osg::DEBUG_INFO;
    OSG_NOTIFY(l) << msg << std::endl;
}


osgDB::ReaderWriter::ReadResult ReaderWriter3DS::readNode(std::istream& fin,  const osgDB::ReaderWriter::Options* options) const
{
    std::string optFileName;
    if (options)
    {
        optFileName = options->getPluginStringData("STREAM_FILENAME");
        if (optFileName.empty()) optFileName = options->getPluginStringData("filename");
    }
    return doReadNode(fin, options, optFileName);
}

osgDB::ReaderWriter::ReadResult ReaderWriter3DS::doReadNode(std::istream& fin,  const osgDB::ReaderWriter::Options* options, const std::string & fileNamelib3ds) const
{
    osg::ref_ptr<Options> local_opt = options ? static_cast<Options*>(options->clone(osg::CopyOp::SHALLOW_COPY)) : new Options;
    local_opt->getDatabasePathList().push_front(osgDB::getFilePath(fileNamelib3ds));

    osgDB::ReaderWriter::ReadResult result = ReadResult::FILE_NOT_HANDLED;

    // Prepare io structure to tell how to read the stream
    Lib3dsIo io;
    io.self = &fin;
    io.seek_func = filei_seek_func;
    io.tell_func = filei_tell_func;
    io.read_func = filei_read_func;
    io.write_func = NULL;
    io.log_func = fileio_log_func;

    Lib3dsFile * file3ds = lib3ds_file_new();
    if (lib3ds_file_read(file3ds, &io) != 0)
    {
        result = constructFrom3dsFile(file3ds,fileNamelib3ds,options);
        lib3ds_file_free(file3ds);
    }

    return(result);
}

osgDB::ReaderWriter::ReadResult ReaderWriter3DS::readNode(const std::string& file, const osgDB::ReaderWriter::Options* options) const
{
    std::string ext = osgDB::getLowerCaseFileExtension(file);
    if (!acceptsExtension(ext)) return ReadResult::FILE_NOT_HANDLED;

    std::string fileName = osgDB::findDataFile( file, options );
    if (fileName.empty()) return ReadResult::FILE_NOT_FOUND;

    // Do not use the lib3ds_file_open() as:
    //   1. It relies on FILE* instead of iostreams (less safe)
    //   2. It doesn't allow us to set a custom log output
    osgDB::ifstream fin(fileName.c_str(), std::ios_base::in | std::ios_base::binary);
    if (!fin.good()) return ReadResult::ERROR_IN_READING_FILE;
    return doReadNode(fin, options, fileName);
/*
    osgDB::ReaderWriter::ReadResult result = ReadResult::FILE_NOT_HANDLED;
    Lib3dsFile *f = lib3ds_file_open(fileName.c_str());        // ,options

    if (f)
    {
        osg::ref_ptr<Options> local_opt = options ? static_cast<Options*>(options->clone(osg::CopyOp::SHALLOW_COPY)) : new Options;
        local_opt->getDatabasePathList().push_front(osgDB::getFilePath(fileName));

        result = constructFrom3dsFile(f,file,local_opt.get());
        lib3ds_file_free(f);
    }

    return result;
*/
}

osgDB::ReaderWriter::ReadResult ReaderWriter3DS::constructFrom3dsFile(Lib3dsFile *f,const std::string& fileName, const osgDB::ReaderWriter::Options* options) const
{
    if (f==NULL) return ReadResult::FILE_NOT_HANDLED;

    // MIKEC
    // This appears to build the matrix structures for the 3ds structure
    // It wasn't previously necessary because all the meshes are stored in world coordinates
    // but is VERY necessary if you want to use pivot points...
    lib3ds_file_eval(f,0.0f); // second param is time 't' for animated files

    ReaderObject reader(options);

    reader._directory = ( options && !options->getDatabasePathList().empty() ) ? options->getDatabasePathList().front() : osgDB::getFilePath(fileName);

    ReaderObject::StateSetMap drawStateMap;
    unsigned int numMaterials = f->nmaterials;
    drawStateMap.insert(drawStateMap.begin(), numMaterials, StateSetInfo());        // Setup the map
    for (unsigned int imat=0; imat<numMaterials; ++imat)
    {
        Lib3dsMaterial * mat = f->materials[imat];
        drawStateMap[imat] = reader.createStateSet(mat);
    }

    if (osg::getNotifyLevel()>=osg::INFO)
    {
        int level=0;
        std::cout << "NODE TRAVERSAL of 3ds file "<<f->name<<std::endl;
        for(Lib3dsNode *node=f->nodes; node; node=node->next)
        {
            print(node,level+1);
        }
        std::cout << "MESH TRAVERSAL of 3ds file "<<f->name<<std::endl;
        for (int imesh=0; imesh<f->nmeshes; ++imesh){
            print(f->meshes[imesh],level+1);
        }
    }

    // We can traverse by meshes (old method, broken for pivot points, but otherwise works), or by nodes (new method, not so well tested yet)
    // if your model is broken, especially wrt object positions try setting this flag. If that fixes it,
    // send me the model
    bool traverse_nodes=true;

    // MIKEC: have found 3ds files with NO node structure - only meshes, for this case we fall back to the old traverse-by-meshes code
    // Loading and re-exporting these files from 3DS produces a file with correct node structure, so perhaps these are not 100% conformant?
    if (f->nodes == NULL)
    {
        OSG_WARN<<"Warning: in 3ds loader: file has no nodes, traversing by meshes instead"<< std::endl;
        traverse_nodes=false;
    }

    osg::Node* group = NULL;

    if (!traverse_nodes) // old method, traverse by mesh
    {
        group = new osg::Group();
        for (int imesh=0; imesh<f->nmeshes; ++imesh)
        {
            reader.processMesh(drawStateMap,group->asGroup(),f->meshes[imesh],NULL);
        }
    }
    else
    { // new method
        Lib3dsNode *node=f->nodes;
        if (!node->next)
        {
            group = reader.processNode(drawStateMap,f,node);
        }
        else
        {
            group = new osg::Group();
            for(; node; node=node->next)
            {
                group->asGroup()->addChild(reader.processNode(drawStateMap,f,node));
            }
        }
    }
    if (group && group->getName().empty()) group->setName(fileName);

    if (osg::getNotifyLevel()>=osg::INFO)
    {
        OSG_INFO << "Final OSG node structure looks like this:"<< endl;
        PrintVisitor pv(osg::notify(osg::INFO));
        group->accept(pv);
    }

    return group;
}

struct RemappedFace
{
    Lib3dsFace* face;        // Original face definition.
    osg::Vec3f normal;
    unsigned int index[3];   // Indices to OSG vertex/normal/texcoord arrays.
};

struct VertexParams
{
    VertexParams() : matrix(NULL), smoothNormals(false), scaleUV(1.f, 1.f), offsetUV(0.f, 0.f) { }
    const osg::Matrix* matrix;
    bool smoothNormals;
    osg::Vec2f scaleUV;
    osg::Vec2f offsetUV;
};

static bool isFaceValid(const Lib3dsMesh* mesh, const Lib3dsFace* face)
{
    return
        face->index[0] < mesh->nvertices &&
        face->index[1] < mesh->nvertices &&
        face->index[2] < mesh->nvertices;
}

bool isNumber(float x)
{
    // This looks like it should always be true,
    // but it's false if x is a NaN.
    return (x == x);
}


/* ChrisD: addVertex handles the averaging of normals and spltting of vertices
   required to implement normals for smoothing groups. When a shared
   vertex is encountered when smoothing is required, normals are added
   and normalized. When a shared vertex is encountered when smoothing is
   not required, we must split the vertex if a different normal is required.
   For example if we are processing a cube mesh with no smoothing group
   made from 12 triangles and 8 vertices, the resultant mesh should have
   24 vertices to accommodate the 3 different normals at each vertex.
  */
static void addVertex(
    const Lib3dsMesh* mesh,
    RemappedFace& remappedFace,
    unsigned int i,
    osg::Geometry* geometry,
    std::vector<int>& origToNewMapping,
    std::vector<int>& splitVertexChain,
    const VertexParams& params)
{
    osg::Vec3Array* vertices = (osg::Vec3Array*)geometry->getVertexArray();
    osg::Vec3Array* normals = (osg::Vec3Array*)geometry->getNormalArray();
    osg::Vec2Array* texCoords = (osg::Vec2Array*)geometry->getTexCoordArray(0);

    unsigned int index = remappedFace.face->index[i];
    if (origToNewMapping[index] == -1)
    {
        int newIndex = vertices->size();
        remappedFace.index[i] = newIndex;
        origToNewMapping[index] = newIndex;

        // Add the vertex position
        osg::Vec3 vertex = copyLib3dsVec3ToOsgVec3(mesh->vertices[index]);
        if (params.matrix) vertex = vertex * (*params.matrix);
        vertices->push_back(vertex);

        // Add the vertex normal
        normals->push_back(remappedFace.normal);

        // Add the texture coordinate.
        if (texCoords)
        {
            osg::Vec2f texCoord(mesh->texcos[index][0], mesh->texcos[index][1]);
            texCoord = componentMultiply(texCoord, params.scaleUV);
            texCoord += params.offsetUV;
            if (!isNumber(texCoord.x()) || !isNumber(texCoord.y())) {
                OSG_WARN << "NaN found in texcoord" << std::endl;
                texCoord.set(0,0);
            }
            texCoords->push_back(texCoord);
        }

        // New vertex, so not split yet.
        splitVertexChain.push_back(-1);
    }
    else
    {
        int newIndex = origToNewMapping[index];
        if (params.smoothNormals)
        {
            // Average the normals on the shared vertex.
            remappedFace.index[i] = newIndex;
            osg::Vec3f normal = (*normals)[newIndex];
            normal += remappedFace.normal;
            normal.normalize();
            (*normals)[newIndex] = normal;
        }
        else
        {
            // Find a split vertex chained from newIndex which has the 'same' normal.
            int sharedVertexIndex = newIndex;
            do
            {
                osg::Vec3f normal = (*normals)[sharedVertexIndex];
                float normalDifference = (remappedFace.normal - normal).length2();
                if (normalDifference < 1e-6) break;
                sharedVertexIndex = splitVertexChain[sharedVertexIndex];
            } while (sharedVertexIndex != -1);

            if (sharedVertexIndex == -1)
            {
                // When different normals on a shared vertex required, split the vertex.
                int splitVertexIndex = vertices->size();
                remappedFace.index[i] = splitVertexIndex;
                vertices->push_back((*vertices)[newIndex]);
                normals->push_back(remappedFace.normal);
                if (texCoords)
                {
                    texCoords->push_back((*texCoords)[newIndex]);
                }
                splitVertexChain[newIndex] = splitVertexIndex;
                splitVertexChain.push_back(-1);
            }
            else
            {
                // When normals on shared vertex are identical (or very similar), keep it shared.
                remappedFace.index[i] = sharedVertexIndex;
            }
        }
    }
}

static bool addFace(
    const Lib3dsMesh* mesh,
    RemappedFace& remappedFace,
    osg::Geometry* geometry,
    std::vector<int>& origToNewMapping,
    std::vector<int>& splitVertexChain,
    const VertexParams& params)
{
    if (isFaceValid(mesh, remappedFace.face))
    {
        addVertex(mesh, remappedFace, 0, geometry, origToNewMapping, splitVertexChain, params);
        addVertex(mesh, remappedFace, 1, geometry, origToNewMapping, splitVertexChain, params);
        addVertex(mesh, remappedFace, 2, geometry, origToNewMapping, splitVertexChain, params);
        return true;
    }
    else
    {
        // Avoids crash with corrupted files.
        remappedFace.face = NULL;
        return false;
    }
}

template <typename Prim>
void fillTriangles(osg::Geometry & geom, const std::vector<RemappedFace> & remappedFaces, unsigned int numIndices) {
    //if (m->nvertices < std::numeric_limits<unsigned short>::max()) {
    osg::ref_ptr<Prim> elements( new Prim(osg::PrimitiveSet::TRIANGLES, numIndices) );
    typename Prim::iterator index_itr( elements->begin() );
    for (unsigned int i = 0; i < remappedFaces.size(); ++i)
    {
        const RemappedFace& remappedFace = remappedFaces[i];
        if (remappedFace.face != NULL)
        {
            *(index_itr++) = remappedFace.index[0];
            *(index_itr++) = remappedFace.index[1];
            *(index_itr++) = remappedFace.index[2];
        }
    }
    geom.addPrimitiveSet(elements.get());
}


/**
use matrix to pretransform geometry, or NULL to do nothing
*/
osg::Drawable* ReaderWriter3DS::ReaderObject::createDrawable(Lib3dsMesh *m,FaceList& faceList, const osg::Matrix * matrix, StateSetInfo & ssi, bool smoothVertexNormals)
{
    // Avoid creating geoms for empty face list because otherwise osg asserts/crashes during render traversal.
    if (faceList.empty()) return NULL;

    osg::Geometry * geom = new osg::Geometry;

    VertexParams params;
    params.matrix = matrix;
    params.smoothNormals = smoothVertexNormals;

    std::vector<RemappedFace> remappedFaces(faceList.size());

    scoped_array<Lib3dsVector> normals( new Lib3dsVector[m->nfaces] );        // Temporary array
    lib3ds_mesh_calculate_face_normals(m, normals.get());

    osg::ref_ptr<osg::Vec3Array> osg_vertices = new osg::Vec3Array();
    osg_vertices->reserve(m->nvertices);
    geom->setVertexArray(osg_vertices.get());

    osg::ref_ptr<osg::Vec3Array> osg_normals = new osg::Vec3Array();
    osg_normals->reserve(m->nvertices);
    geom->setNormalArray(osg_normals.get(), osg::Array::BIND_PER_VERTEX);

    osg::ref_ptr<osg::Vec2Array> osg_texCoords = NULL;

    if (m->texcos)
    {
        osg_texCoords = new osg::Vec2Array();
        osg_texCoords->reserve(m->nvertices);
        geom->setTexCoordArray(0, osg_texCoords.get());

        // Texture 0 parameters (only one texture supported for now)
        if (ssi.lib3dsmat && *(ssi.lib3dsmat->texture1_map.name))     // valid texture = name not empty
        {
            Lib3dsTextureMap & tex3ds = ssi.lib3dsmat->texture1_map;
            params.scaleUV = osg::Vec2f(tex3ds.scale[0], tex3ds.scale[1]);
            params.offsetUV = osg::Vec2f(tex3ds.offset[0], tex3ds.offset[1]);
            if (tex3ds.rotation != 0) OSG_NOTICE << "3DS texture rotation not supported yet" << std::endl;
            //TODO: tint_1, tint_2, tint_r, tint_g, tint_b
        }
    }

    // The map between lib3ds mesh vertex indices and remapped osg vertices.
    std::vector<int> origToNewMapping(m->nvertices, -1);

    // If osg vertices need to be split to hold a different vertex normal,
    // splitVertexChain allows us to look them up.
    std::vector<int> splitVertexChain;
    splitVertexChain.reserve(m->nvertices);

    unsigned int faceIndex = 0;
    unsigned int faceCount = 0;
    unsigned int maxVertexIndex = 0;
    for (FaceList::iterator itr = faceList.begin();
        itr != faceList.end();
        ++itr, ++faceIndex)
    {
        osg::Vec3f normal(copyLib3dsVec3ToOsgVec3(normals[*itr]));
        if (matrix) normal = osg::Matrix::transform3x3(normal, *(params.matrix));
        normal.normalize();

        Lib3dsFace& face = m->faces[*itr];
        RemappedFace & rf = remappedFaces[faceIndex];
        rf.face = &face;
        rf.normal = normal;
        if (addFace(m, rf, geom, origToNewMapping, splitVertexChain, params))
        {
            ++faceCount;
        }
        // Get the highest index
        maxVertexIndex = osg::maximum(maxVertexIndex, rf.index[0]);
        maxVertexIndex = osg::maximum(maxVertexIndex, rf.index[1]);
        maxVertexIndex = osg::maximum(maxVertexIndex, rf.index[2]);
    }

    // 'Shrink to fit' all vertex arrays because potentially faceList refers to fewer vertices than the whole mesh.
    // This will almost certainly be the case where mesh has been broken down into smoothing groups.
    if (osg_vertices.valid() && osg_vertices->size() < osg_vertices->capacity()) osg_vertices->trim();
    if (osg_normals.valid() && osg_normals->size() < osg_normals->capacity()) osg_normals->trim();
    if (osg_texCoords.valid() && osg_texCoords->size() < osg_texCoords->capacity()) osg_texCoords->trim();

    // Set geometry color to white.
    osg::ref_ptr<osg::Vec4ubArray> osg_colors = new osg::Vec4ubArray(1);
    (*osg_colors)[0].set(255,255,255,255);
    geom->setColorArray(osg_colors.get(), osg::Array::BIND_OVERALL);

    // Create triangle primitives.
    // Remapping may create additional vertices, thus creating indices over USHORT_MAX
    if (maxVertexIndex < std::numeric_limits<unsigned short>::max()) {
        fillTriangles<DrawElementsUShort>(*geom, remappedFaces, faceCount * 3);
    } else {
        fillTriangles<DrawElementsUInt>  (*geom, remappedFaces, faceCount * 3);
   }
#if 0
    osgUtil::TriStripVisitor tsv;
    tsv.stripify(*geom);
#endif

    return geom;
}


osg::Texture2D*  ReaderWriter3DS::ReaderObject::createTexture(Lib3dsTextureMap *texture,const char* label,bool& transparency)
{
    if (texture && *(texture->name))
    {
        OSG_INFO<<"texture->name="<<texture->name<<", _directory="<<_directory<<std::endl;

        // First try already loaded textures.
        TexturesMap::iterator itTex = texturesMap.find(texture->name);
        if (itTex != texturesMap.end()) {
            OSG_DEBUG << "Texture '" << texture->name << "' found in cache." << std::endl;
            return itTex->second.get();
        }

        // Texture not in cache: locate and load.
        std::string fileName = osgDB::findFileInDirectory(texture->name,_directory,osgDB::CASE_INSENSITIVE);
        if (fileName.empty())
        {
            // file not found in .3ds file's directory, so we'll look in the datafile path list.
            fileName = osgDB::findDataFile(texture->name,options, osgDB::CASE_INSENSITIVE);
            OSG_INFO<<"texture->name="<<texture->name<<", _directory="<<_directory<<std::endl;
        }

        if (fileName.empty())
        {
            if (osgDB::containsServerAddress(_directory))
            {
                // if 3DS file is loaded from http, just attempt to load texture from same location.
                fileName = _directory + "/" + texture->name;
            }
            else
            {
                // MIKEC: We can still continue to call osgDB::readRefImageFile in case user has a ReadFileCallback registered
                //        in that case we just use the image's filename as it exists in the 3DS file
                fileName=texture->name;
            }
        }

        if (label) { OSG_DEBUG << label; }
        else { OSG_DEBUG << "texture name"; }

        OSG_DEBUG << " '"<<texture->name<<"'"<< std::endl;
        OSG_DEBUG << "    texture flag        "<<texture->flags<< std::endl;
        OSG_DEBUG << "    LIB3DS_TEXTURE_DECALE       "<<((texture->flags)&LIB3DS_TEXTURE_DECALE)<< std::endl;
        OSG_DEBUG << "    LIB3DS_TEXTURE_MIRROR       "<<((texture->flags)&LIB3DS_TEXTURE_MIRROR)<< std::endl;
        OSG_DEBUG << "    LIB3DS_TEXTURE_NEGATE       "<<((texture->flags)&LIB3DS_TEXTURE_NEGATE)<< std::endl;
        OSG_DEBUG << "    LIB3DS_TEXTURE_NO_TILE      "<<((texture->flags)&LIB3DS_TEXTURE_NO_TILE)<< std::endl;
        OSG_DEBUG << "    LIB3DS_TEXTURE_SUMMED_AREA  "<<((texture->flags)&LIB3DS_TEXTURE_SUMMED_AREA)<< std::endl;
        OSG_DEBUG << "    LIB3DS_TEXTURE_ALPHA_SOURCE "<<((texture->flags)&LIB3DS_TEXTURE_ALPHA_SOURCE)<< std::endl;
        OSG_DEBUG << "    LIB3DS_TEXTURE_TINT         "<<((texture->flags)&LIB3DS_TEXTURE_TINT)<< std::endl;
        OSG_DEBUG << "    LIB3DS_TEXTURE_IGNORE_ALPHA "<<((texture->flags)&LIB3DS_TEXTURE_IGNORE_ALPHA)<< std::endl;
        OSG_DEBUG << "    LIB3DS_TEXTURE_RGB_TINT     "<<((texture->flags)&LIB3DS_TEXTURE_RGB_TINT)<< std::endl;

        osg::ref_ptr<osg::Image> osg_image = osgDB::readRefImageFile(fileName.c_str(), options); //Absolute Path
        if (!osg_image.valid())
        {
            OSG_NOTICE << "Warning: Cannot create texture "<<texture->name<< std::endl;
            return NULL;
        }
        if (osg_image->getFileName().empty()) // it should be done in OSG with osgDB::readRefImageFile(fileName.c_str());
            osg_image->setFileName(fileName);
        osg::Texture2D* osg_texture = new osg::Texture2D;
        osg_texture->setImage(osg_image.get());
        osg_texture->setName(texture->name);
        // does the texture support transparancy?
        transparency = ((texture->flags)&LIB3DS_TEXTURE_ALPHA_SOURCE)!=0;

        // what is the wrap mode of the texture.
        osg::Texture2D::WrapMode wm = ((texture->flags)&LIB3DS_TEXTURE_NO_TILE) ?
                osg::Texture2D::CLAMP :
                osg::Texture2D::REPEAT;
        osg_texture->setWrap(osg::Texture2D::WRAP_S,wm);
        osg_texture->setWrap(osg::Texture2D::WRAP_T,wm);
        osg_texture->setWrap(osg::Texture2D::WRAP_R,wm);
                                 // bilinear.
        osg_texture->setFilter(osg::Texture2D::MIN_FILTER,osg::Texture2D::LINEAR_MIPMAP_NEAREST);

        // Insert in cache map
        texturesMap.insert(TexturesMap::value_type(texture->name, osg_texture));
        return osg_texture;
    }
    else
        return NULL;
}


ReaderWriter3DS::StateSetInfo ReaderWriter3DS::ReaderObject::createStateSet(Lib3dsMaterial *mat)
{
    if (mat==NULL) return StateSetInfo();

    bool textureTransparency=false;
    bool transparency = false;
    float alpha = 1.0f - mat->transparency;
    int unit = 0;

    osg::StateSet* stateset = new osg::StateSet;
    osg::Material* material = new osg::Material;

    osg::Vec3 ambient(mat->ambient[0],mat->ambient[1],mat->ambient[2]);
    osg::Vec3 diffuse(mat->diffuse[0],mat->diffuse[1],mat->diffuse[2]);
    osg::Vec3 specular(mat->specular[0],mat->specular[1],mat->specular[2]);
    specular *= mat->shin_strength;
    float shininess = mat->shininess*128.0f;

    // diffuse
    osg::Texture2D* texture1_map = createTexture(&(mat->texture1_map),"texture1_map",textureTransparency);
    if (texture1_map)
    {
        if(textureTransparency) transparency = true;

        // a diffuse texture can be transparent (e.g. a PNG with alpha channel)
        if (texture1_map->getImage()->isImageTranslucent()) transparency = true;

        // set UV scaling...
        // to do: import offset and scaling
        float uscale = mat->texture1_map.scale[0];
        float vscale = mat->texture1_map.scale[1];
        if (uscale != 1.0 || vscale != 1.0)
        {
            osg::ref_ptr<osg::TexMat> texmat = new osg::TexMat();
            osg::Matrix uvScaling;
            uvScaling.makeScale(osg::Vec3(uscale, vscale, 1.0));
            texmat->setMatrix(uvScaling);
            stateset->setTextureAttributeAndModes(unit, texmat.get(), osg::StateAttribute::ON);
        }

        stateset->setTextureAttributeAndModes(unit, texture1_map, osg::StateAttribute::ON);

        double factor = mat->texture1_map.percent;
        if(factor < 1.0)
        {
            osg::TexEnvCombine* texenv = new osg::TexEnvCombine();
            texenv->setCombine_RGB(osg::TexEnvCombine::MODULATE);
            texenv->setSource0_RGB(osg::TexEnvCombine::TEXTURE);
            texenv->setSource1_RGB(osg::TexEnvCombine::PREVIOUS);
            texenv->setSource2_RGB(osg::TexEnvCombine::CONSTANT);
            texenv->setConstantColor(osg::Vec4(factor, factor, factor, factor));
            stateset->setTextureAttributeAndModes(unit, texenv, osg::StateAttribute::ON);
        }
        else
        {
            // from an email from Eric Hamil, September 30, 2003.
            // According to the 3DS spec, and other
            // software (like Max, Lightwave, and Deep Exploration) a 3DS material that has
            // a non-white diffuse base color and a 100% opaque bitmap texture, will show the
            // texture with no influence from the base color.

            // so we'll override material back to white.
            // and no longer require the decal hack below...
#if 0
            // Eric original fallback
            osg::Vec4 white(1.0f,1.0f,1.0f,alpha);
            material->setAmbient(osg::Material::FRONT_AND_BACK,white);
            material->setDiffuse(osg::Material::FRONT_AND_BACK,white);
            material->setSpecular(osg::Material::FRONT_AND_BACK,white);
#else
            // try alternative to avoid saturating with white
            // setting white as per OpenGL defaults.
            ambient.set(0.2f,0.2f,0.2f);
            diffuse.set(0.8f,0.8f,0.8f);
            specular.set(0.0f,0.0f,0.0f);
#endif
        }

        unit++;
    }

    // opacity
    osg::Texture2D* opacity_map = createTexture(&(mat->opacity_map),"opacity_map", textureTransparency);
    if (opacity_map)
    {
        // set UV scaling...
        // to do: import offset and scaling
        float uscale = mat->opacity_map.scale[0];
        float vscale = mat->opacity_map.scale[1];
        if (uscale != 1.0 || vscale != 1.0)
        {
            osg::ref_ptr<osg::TexMat> texmat = new osg::TexMat();
            osg::Matrix uvScaling;
            uvScaling.makeScale(osg::Vec3(uscale, vscale, 1.0));
            texmat->setMatrix(uvScaling);
            stateset->setTextureAttributeAndModes(unit, texmat.get(), osg::StateAttribute::ON);
        }

        double factor = mat->opacity_map.percent;

        // opacity map: add an alpha channel to the image to make opacity work
        // (rebuid the image anyway if it must be rescaled according to factor)
        if(!opacity_map->getImage()->isImageTranslucent() || factor<1.0)
        {
            osg::notify(WARN)<<"Image without alpha channel for opacity. An extra alpha channel will be added."<<std::endl;
            double one_minus_factor = 1.0 - factor;
            osg::ref_ptr<osg::Image> osg_image = opacity_map->getImage();
            int nc = (int)osg_image->getPixelSizeInBits()/8;
            unsigned char* orig_img_data = (unsigned char*)osg_image->getDataPointer();
            int n = osg_image->s()*osg_image->t()*4;
            unsigned char* img_data = new unsigned char[n];
            // rebuild the texture with alpha channel (rescale according to map amount)
            for (int i=0, j=0; i<n; i+=4, j+=nc)
            {
                img_data[i] = img_data[i+1] = img_data[i+2] = img_data[i+3] = (unsigned char)(one_minus_factor + orig_img_data[j]*factor);
            }
            osg_image->setImage(osg_image->s(),osg_image->t(),osg_image->r(), GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, img_data, osg::Image::USE_NEW_DELETE);
            opacity_map->setImage(osg_image.get());
        }

        transparency = true;

        stateset->setTextureAttributeAndModes(unit, opacity_map, osg::StateAttribute::ON);

        // set up and enable the texture combiner
        osg::TexEnvCombine* texenv = new osg::TexEnvCombine();

        texenv->setCombine_RGB(osg::TexEnvCombine::REPLACE);
        texenv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS);
        texenv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR);

        texenv->setCombine_Alpha(osg::TexEnvCombine::MODULATE);
        texenv->setSource0_Alpha(osg::TexEnvCombine::TEXTURE);
        texenv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA);
        texenv->setSource1_Alpha(osg::TexEnvCombine::PRIMARY_COLOR);
        texenv->setOperand1_Alpha(osg::TexEnvCombine::SRC_ALPHA);

        stateset->setTextureAttributeAndModes(unit, texenv, osg::StateAttribute::ON);

        osg::TexEnv* tenv = new osg::TexEnv();
        tenv->setMode(osg::TexEnv::MODULATE);
        stateset->setTextureAttributeAndModes(unit, tenv, osg::StateAttribute::ON);

        unit++;
    }

    // derived from code in src/osgPlugins/fbx/fbxRMesh.cpp
    osg::ref_ptr<osg::Texture> reflection_map = createTexture(&(mat->reflection_map),"reflection_map",textureTransparency);
    if (reflection_map)
    {
        stateset->setTextureAttributeAndModes(unit, reflection_map.get(), osg::StateAttribute::ON);

        // setup spherical map...
        osg::ref_ptr<osg::TexGen> texgen = new osg::TexGen();
        texgen->setMode(osg::TexGen::SPHERE_MAP);
        stateset->setTextureAttributeAndModes(unit, texgen.get(), osg::StateAttribute::ON);

        // setup combiner for factor...
        double factor = mat->reflection_map.percent;
        osg::ref_ptr<osg::TexEnvCombine> texenv = new osg::TexEnvCombine();
        texenv->setCombine_RGB(osg::TexEnvCombine::INTERPOLATE);
        texenv->setSource0_RGB(osg::TexEnvCombine::TEXTURE);
        texenv->setSource1_RGB(osg::TexEnvCombine::PREVIOUS);
        texenv->setSource2_RGB(osg::TexEnvCombine::CONSTANT);
        texenv->setCombine_Alpha(osg::TexEnvCombine::REPLACE);
        texenv->setSource0_Alpha(osg::TexEnvCombine::CONSTANT);
        texenv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA);
        texenv->setConstantColor(osg::Vec4(factor, factor, factor, alpha));

        stateset->setTextureAttributeAndModes(unit, texenv.get(), osg::StateAttribute::ON);
        unit++;
    }

    // material
    material->setName(mat->name);
    material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4(ambient, alpha));
    material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4(diffuse, alpha));
    material->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4(specular, alpha));
    material->setShininess(osg::Material::FRONT_AND_BACK, shininess);

    stateset->setAttribute(material);

    if ((alpha < 1.0f) || transparency)
    {
        stateset->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
        stateset->setMode(GL_BLEND,osg::StateAttribute::ON);
        stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
    }

    // Set back face culling state if single sided material applied.
    // This seems like a reasonable assumption given that the backface cull option
    // doesn't appear to be encoded directly in the 3DS format, and also because
    // it mirrors the effect of code in 3DS writer which uses the face culling
    // attribute to determine the state of the 'two_sided' 3DS material being written.
    if (!mat->two_sided)
    {
        stateset->setAttributeAndModes(new osg::CullFace(osg::CullFace::BACK));
    }

/*
    osg::ref_ptr<osg::Texture> texture1_mask = createTexture(&(mat->texture1_mask),"texture1_mask",textureTransparancy);
    osg::ref_ptr<osg::Texture> texture2_map = createTexture(&(mat->texture2_map),"texture2_map",textureTransparancy);
    osg::ref_ptr<osg::Texture> texture2_mask = createTexture(&(mat->texture2_mask),"texture2_mask",textureTransparancy);
    osg::ref_ptr<osg::Texture> opacity_map = createTexture(&(mat->opacity_map),"opacity_map",textureTransparancy);
    osg::ref_ptr<osg::Texture> opacity_mask = createTexture(&(mat->opacity_mask),"opacity_mask",textureTransparancy);
    osg::ref_ptr<osg::Texture> bump_map = createTexture(&(mat->bump_map),"bump_map",textureTransparancy);
    osg::ref_ptr<osg::Texture> bump_mask = createTexture(&(mat->bump_mask),"bump_mask",textureTransparancy);
    osg::ref_ptr<osg::Texture> specular_map = createTexture(&(mat->specular_map),"specular_map",textureTransparancy);
    osg::ref_ptr<osg::Texture> specular_mask = createTexture(&(mat->specular_mask),"specular_mask",textureTransparancy);
    osg::ref_ptr<osg::Texture> shininess_map = createTexture(&(mat->shininess_map),"shininess_map",textureTransparancy);
    osg::ref_ptr<osg::Texture> shininess_mask = createTexture(&(mat->shininess_mask),"shininess_mask",textureTransparancy);
    osg::ref_ptr<osg::Texture> self_illum_map = createTexture(&(mat->self_illum_map),"self_illum_map",textureTransparancy);
    osg::ref_ptr<osg::Texture> self_illum_mask = createTexture(&(mat->self_illum_mask),"self_illum_mask",textureTransparancy);
    osg::ref_ptr<osg::Texture> reflection_map = createTexture(&(mat->reflection_map),"reflection_map",textureTransparancy);
    osg::ref_ptr<osg::Texture> reflection_mask = createTexture(&(mat->reflection_mask),"reflection_mask",textureTransparancy);
*/
    return StateSetInfo(stateset, mat);
}


osgDB::ReaderWriter::WriteResult ReaderWriter3DS::writeNode(const osg::Node& node,const std::string& fileName,const Options* options) const
{
    std::string ext = osgDB::getLowerCaseFileExtension(fileName);
    if (!acceptsExtension(ext)) return WriteResult::FILE_NOT_HANDLED;

    osgDB::makeDirectoryForFile(fileName.c_str());
    osgDB::ofstream fout(fileName.c_str(), std::ios_base::out | std::ios_base::binary);
    if (!fout.good()) return WriteResult::ERROR_IN_WRITING_FILE;
    return doWriteNode(node, fout, options, fileName);
/*
    bool ok = true;
    Lib3dsFile * file3ds = lib3ds_file_new();
    if (!file3ds) return WriteResult(WriteResult::ERROR_IN_WRITING_FILE);

    osg::ref_ptr<Options> local_opt = options ? static_cast<Options*>(options->clone(osg::CopyOp::SHALLOW_COPY)) : new Options;
    local_opt->getDatabasePathList().push_front(osgDB::getFilePath(fileName));
    if (!createFileObject(node, file3ds, fileName, local_opt)) ok = false;
    if (ok && !lib3ds_file_save(file3ds, fileName.c_str())) ok = false;
    lib3ds_file_free(file3ds);

    return ok ? WriteResult(WriteResult::FILE_SAVED) : WriteResult(WriteResult::ERROR_IN_WRITING_FILE);
*/
}


osgDB::ReaderWriter::WriteResult ReaderWriter3DS::writeNode(const osg::Node& node,std::ostream& fout,const Options* options) const
{
    //OSG_WARN << "!!WARNING!! 3DS write support is incomplete" << std::endl;
    std::string optFileName;
    if (options)
    {
        optFileName = options->getPluginStringData("STREAM_FILENAME");
    }

    return doWriteNode(node, fout, options, optFileName);
}

osgDB::ReaderWriter::WriteResult ReaderWriter3DS::doWriteNode(const osg::Node& node,std::ostream& fout, const Options* options, const std::string & fileNamelib3ds) const
{
    osg::ref_ptr<Options> local_opt = options ? static_cast<Options*>(options->clone(osg::CopyOp::SHALLOW_COPY)) : new Options;
    local_opt->getDatabasePathList().push_front(osgDB::getFilePath(fileNamelib3ds));

    Lib3dsIo io;
    io.self = &fout;
    io.seek_func = fileo_seek_func;
    io.tell_func = fileo_tell_func;
    io.read_func = NULL;
    io.write_func = fileo_write_func;
    io.log_func = fileio_log_func;

    Lib3dsFile * file3ds = lib3ds_file_new();
    if (!file3ds) return WriteResult(WriteResult::ERROR_IN_WRITING_FILE);

    bool ok = true;
    if (!createFileObject(node, file3ds, fileNamelib3ds, local_opt.get())) ok = false;
    if (ok && !lib3ds_file_write(file3ds, &io)) ok = false;
    lib3ds_file_free(file3ds);

    return ok ? WriteResult(WriteResult::FILE_SAVED) : WriteResult(WriteResult::ERROR_IN_WRITING_FILE);
    //return ok ? WriteResult(WriteResult::FILE_SAVED) : WriteResult(WriteResult::FILE_NOT_HANDLED);
}

bool ReaderWriter3DS::createFileObject(const osg::Node& node, Lib3dsFile * file3ds,const std::string& fileName, const osgDB::ReaderWriter::Options* options) const
{
    plugin3ds::WriterNodeVisitor w(file3ds, fileName, options, osgDB::getFilePath(node.getName()));
    const_cast<osg::Node &>(node).accept(w);                // Ugly const_cast<> for visitor...
    if (!w.succeeded()) return false;
    w.writeMaterials();
    return w.succeeded();
}

