/* osgEarth
 * Copyright 2025 Pelican Mapping
 * MIT License
 */
#pragma once
#include <osgEarth/Common>
#include <osgEarth/Profile>
#include <osgEarth/Geometry>
#include <osgEarth/Style>
#include <osgEarth/GeoCommon>
#include <osgEarth/SpatialReference>
#include <osgEarth/StringUtils>
#include <osg/Shape>
#include <vector>

namespace osgEarth
{
    namespace Util
    {
        class FilterContext;
        class Session;
    }

    /**
     * Metadata and schema information for feature data.
     */
    class OSGEARTH_EXPORT FeatureProfile : public osg::Referenced
    {
    public:
        //! Construct a non-tiled feature profile with an extent
        FeatureProfile(const GeoExtent& featuresExtent);

        //! Construct a TILED feature profile with a tiling schema
        FeatureProfile(const Profile* tilingProfile);

        //! Copy ctor
        FeatureProfile(const FeatureProfile& rhs) = default;

        //! Spatial extents of the features data
        void setExtent(const GeoExtent& value) { _extent = value; }
        const GeoExtent& getExtent() const { return _extent; }

        //! Spatial reference system of feature data
        const SpatialReference* getSRS() const { return _extent.getSRS(); }

        //! Whether the feature data is pre-tiled
        bool isTiled() const;

        //! For tiled data, the tiling profile
        const osgEarth::Profile* getTilingProfile() const;
        void setTilingProfile( const osgEarth::Profile* profile );

        //! For tiled data, the first tiling level of detail
        int getFirstLevel() const;
        void setFirstLevel(int firstLevel );

        //! For tiled data, the last tiling level fo detail
        int getMaxLevel() const;
        void setMaxLevel(int maxLevel);

        //! Interpolation method for geodetic data
        optional<GeoInterpolation>& geoInterp() { return _geoInterp; }
        const optional<GeoInterpolation>& geoInterp() const { return _geoInterp; }

    protected:
        virtual ~FeatureProfile() { }

        osg::ref_ptr< const osgEarth::Profile > _tilingProfile;
        GeoExtent _extent;
        int _firstLevel = 0;
        int _maxLevel = -1;
        optional<GeoInterpolation> _geoInterp;
    };

    /** Featuer attribute types */
    enum AttributeType
    {
        ATTRTYPE_UNSPECIFIED,
        ATTRTYPE_STRING,
        ATTRTYPE_DOUBLE,
        ATTRTYPE_INT,
        ATTRTYPE_BOOL
    };

    /** Union of attribute values. The order here must match that of AttributeType. */
    using AttributeUnion = std::variant<std::monostate, std::string, double, std::int64_t, bool>;

    /**
    * A value of an attribute in a feature.
    * This is a union type that can hold a string, double, int, or bool
    * and includes some convenience getter methods.
    */
    struct OSGEARTH_EXPORT AttributeValue : public AttributeUnion
    {
        inline const std::string& getString() const {
            static const std::string not_a_string;
            OE_SOFT_ASSERT(is<std::string>(), "Attribute value is not a string; consider using getAsString instead");
            return is<std::string>() ? get<std::string>() : not_a_string;
        }

        inline std::string getAsString(const std::string& defaultValue = {}) const {
            return
                is<std::string>() ? get<std::string>() :
                is<double>() ? std::to_string(get<double>()) :
                is<std::int64_t>() ? std::to_string(get<std::int64_t>()) :
                is<bool>() ? (get<bool>() ? "true" : "false") :
                defaultValue;
        }

        inline double getDouble() const {
            OE_SOFT_ASSERT(is<double>(), "Attribute value is not a double; consider using getAsDouble instead");
            return is<double>() ? get<double>() : 0.0;
        }

        inline double getAsDouble(double defaultValue = 0.0) const {
            return
                is<double>() ? get<double>() :
                is<std::int64_t>() ? static_cast<double>(get<std::int64_t>()) :
                is<std::string>() ? std::atof(get<std::string>().c_str()) :
                is<bool>() ? (get<bool>() ? 1.0 : 0.0) :
                defaultValue;
        }

        inline std::int64_t getInt() const {
            OE_SOFT_ASSERT(is<std::int64_t>(), "Attribute value is not an int; consider using getAsInt instead");
            return is<std::int64_t>() ? get<std::int64_t>() : 0LL;
        }

        inline std::int64_t getAsInt(std::int64_t defaultValue = 0) const {
            return
                is<std::int64_t>() ? get<std::int64_t>() :
                is<double>() ? static_cast<std::int64_t>(get<double>()) :
                is<std::string>() ? std::atoll(get<std::string>().c_str()) :
                is<bool>() ? (get<bool>() ? 1LL : 0LL) :
                defaultValue;
        }

        inline bool getBool() const {
            OE_SOFT_ASSERT(is<bool>(), "Attribute value is not a bool; consider using getAsBool instead");
            return is<bool>() ? get<bool>() : false;
        }

        inline bool getAsBool(bool defaultValue = false) const {
            return
                is<bool>() ? get<bool>() :
                is<std::int64_t>() ? (get<std::int64_t>() != 0LL) :
                is<double>() ? (get<double>() != 0.0) :
                is<std::string>() ? Util::ci_equals(get<std::string>(), "true") || get<std::string>() == "on" || get<std::string>() == "1" :
                defaultValue;
        }

        inline AttributeType getType() const {
            return (AttributeType)index();
        }

        template<class T> inline bool is() const {
            return std::holds_alternative<T>(*this);
        }

        template<class T> inline const T& get() const {
            return std::get<T>(*this);
        }
    };

    using AttributeTable = vector_map<std::string, AttributeValue>;

    using FeatureID = std::int64_t;

    // MUST be a vector_map to maintain attribute index order.
    using FeatureSchema = vector_map<std::string, AttributeType>;

    class Feature;

    using FeatureList = std::vector<osg::ref_ptr<Feature>>;

    /**
     * Basic building block of vector feature data.
     */
    class OSGEARTH_EXPORT Feature : public osg::Referenced
    {
    public:

        //! Construct a feature
        Feature(Geometry* geom, const SpatialReference* srs, const Style& style =Style(), FeatureID fid =0LL );

        //! Construct a feature (copy)
        Feature(const Feature& rhs);

    public:

        //! The unique ID of this feature (unique relative to its provider)
        FeatureID getFID() const { return _fid; }

        //! Set the FID of this feature.
        void setFID(FeatureID fid);

        //! Extent of this feature
        GeoExtent getExtent() const;

        //! The geometry in this feature.
        void setGeometry( Geometry* geom );
        Geometry* getGeometry() { return _geom.get(); }
        const Geometry* getGeometry() const { return _geom.get(); }

        //! The spatial reference of the geometry in this feature.
        const SpatialReference* getSRS() const { return _srs.get(); }
        void setSRS( const SpatialReference* srs );

        //! Computes the bound of this feature in the specified SRS.
        bool getWorldBound( const SpatialReference* srs, osg::BoundingSphered& out_bound ) const;

        //! Gets a polytope, in world coordinates (proj or ECEF) that bounds the
        //! geographic extents covered by this feature. This is useful for roughly
        //! intersecting the feature with the terrain graph.
        bool getWorldBoundingPolytope( const SpatialReference* srs, osg::Polytope& out_polytope ) const;

        //! Gets a polytope, in world coordinates (proj or ECEF) that bounds the
        //! world coordinates covered by the given bounding sphere. This is useful for roughly
        //! intersecting features with the terrain graph.
        static bool getWorldBoundingPolytope( const osg::BoundingSphered& bs, const SpatialReference* srs, osg::Polytope& out_polytope );

        //! Calculates the extent of this feature.
        OE_DEPRECATED("Use getExtent() instead")
        GeoExtent calculateExtent() const;

        const AttributeTable& getAttrs() const { return _attrs; }

        //! Sets an attribute value
        void set(const std::string& name, const std::string& value);
        void set(const std::string& name, const char* value);
        void set(const std::string& name, double value);
        void set(const std::string& name, std::int64_t value);
        void set(const std::string& name, int value) { set(name, static_cast<std::int64_t>(value)); }
        void set(const std::string& name, bool value);
        void set(const std::string& name, const AttributeValue& value);

        //! Sets the named attribute to null (with no implicit type)
        void setNull(const std::string& name);

        //! Removes an attribute
        void removeAttribute(const std::string& name);

        bool hasAttr( const std::string& name ) const;

        std::string getString( const std::string& name ) const;
        double getDouble( const std::string& name, double defaultValue =0.0 ) const;
        std::int64_t getInt( const std::string& name, std::int64_t defaultValue =0 ) const;
        bool getBool( const std::string& name, bool defaultValue =false ) const;

        //! Index of the names attribute (for fast access)
        int indexOf(const std::string& name) const {
            return _attrs.indexOf(name);
        }

        inline std::string getString(int index) const {
            return _attrs.at(index).getString();
        }
        inline double getDouble(int index) const {
            return _attrs.at(index).getDouble();
        }
        inline std::int64_t getInt(int index) const {
            return _attrs.at(index).getInt();
        }
        inline bool getBool(int index) const {
            return _attrs.at(index).getBool();
        }        

        //! Whether the attribute is set, meaning it is non-NULL
        bool isSet(const std::string& name) const;

        //! Optional embedded style
        const Style* style() const { return _style.get(); }
        const Style* getStyle() const { return _style.get(); }
        Style& getOrCreateStyle();
        void setStyle(const Style& style);

        /** Geodetic interpolation method. */
        optional<GeoInterpolation>& geoInterp() { return _geoInterp; }
        const optional<GeoInterpolation>& geoInterp() const { return _geoInterp; }

    public:
        //! Gets a GeoJSON representation of this Feature.
        //! @param includeNulls if true, null properties will be included in the output
        std::string getGeoJSON(bool includeNulls = false) const;

        //! Gets a GeoJSON representation of a collection of features.
        //! @param features the features to convert
        //! @param includeNulls if true, null properties will be included in the output
        static std::string featuresToGeoJSON(const FeatureList& features, bool includeNulls = false);

        //! Transforms this Feature to the given SpatialReference
        void transform( const SpatialReference* srs );

        //! Splits this feature into multiple features if it is a geodetic feature and cross the date line.
        void splitAcrossAntimeridian();

    protected:

        Feature() = default;
        Feature(FeatureID fid);

        virtual ~Feature() { }

        FeatureID _fid = 0LL;
        osg::ref_ptr<Geometry> _geom;
        osg::ref_ptr<const SpatialReference> _srs;
        AttributeTable _attrs;
        optional<GeoInterpolation> _geoInterp;
        std::shared_ptr<Style> _style;
    };

    //! Evaluate an expression against a feature and a filter context.
    extern OSGEARTH_EXPORT std::string evaluateExpression(const std::string& expr, Feature* feature, const FilterContext& context);
    extern OSGEARTH_EXPORT std::string evaluateExpression(const std::string& expr, osg::ref_ptr<Feature> feature, const FilterContext& context);

} // namespace osgEarth
