/* -*-c++-*- */
/* osgEarth - Dynamic map generation toolkit for OpenSceneGraph
 * Copyright 2008-2016 Pelican Mapping
 * http://osgearth.org
 *
 * osgEarth is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */
#ifndef OSGEARTH_BUILDINGS_COMPILER_OUTPUT_H
#define OSGEARTH_BUILDINGS_COMPILER_OUTPUT_H

#include "Common"
#include "CompilerSettings"
#include "FilterUsage"

#include <osg/Geode>
#include <osg/Matrix>
#include <osg/TextureBuffer>
#include <osgEarth/Progress>
#include <osgEarth/Session>
#include <osgEarth/FeatureIndex>
#include <osgEarth/MetadataNode>
#include <osgEarth/ModelResource>
#include <osgEarth/ResourceCache>
#include <osgEarth/StateSetCache>
#include <osgEarth/TextureArena>
#include <osgEarth/Chonk>

#include <map>
#include <unordered_map>

namespace osg
{
   class MatrixTransform;
}

namespace osgEarth { namespace Buildings 
{
    using namespace osgEarth;

    //! Data structure that maintains weak references to any
    //! potentially shareable data within the GL4/NV framework,
    //! including chonks and textures.
    struct ResidentData
    {
        using Ptr = std::shared_ptr<ResidentData>;

        Mutex _m;
        std::unordered_map<ModelResource*, Chonk::WeakPtr> _chonks;
        std::unordered_map<osg::Texture*, Texture::WeakPtr> _textures;
    };

    struct TextureCache : public osg::Referenced
    {
        std::mutex _mutex;

        osg::ref_ptr<osg::StateAttribute> getOrCreate(SkinResource* skin, const osgDB::Options* readOptions)
        {
            std::lock_guard<std::mutex> lock(_mutex);

            osg::ref_ptr<osg::StateAttribute> tex;

            auto iter = _cache.find(skin->imageURI()->full());
            if (iter != _cache.end())
            {
                tex = iter->second;
            }

            if (!tex.valid())
            {
                tex = skin->createStateAttribute(readOptions);
                //tex = skin->createTexture(readOptions);
                if (tex.valid())
                {
                    _cache[skin->imageURI()->full()] = tex;
                }
            }

            return tex;
        }

        osg::ref_ptr<osg::StateAttribute> getOrInsert(osg::Texture* tex)
        {
            osg::ref_ptr<osg::StateAttribute> result = tex;

            if (tex &&
                tex->getNumImages() > 0 &&
                tex->getImage(0) &&
                !tex->getImage(0)->getFileName().empty() &&     // has a valid filename
                dynamic_cast<osg::TextureBuffer*>(tex) == 0L)   // isn't an instance data texture
            {
                std::lock_guard<std::mutex> lock(_mutex);

                auto& t = _cache[tex->getImage(0)->getFileName()];
                if (!t.valid())
                {
                    t = tex;
                }

                result = t;
            }

            return result;
        }

        void releaseGLObjects(osg::State* state)
        {
            std::lock_guard<std::mutex> lock(_mutex);

            for (auto iter : _cache)
            {
                auto& tex = iter.second;
                if (tex.valid())
                {
                    tex->releaseGLObjects(state);
                }
            }
        }

        void resizeGLObjectBuffers(unsigned size)
        {
            std::lock_guard<std::mutex> lock(_mutex);
            for (auto iter : _cache)
            {
                auto& tex = iter.second;
                if (tex.valid())
                {
                    tex->resizeGLObjectBuffers(size);
                }
            }
        }

        void clear()
        {
            std::lock_guard<std::mutex> lock(_mutex);
            _cache.clear();
        }

        // todo: consider changing this to an observer_ptr -gw
        std::unordered_map<std::string, osg::ref_ptr<osg::StateAttribute> > _cache;
    };

    /**
     * Object passed to the building compiler that collects all the
     * OSG output generated by the compilation process.
     */
    class OSGEARTHBUILDINGS_EXPORT CompilerOutput
    {
    public:
        CompilerOutput();

        /** Name of this compiler output */
        void setName(const std::string& name) { _name = name; }

        /** Tile key of data that results in this output */
        void setTileKey(const TileKey& key) { _key = key; }

        void setTextureCache(TextureCache* cache) { _texCache = cache; }

        //! Stateset cache for shader generation
        void setStateSetCache(StateSetCache* value) { _stateSetCache = value; }

        //! Texture arena to use for GL4/NV rendering
        void setTextureArena(TextureArena* value) { _textures = value; }

        //! Set chonks arena to use
        void setResidentData(ResidentData::Ptr value) { _residentData = value; }

        /** Read output from a cache bin */
        osg::Node* readFromCache(const osgDB::Options* readOptions, ProgressCallback* progress) const;

        /** Write output to a cache bin */
        void writeToCache(osg::Node*, const osgDB::Options*, ProgressCallback*) const;

        /** Build and return a scene graph based on the output in this object. */
        osg::Node* createSceneGraph(Session* session, const CompilerSettings& settings, const osgDB::Options* readOptions, ProgressCallback*) const;

        void setRange(float value) { _range = value; }
        float getRange() const     { return _range; }
        
        /** Delocatization matrix to apply to the entire output. */
        void setLocalToWorld(const osg::Matrix& m);
        const osg::Matrix& getLocalToWorld() const { return _local2world; }
        const osg::Matrix& getWorldToLocal() const { return _world2local; }

        /** Feature index to use (optional) */
        void setIndex(FeatureIndexBuilder* index) { _index = index; }
        FeatureIndexBuilder* getIndex()           { return _index; }

        void setMetadata(MetadataNode* metadata) { _metadata = metadata; }
        MetadataNode* getMetadata() { return _metadata; }

        /** Sets the currently active feature (for indexing purposes). If an index is set,
            calls to addDrawable or addInstance will prompt the indexer to tag the new
            data with this feature. */
        void setCurrentFeature(Feature* f) { _currentFeature = f; }
        Feature* getCurrentFeature() { return _currentFeature; }

        /** Run on the result of cretaeSceneGraph or readFromCache to install VPs. */
        void postProcess(osg::Node* node, const CompilerSettings& settings, ProgressCallback* progress) const;

        void setFilterUsage(FilterUsage usage);
    public:
        
        /** Adds a drawable to the output. */
        void addDrawable(osg::Drawable* drawable);

        /** Adds a drawable, categorized under a tag. */
        void addDrawable(osg::Drawable* drawable, const std::string& tag);

        /** Adds an instance of a model resource */
        void addInstance(ModelResource* model, const osg::Matrix& matrix);

        /** The group containing externally referenced models */
        osg::Group* getExternalModelsGroup() const { return _externalModelsGroup.get(); }

        /** Group holding debugging geometry */
        osg::Group* getDebugGroup() const { return _debugGroup.get(); }
        
        /** Returns the StateSet unique to this skin resource (may be empty) - used for caching. */
        osg::StateSet* getSkinStateSet(SkinResource* skin, const osgDB::Options* readOptions);

        osg::StateAttribute* getTexture(SkinResource* skin, const osgDB::Options* readOptions) {
            return _texCache->getOrCreate(skin, readOptions);
        }

    protected:

        osg::Matrix _local2world, _world2local;

        osg::ref_ptr<osg::Geode> _defaultGeode;
        using TaggedGeodes = std::unordered_map<std::string, osg::ref_ptr<osg::Geode>>;
        TaggedGeodes _geodes;
        
        using InstanceVector = std::vector<std::pair<osg::Matrix, osg::ref_ptr< Feature > > >;
        using InstanceMap = std::map<osg::ref_ptr<ModelResource>, InstanceVector>;
        InstanceMap _instances;
        
        osg::ref_ptr<osg::Group> _externalModelsGroup;

        osg::ref_ptr<osg::Group> _debugGroup;

        // caches catalog resources like skins and instanced models for this output object.
        osg::ref_ptr<ResourceCache> _resourceCache;

        FeatureIndexBuilder* _index;
        MetadataNode* _metadata;

        Feature* _currentFeature;

        float _range;

        TileKey _key;
        std::string _name;

        using SkinStateSetCache = std::unordered_map<std::string, osg::ref_ptr<osg::StateSet>>;
        SkinStateSetCache _skinStateSetCache;

        osg::ref_ptr<TextureCache> _texCache;

        osg::ref_ptr<StateSetCache> _stateSetCache;

        osg::ref_ptr<TextureArena> _textures;

        ResidentData::Ptr _residentData;

        FilterUsage _filterUsage;

        std::string createCacheKey() const;

        void addInstances(osg::MatrixTransform* root, Session* session, const CompilerSettings& settings, const osgDB::Options* readOptions, ProgressCallback*) const;
        void addInstancesNormal(osg::MatrixTransform* root, Session* session, const CompilerSettings& settings, const osgDB::Options* readOptions, ProgressCallback*) const;
        void addInstancesZeroWorkCallbackBased(osg::MatrixTransform* root, Session* session, const CompilerSettings& settings, const osgDB::Options* readOptions, ProgressCallback*) const;

        osg::Node* createSceneGraphLegacy(
            Session*                session,
            const CompilerSettings& settings,
            const osgDB::Options*   readOptions,
            ProgressCallback*       progress) const;

        osg::Node* createSceneGraphUnifiedNV(
            Session*                session,
            const CompilerSettings& settings,
            const osgDB::Options*   readOptions,
            ProgressCallback*       progress) const;

    };
} }

#endif // OSGEARTH_BUILDINGS_COMPILER_OUTPUT_H
