// -*-c++-*-

/*
 * Wavefront OBJ loader for Open Scene Graph
 *
 * Copyright (C) 2001,2007 Ulrich Hertlein <u.hertlein@sandbox.de>
 *
 * Modified by Robert Osfield to support per Drawable coord, normal and
 * texture coord arrays, bug fixes, and support for texture mapping.
 *
 * Writing support added 2007 by Stephan Huber, http://digitalmind.de, 
 * some ideas taken from the dae-plugin
 *
 * The Open Scene Graph (OSG) is a cross platform C++/OpenGL library for 
 * real-time rendering of large 3D photo-realistic models. 
 * The OSG homepage is http://www.openscenegraph.org/
 */

#if defined(_MSC_VER)
    #pragma warning( disable : 4786 )
#endif

#include <stdlib.h>
#include <string>

#include <osg/Notify>
#include <osg/Node>
#include <osg/MatrixTransform>
#include <osg/Geode>
#include <osg/Vec3f>

#include <osg/Geometry>
#include <osg/StateSet>
#include <osg/Material>
#include <osg/Texture2D>
#include <osg/TexGen>
#include <osg/TexMat>

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

#include <osgUtil/TriStripVisitor>
#include <osgUtil/SmoothingVisitor>
#include <osgUtil/Tessellator>

#include "obj.h"
#include "OBJWriterNodeVisitor.h"

#include <map>
#include <set>

class ReaderWriterOBJ : public osgDB::ReaderWriter
{
public:
    ReaderWriterOBJ()
    {
        supportsExtension("obj","Alias Wavefront OBJ format");
        supportsOption("noRotation","Do not do the default rotate about X axis");
        supportsOption("noTesselateLargePolygons","Do not do the default tesselation of large polygons");
        supportsOption("noTriStripPolygons","Do not do the default tri stripping of polygons");
        supportsOption("generateFacetNormals","generate facet normals for verticies without normals");

        supportsOption("DIFFUSE=<unit>", "Set texture unit for diffuse texture");
        supportsOption("AMBIENT=<unit>", "Set texture unit for ambient texture");
        supportsOption("SPECULAR=<unit>", "Set texture unit for specular texture");
        supportsOption("SPECULAR_EXPONENT=<unit>", "Set texture unit for specular exponent texture");
        supportsOption("OPACITY=<unit>", "Set texture unit for opacity/dissolve texture");
        supportsOption("BUMP=<unit>", "Set texture unit for bumpmap texture");
        supportsOption("DISPLACEMENT=<unit>", "Set texture unit for displacement texture");
        supportsOption("REFLECTION=<unit>", "Set texture unit for reflection texture");

    }

    virtual const char* className() const { return "Wavefront OBJ Reader"; }

    virtual ReadResult readNode(const std::string& fileName, const osgDB::ReaderWriter::Options* options) const;

    virtual ReadResult readNode(std::istream& fin, const Options* options) const;
    
    virtual WriteResult writeObject(const osg::Object& obj,const std::string& fileName,const Options* options=NULL) const 
    {
        const osg::Node* node = dynamic_cast<const osg::Node*>(&obj);
        if (node)
            return writeNode(*node, fileName, options);
        else 
            return WriteResult(WriteResult::FILE_NOT_HANDLED); 
    }
    
    virtual WriteResult writeNode(const osg::Node& node,const std::string& fileName,const Options* options =NULL) const 
    { 
        if (!acceptsExtension(osgDB::getFileExtension(fileName)))
            return WriteResult(WriteResult::FILE_NOT_HANDLED); 
            
        osgDB::ofstream f(fileName.c_str());
        std::string materialFile = osgDB::getNameLessExtension(fileName) + ".mtl";
        OBJWriterNodeVisitor nv(f, osgDB::getSimpleFileName(materialFile));
        
        // we must cast away constness
        (const_cast<osg::Node*>(&node))->accept(nv);
        
        osgDB::ofstream mf(materialFile.c_str());
        nv.writeMaterials(mf);
         
        return WriteResult(WriteResult::FILE_SAVED); 
    }

    
    virtual WriteResult writeObject(const osg::Object& obj,std::ostream& fout,const Options* options=NULL) const 
    {
        const osg::Node* node = dynamic_cast<const osg::Node*>(&obj);
        if (node)
            return writeNode(*node, fout, options);
        else 
            return WriteResult(WriteResult::FILE_NOT_HANDLED); 
    }

    virtual WriteResult writeNode(const osg::Node& node,std::ostream& fout,const Options* =NULL) const 
    { 
        // writing to a stream does not support materials
        
        OBJWriterNodeVisitor nv(fout);
        
        // we must cast away constness
        (const_cast<osg::Node*>(&node))->accept(nv);
    
        return WriteResult(WriteResult::FILE_SAVED); 
    }



protected:

     struct ObjOptionsStruct {
        bool rotate;
        bool noTesselateLargePolygons;
        bool noTriStripPolygons;
        bool generateFacetNormals;
        bool fixBlackMaterials;
        // This is the order in which the materials will be assigned to texture maps, unless
        // otherwise overriden
        typedef std::vector< std::pair<int,obj::Material::Map::TextureMapType> > TextureAllocationMap;
        TextureAllocationMap textureUnitAllocation;
    };

    typedef std::map< std::string, osg::ref_ptr<osg::StateSet> > MaterialToStateSetMap;
    
    void buildMaterialToStateSetMap(obj::Model& model, MaterialToStateSetMap& materialToSetSetMapObj, ObjOptionsStruct& localOptions, const Options* options) const;
    
    osg::Geometry* convertElementListToGeometry(obj::Model& model, obj::Model::ElementList& elementList, ObjOptionsStruct& localOptions) const;
    
    osg::Node* convertModelToSceneGraph(obj::Model& model, ObjOptionsStruct& localOptions, const Options* options) const;

    inline osg::Vec3 transformVertex(const osg::Vec3& vec, const bool rotate) const ;
    inline osg::Vec3 transformNormal(const osg::Vec3& vec, const bool rotate) const ;
 
    ObjOptionsStruct parseOptions(const Options* options) const;


};

inline osg::Vec3 ReaderWriterOBJ::transformVertex(const osg::Vec3& vec, const bool rotate) const
{
    if(rotate==true)
    {
        return osg::Vec3(vec.x(),-vec.z(),vec.y());
    }
    else
    {
        return vec;
    }
}

inline osg::Vec3 ReaderWriterOBJ::transformNormal(const osg::Vec3& vec, const bool rotate) const
{
    if(rotate==true)
    {
        return osg::Vec3(vec.x(),-vec.z(),vec.y());
    }
    else
    {
        return vec;
    }
}


// register with Registry to instantiate the above reader/writer.
REGISTER_OSGPLUGIN(obj, ReaderWriterOBJ)

static void load_material_texture(    obj::Model &model,
                                    obj::Material::Map &map,
                                    osg::StateSet *stateset,
                                    const unsigned int texture_unit,
                                    const osgDB::Options* options)
{
    std::string filename = map.name;
    if (!filename.empty())
    {
        osg::ref_ptr< osg::Image > image;
        if ( !model.getDatabasePath().empty() ) 
        {
            // first try with database path of parent. 
            image = osgDB::readRefImageFile(model.getDatabasePath()+'/'+filename, options);
        }
        
        if ( !image.valid() )
        {
            // if not already set then try the filename as is.
            image = osgDB::readRefImageFile(filename, options);
        }

        if ( image.valid() )
        {
            osg::Texture2D* texture = new osg::Texture2D( image.get() );
            osg::Texture::WrapMode textureWrapMode;
            if(map.clamp == true)
            {
                textureWrapMode = osg::Texture::CLAMP_TO_BORDER;
                texture->setBorderColor(osg::Vec4(0.0,0.0,0.0,0.0));    // transparent
                //stateset->setMode(GL_BLEND, osg::StateAttribute::ON);
                //stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
            }
            else
            {
                textureWrapMode = osg::Texture::REPEAT;
            }

            texture->setWrap(osg::Texture2D::WRAP_R, textureWrapMode);
            texture->setWrap(osg::Texture2D::WRAP_S, textureWrapMode);
            texture->setWrap(osg::Texture2D::WRAP_T, textureWrapMode);
            stateset->setTextureAttributeAndModes( texture_unit, texture,osg::StateAttribute::ON );
            
            if ( map.type == obj::Material::Map::REFLECTION )
            {
                osg::TexGen* texgen = new osg::TexGen;
                texgen->setMode(osg::TexGen::SPHERE_MAP);
                stateset->setTextureAttributeAndModes( texture_unit,texgen,osg::StateAttribute::ON );
            }
            
            if  ( image->isImageTranslucent())
            {
                OSG_INFO<<"Found transparent image"<<std::endl;
                stateset->setMode(GL_BLEND, osg::StateAttribute::ON);
                stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
            }
        }
    }

    if (map.uScale != 1.0f || map.vScale != 1.0f ||
            map.uOffset != 0.0f || map.vOffset != 0.0f)
    {
        osg::Matrix mat;
        if (map.uScale != 1.0f || map.vScale != 1.0f)
        {
            OSG_DEBUG << "Obj TexMat scale=" << map.uScale << "," << map.vScale << std::endl;
            mat *= osg::Matrix::scale(map.uScale, map.vScale, 1.0);
        }
        if (map.uOffset != 0.0f || map.vOffset != 0.0f)
        {
            OSG_DEBUG << "Obj TexMat offset=" << map.uOffset << "," << map.uOffset << std::endl;
            mat *= osg::Matrix::translate(map.uOffset, map.vOffset, 0.0);
        }

        osg::TexMat* texmat = new osg::TexMat;
        texmat->setMatrix(mat);
        stateset->setTextureAttributeAndModes( texture_unit,texmat,osg::StateAttribute::ON );
    }
}


void ReaderWriterOBJ::buildMaterialToStateSetMap(obj::Model& model, MaterialToStateSetMap& materialToStateSetMap, ObjOptionsStruct& localOptions, const Options* options) const
{
    if (localOptions.fixBlackMaterials)
    {
        // hack to fix Maya exported models that contian all black materials.
        int numBlack = 0;
        int numNotBlack = 0;
        obj::Model::MaterialMap::iterator itr;
        for(itr = model.materialMap.begin();
            itr != model.materialMap.end();
            ++itr)
        {
            obj::Material& material = itr->second;
            if (material.ambient==osg::Vec4(0.0f,0.0f,0.0f,1.0f) &&
                material.diffuse==osg::Vec4(0.0f,0.0f,0.0f,1.0f))
            {
                ++numBlack;
            }
            else
            {
                ++numNotBlack;
            }
        }
        
        if (numNotBlack==0 && numBlack!=0)
        {
            for(itr = model.materialMap.begin();
                itr != model.materialMap.end();
                ++itr)
            {
                obj::Material& material = itr->second;
                if (material.ambient==osg::Vec4(0.0f,0.0f,0.0f,1.0f) &&
                    material.diffuse==osg::Vec4(0.0f,0.0f,0.0f,1.0f))
                {
                    material.ambient.set(0.3f,0.3f,0.3f,1.0f);
                    material.diffuse.set(1.0f,1.0f,1.0f,1.0f);
                }
            }
        }
    }
    
    for(obj::Model::MaterialMap::iterator itr = model.materialMap.begin();
        itr != model.materialMap.end();
        ++itr)
    {
        obj::Material& material = itr->second;
       
        osg::ref_ptr< osg::StateSet > stateset = new osg::StateSet;

        bool isTransparent = false;

        // handle material colors
        // http://java3d.j3d.org/utilities/loaders/obj/sun.html
        if (material.illum != 0)
        {
            osg::Material* osg_material = new osg::Material;
            stateset->setAttribute(osg_material);
            osg_material->setName(material.name);
            osg_material->setAmbient(osg::Material::FRONT_AND_BACK,material.ambient);
            osg_material->setDiffuse(osg::Material::FRONT_AND_BACK,material.diffuse);
            osg_material->setEmission(osg::Material::FRONT_AND_BACK,material.emissive);

            if (material.illum == 2) {
                osg_material->setSpecular(osg::Material::FRONT_AND_BACK,material.specular);
            } else {
                osg_material->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4(0,0,0,1));
            }
            osg_material->setShininess(osg::Material::FRONT_AND_BACK,(material.Ns/1000.0f)*128.0f ); // note OBJ shiniess is 0..1000.
            
            if (material.ambient[3]!=1.0 ||
                material.diffuse[3]!=1.0 ||
                material.specular[3]!=1.0||
                material.emissive[3]!=1.0)
            {
                OSG_INFO<<"Found transparent material"<<std::endl;
                isTransparent = true;
            }
        }
        
        // If the user has explicitly set the required texture type to unit map via the options 
        // string, then we load ONLY those textures that are in the map.
        if(localOptions.textureUnitAllocation.size()>0)
        {
            for(unsigned int i=0;i<localOptions.textureUnitAllocation.size();i++)
            {
                // firstly, get the option set pair
                int unit = localOptions.textureUnitAllocation[i].first;
                obj::Material::Map::TextureMapType type = localOptions.textureUnitAllocation[i].second;
                // secondly, see if this texture type (e.g. DIFFUSE) is one of those in the material
                int index = -1;
                for(unsigned int j=0;j<material.maps.size();j++)
                {
                    if(material.maps[j].type == type)
                    {
                        index = (int) j;
                        break;
                    }
                }
                if(index>=0) load_material_texture( model, material.maps[index], stateset.get(), unit, options );
            }
        }
        // If the user has set no options, then we load them up in the order contained in the enum. This
        // latter method is an attempt not to break user's existing code
        else
        {
            int unit = 0;
            for(int i=0;i<(int) obj::Material::Map::UNKNOWN;i++) // for each type
            {
                obj::Material::Map::TextureMapType type = (obj::Material::Map::TextureMapType) i;
                // see if this texture type (e.g. DIFFUSE) is one of those in the material
                int index = -1;
                for(unsigned int j=0;j<material.maps.size();j++)
                {
                    if(material.maps[j].type == type)
                    {
                        index = (int) j;
                        break;
                    }
                }
                if(index>=0)
                {
                    load_material_texture( model, material.maps[index], stateset.get(), unit, options );
                    unit++;
                }
            }
        }
      
        if (isTransparent)
        {
            stateset->setMode(GL_BLEND, osg::StateAttribute::ON);
            stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
        }

        materialToStateSetMap[material.name] = stateset.get();
    }
}

osg::Geometry* ReaderWriterOBJ::convertElementListToGeometry(obj::Model& model, obj::Model::ElementList& elementList, ObjOptionsStruct& localOptions) const
{
    
    unsigned int numVertexIndices = 0;
    unsigned int numNormalIndices = 0;
    unsigned int numTexCoordIndices = 0;
    
    unsigned int numPointElements = 0;
    unsigned int numPolylineElements = 0;
    unsigned int numPolygonElements = 0;

    obj::Model::ElementList::iterator itr;
        
    if (localOptions.generateFacetNormals == true) {
        for(itr=elementList.begin();
                itr!=elementList.end();
                    ++itr)
        {
            obj::Element& element = *(*itr);
            if (element.dataType==obj::Element::POINTS || element.dataType==obj::Element::POLYLINE)
                continue;
                        
            if (element.normalIndices.size() == 0) {
                // fill in the normals
                int a = element.vertexIndices[0];
                int b = element.vertexIndices[1];
                int c = element.vertexIndices[2];

                osg::Vec3f ab(model.vertices[b]);
                osg::Vec3f ac(model.vertices[c]);

                ab -= model.vertices[a];
                ac -= model.vertices[a];

                osg::Vec3f Norm( ab ^ ac );
                Norm.normalize();
                int normal_idx = model.normals.size();
                model.normals.push_back(Norm);

                for (unsigned i=0 ; i < element.vertexIndices.size() ; i++)
                    element.normalIndices.push_back(normal_idx);
            }
        }
    }
        
        
        
    for(itr=elementList.begin();
        itr!=elementList.end();
        ++itr)
    {
        obj::Element& element = *(*itr);

        numVertexIndices += element.vertexIndices.size();
        numNormalIndices += element.normalIndices.size();
        numTexCoordIndices += element.texCoordIndices.size();

        numPointElements += (element.dataType==obj::Element::POINTS) ? 1 : 0;
        numPolylineElements += (element.dataType==obj::Element::POLYLINE) ? 1 : 0;
        numPolygonElements += (element.dataType==obj::Element::POLYGON) ? 1 : 0;

    }

    if (numVertexIndices==0) return 0;
    
    if (numNormalIndices!=0 && numNormalIndices!=numVertexIndices)
    {
        OSG_NOTICE<<"Incorrect number of normals, ignore them"<<std::endl;
        numNormalIndices = 0;
    }
    
    if (numTexCoordIndices!=0 && numTexCoordIndices!=numVertexIndices)
    {
        OSG_NOTICE<<"Incorrect number of normals, ignore them"<<std::endl;
        numTexCoordIndices = 0;
    }
    
    osg::Vec3Array* vertices = numVertexIndices ? new osg::Vec3Array : 0;
    osg::Vec3Array* normals = numNormalIndices ? new osg::Vec3Array : 0;
    osg::Vec2Array* texcoords = numTexCoordIndices ? new osg::Vec2Array : 0;
    
    if (vertices) vertices->reserve(numVertexIndices);
    if (normals) normals->reserve(numNormalIndices);
    if (texcoords) texcoords->reserve(numTexCoordIndices);
    
    osg::Geometry* geometry = new osg::Geometry;
    if (vertices) geometry->setVertexArray(vertices);
    if (normals)
    {
        geometry->setNormalArray(normals);
        geometry->setNormalBinding(osg::Geometry::BIND_PER_VERTEX);
    }
    if (texcoords)
    {
        geometry->setTexCoordArray(0,texcoords);
    }
    

    if (numPointElements>0)
    {
        unsigned int startPos = vertices->size();
        unsigned int numPoints = 0;
        for(itr=elementList.begin();
            itr!=elementList.end();
            ++itr)
        {
            obj::Element& element = *(*itr);
            if (element.dataType==obj::Element::POINTS)
            {
                for(obj::Element::IndexList::iterator index_itr = element.vertexIndices.begin();
                    index_itr != element.vertexIndices.end();
                    ++index_itr)
                {
                    vertices->push_back(transformVertex(model.vertices[*index_itr],localOptions.rotate));
                    ++numPoints;
                }
                if (numNormalIndices)
                {
                    for(obj::Element::IndexList::iterator index_itr = element.normalIndices.begin();
                        index_itr != element.normalIndices.end();
                        ++index_itr)
                    {
                        normals->push_back(transformNormal(model.normals[*index_itr],localOptions.rotate));
                    }
                }
                if (numTexCoordIndices)
                {
                    for(obj::Element::IndexList::iterator index_itr = element.texCoordIndices.begin();
                        index_itr != element.texCoordIndices.end();
                        ++index_itr)
                    {
                        texcoords->push_back(model.texcoords[*index_itr]);
                    }
                }
            }
        }

        osg::DrawArrays* drawArrays = new osg::DrawArrays(GL_POINTS,startPos,numPoints);
        geometry->addPrimitiveSet(drawArrays);
    }
    
    if (numPolylineElements>0)
    {
        unsigned int startPos = vertices->size();
        osg::DrawArrayLengths* drawArrayLengths = new osg::DrawArrayLengths(GL_LINES,startPos);

        for(itr=elementList.begin();
            itr!=elementList.end();
            ++itr)
        {
            obj::Element& element = *(*itr);
            if (element.dataType==obj::Element::POLYLINE)
            {
                drawArrayLengths->push_back(element.vertexIndices.size());

                for(obj::Element::IndexList::iterator index_itr = element.vertexIndices.begin();
                    index_itr != element.vertexIndices.end();
                    ++index_itr)
                {
                    vertices->push_back(transformVertex(model.vertices[*index_itr],localOptions.rotate));
                }
                if (numNormalIndices)
                {
                    for(obj::Element::IndexList::iterator index_itr = element.normalIndices.begin();
                        index_itr != element.normalIndices.end();
                        ++index_itr)
                    {
                        normals->push_back(transformNormal(model.normals[*index_itr],localOptions.rotate));
                    }
                }
                if (numTexCoordIndices)
                {
                    for(obj::Element::IndexList::iterator index_itr = element.texCoordIndices.begin();
                        index_itr != element.texCoordIndices.end();
                        ++index_itr)
                    {
                        texcoords->push_back(model.texcoords[*index_itr]);
                    }
                }
            }
        }

        geometry->addPrimitiveSet(drawArrayLengths);

    }

    // #define USE_DRAWARRAYLENGTHS

    if (numPolygonElements>0)
    {
        unsigned int startPos = vertices->size();
        
        #ifdef USE_DRAWARRAYLENGTHS
            osg::DrawArrayLengths* drawArrayLengths = new osg::DrawArrayLengths(GL_POLYGON,startPos);
            geometry->addPrimitiveSet(drawArrayLengths);
        #endif

        for(itr=elementList.begin();
            itr!=elementList.end();
            ++itr)
        {
            obj::Element& element = *(*itr);
            if (element.dataType==obj::Element::POLYGON)
            {

                




                #ifdef USE_DRAWARRAYLENGTHS
                    drawArrayLengths->push_back(element.vertexIndices.size());
                #else
                    if (element.vertexIndices.size()>4)
                    {
                        osg::DrawArrays* drawArrays = new osg::DrawArrays(GL_POLYGON,startPos,element.vertexIndices.size());
                        startPos += element.vertexIndices.size();
                        geometry->addPrimitiveSet(drawArrays);
                    }
                    else
                    {
                        osg::DrawArrays* drawArrays = new osg::DrawArrays(GL_TRIANGLE_FAN,startPos,element.vertexIndices.size());
                        startPos += element.vertexIndices.size();
                        geometry->addPrimitiveSet(drawArrays);
                    }
                #endif

            
                if (model.needReverse(element))
                {
                    // need to reverse so add to OSG arrays in same order as in OBJ, as OSG assume anticlockwise ordering.
                    for(obj::Element::IndexList::reverse_iterator index_itr = element.vertexIndices.rbegin();
                        index_itr != element.vertexIndices.rend();
                        ++index_itr)
                    {
                        vertices->push_back(transformVertex(model.vertices[*index_itr],localOptions.rotate));
                    }
                    if (numNormalIndices)
                    {
                        for(obj::Element::IndexList::reverse_iterator index_itr = element.normalIndices.rbegin();
                            index_itr != element.normalIndices.rend();
                            ++index_itr)
                        {
                            normals->push_back(transformNormal(model.normals[*index_itr],localOptions.rotate));
                        }
                    }


                    if (numTexCoordIndices)
                    {
                        for(obj::Element::IndexList::reverse_iterator index_itr = element.texCoordIndices.rbegin();
                            index_itr != element.texCoordIndices.rend();
                            ++index_itr)
                        {
                            texcoords->push_back(model.texcoords[*index_itr]);
                        }
                    }
                }
                else
                {
                    // no need to reverse so add to OSG arrays in same order as in OBJ.
                    for(obj::Element::IndexList::iterator index_itr = element.vertexIndices.begin();
                        index_itr != element.vertexIndices.end();
                        ++index_itr)
                    {
                        vertices->push_back(transformVertex(model.vertices[*index_itr],localOptions.rotate));
                    }
                    if (numNormalIndices)
                    {
                        for(obj::Element::IndexList::iterator index_itr = element.normalIndices.begin();
                            index_itr != element.normalIndices.end();
                            ++index_itr)
                        {
                            normals->push_back(transformNormal(model.normals[*index_itr],localOptions.rotate));
                        }
                    }
                    if (numTexCoordIndices)
                    {
                        for(obj::Element::IndexList::iterator index_itr = element.texCoordIndices.begin();
                            index_itr != element.texCoordIndices.end();
                            ++index_itr)
                        {
                            texcoords->push_back(model.texcoords[*index_itr]);
                        }
                    }
                }
            }
        }


    }
    
    return geometry;
}

osg::Node* ReaderWriterOBJ::convertModelToSceneGraph(obj::Model& model, ObjOptionsStruct& localOptions, const Options* options) const
{

    if (model.elementStateMap.empty()) return 0;

    osg::Group* group = new osg::Group;

    // set up the materials
    MaterialToStateSetMap materialToStateSetMap;
    buildMaterialToStateSetMap(model, materialToStateSetMap, localOptions, options);

    // go through the groups of related elements and build geometry from them.
    for(obj::Model::ElementStateMap::iterator itr=model.elementStateMap.begin();
        itr!=model.elementStateMap.end();
        ++itr)
    {
    
        const obj::ElementState& es = itr->first;
        obj::Model::ElementList& el = itr->second;

        osg::Geometry* geometry = convertElementListToGeometry(model,el,localOptions);

        if (geometry)
        {
            MaterialToStateSetMap::const_iterator it = materialToStateSetMap.find(es.materialName);
            if (it == materialToStateSetMap.end())
            {
                OSG_WARN << "Obj unable to find material '" << es.materialName << "'" << std::endl;
            }

            osg::StateSet* stateset = materialToStateSetMap[es.materialName].get();
            geometry->setStateSet(stateset);
        
            // tesseleate any large polygons
            if (!localOptions.noTesselateLargePolygons)
            {
                osgUtil::Tessellator tessellator;
                tessellator.retessellatePolygons(*geometry);
            }
            
            // tri strip polygons to improve graphics peformance
            if (!localOptions.noTriStripPolygons)
            {
                osgUtil::TriStripVisitor tsv;
                tsv.stripify(*geometry);
            }
            
            // if no normals present add them.
            if (localOptions.generateFacetNormals==false && (!geometry->getNormalArray() || geometry->getNormalArray()->getNumElements()==0))
            {
                osgUtil::SmoothingVisitor sv;
                sv.smooth(*geometry);
            }


            osg::Geode* geode = new osg::Geode;
            geode->addDrawable(geometry);
            
            if (es.objectName.empty())
            {
                geode->setName(es.groupName);
            }
            else if (es.groupName.empty())
            {
                geode->setName(es.objectName);
            }
            else
            {
                geode->setName(es.groupName + std::string(":") + es.objectName);
            }

            group->addChild(geode);

        }
    }

    return group;
}

ReaderWriterOBJ::ObjOptionsStruct ReaderWriterOBJ::parseOptions(const osgDB::ReaderWriter::Options* options) const
{
    ObjOptionsStruct localOptions;
    localOptions.rotate = true;
    localOptions.noTesselateLargePolygons = false;
    localOptions.noTriStripPolygons = false;
    localOptions.generateFacetNormals = false;
    localOptions.fixBlackMaterials = true;

    if (options!=NULL)
    {
        std::istringstream iss(options->getOptionString());
        std::string opt;
        while (iss >> opt)
        {
            // split opt into pre= and post=
            std::string pre_equals;
            std::string post_equals;

            size_t found = opt.find("=");
            if(found!=std::string::npos)
            {
                pre_equals = opt.substr(0,found);
                post_equals = opt.substr(found+1);
            } 
            else
            {
                pre_equals = opt;
            }

            if (pre_equals == "noRotation")
            {
                localOptions.rotate = false;
            }                
            else if (pre_equals == "noTesselateLargePolygons")
            {
                localOptions.noTesselateLargePolygons = true;
            }                
            else if (pre_equals == "noTriStripPolygons")
            {
                localOptions.noTriStripPolygons = true;
            }
            else if (pre_equals == "generateFacetNormals")
            {
                localOptions.generateFacetNormals = true;
            }
            else if (post_equals.length()>0)
            {    
                obj::Material::Map::TextureMapType type = obj::Material::Map::UNKNOWN;                        
                // Now we check to see if we have anything forcing a texture allocation
                if        (pre_equals == "DIFFUSE")            type = obj::Material::Map::DIFFUSE;
                else if (pre_equals == "AMBIENT")            type = obj::Material::Map::AMBIENT;
                else if (pre_equals == "SPECULAR")            type = obj::Material::Map::SPECULAR;
                else if (pre_equals == "SPECULAR_EXPONENT") type = obj::Material::Map::SPECULAR_EXPONENT;
                else if (pre_equals == "OPACITY")            type = obj::Material::Map::OPACITY;
                else if (pre_equals == "BUMP")                type = obj::Material::Map::BUMP;
                else if (pre_equals == "DISPLACEMENT")        type = obj::Material::Map::DISPLACEMENT;
                else if (pre_equals == "REFLECTION")        type = obj::Material::Map::REFLECTION;
                
                if (type!=obj::Material::Map::UNKNOWN)
                {
                    int unit = atoi(post_equals.c_str());    // (probably should use istringstream rather than atoi)
                    localOptions.textureUnitAllocation.push_back(std::make_pair(unit,(obj::Material::Map::TextureMapType) type));
                    OSG_NOTICE<<"Obj Found map in options, ["<<pre_equals<<"]="<<unit<<std::endl;
                }
            }
        }
    }
    return localOptions;
}


// read file and convert to OSG.
osgDB::ReaderWriter::ReadResult ReaderWriterOBJ::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;
    
    
    osgDB::ifstream fin(fileName.c_str());
    if (fin)
    {
    
        // code for setting up the database path so that internally referenced file are searched for on relative paths. 
        osg::ref_ptr<Options> local_opt = options ? static_cast<Options*>(options->clone(osg::CopyOp::SHALLOW_COPY)) : new Options;
        local_opt->setDatabasePath(osgDB::getFilePath(fileName));

        obj::Model model;
        model.setDatabasePath(osgDB::getFilePath(fileName.c_str()));
        model.readOBJ(fin, local_opt.get());

        ObjOptionsStruct localOptions = parseOptions(options);

        osg::Node* node = convertModelToSceneGraph(model, localOptions, options);
        return node;
    }
    
    return ReadResult::FILE_NOT_HANDLED;
}

osgDB::ReaderWriter::ReadResult ReaderWriterOBJ::readNode(std::istream& fin, const Options* options) const
{
    if (fin)
    {
        obj::Model model;
        model.readOBJ(fin, options);
        
        ObjOptionsStruct localOptions = parseOptions(options);
        
        osg::Node* node = convertModelToSceneGraph(model, localOptions, options);
        return node;
    }
    
    return ReadResult::FILE_NOT_HANDLED;
}
