/*
 * FileDialog.cpp
 * --------------
 * Purpose: File and folder selection dialogs implementation.
 * Notes  : (currently none)
 * Authors: OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */


#include "stdafx.h"
#include "FileDialog.h"
#include "Mainfrm.h"
#include "InputHandler.h"


OPENMPT_NAMESPACE_BEGIN

class CFileDialogEx : public CFileDialog
{
public:
	CFileDialogEx(bool bOpenFileDialog,
		LPCTSTR lpszDefExt,
		LPCTSTR lpszFileName,
		DWORD dwFlags,
		LPCTSTR lpszFilter,
		CWnd *pParentWnd,
		DWORD dwSize,
		BOOL bVistaStyle,
		bool preview)
		: CFileDialog(bOpenFileDialog ? TRUE : FALSE, lpszDefExt, lpszFileName, dwFlags, lpszFilter, pParentWnd, dwSize, bVistaStyle)
		, m_fileNameBuf(65536)
		, doPreview(preview)
		, played(false)
	{
		// MFC's filename buffer is way too small for multi-selections of a large number of files.
		_tcsncpy(m_fileNameBuf.data(), lpszFileName, m_fileNameBuf.size());
		m_fileNameBuf.back() = '\0';
		m_ofn.lpstrFile = m_fileNameBuf.data();
		m_ofn.nMaxFile = mpt::saturate_cast<DWORD>(m_fileNameBuf.size());
	}

	~CFileDialogEx()
	{
		if(played)
		{
			CMainFrame::GetMainFrame()->StopPreview();
		}
	}

#if NTDDI_VERSION >= NTDDI_VISTA
	// MFC's AddPlace() is declared as throw() but can in fact throw if any of the COM calls fail, e.g. because the place does not exist.
	// Avoid this by re-implementing our own version which doesn't throw.
	void AddPlace(const mpt::PathString &path)
	{
		if(m_bVistaStyle && path.IsDirectory())
		{
			CComPtr<IShellItem> shellItem;
			HRESULT hr = SHCreateItemFromParsingName(path.ToWide().c_str(), nullptr, IID_IShellItem, reinterpret_cast<void **>(&shellItem));
			if(SUCCEEDED(hr))
			{
				static_cast<IFileDialog*>(m_pIFileDialog)->AddPlace(shellItem, FDAP_TOP);
			}
		}
	}
#endif

protected:
	std::vector<TCHAR> m_fileNameBuf;
	CString oldName;
	bool doPreview, played;

	void OnFileNameChange() override
	{
		if(doPreview)
		{
			CString name = GetPathName();
			if(!name.IsEmpty() && name != oldName)
			{
				oldName = name;
				if(CMainFrame::GetMainFrame()->PlaySoundFile(mpt::PathString::FromCString(name), NOTE_MIDDLEC))
				{
					played = true;
				}
			}
		}
		CFileDialog::OnFileNameChange();
	}
};


// Display the file dialog.
bool FileDialog::Show(CWnd *parent)
{
	m_filenames.clear();

	// First, set up the dialog...
	CFileDialogEx dlg(m_load,
		m_defaultExtension.empty() ? nullptr : m_defaultExtension.c_str(),
		m_defaultFilename.c_str(),
		OFN_EXPLORER | OFN_NOCHANGEDIR | OFN_HIDEREADONLY | OFN_ENABLESIZING | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | (m_multiSelect ? OFN_ALLOWMULTISELECT : 0) | (m_load ? 0 : (OFN_OVERWRITEPROMPT | OFN_NOREADONLYRETURN)),
		m_extFilter.c_str(),
		parent != nullptr ? parent : CMainFrame::GetMainFrame(),
		0,
		(mpt::OS::Windows::IsWine() || mpt::OS::Windows::Version::Current().IsBefore(mpt::OS::Windows::Version::WinVista)) ? FALSE : TRUE,
		m_preview && TrackerSettings::Instance().previewInFileDialogs);
	OPENFILENAME &ofn = dlg.GetOFN();
	ofn.nFilterIndex = m_filterIndex != nullptr ? *m_filterIndex : 0;
	if(!m_workingDirectory.empty())
	{
		ofn.lpstrInitialDir = m_workingDirectory.c_str();
	}
#if NTDDI_VERSION >= NTDDI_VISTA
	const auto places =
	{
		&TrackerSettings::Instance().PathPluginPresets,
		&TrackerSettings::Instance().PathPlugins,
		&TrackerSettings::Instance().PathSamples,
		&TrackerSettings::Instance().PathInstruments,
		&TrackerSettings::Instance().PathSongs,
	};
	for(const auto place : places)
	{
		dlg.AddPlace(place->GetDefaultDir());
	}
	for(const auto &place : m_places)
	{
		dlg.AddPlace(place);
	}
#endif

	// Do it!
	BypassInputHandler bih;
	if(dlg.DoModal() != IDOK)
	{
		return false;
	}

	// Retrieve variables
	if(m_filterIndex != nullptr)
		*m_filterIndex = ofn.nFilterIndex;

	if(m_multiSelect)
	{
#if NTDDI_VERSION >= NTDDI_VISTA
		// Multiple files might have been selected
		if(CComPtr<IShellItemArray> shellItems = dlg.GetResults(); shellItems != nullptr)
		{
			// Using the old-style GetNextPathName doesn't work properly when the user performs a search and selects files from different folders.
			// Hence we use that only as a fallback.
			DWORD numItems = 0;
			shellItems->GetCount(&numItems);
			for(DWORD i = 0; i < numItems; i++)
			{
				CComPtr<IShellItem> shellItem;
				shellItems->GetItemAt(i, &shellItem);

				LPWSTR filePath = nullptr;
				if(HRESULT hr = shellItem->GetDisplayName(SIGDN_FILESYSPATH, &filePath); SUCCEEDED(hr))
				{
					m_filenames.push_back(mpt::PathString::FromWide(filePath));
					::CoTaskMemFree(filePath);
				}
			}
		} else
#endif
		{
			POSITION pos = dlg.GetStartPosition();
			while(pos != nullptr)
			{
				m_filenames.push_back(mpt::PathString::FromCString(dlg.GetNextPathName(pos)));
			}
		}
	} else
	{
		// Only one file
		m_filenames.push_back(mpt::PathString::FromCString(dlg.GetPathName()));
	}

	if(m_filenames.empty())
	{
		return false;
	}

	m_workingDirectory = m_filenames.front().AsNative().substr(0, ofn.nFileOffset);
	m_extension = m_filenames.front().AsNative().substr(ofn.nFileExtension);

	return true;
}


// Helper callback to set start path.
int CALLBACK BrowseForFolder::BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM /*lParam*/, LPARAM lpData)
{
	if(uMsg == BFFM_INITIALIZED && lpData != NULL)
	{
		const BrowseForFolder *that = reinterpret_cast<BrowseForFolder *>(lpData);
		SendMessage(hwnd, BFFM_SETSELECTION, TRUE, reinterpret_cast<LPARAM>(that->m_workingDirectory.AsNative().c_str()));
	}
	return 0;
}


// Display the folder dialog.
bool BrowseForFolder::Show(CWnd *parent)
{
	// Note: MFC's CFolderPickerDialog won't work on pre-Vista systems, as it tries to use OPENFILENAME.
	BypassInputHandler bih;
	TCHAR path[MAX_PATH];
	BROWSEINFO bi;
	MemsetZero(bi);
	bi.hwndOwner = (parent != nullptr ? parent : theApp.m_pMainWnd)->m_hWnd;
	if(!m_caption.IsEmpty()) bi.lpszTitle = m_caption;
	bi.pszDisplayName = path;
	bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_USENEWUI;
	bi.lpfn = BrowseCallbackProc;
	bi.lParam = reinterpret_cast<LPARAM>(this);
	LPITEMIDLIST pid = SHBrowseForFolder(&bi);
	bool success = pid != nullptr && SHGetPathFromIDList(pid, path);
	CoTaskMemFree(pid);
	if(success)
	{
		m_workingDirectory = mpt::PathString::FromNative(path);
	}
	return success;
}


OPENMPT_NAMESPACE_END