/* -*-c++-*- OpenSceneGraph - Copyright (C) 1998-2006 Robert Osfield 
 *
 * This library is open source and may be redistributed and/or modified under  
 * the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or 
 * (at your option) any later version.  The full license is in LICENSE file
 * included with this distribution, and on the openscenegraph.org website.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 * OpenSceneGraph Public License for more details.
*/

//
// OpenFlightŪ loader for OpenSceneGraph
//
//  Copyright (C) 2005-2007  Brede Johansen
//

#include <stdexcept>
#include <osg/Notify>
#include <osg/ProxyNode>
#include <osgDB/FileNameUtils>
#include <osgDB/FileUtils>
#include <osgDB/Registry>
#include <osgDB/ReadFile>
#include <OpenThreads/ReentrantMutex>
#include <osgUtil/Optimizer>

#include "Registry.h"
#include "Document.h"
#include "RecordInputStream.h"
#include "DataOutputStream.h"
#include "FltExportVisitor.h"
#include "ExportOptions.h"

#define SERIALIZER() OpenThreads::ScopedLock<OpenThreads::ReentrantMutex> lock(_serializerMutex)  

using namespace flt;
using namespace osg;
using namespace osgDB;


class ReadExternalsVisitor : public osg::NodeVisitor
{
    osg::ref_ptr<ReaderWriter::Options> _options;
    bool _cloneExternalReferences;

public:

    ReadExternalsVisitor(ReaderWriter::Options* options) :
        osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN),
        _options(options),
        _cloneExternalReferences(false)
    {
        if (options)
            _cloneExternalReferences = (options->getOptionString().find("cloneExternalReferences")!=std::string::npos);
    }

    virtual ~ReadExternalsVisitor() {}

    virtual void apply(ProxyNode& node)
    {
        // Transfer ownership of pools.
        _options->setUserData( node.getUserData() );
        node.setUserData(NULL);

        for (unsigned int pos=0; pos<node.getNumFileNames(); pos++)
        {
            std::string filename = node.getFileName(pos);

            // read external
            osg::ref_ptr<osg::Node> external = osgDB::readNodeFile(filename,_options.get());
            if (external.valid())
            {
                if (_cloneExternalReferences)
                    external = dynamic_cast<osg::Node*>(external->clone(osg::CopyOp(osg::CopyOp::DEEP_COPY_NODES)));

                node.addChild(external.get());
            }
        }
    }
};



/*!

FLTReaderWriter supports importing and exporting OSG scene graphs
from/to OpenFlight files.

<table>
  <tr>
    <th></th>
    <th align="center">Node</th>
    <th align="center">Object</th>
    <th align="center">Image</th>
    <th align="center">HeightField</th>
  </tr>
  <tr>
    <td align="right">Read</td>
    <td align="center"><strong>X</strong></td>
    <td></td>
    <td></td>
    <td></td>
  </tr>
  <tr>
    <td align="right">Write</td>
    <td align="center"><strong>X</strong></td>
    <td></td>
    <td></td>
    <td></td>
  </tr>
</table>

*/

class FLTReaderWriter : public ReaderWriter
{
    public:
        FLTReaderWriter()
          : _implicitPath( "." )
        {
            supportsExtension("flt","OpenFlight format");
            
            supportsOption("clampToEdge","Import option");
            supportsOption("keepExternalReferences","Import option");
            supportsOption("preserveFace","Import option");
            supportsOption("preserveObject","Import option");
            supportsOption("dofAnimation","Import option");
            supportsOption("billboardCenter","Import option");
            supportsOption("noTextureAlphaForTransparancyBinning","Import option");
            supportsOption("readObjectRecordData","Import option");
            supportsOption("noUnitsConversion","Import option");
            supportsOption("convertToFeet","Import option");
            supportsOption("convertToInches","Import option");
            supportsOption("convertToMeters","Import option");
            supportsOption("convertToKilometers","Import option");
            supportsOption("convertToNauticalMiles","Import option");

            supportsOption( "version=<ver>", "Export option: Specifies the version of the output OpenFlight file. Supported values include 15.7, 15.8, and 16.1. Default is 16.1. Example: \"version=15.8\"." );
            supportsOption( "units=<units>", "Export option: Specifies the contents of the Units field of the OpenFliht header record. Valid values include INCHES, FEET, METERS, KILOMETERS, and NATICAL_MILES. Default is METERS. Example: \"units=METERS\"." );
            supportsOption( "validate", "Export option: If present in the Options string, the plugin does not write an OpenFlight file. Instead, it returns an indication of the scene graph's suitability for OpenFlight export." );
            supportsOption( "tempDir=<dir>", "Export option: Specifies the directory to use for creation of temporary files. If not specified, the directory is taken from the file name. If the file doesn't contain a path, the current working directory is used. Applications should set this to the name of their app-specific temp directory. If the path contains spaces, use double quotes to ensure correct parsing. Examples: \"tempDir=/tmp\", \"tempDir=\"C:\\My Temp Dir\"." );
            supportsOption( "lighting=<ON|OFF>", "Export option: Specifies a default enable/disable state for lighting, for Nodes in the exported scene graph that don't set it explicitly. By default, the exporter assumes lighting is enabled (GL_LIGHTING ON). Set this to either ON or OFF. Example: \"lighting=OFF\"." );
            supportsOption( "stripTextureFilePath", "Export option: If present in the Options string, the exporter strips the path from texture file names, and writes only the texture file name to the FLT Texture Palette. By default, the exporter doesn't strip the path, and the name written to the Texture Palette is taken directly from the osg::Image object referenced by the osg::Texture2D StateAttribute." );
        }

        virtual const char* className() const { return "FLT Reader/Writer"; }

        virtual bool acceptsExtension(const std::string& extension) const
        {
            return equalCaseInsensitive(extension,"flt") || extension.empty();
        }

        virtual ReadResult readObject(const std::string& file, const Options* options) const
        {
            return readNode(file, options);
        }
        
        virtual ReadResult readNode(const std::string& file, const Options* options) const
        {
            SERIALIZER();

            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;

            // in local cache?
            {
                osg::Node* node = flt::Registry::instance()->getExternalFromLocalCache(fileName);
                if (node)
                    return ReadResult(node, ReaderWriter::ReadResult::FILE_LOADED_FROM_CACHE);
            }

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

            // read file
            {
                osgDB::ifstream istream;
                istream.imbue(std::locale::classic());
                istream.open(fileName.c_str(), std::ios::in | std::ios::binary);

                if (istream)
                {
                    rr = readNode(istream,local_opt.get());
                }
            }

            static int nestedExternalsLevel = 0;
            if (rr.success())
            {
                // add to local cache.
                flt::Registry::instance()->addExternalToLocalCache(fileName,rr.getNode());
        
                bool keepExternalReferences = false;
                if (options)
                    keepExternalReferences = (options->getOptionString().find("keepExternalReferences")!=std::string::npos);


                if ( !keepExternalReferences )
                {
                    osg::notify(osg::DEBUG_INFO) << "keepExternalReferences not found, so externals will be re-readed"<<std::endl;
                    // read externals.
                    if (rr.getNode())
                    {
                        nestedExternalsLevel++;
                        ReadExternalsVisitor visitor(local_opt.get());
                        rr.getNode()->accept(visitor);
                        nestedExternalsLevel--;
                    }
                }
                else
                {
                    osg::notify(osg::DEBUG_INFO) << "keepExternalReferences found, so externals will be left as ProxyNodes"<<std::endl;    
                }
            }

            // clear local cache.
            if (nestedExternalsLevel==0)
                flt::Registry::instance()->clearLocalCache();

            return rr;
        }
        
        virtual ReadResult readObject(std::istream& fin, const Options* options) const
        {
            return readNode(fin, options);
        }
        
        virtual ReadResult readNode(std::istream& fin, const Options* options) const
        {
            Document document;
            document.setOptions(options);

            // option string and parent pools
            if (options)
            {
                const char readerMsg[] = "flt reader option: ";
                
                document.setReplaceClampWithClampToEdge((options->getOptionString().find("clampToEdge")!=std::string::npos));
                osg::notify(osg::DEBUG_INFO) << readerMsg << "clampToEdge=" << document.getReplaceClampWithClampToEdge() << std::endl;

                document.setKeepExternalReferences((options->getOptionString().find("keepExternalReferences")!=std::string::npos));
                osg::notify(osg::DEBUG_INFO) << readerMsg << "keepExternalReferences=" << document.getKeepExternalReferences() << std::endl;

                document.setPreserveFace((options->getOptionString().find("preserveFace")!=std::string::npos));
                osg::notify(osg::DEBUG_INFO) << readerMsg << "preserveFace=" << document.getPreserveFace() << std::endl;

                document.setPreserveObject((options->getOptionString().find("preserveObject")!=std::string::npos));
                osg::notify(osg::DEBUG_INFO) << readerMsg << "preserveObject=" << document.getPreserveObject() << std::endl;

                document.setDefaultDOFAnimationState((options->getOptionString().find("dofAnimation")!=std::string::npos));
                osg::notify(osg::DEBUG_INFO) << readerMsg << "dofAnimation=" << document.getDefaultDOFAnimationState() << std::endl;

                document.setUseBillboardCenter((options->getOptionString().find("billboardCenter")!=std::string::npos));
                osg::notify(osg::DEBUG_INFO) << readerMsg << "billboardCenter=" << document.getUseBillboardCenter() << std::endl;

                document.setUseTextureAlphaForTransparancyBinning(options->getOptionString().find("noTextureAlphaForTransparancyBinning")==std::string::npos);
                osg::notify(osg::DEBUG_INFO) << readerMsg << "noTextureAlphaForTransparancyBinning=" << !document.getUseTextureAlphaForTransparancyBinning() << std::endl;

                document.setReadObjectRecordData(options->getOptionString().find("readObjectRecordData")==std::string::npos);
                osg::notify(osg::DEBUG_INFO) << readerMsg << "readObjectRecordData=" << !document.getReadObjectRecordData() << std::endl;

                document.setDoUnitsConversion((options->getOptionString().find("noUnitsConversion")==std::string::npos)); // default to true, unless noUnitsConversion is specified.
                osg::notify(osg::DEBUG_INFO) << readerMsg << "noUnitsConversion=" << !document.getDoUnitsConversion() << std::endl;

                if (document.getDoUnitsConversion())
                {
                    if (options->getOptionString().find("convertToFeet")!=std::string::npos)
                        document.setDesiredUnits(FEET);
                    else if (options->getOptionString().find("convertToInches")!=std::string::npos)
                        document.setDesiredUnits(INCHES);
                    else if (options->getOptionString().find("convertToMeters")!=std::string::npos)
                        document.setDesiredUnits(METERS);
                    else if (options->getOptionString().find("convertToKilometers")!=std::string::npos)
                        document.setDesiredUnits(KILOMETERS);
                    else if (options->getOptionString().find("convertToNauticalMiles")!=std::string::npos)
                        document.setDesiredUnits(NAUTICAL_MILES);
                }

                const ParentPools* pools = dynamic_cast<const ParentPools*>( options->getUserData() );
                if (pools)
                {
                    // This file is an external reference. The individual pools will
                    // be non-NULL if the parent is overriding the ext ref model's pools.
                    if (pools->getColorPool())
                        document.setColorPool( pools->getColorPool(), true );
                    if (pools->getTexturePool())
                        document.setTexturePool( pools->getTexturePool(), true );
                    if (pools->getMaterialPool())
                        document.setMaterialPool( pools->getMaterialPool(), true );
                    if (pools->getLightSourcePool())
                        document.setLightSourcePool( pools->getLightSourcePool(), true );
                    if (pools->getLPAppearancePool())
                        document.setLightPointAppearancePool( pools->getLPAppearancePool(), true );
                    if (pools->getLPAnimationPool())
                        document.setLightPointAnimationPool( pools->getLPAnimationPool(), true );
                    if (pools->getShaderPool())
                        document.setShaderPool( pools->getShaderPool(), true );
                }
            }

            const int RECORD_HEADER_SIZE = 4;
            opcode_type continuationOpcode = INVALID_OP;
            std::string continuationBuffer;

            while (fin.good() && !document.done())
            {
                // The continuation record complicates things a bit.

                // Get current read position in stream.
                std::istream::pos_type pos = fin.tellg();

                // get opcode and size
                flt::DataInputStream dataStream(fin.rdbuf());
                opcode_type opcode = (opcode_type)dataStream.readUInt16();
                size_type   size   = (size_type)dataStream.readUInt16();

                // If size == 0, an EOF has probably been reached, i.e. there is nothing 
                // more to read so we must return.
                if (size==0)
                {
                    // If a header was read, we return it.
                    // This allows us handle files with empty hierarchies.
                    if (document.getHeaderNode())
                    {
                        return document.getHeaderNode();
                    }
                    else // (no valid header)
                    {
                        return ReadResult::ERROR_IN_READING_FILE;
                    }
                }

                // variable length record complete?
                if (!continuationBuffer.empty() && opcode!=CONTINUATION_OP)
                {
                    // parse variable length record
                    std::stringbuf sb(continuationBuffer);
                    flt::RecordInputStream recordStream(&sb);
                    recordStream.readRecordBody(continuationOpcode, continuationBuffer.length(), document);

                    continuationOpcode = INVALID_OP;
                    continuationBuffer.clear();
                }

                // variable length record use continuation buffer in case next
                // record is a continuation record.
                if (opcode==EXTENSION_OP ||
                    opcode==NAME_TABLE_OP ||
                    opcode==LOCAL_VERTEX_POOL_OP ||
                    opcode==MESH_PRIMITIVE_OP)
                {
                    continuationOpcode = opcode;

                    if (size > RECORD_HEADER_SIZE)
                    {
                        // Put record in buffer.
                        std::string buffer((std::string::size_type)size-RECORD_HEADER_SIZE,'\0');
                        fin.read(&buffer[0], size-RECORD_HEADER_SIZE);

                        // Can't parse it until we know we have the complete record.
                        continuationBuffer = buffer;
                    }
                }
                else if (opcode==CONTINUATION_OP)
                {
                    if (size > RECORD_HEADER_SIZE)
                    {
                        std::string buffer((std::string::size_type)size-RECORD_HEADER_SIZE,'\0');
                        fin.read(&buffer[0], size-RECORD_HEADER_SIZE);

                        // The record continues.
                        continuationBuffer.append(buffer);
                    }
                }
                else if (opcode==VERTEX_PALETTE_OP)
                {
                    // Vertex Palette needs the file stream as it reads beyond the current record.
                    flt::RecordInputStream recordStream(fin.rdbuf());
                    recordStream.readRecordBody(opcode, size, document);
                }
                else // normal (fixed size) record.
                {
                    // Put record in buffer.
                    std::string buffer((std::string::size_type)size,'\0');
                    if (size > RECORD_HEADER_SIZE)
                        fin.read(&buffer[0], size-RECORD_HEADER_SIZE);

                    // Parse buffer.
                    std::stringbuf sb(buffer);
                    flt::RecordInputStream recordStream(&sb);
                    recordStream.readRecordBody(opcode, size, document);
                }
            }

            if (!document.getHeaderNode())
                return ReadResult::ERROR_IN_READING_FILE;

            if (!document.getPreserveFace())
            {
                osgUtil::Optimizer optimizer;
                optimizer.optimize(document.getHeaderNode(),
                    osgUtil::Optimizer::SHARE_DUPLICATE_STATE |
                    osgUtil::Optimizer::MERGE_GEOMETRY | 
                    osgUtil::Optimizer::MERGE_GEODES | 
                    osgUtil::Optimizer::TESSELLATE_GEOMETRY |
                    osgUtil::Optimizer::STATIC_OBJECT_DETECTION);
            }

            return document.getHeaderNode();
        }

        virtual WriteResult writeObject(const Object& object,const std::string& fileName, const osgDB::ReaderWriter::Options* options) const
        {
            const Node* node = dynamic_cast<const Node*>(&object);
            if (node) return writeNode( *node, fileName, options );
            return WriteResult::FILE_NOT_HANDLED;
        }

        virtual WriteResult writeNode( const osg::Node& node, const std::string& fileName, const Options* options ) const
        {
            if ( fileName.empty() )
            {
                return WriteResult::FILE_NOT_HANDLED;
            }

            std::string ext = osgDB::getLowerCaseFileExtension( fileName );
            if ( !acceptsExtension(ext) )
                return WriteResult::FILE_NOT_HANDLED;

            // Get and save the implicit path name (in case a path wasn't specified in Options).
            std::string filePath = osgDB::getFilePath( fileName );
            if (!filePath.empty())
                _implicitPath = filePath;

            osgDB::ofstream fOut;
            fOut.open( fileName.c_str(), std::ios::out | std::ios::binary );
            if ( fOut.fail())
            {
                osg::notify( osg::FATAL ) << "fltexp: Failed to open output stream." << std::endl;
                return WriteResult::ERROR_IN_WRITING_FILE;
            }

            WriteResult wr = WriteResult::FILE_NOT_HANDLED;
            wr = writeNode( node, fOut, options );
            fOut.close();

            return wr;
        }


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

        virtual WriteResult writeNode( const osg::Node& node, std::ostream& fOut, const Options* options ) const
        {
            // Convert Options to FltOptions.
            ExportOptions* fltOpt = new ExportOptions( options );
            fltOpt->parseOptionsString();

            // If user didn't specify a temp dir, use the output directory
            //   that was implicit in the output file name.
            if (fltOpt->getTempDir().empty())
                fltOpt->setTempDir( _implicitPath );
            if (!fltOpt->getTempDir().empty())
            {
                // If the temp directory doesn't already exist, make it.
                if ( !osgDB::makeDirectory( fltOpt->getTempDir() ) )
                {
                    osg::notify( osg::FATAL ) << "fltexp: Error creating temp dir: " << fltOpt->getTempDir() << std::endl;
                    return WriteResult::ERROR_IN_WRITING_FILE;
                }
            }

            flt::DataOutputStream dos( fOut.rdbuf(), fltOpt->getValidateOnly() );
            flt::FltExportVisitor fnv( &dos, fltOpt );

            // Hm. 'node' is const, but in order to write out this scene graph,
            //   must use Node::accept() which requires 'node' to be non-const.
            //   Pretty much requires casting away const.
            osg::Node* nodeNonConst = const_cast<osg::Node*>( &node );
            if (!nodeNonConst)
                return WriteResult::ERROR_IN_WRITING_FILE;
            nodeNonConst->accept( fnv );
            fnv.complete( node );

            return fltOpt->getWriteResult();
        }

    protected:
        mutable std::string _implicitPath;

        mutable OpenThreads::ReentrantMutex _serializerMutex;
};

// now register with Registry to instantiate the above
// reader/writer.
REGISTER_OSGPLUGIN(OpenFlight, FLTReaderWriter)
