/***************************************************************************
                          dms.h  -  K Desktop Planetarium
                             -------------------
    begin                : Sun Feb 11 2001
    copyright            : (C) 2001 by Jason Harris
    email                : jharris@30doradus.org
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#ifndef DMS_H_
#define DMS_H_

#include <QDebug>

#include <QString>

#include <cmath>
#include "../nan.h"

/** @class dms
 * @short An angle, stored as degrees, but expressible in many ways.
 * @author Jason Harris
 * @version 1.0
 *
 * dms encapsulates an angle.  The angle is stored as a double,
 * equal to the value of the angle in degrees.  Methods are available
 * for setting/getting the angle as a floating-point measured in
 * Degrees or Hours, or as integer triplets (degrees, arcminutes,
 * arcseconds or hours, minutes, seconds).  There is also a method
 * to set the angle according to a radian value, and to return the
 * angle expressed in radians.  Finally, a SinCos() method computes
 * the sin and cosine of the angle. 
 */
class dms {
public:
    /** Default constructor. */
    dms() : D( NaN::d ) {}

    /** @short Set the floating-point value of the angle according to the four integer arguments.
     * @param d degree portion of angle (int).  Defaults to zero.
     * @param m arcminute portion of angle (int).  Defaults to zero.
     * @param s arcsecond portion of angle (int).  Defaults to zero.
     * @param ms arcsecond portion of angle (int).  Defaults to zero.
     */
    explicit dms( const int &d, const int &m=0, const int &s=0, const int &ms=0 ) { setD( d, m, s, ms ); }

    /** @short Construct an angle from a double value.
     *
     * Creates an angle whose value in Degrees is equal to the argument.
     * @param x angle expressed as a floating-point number (in degrees)
     */
    explicit dms( const double &x ) : D(x) {}

    /** @short Construct an angle from a string representation.
     *
     * Attempt to create the angle according to the string argument.  If the string
     * cannot be parsed as an angle value, the angle is set to zero.
     *
     * @warning There is not an unambiguous notification that it failed to parse the string,
     * since the string could have been a valid representation of zero degrees.
     * If this is a concern, use the setFromString() function directly instead.
     *
     * @param s the string to parse as a dms value.
     * @param isDeg if true, value is in degrees; if false, value is in hours.
     * @sa setFromString()
     */
    explicit dms( const QString &s, bool isDeg=true ) { setFromString( s, isDeg ); }

    /** @return integer degrees portion of the angle
     */
    inline int degree() const { return int( D ) ; }

    /** @return integer arcminutes portion of the angle.
     * @note an arcminute is 1/60 degree.
     */
    int arcmin() const;

    /** @return integer arcseconds portion of the angle
     * @note an arcsecond is 1/60 arcmin, or 1/3600 degree.
     */
    int arcsec() const;

    /** @return integer milliarcseconds portion of the angle
     * @note a  milliarcsecond is 1/1000 arcsecond.
     */
    int marcsec() const;

    /** @return angle in degrees expressed as a double.
    	*/
    inline const double& Degrees() const { return D; }

    /** @return integer hours portion of the angle
     * @note an angle can be measured in degrees/arcminutes/arcseconds
     * or hours/minutes/seconds.  An hour is equal to 15 degrees.
     */
    inline int hour() const { return int( reduce().Degrees()/15.0 ); }

    /** @return integer minutes portion of the angle
     * @note a minute is 1/60 hour (not the same as an arcminute)
     */
    int minute() const;

    /** @return integer seconds portion of the angle
     * @note a second is 1/3600 hour (not the same as an arcsecond)
     */
    int second() const;

    /** @return integer milliseconds portion of the angle
     * @note a millisecond is 1/1000 second (not the same as a milliarcsecond)
     */
    int msecond() const;

    /** @return angle in hours expressed as a double.
     * @note an angle can be measured in degrees/arcminutes/arcseconds
     * or hours/minutes/seconds.  An hour is equal to 15 degrees.
     */
    inline double Hours() const { return reduce().Degrees()/15.0; }

    /** Sets floating-point value of angle, in degrees.
     * @param x new angle (double)
     */
    void setD( const double &x ) { D = x; }

    /** @short Sets floating-point value of angle, in degrees.
     * 
     * This is an overloaded member function; it behaves essentially
     * like the above function.  The floating-point value of the angle
     * (D) is determined from the following formulae:
     * 
     * \f$ fabs(D) = fabs(d) + \frac{(m + (s/60))}{60} \f$
     * \f$ sgn(D) = sgn(d) \f$
     * 
     * @param d integer degrees portion of angle
     * @param m integer arcminutes portion of angle
     * @param s integer arcseconds portion of angle
     * @param ms integer arcseconds portion of angle
     */
    void setD( const int &d, const int &m, const int &s, const int &ms=0 );

    /** @short Sets floating-point value of angle, in hours.
     * 
     * Converts argument from hours to degrees, then
     * sets floating-point value of angle, in degrees.
     * @param x new angle, in hours (double)
     * @sa setD()
     */
    void setH( const double &x );

    /** @short Sets floating-point value of angle, in hours.
     * 
     * Converts argument values from hours to degrees, then
     * sets floating-point value of angle, in degrees.
     * This is an overloaded member function, provided for convenience.  It
     * behaves essentially like the above function.
     * @param h integer hours portion of angle
     * @param m integer minutes portion of angle
     * @param s integer seconds portion of angle
     * @param ms integer milliseconds portion of angle
     * @sa setD()
     */
    void setH( const int &h, const int &m, const int &s, const int &ms=0 );

    /** @short Attempt to parse the string argument as a dms value, and set the dms object
     * accordingly.
     * @param s the string to be parsed as a dms value.  The string can be an int or
     * floating-point value, or a triplet of values (d/h, m, s) separated by spaces or colons.
     * @param isDeg if true, the value is in degrees.  Otherwise, it is in hours.
     * @return true if sting was parsed successfully.  Otherwise, set the dms value
     * to 0.0 and return false.
     */
    bool setFromString( const QString &s, bool isDeg=true );

    /** @short Compute Sine and Cosine of the angle simultaneously.
     * On machines using glibc >= 2.1, calling SinCos() is somewhat faster
     * than calling sin() and cos() separately.
     * The values are returned through the arguments (passed by reference).
     *
     * @param s Sine of the angle
     * @param c Cosine of the angle
     * @sa sin() cos()
     */
    inline void SinCos( double &s, double &c ) const;

    /** @short Compute the Angle's Sine.
     *
     * @return the Sine of the angle.
     * @sa cos()
     */
    double sin() const { return ::sin(D*DegToRad); }

    /** @short Compute the Angle's Cosine.
     *
     * @return the Cosine of the angle.
     * @sa sin()
     */
    double cos() const { return ::cos(D*DegToRad); }

    /** @short Express the angle in radians.
     * @return the angle in radians (double)
     */
    double radians() const { return D*DegToRad; }

    /** @short Set angle according to the argument, in radians.
     *
     * This function converts the argument to degrees, then sets the angle
     * with setD().
     * @param a angle in radians
     */
    void setRadians( const double &a );

    /** return the equivalent angle between 0 and 360 degrees.
     * @warning does not change the value of the parent angle itself.
     */
    const dms reduce() const;

    /** @return a nicely-formatted string representation of the angle
     * in degrees, arcminutes, and arcseconds.
     * @param machineReadable uses a colon separator and produces +/-dd:mm:ss format instead
     */
    const QString toDMSString(const bool forceSign = false, const bool machineReadable = false ) const;

    /** @return a nicely-formatted string representation of the angle
     * in hours, minutes, and seconds.
     * @param machineReadable uses a colon separator and produces hh:mm:ss format instead
     */
    const QString toHMSString( const bool machineReadable = false ) const;

    /** PI is a const static member; it's public so that it can be used anywhere,
     * as long as dms.h is included.
     */
    static const double PI;

    /** DegToRad is a const static member equal to the number of radians in
     * one degree (dms::PI/180.0).
     */
    static const double DegToRad;

    /** @short Static function to create a DMS object from a QString.
     *
     * There are several ways to specify the angle:
     * @li Integer numbers  ( 5 or -33 )
     * @li Floating-point numbers  ( 5.0 or -33.0 )
     * @li colon-delimited integers ( 5:0:0 or -33:0:0 )
     * @li colon-delimited with float seconds ( 5:0:0.0 or -33:0:0.0 )
     * @li colon-delimited with float minutes ( 5:0.0 or -33:0.0 )
     * @li space-delimited ( 5 0 0; -33 0 0 ) or ( 5 0.0 or -33 0.0 )
     * @li space-delimited, with unit labels ( 5h 0m 0s or -33d 0m 0s )
     * @param s the string to be parsed as an angle value
     * @param deg if true, s is expressed in degrees; if false, s is expressed in hours
     * @return a dms object whose value is parsed from the string argument
     */
    static dms fromString(const QString & s, bool deg);

    dms operator - () { return dms(-D); }
private:
    double D;

    friend dms operator+(dms, dms);
    friend dms operator-(dms, dms);
};

/// Add two angles
inline dms operator + (dms a, dms b) {
    return dms( a.D + b.D);
}

/// Subtract angles
inline dms operator - (dms a, dms b) {
    return dms( a.D - b.D);
}

// Inline sincos
inline void dms::SinCos(double& s, double& c) const {
#ifdef __GLIBC__
#if ( __GLIBC__ >= 2 && __GLIBC_MINOR__ >=1 && !defined(__UCLIBC__))
    //GNU version
    sincos( radians(), &s, &c );
#else
    //For older GLIBC versions
    s = ::sin( radians() );
    c = ::cos( radians() );
#endif
#else
    //ANSI-compliant version
    s = ::sin( radians() );
    c = ::cos( radians() );
#endif
}

/** Overloaded equality operator */
inline bool operator == (const dms& a1, const dms& a2) { return a1.Degrees() == a2.Degrees(); }

#endif
