/***************************************************************************
 *
 * PROJECT: The Dark Mod - Updater
 * $Revision$
 * $Date$
 * $Author$
 *
 ***************************************************************************/

#include "ConsoleUpdater.h"

#include "Util.h"

namespace tdm
{

namespace updater
{

namespace
{
	const std::size_t PROGRESS_METER_WIDTH = 25;

	std::string GetShortenedString(const std::string& input, std::size_t maxLength)
	{
		if (input.length() > maxLength)
		{
			if (maxLength == 0)
			{
				return "";
			}
			else if (maxLength < 3)
			{
				return std::string(maxLength, '.');
			}

			std::size_t diff = input.length() - maxLength + 3; // 3 chars for the ellipsis
			std::size_t curLength = input.length();

			return input.substr(0, (curLength - diff) / 2) + "..." +
				input.substr((curLength + diff) / 2);
		}

		return input;
	}
}

ConsoleUpdater::ConsoleUpdater(int argc, char* argv[]) :
	_outcome(None),
	_options(argc, argv),
	_controller(*this, fs::path(argv[0]).leaf(), _options),
	_done(false)
{
	_abortSignalHandler = boost::bind(&ConsoleUpdater::OnAbort, this, _1);

	signal(SIGABRT, AbortSignalHandler);
	signal(SIGTERM, AbortSignalHandler);
	signal(SIGINT, AbortSignalHandler);
}

ConsoleUpdater::~ConsoleUpdater()
{
	_abortSignalHandler = boost::function<void(int)>();
}

void ConsoleUpdater::Run()
{
	// Parse the command line
	if (_options.IsSet("help"))
	{
		_options.PrintHelp();
		_outcome = Ok;
		return;
	}

	_controller.StartOrContinue();

	// Main loop, just keep the controller object going
	while (!_done)
	{
		Sleep(50);
	}

	if (!_controller.AllThreadsDone())
	{
		// Termination seems to be abnormal, attempt to exit gracefully 

		TraceLog::WriteLine(LOG_STANDARD, " Waiting 5 seconds for threads to finish their work...");
		std::size_t count = 0;

		while (count++ < 100 && !_controller.AllThreadsDone())
		{
			Sleep(50);
		}

		// Exit anyway after 5000 milliseconds

		_controller.PerformPostUpdateCleanup();
	}
}

void ConsoleUpdater::AbortSignalHandler(int signal)
{
	if (_abortSignalHandler)
	{
		_abortSignalHandler(signal);
	}
}

void ConsoleUpdater::OnAbort(int signal)
{
	TraceLog::Error("\nAbort signal received, trying to exit gracefully.");

	_controller.Abort();

	_done = true; // exit main loop
}

void ConsoleUpdater::OnStartStep(UpdateStep step)
{
	switch (step)
	{
	case Init:
		TraceLog::WriteLine(LOG_STANDARD, "----------------------------------------------------------------------------");
		TraceLog::WriteLine(LOG_STANDARD, " Initialising...");
		break;

	case CleanupPreviousSession:
		TraceLog::WriteLine(LOG_STANDARD, "----------------------------------------------------------------------------");
		TraceLog::WriteLine(LOG_STANDARD, " Cleaning up previous update session...");
		break;

	case UpdateMirrors:
		TraceLog::WriteLine(LOG_STANDARD, "----------------------------------------------------------------------------");
		TraceLog::WriteLine(LOG_STANDARD, " Downloading mirror information...");
		break;

	case DownloadCrcs:
		TraceLog::WriteLine(LOG_STANDARD, "----------------------------------------------------------------------------");
		TraceLog::WriteLine(LOG_STANDARD, " Downloading CRC file...");
		break;

	case DownloadVersionInfo:
		TraceLog::WriteLine(LOG_STANDARD, "----------------------------------------------------------------------------");
		TraceLog::WriteLine(LOG_STANDARD, " Downloading version info file...");
		break;

	case DetermineLocalVersion:
		TraceLog::WriteLine(LOG_STANDARD, "----------------------------------------------------------------------------");
		TraceLog::WriteLine(LOG_STANDARD, " Trying to match local files to version definitions...");
		break;

	case CompareLocalFilesToNewest:
		TraceLog::WriteLine(LOG_STANDARD, "----------------------------------------------------------------------------");
		TraceLog::WriteLine(LOG_STANDARD, " Comparing local files to server definitions...");
		break;

	case DownloadNewUpdater:
		TraceLog::WriteLine(LOG_STANDARD, "----------------------------------------------------------------------------");
		TraceLog::WriteLine(LOG_STANDARD, " Downloading TDM Update application...");
		break;

	case DownloadDifferentialUpdate:
		TraceLog::WriteLine(LOG_STANDARD, "----------------------------------------------------------------------------");
		TraceLog::WriteLine(LOG_STANDARD, " Downloading differential update package...");
		break;

	case PerformDifferentialUpdate:
		TraceLog::WriteLine(LOG_STANDARD, "----------------------------------------------------------------------------");
		TraceLog::WriteLine(LOG_STANDARD, " Applying differential update package...");
		break;

	case DownloadFullUpdate:
		TraceLog::WriteLine(LOG_STANDARD, "----------------------------------------------------------------------------");
		TraceLog::WriteLine(LOG_STANDARD, " Downloading updates...");
		break;
	};
}

void ConsoleUpdater::OnFinishStep(UpdateStep step)
{
	switch (step)
	{
	case Init:
	{
		TraceLog::WriteLine(LOG_STANDARD, " Done.");
	}
	break;

	case CleanupPreviousSession:
	{
		TraceLog::WriteLine(LOG_STANDARD, " Done.");
	}
	break;

	case UpdateMirrors:
	{
		// Mirrors
		bool keepMirrors = _options.IsSet("keep-mirrors");

		if (keepMirrors)
		{
			TraceLog::WriteLine(LOG_STANDARD, " Skipped downloading mirrors.");
		}
		else
		{
			TraceLog::WriteLine(LOG_STANDARD, " Done downloading mirrors.");
		}

		std::size_t numMirrors = _controller.GetNumMirrors();

		TraceLog::WriteLine(LOG_STANDARD, (boost::format("   Found %d mirror%s.") % numMirrors % (numMirrors == 1 ? "" : "s")).str());

		if (numMirrors == 0)
		{
			TraceLog::WriteLine(LOG_STANDARD, " No mirror information available - cannot continue.");

			// Stop right here
			_controller.Abort();
		}
	}
	break;

	case DownloadCrcs:
	{
		//TraceLog::WriteLine(LOG_STANDARD, " Done downloading checksums.");
	}
	break;

	case DownloadVersionInfo:
	{
		TraceLog::Write(LOG_STANDARD, " Done downloading versions.");

		if (!_controller.GetNewestVersion().empty())
		{
			std::string newest = (boost::format(" Newest version is %s.") % _controller.GetNewestVersion()).str();
			TraceLog::WriteLine(LOG_STANDARD, newest);
		}
		else
		{
			TraceLog::WriteLine(LOG_STANDARD, "");
		}
	}
	break;

	case DetermineLocalVersion:
	{
		TraceLog::Write(LOG_STANDARD, " Done comparing local files: ");

		if (_controller.GetLocalVersion().empty())
		{
			TraceLog::WriteLine(LOG_STANDARD, "no luck, PK4 files do not match.");
		}
		else
		{
			std::string versionFound = (boost::format("local version is %s.") % _controller.GetLocalVersion()).str();
			TraceLog::WriteLine(LOG_STANDARD, versionFound);
		}
	}
	break;

	case CompareLocalFilesToNewest:
	{
		TraceLog::WriteLine(LOG_STANDARD, " Done comparing local files to server definitions.");

		std::string sizeStr = Util::GetHumanReadableBytes(_controller.GetTotalDownloadSize());
		std::size_t numFiles = _controller.GetNumFilesToBeUpdated();

		std::string totalSize = (boost::format("  %d %s to be downloaded (size: %s).") % 
								 numFiles % (numFiles == 1 ? "file needs" : "files need") % sizeStr).str();

		// Print a summary
		if (_controller.NewUpdaterAvailable())
		{
			TraceLog::WriteLine(LOG_STANDARD, " A new updater is available: " + totalSize);
		}
		else if (_controller.LocalFilesNeedUpdate())
		{
			if (_controller.DifferentialUpdateAvailable())
			{
				TraceLog::Write(LOG_STANDARD, " A differential update is available.");

				sizeStr = Util::GetHumanReadableBytes(_controller.GetTotalDifferentialUpdateSize());
				totalSize = (boost::format(" Download size: %s") % sizeStr).str();	
			}
			else
			{
				TraceLog::Write(LOG_STANDARD, " Updates are available.");
			}

			TraceLog::WriteLine(LOG_STANDARD, totalSize);
		}
		else
		{
			TraceLog::WriteLine(LOG_STANDARD, " Your TDM installation is up to date");
		}
	}
	break;

	case DownloadNewUpdater:
	{
		TraceLog::WriteLine(LOG_STANDARD, " Done downloading updater - will restart the application.");
	}
	break;

	case DownloadDifferentialUpdate:
	{
		TraceLog::WriteLine(LOG_STANDARD, " Done downloading the differential update.");
	}
	break;

	case PerformDifferentialUpdate:
	{
		TraceLog::WriteLine(LOG_STANDARD, " Done applying the differential update.");
	}
	break;

	case DownloadFullUpdate:
	{
		TraceLog::WriteLine(LOG_STANDARD, " Done downloading updates.");

		std::string totalBytesStr = (boost::format(" Total bytes downloaded: %s") % Util::GetHumanReadableBytes(_controller.GetTotalBytesDownloaded())).str();
		TraceLog::WriteLine(LOG_STANDARD, totalBytesStr);

		if (!_controller.LocalFilesNeedUpdate())
		{
			TraceLog::WriteLine(LOG_STANDARD, "----------------------------------------------------------------------------");
			TraceLog::WriteLine(LOG_STANDARD, " Your TDM installation is up to date.");
		}
	}
	break;

	case PostUpdateCleanup:
		break;

	case Done:
		_done = true; // break main loop
		break;

	case RestartUpdater:
		_done = true; // break main loop
		break;
	};
}

void ConsoleUpdater::OnFailure(UpdateStep step, const std::string& errorMessage)
{
	TraceLog::WriteLine(LOG_STANDARD, "");
	TraceLog::Error(errorMessage);

	_done = true; // break main loop
}

void ConsoleUpdater::OnMessage(const std::string& message)
{
	TraceLog::WriteLine(LOG_STANDARD, "=======================================");
	TraceLog::WriteLine(LOG_STANDARD, message);
}

void ConsoleUpdater::OnWarning(const std::string& message)
{
	TraceLog::WriteLine(LOG_STANDARD, "============== WARNING ================");
	TraceLog::WriteLine(LOG_STANDARD, message);
	TraceLog::WriteLine(LOG_STANDARD, "=======================================");

	TraceLog::WriteLine(LOG_STANDARD, "Waiting 10 seconds before continuing automatically...");

	Util::Wait(10000);
}

void ConsoleUpdater::OnProgressChange(const ProgressInfo& info)
{
	switch (info.type)
	{
	case ProgressInfo::FileDownload:
		// Download progress
		if (!_info.file.empty() && !info.file.empty() && info.file != _info.file)
		{
			// New file, finish the current download
			_info.progressFraction = 1.0f;
			PrintProgress();

			// Add a line break when a new file starts
			TraceLog::WriteLine(LOG_PROGRESS, "");

			TraceLog::WriteLine(LOG_PROGRESS, 
				(boost::format(" Downloading from Mirror %s: %s") % info.mirrorDisplayName % info.file.string()).str());
		}
		else if (_info.file.empty())
		{
			// First file
			TraceLog::WriteLine(LOG_PROGRESS, 
				(boost::format(" Downloading from Mirror %s: %s") % info.mirrorDisplayName % info.file.string()).str());
		}

		_info = info;

		// Print the new info
		PrintProgress();

		// Add a new line if we're done here
		if (info.progressFraction >= 1)
		{
			TraceLog::WriteLine(LOG_PROGRESS, "");
		}
		break;

	case ProgressInfo::FileOperation:

		_info = info;

		// Print the new info
		PrintProgress();

		// Add a new line if we're done here
		if (info.progressFraction >= 1)
		{
			TraceLog::WriteLine(LOG_PROGRESS, "");
		}
		break;
	};
}

void ConsoleUpdater::PrintProgress()
{
	TraceLog::Write(LOG_PROGRESS, "\r");

	// Progress bar
	std::size_t numTicks = static_cast<std::size_t>(floor(_info.progressFraction * PROGRESS_METER_WIDTH));
	std::string progressBar(numTicks, '=');
	std::string progressSpace(PROGRESS_METER_WIDTH - numTicks, ' ');

	std::string line = " [" + progressBar + progressSpace + "]";
	
	// Percent
	line += (boost::format(" %2.1f%%") % (_info.progressFraction*100)).str();

	switch (_info.type)
	{
	case ProgressInfo::FileDownload:	
	{
		line += " at " + Util::GetHumanReadableBytes(static_cast<std::size_t>(_info.downloadSpeed)) + "/sec ";
	}
	break;

	case ProgressInfo::FileOperation:
	{
		std::string verb;

		switch (_info.operationType)
		{
		case ProgressInfo::Check: 
			verb = "Checking: "; 
			break;
		case ProgressInfo::Remove: 
			verb = "Removing: ";
			break;
		case ProgressInfo::Replace: 
			verb = "Replacing: ";
			break;
		case ProgressInfo::Add: 
			verb = "Adding: ";
			break;
		case ProgressInfo::RemoveFilesFromPK4: 
			verb = "Preparing PK4: ";
			break;
		default: 
			verb = "File: ";
		};

		line += " " + verb;

		std::size_t remainingLength = line.length() > 79 ? 0 : 79 - line.length();
		line += GetShortenedString(_info.file.leaf(), remainingLength);
	}
	break;
	};

	// Expand the line length to 79 characters
	if (line.length() < 79)
	{
		line += std::string(79 - line.length(), ' ');
	}
	else if (line.length() > 79)
	{
		line = line.substr(0, 79);
	}

	TraceLog::Write(LOG_PROGRESS, line);
}

void ConsoleUpdater::OnStartDifferentialUpdate(const DifferentialUpdateInfo& info)
{
}

void ConsoleUpdater::OnPerformDifferentialUpdate(const DifferentialUpdateInfo& info)
{
}

void ConsoleUpdater::OnFinishDifferentialUpdate(const DifferentialUpdateInfo& info)
{
}

boost::function<void(int)> ConsoleUpdater::_abortSignalHandler;

} // namespace

} // namespace
