#include "stdafx.h"
#include "ClipEditThread.h"
#include "Options.h"
#include "Misc.h"
#include "Clip.h"
#include "CP_Main.h"
#include "ConvertRTFToText.h"
#include "..\Shared\TextConvert.h"

#define EVENT_FILE_CHANGED 1

#define MAX_TIMEOUT 86400

CClipEditThread::CClipEditThread()
{
	m_folderHandle = INVALID_HANDLE_VALUE;
	m_threadName = _T("ClipEditTrackingThread");
	m_waitTimeout = MAX_TIMEOUT;
}

CClipEditThread::~CClipEditThread()
{
	Close();
}

void CClipEditThread::Close()
{
	Stop();
		
	if (m_folderHandle != INVALID_HANDLE_VALUE)
	{
		CloseHandle(m_folderHandle);
		m_folderHandle = INVALID_HANDLE_VALUE;
	}

	RemoveEvent(EVENT_FILE_CHANGED);
	m_overlapped.hEvent = INVALID_HANDLE_VALUE;	

	CString editClipFolder = CGetSetOptions::GetPath(PATH_EDIT_CLIPS);
	DeleteFolderFiles(editClipFolder, TRUE, CTimeSpan(7, 0, 0, 0));
}

void CClipEditThread::StartWatchingFolderForChanges()
{
	CString editClipFolder = CGetSetOptions::GetPath(PATH_EDIT_CLIPS);

	m_folderHandle = CreateFileW(editClipFolder, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL);
	
	m_overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
	AddEvent(EVENT_FILE_CHANGED, m_overlapped.hEvent);	

	RefreshWatch();		

	Start();
}

void CClipEditThread::WatchFile(CString filePath)
{
	ATL::CCritSecLock csLock(m_fileEditsLock.m_sect);

	nsPath::CPath path(filePath);

	//start the edit count at 0, the first edit notification is us saving the file, after that handle the file change
	m_fileEditStarts[path.GetName()] = CTime::GetCurrentTime();
}

void CClipEditThread::RefreshWatch()
{
	memset(m_fileChangeBuffer, 0, sizeof(m_fileChangeBuffer));

	DWORD bytesReturned = 0;
	ReadDirectoryChangesW(m_folderHandle, m_fileChangeBuffer, sizeof(m_fileChangeBuffer), FALSE, 
		FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_SIZE, 
		&bytesReturned, &m_overlapped, NULL);
}

void CClipEditThread::OnTimeOut(void* param)
{
	if (m_waitTimeout == MAX_TIMEOUT)
	{
		CString editClipFolder = CGetSetOptions::GetPath(PATH_EDIT_CLIPS);
		DeleteFolderFiles(editClipFolder, TRUE, CTimeSpan(7, 0, 0, 0));

		//cleanup up the list of edits that we started, after 7 days just keeps the list from growing too large, hopefull they don't have millions of edits in 7 days
		for (auto it = m_fileEditStarts.begin(); it != m_fileEditStarts.end();)
		{
			auto diff = CTime::GetCurrentTime() - it->second;
			if (diff.GetTotalSeconds() > (86400 * 7))
			{
				it = m_fileEditStarts.erase(it);
			}
			else
			{
				it++;
			}
		}		
	}
	else
	{
		for (auto const& toSave : m_filesToSave)
		{
			CString fileName = toSave.first;
			CString editClipFileName = _T("EditClip_");
			CString newClipFileName = _T("NewClip_");

			if (fileName.Find(editClipFileName, 0) == 0)
			{
				nsPath::CPath path(fileName);
				CString idString = path.GetTitle().Mid(editClipFileName.GetLength());

				int id = _wtoi(idString);
				if (id > 0)
				{
					SaveToClip(fileName, id);
				}
			}
			else if (fileName.Find(newClipFileName, 0) == 0)
			{
				SaveToClip(fileName, -1);
			}
		}

		m_filesToSave.clear();
		m_waitTimeout = MAX_TIMEOUT;
	}
}

void CClipEditThread::OnEvent(int eventId, void* param)
{
	switch (eventId)
	{
		case EVENT_FILE_CHANGED:
		{
			OnFileChanged();
			break;
		}
	}
}

void CClipEditThread::OnFileChanged()
{
	FILE_NOTIFY_INFORMATION* pNotify = m_fileChangeBuffer;
	int loopCount = 0;
	bool fileModified = false;
	CString editClipFileName = _T("EditClip_");
	CString newClipFileName = _T("NewClip_");

	while (true)
	{
		CString fileName(pNotify->FileName, pNotify->FileNameLength / sizeof(WCHAR));
				
		//we can't filter by the action, modify as ms word doesn't modify the file they replace it
		///so just look for anything that matches our file names

		if (pNotify->Action != FILE_ACTION_ADDED && pNotify->Action != FILE_ACTION_REMOVED)
		{
			bool addToChanges = true;			

			{
				ATL::CCritSecLock csLock(m_fileEditsLock.m_sect);
				auto exists = m_fileEditStarts.find(fileName);
				if (exists != m_fileEditStarts.end())
				{
					auto startEdit = m_fileEditStarts[fileName];
					auto diff = CTime::GetCurrentTime() - startEdit;
					if (diff.GetTotalSeconds() < CGetSetOptions::m_clipEditSaveDelayAfterLoadSeconds)
					{
						Log(StrF(_T("%s has changed close to when we started editing the file, diff: %lld, limit: %d, not handling change"), fileName, diff.GetTotalSeconds(), CGetSetOptions::m_clipEditSaveDelayAfterLoadSeconds));
						addToChanges = false;
					}
				}
				else //not in our list of files we initiated the change with
				{
					if (fileName.Find(newClipFileName, 0) == 0)
					{
						Log(StrF(_T("New clip file changed: %s, this was not in Ditto list of files we initiated the change for, not handling change"), fileName));						
						addToChanges = false;
					}
				}
			}

			if (fileName.Find(editClipFileName, 0) == -1 && fileName.Find(newClipFileName, 0) == -1)
			{
				addToChanges = false;
				Log(StrF(_T("File %s is not a Ditto file of format EditClip or NewClip, not handling change"), fileName));
			}

			if (addToChanges)
			{				
				Log(StrF(_T("%s file changed, adding to list to be saved back to Ditto"), fileName));
				m_filesToSave[fileName] = true;
				fileModified = true;				
			}
		}

		if (pNotify->NextEntryOffset <= 0 || loopCount > 1000)
		{
			break;
		}

		pNotify = (FILE_NOTIFY_INFORMATION*)((BYTE*)pNotify + pNotify->NextEntryOffset);

		if (pNotify == nullptr)
		{
			break;
		}

		loopCount++;
	}

	RefreshWatch();

	if (fileModified)
	{
		m_waitTimeout = CGetSetOptions::m_clipEditSaveDelayAfterSaveSeconds * 1000;
	}
}

bool CClipEditThread::SaveToClip(CString filePath, int id)
{
	bool savedClip = false;

	Log(StrF(_T("ClipFile: %s, ClipId: %d, has changed saving back to Ditto"), filePath, id));

	if (id < 0)
	{
		auto exists = m_newClipIds.find(filePath);
		if (exists != m_newClipIds.end())
		{
			id = m_newClipIds[filePath];
		}
	}

	CClip clip;
	if (id >= 0)
	{
		if (clip.LoadMainTable(id) == FALSE)
		{
			Log(StrF(_T("Error loading clip id: %d, not saving"), id));
			return false;
		}

		clip.LoadFormats(id);
	}			

	CString unicodeText;
	CStringA utf8Text;
	bool unicode = false;
	std::vector<BYTE> cf_dibBytes;
	std::vector<BYTE> pngBytes;
	
	CString editClipFolder = CGetSetOptions::GetPath(PATH_EDIT_CLIPS);
	CString fullFilePath = editClipFolder + filePath;

	nsPath::CPath path(filePath);
	auto extenstion = path.GetExtension().MakeLower();

	if (extenstion == _T("png") || extenstion == _T("bmp"))
	{
		if (ReadImageFile(fullFilePath, cf_dibBytes, pngBytes) == false)
		{
			Log(StrF(_T("Error reading image file %s, clip id: %d, not saving"), fullFilePath, id));
			return false;
		}
	}
	else if (ReadFile(fullFilePath, unicode, unicodeText, utf8Text) == false)
	{
		Log(StrF(_T("Error reading text file %s, clip id: %d, not saving"), fullFilePath, id));
		return false;
	}

	if (id < 0 &&
		unicodeText == _T("") &&
		utf8Text == "" &&
		cf_dibBytes.size() <= 0 &&
		pngBytes.size() <= 0)
	{
		Log(StrF(_T("Not saving new clip that is empty, no text or image bytes, path: %s, clip id: %d, not saving"), fullFilePath, id));
		return false;
	}	

	BOOL modifyDescription = CGetSetOptions::GetUpdateDescWhenSavingClip();
	
	if (extenstion == _T("bmp") || extenstion == _T("png"))
	{		
		clip.SaveFormats(nullptr, nullptr, nullptr, modifyDescription, &cf_dibBytes, &pngBytes);				
	}
	else if (extenstion == _T("txt"))
	{
		if (unicode)
		{
			clip.SaveFormats(&unicodeText, nullptr, nullptr, modifyDescription);
		}
		else
		{
			unicodeText = CTextConvert::Utf8ToUnicode(utf8Text);
			clip.SaveFormats(&unicodeText, nullptr, nullptr, modifyDescription);
		}
	}
	else if (extenstion == _T("rtf"))
	{
		if (GetTextFromRTF(utf8Text, unicodeText))
		{
			clip.SaveFormats(&unicodeText, nullptr, &utf8Text, modifyDescription);
		}
		else
		{
			clip.SaveFormats(nullptr, nullptr, &utf8Text, modifyDescription);			
		}
	}

	//refresh the clip in the UI
	if (id == -1)
	{
		m_newClipIds[filePath] = clip.m_id;
		theApp.RefreshView(CopyReasonEnum::COPY_TO_UNKOWN);
	}
	else if (id > 0)
	{
		theApp.RefreshClipInUI(id, UPDATE_CLIP_DESCRIPTION);
	}			

	savedClip = true;	

	return savedClip;
}

bool CClipEditThread::ReadFile(CString filePath, bool &unicode, CString &unicodeText, CStringA &utf8Text)
{
	CFile file;
	CFileException ex;
	if (!file.Open(filePath, CFile::modeRead | CFile::typeBinary | CFile::shareDenyNone, &ex))
	{
		CString error;
		ex.GetErrorMessage(error.GetBufferSetLength(200), 200);
		error.ReleaseBuffer();
		log(StrF(_T("LoadFormatsFromFile - Error opening file: %s, Error: %s\r\n"), filePath, error));
		return false;
	}

	if (file.GetLength() >= 2)
	{
		wchar_t header;
		file.Read(&header, sizeof(wchar_t));
		if (header == 0xFEFF)
		{
			unicode = true;
		}
		else
		{
			file.SeekToBegin();
		}
	}

	if (unicode)
	{
		const UINT bufferSize = (UINT)((file.GetLength() - 2) / 2);
		file.Read(unicodeText.GetBufferSetLength(bufferSize), bufferSize * 2);
		unicodeText.ReleaseBuffer();
	}
	else
	{
		const UINT bufferSize = (UINT)(file.GetLength());
		file.Read(utf8Text.GetBufferSetLength(bufferSize), bufferSize);
		utf8Text.ReleaseBuffer();
	}

	return true;
}

std::vector<BYTE> CImageToPNGBytes(const CImage& image, REFGUID guidFileType)
{
	IStream* pStream = nullptr;
	HRESULT hr = CreateStreamOnHGlobal(nullptr, TRUE, &pStream);
	if (FAILED(hr)) {
		return {};
	}

	ULARGE_INTEGER ulSize;

	hr = image.Save(pStream, guidFileType);
	if (FAILED(hr)) {
		pStream->Release();
		return {};
	}

	LARGE_INTEGER liZero = { 0 };
	hr = pStream->Seek(liZero, STREAM_SEEK_SET, nullptr);
	if (FAILED(hr)) {
		pStream->Release();
		return {};
	}

	hr = pStream->Seek({ 0 }, STREAM_SEEK_END, &ulSize);
	if (FAILED(hr)) {
		pStream->Release();
		return {};
	}

	std::vector<BYTE> pngBytes((UINT)ulSize.QuadPart);

	hr = pStream->Seek(liZero, STREAM_SEEK_SET, nullptr);
	if (FAILED(hr)) {
		pStream->Release();
		return {};
	}

	hr = pStream->Read(pngBytes.data(), (UINT)ulSize.QuadPart, nullptr);
	pStream->Release();

	if (FAILED(hr)) {
		return {};
	}
	return pngBytes;
}

bool CClipEditThread::ReadImageFile(CString path, std::vector<BYTE> &cf_dibBytes, std::vector<BYTE> & pngBytes)
{
	CImage image;
	HRESULT hr = image.Load(path);
	if (FAILED(hr)) 
	{
		Log(StrF(_T("Failed to load image, %s"), path));
		return false;
	}

	pngBytes = CImageToPNGBytes(image, Gdiplus::ImageFormatPNG);	

	return true;
}

BOOL CClipEditThread::GetTextFromRTF(CStringA rtf, CString &unicodeText)
{
	CConvertRTFToText cc;
	if (cc.Create())
	{
		unicodeText = cc.GetTextFromRTF(rtf);
		cc.DestroyWindow();

		if (rtf != "" && unicodeText == "")
		{
			log(StrF(_T("Failed to convert rtf to text, rtf text is not empty but text is empty")));
		}

		return true;
	}
	else
	{
		log(StrF(_T("Failed to create rtf to text window")));
	}	

	return false;
}