//------------------------------------------------------------------------------
// <copyright file="LqrSMACSubseaStation.h" company="LiquidRobotics">
//
// Copyright (c) 2014 Liquid Robotics, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// This software is associated with a release.  Liquid Robotics makes no
// representation that the design, interfaces or component distribution will
// remain unchanged in future releases.
//
// </copyright>
//
// <summary>
//   Declaration of SubseaStation class, a serializable entity that holds the
//   attributes of a subsea asset.
// </summary>
//
// <owner>Mike Cookson</owner>
//------------------------------------------------------------------------------

#ifndef __LqrSMACSubseaStation_H__
#define __LqrSMACSubseaStation_H__

#include "LqrSMACGeodetic.h"
#include "LqrSMACSharedLib.h"
#include <iostream>
#include <map>
#include <string>
#include <boost/serialization/access.hpp>
#include <boost/serialization/nvp.hpp>
#include <boost/serialization/version.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/serialization/map.hpp>

namespace LiquidRobotics {
namespace SMACFramework {

///
/// SubseaStation is a Boost serializable entity that holds the attributes of a subsea asset.
/// This class was developed for use with acoustic gateway applications, but it is likely to
/// be useful for other applications.  It can be used stand-alone or can be included as part
/// of a SubseaStationMap.
///
class SHARED_LIB_PUBLIC SubseaStation
{
public:

	typedef boost::shared_ptr<SubseaStation> Ptr;
	typedef std::string string;
	typedef std::map<string,string> StringMap;

	SubseaStation();

	/// Class constructor with required parameters
	SubseaStation(
			const string& _name,			/**< [in] name of the station */
			double _latitude,				/**< [in] location of the station (latitude) */
			double _longitude				/**< [in] location of the station (longitude) */
			);

	/// Class constructor with a string list of keys and values like "name=Station1 latitude=37.1234 longitude=-121.5678 depth=100.1"
	SubseaStation(
			const string& settings			/**< [in] string with keys and values as a list of equations, throws if invalid construction */
			);

	/// Class constructor that copies another station instance
	SubseaStation(
			const SubseaStation& rhs		/**< [in] station instance to be copied */
			);

	/// Assignment operator for SubseaStation
	SubseaStation& operator=(const SubseaStation& rhs);

	/// Checks the station angular limit, slant range, watch circle and depth against given location at the surface
	bool CheckLimits(
			const Geodetic::Location& currentLocation,	/**< [in] latitude and longitude of the current location (at the surface) */
			bool throwOnError = true		/**< [in] true=throw an exception if limits check failed (default), false=just return true if OK or false if failed */
			) const;

//	/// Gets the range from a surface position to this station.
//	void SubseaStation::GetRangeBearing (
//			const Geodetic::Location& currentLocation,	/**< [in] latitude and longitude of the current location (at the surface) */
//			double& range,
//			double& bearing
//			) const;

	/// Clears the angular limit setting (if any)
	void ClearAngularLimit();

	/// Clears the optional attribute with the given name (if any)
	void ClearAttribute(
			const string& attributeName		/**< [in] the name of the attribute to clear */
			);

	/// Clears all optional attributes
	void ClearAttributes();

	/// Clears the depth setting (if any)
	void ClearDepth();

	/// Clears the slant range limit (if any)
	void ClearSlantRangeLimit();

	/// Clears the watch circle limit (if any)
	void ClearWatchCircle();

	/// Computes the angle (degrees) between the subsea modem and the current modem given a ping distance and the correct depth configured (throws if no depth configured)
	double ComputeAngleFromSlantDistance(
			double slantDistance	/**< [in] measured diagonal distance to subsea modem (meters) */
			) const;

	/// Computes the angle (degrees) between the subsea modem and the current modem given a measurement of the surface distance; for example, this might be done using GPS (throws if no depth configured)
	double ComputeAngleFromSurfaceDistance(
			double surfaceDistance	/**< [in] measured distance to subsea modem at the surface (meters) */
			) const;

	/// Computes the watch circle using slant range, angular limit and depth (does not take the configured watch circle into account.)  Returns zero if no depth or no limits configured
	double ComputeWatchCircle() const;

	/// Returns the angular limit: the maximum angle between the station and the current position (degrees); throws if no angular limit set.
	double GetAngularLimit() const;

	/// Retrieves the angular limit and stores it in "output" The angular limit is maximum angle between the station and the current position (degrees)  No value is set in "output" and the function returns false if none was configured; returns true if the value was retrieved.
	bool GetAngularLimit(
			double& output			/**< [out] the angular limit (degrees, only set if there is a configured angular limit) */
			) const;

	/// Retrieves the attribute with the given name; throws if there is no attribute with the given name.
	string GetAttribute(
			const string& attributeName		/**< [in] the name of the attribute to return */
			) const;

	/// Retrieves the attribute with the given name and stores it in "output"  Returns true if there is an attribute match or false if there is no match.
	bool GetAttribute(
			const string& attributeName,	/**< [in] the name of the attribute to return */
			string& output					/**< [out] receives the attribute value if there is a match */
			) const;

	/// Returns the map of optional attributes
	const StringMap& GetAttributesMap() const;

	/// Returns the configured depth or throws an exception if no depth was configured
	double GetDepth() const;

	/// If there is a configure depth it is returned in "output and returns true; otherwise, returns false.
	bool GetDepth(
			double& output					/**< [out] receives the depth value (meters) if it is configured */
			) const;

	/// Returns the configured latitude
	double GetLatitude() const;

	/// Returns the configured latitude and longitude as a geodetic location value
	Geodetic::Location GetLocation() const;

	/// Returns the configured longitude
	double GetLongitude() const;

	/// Returns the configured name
	const string& GetName() const;

	/// Returns the configured slant range limit (meters) or throws if none is configured
	double GetSlantRangeLimit() const;

	/// If there is a configured slant range (meters), it is returned in "output" and the method returns true; otherwise, returns false
	bool GetSlantRangeLimit(
			double& output					/**< [out] receives the slant range limit (meters) if it is configured */
			) const;

	/// Returns the configured watch circle (radius in meters) or throws if none is configured
	double GetWatchCircle() const;

	/// If there is a configured watch circle (radius in meters), it is returned in "output" and the method returns true; otherwise, returns false
	bool GetWatchCircle(
			double& output					//*< [out] receives the watch circle (radius in meters) if it is configured
			) const;

	/// Returns true if an angular limit is configured or false if it is not
	bool HasAngularLimit() const;

	/// Returns true if there is a configured attribute of the given name or false if there is not
	bool HasAttribute(const string& attributeName) const;

	/// Returns true if there is a configured depth or false if there is not
	bool HasDepth() const;

	/// Returns true if there is a configured slant range limit or false if there is not
	bool HasSlantRangeLimit() const;

	/// Returns true if there is a configured watch circle or false if there is not
	bool HasWatchCircle() const;

	/// Updates the station parameters using a list of key/value pairs in "settings", optionally prevents the name to be set
	void Set(
			const string& settings,			//*< [in] a set of key/value pairs as equations
			bool allowNameSet = true		//*< [in] true if it's OK to set the name; false if doing so will throw an exception
			);

	/// Sets a new value for the angular limit (degrees)
	void SetAngularLimit(
			double newValue					//*< [in] the new angular limit value (degrees)
			);

	/// Sets an optional attribute
	void SetAttribute(
			const string& attributeName,	//*< [in] the name of an optional parameter to set
			const string& newValue			//*< [in] the value for the optional parameter
			);

	/// Sets a depth (meters)
	void SetDepth(
			double newValue					//*< [in] the new depth value (meters)
			);

	/// Sets a new value for the station latitude
	void SetLatitude(
			double newValue					//*< [in] the new latitude value (degrees)
			);

	/// Sets a latitude/longitude value for the station
	void SetLocation(
			const Geodetic::Location& newValue	//*< [in] the new value for latitude and longitude as a geodetic location
			);

	/// Sets a new value for the station longitude
	void SetLongitude(
			double newValue					//*< [in] the new longitude value (degrees)
			);

	/// Sets a new value for the station name
	void SetName(
			const string& newValue			//*< [in] the new station name
			);

	/// Sets a new value for the slant range limit (meters)
	void SetSlantRangeLimit(
			double newValue					//*< [in] new slant range limit (meters)
			);

	/// Sets a new value for the watch circle (radius in meters)
	void SetWatchCircle(
			double newValue					//*< [in] new watch circle (meters)
			);

	/// Returns the station configuration as a string using the stream operator
	string ToString() const;

	/// Run various instantiation tests
	static void RunTests();

private:

//	SubseaStation();

	friend class boost::serialization::access;

	static std::string DoubleToStr(const double d);
	static double StrToDouble(const string& str);

	template<class Archive>
	static void save(Archive& ar, const boost::shared_ptr<double>& ptr, const char* valName)
	{
	    using namespace std;
	    using namespace boost::serialization;
	    string str;

	    if (ptr) str = DoubleToStr(*ptr);
	    ar & make_nvp(valName, str);
	}

	template<class Archive>
	static void load(Archive& ar, boost::shared_ptr<double>& ptr, const char* valName)
	{
	    using namespace std;
	    using namespace boost::serialization;
	    string temp;

	    ar & make_nvp(valName, temp);

	    if (temp.length())
	    {
			try
			{
				double d = StrToDouble(temp);
				ptr.reset(new double(d));
			}
			catch(...)
			{
				ptr.reset();
			}
	    }
	    else
	    	ptr.reset();
	}

	template<class Archive>
	void save(Archive& ar, const unsigned int version) const
	{
		ar & BOOST_SERIALIZATION_NVP(name);					// version 1
		ar & BOOST_SERIALIZATION_NVP(latitude);				// version 1
		ar & BOOST_SERIALIZATION_NVP(longitude);			// version 1
		save(ar, depth, "depth");							// version 1
		save(ar, angularLimit, "angularLimit");				// version 1
		save(ar, slantRangeLimit, "slantRangeLimit");		// version 1
		save(ar, watchCircle, "watchCircle");				// version 1
		ar & BOOST_SERIALIZATION_NVP(attributes);			// version 1
	}

	template<class Archive>
	void load(Archive& ar, const unsigned int version)
	{
		ar & BOOST_SERIALIZATION_NVP(name);					// version 1
		ar & BOOST_SERIALIZATION_NVP(latitude);				// version 1
		ar & BOOST_SERIALIZATION_NVP(longitude);			// version 1
		load(ar, depth, "depth");							// version 1
		load(ar, angularLimit, "angularLimit");				// version 1
		load(ar, slantRangeLimit, "slantRangeLimit");		// version 1
		load(ar, watchCircle, "watchCircle");				// version 1
		ar & BOOST_SERIALIZATION_NVP(attributes);			// version 1

		ValidateAll();
	}

	BOOST_SERIALIZATION_SPLIT_MEMBER()

	void ValidateAll();

	string name;									///< common name of the station instance (also used by SubseaStationMap as the key)
	double latitude;								///< latitude of the station
	double longitude;								///< longitude of the station
	boost::shared_ptr<double> depth;				///< depth of the station in meters (nil=none given)
	boost::shared_ptr<double> angularLimit;			///< angular limit (degrees) of the station (nil=none given)
	boost::shared_ptr<double> slantRangeLimit;		///< maximum slant range (meters) to allow operation (nil=none given)
	boost::shared_ptr<double> watchCircle;			///< maximum surface range to allow operation (radius in meters, nil=none given)
	StringMap attributes;							///< miscellaneous attributes; name values must not clash with standard attribute names; indexes are all uppercase
};

}
}

/// Stream operator for SubseaStation
std::ostream& operator<<(std::ostream& stm, const LiquidRobotics::SMACFramework::SubseaStation& station);

BOOST_CLASS_VERSION(LiquidRobotics::SMACFramework::SubseaStation, 1)

#endif

