#include "Duplicator.h"

#include <algorithm>
#include <locale>

Duplicator::Duplicator( void )
{
}

void Duplicator::SetSourceDirectory( const wchar_t* pDirPath )
{
	Duplicator::ModifyPath( true, false, pDirPath, m_SourceDirectoryPath );
}

const wchar_t* Duplicator::GetSourceDirectory( void ) const
{
	return ( m_SourceDirectoryPath.size() > 0 )? &( m_SourceDirectoryPath[0] ) : L"";
}

void Duplicator::SetTargetDirectory( const wchar_t* pDirPath )
{
	Duplicator::ModifyPath( true, false, pDirPath, m_TargetDirectoryPath );
}

const wchar_t* Duplicator::GetTargetDirectory( void ) const
{
	return ( m_TargetDirectoryPath.size() > 0 )? &( m_TargetDirectoryPath[0] ) : L"";
}

void Duplicator::AddExcludeDirectory( const wchar_t* pDirPath, bool bFileOnly )
{
	if( ( pDirPath == NULL ) ||
		( ::wcslen( pDirPath ) == 0 ) )
	{
		return;
	}

	Duplicator::EXCL_DIR_INFO info;

	Duplicator::ModifyPath( true, false, pDirPath, info.key );
	info.bFileOnly = bFileOnly;

	m_ExcludeDirectoryList.push_back( info );
}

void Duplicator::AddExcludeFile( const wchar_t* pFilePath, bool bNameOnly )
{
	if( ( pFilePath == NULL ) ||
		( ::wcslen( pFilePath ) == 0 ) )
	{
		return;
	}

	Duplicator::EXCL_FILE_INFO info;

	Duplicator::ModifyPath( false, true, pFilePath, info.key );
	info.bNameOnly = bNameOnly;

	m_ExcludeFileList.push_back( info );
}

void Duplicator::AddExcludeFileExt( const wchar_t* pFileExt )
{
	if( ( pFileExt == NULL ) ||
		( ::wcslen( pFileExt ) == 0 ) )
	{
		return;
	}

	std::wstring temp;

	if( pFileExt[0] != L'.' )
	{
		temp = L".";
	}
	else
	{
		temp = L"";
	}

	Duplicator::ModifyPath( false, true, pFileExt, temp );

	m_ExcludeFileExtList.push_back( temp );
}

bool Duplicator::IsAvailable( void ) const
{
	return ( m_SourceDirectoryPath.size() > 0 ) && ( m_TargetDirectoryPath.size() > 0 );
}

bool Duplicator::Execute( void )
{
	if( ( m_SourceDirectoryPath.size() == 0 ) ||
		( m_TargetDirectoryPath.size() == 0 ) )
	{
		return false;
	}

	unsigned long long workCount = 0;

	Duplicator::FileInfoMap srcFileInfoMap;
	Duplicator::DirectoryInfoList srcDirInfoList;

	Duplicator::FileInfoMap tgtFileInfoMap;
	Duplicator::DirectoryInfoList tgtDirInfoList;

	////////////////////////////////////////////////////////////////////////////////////////////////////
	// ソースディレクトリを解析
	////////////////////////////////////////////////////////////////////////////////////////////////////

	if( Duplicator::CreateFileInfoMap( m_SourceDirectoryPath, L"", srcFileInfoMap, srcDirInfoList, true ) == false )
	{
		::wprintf( L" Error! : ソースディレクトリの解析中にエラーが発生しました : DirectoryName[\"%s\"]\n", m_SourceDirectoryPath.c_str() );
		return false;
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	// ターゲットディレクトリを解析
	////////////////////////////////////////////////////////////////////////////////////////////////////

	if( Duplicator::CreateFileInfoMap( m_TargetDirectoryPath, L"", tgtFileInfoMap, tgtDirInfoList, false ) == false )
	{
		::wprintf( L" Error! : ターゲットディレクトリの解析中にエラーが発生しました : DirectoryName[\"%s\"]\n", m_TargetDirectoryPath.c_str() );
		return false;
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	// ターゲットディレクトリの不要なファイルを削除
	////////////////////////////////////////////////////////////////////////////////////////////////////

	{
		Duplicator::FileInfoMap::iterator it_tgt_begin = tgtFileInfoMap.begin();
		Duplicator::FileInfoMap::iterator it_tgt_end = tgtFileInfoMap.end();
		Duplicator::FileInfoMap::iterator it_tgt;

		Duplicator::FileInfoMap::iterator it_src_end = srcFileInfoMap.end();

		for( it_tgt = it_tgt_begin; it_tgt != it_tgt_end; ++it_tgt )
		{
			const std::wstring& key = it_tgt->first;
			const Duplicator::FILE_INFO& fileInfo = it_tgt->second;

			Duplicator::FileInfoMap::iterator it_src = srcFileInfoMap.find( key );
			if( it_src == it_src_end )
			{
				const std::wstring& fileName = fileInfo.name;
				std::wstring fullFileName = m_TargetDirectoryPath + fileName;

				if( TrashFile( fullFileName.c_str() ) == true )
				{
					::wprintf( L" Delete : File[\"%s\"]\n", fileName.c_str() );
					workCount++;
				}
				else
				{
					::wprintf( L" Error! : Delete : File[\"%s\"]\n", fileName.c_str() );
					return false;
				}
			}
		}
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	// ターゲットディレクトリの不要なディレクトリを削除
	////////////////////////////////////////////////////////////////////////////////////////////////////

	{
		Duplicator::DirectoryInfoList::iterator it_tgt_begin = tgtDirInfoList.begin();
		Duplicator::DirectoryInfoList::iterator it_tgt_end = tgtDirInfoList.end();
		Duplicator::DirectoryInfoList::iterator it_tgt;

		Duplicator::DirectoryInfoList::iterator it_src_begin = srcDirInfoList.begin();
		Duplicator::DirectoryInfoList::iterator it_src_end = srcDirInfoList.end();
		Duplicator::DirectoryInfoList::iterator it_src;

		for( it_tgt = it_tgt_begin; it_tgt != it_tgt_end; ++it_tgt )
		{
			const Duplicator::DIR_INFO& dirInfo = ( *it_tgt );

			it_src = std::find( it_src_begin, it_src_end, dirInfo );
			if( it_src == it_src_end )
			{
				const std::wstring& dirName = dirInfo.name;
				std::wstring fullDirName = m_TargetDirectoryPath + dirName;

				if( Duplicator::DeleteDirectory( fullDirName ) == true )
				{
					::wprintf( L" Delete : Directory[\"%s\"]\n", dirName.c_str() );
					workCount++;
				}
				else
				{
					::wprintf( L" Error! : Delete : Directory[\"%s\"]\n", dirName.c_str() );
					return false;
				}
			}
		}
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	// ターゲットディレクトリに必要なディレクトリを作成
	////////////////////////////////////////////////////////////////////////////////////////////////////

	{
		Duplicator::DirectoryInfoList::iterator it_src_begin = srcDirInfoList.begin();
		Duplicator::DirectoryInfoList::iterator it_src_end = srcDirInfoList.end();
		Duplicator::DirectoryInfoList::iterator it_src;

		Duplicator::DirectoryInfoList::iterator it_tgt_begin = tgtDirInfoList.begin();
		Duplicator::DirectoryInfoList::iterator it_tgt_end = tgtDirInfoList.end();
		Duplicator::DirectoryInfoList::iterator it_tgt;

		for( it_src = it_src_begin; it_src != it_src_end; ++it_src )
		{
			const Duplicator::DIR_INFO& dirInfo = ( *it_src );

			it_tgt = std::find( it_tgt_begin, it_tgt_end, dirInfo );
			if( it_tgt == it_tgt_end )
			{
				const std::wstring& dirName = dirInfo.name;
				std::wstring fullDirName = m_TargetDirectoryPath + dirName;
				
				if( ::CreateDirectory( fullDirName.c_str(), NULL ) == TRUE )
				{
					::wprintf( L" New    : Directory[\"%s\"]\n", dirName.c_str() );
					workCount++;
				}
				else
				{
					::wprintf( L" Error! : New    : Directory[\"%s\"]\n", dirName.c_str() );
					return false;
				}
			}
		}
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	// ターゲットディレクトリにファイルをコピー
	////////////////////////////////////////////////////////////////////////////////////////////////////

	{
		Duplicator::FileInfoMap::iterator it_src_begin = srcFileInfoMap.begin();
		Duplicator::FileInfoMap::iterator it_src_end = srcFileInfoMap.end();
		Duplicator::FileInfoMap::iterator it_src;

		Duplicator::FileInfoMap::iterator it_tgt_begin = tgtFileInfoMap.begin();
		Duplicator::FileInfoMap::iterator it_tgt_end = tgtFileInfoMap.end();
		Duplicator::FileInfoMap::iterator it_tgt;

		std::wstring srcFileName;
		std::wstring tgtFileName;

		for( it_src = it_src_begin; it_src != it_src_end; ++it_src )
		{
			const std::wstring& key = it_src->first;
			const std::wstring& fileName = it_src->second.name;

			bool bNewFile;
			bool bCopyFile;

			srcFileName = m_SourceDirectoryPath + fileName;
			tgtFileName = m_TargetDirectoryPath + fileName;

			it_tgt = tgtFileInfoMap.find( key );
			if( it_tgt != it_tgt_end )
			{
				const FILETIME* pSrcTime = &( it_src->second.time );
				const FILETIME* pTgtTime = &( it_tgt->second.time );

				LONG comp = CompareFileTime( pTgtTime, pSrcTime );

				if( comp == 0 )
				{
					bCopyFile = false;
				}
				else if( comp > 0 )
				{
					//ターゲットのファイルのほうが新しい
					wchar_t yn = L'n';

					::wprintf( L" ソースよりターゲットのファイル \"%s\" の更新日時が新しいです。\n", fileName.c_str() );
					::wprintf( L" 上書きしてもよろしいですか？[y/n]\n" );

					do
					{
						yn = _getwch();
					}
					while( ( yn != L'y' ) && ( yn != L'Y' ) && ( yn != L'n' ) && ( yn != L'N' ) );

					bCopyFile = ( ( yn == 'y' ) || ( yn == 'Y' ) );
				}
				else
				{
					bCopyFile = true;
				}
/*
				WORD secDiff = ( tgtTime.wSecond > srcTime.wSecond )? ( tgtTime.wSecond - srcTime.wSecond ) : ( srcTime.wSecond - tgtTime.wSecond );

				if( ( tgtTime.wYear == srcTime.wYear ) &&
					( tgtTime.wMonth == srcTime.wMonth ) &&
					( tgtTime.wDay == srcTime.wDay ) &&
					( tgtTime.wHour == srcTime.wHour ) &&
					( tgtTime.wMinute == srcTime.wMinute ) &&
					( secDiff <= 2 ) )
				{
					bCopyFile = false;
				}
				else
				{
					if( ( tgtTime.wYear > srcTime.wYear ) ||
						( tgtTime.wMonth > srcTime.wMonth ) ||
						( tgtTime.wDay > srcTime.wDay ) ||
						( tgtTime.wHour > srcTime.wHour ) ||
						( tgtTime.wMinute > srcTime.wMinute ) ||
						( ( secDiff > 2 ) && ( tgtTime.wSecond > srcTime.wSecond ) ) )
					{
						//ターゲットのファイルのほうが新しい
						wchar_t yn = L'n';

						::wprintf( L" ソースよりターゲットのファイル \"%s\" の更新日時が新しいです。\n", fileName.c_str() );
						::wprintf( L" 上書きしてもよろしいですか？[y/n]\n" );

						do
						{
							yn = _getwch();
						}
						while( ( yn != L'y' ) && ( yn != L'Y' ) && ( yn != L'n' ) && ( yn != L'N' ) );

						bCopyFile = ( ( yn == 'y' ) || ( yn == 'Y' ) );
					}
					else
					{
						bCopyFile = true;
					}
				}
*/
				bNewFile = false;
			}
			else
			{
				bNewFile = true;
				bCopyFile = true;
			}

			if( bCopyFile == true )
			{
				if( ::CopyFile( srcFileName.c_str(), tgtFileName.c_str(), FALSE ) == TRUE )
				{
					::wprintf( L" %s : File[\"%s\"]\n", ( bNewFile == true )? L"New   " : L"Update", fileName.c_str() );
					workCount++;
				}
				else
				{
					::wprintf( L" Error! : %s : File[\"%s\"]\n", ( bNewFile == true )? L"New   " : L"Update", fileName.c_str() );
					return false;
				}
			}
		}
	}

	if( workCount == 0 )
	{
		::wprintf( L" 更新はありません。\n" );
	}

	return true;
}

wchar_t Duplicator::PathToKey( wchar_t ch )
{
	if( ch == L'/' )
	{
		return L'\\';
	}

	return ::towlower( ch );
}

void Duplicator::ModifyPath( bool bDirectory, bool bAppend, const wchar_t* pSource, std::wstring& target )
{
	if( ( pSource == NULL ) ||
		( ::wcslen( pSource ) == 0 ) )
	{
		if( bAppend == false )
		{
			target = L"";
		}
	}
	else
	{
		if( bAppend == true )
		{
			target += pSource;
		}
		else
		{
			target = pSource;
		}

		std::transform( target.begin(), target.end(), target.begin(), Duplicator::PathToKey );

		if( ( bDirectory == true ) &&
			( target[target.size() - 1] != L'\\' ) )
		{
			target += L'\\';
		}
	}
}

void Duplicator::SetDirectoryName( const wchar_t* pSource, std::wstring& target )
{
	if( ( pSource == NULL ) ||
		( ::wcslen( pSource ) == 0 ) )
	{
		target = L"";
	}
	else
	{
		wchar_t lastChar = pSource[::wcslen( pSource ) - 1];

		target = pSource;

		if( ( lastChar != L'\\' ) &&
			( lastChar != L'/' ) )
		{
			target += L'\\';
		}
	}
}

bool Duplicator::TrashFile( const std::wstring& fileName )
{
	size_t pathSize = fileName.size() + 2;

	wchar_t* path = new wchar_t[pathSize];
	if( path != NULL )
	{
		::wcscpy_s( path, pathSize, fileName.c_str() );
		path[pathSize - 1] = NULL;
	}
	else
	{
		return false;
	}

	SHFILEOPSTRUCT sf;
	int result;

	ZeroMemory( &sf, sizeof( sf ) );
	sf.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT;
	sf.wFunc = FO_DELETE;
	sf.pFrom = path;

	result = ::SHFileOperation( &sf );

	delete [] path;

	return ( result == 0 );
}

bool Duplicator::DeleteDirectory( const std::wstring& dirName )
{
	std::wstring search = dirName + L"*.*";

	bool result = true;

	WIN32_FIND_DATA wfd;
	HANDLE hFind;

	ZeroMemory( &wfd, sizeof( wfd ) );

	hFind = ::FindFirstFile( search.c_str(), &wfd );
	if( hFind != INVALID_HANDLE_VALUE )
	{
		do
		{
			if( ( ::wcscmp( wfd.cFileName, L"." ) != 0 ) &&
				( ::wcscmp( wfd.cFileName, L".." ) != 0 ) )
			{
				if( ( wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) == FILE_ATTRIBUTE_DIRECTORY )
				{
					std::wstring nextDirName = dirName + wfd.cFileName + L"\\";

					result = DeleteDirectory( nextDirName );
				}
			}
		}
		while( ( result == true ) && ( ::FindNextFile( hFind, &wfd ) == TRUE ) );

		::FindClose( hFind );

		if( result == true )
		{
			if( ::RemoveDirectory( dirName.c_str() ) == FALSE )
			{
				result = false;
			}
		}
	}

	return result;
}

bool Duplicator::CreateFileInfoMap( const std::wstring& baseDirName,
									const std::wstring& dirName,
									Duplicator::FileInfoMap& fileInfoMap,
									Duplicator::DirectoryInfoList& dirNameList,
									bool bExcludeEnabled )
{
	std::wstring search = baseDirName + dirName + L"*.*";

	bool result = true;

	WIN32_FIND_DATA wfd;
	HANDLE hFind;

	ZeroMemory( &wfd, sizeof( wfd ) );

	hFind = ::FindFirstFile( search.c_str(), &wfd );
	if( hFind != INVALID_HANDLE_VALUE )
	{
		do
		{
			if( ( ::wcscmp( wfd.cFileName, L"." ) != 0 ) &&
				( ::wcscmp( wfd.cFileName, L".." ) != 0 ) )
			{
				if( ( wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) == FILE_ATTRIBUTE_DIRECTORY )
				{
					std::wstring nextDirName = dirName + wfd.cFileName + L"\\";
					std::wstring key;

					Duplicator::ModifyPath( true, false, nextDirName.c_str(), key );

					if( bExcludeEnabled == true )
					{
						switch( IsExcludeDirectory( key.c_str() ) )
						{
						case Duplicator::EDR_OK:
							dirNameList.push_back( Duplicator::DIR_INFO( key, nextDirName ) );
							result = Duplicator::CreateFileInfoMap( baseDirName, nextDirName, fileInfoMap, dirNameList, bExcludeEnabled );
							break;
						case Duplicator::EDR_EXCLUDE_FILE:
							dirNameList.push_back( Duplicator::DIR_INFO( key, nextDirName ) );
							break;
						}
					}
					else
					{
						dirNameList.push_back( Duplicator::DIR_INFO( key, nextDirName ) );
						result = Duplicator::CreateFileInfoMap( baseDirName, nextDirName, fileInfoMap, dirNameList, bExcludeEnabled );
					}
				}
				else
				{
					std::wstring fileName = dirName + wfd.cFileName;
					std::wstring key;

					Duplicator::ModifyPath( false, false, fileName.c_str(), key );

					if(	( bExcludeEnabled == false ) ||
						( IsExcludeFile( key.c_str() ) == false ) )
					{
						std::wstring fullFileName = baseDirName + fileName;

						HANDLE hFile = ::CreateFile( fullFileName.c_str(), GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
						if( hFile != INVALID_HANDLE_VALUE )
						{
							FILETIME fileTime;

							ZeroMemory( &fileTime, sizeof( fileTime ) );

							if( ::GetFileTime( hFile, NULL, NULL, &fileTime ) == TRUE )
							{
								fileInfoMap.insert( Duplicator::FileInfoMap::value_type( key, Duplicator::FILE_INFO( fileName, fileTime ) ) );
							}
							else
							{
								result = false;
							}

							::CloseHandle( hFile );
						}
						else
						{
							result = false;
						}
					}
				}
			}
		}
		while( ( result == true ) && ( ::FindNextFile( hFind, &wfd ) == TRUE ) );

		::FindClose( hFind );
	}

	return result;
}

Duplicator::EXCL_DIR_RESULT Duplicator::IsExcludeDirectory( const wchar_t* pKey )
{
	Duplicator::DirectoryList::iterator it_begin = m_ExcludeDirectoryList.begin();
	Duplicator::DirectoryList::iterator it_end = m_ExcludeDirectoryList.end();
	Duplicator::DirectoryList::iterator it;

	for( it = it_begin; it != it_end; ++it )
	{
		const Duplicator::EXCL_DIR_INFO& info = ( *it );

		if( info.key == pKey )
		{
			return ( info.bFileOnly == false )? Duplicator::EDR_EXCLUDE : Duplicator::EDR_EXCLUDE_FILE;
		}
	}

	return Duplicator::EDR_OK;
}

bool Duplicator::IsExcludeFile( const wchar_t* pKey )
{
	size_t keyLen = ::wcslen( pKey );

	////////////////////////////////////////////////////////////////////////////////////////////////////
	// ファイルの名前で検索
	////////////////////////////////////////////////////////////////////////////////////////////////////

	Duplicator::FileList::iterator it_fn_begin = m_ExcludeFileList.begin();
	Duplicator::FileList::iterator it_fn_end = m_ExcludeFileList.end();
	Duplicator::FileList::iterator it_fn;

	for( it_fn = it_fn_begin; it_fn != it_fn_end; ++it_fn )
	{
		const Duplicator::EXCL_FILE_INFO& info = ( *it_fn );

		if( info.bNameOnly == true )
		{
			size_t nameLen = info.key.size();

			if( ( keyLen >= nameLen ) &&
				( ::wcscmp( &( pKey[keyLen - nameLen] ), info.key.c_str() ) == 0 ) )
			{
				return true;
			}
		}
		else
		{
			if( info.key == pKey )
			{
				return true;
			}
		}
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	// ファイルの拡張子で検索
	////////////////////////////////////////////////////////////////////////////////////////////////////

	Duplicator::FileExtList::iterator it_fe_begin = m_ExcludeFileExtList.begin();
	Duplicator::FileExtList::iterator it_fe_end = m_ExcludeFileExtList.end();
	Duplicator::FileExtList::iterator it_fe;

	for( it_fe = it_fe_begin; it_fe != it_fe_end; ++it_fe )
	{
		const std::wstring& fileExt = ( *it_fe );
		size_t extLen = fileExt.size();

		if( keyLen <= extLen )
		{
			continue;
		}

		if( ::wcscmp( &( pKey[keyLen - extLen] ), &( fileExt[0] ) ) == 0 )
		{
			return true;
		}
	}

	return false;
}
