/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
 * Copyright 2018 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/>
 */
#ifndef OSGEARTH_UNITS_H
#define OSGEARTH_UNITS_H 1

#include <osgEarth/Common>
#include <osgEarth/Config>
#include <ostream>

namespace osgEarth
{
    class Registry;

    class OSGEARTH_EXPORT Units
    {
    public:
        // linear
        static const Units CENTIMETERS;
        static const Units DATA_MILES;
        static const Units FATHOMS;
        static const Units FEET;
        static const Units FEET_US_SURVEY;  // http://www.wsdot.wa.gov/reference/metrics/foottometer.htm
        static const Units INCHES;
        static const Units KILOFEET;
        static const Units KILOMETERS;
        static const Units KILOYARDS;
        static const Units METERS;
        static const Units MILES;           // statute miles
        static const Units MILLIMETERS;
        static const Units NAUTICAL_MILES;
        static const Units YARDS;

        // angular
        static const Units BAM;
        static const Units DEGREES;
        static const Units NATO_MILS; // http://www.convertworld.com/en/angle/Mil+(NATO).html
        static const Units RADIANS;
        static const Units DECIMAL_HOURS;

        // temporal
        static const Units DAYS;
        static const Units HOURS;
        static const Units MICROSECONDS;
        static const Units MILLISECONDS;
        static const Units MINUTES;
        static const Units SECONDS;
        static const Units WEEKS;

        // speed
        static const Units FEET_PER_SECOND;
        static const Units YARDS_PER_SECOND;
        static const Units METERS_PER_SECOND;
        static const Units KILOMETERS_PER_SECOND;
        static const Units KILOMETERS_PER_HOUR;
        static const Units MILES_PER_HOUR;
        static const Units DATA_MILES_PER_HOUR;
        static const Units KNOTS;

        // screen
        static const Units PIXELS;

        // unit types.
        enum Type
        { 
            TYPE_LINEAR, 
            TYPE_ANGULAR, 
            TYPE_TEMPORAL, 
            TYPE_SPEED, 
            TYPE_SCREEN_SIZE,
            TYPE_INVALID
        };

    public:

        static bool parse( const std::string& input, Units& output );

        // parses a value+units string (like "15cm" or "24px")
        static bool parse( const std::string& input, double& out_value, Units& out_units, const Units& defaultUnits );
        static bool parse( const std::string& input, float& out_value, Units& out_units, const Units& defaultUnits );
        static bool parse( const std::string& input, int& out_value, Units& out_units, const Units& defaultUnits );

        static bool convert( const Units& from, const Units& to, double input, double& output ) {
            if ( canConvert(from, to) ) {
                if ( from._type == TYPE_LINEAR || from._type == TYPE_ANGULAR || from._type == TYPE_TEMPORAL )
                    convertSimple( from, to, input, output );
                else if ( from._type == TYPE_SPEED )
                    convertSpeed( from, to, input, output );
                return true;
            }
            return false;
        }

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

        static bool canConvert( const Units& from, const Units& to ) {
            return from._type == to._type;
        }

        bool canConvert( const Units& to ) const {
            return _type == to._type;
        }

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

        double convertTo( const Units& to, double input ) const {
            return convert( *this, to, input );
        }
        
        const std::string& getName() const { return _name; }

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

        const Type& getType() const { return _type; }

        bool operator == ( const Units& rhs ) const {
            return _type == rhs._type && _toBase == rhs._toBase; }
        
        bool operator != ( const Units& rhs ) const {
            return _type != rhs._type || _toBase != rhs._toBase; }

        bool isLinear() const { return _type == TYPE_LINEAR; }

        bool isAngular() const { return _type == TYPE_ANGULAR; }

        bool isTemporal() const { return _type == TYPE_TEMPORAL; }

        bool isSpeed() const { return _type == TYPE_SPEED; }

        bool isScreenSize() const { return _type == TYPE_SCREEN_SIZE; }

    public:

        // Make a new unit definition (LINEAR, ANGULAR, TEMPORAL, SCREEN)
        Units( const std::string& name, const std::string& abbr, const Type& type, double toBase );

        // Maks a new unit definition (SPEED)
        Units( const std::string& name, const std::string& abbr, const Units& distance, const Units& time );

        Units() : _type(TYPE_INVALID), _toBase(0.0), _distance(0L), _time(0L) { }

    private:

        static void convertSimple( const Units& from, const Units& to, double input, double& output ) {
            output = input * from._toBase / to._toBase;
        }
        static void convertSpeed( const Units& from, const Units& to, double input, double& output ) {
            double t = from._distance->convertTo( *to._distance, input );
            output = to._time->convertTo( *from._time, t );
        }


        std::string _name, _abbr;
        Type _type;
        double _toBase;
        const Units* _distance;
        const Units* _time;
        

        // called by Registry to register system units
        static void registerAll(class Registry* registry);
        friend class Registry;

    public:

        // returns 0 upon success, error code on failure
        static int unitTest();

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

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

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

        // loads the qualified number from an old-school config (e.g., { value="123" units="km" } )
        qualified_double<T>( const Config& conf, const Units& defaultUnits ) : _value(0.0) {
            if ( conf.hasValue("value") ) {
                _value = conf.value<double>("value", 0.0);
                if ( !Units::parse( conf.value("units"), _units ) )
                    _units = defaultUnits;
            }
        }

        void set( double value, const Units& 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, Units());
        }

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

        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);
        }

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

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

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

        Config getConfig() const {
            Config conf;
            conf.set("value", _value);
            conf.set("units", _units.getAbbr());
            return conf;
        }

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

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

    protected:
        double _value;
        Units  _units;
    };

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

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

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

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

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

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

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

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

    template<> inline
    void Config::set<Speed>( const std::string& key, const Speed& opt ) {
        set(key, opt.asParseableString());
    }
    template<> inline
    void Config::set<Speed>( const std::string& key, const optional<Speed>& opt ) {
        if ( opt.isSet() ) { set(key, opt.get()); }
    }
    template<> inline
    bool Config::get<Speed>( const std::string& key, optional<Speed>& output ) const {
        if ( hasValue(key) ) { output = Speed(value(key)); return true; }
        else return false;
    }
    template<> inline
    Speed as( const std::string& str, const Speed& default_value ) {
        double val;
        Units units;
        if ( Units::parse(str, val, units, Units::METERS_PER_SECOND) )
            return Speed(val, units);
        else
            return default_value;
    }
    
    template<> inline
    void Config::set<ScreenSize>( const std::string& key, const ScreenSize& opt ) {
        set(key, opt.asParseableString());
    }
    template<> inline
    void Config::set<ScreenSize>( const std::string& key, const optional<ScreenSize>& opt ) {
        if ( opt.isSet() ) { set(key, opt.get()); }
    }
    template<> inline
    bool Config::get<ScreenSize>( const std::string& key, optional<ScreenSize>& output ) const {
        if ( hasValue(key) ) { output = ScreenSize(value(key)); return true; }
        else return false;
    }
    template<> inline
    ScreenSize as( const std::string& str, const ScreenSize& default_value ) {
        double val;
        Units units;
        if ( Units::parse(str, val, units, Units::PIXELS) )
            return ScreenSize(val, units);
        else
            return default_value;
    }
}

#endif // OSGEARTH_UNITS_H
