/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
* Copyright 2008-2012 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.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
* 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_METATILE_H
#define OSGEARTH_METATILE_H

#include <osgEarth/Common>
#include <osgEarth/ImageUtils>
#include <osgEarth/GeoData>
#include <osgEarth/TileKey>
#include <osgEarth/Progress>
#include <osg/Image>

namespace osgEarth { namespace Util
{
    using namespace osgEarth;

    /**
     * Metadata groups a collection of adjacent data tiles
     * together to facilitate operations that overlap multiple tiles.
     */
    class OSGEARTH_EXPORT MetaImage
    {
    public:
        //! Construct a new Metatiled image
        MetaImage() { }

        //! Sets the data at location (x,y), where (0,0) is the center.
        bool setImage(int x, int y, osg::Image* image, const osg::Matrix& scaleBias);

        //! Gets the image at the neighbor location (x,y).
        const osg::Image* getImage(int x, int y) const;

        //! Gets the positioning matrix for neightbor location (x,y).
        const osg::Matrix& getScaleBias(int x, int y) const;

        //! Reads the data from parametric location (u,v), where [u,v] in [-1, +2].
        //! Returns true upon success with the value in [output];
        //! false if there is no tile at the read location.
        virtual bool read(double u, double v, osg::Vec4f& output);

        void dump() const;

    protected:

        struct Tile {
            Tile();
            bool _failed;
            osg::ref_ptr<const osg::Image> _imageRef;
            ImageUtils::PixelReader _read;
            osg::Matrix _scaleBias;
        };

        Tile _tiles[3][3]; // col, row
    };


    class OSGEARTH_EXPORT TileKeyMetaImage : public MetaImage
    {
    public:
        using CreateImageFunction = std::function<
            GeoImage(const TileKey&, ProgressCallback*)>;

        //! Construct
        TileKeyMetaImage();

        //! Sets the center tilekey
        void setTileKey(const TileKey& value);

        //! CreateImage function
        void setCreateImageFunction(CreateImageFunction value);

    public: // MetaImage

        bool read(double u, double v, osg::Vec4f& output) override;

    private:
        TileKey _center;
        CreateImageFunction _createImage;
    };


    /**
     * MetaTile is a framework for treating a center tile and its 8 neighbors
     * as a single larger tile. As you sample the tile with the read() function,
     * it will automatically load the neighbors based on the u,v coordinates.
     */
    template<typename T>
    class MetaTile
    {
    public:
        using CreateTileFunction = std::function<T(const TileKey&, ProgressCallback*)>;

        MetaTile()
            : _progress(nullptr) { }

        //! Function that will create new neighbor tiles given a tile key
        inline void setCreateTileFunction(CreateTileFunction value);

        //! Center tile of this metatile. Must call this before attempting
        //! to read. Must call setCreateTileFunction before calling this.
        //! This will "fall back" on ancestor tiles until it find valid data.
        inline void setCenterTileKey(const TileKey& key, ProgressCallback* progress);

        //! Center tile of this metatile. Must call this before attempting
        //! to read. Must call setCreateTileFunction before calling this.
        inline void setCenterTileKey(const TileKey& key, const osg::Matrix& scale_bias);

        //! Read the value of a pixel of unit coordinates [u,v] relative to
        //! the center tile of the meta
        inline bool read(typename T::pixel_type& output, double u, double v);
        inline bool read(typename T::pixel_type& output, int s, int t);

        //! Read the value of a pixel of unit coordinates [u,v] relative to
        //! the center tile of the meta and return a pointer to the
        //! underlying data value instead of copying it to avoid expensive data copies.
        inline const typename T::pixel_type* read(double u, double v);
        inline const typename T::pixel_type* read(int s, int t);

        //! The scale&bias of the tile relative to the key originally passed
        //! to setCenterTileKey
        inline const osg::Matrix& getScaleBias() const {
            return _scale_bias;
        }

        inline bool valid() {
            return _tiles(0,0)._data.valid();
        }

        const T& getCenterTile() {
            return _tiles(0, 0)._data;
        }

    private:
        TileKey _centerKey;
        CreateTileFunction _createTile;
        ProgressCallback* _progress;

        // one component tile
        struct Tile {
            Tile() : _failed(false) { }
            bool _failed;
            T _data;
        };

        // sparse grid for metatile components
        struct Grid : public std::unordered_map<int, Tile> {
            inline Tile& operator()(int x, int y) {
                return this->operator[](y * 100 + x);
            }
        };
        Grid _tiles;
        osg::Matrix _scale_bias; // scale/bias matrix of _centerKey
        unsigned _width, _height;
    };

    template<typename T>
    void MetaTile<T>::setCenterTileKey(const TileKey& original_key, ProgressCallback* progress)
    {
        OE_HARD_ASSERT(_createTile != nullptr, "Must call setCreateTileFunction() before calling setCenterTileKey()");
        // Fall back on parent keys until we get real data
        TileKey key;
        for (key = original_key;
            !_tiles(0,0)._data.valid() && key.valid();
            key.makeParent())
        {
            _tiles(0,0)._data = _createTile(key, progress);
            _centerKey = key;
            _width = _tiles(0, 0)._data.s();
            _height = _tiles(0, 0)._data.t();
        }
        original_key.getExtent().createScaleBias(_centerKey.getExtent(), _scale_bias);
        _progress = progress;
    }

    template<typename T>
    void MetaTile<T>::setCenterTileKey(
        const TileKey& original_key,
        const osg::Matrix& scale_bias)
    {
        OE_HARD_ASSERT(_createTile != nullptr, "Must call setCreateTileFunction() before calling setCenterTileKey()");
        // Fall back on parent keys until we get real data
        TileKey key;
        for (key = original_key;
            !_tiles(0, 0)._data.valid() && key.valid();
            key.makeParent())
        {
            _tiles(0, 0)._data = _createTile(key, nullptr);
            _centerKey = key;
            _width = _tiles(0, 0)._data.s();
            _height = _tiles(0, 0)._data.t();
        }
        original_key.getExtent().createScaleBias(_centerKey.getExtent(), _scale_bias);
        _scale_bias.preMult(scale_bias);
    }

    template<typename T>
    void MetaTile<T>::setCreateTileFunction(typename MetaTile<T>::CreateTileFunction value)
    {
        _createTile = value;
    }

    template<typename T>
    bool MetaTile<T>::read(typename T::pixel_type& output, double u, double v)
    {
        // scale and bias the u,v to the real center key
        u = u * _scale_bias(0, 0) + _scale_bias(3, 0);
        v = v * _scale_bias(1, 1) + _scale_bias(3, 1);

        // tile number:
        // TODO: when this hits an exact boundary (i.e. 1.0) which is the
        // correct tile to choose? The actual answer is that when using 
        // metatile, you should probably always use forEachPixelOnCenter
        // which will never land on a boundary exactly. -gw
        int x = (int)::floor(u);
        int y = (int)::floor(v);

        Tile& tile = _tiles(x, y);

        // if we already tried to load this tile and failed, bail out
        if (tile._failed)
            return false;

        // if we still need to load this tile, do so
        if (!tile._data.valid() && _createTile != nullptr)
        {
            TileKey key = _centerKey.createNeighborKey(x, -y);
            tile._data = _createTile(key, nullptr);
            if (!tile._data.valid())
            {
                tile._failed = true;
            }
        }

        if (tile._failed)
            return false;

        // tile-local coordinates for sampling:
        u = u - (double)x;
        v = v - (double)y;

        return tile._data.read(output, u, v);
    }

    template<typename T>
    const typename T::pixel_type* MetaTile<T>::read(double u, double v)
    {
        // scale and bias the u,v to the real center key
        u = u * _scale_bias(0, 0) + _scale_bias(3, 0);
        v = v * _scale_bias(1, 1) + _scale_bias(3, 1);

        // tile number:
        // TODO: when this hits an exact boundary (i.e. 1.0) which is the
        // correct tile to choose? The actual answer is that when using 
        // metatile, you should probably always use forEachPixelOnCenter
        // which will never land on a boundary exactly. -gw
        int x = (int)::floor(u);
        int y = (int)::floor(v);

        Tile& tile = _tiles(x, y);

        // if we already tried to load this tile and failed, bail out
        if (tile._failed)
            return nullptr;

        // if we still need to load this tile, do so
        if (!tile._data.valid() && _createTile != nullptr)
        {
            TileKey key = _centerKey.createNeighborKey(x, -y);
            tile._data = _createTile(key, nullptr);
            if (!tile._data.valid())
            {
                tile._failed = true;
            }
        }

        if (tile._failed)
            return nullptr;

        // tile-local coordinates for sampling:
        u = u - (double)x;
        v = v - (double)y;

        return tile._data.read(u, v);
    }

    template<typename T>
    bool MetaTile<T>::read(typename T::pixel_type& output, int s, int t)
    {
        if (_tiles(0, 0)._failed)
            return false;

        // scale and bias the u,v to the real center key
        s = (int)floor((double)s * _scale_bias(0, 0) + _scale_bias(3, 0)*(double)_width);
        t = (int)floor((double)t * _scale_bias(1, 1) + _scale_bias(3, 1)*(double)_height);

        // tile number:
        int x = (int)::floor((double)s / (double)_width);
        int y = (int)::floor((double)t / (double)_height);

        Tile& tile = _tiles(x, y);

        // if we already tried to load this tile and failed, bail out
        if (tile._failed)
            return false;

        // if we still need to load this tile, do so
        if (!tile._data.valid() && _createTile != nullptr)
        {
            TileKey key = _centerKey.createNeighborKey(x, -y);
            tile._data = _createTile(key, nullptr);
            if (!tile._data.valid())
            {
                tile._failed = true;
            }
        }

        if (tile._failed)
            return false;

        // tile-local coordinates for sampling:
        if (s < 0) {
            s = s % _width;
            if (s < 0) s += _width;
        }
        else s = s % _width;

        if (t < 0) {
            t = t % _height;
            if (t < 0) t += _height;
        }
        else t = t % _height;

        return tile._data.read(output, (unsigned)s, (unsigned)t);
    }

    template<typename T>
    const typename T::pixel_type* MetaTile<T>::read(int s, int t)
    {
        if (_tiles(0, 0)._failed)
            return nullptr;

        // scale and bias the u,v to the real center key
        s = (int)floor((double)s * _scale_bias(0, 0) + _scale_bias(3, 0) * (double)_width);
        t = (int)floor((double)t * _scale_bias(1, 1) + _scale_bias(3, 1) * (double)_height);

        // tile number:
        int x = (int)::floor((double)s / (double)_width);
        int y = (int)::floor((double)t / (double)_height);

        Tile& tile = _tiles(x, y);

        // if we already tried to load this tile and failed, bail out
        if (tile._failed)
            return nullptr;

        // if we still need to load this tile, do so
        if (!tile._data.valid() && _createTile != nullptr)
        {
            TileKey key = _centerKey.createNeighborKey(x, -y);
            tile._data = _createTile(key, nullptr);
            if (!tile._data.valid())
            {
                tile._failed = true;
            }
        }

        if (tile._failed)
            return nullptr;

        // tile-local coordinates for sampling:
        if (s < 0) {
            s = s % _width;
            if (s < 0) s += _width;
        }
        else s = s % _width;

        if (t < 0) {
            t = t % _height;
            if (t < 0) t += _height;
        }
        else t = t % _height;

        return tile._data.read((unsigned)s, (unsigned)t);
    }

} }

#endif // OSGEARTH_METATILE_H
