// -*-c++-*-

/*
 * Lightwave Object loader for Open Scene Graph
 *
 * Copyright (C) 2001 Ulrich Hertlein <u.hertlein@web.de>
 * Improved LWO2 reader is (C) 2003-2004 Marco Jez <marco.jez@poste.it>
 *
 * 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 <string>
#include <memory>
#include <sstream>
#include <algorithm>

#include <osg/Notify>
#include <osg/Node>
#include <osg/Group>
#include <osg/Geode>
#include <osg/Group>
#include <osg/Texture2D>
#include <osg/Geometry>
#include <osg/StateSet>

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

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

#include <string.h>

#include "Converter.h"
#include "VertexMap.h"

#include "old_lw.h"
#include "old_Lwo2.h"

class ReaderWriterLWO : public osgDB::ReaderWriter
{
public:
    ReaderWriterLWO()
    {
        supportsExtension("lwo","Lightwave object format");
        supportsExtension("lw","Lightwave object format");
        supportsExtension("geo","Lightwave geometry format");
    }
    
    virtual const char* className() const { return "Lightwave Object Reader"; }

    virtual ReadResult 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;

        // 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));

        ReadResult result = readNode_LWO1(fileName,local_opt.get());
        if (result.success()) return result;
        
        if (!options || options->getOptionString() != "USE_OLD_READER") {
            ReadResult result = readNode_LWO2(fileName, local_opt.get());
            if (result.success()) return result;
        }

        return readNode_old_LWO2(fileName, local_opt.get());
    }

    lwosg::Converter::Options parse_options(const Options *options) const;

    virtual ReadResult readNode_LWO2(const std::string& fileName, const osgDB::ReaderWriter::Options*) const;
    virtual ReadResult readNode_old_LWO2(const std::string& fileName, const osgDB::ReaderWriter::Options*) const;
    virtual ReadResult readNode_LWO1(const std::string& fileName, const osgDB::ReaderWriter::Options*) const;

protected:

    

};

lwosg::Converter::Options ReaderWriterLWO::parse_options(const Options *options) const
{
    lwosg::Converter::Options conv_options;

    if (options) {
        std::istringstream iss(options->getOptionString());
        std::string opt;
        while (iss >> opt) {
            if (opt == "COMBINE_GEODES")           conv_options.combine_geodes = true;
            if (opt == "FORCE_ARB_COMPRESSION")    conv_options.force_arb_compression = true;
            if (opt == "USE_OSGFX")                conv_options.use_osgfx = true;
            if (opt == "NO_LIGHTMODEL_ATTRIBUTE")  conv_options.apply_light_model = false;
            if (opt == "BIND_TEXTURE_MAP")
            {
                std::string mapname;
                int unit;
                if (iss >> mapname >> unit)
                {
                    conv_options.texturemap_bindings.insert(lwosg::VertexMap_binding_map::value_type(mapname,  unit));
                }
            }
            if (opt == "MAX_TEXTURE_UNITS") {
                int n;
                if (iss >> n) {
                    conv_options.max_tex_units = n;
                }
            }
        }
    }

    return conv_options;
}


// register with Registry to instantiate the above reader/writer.
REGISTER_OSGPLUGIN(lwo, ReaderWriterLWO)

osgDB::ReaderWriter::ReadResult ReaderWriterLWO::readNode_LWO2(const std::string &fileName, const osgDB::ReaderWriter::Options *options) const
{
    lwosg::Converter::Options conv_options = parse_options(options);

    lwosg::Converter converter(conv_options, options);
    osg::ref_ptr<osg::Node> node = converter.convert(fileName);
    if (node.valid()) {
        return node.release();
    }

    return ReadResult::FILE_NOT_HANDLED;
}


osgDB::ReaderWriter::ReadResult ReaderWriterLWO::readNode_old_LWO2(const std::string& fileName, const osgDB::ReaderWriter::Options*) const
{
    std::auto_ptr<Lwo2> lwo2(new Lwo2());
    if (lwo2->ReadFile(fileName))
    {
        osg::ref_ptr<Group> group = new osg::Group();
        if (lwo2->GenerateGroup(*group)) return group.release();
    }
    return ReadResult::FILE_NOT_HANDLED;
}





// collect all the data relavent to a particular osg::Geometry being created.
struct GeometryCollection
{
    GeometryCollection():
        _numPrimitives(0),
        _numPrimitivesWithTexCoords(0),
        _numPoints(0),
        _texturesActive(false),
        _vertices(osg::Vec3Array::iterator()),
        _texcoords(osg::Vec2Array::iterator()),
        _coordCount(0),
        _geom(0) {}

    int                         _numPrimitives;
    int                         _numPrimitivesWithTexCoords;
    int                         _numPoints;
    bool                        _texturesActive;
    osg::Vec3Array::iterator    _vertices;
    osg::Vec2Array::iterator    _texcoords;
    int                         _coordCount;
    osg::Geometry*              _geom;
};



// read file and convert to OSG.
osgDB::ReaderWriter::ReadResult ReaderWriterLWO::readNode_LWO1(const std::string& fileName, const osgDB::ReaderWriter::Options*) const
{
    lwObject* lw = lw_object_read(fileName.c_str(),osg::notify(osg::INFO));
    if (!lw)
        return ReadResult::FILE_NOT_HANDLED;

    osg::notify(osg::INFO) << "faces " << lw->face_cnt << std::endl;
    osg::notify(osg::INFO) << "materials " << lw->material_cnt << std::endl;
    osg::notify(osg::INFO) << "vertices " << lw->vertex_cnt << std::endl;

    typedef std::map<int,GeometryCollection> MaterialToGeometryCollectionMap;
    MaterialToGeometryCollectionMap mtgcm;
    
    // bin the indices for each material into the mtis;
    int i;
    for (i = 0; i < lw->face_cnt; ++i)
    {
        lwFace& face = lw->face[i];
        if (face.index_cnt>=3)
        {
            GeometryCollection& gc = mtgcm[face.material];
            gc._numPoints += face.index_cnt;
            gc._numPrimitives += 1;
            if (face.texcoord) gc._numPrimitivesWithTexCoords += 1;
        }
    }
    
    MaterialToGeometryCollectionMap::iterator itr;
    for(itr=mtgcm.begin(); itr!=mtgcm.end(); ++itr)
    {
        GeometryCollection& gc = itr->second;

        if (gc._numPrimitives)
        {
            lwMaterial& lw_material = lw->material[itr->first];

            gc._geom = new osg::Geometry;
            
            osg::Vec3Array* vertArray = new osg::Vec3Array(gc._numPoints);
            gc._vertices = vertArray->begin();
            gc._geom->setVertexArray(vertArray);

            // set up color.
            osg::Vec4Array* colors = new osg::Vec4Array(1);
            (*colors)[0].set(lw_material.r,
                             lw_material.g,
                             lw_material.b,
                             1.0f);
                             
            gc._geom->setColorArray(colors);
            gc._geom->setColorBinding(osg::Geometry::BIND_OVERALL);
    
            // set up texture if needed.
            if (gc._numPrimitivesWithTexCoords==gc._numPrimitives)
            {
                if (lw_material.ctex.flags && strlen(lw_material.ctex.name)!=0)
                {
                    osg::notify(osg::INFO) << "ctex " << lw_material.ctex.name << std::endl;
                    osg::ref_ptr<osg::Image> image = osgDB::readRefImageFile(lw_material.ctex.name);
                    if (image.valid())
                    {
                        // create state
                        osg::StateSet* stateset = new osg::StateSet;

                        // create texture
                        osg::Texture2D* texture = new osg::Texture2D;
                        texture->setImage(image.get());

                        // texture wrap mode
                        static osg::Texture::WrapMode mode[] = {
                            osg::Texture::CLAMP,
                            osg::Texture::CLAMP,
                            osg::Texture::REPEAT,
                            osg::Texture::MIRROR
                        };
                        texture->setWrap(osg::Texture::WRAP_S,
                                         mode[lw_material.ctex.u_wrap]);
                        texture->setWrap(osg::Texture::WRAP_T,
                                         mode[lw_material.ctex.v_wrap]);

                        stateset->setTextureAttributeAndModes(0,texture,osg::StateAttribute::ON);
                        gc._texturesActive=true;
                        
                        gc._geom->setStateSet(stateset);

                        osg::Vec2Array* texcoordArray = new osg::Vec2Array(gc._numPoints);
                        gc._texcoords = texcoordArray->begin();
                        gc._geom->setTexCoordArray(0,texcoordArray);
                    }
                }
            }
        }        
    }
    
    
    for (i = 0; i < lw->face_cnt; ++i)
    {
        lwFace& face = lw->face[i];
        if (face.index_cnt>=3)
        {
            GeometryCollection& gc = mtgcm[face.material];
            
            osg::PrimitiveSet::Mode mode;
            switch(face.index_cnt)
            {
                case(0):
                    mode = osg::PrimitiveSet::POINTS;
                    break;
                case(1):
                    mode = osg::PrimitiveSet::POINTS;
                    break;
                case(2):
                    mode = osg::PrimitiveSet::LINES;
                    break;
                case(3):
                    mode = osg::PrimitiveSet::TRIANGLES;
                    break;
                case(4):
                    mode = osg::PrimitiveSet::QUADS;
                    break;
                default:
                    mode = osg::PrimitiveSet::POLYGON;
                    break;
            }
                        
            gc._geom->addPrimitiveSet(new osg::DrawArrays(mode,gc._coordCount,face.index_cnt));
            gc._coordCount += face.index_cnt;

            // From the spec_low.lxt :
            //   "By convention, the +X direction is to the right or east, the +Y
            //    direction is upward, and the +Z direction is forward or north"
            // However, the osg sticks to the more conventional, y to the north,
            // z upwards, x is the same - rigth/east.  To handle this difference
            // simple exchange osg_z for lwo_y, and osg_y for lwo_z.

            // add the corners in reverse order to reverse the windings, to keep the anticlockwise rotation of polys.
            int j;
            for(j=face.index_cnt-1;j>=0;--j)
            {
                (*gc._vertices++).set(lw->vertex[face.index[j]*3], lw->vertex[face.index[j]*3+2], lw->vertex[face.index[j]*3+1]);
            }
            
            if (gc._texturesActive && face.texcoord)
            {
                for(j=face.index_cnt-1;j>=0;--j)
                {
                    (*gc._texcoords++).set(face.texcoord[j*2],face.texcoord[j*2+1]);
                }            
            }            
        }
    }

    osg::Geode* geode = new osg::Geode;
    
    osgUtil::Tessellator tessellator;
    
    // add everthing into the Geode.    
    osgUtil::SmoothingVisitor smoother;
    for(itr=mtgcm.begin();
        itr!=mtgcm.end();
        ++itr)
    {
        GeometryCollection& gc = itr->second;
        if (gc._geom)
        {
            
            tessellator.retessellatePolygons(*gc._geom);
        
            smoother.smooth(*gc._geom);
            
            geode->addDrawable(gc._geom);
        }

    }

    // free
    lw_object_free(lw);

    return geode;
}
