/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
 * Copyright 2025 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/>
 */
#pragma once
#include <osgEarthProcedural/Export>
#include <osgEarth/Common>
#include <osg/Vec3d>
#include <osg/Node>
#include <vector>
#include <deque>
#include <memory>
#include <set>
#include <functional>
#include <unordered_map>
#include <unordered_set>
#include <cstdint>

namespace osgEarth {
    class Feature;
    class Geometry;
    class GeoExtent;
}

namespace osgEarth {
    namespace Procedural
    {
        class OSGEARTHPROCEDURAL_EXPORT RoadNetwork
        {
        public:
            // 1eN, where N = decimal places of precision for comparing 2D coordinates
            static constexpr double precision = 1e3;

            struct QuantizedVec3d
            {
                const double _x, _y;
                const std::int64_t _xr, _yr; // quantized coordinates
                mutable double _z;

                QuantizedVec3d(const osg::Vec3d& p) :
                    _x(p.x()), _y(p.y()), _z(p.z()),
                    _xr(static_cast<std::int64_t>(p.x() * precision)), _yr(static_cast<std::int64_t>(p.y() * precision)) { }

                const double& x() const { return _x; }
                const double& y() const { return _y; }
                const double& z() const { return _z; }

                inline bool operator == (const QuantizedVec3d& rhs) const {
                    return _xr == rhs._xr && _yr == rhs._yr;
                }

                inline bool operator != (const QuantizedVec3d& rhs) const {
                    return _xr != rhs._xr || _yr != rhs._yr;
                }

                inline bool operator == (const osg::Vec3d& rhs) const {
                    return operator == (QuantizedVec3d(rhs));
                }

                inline bool operator != (const osg::Vec3d& rhs) const {
                    return operator != (QuantizedVec3d(rhs));
                }

                inline bool operator < (const QuantizedVec3d& rhs) const {
                    if (_xr < rhs._xr) return true;
                    if (_xr > rhs._xr) return false;
                    return _yr < rhs._yr;
                }
            };

            struct Way;

            /**
            * "Junction" is a point where one or more Ways meet. The Way itself
            * may have a linestring, but its endpoints are called Junctions.
            */
            struct Junction : public QuantizedVec3d
            {
                Junction(const osg::Vec3d& p) : QuantizedVec3d(p) { }

                inline bool is_endpoint() const { return ways.size() == 1; }
                inline bool is_midpoint() const { return ways.size() > 1; }

                mutable std::vector<Way*> ways;
            };

            using JunctionSet = std::set<Junction>;

            /**
            * "Way" is a linear feature made up of two or more points and is a direct
            * representation of Geometry within a Feature.
            */
            struct Way
            {
                Way(const Junction& a, const Junction& b, Feature* f, Geometry* g) :
                    start(&a), end(&b), feature(f), geometry(g) { }
                const Junction* start = nullptr;
                const Junction* end = nullptr;
                double length = 0.0;
                Feature* feature = nullptr;
                Geometry* geometry = nullptr;
            };

            /**
            * "Relation" is just a string of "Way" objects that are connected by there endpoints
            * and (probably) represent the same conceptual feature (like a named road or a bridge)
            */
            struct Relation
            {
                std::vector<Way*> ways;
                double length = 0.0;
            };

            RoadNetwork() = default;

            JunctionSet junctions;
            JunctionSet midpoints;
            std::deque<Way> ways; // must use a container that doesn't invalidate points to members
            std::vector<Relation> relations;
            std::function<Way*(const Junction& junction, Way* incoming_way, const std::vector<Way*>& exclusions)> nextWayInRelation;
            std::function<bool(const Feature* a, const Feature* b)> canMerge;
            std::unordered_map<const Geometry*, std::pair<bool, bool>> geometryEndpointFlags; // front,back

            //! Add a (linear) feature. Each of its endpoints will become a Junction.
            //! One or both may already exist in the network. Each of its full
            //! Geometry's will become a Way.
            void addFeature(Feature* feature);

            //! Add a single node to the map
            const Junction& addJunction(const osg::Vec3d& p);

            //! Combine edges that share nodes into strings of edges called EdgeStrings.
            //! Find all endpoints (nodes with only one edge) and traverse until you find
            //! another node with only one edge. If there is a fork, use the user lambda
            //! functions to decide which way to go.
            void buildRelations();

            //! Get the features for which the centroid of the relation owning the
            //! feature in within the extent.
            void getFeatures(const GeoExtent& extent, std::unordered_set<std::int64_t>& output) const;

            //! Searches through each Relation and looks for opportunities to merge
            //! the comprising Ways into single features for the purposes of rendering.
            //! Sometimes two ways will abut, but they will not render as a connected
            //! feature - this helps mitigate that.
            //! TODO: FUTURE: create valid intersections at locations where incompatible
            //! features abut.
            void mergeRelations(std::vector<osg::ref_ptr<Feature>>& output);
        };
    }
}