/* osgEarth
 * Copyright 2025 Pelican Mapping
 * MIT License
 */
#pragma once
#include <osgEarth/Map>

 //! optional property macro for referencing another layer
#define OE_OPTION_LAYER(TYPE, NAME) \
    private: \
        LayerReference< TYPE > _layerRef_ ## NAME ; \
    public: \
        LayerReference< TYPE >& NAME () { return _layerRef_ ## NAME ; } \
        const LayerReference< TYPE >& NAME () const { return _layerRef_ ## NAME ; }

namespace osgEarth
{
    /**
     * Helper class for Layers that reference other layers.
     */
    template<typename T>
    class LayerReference
    {
    public:
        using TypedOptions = typename T::Options;

        //! User can call this to set the layer by hand (instead of finding it
        //! in the map or in an embedded options structure)
        void setLayer(T* layer) 
        {
            _layer = layer;
        }

        //! Contained layer object
        T* getLayer() const 
        {
            return _layer.get();
        }

        //! Whether this reference is set at all.
        bool isSet() const
        {
            return _layer.valid() || _embeddedOptions || _externalLayerName.isSet();
        }

        //! Whether the user called setLayer to establish the reference
        //! (as opposed to finding it in an embedded options or in the map)
        bool isSetByUser() const 
        {
            return _layer.valid() && !_embeddedOptions && !_externalLayerName.isSet();
        }

        //! Whether the patch layer is valid an open for business
        bool isOpen() const
        {
            return _layer.valid() && _layer->isOpen();
        }

        //! creates the layer but does not open it.
        //! You can use this if you need to configure the layer first.
        T* create(const osgDB::Options* readOptions)
        {
            if (_embeddedOptions)
            {
                auto typedLayer = Layer::create_as<T>(*_embeddedOptions.get());
                if (typedLayer.valid())
                {
                    setLayer(typedLayer.get());
                }
            }
            return _layer.get();
        }

        //! open the layer pointed to in the reference and return a status code
        Status open(const osgDB::Options* readOptions)
        {
            if (_embeddedOptions)
            {
                auto typedLayer = Layer::create_as<T>(*_embeddedOptions.get());
                if (typedLayer.valid())
                {
                    typedLayer->setReadOptions(readOptions);
                    const Status& layerStatus = typedLayer->open();
                    if (layerStatus.isError())
                    {
                        return layerStatus;
                    }
                    _layer = typedLayer.get();
                }
            }
            else if (_layer.valid() && !_layer->isOpen())
            {
                _layer->setReadOptions(readOptions);
                const Status& layerStatus = _layer->open();
                if (layerStatus.isError())
                {
                    return layerStatus;
                }
            }
            return Status::OK();
        }

        void close()
        {
            _layer = NULL;
        }

        //! Find a layer in the map and set this reference to point at it 
        void addedToMap(const Map* map)
        {
            if (!getLayer() && _externalLayerName.isSet())
            {
                T* layer = map->getLayerByName<T>(_externalLayerName.get());
                if (layer)
                {
                    _layer = layer;

                    if (!layer->isOpen())
                    {
                        layer->open();
                    }
                }
            }
            else if (getLayer() && _embeddedOptions)
            {
                _layer->addedToMap(map);
            }
        }

        //! If this reference was set by findInMap, release it.
        void removedFromMap(const Map* map)
        {
            if (map && _layer.valid())
            {
                if (_embeddedOptions)
                {
                    _layer->removedFromMap(map);
                }

                // Do not set _layer to nullptr. It may still be in use
                // and this is none of the Map's business.
            }
        }

        //! Get the layer ref from either a name or embedded option
        void get(const Config& conf, const std::string& tag)
        {
            // first try the config itself:
            if (ci_equals(conf.key(), tag))
            {
                _externalLayerName = trim(conf.value());
            }

            // then an immediate child:
            if (!_externalLayerName.isSet())
            {
                conf.get(tag, _externalLayerName);
            }

            if (!_externalLayerName.isSet())
            {
                // next try to find a child called (tag) and try to make the layer
                // from it's children:
                if (conf.hasChild(tag) && conf.child(tag).children().size() >= 1)
                {
                    const Config& tag_content = *conf.child(tag).children().begin();
                    {
                        auto layer = Layer::create_as<T>(tag_content);
                        if (layer.valid())
                        {
                            _embeddedOptions = std::make_shared<TypedOptions>(tag_content);
                        }
                    }
                }

                // failing that, try each child of the config.
                if (!_embeddedOptions)
                {
                    for(auto& child_conf: conf.children())
                    {
                        auto layer = Layer::create_as<T>(child_conf);
                        if (layer.valid())
                        {
                            _embeddedOptions = std::make_shared<TypedOptions>(child_conf);
                            break;
                        }
                    }
                }
            }
        }

        //! Set the layer ref options in the config
        void set(Config& conf, const std::string& tag) const
        {
            if (_externalLayerName.isSet())
            {
                conf.set(tag, _externalLayerName);
            }
            else if (_embeddedOptions)
            {
                conf.set(_embeddedOptions->getConfig());
            }
            else if (isSetByUser()) // should be true
            {
                conf.set(_layer->getConfig());
            }
        }

        void add(Config& conf, const std::string& tag) const
        {
            if (_externalLayerName.isSet())
            {
                conf.add(tag, _externalLayerName);
            }
            else if (_embeddedOptions)
            {
                conf.add(_embeddedOptions->getConfig());
            }
            else if (isSetByUser()) // should be true
            {
                conf.add(_layer->getConfig());
            }
        }

        const std::shared_ptr<TypedOptions>& embeddedOptions() const
        {
            return _embeddedOptions;
        }

        void setEmbeddedOptions(const TypedOptions& value)
        {
            _embeddedOptions = std::make_shared<TypedOptions>(value);
        }

        const optional<std::string>& externalLayerName() const { return _externalLayerName; }

        void setExternalLayerName(const std::string& value)
        {
            _externalLayerName = value;
        }

    private:
        osg::ref_ptr<T> _layer;
        optional<std::string> _externalLayerName;
        std::shared_ptr<TypedOptions> _embeddedOptions;
    };
}
