/*-------------------------------------------------------------------------
 * drawElements Quality Program Test Executor
 * ------------------------------------------
 *
 * Copyright 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *//*!
 * \file
 * \brief Test log compare utility.
 *//*--------------------------------------------------------------------*/

#include "xeTestLogParser.hpp"
#include "xeTestResultParser.hpp"
#include "deFilePath.hpp"
#include "deString.h"
#include "deThread.hpp"
#include "deCommandLine.hpp"

#include <vector>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <set>
#include <map>

using std::vector;
using std::string;
using std::set;
using std::map;

enum OutputMode
{
	OUTPUTMODE_ALL = 0,
	OUTPUTMODE_DIFF,

	OUTPUTMODE_LAST
};

enum OutputFormat
{
	OUTPUTFORMAT_TEXT = 0,
	OUTPUTFORMAT_CSV,

	OUTPUTFORMAT_LAST
};

enum OutputValue
{
	OUTPUTVALUE_STATUS_CODE = 0,
	OUTPUTVALUE_STATUS_DETAILS,

	OUTPUTVALUE_LAST
};

namespace opt
{

DE_DECLARE_COMMAND_LINE_OPT(OutMode,	OutputMode);
DE_DECLARE_COMMAND_LINE_OPT(OutFormat,	OutputFormat);
DE_DECLARE_COMMAND_LINE_OPT(OutValue,	OutputValue);

static void registerOptions (de::cmdline::Parser& parser)
{
	using de::cmdline::Option;
	using de::cmdline::NamedValue;

	static const NamedValue<OutputMode> s_outputModes[] =
	{
		{ "all",	OUTPUTMODE_ALL	},
		{ "diff",	OUTPUTMODE_DIFF	}
	};
	static const NamedValue<OutputFormat> s_outputFormats[] =
	{
		{ "text",	OUTPUTFORMAT_TEXT	},
		{ "csv",	OUTPUTFORMAT_CSV	}
	};
	static const NamedValue<OutputValue> s_outputValues[] =
	{
		{ "code",		OUTPUTVALUE_STATUS_CODE		},
		{ "details",	OUTPUTVALUE_STATUS_DETAILS	}
	};

	parser << Option<OutFormat>		("f",	"format",		"Output format",	s_outputFormats,	"csv")
		   << Option<OutMode>		("m",	"mode",			"Output mode",		s_outputModes,		"all")
		   << Option<OutValue>		("v",	"value",		"Value to extract",	s_outputValues,		"code");
}

} // opt

struct CommandLine
{
	CommandLine (void)
		: outMode	(OUTPUTMODE_ALL)
		, outFormat	(OUTPUTFORMAT_CSV)
		, outValue	(OUTPUTVALUE_STATUS_CODE)
	{
	}

	OutputMode			outMode;
	OutputFormat		outFormat;
	OutputValue			outValue;
	vector<string>		filenames;
};

struct ShortBatchResult
{
	vector<xe::TestCaseResultHeader>	resultHeaders;
	map<string, int>					resultMap;
};

class ShortResultHandler : public xe::TestLogHandler
{
public:
	ShortResultHandler (ShortBatchResult& result)
		: m_result(result)
	{
	}

	void setSessionInfo (const xe::SessionInfo&)
	{
		// Ignored.
	}

	xe::TestCaseResultPtr startTestCaseResult (const char* casePath)
	{
		return xe::TestCaseResultPtr(new xe::TestCaseResultData(casePath));
	}

	void testCaseResultUpdated (const xe::TestCaseResultPtr&)
	{
		// Ignored.
	}

	void testCaseResultComplete (const xe::TestCaseResultPtr& caseData)
	{
		xe::TestCaseResultHeader	header;
		int							caseNdx	= (int)m_result.resultHeaders.size();

		header.casePath			= caseData->getTestCasePath();
		header.caseType			= xe::TESTCASETYPE_SELF_VALIDATE;
		header.statusCode		= caseData->getStatusCode();
		header.statusDetails	= caseData->getStatusDetails();

		if (header.statusCode == xe::TESTSTATUSCODE_LAST)
		{
			xe::TestCaseResult fullResult;

			xe::parseTestCaseResultFromData(&m_testResultParser, &fullResult, *caseData.get());

			header = xe::TestCaseResultHeader(fullResult);
		}

		// Insert into result list & map.
		m_result.resultHeaders.push_back(header);
		m_result.resultMap[header.casePath] = caseNdx;
	}

private:
	ShortBatchResult&		m_result;
	xe::TestResultParser	m_testResultParser;
};

static void readLogFile (ShortBatchResult& batchResult, const char* filename)
{
	std::ifstream		in				(filename, std::ifstream::binary|std::ifstream::in);
	ShortResultHandler	resultHandler	(batchResult);
	xe::TestLogParser	parser			(&resultHandler);
	deUint8				buf				[1024];
	int					numRead			= 0;

	for (;;)
	{
		in.read((char*)&buf[0], DE_LENGTH_OF_ARRAY(buf));
		numRead = (int)in.gcount();

		if (numRead <= 0)
			break;

		parser.parse(&buf[0], numRead);
	}

	in.close();
}

class LogFileReader : public de::Thread
{
public:
	LogFileReader (ShortBatchResult& batchResult, const char* filename)
		: m_batchResult	(batchResult)
		, m_filename	(filename)
	{
	}

	void run (void)
	{
		readLogFile(m_batchResult, m_filename.c_str());
	}

private:
	ShortBatchResult&	m_batchResult;
	std::string			m_filename;
};

static void computeCaseList (vector<string>& cases, const vector<ShortBatchResult>& batchResults)
{
	// \todo [2012-07-10 pyry] Do proper case ordering (eg. handle missing cases nicely).
	set<string> addedCases;

	for (vector<ShortBatchResult>::const_iterator batchIter = batchResults.begin(); batchIter != batchResults.end(); batchIter++)
	{
		for (vector<xe::TestCaseResultHeader>::const_iterator caseIter = batchIter->resultHeaders.begin(); caseIter != batchIter->resultHeaders.end(); caseIter++)
		{
			if (addedCases.find(caseIter->casePath) == addedCases.end())
			{
				cases.push_back(caseIter->casePath);
				addedCases.insert(caseIter->casePath);
			}
		}
	}
}

static void getTestResultHeaders (vector<xe::TestCaseResultHeader>& headers, const vector<ShortBatchResult>& batchResults, const char* casePath)
{
	headers.resize(batchResults.size());

	for (int ndx = 0; ndx < (int)batchResults.size(); ndx++)
	{
		const ShortBatchResult&				batchResult	= batchResults[ndx];
		map<string, int>::const_iterator	resultPos	= batchResult.resultMap.find(casePath);

		if (resultPos != batchResult.resultMap.end())
			headers[ndx] = batchResult.resultHeaders[resultPos->second];
		else
		{
			headers[ndx].casePath	= casePath;
			headers[ndx].caseType	= xe::TESTCASETYPE_SELF_VALIDATE;
			headers[ndx].statusCode	= xe::TESTSTATUSCODE_LAST;
		}
	}
}

static const char* getStatusCodeName (xe::TestStatusCode code)
{
	if (code == xe::TESTSTATUSCODE_LAST)
		return "Missing";
	else
		return xe::getTestStatusCodeName(code);
}

static bool runCompare (const CommandLine& cmdLine, std::ostream& dst)
{
	vector<ShortBatchResult>	results;
	vector<string>				batchNames;
	bool						compareOk	= true;

	XE_CHECK(!cmdLine.filenames.empty());

	try
	{
		// Read in batch results
		results.resize(cmdLine.filenames.size());
		{
			std::vector<de::SharedPtr<LogFileReader> > readers;

			for (int ndx = 0; ndx < (int)cmdLine.filenames.size(); ndx++)
			{
				readers.push_back(de::SharedPtr<LogFileReader>(new LogFileReader(results[ndx], cmdLine.filenames[ndx].c_str())));
				readers.back()->start();
			}

			for (int ndx = 0; ndx < (int)cmdLine.filenames.size(); ndx++)
			{
				readers[ndx]->join();

				// Use file name as batch name.
				batchNames.push_back(de::FilePath(cmdLine.filenames[ndx].c_str()).getBaseName());
			}
		}

		// Compute unified case list.
		vector<string> caseList;
		computeCaseList(caseList, results);

		// Stats.
		int		numCases		= (int)caseList.size();
		int		numEqual		= 0;

		if (cmdLine.outFormat == OUTPUTFORMAT_CSV)
		{
			dst << "TestCasePath";
			for (vector<string>::const_iterator nameIter = batchNames.begin(); nameIter != batchNames.end(); nameIter++)
				dst << "," << *nameIter;
			dst << "\n";
		}

		// Compare cases.
		for (vector<string>::const_iterator caseIter = caseList.begin(); caseIter != caseList.end(); caseIter++)
		{
			const string&						caseName	= *caseIter;
			vector<xe::TestCaseResultHeader>	headers;
			bool								allEqual	= true;

			getTestResultHeaders(headers, results, caseName.c_str());

			for (vector<xe::TestCaseResultHeader>::const_iterator iter = headers.begin()+1; iter != headers.end(); iter++)
			{
				if (iter->statusCode != headers[0].statusCode)
				{
					allEqual = false;
					break;
				}
			}

			if (allEqual)
				numEqual += 1;

			if (cmdLine.outMode == OUTPUTMODE_ALL || !allEqual)
			{
				if (cmdLine.outFormat == OUTPUTFORMAT_TEXT)
				{
					dst << caseName << "\n";
					for (int ndx = 0; ndx < (int)headers.size(); ndx++)
						dst << "  " << batchNames[ndx] << ": " << getStatusCodeName(headers[ndx].statusCode) << " (" << headers[ndx].statusDetails << ")\n";
					dst << "\n";
				}
				else if (cmdLine.outFormat == OUTPUTFORMAT_CSV)
				{
					dst << caseName;
					for (vector<xe::TestCaseResultHeader>::const_iterator iter = headers.begin(); iter != headers.end(); iter++)
						dst << "," << (cmdLine.outValue == OUTPUTVALUE_STATUS_CODE ? getStatusCodeName(iter->statusCode) : iter->statusDetails.c_str());
					dst << "\n";
				}
			}
		}

		compareOk = numEqual == numCases;

		if (cmdLine.outFormat == OUTPUTFORMAT_TEXT)
		{
			dst << "  " << numEqual << " / " << numCases << " test case results match.\n";
			dst << "  Comparison " << (compareOk ? "passed" : "FAILED") << "!\n";
		}
	}
	catch (const std::exception& e)
	{
		printf("%s\n", e.what());
		compareOk = false;
	}

	return compareOk;
}

static bool parseCommandLine (CommandLine& cmdLine, int argc, const char* const* argv)
{
	de::cmdline::Parser			parser;
	de::cmdline::CommandLine	opts;

	XE_CHECK(argc >= 1);

	opt::registerOptions(parser);

	if (!parser.parse(argc-1, &argv[1], &opts, std::cerr)	||
		opts.getArgs().empty())
	{
		std::cout << argv[0] << ": [options] [filenames]\n";
		parser.help(std::cout);
		return false;
	}

	cmdLine.outFormat	= opts.getOption<opt::OutFormat>();
	cmdLine.outMode		= opts.getOption<opt::OutMode>();
	cmdLine.outValue	= opts.getOption<opt::OutValue>();
	cmdLine.filenames	= opts.getArgs();

	return true;
}

int main (int argc, const char* const* argv)
{
	CommandLine cmdLine;

	if (!parseCommandLine(cmdLine, argc, argv))
		return -1;

	try
	{
		bool compareOk = runCompare(cmdLine, std::cout);
		return compareOk ? 0 : -1;
	}
	catch (const std::exception& e)
	{
		printf("FATAL ERROR: %s\n", e.what());
		return -1;
	}
}