//--------------------------------------------------------------------------------
// <copyright file="LqrSMACLatLon_Component.cpp" 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>
//   Declarations for the LatLon_Component class, which encapsulates a latitude or
//   a longitude.
// </summary>
//
// <owner>Mike Cookson</owner>
//--------------------------------------------------------------------------------

#include <sstream>
#include <stdexcept>
#include <math.h>
#include "LqrSMACLatLon.h"
#include "LqrSMACPrintfBuffer.h"
#include "nsextend.h"

using namespace std;

namespace LiquidRobotics {
namespace SMACFramework {

static const string invalidFixMessage("Fix is invalid");

LatLon::Component::Component(bool latitude, bool locked)
: isValid(false)
, isLatitude(latitude)
, lockedHemisphere(locked)
, hemisphere(LatLon::NORTH)
, degrees(0)
, minutes(0)
, seconds(0)
, numeric(0)
{
}

LatLon::Component::Component(FCCNumeric _numeric, bool latitude, bool locked)
: isValid(false)
, isLatitude(latitude)
, lockedHemisphere(locked)
, hemisphere(LatLon::NORTH)
, degrees(0)
, minutes(0)
, seconds(0)
, numeric(_numeric)
{
	ComputeComponents(numeric, latitude, degrees, minutes, seconds, hemisphere);
	isValid = true;
}

LatLon::Component::Component(unsigned _degrees, unsigned _minutes, float _seconds, Hemisphere _hemisphere, bool locked)
: isValid(false)
, isLatitude((_hemisphere == LatLon::NORTH) || (_hemisphere == LatLon::SOUTH))
, lockedHemisphere(locked)
, hemisphere(_hemisphere)
, degrees(_degrees)
, minutes(_minutes)
, seconds(_seconds)
, numeric(0)
{
	ComputeNumeric(degrees, minutes, seconds, hemisphere, numeric);
	isValid = true;
}

LatLon::Component::Component(const Component& right, bool locked)
: isValid(right.isValid)
, isLatitude(right.isLatitude)
, lockedHemisphere(locked)
, hemisphere(right.hemisphere)
, degrees(right.degrees)
, minutes(right.minutes)
, seconds(right.seconds)
, numeric(right.numeric)
{
}

LatLon::Component::~Component()
{
}

LatLon::Component& LatLon::Component::operator=(const Component& right)
{
	if (this != &right)
	{
		isValid = right.isValid;
		isLatitude = right.isLatitude;
		lockedHemisphere = right.lockedHemisphere;
		hemisphere = right.hemisphere;
		degrees = right.degrees;
		minutes = right.minutes;
		seconds = right.seconds;
		numeric = right.numeric;
	}

	return *this;
}

bool LatLon::Component::IsValid() const
{
	return isValid;
}

void LatLon::Component::Invalidate()
{
	isValid = false;
}

bool LatLon::Component::IsLatitude() const
{
	return isLatitude;
}

bool LatLon::Component::IsLongitude() const
{
	return !isLatitude;
}

LatLon::Hemisphere LatLon::Component::GetHemisphere() const
{
	if (!IsValid()) throw InvalidFix(invalidFixMessage);
	return hemisphere;
}

void LatLon::Component::SetHemisphere(Hemisphere newValue)
{
	bool newIsLatitude = (hemisphere == LatLon::NORTH) || (hemisphere == LatLon::SOUTH);

	if (lockedHemisphere && (newIsLatitude != isLatitude))
		throw invalid_argument(string("Cannot change locked component type"));

	if (hemisphere != newValue)
	{
		isValid = false;
		isLatitude = newIsLatitude;
		hemisphere = newValue;
		ComputeNumeric(degrees, minutes, seconds, hemisphere, numeric);
		isValid = true;
	}
}

LatLon::FCCNumeric LatLon::Component::GetNumeric() const
{
	if (!IsValid()) throw InvalidFix(invalidFixMessage);
	return numeric;
}

void LatLon::Component::SetNumeric(FCCNumeric newValue)
{
	if (numeric != newValue)
	{
		isValid = false;
		numeric = newValue;
		ComputeComponents(numeric, isLatitude, degrees, minutes, seconds, hemisphere);
		isValid = true;
	}
}

unsigned LatLon::Component::GetDegrees() const
{
	if (!IsValid()) throw InvalidFix(invalidFixMessage);
	return degrees;
}

void LatLon::Component::SetDegrees(unsigned newValue)
{
	if (degrees != newValue)
	{
		isValid = false;
		degrees = newValue;
		ComputeNumeric(degrees, minutes, seconds, hemisphere, numeric);
		isValid = true;
	}
}

unsigned LatLon::Component::GetMinutes() const
{
	if (!IsValid()) throw InvalidFix(invalidFixMessage);
	return minutes;
}

void LatLon::Component::SetMinutes(unsigned newValue)
{
	if (minutes != newValue)
	{
		isValid = false;
		minutes = newValue;
		ComputeNumeric(degrees, minutes, seconds, hemisphere, numeric);
		isValid = true;
	}
}

float LatLon::Component::GetMinutesAsFloat() const
{
	unsigned minU = GetMinutes();
	float secF = GetSeconds();

	float minF = ((float) minU) + secF/60.0f;
	return minF;
}

void LatLon::Component::SetMinutesAsFloat(float newValue)
{
	unsigned minU = (unsigned) floor(newValue);
	float secF = 60.0f*(newValue-minU);

	isValid = false;
	minutes = minU;
	seconds = secF;
	ComputeNumeric(degrees, minutes, seconds, hemisphere, numeric);
	isValid = true;
}

float LatLon::Component::GetSeconds() const
{
	if (!IsValid()) throw InvalidFix(invalidFixMessage);
	return seconds;
}

void LatLon::Component::SetSeconds(float newValue)
{
	if (seconds != newValue)
	{
		isValid = false;
		seconds = newValue;
		ComputeNumeric(degrees, minutes, seconds, hemisphere, numeric);
		isValid = true;
	}
}

double LatLon::Component::GetAsDouble() const
{
	if (!IsValid()) throw InvalidFix(invalidFixMessage);

	const double retVal = degrees + minutes/60.0 + seconds/3600.0;
	return ((hemisphere == WEST) || (hemisphere == SOUTH)) ? -retVal : retVal;
}

void LatLon::Component::SetAsDouble(double newValue)
{
	isValid = false;

	if (isLatitude)
		hemisphere = (newValue >= 0) ? NORTH : SOUTH;
	else
		hemisphere = (newValue >= 0) ? EAST : WEST;

	newValue = fabs(newValue);
	degrees = (unsigned) floor(newValue);
	newValue = (newValue-degrees)*60.0;
	minutes = (unsigned) floor(newValue);
	seconds = (float) ((newValue-minutes)*60.0);
	ComputeNumeric(degrees, minutes, seconds, hemisphere, numeric);
	isValid = true;
}

string LatLon::Component::ToString() const
{
	stringstream stm;

	stm << *this;
	return stm.str();
}

SHARED_LIB_PUBLIC std::ostream& operator<<(std::ostream& out, const LatLon::Component& comp)
{
	if (comp.IsValid())
	{
		string hemisStr;

		switch(comp.GetHemisphere())
		{
		case LatLon::NORTH:		hemisStr = "N";		break;
		case LatLon::SOUTH:		hemisStr = "S";		break;
		case LatLon::EAST:		hemisStr = "E";		break;
		case LatLon::WEST:		hemisStr = "W";		break;

		default:
			{
				stringstream stm;
				stm << comp.GetHemisphere() << "?";
				hemisStr = stm.str();
				break;
			}
		}

		out << PrintfBuffer("%.3u %07.4f %s", comp.GetDegrees(), comp.GetMinutesAsFloat(), hemisStr.c_str());
	}
	else
		out << "*invalid*";

	return out;
}

void LatLon::Component::ComputeComponents(FCCNumeric numeric, bool islatitude, unsigned& degrees, unsigned& minutes, float& seconds, Hemisphere& hemisphere)
{
	if (islatitude)
	{
		if (numeric < 0)
		{
			numeric = -numeric;
			hemisphere = LatLon::SOUTH;
		}
		else
			hemisphere = LatLon::NORTH;
	}
	else
	{
		if (numeric < 0)
		{
			numeric = -numeric;
			hemisphere = LatLon::WEST;
		}
		else
			hemisphere = LatLon::EAST;
	}

	degrees = (unsigned) (numeric / 600000L);
	numeric %= 600000L;
	minutes = (unsigned) (numeric / 10000L);

	unsigned secint = (unsigned) (numeric % 10000L);

	seconds = secint / (10000.0f / 60.0f);

	switch(hemisphere)
	{
	case LatLon::NORTH:
	case LatLon::SOUTH:

		if ((degrees > 90) || ((degrees == 90) && (minutes || seconds)))
			throw invalid_argument("degrees, minutes and seconds cannot exceed 90 for longitude");

		break;

	case LatLon::EAST:
	case LatLon::WEST:

		if ((degrees > 180) || ((degrees == 180) && (minutes || seconds)))
			throw invalid_argument("degrees, minutes and seconds cannot exceed 180 for longitude");

		break;
	}

	if (minutes > 59)		throw invalid_argument("minutes cannot exceed 59");
	if (seconds > 9999)		throw invalid_argument("seconds cannot exceed 9999");
}

void LatLon::Component::ComputeNumeric(unsigned degrees, unsigned minutes, float seconds, Hemisphere hemisphere, FCCNumeric& numeric)
{
	if (minutes > 59)		throw invalid_argument("minutes cannot exceed 59");

	if ((seconds < 0) || (seconds >= 60.0f))
		throw invalid_argument("seconds: 0 <= seconds < 60");

	switch(hemisphere)
	{
	case LatLon::NORTH:
	case LatLon::SOUTH:

		if ((degrees > 90) || ((degrees == 90) && (minutes || seconds)))
			throw invalid_argument("degrees, minutes and seconds cannot exceed 90 for longitude");

		break;

	case LatLon::EAST:
	case LatLon::WEST:

		if ((degrees > 180) || ((degrees == 180) && (minutes || seconds)))
			throw invalid_argument("degrees, minutes and seconds cannot exceed 180 for longitude");

		break;

	default:

		throw invalid_argument(string("Invalid value for hemisphere"));
	}

	numeric = ((FCCNumeric) 600000L*degrees) + ((FCCNumeric) 10000L*minutes) + ((FCCNumeric) floor(round((seconds * (10000.0f / 60.0f)))));

	switch(hemisphere)
	{
	case LatLon::SOUTH:
	case LatLon::WEST:

		numeric = -numeric;
		break;

	default:

		break;
	}
}

}
}
