//------------------------------------------------------------------------------
// <copyright file="LqrSMACCSVSampleFileSet.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>
//   Declaration of CSVSampleFileSet class, which encapsulates operations of a
//   sensor data logger.
// </summary>
//
// <owner>Mike Cookson</owner>
//------------------------------------------------------------------------------

#include "LqrSMACCSVSampleFileSet.h"
#include "LqrSMACByteStream.h"
#include "LqrSMACTempFile.h"
#include "nsextend.h"
#include <stdlib.h>
#include <sstream>
#include <sys/types.h>
#include <sys/wait.h>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
#include <boost/date_time/gregorian/gregorian.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>

using namespace std;
using namespace LiquidRobotics::SMACFramework;

//const string recordTypeHeader = "RecType";
const string dateHeader = "Date";
const string timeHeader = "Time";
const string millisecondsHeader = "Millisec";
const string latitudeHeader = "Latitude";
const string longitudeHeader = "Longitude";


struct CommonFileUtilities
{
	static string DecompressFile(const string& maybeCompressedFile, const string& alternateFileName, bool throwOnFailure = true)
	{
		string result;

		if (maybeCompressedFile.length() && alternateFileName.length())
		{
			stringstream cmd;
			cmd << "file -b " << QuotedFileName(maybeCompressedFile);
			string classifyCmd = GetCmdOutput(cmd.str(), throwOnFailure);

			if (classifyCmd.find("bzip2") != string::npos)
			{
				cmd.str("");
				cmd << "bzcat " << QuotedFileName(maybeCompressedFile) << " >" << QuotedFileName(alternateFileName);
				if (RunCommand(cmd.str(), throwOnFailure))
					result = alternateFileName;
			}
			else if (classifyCmd.find("gzip") != string::npos)
			{
				cmd.str("");
				cmd << "gzcat " << QuotedFileName(maybeCompressedFile) << " >" << QuotedFileName(alternateFileName);
				if (RunCommand(cmd.str(), throwOnFailure))
					result = alternateFileName;
			}
			else
				result = maybeCompressedFile;
		}

		return result;
	}

	static string GetCmdOutput(const string& cmd, bool throwOnError = true)
	{
		FILE* fp = popen(cmd.c_str(), "r");
		string result;

		if (fp)
		{
			stringstream buffer;
			char c;

			while(c = fgetc(fp), !feof(fp) && !ferror(fp))
				buffer.put(c);

			pclose(fp);
			result = buffer.str();
		}
		else if (throwOnError)
		{
			stringstream msg;
			msg << "Failed popen() for command: " << cmd.c_str();
			throw runtime_error(msg.str());
		}

		return result;
	}

	static bool MakeFileCopy(const string& oldName, const string& newName, bool throwOnFailure = true)
	{
		stringstream cmd;
		cmd << "cp " << QuotedFileName(oldName) << " " << QuotedFileName(newName);
		return RunCommand(cmd.str(), throwOnFailure);
	}

	static string QuotedFileName(const string& fileName)
	{
		stringstream fnstm;
		bool quoted = false;
		const char quoteChar = '\'';
		const char altQuoteChar = '\"';

		for (string::const_iterator it = fileName.begin(); it != fileName.end(); ++it)
		{
			const bool isQuote = (*it == quoteChar);

			if (isQuote == quoted)
			{
				fnstm << quoteChar;
				quoted = !quoted;
			}

			if (isQuote)
				fnstm << altQuoteChar << quoteChar << altQuoteChar;
			else
				fnstm << *it;
		}

		if (quoted)
			fnstm << quoteChar;

		string result = fnstm.str();
		return result;
	}

	static bool RunCommand(const string& cmd, bool throwOnFailure = true)
	{
		const int status = system(cmd.c_str());
		bool result;

		if (WIFEXITED(status))
		{
			const int exitStatus = WEXITSTATUS(status);

			result = !exitStatus;

			if (!result && throwOnFailure)
			{
				stringstream msg;
				msg << "system() failed with exit status " << exitStatus << " for command: " << cmd;
				throw runtime_error(msg.str());
			}
		}
		else if (throwOnFailure)
		{
			stringstream msg;
			msg << "system() exited abnormally for command: " << cmd;
			throw runtime_error(msg.str());
		}
		else
			result = false;

		return result;
	}
};

struct TempCopiedFile : private CommonFileUtilities
{
public:

	typedef boost::shared_ptr<TempCopiedFile> Ptr;

	TempCopiedFile(const string& fileName)
	: originalFileName(fileName)
	{
		tempFileName = tempFile.GetName();
	}

	bool Initialize(bool throwOnFailure = true)
	{
		bool result;

		if (!tempFile.IsOpen())
		{
			result = false;
			if (throwOnFailure)
				throw runtime_error("Failed to create temporary file");
		}
		else if (tempFile.Close(),
				 !MakeFileCopy(originalFileName, tempFileName, false))
		{
			result = false;
			if (throwOnFailure)
			{
				stringstream msg;
				msg << "Failed to copy original file name (" << originalFileName << ") to temp file (" << tempFileName << ")";
				throw runtime_error(msg.str());
			}
		}
		else
			result = true;

		return result;
	}

	const string originalFileName;
	TempFile tempFile;
	string tempFileName;
};

struct DecompressedFile : private CommonFileUtilities
{
	typedef boost::shared_ptr<DecompressedFile> Ptr;

	DecompressedFile(const string& fileName)
	: originalFileName(fileName)
	{
		tempFileName = tempFile.GetName();
	}

	bool Initialize(bool throwOnFailure = true)
	{
		bool result;

		if (!tempFile.IsOpen())
		{
			result = false;
			if (throwOnFailure)
				throw runtime_error("Failed to create temporary file");
		}
		else
		{
			tempFile.Close();

			try
			{
				plainTextName = DecompressFile(originalFileName, tempFileName, true);
				result = true;
			}
			catch(exception& ex)
			{
				if (throwOnFailure)
					throw;

				result = false;
			}
		}

		return result;
	}

	const string originalFileName;
	TempFile tempFile;
	string tempFileName;
	string plainTextName;
};

struct PlainTextFile : public TempCopiedFile, public DecompressedFile
{
public:

	PlainTextFile(const string& fileName)
	: TempCopiedFile(fileName)
	, DecompressedFile(TempCopiedFile::tempFileName)
	{
	}

	bool Initialize(bool throwOnFailure = true)
	{
		bool result = TempCopiedFile::Initialize(throwOnFailure) && DecompressedFile::Initialize(throwOnFailure);

		if (result)
		{
			stm.open(plainTextName.c_str(), ios_base::in | ios_base::binary);
			result = stm.is_open();

			if (!result && throwOnFailure)
			{
				stringstream msg;
				msg << "Failed to open plain text file (" << plainTextName << ")";
				throw runtime_error(msg.str());
			}
		}

		return result;
	}

	fstream stm;
};

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)
	{
		using namespace boost::gregorian;
		using namespace boost::posix_time;

		stringstream stm;
		stm << timestamp.substr(offset,4) << "-" << timestamp.substr(offset+4,2) << "-" << timestamp.substr(offset+6,2) << " "
			<< timestamp.substr(offset+8,2) << ":" << timestamp.substr(offset+10,2) << ":" << timestamp.substr(offset+12,2);
		ptime pt;

		try
		{
			pt = time_from_string(stm.str());

			const ptime startOfEpoch(date(1970,Jan,1), time_duration(0,0,0));
			const ptime endOfEpoch(date(2038,Jan,1), time_duration(0,0,0));

			if ((pt < startOfEpoch) || (pt > endOfEpoch))
				throw invalid_argument("Time is out of epoch range");

			time_duration sinceStartOfEpoch = pt-startOfEpoch;
			result = (time_t) sinceStartOfEpoch.total_seconds();
		}
		catch(...)
		{
			result = (time_t) -1;
		}
	}

	return result;
}



namespace LiquidRobotics {
namespace SMACFramework {

CSVSampleFileSet::CSVSampleFileSet(const FileSetSettings& _settings)
: fileSetSettings(_settings)
{
}

CSVSampleFileSet::~CSVSampleFileSet()
{
	if (fileSet)
	{
		fileSet->Shutdown();
		fileSet.reset();
	}
}

void CSVSampleFileSet::Initialize()
{
	fileSet.reset(new FileSet(fileSetSettings));

	if (!fileVolumeCompleteSubscription)
	{
		try
		{
			fileVolumeCompleteSubscription = fileSet->SubscribeFileVolumeCompleteSignal(boost::bind(&CSVSampleFileSet::OnFileVolumeComplete, this, _1, _2, _3));
			newFileSubscription = fileSet->SubscribeNewFileSignal(boost::bind(&CSVSampleFileSet::OnNewFile, this, _1, _2, _3));
			fileSet->Init();
		}
		catch(...)
		{
			ILockableConcept::Lock lock(fileSet->GetFiringMutex());
			fileVolumeCompleteSubscription.reset();
			newFileSubscription.reset();
			lock.unlock();
			fileSet.reset();
		}
	}
}

void CSVSampleFileSet::Shutdown()
{
	if (fileSet)
	{
		fileSet->Shutdown();
		fileSet.reset();
	}
}

void CSVSampleFileSet::FilterSamples(const string& recordType, const TimeRange& timeRange, std::ostream& stm)
{
	RecordTypeSet recordTypeSet;

	if (recordType.length())
		recordTypeSet.insert(recordType);

	ColumnHeaderSet columnHeaders;
	FilterSamples(columnHeaders, recordTypeSet, timeRange, stm);
}

void CSVSampleFileSet::FilterSamples(const ColumnHeaderSet& columnHeaders, const RecordTypeSet& recordTypes, const TimeRange& timeRange, std::ostream& stm)
{
#if BOOST_FILESYSTEM_VERSION < 3
	using namespace boost::filesystem3;
#else
	using namespace boost::filesystem;
#endif

	// step #1 is to copy all of the files that are named like they intersect the desired range
	// into a temporary directory
	typedef std::deque<TempCopiedFile::Ptr> CopiedFileQueue;
	CopiedFileQueue copiedFiles;
	FileSet::FileEntryListPtr allFiles = fileSet->GetFileList();

	for (FileSet::FileEntryList::const_iterator ci = allFiles->begin(); ci != allFiles->end(); ++ci)
	{
		FileSet::FileEntryPtr fileEntry = *ci;
		TimeRange fileRange = GetTimeRangeFromFileName(fileEntry->fileName);

		if (fileRange && timeRange.Intersects(fileRange))
		{
			path fullPath(fileEntry->filePath);
			fullPath /= fileEntry->fileName;

			TempCopiedFile::Ptr copiedFile;
			copiedFile.reset(new TempCopiedFile(fullPath.string()));

			if (copiedFile->Initialize(false))
				copiedFiles.push_back(copiedFile);
		}
	}

	// also make a copy of the work file so that it behaves normally while
	// it's being searched
	const string workFile = fileSet->GetWorkfilePath();
	TempCopiedFile::Ptr copiedWorkFile;
	copiedWorkFile.reset(new TempCopiedFile(workFile));

	if (copiedWorkFile->Initialize(false))
		copiedFiles.push_back(copiedWorkFile);

	// step #2 is to go through each file and perhaps decompress it; once
	// decompressed it can be searched for matching entries
	bool withHeaders = true;

	while(!copiedFiles.empty())
	{
		TempCopiedFile::Ptr copiedFile = copiedFiles.front();
		copiedFiles.pop_front();

		DecompressedFile decompressedFile(copiedFile->tempFileName);

		if (decompressedFile.Initialize(false))
			if (FilterSamplesPlainTextFile(columnHeaders, recordTypes, timeRange, decompressedFile.plainTextName, stm, withHeaders))
				withHeaders = false;
	}
}

bool CSVSampleFileSet::FilterSamplesPlainTextFile(const ColumnHeaderSet& columnHeaders, const RecordTypeSet& recordTypes,
		const TimeRange& timeRange, const string& fileName, std::ostream& stm, bool withHeaders) const
{
	bool samplesWritten = false;
	stringstream headersStm;
	fstream fstm(fileName.c_str(), ios_base::in | ios_base::binary);

	if (fstm.good())
	{
		// reads out the heading lines for each record type at the start of the file
		// and saves them in the data structure
		FileRecordTypeToInfoMap recordInfo;
		recordInfo.GetFromStream(fstm);

		typedef std::vector<size_t> ActiveColumns;
		typedef boost::shared_ptr<ActiveColumns> ActiveColumnsPtr;
		typedef std::map<string,ActiveColumnsPtr> RecordTypeToActiveColumnsMap;

		RecordTypeToActiveColumnsMap recordTypeToActiveColumns;

		// outputting column headers according to specifications
		for (FileRecordTypeToInfoMap::const_iterator frit = recordInfo.begin(); frit != recordInfo.end(); ++frit)
		{
			ActiveColumnsPtr activeColumns;
			activeColumns.reset(new ActiveColumns);

			if (!recordTypes.size() || (recordTypes.find(frit->first) != recordTypes.end()))
			{
				const FileRecordTypeInfo& typeInfo = *frit->second;
				CSVSample sample;

				sample += frit->first;

				for (StringVector::const_iterator svit = typeInfo.columnHeadersOrdered.begin(); svit != typeInfo.columnHeadersOrdered.end(); ++svit)
					if (!columnHeaders.size() || (columnHeaders.find(*svit) != columnHeaders.end()))
					{
						ColumnHeaderMapToColNum::const_iterator cnit = typeInfo.headerToColNum.find(*svit);

						if (cnit != typeInfo.headerToColNum.end())
						{
							sample += *svit;
							activeColumns->push_back(cnit->second);
						}
					}

				if (activeColumns->size())
				{
					recordTypeToActiveColumns[frit->first] = activeColumns;
					headersStm << sample.ToString();
				}
			}
		}

		// iterate through the rest of the file and pick out each record, looking up the
		// the date and time fields according to the record types
		bool caught = false;
		while(fstm.good())
		{
			CSVSample sample;

			try
			{
				sample.ParseCSV(fstm);
			}
			catch(exception& ex)
			{
				caught = true;
			}

			if (!caught)
			{
				const size_t sampleSize = sample.Size();

				if (sampleSize)
				{
					const string& recordType = sample[0];
					RecordTypeToActiveColumnsMap::const_iterator rait = recordTypeToActiveColumns.find(recordType);

					if ((rait != recordTypeToActiveColumns.end()) && rait->second)
					{
						const timeval recordTime = GetTimeFromSample(sample, recordInfo);

						if (TimeRange::Compare(recordTime,TimeRange::BadValue) && timeRange.InRange(recordTime))
						{
							bool appendSample = false;
							string sampleStr;

							if (!columnHeaders.size())
							{
								appendSample = true;
								sampleStr = sample.ToString();;
							}
							else
							{
								CSVSample outputSample;
								outputSample += rait->first;

								const ActiveColumns& active = *rait->second;

								for (ActiveColumns::const_iterator actit = active.begin(); actit != active.end(); ++actit)
								{
									string columnData;

									if (*actit < sampleSize)
										columnData = sample[*actit];

									outputSample += columnData;
								}

								appendSample = true;
								sampleStr = outputSample.ToString();
							}

							if (appendSample)
							{
								if (!samplesWritten)
								{
									if (withHeaders)
										stm << headersStm.str();

									samplesWritten = true;
								}

								stm << sampleStr;
							}
						}
					}
				}
			}
		}
	}

	return samplesWritten;
}

TimeRange CSVSampleFileSet::GetTimeRangeFromFileContent(const string& fileName) const
{
	TimeRange result(TimeRange::MinValue, TimeRange::MinValue, false);
	PlainTextFile plainFile(fileName);
	if (!plainFile.Initialize(false))
		return result;
	istream& stm = plainFile.stm;

	if (stm.good())
	{
		// reads out the heading lines for each record type at the start of the file
		// and saves them in the data structure
		FileRecordTypeToInfoMap recordInfoMap;
		recordInfoMap.GetFromStream(stm);

		// iterate through the rest of the file and pick out each record, looking up the
		// the date and time fields according to the record types
		bool caught = false;
		while(stm.good())
		{
			CSVSample sample;
			try
			{
				sample.ParseCSV(stm);
			}
			catch(exception& ex)
			{
				caught = true;
			}

			if (!caught)
			{
				timeval tv = GetTimeFromSample(sample, recordInfoMap);

				if (TimeRange::Compare(tv, TimeRange::BadValue))
					result.ExtendRange(tv);
			}
		}
	}

	return result;
}

// prefix-yyyymmddHHMMSS-yyyymmddHHMMSS
TimeRange CSVSampleFileSet::GetTimeRangeFromFileName(const string& fileName) const
{
#if BOOST_FILESYSTEM_VERSION < 3
	using namespace boost::filesystem3;
#else
	using namespace boost::filesystem;
#endif

	TimeRange result(false);
	path fpath(fileName);
	path fnameOnlyPath = fpath.filename();
	string fnameOnly = fnameOnlyPath.string();
	const string& prefix = fileSetSettings.GetPrefix();
	const size_t prefixLength = prefix.length();

	if (boost::starts_with(fnameOnly, prefix)
	 && (fnameOnly.length() >= (prefixLength+30))
	 && (fnameOnly[prefixLength] == '-')
	 && (fnameOnly[prefixLength+15] == '-'))
	{
		const time_t t1 = DecodeTimestamp(fnameOnly, prefixLength+1, false);
		const time_t t2 = DecodeTimestamp(fnameOnly, prefixLength+16, false);

		if ((t1 != (time_t) -1) && (t2 != (time_t) -1))
			result.SetRange(t1, t2);
	}

	return result;
}

void CSVSampleFileSet::RegisterSampleHeaders(const string& recordType, CSVSample& data, bool withStandardColumns)
{
	StringVectorPtr headers;
	headers.reset(new StringVector);

	headers->push_back(recordType);

	if (withStandardColumns)
	{
		//headers->push_back(recordTypeHeader);
		headers->push_back(dateHeader);
		headers->push_back(timeHeader);
		headers->push_back(millisecondsHeader);
		headers->push_back(latitudeHeader);
		headers->push_back(longitudeHeader);
	}

	const CSVSample::ContainerType& container = data.Container();

	for (CSVSample::ContainerType::const_iterator ci = container.begin(); ci != container.end(); ++ci)
		headers->push_back(*ci);

	{ ILockableConcept::Lock lock(dataMutex); recordTypeRegistry[recordType] = headers; }
}

void CSVSampleFileSet::WriteSample(const string& recordType, const timeval& sampleTime,
		double latitude, double longitude, CSVSample& data)
{
	CSVSample temp(0,6+data.Size());
	temp += recordType;

	TimeRange::GetTimevalStrings tvs(sampleTime);
	temp += tvs.dateStr;
	temp += tvs.timeStr;
	temp += tvs.msStr;

	stringstream latStm;
	latStm << setprecision(9) << latitude;
	const string latStr = latStm.str();
	temp += latStr;

	stringstream lonStm;
	lonStm << setprecision(10) << longitude;
	const string lonStr = lonStm.str();
	temp += lonStr;

	const CSVSample::ContainerType& container = data.Container();

	for (CSVSample::ContainerType::const_iterator ci = container.begin(); ci != container.end(); ++ci)
		temp += *ci;

	const string line = temp.ToString();
	fileSet->Write(line);
}

void CSVSampleFileSet::WriteSample(CSVSample& data)
{
	const string line = data.ToString();
	fileSet->Write(line);
}

void CSVSampleFileSet::OnFileVolumeComplete(FileSet& sender, const string& oldFileName, string& proposedFileName)
{
#if BOOST_FILESYSTEM_VERSION < 3
	using namespace boost::filesystem3;
#else
	using namespace boost::filesystem;
#endif

	TimeRange range = GetTimeRangeFromFileContent(oldFileName);

	if (range)
	{
		path old(oldFileName);

		if (old.has_extension())
		{
			path oldExt = old.extension();
			size_t extLen = oldExt.string().length();
			string baseName = oldFileName.substr(0,oldFileName.length()-extLen);
			TimeRange::GetTimevalStrings gtsMin(range.GetMin()), gtsMax(range.GetMax());
			stringstream stm;
			stm << baseName
				<< "-" << gtsMin.compressedDateStr << gtsMin.compressedTimeStr
				<< "-" << gtsMax.compressedDateStr << gtsMax.compressedTimeStr
				<< oldExt.string();
			//newStr = stm.str();
			proposedFileName = stm.str();
		}
	}
}

void CSVSampleFileSet::OnNewFile(FileSet& sender, const string& newFileName, ByteArray& fileInitData)
{
	stringstream stm;
	ILockableConcept::Lock lock(dataMutex);

	for (RecordTypeToHeaders::const_iterator ci = recordTypeRegistry.begin(); ci != recordTypeRegistry.end(); ++ci)
	{
		StringVectorPtr headers = ci->second;
		CSVSample sample;

		for (StringVector::const_iterator sci = headers->begin(); sci != headers->end(); ++sci)
			sample += *sci;

		sample.FormatCSV(stm);
	}

	string str = stm.str();

	if (str.length())
	{
		const ByteArray::byte* pStart = (const ByteArray::byte*) str.data();
		fileInitData.assign(pStart, pStart+str.length());
	}
}

void CSVSampleFileSet::FileRecordTypeToInfoMap::GetFromStream(istream& stm)
{
	clear();

	bool looping = true;

	while(looping && stm.good())
	{
		CSVSample sample;
		bool goodStartPos = false;
		streampos startPos;

		try
		{
			startPos = stm.tellg();

			if (stm.good())
			{
				goodStartPos = true;
				sample.ParseCSV(stm);
			}
			else
				looping = false;
		}
		catch(...)
		{
			if (goodStartPos)
				stm.seekg(startPos);

			looping = false;
		}

		const size_t numColumns = sample.Size();

		if (numColumns)
		{
			const string& recordType = sample[0];
			const_iterator thiscit = find(recordType);

			if (thiscit == end())
			{
				FileRecordTypeInfo::Ptr infoPtr;
				infoPtr.reset(new FileRecordTypeInfo);

				infoPtr->recordType = recordType;

				for (size_t idx = 1; idx < numColumns; ++idx)
				{
					infoPtr->columnHeadersOrdered.push_back(sample[idx]);
					infoPtr->headerToColNum[sample[idx]] = idx;
				}

				(*this)[recordType] = infoPtr;
			}
			else
			{
				stm.seekg(startPos);
				looping = false;
			}
		}
	}
}

timeval CSVSampleFileSet::GetTimeFromSample(const CSVSample& sample, FileRecordTypeToInfoMap& recordInfo)
{
	timeval result = TimeRange::BadValue;
	const size_t sampleSize = sample.Size();

	if (sampleSize)
	{
		const string& recordType = sample[0];
		FileRecordTypeToInfoMap::const_iterator rcit = recordInfo.find(recordType);

		// it's unexpected there will be no match for the record name in the file: ignored for now
		if (rcit != recordInfo.end())
		{
			ColumnHeaderMapToColNum& columns = rcit->second->headerToColNum;;

			ColumnHeaderMapToColNum::const_iterator cciDate = columns.find(dateHeader);
			ColumnHeaderMapToColNum::const_iterator cciTime = columns.find(timeHeader);
			ColumnHeaderMapToColNum::const_iterator cciMS = columns.find(millisecondsHeader);

			if ((cciDate != columns.end())
			 && (cciTime != columns.end())
			 && (cciMS != columns.end())
			 && (cciDate->second < sampleSize)
			 && (cciTime->second < sampleSize)
			 && (cciMS->second < sampleSize))
			{
				stringstream ttemp;
				ttemp << sample[cciTime->second] << "." << sample[cciMS->second];

				if (!TimeRange::FromString(sample[cciDate->second], ttemp.str(), result, false))
					result = TimeRange::BadValue;
			}
		}
	}

	return result;
}

}
}

