// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2003 David Megginson (public domain)

#pragma once

#include <cassert>
#include <memory>

#include <simgear/props/props.hxx>
#include <simgear/structure/subsystem_mgr.hxx>
#include <simgear/props/tiedpropertylist.hxx>

#include <Navaids/positioned.hxx>
#include <Navaids/FlightPlan.hxx>
#include <Instrumentation/rnav_waypt_controller.hxx>

#define FG_210_COMPAT 1

/**
 * Model a GPS radio.
 *
 * Input properties:
 *
 * /position/longitude-deg
 * /position/latitude-deg
 * /position/altitude-ft
 * /environment/magnetic-variation-deg
 * /systems/electrical/outputs/gps
 * /instrumentation/gps/serviceable
 *
 *
 * Output properties:
 *
 * /instrumentation/gps/indicated-longitude-deg
 * /instrumentation/gps/indicated-latitude-deg
 * /instrumentation/gps/indicated-altitude-ft
 * /instrumentation/gps/indicated-vertical-speed-fpm
 * /instrumentation/gps/indicated-track-true-deg
 * /instrumentation/gps/indicated-track-magnetic-deg
 * /instrumentation/gps/indicated-ground-speed-kt
 *
 * /instrumentation/gps/wp-distance-nm
 * /instrumentation/gps/wp-bearing-deg
 * /instrumentation/gps/wp-bearing-mag-deg
 * /instrumentation/gps/TTW
 * /instrumentation/gps/course-deviation-deg
 * /instrumentation/gps/course-error-nm
 * /instrumentation/gps/to-flag
 * /instrumentation/gps/odometer
 * /instrumentation/gps/trip-odometer
 * /instrumentation/gps/true-bug-error-deg
 * /instrumentation/gps/magnetic-bug-error-deg
 */
class GPS : public SGSubsystem,
            public flightgear::RNAV,
            public flightgear::FlightPlan::Delegate
{
public:
    GPS (SGPropertyNode *node, bool defaultGPSMode = false);
    GPS ();
    virtual ~GPS();

  // SGSubsystem interface
    void init() override;
    void reinit() override;
    void bind() override;
    void unbind() override;
    void update (double delta_time_sec) override;
    void shutdown() override;

    // Subsystem identification.
    static const char* staticSubsystemClassId() { return "gps"; }

  // RNAV interface
    SGGeod position() override;
    double trackDeg() override;
    double groundSpeedKts() override;
    double vspeedFPM() override;
    double magvarDeg() override;
    double selectedMagCourse() override;
    double overflightDistanceM() override;
    double overflightArmDistanceM() override;
    double overflightArmAngleDeg() override;
    bool canFlyBy() const override;
    double maxFlyByTurnAngleDeg() const override;

    std::optional<LegData> previousLegData() override;

    std::optional<double> nextLegTrack() override;

    double turnRadiusNm(double groundSpeedKnots) override;
private:
    friend class SearchFilter;

    void setFlyByMaxTurnAngle(double maxAngle);

    /**
     * Configuration manager, track data relating to aircraft installation
     */
    class Config
    {
    public:
        Config();

        void bind(GPS* aOwner, SGPropertyNode* aCfg);

        bool turnAnticipationEnabled() const { return _enableTurnAnticipation; }

        /**
         * Desired turn rate in degrees/second. From this we derive the turn
         * radius and hence how early we need to anticipate it.
         */
        double turnRateDegSec() const        { return _turnRate; }

        /**
         * Distance at which we switch to next waypoint.
         */
        double overflightDistanceNm() const { return _overflightDistance; }
        /**
         * Distance at which we arm overflight sequencing. Once inside this
         * distance, a change of the wp1 'TO' flag to false will be considered
         * overlight of the wp.
         */
        double overflightArmDistanceNm() const { return _overflightArmDistance; }
        /**
         * abs angle at which we arm overflight sequencing.
         */
        double overflightArmAngleDeg() const { return _overflightArmAngle; }

        /**
         * Time before the next WP to activate an external annunciator
         */
        double waypointAlertTime() const     { return _waypointAlertTime; }

        bool requireHardSurface() const      { return _requireHardSurface; }

        bool cdiDeflectionIsAngular() const  { return (_cdiMaxDeflectionNm <= 0.0); }

        double cdiDeflectionLinearPeg() const
        {
            assert(_cdiMaxDeflectionNm > 0.0);
            return _cdiMaxDeflectionNm;
        }

        bool driveAutopilot() const          { return _driveAutopilot; }

        bool courseSelectable() const        { return _courseSelectable; }

        /**
         * Select whether we fly the leg track between waypoints, or
         * use a direct course from the turn end. Since this is likely confusing,
         * look at: http://fgfs.goneabitbursar.com//screenshots/FlyByType-LegType.svg
         * For fly-by waypoints, there is no difference. For fly-over waypoints,
         * this selects if we fly TF or DF mode.
         */
        bool followLegTrackToFix() const      { return _followLegTrackToFix; }


        bool delegateDoesSequencing() const    { return _delegateSequencing; }

        double maxFlyByTurnAngleDeg() const { return _maxFlyByTurnAngle; }

        void setMaxFlyByTurnAngle(double deg)
        {
            _maxFlyByTurnAngle = deg;
        }

    private:
        bool _enableTurnAnticipation;

        // desired turn rate in degrees per second
        double _turnRate;

        // distance from waypoint to arm overflight sequencing (in nm)
        double _overflightDistance;

        // distance from waypoint to arm overflight sequencing (in nm)
        double _overflightArmDistance;

        //abs angle from course to waypoint to arm overflight sequencing (in deg)
        double _overflightArmAngle;

        // time before reaching a waypoint to trigger annunciator light/sound
        // (in seconds)
        double _waypointAlertTime;

        // should we require a hard-surfaced runway when filtering?
        bool _requireHardSurface;

        double _cdiMaxDeflectionNm;

        // should we drive the autopilot directly or not?
        bool _driveAutopilot;

        // is selected-course-deg read to set desired-course or not?
        bool _courseSelectable;

        // do we fly direct to fixes, or follow the leg track closely?
        bool _followLegTrackToFix;

        // do we handle waypoint sequencing ourselves, or let the delegate do it?
        // default is we do it, for backwards compatibility
        bool _delegateSequencing = false;

        double _maxFlyByTurnAngle = 90.0;
    };

    class SearchFilter : public FGPositioned::Filter
    {
    public:
        virtual bool pass(FGPositioned* aPos) const;

        virtual FGPositioned::Type minType() const;
        virtual FGPositioned::Type maxType() const;
    };

    /** reset all output properties to default / non-service values */
    void clearOutput();

    void updateBasicData(double dt);

    void updateTrackingBug();
    void updateRouteData();
    void driveAutopilot();

    /** Update one-shot things when WP1 / leg data change */
    void wp1Changed();

    void clearScratch();

    /** Predicate, determine if the lon/lat position in the scratch is
     * valid or not. */
    bool isScratchPositionValid() const;
    FGPositionedRef positionedFromScratch() const;

#if FG_210_COMPAT
    void setScratchFromPositioned(FGPositioned* aPos, int aIndex);
    void setScratchFromCachedSearchResult();
    void setScratchFromRouteWaypoint(int aIndex);

    /** Add airport-specific information to a scratch result */
    void addAirportToScratch(FGAirport* aAirport);

    FGPositioned::Filter* createFilter(FGPositioned::Type aTy);

    /** Search kernel - called each time we step through a result */
    void performSearch();

    // command handlers
    void loadRouteWaypoint();
    void loadNearest();
    void search();
    void nextResult();
    void previousResult();
    void defineWaypoint();
    void insertWaypointAtIndex(int aIndex);
    void removeWaypointAtIndex(int aIndex);
    void commandExitHold();

    // tied-property getter/setters
    double getScratchDistance() const;
    double getScratchMagBearing() const;
    double getScratchTrueBearing() const;
    bool getScratchHasNext() const;

#endif

    // command handlers
    void selectLegMode();
    void selectOBSMode(flightgear::Waypt* waypt);
    void directTo();

    // tied-property getter/setters
    void setCommand(const char* aCmd);
    const char* getCommand() const { return ""; }

    const char* getMode() const { return _mode.c_str(); }
    bool getScratchValid() const { return _scratchValid; }

    double getSelectedCourse() const { return _selectedCourse; }
    void setSelectedCourse(double crs);
    double getDesiredCourse() const { return _desiredCourse; }

    double getCDIDeflection() const;

    double getLegDistance() const;
    double getLegCourse() const;
    double getLegMagCourse() const;

    double getTrueTrack() const { return _last_true_track; }
    double getMagTrack() const;
    double getGroundspeedKts() const { return _last_speed_kts; }
    double getVerticalSpeed() const { return _last_vertical_speed; }

    const char* getWP0Ident() const;
    const char* getWP0Name() const;

    bool getWP1IValid() const;
    const char* getWP1Ident() const;
    const char* getWP1Name() const;

    double getWP1Distance() const;
    double getWP1TTW() const;
    const char* getWP1TTWString() const;
    double getWP1Bearing() const;
    double getWP1MagBearing() const;
    double getWP1CourseDeviation() const;
    double getWP1CourseErrorNm() const;
    bool getWP1ToFlag() const;
    bool getWP1FromFlag() const;

    // true-bearing-error and mag-bearing-error

    double computeTurnRadiusNm(double aGroundSpeedKts) const;

    /**
     * Tied-properties helper, record nodes which are tied for easy un-tie-ing
     */
    template <typename T>
    void tie(SGPropertyNode* aNode, const char* aRelPath, const SGRawValue<T>& aRawValue)
    {
        _tiedProperties.Tie(aNode->getNode(aRelPath, true), aRawValue);
    }

    /** helper, tie the lat/lon/elev of a SGGeod to the named children of aNode */
    void tieSGGeod(SGPropertyNode* aNode, SGGeod& aRef,
                   const char* lonStr, const char* latStr, const char* altStr);

    /** helper, tie a SGGeod to properties, but read-only */
    void tieSGGeodReadOnly(SGPropertyNode* aNode, SGGeod& aRef,
                           const char* lonStr, const char* latStr, const char* altStr);

    void updateCurrentWpNode(const SGGeod& p);

// FlightPlan::Delegate
    void currentWaypointChanged() override;
    void waypointsChanged() override;
    void cleared() override;
    void endOfFlightPlan() override;

    void doSequence();
    void routeManagerFlightPlanChanged(SGPropertyNode*);
    void routeActivated(SGPropertyNode*);

// members
    SGPropertyNode_ptr _gpsNode;
    SGPropertyNode_ptr _currentWayptNode;
    SGPropertyNode_ptr _currentWpLatNode,
        _currentWpLonNode, _currentWpAltNode;

    SGPropertyNode_ptr _magvar_node;
    SGPropertyNode_ptr _serviceable_node;
    SGPropertyNode_ptr _electrical_node;
    SGPropertyNode_ptr _tracking_bug_node;
    SGPropertyNode_ptr _raim_node;

    SGPropertyNode_ptr _odometer_node;
    SGPropertyNode_ptr _trip_odometer_node;
    SGPropertyNode_ptr _true_bug_error_node;
    SGPropertyNode_ptr _magnetic_bug_error_node;
    SGPropertyNode_ptr _eastWestVelocity;
    SGPropertyNode_ptr _northSouthVelocity;

    //  SGPropertyNode_ptr _route_active_node;
    SGPropertyNode_ptr _route_current_wp_node;
    SGPropertyNode_ptr _routeDistanceNm;
    SGPropertyNode_ptr _routeETE;
    SGPropertyNode_ptr _desiredCourseNode;

    double _selectedCourse;
    double _desiredCourse;

    bool _dataValid;
    SGGeod _last_pos;
    bool _lastPosValid;
    double _last_speed_kts;
    double _last_true_track;
    double _last_vertical_speed;
    double _lastEWVelocity;
    double _lastNSVelocity;

    /**
     * the instrument manager creates a default instance of us,
     * if no explicit GPS is specific in the aircraft's instruments.xml file.
     * This allows default route-following to work with the generic autopilot.
     * This flag is set in that case, to inform us we're a 'fake' installation,
     * and not to worry about electrical power or similar.
     */
    bool _defaultGPSMode;

    std::string _mode;
    Config _config;
    std::string _name;
    int _num;

    SGGeod _wp0_position;
    SGGeod _indicated_pos;
    double _legDistanceNm;

    // scratch data
    SGGeod _scratchPos;
    SGPropertyNode_ptr _scratchNode;
    bool _scratchValid;
#if FG_210_COMPAT
    // search data
    int _searchResultIndex;
    std::string _searchQuery;
    FGPositioned::Type _searchType;
    bool _searchExact;
    FGPositionedList _searchResults;
    bool _searchIsRoute; ///< set if 'search' is actually the current route
    bool _searchHasNext; ///< is there a result after this one?
    bool _searchNames; ///< set if we're searching names instead of idents
#endif

    std::optional<RNAV::LegData> _wp0Data;

    std::unique_ptr<flightgear::WayptController> _wayptController;

    flightgear::WayptRef _prevWaypt;
    flightgear::WayptRef _currentWaypt;

    // autopilot drive properties
    SGPropertyNode_ptr _apDrivingFlag;
    SGPropertyNode_ptr _apTrueHeading;

    simgear::TiedPropertyList _tiedProperties;

    flightgear::FlightPlanRef _route;

    SGPropertyChangeCallback<GPS> _callbackFlightPlanChanged;
    SGPropertyChangeCallback<GPS> _callbackRouteActivated;
};
