//------------------------------------------------------------------------------
// <copyright file="LqrSMACTimeRange.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>
//   Implementation of the TimeRange class, which stores a range time timeval
//   values and has methods for comparing them.
// </summary>
//
// <owner>Mike Cookson</owner>
//------------------------------------------------------------------------------

#include "LqrSMACTimeRange.h"
#include "nsextend.h"
#include <climits>
#include <sstream>
#include <boost/date_time/gregorian/gregorian.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>

using namespace std;

namespace LiquidRobotics {
namespace SMACFramework {

const timeval TimeRange::BadValue = TimeRange::TimeWithFraction(-1, 0);
const timeval TimeRange::MinValue = TimeRange::TimeWithFraction(0, 0);
const timeval TimeRange::MaxValue = TimeRange::TimeWithFraction(LONG_MAX, 999999);

TimeRange::TimeRange(bool _isGood)
: from(MinValue)
, to(MaxValue)
, isGood(_isGood)
{
}

TimeRange::TimeRange(const timeval& _tv, bool _isGood)
: from(_tv)
, to(_tv)
, isGood(_isGood)
{
}

TimeRange::TimeRange(time_t _from, time_t _to, bool _isGood)
: from(TimeWithFraction(_from, MinValue.tv_usec))
, to(TimeWithFraction(_to, MaxValue.tv_usec))
, isGood(_isGood)
{
}

TimeRange::TimeRange(const timeval& _from, const timeval& _to, bool _isGood)
: from(_from)
, to(_to)
, isGood(_isGood)
{
}

TimeRange::TimeRange(const TimeRange& rhs)
: from(rhs.from)
, to(rhs.to)
, isGood(rhs.isGood)
{
}

bool TimeRange::InRange(const timeval& tv) const
{
	return isGood && (Compare(from, tv) <= 0) && (Compare(tv, to) <= 0);
}

int TimeRange::Compare(const timeval& a, const timeval& b)
{
	int result;

	if (a.tv_sec < b.tv_sec)
		result = -1;
	else if (a.tv_sec > b.tv_sec)
		result = 1;
	else if (a.tv_usec < b.tv_usec)
		result = -1;
	else if (a.tv_usec > b.tv_usec)
		result = 1;
	else
		result = 0;

	return result;
}

TimeRange::operator bool() const
{
	return isGood;
}

timeval TimeRange::GetMin() const
{
	return from;
}

timeval TimeRange::GetMax() const
{
	return to;
}

void TimeRange::SetGood(bool newValue)
{
	isGood = newValue;
}

void TimeRange::SetMin(time_t t)
{
	from = TimeWithFraction(t, MinValue.tv_usec);
}

void TimeRange::SetMin(const timeval& tv)
{
	from = tv;
}

void TimeRange::SetMax(time_t t)
{
	to = TimeWithFraction(t, MaxValue.tv_usec);
}

void TimeRange::SetMax(const timeval& tv)
{
	to = tv;
}

void TimeRange::SetRange(const timeval& _tv, bool _isGood)
{
	from = _tv;
	to = _tv;
	isGood = _isGood;
}

void TimeRange::SetRange(time_t _from, time_t _to, bool _isGood)
{
	from = TimeWithFraction(_from, MinValue.tv_usec);
	to = TimeWithFraction(_to, MaxValue.tv_usec);
	isGood = _isGood;
}

void TimeRange::SetRange(const timeval& _from, const timeval& _to, bool _isGood)
{
	from = _from;
	to = _to;
	isGood = _isGood;
}

bool TimeRange::ValidRange() const
{
	return isGood && (Compare(from, to) <= 0);
}

bool TimeRange::ExtendRange(const timeval& tv)
{
	bool result = false;

	if (!isGood)
	{
		from = to = tv;
		isGood = true;
		result = true;
	}
	else
	{
		if (Compare(tv,from) < 0)
		{
			from = tv;
			result = true;
		}

		if (Compare(tv,to) > 0)
		{
			to = tv;
			result = true;
		}
	}

	return result;
}

string TimeRange::ToString() const
{
	stringstream msg;
	msg << ToString(from) << " - " << ToString(to);
	if (!isGood) msg << " (bad)";
	return msg.str();
}

inline int ConvertField(const string& str, size_t start, size_t len, int minVal, int maxVal, const string& fieldName)
{
	int result;
	string field = str.substr(start,len);

	try
	{
		result = boost::lexical_cast<int>(field);
	}
	catch(exception& ex)
	{
		stringstream msg;
		msg << "Caught exception while converting " << fieldName << " (" << field << ") to integer: " << ex.what();
		throw invalid_argument(msg.str());
	}

	if ((result < minVal) || (result > maxVal))
	{
		stringstream msg;
		msg << "The " << fieldName << " value (" << result << ") is out of range " << minVal << ".." << maxVal;
		throw invalid_argument(msg.str());
	}

	return result;
}

// yyyymmddHHMMSS
inline time_t DecodeTimestamp(const string& timestamp, size_t offset = 0, bool doThrow = true)
{
	time_t result = (time_t) -1;

	if (timestamp.length() >= offset+14)
	{
		try
		{
			tm t;
			t.tm_sec = ConvertField(timestamp, offset+12, 2, 0, 59, "seconds");;
			t.tm_min = ConvertField(timestamp, offset+10, 2, 0, 59, "minutes");
			t.tm_hour = ConvertField(timestamp, offset+8, 2, 0, 23, "hours");
			t.tm_mday = ConvertField(timestamp, offset+6, 2, 1, 31, "day");
			t.tm_mon = ConvertField(timestamp, offset+4, 2, 1, 12, "month")-1;
			t.tm_year = ConvertField(timestamp, offset, 4, 1970, 2038, "year")-1900;
			t.tm_wday = 0;
			t.tm_yday = 0;
			t.tm_isdst = 0;

			result = mktime(&t);

			if (result == (time_t) -1)
			{
				stringstream msg;
				msg << "One or more fields in " << timestamp << " do not describe a valid date/time in yyyymmddHHMMSS format";
				throw invalid_argument(msg.str());
			}
		}
		catch(...)
		{
			if (doThrow)
				throw;

			result = -1;
		}
	}

	return result;
}

bool TimeRange::FromString(const string& dateStr, const string& timeStr, timeval& tv, bool doThrow)
{
	using namespace boost::posix_time;
	using namespace boost::gregorian;

	bool result;

	try
	{
		date d(from_string(dateStr));
		time_duration t;

		if ((timeStr.length() >= 6)
		 && isdigit(timeStr[0])
		 && isdigit(timeStr[1])
		 && isdigit(timeStr[2])
		 && isdigit(timeStr[3])
		 && isdigit(timeStr[4])
		 && isdigit(timeStr[5]))
		{
			stringstream tmp;
			tmp << timeStr.substr(0,2) << ":" << timeStr.substr(2,2) << ":" << timeStr.substr(4);

			t = duration_from_string(tmp.str());
		}
		else
			t = duration_from_string(timeStr);

		const date firstDate(1970,Jan,1);
		const long secsPerDay = 24*60*60;

		if (d < firstDate)
			throw invalid_argument("Dates before 1970 are not supported");
		else if (d >= date(2038,Jan,1))
			throw invalid_argument("Dates after 2037 are not supported");
		else if (t.is_special())
			throw invalid_argument("Special time values are not supported");
		else if (t.total_seconds() < 0)
			throw invalid_argument("Negative times of day are not supported");
		else if (t.total_seconds() >= secsPerDay)
			throw invalid_argument("Times of day over 24 hours are not supported");

		tv.tv_sec = secsPerDay*(d.julian_day()-firstDate.julian_day())+t.total_seconds();

		if (t.fractional_seconds())
			tv.tv_usec = (suseconds_t) floor(t.fractional_seconds()*pow(10,6-t.num_fractional_digits()));
		else
			tv.tv_usec = 0;

		result = true;
	}
	catch(...)
	{
		tv.tv_sec = 0;
		tv.tv_usec = 0;
		result = false;
		if (doThrow) throw;
	}

	return result;
}

string TimeRange::ToString(const timeval& tv)
{
	return GetTimevalStrings(tv).ToString();
}

timeval TimeRange::TimeWithFraction(time_t sec, __suseconds_t usec)
{
	timeval tv;
	tv.tv_sec = sec;
	tv.tv_usec = usec;
	return tv;
}

TimeRange::GetTimevalStrings::GetTimevalStrings(const timeval& tv)
{
	tm _tm = *gmtime(&tv.tv_sec);

	stringstream stm;
	stm << utos(_tm.tm_year+1900,10,4) << "/" << utos(_tm.tm_mon+1,10,2) << "/"  << utos(_tm.tm_mday,10,2);
	dateStr = stm.str();

	stm.str("");
	stm << utos(_tm.tm_year+1900,10,4) << utos(_tm.tm_mon+1,10,2) << utos(_tm.tm_mday,10,2);
	compressedDateStr = stm.str();

	stm.str("");
	stm << utos(_tm.tm_hour,10,2) << ":" << utos(_tm.tm_min,10,2) << ":" << utos(_tm.tm_sec,10,2);
	timeStr = stm.str();

	stm.str("");
	stm << utos(_tm.tm_hour,10,2) << utos(_tm.tm_min,10,2) << utos(_tm.tm_sec,10,2);
	compressedTimeStr = stm.str();

	suseconds_t usec = tv.tv_usec;

	if (usec < 0)
		usec = 0;
	else if ((usec = (usec+500)/1000) >= 1000)
		usec = 999;

	msStr = utos(usec,10,3);
}

string TimeRange::GetTimevalStrings::ToString(bool compressed) const
{
	stringstream msg;

	if (compressed)
		msg << compressedDateStr << compressedTimeStr << msStr;
	else
		msg << dateStr << " " << timeStr << "." << msStr;

	return msg.str();
}

bool TimeRange::Intersects(const TimeRange& other) const
{
	bool result = other.InRange(from);

	if (!result)	result = other.InRange(to);
	if (!result)	result = InRange(other.from);
	if (!result)	result = InRange(other.to);

	return result;
}

}
}
