/* -*-c++-*- */
/* osgEarth - Dynamic map generation toolkit for OpenSceneGraph
* Copyright 2008-2010 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_CACHING_H
#define OSGEARTH_CACHING_H 1

#include <osgEarth/Common>
#include <osgEarth/Config>
#include <osgEarth/TMS>
#include <osgEarth/TileKey>

#include <osg/Referenced>
#include <osg/Object>
#include <osg/Image>
#include <osg/Shape>
#include <osg/Timer>
#include <osgDB/ReadFile>

#include <OpenThreads/ReadWriteMutex>

#include <string>

namespace osgEarth
{
    /**
     * Base class for Cache implementation options.
     */
    class CacheOptions : public DriverConfigOptions // no export (header only)
    {
    public:
        CacheOptions( const ConfigOptions& options =ConfigOptions() )
            : DriverConfigOptions( options ),
              _cacheOnly( false )
        { 
            fromConfig( _conf ); 
        }

        /** Whether to run exclusively off the cache (and not fetch files from tile sources) */
        optional<bool> cacheOnly() { return _cacheOnly; }
        const optional<bool> cacheOnly() const { return _cacheOnly; }

    public:
        virtual Config getConfig() const {
            Config conf = ConfigOptions::getConfig();
            conf.updateIfSet( "cache_only", _cacheOnly );
            return conf;
        }
        virtual void mergeConfig( const Config& conf ) {
            ConfigOptions::mergeConfig( conf );            
            fromConfig( conf );
        }
    private:
        void fromConfig( const Config& conf ) {
            conf.getIfSet( "cache_only", _cacheOnly );
        }

        optional<bool> _cacheOnly;
    };

    //----------------------------------------------------------------------

    /**
     * Options class for any cache that caches tiles as individual files on disk.
     */
    class DiskCacheOptions : public CacheOptions
    {
    public:
        DiskCacheOptions( const ConfigOptions& options =ConfigOptions() )
            : CacheOptions( options ),
              _writeWorldFiles( false )
        {
            fromConfig( _conf );
        }

        /** The folder path in which to store the cache data */
        void setPath( const std::string& value ) { _path = value; }
        const std::string& path() const { return _path; }

        /** Whether to write out "world" files alongside the cached tiles */
        optional<bool>& writeWorldFiles() { return _writeWorldFiles; }
        const optional<bool>& writeWorldFiles() const { return _writeWorldFiles; }

    public:
        virtual Config getConfig() const {
            Config conf = CacheOptions::getConfig();
            conf.update("path", _path);
            conf.updateIfSet("write_world_files", _writeWorldFiles);
            return conf;
        }
        virtual void mergeConfig( const Config& conf ) {
            CacheOptions::mergeConfig( conf );
            fromConfig( conf );
        }

    private:
        void fromConfig( const Config& conf ) {
            _path = conf.value("path");
            conf.getIfSet("write_world_files", _writeWorldFiles);
        }

        std::string    _path;
        optional<bool> _writeWorldFiles;
    };

    //----------------------------------------------------------------------

    /**
     * Options for a TMS-style disk cache. The cache is stored on disk in a file hierarchy
     * identical to that in the TMS specification:
     * http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification
     */
    class TMSCacheOptions : public DiskCacheOptions // no export (header only)
    {
    public:
        TMSCacheOptions( const ConfigOptions& options =ConfigOptions() )
            : DiskCacheOptions( options ),
              _invertY( false )
        {
            setDriver("tms");
            fromConfig( _conf );
        }

        /** Wheher to invert the Y tile indicies ("google type") */
        optional<bool>& invertY() { return _invertY; }
        const optional<bool>& invertY() const { return _invertY; }

    public:
        virtual Config getConfig() const {
            Config conf = DiskCacheOptions::getConfig();
            conf.updateIfSet("invert_y", _invertY);
            return conf;
        }
        virtual void mergeConfig( const Config& conf ) {
            DiskCacheOptions::mergeConfig( conf );
            fromConfig( conf );
        }

    private:
        void fromConfig( const Config& conf ) {
            conf.getIfSet("invert_y", _invertY);
        }

        optional<bool> _invertY;
    };

  //----------------------------------------------------------------------

  /**
   * Cache specification is a pairing of cache ID and cache format.
   */
    struct CacheSpec
    {
        CacheSpec() { }
        CacheSpec( const std::string& cacheId, const std::string& format, const std::string& name ="")
            :  _cacheId( cacheId ), _format(format), _name(name) { }

        bool empty() const { return _cacheId.empty(); }

        const std::string& cacheId() const { return _cacheId; }
        const std::string& format() const { return _format; }
        const std::string& name() const { return _name; }

    private:
        std::string _cacheId;
        std::string _format;
        std::string _name; // this is only here so you can see what layer the cache spec is referencing.
    };

  //----------------------------------------------------------------------

  /**
   * A Cache is an object that stores and retrieves image or heightfield rasters.
   * This is the base class for all such implementations.
   */
  class OSGEARTH_EXPORT Cache : public osg::Object
  {
  public:
    /**
    * Gets the cached image for the given TileKey
    */
    virtual bool getImage( const TileKey& key, const CacheSpec& spec, osg::ref_ptr<const osg::Image>& out_image ) =0;

    /**
    * Sets the cached image for the given TileKey
    */
    virtual void setImage( const TileKey& key, const CacheSpec& spec, const osg::Image* image ) = 0;

    /**
    * Gets the cached heightfield for the given TileKey
    */
    virtual bool getHeightField( const TileKey& key, const CacheSpec& spec, osg::ref_ptr<const osg::HeightField>& out_hf );

    /**
    * Sets the cached heightfield for the given TileKey
    */
    virtual void setHeightField( const TileKey& key, const CacheSpec& spec, const osg::HeightField* hf );

    /**
    * Gets the current MapConfig filename.  This is used for getting relative paths to the MapConfig.
    */
    virtual const std::string& getReferenceURI() { return _refURI; }

    /**
    * Sets the current MapConfig filename.
    */
    virtual void setReferenceURI( const std::string& value ) { _refURI = value; }

    /**
    * Gets whether the given TileKey is cached or not
    */
    virtual bool isCached( const TileKey& key, const CacheSpec& spec) const { return false; }

    /**
    * Store the TileMap for the given profile.
    */
    virtual void storeProperties( const CacheSpec& spec, const Profile* profile, unsigned int tileSize ) 
        { }

    /**
    * Loads the TileMap for the given cacheId.
    */
    virtual bool loadProperties( 
        const std::string&           cacheId, 
        CacheSpec&                   out_spec, 
        osg::ref_ptr<const Profile>& out_profile,
        unsigned int&                out_tileSize ) { return false; }

    /**
     * Compact the cache, if supported by the underlying implementation.
     * You can optionally specify that the compaction operation run in 
     * a background thread if supported.
     *
     * This method will return false if the implementation does not support
     * compaction.
     */
    virtual bool compact( bool async =true ) { return false; }

    /**
     * Delete old entries from the cache. The timestamp is UTC, second since epoch.
     * You can optionally control whether the purge operation should run in the
     * background (async=true) if supported.
     *
     * This method will return false if the implementation does not support 
     * the operation.
     */
    virtual bool purge(
        const std::string& cacheId,
        int olderThanTimeStamp =0L,
        bool async =true ) { return false; }

    const CacheOptions& getCacheOptions() const { return _options; }

  public:
      Cache( const CacheOptions& options =CacheOptions() );
      Cache( const Cache& rhs, const osg::CopyOp& op =osg::CopyOp::DEEP_COPY_ALL );

  protected:
      CacheOptions _options;
      std::string _refURI;
  };

  //----------------------------------------------------------------------

  /**
   * In-memory tile cache.
   */
  class OSGEARTH_EXPORT MemCache : public Cache
  {
  public:
    MemCache( int maxTilesInCache =16 );
    MemCache( const MemCache& rhs, const osg::CopyOp& op =osg::CopyOp::DEEP_COPY_ALL );
    META_Object(osgEarth,MemCache);

    /**
     * Gets the maximum number of tiles to keep in the cache
     */
    unsigned int getMaxNumTilesInCache() const;

    /**
     * Sets the maximum number of tiles to keep in the cache
     */
    void setMaxNumTilesInCache(unsigned int max);

    /**
     * Gets whether the given TileKey is cached or not
     */
    virtual bool isCached( const TileKey& key, const CacheSpec& spec ) const;

    /**
     * Gets the cached image for the given TileKey
     */
    virtual bool getImage( const TileKey& key, const CacheSpec& spec, osg::ref_ptr<const osg::Image>& out_image );

    /**
     * Sets the cached image for the given TileKey
     */
    virtual void setImage( const TileKey& key, const CacheSpec& spec, const osg::Image* image );

    /**
     * Gets the cached heightfield for the given TileKey
     */
    virtual bool getHeightField( const TileKey& key, const CacheSpec& spec, osg::ref_ptr<const osg::HeightField>& out_hf );

    /**
     * Sets the cached heightfield for the given TileKey
     */
    virtual void setHeightField( const TileKey& key, const CacheSpec& spec, const osg::HeightField* hf );

    /** Delete entries from the cache. */
    virtual bool purge( const std::string& cacheId, int olderThan, bool async );

  protected:
    /**
     * Gets the cached object for the given TileKey
     */
    bool getObject( const TileKey& key, const CacheSpec& spec, osg::ref_ptr<const osg::Object>& out_result );

    /**
     * Sets the cached object for the given TileKey
     */
    void setObject( const TileKey& key, const CacheSpec& spec, const osg::Object* image );

    struct CachedObject
    {
      std::string _key;
      osg::ref_ptr<const osg::Object> _object;
    };

    typedef std::list<CachedObject> ObjectList;
    ObjectList _objects;

    typedef std::map<std::string,ObjectList::iterator> KeyToIteratorMap;
    KeyToIteratorMap _keyToIterMap;

    unsigned int _maxNumTilesInCache;
    OpenThreads::Mutex _mutex;

  };

  /**
   * Base class for any cache that stores tile files to disk
   */
  class OSGEARTH_EXPORT DiskCache : public Cache
  {
  public:
    DiskCache( const DiskCacheOptions& options =DiskCacheOptions() );

    DiskCache( const DiskCache& rhs, const osg::CopyOp& op =osg::CopyOp::DEEP_COPY_ALL );

    META_Object(osgEarth,DiskCache);

    /**
    * Gets the path of this DiskCache
    */
    std::string getPath() const;

    /**
    * Gets whether the given TileKey is cached or not
    */
    virtual bool isCached( const TileKey& key, const CacheSpec& spec ) const;

    /**
    * Gets the filename to cache to for the given TileKey
    */
    virtual std::string getFilename( const TileKey& key, const CacheSpec& spec ) const;

    /**
    * Gets the cached image for the given TileKey
    */
    virtual bool getImage( const TileKey& key, const CacheSpec& spec, osg::ref_ptr<const osg::Image>& out_image );

    /**
    * Sets the cached image for the given TileKey
    */
    virtual void setImage( const TileKey& key, const CacheSpec& spec, const osg::Image* image );

    /**
    * Store the TileMap for the given profile.
    */
    virtual void storeProperties( const CacheSpec& spec, const Profile* profile, unsigned int tileSize );

    /**
    * Loads the TileMap for the given layer.
    */
    virtual bool loadProperties( 
        const std::string&           cacheId, 
        CacheSpec&                   out_spec, 
        osg::ref_ptr<const Profile>& out_profile,
        unsigned int&                out_tileSize );


  protected:
    std::string getTMSPath(const std::string& cacheId) const;

    struct LayerProperties
    {
      std::string _format;
      unsigned int _tile_size;
      osg::ref_ptr< const Profile > _profile;
    };

    typedef std::map< std::string, LayerProperties > LayerPropertiesCache;
    LayerPropertiesCache _layerPropertiesCache;
    bool        _writeWorldFilesOverride;     

  private:
      DiskCacheOptions _options;
  };

  /**
   * Disk-based cache that stores image tiles according the the OSGeo TMS specification.
   * TODO: move this imlpementation into a proper driver plugin
   */
  class OSGEARTH_EXPORT TMSCache : public DiskCache
  {
  public:
    TMSCache(const TMSCacheOptions& options =TMSCacheOptions() );

    TMSCache(const TMSCache& rhs, const osg::CopyOp& op =osg::CopyOp::DEEP_COPY_ALL);
    META_Object(osgEarth,TMSCache);

    /**
    * Gets whether or not to invert the y tile index
    */
    const bool& getInvertY() {return _invertY; }

    /**
    * Sets whether or not to invert the y tile index
    */
    void setInvertY(const bool &invertY) {_invertY = invertY;}

    /**
    * Gets the filename to cache to for the given TileKey
    */
    virtual std::string getFilename( const TileKey& key, const CacheSpec& spec ) const;

  private:
    bool _invertY;
    TMSCacheOptions _options;
  };

  //----------------------------------------------------------------------

  /**
   * Base class for a cache driver plugin.
   */
  class OSGEARTH_EXPORT CacheDriver : public osgDB::ReaderWriter
  {
  public:
      const CacheOptions& getCacheOptions( const osgDB::ReaderWriter::Options* options ) const;
  };

  //----------------------------------------------------------------------

  /** 
   * Factory class that can load and instantiate a Cache implementation based on the
   * information in the CacheOptions settings.
   */
  class OSGEARTH_EXPORT CacheFactory
  {
  public:
      static Cache* create( const CacheOptions& options );
  };
}

#endif // OSGEARTH_CACHING_H
