/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
 * Copyright 2020 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 <osgEarth/Common>
#include <osgEarth/Config>
#include <ostream>

// make sure we don't collide with common macros
#undef DEGREES
#undef RADIANS

namespace osgEarth
{
    class Registry;

    namespace Units
    {
        enum class Domain
        {
            DISTANCE,
            ANGLE,
            TIME,
            SPEED,
            SCREEN,
            INVALID
        };
    }

    class OSGEARTH_EXPORT UnitsType
    {
    public:
        inline bool valid() const {
            return
                _type == Units::Domain::SPEED ? (_distance != nullptr && _time != nullptr) :
                _type != Units::Domain::INVALID;
        }

        inline bool canConvert(const UnitsType& to) const;

        inline bool convertTo(const UnitsType& to, double input, double& output) const;

        inline double convertTo(const UnitsType& to, double input) const;

        const std::string& getName() const { return _name; }

        const std::string& getAbbr() const { return _abbr; }

        const Units::Domain& getDomain() const { return _type; }

        bool operator == (const UnitsType& rhs) const {
            return
                valid() &&
                rhs.valid() &&
                _type == rhs._type &&
                _toBase == rhs._toBase &&
                (_type != Units::Domain::SPEED || *_distance == *rhs._distance) &&
                (_type != Units::Domain::SPEED || *_time == *rhs._time);
        }

        bool operator != (const UnitsType& rhs) const {
            return !operator==(rhs);
        }

        bool isLinear() const { return _type == Units::Domain::DISTANCE; }
        bool isDistance() const { return _type == Units::Domain::DISTANCE; }
        bool isAngular() const { return _type == Units::Domain::ANGLE; }
        bool isAngle() const { return _type == Units::Domain::ANGLE; }
        bool isTemporal() const { return _type == Units::Domain::TIME; }
        bool isTime() const { return _type == Units::Domain::TIME; }
        bool isSpeed() const { return _type == Units::Domain::SPEED; }
        bool isScreenSize() const { return _type == Units::Domain::SCREEN; }

        // Make a new unit definition (LINEAR, ANGULAR, TEMPORAL, SCREEN)
        UnitsType(const char* name, const char* abbr, const Units::Domain& type, double toBase) :
            _name(name),
            _abbr(abbr),
            _type(type),
            _toBase(toBase) { }

        // Maks a new unit definition (SPEED)
        UnitsType(const char* name, const char* abbr, const UnitsType& distance, const UnitsType& time) :
            _name(name),
            _abbr(abbr),
            _type(Units::Domain::SPEED),
            _toBase(1.0),
            _distance(&distance),
            _time(&time) { }

        UnitsType() { }

        std::string _name;
        std::string _abbr;
        Units::Domain _type = Units::Domain::INVALID;
        double _toBase = 0.0;
        const UnitsType* _distance = nullptr;
        const UnitsType* _time = nullptr;
    };


    namespace Units
    {
        // Distances; factor converts to METERS:
        const UnitsType CENTIMETERS("centimeters", "cm", Units::Domain::DISTANCE, 0.01);
        const UnitsType FEET("feet", "ft", Units::Domain::DISTANCE, 0.3048);
        const UnitsType FEET_US_SURVEY("feet(us)", "ft", Units::Domain::DISTANCE, 12.0 / 39.37);
        const UnitsType KILOMETERS("kilometers", "km", Units::Domain::DISTANCE, 1000.0);
        const UnitsType METERS("meters", "m", Units::Domain::DISTANCE, 1.0);
        const UnitsType MILES("miles", "mi", Units::Domain::DISTANCE, 1609.334);
        const UnitsType MILLIMETERS("millimeters", "mm", Units::Domain::DISTANCE, 0.001);

        const UnitsType YARDS("yards", "yd", Units::Domain::DISTANCE, 0.9144);
        const UnitsType NAUTICAL_MILES("nautical miles", "nm", Units::Domain::DISTANCE, 1852.0);
        const UnitsType DATA_MILES("data miles", "dm", Units::Domain::DISTANCE, 1828.8);
        const UnitsType INCHES("inches", "in", Units::Domain::DISTANCE, 0.0254);
        const UnitsType FATHOMS("fathoms", "fm", Units::Domain::DISTANCE, 1.8288);
        const UnitsType KILOFEET("kilofeet", "kf", Units::Domain::DISTANCE, 304.8);
        const UnitsType KILOYARDS("kiloyards", "kyd", Units::Domain::DISTANCE, 914.4);

        // Factor converts unit into RADIANS:
        const UnitsType DEGREES("degrees", "\xb0", Units::Domain::ANGLE, 0.017453292519943295);
        const UnitsType RADIANS("radians", "rad", Units::Domain::ANGLE, 1.0);
        const UnitsType BAM("BAM", "bam", Units::Domain::ANGLE, 6.283185307179586476925286766559);
        const UnitsType NATO_MILS("mils", "mil", Units::Domain::ANGLE, 9.8174770424681038701957605727484e-4);
        const UnitsType DECIMAL_HOURS("hours", "h", Units::Domain::ANGLE, 15.0 * 0.017453292519943295);

        // Factor convert unit into SECONDS:
        const UnitsType DAYS("days", "d", Units::Domain::TIME, 86400.0);
        const UnitsType HOURS("hours", "hr", Units::Domain::TIME, 3600.0);
        const UnitsType MICROSECONDS("microseconds", "us", Units::Domain::TIME, 0.000001);
        const UnitsType MILLISECONDS("milliseconds", "ms", Units::Domain::TIME, 0.001);
        const UnitsType MINUTES("minutes", "min", Units::Domain::TIME, 60.0);
        const UnitsType SECONDS("seconds", "s", Units::Domain::TIME, 1.0);
        const UnitsType WEEKS("weeks", "wk", Units::Domain::TIME, 604800.0);

        const UnitsType FEET_PER_SECOND("feet per second", "ft/s", Units::FEET, Units::SECONDS);
        const UnitsType YARDS_PER_SECOND("yards per second", "yd/s", Units::YARDS, Units::SECONDS);
        const UnitsType METERS_PER_SECOND("meters per second", "m/s", Units::METERS, Units::SECONDS);
        const UnitsType KILOMETERS_PER_SECOND("kilometers per second", "km/s", Units::KILOMETERS, Units::SECONDS);
        const UnitsType KILOMETERS_PER_HOUR("kilometers per hour", "kmh", Units::KILOMETERS, Units::HOURS);
        const UnitsType MILES_PER_HOUR("miles per hour", "mph", Units::MILES, Units::HOURS);
        const UnitsType DATA_MILES_PER_HOUR("data miles per hour", "dm/h", Units::DATA_MILES, Units::HOURS);
        const UnitsType KNOTS("nautical miles per hour", "kts", Units::NAUTICAL_MILES, Units::HOURS);

        const UnitsType PIXELS("pixels", "px", Units::Domain::SCREEN, 1.0);

        extern OSGEARTH_EXPORT bool parse(const std::string& input, UnitsType& output);
        extern OSGEARTH_EXPORT bool parse(const std::string& input, double& out_value, UnitsType& out_units, const UnitsType& defaultUnits);
        extern OSGEARTH_EXPORT bool parse(const std::string& input, float& out_value, UnitsType& out_units, const UnitsType& defaultUnits);
        //extern OSGEARTH_EXPORT bool parse(const std::string& input, int& out_value, UnitsType& out_units, const UnitsType& defaultUnits);


        inline bool canConvert(const UnitsType& from, const UnitsType& to) {
            return from.getDomain() == to.getDomain();
        }

        inline void convertSimple(const UnitsType& from, const UnitsType& to, double input, double& output) {
            output = input * from._toBase / to._toBase;
        }

        inline void convertSpeed(const UnitsType& from, const UnitsType& to, double input, double& output) {
            double t = from._distance->convertTo(*to._distance, input);
            output = to._time->convertTo(*from._time, t);
        }

        inline bool convert(const UnitsType& from, const UnitsType& to, double input, double& output) {
            if (canConvert(from, to)) {
                if (from.isDistance() || from.isAngle() || from.isTime())
                    convertSimple(from, to, input, output);
                else if (from.isSpeed())
                    convertSpeed(from, to, input, output);
                return true;
            }
            return false;
        }

        inline double convert(const UnitsType& from, const UnitsType& to, double input) {
            double output = input;
            convert(from, to, input, output);
            return output;
        }

        extern OSGEARTH_EXPORT void registerAll(Registry* registry);

        extern OSGEARTH_EXPORT int unitTest();
    }

    template<> inline Config& Config::set<UnitsType>(const std::string& key, const UnitsType& obj) {
        set(key, obj._name);
        return *this;
    }
    template<> inline Config& Config::set<UnitsType>(const std::string& key, const optional<UnitsType>& opt) {
        if (opt.isSet()) { set(key, opt.get()); }
        return *this;
    }
    template<> inline bool Config::get<UnitsType>(const std::string& key, optional<UnitsType>& output) const {
        if (hasValue(key)) {
            UnitsType out;
            if (Units::parse(value(key), out)) {
                output = out;
                return true;
            }
        }
        return false;
    }

    namespace detail
    {
        template<typename T>
        class qualified_double
        {
        public:
            qualified_double(double value, const UnitsType& units) : _value(value), _units(units) { }

            qualified_double(const T& rhs) : _value(rhs._value), _units(rhs._units) { }

            // parses the qualified number from a parseable string (e.g., "123km")
            qualified_double(const std::string& parseable, const UnitsType& defaultUnits) : _value(0.0), _units(defaultUnits) {
                _parsedOK = Units::parse(parseable, _value, _units, defaultUnits);
            }

            void set(double value, const UnitsType& units) {
                _value = value;
                _units = units;
            }

            T& operator = (const T& rhs) {
                set(rhs._value, rhs._units);
                return static_cast<T&>(*this);
            }

            T operator + (const T& rhs) const {
                return _units.canConvert(rhs._units) ?
                    T(_value + rhs.as(_units), _units) :
                    T(0, {});
            }

            T operator - (const T& rhs) const {
                return _units.canConvert(rhs._units) ?
                    T(_value - rhs.as(_units), _units) :
                    T(0, {});
            }

            T operator * (double rhs) const {
                return T(_value * rhs, _units);
            }

            T operator / (double rhs) const {
                return T(_value / rhs, _units);
            }

            bool operator == (const T& rhs) const {
                return _units.canConvert(rhs._units) && rhs.as(_units) == _value;
            }

            bool operator != (const T& rhs) const {
                return !_units.canConvert(rhs._units) || rhs.as(_units) != _value;
            }

            bool operator < (const T& rhs) const {
                return _units.canConvert(rhs._units) && _value < rhs.as(_units);
            }

            bool operator <= (const T& rhs) const {
                return _units.canConvert(rhs._units) && _value <= rhs.as(_units);
            }

            bool operator > (const T& rhs) const {
                return _units.canConvert(rhs._units) && _value > rhs.as(_units);
            }

            bool operator >= (const T& rhs) const {
                return _units.canConvert(rhs._units) && _value >= rhs.as(_units);
            }

            bool operator == (double rhs) const {
                return _value == rhs;
            }

            bool operator != (double rhs) const {
                return _value != rhs;
            }

            double as(const UnitsType& convertTo) const {
                return _units.convertTo(convertTo, _value);
            }

            T to(const UnitsType& convertTo) const {
                return T(as(convertTo), convertTo);
            }

            double getValue() const { return _value; }
            const UnitsType& getUnits() const { return _units; }

            std::string asString() const {
                return Stringify() << _value << _units.getAbbr();
            }

            virtual std::string asParseableString() const {
                return asString();
            }

            bool fullyParsed() const {
                return _parsedOK;
            }

        protected:
            double _value;
            UnitsType _units;
            bool _parsedOK = true;
        };
    }

    struct Distance : public detail::qualified_double<Distance> {
        Distance() : detail::qualified_double<Distance>(0, Units::METERS) { }
        Distance(double value, const UnitsType& units) : detail::qualified_double<Distance>(value, units) { }
        Distance(const std::string& str, const UnitsType& fallback) : detail::qualified_double<Distance>(str, fallback) { }
    };
    typedef Distance Linear; // backwards compat

    struct Angle : public detail::qualified_double<Angle> {
        Angle() : detail::qualified_double<Angle>(0, Units::DEGREES) { }
        Angle(double value, const UnitsType& units) : detail::qualified_double<Angle>(value, units) { }
        Angle(const std::string& str, const UnitsType& fallback) : detail::qualified_double<Angle>(str, fallback) { }
        std::string asParseableString() const {
            if ( _units == Units::DEGREES ) return Stringify() << _value;
            else return asString();
        }
    };
    typedef Angle Angular; // backwards compat

    struct Duration : public detail::qualified_double<Duration> {
        Duration() : detail::qualified_double<Duration>(0, Units::SECONDS) { }
        Duration(double value, const UnitsType& units) : detail::qualified_double<Duration>(value, units) { }
        Duration(const std::string& str, const UnitsType& fallback) : detail::qualified_double<Duration>(str, Units::SECONDS) { }
    };
    typedef Duration Temporal; // backwards compat

    struct Speed : public detail::qualified_double<Speed> {
        Speed() : detail::qualified_double<Speed>(0, Units::METERS_PER_SECOND) { }
        Speed(double value, const UnitsType& units) : detail::qualified_double<Speed>(value, units) { }
        Speed(const std::string& str, const UnitsType& fallback) : detail::qualified_double<Speed>(str, Units::METERS_PER_SECOND) { }
    };

    struct ScreenSize : public detail::qualified_double<ScreenSize> {
        ScreenSize() : detail::qualified_double<ScreenSize>(0, Units::PIXELS) { }
        ScreenSize(double value, const UnitsType& units) : detail::qualified_double<ScreenSize>(value, units) { }
        ScreenSize(const std::string& str, const UnitsType& fallback) : detail::qualified_double<ScreenSize>(str, Units::PIXELS) { }
    };



    // UnitsType inlines
    inline bool UnitsType::canConvert(const UnitsType& to) const {
        return _type == to._type;
    }

    inline bool UnitsType::convertTo(const UnitsType& to, double input, double& output)  const {
        return Units::convert(*this, to, input, output);
    }

    inline double UnitsType::convertTo(const UnitsType& to, double input) const {
        return Units::convert(*this, to, input);
    }
    

    // Config specializations:
    
    template<> inline
        Config& Config::set<Distance>(const std::string& key, const Distance& obj) {
        set(key, obj.asParseableString());
        return *this;
    }
    template<> inline
    Config& Config::set<Distance>( const std::string& key, const optional<Distance>& opt ) {
        if ( opt.isSet() ) { set(key, opt.get()); }
        return *this;
    }
    template<> inline
    bool Config::get<Distance>( const std::string& key, optional<Distance>& output ) const {
        if ( hasValue(key) ) { output = Distance(value(key), Units::METERS); return true; }
        else return false;
    }
    
    template<> inline
    Config& Config::set<Angle>( const std::string& key, const Angle& opt ) {
        set(key, opt.asParseableString());
        return *this;
    }
    template<> inline
    Config& Config::set<Angle>( const std::string& key, const optional<Angle>& opt ) {
        if ( opt.isSet() ) { set(key, opt.get()); }
        return *this;
    }
    template<> inline
    bool Config::get<Angle>( const std::string& key, optional<Angle>& output ) const {
        if ( hasValue(key) ) { output = Angle(value(key), Units::DEGREES); return true; }
        else return false;
    }

    
    template<> inline
    Config& Config::set<Duration>( const std::string& key, const Duration& opt ) {
        set(key, opt.asParseableString());
        return *this;
    }
    template<> inline
    Config& Config::set<Duration>( const std::string& key, const optional<Duration>& opt ) {
        if ( opt.isSet() ) { set(key, opt.get()); }
        return *this;
    }

    template<> inline
    bool Config::get<Duration>( const std::string& key, optional<Duration>& output ) const {
        if ( hasValue(key) ) { output = Duration(value(key), Units::SECONDS); return true; }
        else return false;
    }

    template<> inline
    Config& Config::set<Speed>( const std::string& key, const Speed& opt ) {
        set(key, opt.asParseableString());
        return *this;
    }
    template<> inline
    Config& Config::set<Speed>( const std::string& key, const optional<Speed>& opt ) {
        if ( opt.isSet() ) { set(key, opt.get()); }
        return *this;
    }
    template<> inline
    bool Config::get<Speed>( const std::string& key, optional<Speed>& output ) const {
        if ( hasValue(key) ) { output = Speed(value(key), Units::METERS_PER_SECOND); return true; }
        else return false;
    }
    
    template<> inline
    Config& Config::set<ScreenSize>( const std::string& key, const ScreenSize& opt ) {
        set(key, opt.asParseableString());
        return *this;
    }
    template<> inline
    Config& Config::set<ScreenSize>( const std::string& key, const optional<ScreenSize>& opt ) {
        if ( opt.isSet() ) { set(key, opt.get()); }
        return *this;
    }
    template<> inline
    bool Config::get<ScreenSize>( const std::string& key, optional<ScreenSize>& output ) const {
        if ( hasValue(key) ) { output = ScreenSize(value(key), Units::PIXELS); return true; }
        else return false;
    }

    // osgEarth::Strings specializations
    namespace Util
    {
        template<> inline
        Distance as(const std::string& str, const Distance& default_value) {
            double val;
            UnitsType units;
            if (Units::parse(str, val, units, Units::METERS))
                return Distance(val, units);
            else
                return default_value;
        }

        template<> inline
        Angle as(const std::string& str, const Angle& default_value) {
            double val;
            UnitsType units;
            if (Units::parse(str, val, units, Units::DEGREES))
                return Angle(val, units);
            else
                return default_value;
        }

        template<> inline
        Duration as(const std::string& str, const Duration& default_value) {
            double val;
            UnitsType units;
            if (Units::parse(str, val, units, Units::SECONDS))
                return Duration(val, units);
            else
                return default_value;
        }

        template<> inline
        Speed as(const std::string& str, const Speed& default_value) {
            double val;
            UnitsType units;
            if (Units::parse(str, val, units, Units::METERS_PER_SECOND))
                return Speed(val, units);
            else
                return default_value;
        }

        template<> inline
        ScreenSize as(const std::string& str, const ScreenSize& default_value) {
            double val;
            UnitsType units;
            if (Units::parse(str, val, units, Units::PIXELS))
                return ScreenSize(val, units);
            else
                return default_value;
        }
    }
}
