//
// This file is part of the aMule Project.
//
// Copyright (c) 2003-2011 aMule Team ( admin@amule.org / http://www.amule.org )
// Copyright (c) 2002-2011 Merkur ( devs@emule-project.net / http://www.emule-project.net )
//
// Any parts of this program derived from the xMule, lMule or eMule project,
// or contributed by third-party developers are copyrighted by their
// respective authors.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301, USA
//

#include <wx/app.h>

#include "PartFileConvert.h"

#include "amule.h"
#include "DownloadQueue.h"
#include <common/Format.h>
#include "Logger.h"
#include "PartFile.h"
#include "Preferences.h"
#include "SharedFileList.h"
#include <common/FileFunctions.h>
#include "OtherFunctions.h"
#include "GuiEvents.h"
#include "DataToText.h"

static unsigned s_nextJobId = 0;


struct ConvertJob {
	unsigned	id;
	CPath		folder;
	CPath		filename;
	wxString	filehash;
	ConvStatus	state;
	uint32_t	size;
	uint32_t	spaceneeded;
	uint8		partmettype;
	bool		removeSource;
	ConvertJob(const CPath& file, bool deleteSource, ConvStatus status)
		: id(s_nextJobId++), folder(file), state(status), size(0), spaceneeded(0), partmettype(PMT_UNKNOWN), removeSource(deleteSource)
		{}
};

ConvertInfo::ConvertInfo(ConvertJob *job)
	: id(job->id),
	  folder(job->folder), filename(job->filename), filehash(job->filehash),
	  state(job->state), size(job->size), spaceneeded(job->spaceneeded)
{}


wxThread*		CPartFileConvert::s_convertPfThread = NULL;
std::list<ConvertJob*>	CPartFileConvert::s_jobs;
ConvertJob*		CPartFileConvert::s_pfconverting = NULL;
wxMutex			CPartFileConvert::s_mutex;


int CPartFileConvert::ScanFolderToAdd(const CPath& folder, bool deletesource)
{
	int count = 0;
	CDirIterator finder(folder);

	CPath file = finder.GetFirstFile(CDirIterator::File, wxT("*.part.met"));
	while (file.IsOk()) {
		ConvertToeMule(folder.JoinPaths(file), deletesource);
		file = finder.GetNextFile();
		count++;
	}
	/* Shareaza
	file = finder.GetFirstFile(CDirIterator::File, wxT("*.sd"));
	while (!file.IsEmpty()) {
		ConvertToeMule(file, deletesource);
		file = finder.GetNextFile();
		count++;
	}
	*/

	file = finder.GetFirstFile(CDirIterator::Dir, wxT("*.*"));
	while (file.IsOk()) {
		ScanFolderToAdd(folder.JoinPaths(file), deletesource);

		file = finder.GetNextFile();
	}

	return count;
}

void CPartFileConvert::ConvertToeMule(const CPath& file, bool deletesource)
{
	if (!file.FileExists()) {
		return;
	}

	ConvertJob* newjob = new ConvertJob(file, deletesource, CONV_QUEUE);

	wxMutexLocker lock(s_mutex);

	s_jobs.push_back(newjob);

	Notify_ConvertUpdateJobInfo(newjob);

	StartThread();
}

void CPartFileConvert::StartThread()
{
	if (!s_convertPfThread) {
		s_convertPfThread = new CPartFileConvert();

		switch ( s_convertPfThread->Create() ) {
			case wxTHREAD_NO_ERROR:
				AddDebugLogLineN( logPfConvert, wxT("A new thread has been created.") );
				break;
			case wxTHREAD_RUNNING:
				AddDebugLogLineC( logPfConvert, wxT("Error, attempt to create an already running thread!") );
				break;
			case wxTHREAD_NO_RESOURCE:
				AddDebugLogLineC( logPfConvert, wxT("Error, attempt to create a thread without resources!") );
				break;
			default:
				AddDebugLogLineC( logPfConvert, wxT("Error, unknown error attempting to create a thread!") );
		}

		// The thread shouldn't hog the CPU, as it will already be hogging the HD
		s_convertPfThread->SetPriority(WXTHREAD_MIN_PRIORITY);

		s_convertPfThread->Run();
	}
}

void CPartFileConvert::StopThread()
{
	if (s_convertPfThread) {
		s_convertPfThread->Delete();
	} else {
		return;
	}

	AddLogLineNS(_("Waiting for partfile convert thread to die..."));
	while (s_convertPfThread) {
		wxSleep(1);
	}
}

wxThread::ExitCode CPartFileConvert::Entry()
{
	int imported = 0;

	for (;;)
	{
		// search next queued job and start it
		{
			wxMutexLocker lock(s_mutex);
			s_pfconverting = NULL;
			for (std::list<ConvertJob*>::iterator it = s_jobs.begin(); it != s_jobs.end(); ++it) {
				if ((*it)->state == CONV_QUEUE) {
					s_pfconverting = *it;
					break;
				}
			}
		}

		if (s_pfconverting) {
			{
				wxMutexLocker lock(s_mutex);
				s_pfconverting->state = CONV_INPROGRESS;
			}

			Notify_ConvertUpdateJobInfo(s_pfconverting);

			ConvStatus convertResult = performConvertToeMule(s_pfconverting->folder);
			{
				wxMutexLocker lock(s_mutex);
				s_pfconverting->state = convertResult;
			}

			if (s_pfconverting->state == CONV_OK) {
				++imported;
			}

			Notify_ConvertUpdateJobInfo(s_pfconverting);

			AddLogLineC(CFormat(_("Importing %s: %s")) % s_pfconverting->folder % GetConversionState(s_pfconverting->state));

			if (TestDestroy()) {
				wxMutexLocker lock(s_mutex);
				DeleteContents(s_jobs);
				break;
			}
		} else {
			break; // nothing more to do now
		}
	}

	// clean up
	Notify_ConvertClearInfos();

	if (imported) {
		theApp->sharedfiles->PublishNextTurn();
	}

	AddDebugLogLineN(logPfConvert, wxT("No more jobs on queue, exiting from thread."));

	s_convertPfThread = NULL;

	return NULL;
}

ConvStatus CPartFileConvert::performConvertToeMule(const CPath& fileName)
{
	wxString filepartindex;

	CPath folder	= fileName.GetPath();
	CPath partfile	= fileName.GetFullName();
	CPath newfilename;

	CDirIterator finder(folder);

	Notify_ConvertUpdateProgressFull(0, _("Reading temp folder"), s_pfconverting->folder.GetPrintable());

	filepartindex = partfile.RemoveAllExt().GetRaw();

	Notify_ConvertUpdateProgress(4, _("Retrieving basic information from download info file"));

	CPartFile* file = new CPartFile();
	s_pfconverting->partmettype = file->LoadPartFile(folder, partfile, false, true);

	switch (s_pfconverting->partmettype) {
		case PMT_UNKNOWN:
		case PMT_BADFORMAT:
			delete file;
			return CONV_BADFORMAT;
	}

	CPath oldfile = folder.JoinPaths(partfile.RemoveExt());

	{
		wxMutexLocker lock(s_mutex);
		s_pfconverting->size = file->GetFileSize();
		s_pfconverting->filename = file->GetFileName();
		s_pfconverting->filehash = file->GetFileHash().Encode();
	}

	Notify_ConvertUpdateJobInfo(s_pfconverting);

	if (theApp->downloadqueue->GetFileByID(file->GetFileHash())) {
		delete file;
		return CONV_ALREADYEXISTS;
	}

	if (s_pfconverting->partmettype == PMT_SPLITTED) {
		unsigned fileindex;
		char *ba = new char [PARTSIZE];

		try {
			CFile inputfile;

			// just count
			unsigned maxindex = 0;
			unsigned partfilecount = 0;
			CPath filePath = finder.GetFirstFile(CDirIterator::File, filepartindex + wxT(".*.part"));
			while (filePath.IsOk()) {
				long l;
				++partfilecount;
				filePath.GetFullName().RemoveExt().GetExt().ToLong(&l);
				fileindex = (unsigned)l;
				filePath = finder.GetNextFile();
				if (fileindex > maxindex) maxindex = fileindex;
			}
			float stepperpart;
			{
				wxMutexLocker lock(s_mutex);
				if (partfilecount > 0) {
					stepperpart = (80.0f / partfilecount);
					if (maxindex * PARTSIZE <= s_pfconverting->size) {
						s_pfconverting->spaceneeded = maxindex * PARTSIZE;
					} else {
						s_pfconverting->spaceneeded = s_pfconverting->size;
					}
				} else {
					stepperpart = 80.0f;
					s_pfconverting->spaceneeded = 0;
				}
			}

			Notify_ConvertUpdateJobInfo(s_pfconverting);

			sint64 freespace = CPath::GetFreeSpaceAt(thePrefs::GetTempDir());
			if (freespace != wxInvalidOffset) {
				if (static_cast<uint64>(freespace) < maxindex * PARTSIZE) {
					delete file;
					delete [] ba;
					return CONV_OUTOFDISKSPACE;
				}
			}

			// create new partmetfile, and remember the new name
			file->CreatePartFile(true);
			newfilename = file->GetFullName();

			Notify_ConvertUpdateProgress(8, _("Creating destination file"));

			file->m_hpartfile.SetLength( s_pfconverting->spaceneeded );

			unsigned curindex = 0;
			CPath filename = finder.GetFirstFile(CDirIterator::File, filepartindex + wxT(".*.part"));
			while (filename.IsOk()) {
				// stats
				++curindex;

				Notify_ConvertUpdateProgress(10 + (curindex * stepperpart), CFormat(_("Loading data from old download file (%u of %u)")) % curindex % partfilecount);

				long l;
				filename.GetFullName().RemoveExt().GetExt().ToLong(&l);
				fileindex = (unsigned)l;
				if (fileindex == 0) {
					filename = finder.GetNextFile();
					continue;
				}

				uint32 chunkstart = (fileindex - 1) * PARTSIZE;

				// open, read data of the part-part-file into buffer, close file
				inputfile.Open(filename, CFile::read);
				uint64 toReadWrite = std::min<uint64>(PARTSIZE, inputfile.GetLength());
				inputfile.Read(ba, toReadWrite);
				inputfile.Close();

				Notify_ConvertUpdateProgress(10 + (curindex * stepperpart), CFormat(_("Saving data block into new single download file (%u of %u)")) % curindex % partfilecount);

				// write the buffered data
				file->m_hpartfile.WriteAt(ba, chunkstart, toReadWrite);

				filename = finder.GetNextFile();
			}
			delete[] ba;
		} catch (const CSafeIOException& e) {
			AddDebugLogLineC(logPfConvert, wxT("IO error while converting partfiles: ") + e.what());

			delete[] ba;
			file->Delete();
			return CONV_IOERROR;
		}

		file->m_hpartfile.Close();
	}
	// import an external common format partdownload
	else //if (pfconverting->partmettype==PMT_DEFAULTOLD || pfconverting->partmettype==PMT_NEWOLD || Shareaza  )
	{
		if (!s_pfconverting->removeSource) {
			wxMutexLocker lock(s_mutex);
			s_pfconverting->spaceneeded = oldfile.GetFileSize();
		}

		Notify_ConvertUpdateJobInfo(s_pfconverting);

		sint64 freespace = CPath::GetFreeSpaceAt(thePrefs::GetTempDir());
		if (freespace == wxInvalidOffset) {
			delete file;
			return CONV_IOERROR;
		} else if (freespace < s_pfconverting->spaceneeded) {
			delete file;
			return CONV_OUTOFDISKSPACE;
		}

		file->CreatePartFile(true);
		newfilename = file->GetFullName();

		file->m_hpartfile.Close();

		bool ret = false;

		Notify_ConvertUpdateProgress(92, _("Copy"));

		CPath::RemoveFile(newfilename.RemoveExt());
		if (!oldfile.FileExists()) {
			// data file does not exist. well, then create a 0 byte big one
			CFile datafile;
			ret = datafile.Create(newfilename.RemoveExt());
		} else if (s_pfconverting->removeSource) {
			ret = CPath::RenameFile(oldfile, newfilename.RemoveExt());
		} else {
			ret = CPath::CloneFile(oldfile, newfilename.RemoveExt(), false);
		}
		if (!ret) {
			file->Delete();
			//delete file;
			return CONV_FAILED;
		}

	}

	Notify_ConvertUpdateProgress(94, _("Retrieving source downloadfile information"));

	CPath::RemoveFile(newfilename);
	if (s_pfconverting->removeSource) {
		CPath::RenameFile(folder.JoinPaths(partfile), newfilename);
	} else {
		CPath::CloneFile(folder.JoinPaths(partfile), newfilename, false);
	}

	file->m_hashlist.clear();

	if (!file->LoadPartFile(thePrefs::GetTempDir(), file->GetPartMetFileName(), false)) {
		//delete file;
		file->Delete();
		return CONV_BADFORMAT;
	}

	if (s_pfconverting->partmettype == PMT_NEWOLD || s_pfconverting->partmettype == PMT_SPLITTED) {
		file->SetCompletedSize(file->transferred);
		file->m_iGainDueToCompression = 0;
		file->m_iLostDueToCorruption = 0;
	}

	Notify_ConvertUpdateProgress(100, _("Adding download and saving new partfile"));

	theApp->downloadqueue->AddDownload(file, thePrefs::AddNewFilesPaused(), 0);
	file->SavePartFile();

	if (file->GetStatus(true) == PS_READY) {
		theApp->sharedfiles->SafeAddKFile(file); // part files are always shared files
	}

	if (s_pfconverting->removeSource) {
		CPath oldFile = finder.GetFirstFile(CDirIterator::File, filepartindex + wxT(".*"));
		while (oldFile.IsOk()) {
			CPath::RemoveFile(folder.JoinPaths(oldFile));
			oldFile = finder.GetNextFile();
		}

		if (s_pfconverting->partmettype == PMT_SPLITTED) {
			CPath::RemoveDir(folder);
		}
	}

	return CONV_OK;
}

// Notification handlers

void CPartFileConvert::RemoveJob(unsigned id)
{
	wxMutexLocker lock(s_mutex);
	for (std::list<ConvertJob*>::iterator it = s_jobs.begin(); it != s_jobs.end(); ++it) {
		if ((*it)->id == id && (*it)->state != CONV_INPROGRESS) {
			ConvertJob *job = *it;
			s_jobs.erase(it);
			Notify_ConvertRemoveJobInfo(id);
			delete job;
			break;
		}
	}
}

void CPartFileConvert::RetryJob(unsigned id)
{
	wxMutexLocker lock(s_mutex);
	for (std::list<ConvertJob*>::iterator it = s_jobs.begin(); it != s_jobs.end(); ++it) {
		if ((*it)->id == id && (*it)->state != CONV_INPROGRESS && (*it)->state != CONV_OK) {
			(*it)->state = CONV_QUEUE;
			Notify_ConvertUpdateJobInfo(*it);
			StartThread();
			break;
		}
	}
}

void CPartFileConvert::ReaddAllJobs()
{
	wxMutexLocker lock(s_mutex);
	for (std::list<ConvertJob*>::iterator it = s_jobs.begin(); it != s_jobs.end(); ++it) {
		Notify_ConvertUpdateJobInfo(*it);
	}
}
