/*
  ==============================================================================

   This file is part of the JUCE framework.
   Copyright (c) Raw Material Software Limited

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-9-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

   THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
   WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
   MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

// Implemented in juce_Messaging_windows.cpp
namespace detail
{
bool dispatchNextMessageOnSystemQueue (bool returnIfNoPendingMessages);
} // namespace detail

class Win32NativeFileChooser final : private Thread
{
public:
    enum { charsAvailableForResult = 32768 };

    Win32NativeFileChooser (Component* parent, int flags, FilePreviewComponent* previewComp,
                            const File& startingFile, const String& titleToUse,
                            const String& filtersToUse)
        : Thread (SystemStats::getJUCEVersion() + ": Native Win32 FileChooser"),
          owner (parent),
          title (titleToUse),
          filtersString (filtersToUse.replaceCharacter (',', ';')),
          selectsDirectories ((flags & FileBrowserComponent::canSelectDirectories)   != 0),
          // When dealing with directories, it is not possible to 'Save' them. However, one can 'Open' a directory in order to save into it.
          // If the 'saveMode' and 'canSelectDirectories' flags are both present, create an FileOpenDialog instead of an FileSaveDialog.
          isSave             ((flags & FileBrowserComponent::saveMode)               != 0 && ! selectsDirectories),
          warnAboutOverwrite ((flags & FileBrowserComponent::warnAboutOverwriting)   != 0),
          selectMultiple     ((flags & FileBrowserComponent::canSelectMultipleItems) != 0)
    {
        auto parentDirectory = startingFile.getParentDirectory();

        // Handle nonexistent root directories in the same way as existing ones
        files.calloc (static_cast<size_t> (charsAvailableForResult) + 1);

        if (startingFile.isDirectory() || startingFile.isRoot())
        {
            initialPath = startingFile.getFullPathName();
        }
        else
        {
            startingFile.getFileName().copyToUTF16 (files,
                                                    static_cast<size_t> (charsAvailableForResult) * sizeof (WCHAR));
            initialPath = parentDirectory.getFullPathName();
        }

        if (! selectsDirectories)
        {
            if (previewComp != nullptr)
                customComponent.reset (new CustomComponentHolder (previewComp));

            setupFilters();
        }
    }

    ~Win32NativeFileChooser() override
    {
        signalThreadShouldExit();

        while (isThreadRunning())
        {
            if (! detail::dispatchNextMessageOnSystemQueue (true))
                Thread::sleep (1);
        }
    }

    void open (bool async)
    {
        results.clear();

        // the thread should not be running
        nativeDialogRef.set (nullptr);

        if (async)
        {
            jassert (! isThreadRunning());

            startThread();
        }
        else
        {
            results = openDialog (false);
            owner->exitModalState (results.size() > 0 ? 1 : 0);
        }
    }

    void cancel()
    {
        ScopedLock lock (deletingDialog);

        customComponent = nullptr;
        shouldCancel = true;

        if (auto hwnd = nativeDialogRef.get())
            PostMessage (hwnd, WM_CLOSE, 0, 0);
    }

    Component* getCustomComponent()    { return customComponent.get(); }

    Array<URL> results;

private:
    //==============================================================================
    class CustomComponentHolder final : public Component
    {
    public:
        CustomComponentHolder (Component* const customComp)
        {
            setVisible (true);
            setOpaque (true);
            addAndMakeVisible (customComp);
            setSize (jlimit (20, 800, customComp->getWidth()), customComp->getHeight());
        }

        void paint (Graphics& g) override
        {
            g.fillAll (Colours::lightgrey);
        }

        void resized() override
        {
            if (Component* const c = getChildComponent (0))
                c->setBounds (getLocalBounds());
        }

    private:
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomComponentHolder)
    };

    //==============================================================================
    const Component::SafePointer<Component> owner;
    String title, filtersString;
    std::unique_ptr<CustomComponentHolder> customComponent;
    String initialPath, returnedString;

    CriticalSection deletingDialog;

    bool selectsDirectories, isSave, warnAboutOverwrite, selectMultiple;

    HeapBlock<WCHAR> files;
    HeapBlock<WCHAR> filters;

    Atomic<HWND> nativeDialogRef { nullptr };
    bool shouldCancel = false;

    struct FreeLPWSTR
    {
        void operator() (LPWSTR ptr) const noexcept { CoTaskMemFree (ptr); }
    };

    bool showDialog (IFileDialog& dialog)
    {
        FILEOPENDIALOGOPTIONS flags = {};

        if (FAILED (dialog.GetOptions (&flags)))
            return false;

        const auto setBit = [] (FILEOPENDIALOGOPTIONS& field, bool value, FILEOPENDIALOGOPTIONS option)
        {
            if (value)
                field |= option;
            else
                field &= ~option;
        };

        setBit (flags, selectsDirectories,         FOS_PICKFOLDERS);
        setBit (flags, warnAboutOverwrite,         FOS_OVERWRITEPROMPT);
        setBit (flags, selectMultiple,             FOS_ALLOWMULTISELECT);
        setBit (flags, customComponent != nullptr, FOS_FORCEPREVIEWPANEON);

        if (FAILED (dialog.SetOptions (flags)) || FAILED (dialog.SetTitle (title.toUTF16())))
            return false;

        PIDLIST_ABSOLUTE pidl = {};

        if (FAILED (SHParseDisplayName (initialPath.toWideCharPointer(), nullptr, &pidl, SFGAO_FOLDER, nullptr)))
        {
            LPWSTR ptr = nullptr;
            auto result = SHGetKnownFolderPath (FOLDERID_Desktop, 0, nullptr, &ptr);
            std::unique_ptr<WCHAR, FreeLPWSTR> desktopPath (ptr);

            if (FAILED (result))
                return false;

            if (FAILED (SHParseDisplayName (desktopPath.get(), nullptr, &pidl, SFGAO_FOLDER, nullptr)))
                return false;
        }

        const auto item = [&]
        {
            ComSmartPtr<IShellItem> ptr;
            SHCreateShellItem (nullptr, nullptr, pidl, ptr.resetAndGetPointerAddress());
            return ptr;
        }();

        if (item != nullptr)
        {
            dialog.SetDefaultFolder (item);

            if (! initialPath.isEmpty())
                dialog.SetFolder (item);
        }

        String filename (files.getData());

        if (FAILED (dialog.SetFileName (filename.toWideCharPointer())))
            return false;

        auto extension = getDefaultFileExtension (filename);

        if (extension.isNotEmpty() && FAILED (dialog.SetDefaultExtension (extension.toWideCharPointer())))
            return false;

        const COMDLG_FILTERSPEC spec[] { { filtersString.toWideCharPointer(), filtersString.toWideCharPointer() } };

        if (! selectsDirectories && FAILED (dialog.SetFileTypes (numElementsInArray (spec), spec)))
            return false;

        struct Events final : public ComBaseClassHelper<IFileDialogEvents>
        {
            explicit Events (Win32NativeFileChooser& o) : owner (o) {}

            JUCE_COMRESULT OnTypeChange (IFileDialog* d) override                                                 { return updateHwnd (d); }
            JUCE_COMRESULT OnFolderChanging (IFileDialog* d, IShellItem*) override                                { return updateHwnd (d); }
            JUCE_COMRESULT OnFileOk (IFileDialog* d) override                                                     { return updateHwnd (d); }
            JUCE_COMRESULT OnFolderChange (IFileDialog* d) override                                               { return updateHwnd (d); }
            JUCE_COMRESULT OnSelectionChange (IFileDialog* d) override                                            { focusWorkaround(); return updateHwnd (d); }
            JUCE_COMRESULT OnShareViolation (IFileDialog* d, IShellItem*, FDE_SHAREVIOLATION_RESPONSE*) override  { return updateHwnd (d); }
            JUCE_COMRESULT OnOverwrite (IFileDialog* d, IShellItem*, FDE_OVERWRITE_RESPONSE*) override            { return updateHwnd (d); }

            /*  Workaround for a bug in Vista+, OpenFileDialog will truncate the initialFile text.
                Moving the caret along the full length of the text and back will reveal the full string.
            */
            void focusWorkaround()
            {
                if (! defaultFileNameTextCaretMoved)
                {
                    auto makeKeyInput = [] (WORD vk, bool pressed)
                    {
                        INPUT i;

                        ZeroMemory (&i, sizeof (INPUT));

                        i.type = INPUT_KEYBOARD;
                        i.ki.wVk = vk;
                        i.ki.dwFlags = pressed ? 0 : KEYEVENTF_KEYUP;

                        return i;
                    };

                    INPUT inputs[] = {
                        makeKeyInput (VK_HOME, true),
                        makeKeyInput (VK_HOME, false),
                        makeKeyInput (VK_END,  true),
                        makeKeyInput (VK_END,  false),
                    };

                    SendInput ((UINT) std::size (inputs), inputs, sizeof (INPUT));
                    defaultFileNameTextCaretMoved = true;
                }
            }

            JUCE_COMRESULT updateHwnd (IFileDialog* d)
            {
                HWND hwnd = nullptr;

                if (auto window = ComSmartPtr (d, IncrementRef::yes).getInterface<IOleWindow>())
                    window->GetWindow (&hwnd);

                ScopedLock lock (owner.deletingDialog);

                if (owner.shouldCancel)
                    d->Close (S_FALSE);
                else if (hwnd != nullptr)
                    owner.nativeDialogRef = hwnd;

                return S_OK;
            }

            bool defaultFileNameTextCaretMoved = false;
            Win32NativeFileChooser& owner;
        };

        {
            ScopedLock lock (deletingDialog);

            if (shouldCancel)
                return false;
        }

        const auto result = [&]
        {
            struct ScopedAdvise
            {
                ScopedAdvise (IFileDialog& d, Events& events) : dialog (d) { dialog.Advise (&events, &cookie); }
                ~ScopedAdvise() { dialog.Unadvise (cookie); }
                IFileDialog& dialog;
                DWORD cookie = 0;
            };

            Events events { *this };
            ScopedAdvise scope { dialog, events };
            return dialog.Show (GetActiveWindow()) == S_OK;
        }();

        ScopedLock lock (deletingDialog);
        nativeDialogRef = nullptr;

        return result;
    }

    //==============================================================================
    Array<URL> openDialogWithoutPreview()
    {
        const auto getUrl = [] (IShellItem& item)
        {
            LPWSTR ptr = nullptr;

            if (item.GetDisplayName (SIGDN_FILESYSPATH, &ptr) != S_OK)
                return URL();

            const auto path = std::unique_ptr<WCHAR, FreeLPWSTR> { ptr };
            return URL (File (String (path.get())));
        };

        if (isSave)
        {
            const auto dialog = [&]
            {
                ComSmartPtr<IFileDialog> ptr;
                ptr.CoCreateInstance (CLSID_FileSaveDialog, CLSCTX_INPROC_SERVER);
                return ptr;
            }();

            if (dialog == nullptr)
                return {};

            showDialog (*dialog);

            const auto item = [&]
            {
                ComSmartPtr<IShellItem> ptr;
                dialog->GetResult (ptr.resetAndGetPointerAddress());
                return ptr;
            }();

            if (item == nullptr)
                return {};

            const auto url = getUrl (*item);

            if (url.isEmpty())
                return {};

            return { url };
        }

        const auto dialog = [&]
        {
            ComSmartPtr<IFileOpenDialog> ptr;
            ptr.CoCreateInstance (CLSID_FileOpenDialog, CLSCTX_INPROC_SERVER);
            return ptr;
        }();

        if (dialog == nullptr)
            return {};

        showDialog (*dialog);

        const auto items = [&]
        {
            ComSmartPtr<IShellItemArray> ptr;
            dialog->GetResults (ptr.resetAndGetPointerAddress());
            return ptr;
        }();

        if (items == nullptr)
            return {};

        Array<URL> result;

        DWORD numItems = 0;
        items->GetCount (&numItems);

        for (DWORD i = 0; i < numItems; ++i)
        {
            ComSmartPtr<IShellItem> scope;
            items->GetItemAt (i, scope.resetAndGetPointerAddress());

            if (scope != nullptr)
            {
                const auto url = getUrl (*scope);

                if (! url.isEmpty())
                    result.add (url);
            }
        }

        return result;
    }

    Array<URL> openDialogWithPreview (bool async)
    {
        Array<URL> selections;

        if (selectsDirectories)
        {
            BROWSEINFO bi = {};
            bi.hwndOwner = GetActiveWindow();
            bi.pszDisplayName = files;
            bi.lpszTitle = title.toWideCharPointer();
            bi.lParam = (LPARAM) this;
            bi.lpfn = browseCallbackProc;
           #ifdef BIF_USENEWUI
            bi.ulFlags = BIF_USENEWUI | BIF_VALIDATE;
           #else
            bi.ulFlags = 0x50;
           #endif

            LPITEMIDLIST list = SHBrowseForFolder (&bi);

            if (! SHGetPathFromIDListW (list, files))
            {
                files[0] = 0;
                returnedString.clear();
            }

            LPMALLOC al;

            if (list != nullptr && SUCCEEDED (SHGetMalloc (&al)))
                al->Free (list);

            if (files[0] != 0)
            {
                File result (String (files.get()));

                if (returnedString.isNotEmpty())
                    result = result.getSiblingFile (returnedString);

                selections.add (URL (result));
            }
        }
        else
        {
            OPENFILENAMEW of = {};

           #ifdef OPENFILENAME_SIZE_VERSION_400W
            of.lStructSize = OPENFILENAME_SIZE_VERSION_400W;
           #else
            of.lStructSize = sizeof (of);
           #endif

            if (files[0] != 0)
            {
                auto startingFile = File (initialPath).getChildFile (String (files.get()));
                startingFile.getFullPathName().copyToUTF16 (files, charsAvailableForResult * sizeof (WCHAR));
            }

            of.hwndOwner = GetActiveWindow();
            of.lpstrFilter = filters.getData();
            of.nFilterIndex = 1;
            of.lpstrFile = files;
            of.nMaxFile = (DWORD) charsAvailableForResult;
            of.lpstrInitialDir = initialPath.toWideCharPointer();
            of.lpstrTitle = title.toWideCharPointer();
            of.Flags = getOpenFilenameFlags (async);
            of.lCustData = (LPARAM) this;
            of.lpfnHook = &openCallback;

            if (isSave)
            {
                auto extension = getDefaultFileExtension (files.getData());

                if (extension.isNotEmpty())
                    of.lpstrDefExt = extension.toWideCharPointer();

                if (! GetSaveFileName (&of))
                    return {};
            }
            else
            {
                if (! GetOpenFileName (&of))
                    return {};
            }

            if (selectMultiple && of.nFileOffset > 0 && files[of.nFileOffset - 1] == 0)
            {
                const WCHAR* filename = files + of.nFileOffset;

                while (*filename != 0)
                {
                    selections.add (URL (File (String (files.get())).getChildFile (String (filename))));
                    filename += wcslen (filename) + 1;
                }
            }
            else if (files[0] != 0)
            {
                selections.add (URL (File (String (files.get()))));
            }
        }

        return selections;
    }

    Array<URL> openDialog (bool async)
    {
        struct Remover
        {
            explicit Remover (Win32NativeFileChooser& chooser) : item (chooser) {}
            ~Remover() { getNativeDialogList().removeValue (&item); }

            Win32NativeFileChooser& item;
        };

        const Remover remover (*this);

        if (customComponent == nullptr)
            return openDialogWithoutPreview();

        return openDialogWithPreview (async);
    }

    void run() override
    {
        results = [&]
        {
            struct ScopedCoInitialize
            {
                // IUnknown_GetWindow will only succeed when instantiated in a single-thread apartment
                ScopedCoInitialize() { [[maybe_unused]] const auto result = CoInitializeEx (nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); }
                ~ScopedCoInitialize() { CoUninitialize(); }
            };

            ScopedCoInitialize scope;

            return openDialog (true);
        }();

        auto safeOwner = owner;
        auto resultCode = results.size() > 0 ? 1 : 0;

        MessageManager::callAsync ([resultCode, safeOwner]
        {
            if (safeOwner != nullptr)
                safeOwner->exitModalState (resultCode);
        });
    }

    static HashMap<HWND, Win32NativeFileChooser*>& getNativeDialogList()
    {
        static HashMap<HWND, Win32NativeFileChooser*> dialogs;
        return dialogs;
    }

    static Win32NativeFileChooser* getNativePointerForDialog (HWND hwnd)
    {
        return getNativeDialogList()[hwnd];
    }

    //==============================================================================
    void setupFilters()
    {
        const size_t filterSpaceNumChars = 2048;
        filters.calloc (filterSpaceNumChars);

        const size_t bytesWritten = filtersString.copyToUTF16 (filters.getData(), filterSpaceNumChars * sizeof (WCHAR));
        filtersString.copyToUTF16 (filters + (bytesWritten / sizeof (WCHAR)),
                                   ((filterSpaceNumChars - 1) * sizeof (WCHAR) - bytesWritten));

        for (size_t i = 0; i < filterSpaceNumChars; ++i)
            if (filters[i] == '|')
                filters[i] = 0;
    }

    DWORD getOpenFilenameFlags (bool async)
    {
        DWORD ofFlags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR | OFN_HIDEREADONLY | OFN_ENABLESIZING;

        if (warnAboutOverwrite)
            ofFlags |= OFN_OVERWRITEPROMPT;

        if (selectMultiple)
            ofFlags |= OFN_ALLOWMULTISELECT;

        if (async || customComponent != nullptr)
            ofFlags |= OFN_ENABLEHOOK;

        return ofFlags;
    }

    String getDefaultFileExtension (const String& filename) const
    {
        const auto extension = filename.contains (".") ? filename.fromLastOccurrenceOf (".", false, false)
                                                       : String();

        if (! extension.isEmpty())
            return extension;

        auto tokens = StringArray::fromTokens (filtersString, ";,", "\"'");
        tokens.trim();
        tokens.removeEmptyStrings();

        if (tokens.size() == 1 && tokens[0].removeCharacters ("*.").isNotEmpty())
            return tokens[0].fromFirstOccurrenceOf (".", false, false);

        return {};
    }

    //==============================================================================
    void initialised (HWND hWnd)
    {
        SendMessage (hWnd, BFFM_SETSELECTIONW, TRUE, (LPARAM) initialPath.toWideCharPointer());
        initDialog (hWnd);
    }

    void validateFailed (const String& path)
    {
        returnedString = path;
    }

    void initDialog (HWND hdlg)
    {
        ScopedLock lock (deletingDialog);
        getNativeDialogList().set (hdlg, this);

        if (shouldCancel)
        {
            EndDialog (hdlg, 0);
        }
        else
        {
            nativeDialogRef.set (hdlg);

            if (customComponent != nullptr)
            {
                Component::SafePointer<Component> safeCustomComponent (customComponent.get());

                RECT dialogScreenRect, dialogClientRect;
                GetWindowRect (hdlg, &dialogScreenRect);
                GetClientRect (hdlg, &dialogClientRect);

                auto screenRectangle = Rectangle<int>::leftTopRightBottom (dialogScreenRect.left,  dialogScreenRect.top,
                                                                           dialogScreenRect.right, dialogScreenRect.bottom);

                auto scale = Desktop::getInstance().getDisplays().getDisplayForRect (screenRectangle, true)->scale;
                auto physicalComponentWidth = roundToInt (safeCustomComponent->getWidth() * scale);

                SetWindowPos (hdlg, nullptr, screenRectangle.getX(), screenRectangle.getY(),
                              physicalComponentWidth + jmax (150, screenRectangle.getWidth()),
                              jmax (150, screenRectangle.getHeight()),
                              SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER);

                auto appendCustomComponent = [safeCustomComponent, dialogClientRect, scale, hdlg]() mutable
                {
                    if (safeCustomComponent != nullptr)
                    {
                        auto scaledClientRectangle = Rectangle<int>::leftTopRightBottom (dialogClientRect.left, dialogClientRect.top,
                                                                                         dialogClientRect.right, dialogClientRect.bottom) / scale;

                        safeCustomComponent->setBounds (scaledClientRectangle.getRight(), scaledClientRectangle.getY(),
                                                        safeCustomComponent->getWidth(), scaledClientRectangle.getHeight());
                        safeCustomComponent->addToDesktop (0, hdlg);
                    }
                };

                if (MessageManager::getInstance()->isThisTheMessageThread())
                    appendCustomComponent();
                else
                    MessageManager::callAsync (appendCustomComponent);
            }
        }
    }

    void destroyDialog (HWND hdlg)
    {
        ScopedLock exiting (deletingDialog);

        getNativeDialogList().remove (hdlg);
        nativeDialogRef.set (nullptr);

        if (MessageManager::getInstance()->isThisTheMessageThread())
            customComponent = nullptr;
        else
            MessageManager::callAsync ([this] { customComponent = nullptr; });
    }

    void selectionChanged (HWND hdlg)
    {
        ScopedLock lock (deletingDialog);

        if (customComponent != nullptr && ! shouldCancel)
        {
            if (FilePreviewComponent* comp = dynamic_cast<FilePreviewComponent*> (customComponent->getChildComponent (0)))
            {
                WCHAR path [MAX_PATH * 2] = { 0 };
                CommDlg_OpenSave_GetFilePath (hdlg, (LPARAM) &path, MAX_PATH);

                if (MessageManager::getInstance()->isThisTheMessageThread())
                {
                    comp->selectedFileChanged (File (path));
                }
                else
                {
                    MessageManager::callAsync ([safeComp = Component::SafePointer<FilePreviewComponent> { comp },
                                                selectedFile = File { path }]() mutable
                                               {
                                                    if (safeComp != nullptr)
                                                        safeComp->selectedFileChanged (selectedFile);
                                               });
                }
            }
        }
    }

    //==============================================================================
    static int CALLBACK browseCallbackProc (HWND hWnd, UINT msg, LPARAM lParam, LPARAM lpData)
    {
        auto* self = reinterpret_cast<Win32NativeFileChooser*> (lpData);

        switch (msg)
        {
            case BFFM_INITIALIZED:       self->initialised (hWnd);                             break;
            case BFFM_VALIDATEFAILEDW:   self->validateFailed (String ((LPCWSTR)     lParam)); break;
            case BFFM_VALIDATEFAILEDA:   self->validateFailed (String ((const char*) lParam)); break;
            default: break;
        }

        return 0;
    }

    static UINT_PTR CALLBACK openCallback (HWND hwnd, UINT uiMsg, WPARAM /*wParam*/, LPARAM lParam)
    {
        auto hdlg = getDialogFromHWND (hwnd);

        switch (uiMsg)
        {
            case WM_INITDIALOG:
            {
                if (auto* self = reinterpret_cast<Win32NativeFileChooser*> (((OPENFILENAMEW*) lParam)->lCustData))
                    self->initDialog (hdlg);

                break;
            }

            case WM_DESTROY:
            {
                if (auto* self = getNativeDialogList()[hdlg])
                    self->destroyDialog (hdlg);

                break;
            }

            case WM_NOTIFY:
            {
                auto ofn = reinterpret_cast<LPOFNOTIFY> (lParam);

                if (ofn->hdr.code == CDN_SELCHANGE)
                    if (auto* self = reinterpret_cast<Win32NativeFileChooser*> (ofn->lpOFN->lCustData))
                        self->selectionChanged (hdlg);

                break;
            }

            default:
                break;
        }

        return 0;
    }

    static HWND getDialogFromHWND (HWND hwnd)
    {
        if (hwnd == nullptr)
            return nullptr;

        HWND dialogH = GetParent (hwnd);

        if (dialogH == nullptr)
            dialogH = hwnd;

        return dialogH;
    }

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32NativeFileChooser)
};

class FileChooser::Native final : public std::enable_shared_from_this<Native>,
                                  public Component,
                                  public FileChooser::Pimpl
{
public:
    Native (FileChooser& fileChooser, int flagsIn, FilePreviewComponent* previewComp)
        : owner (fileChooser),
          nativeFileChooser (std::make_unique<Win32NativeFileChooser> (this, flagsIn, previewComp, fileChooser.startingFile,
                                                                       fileChooser.title, fileChooser.filters))
    {
        auto mainMon = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userBounds.toNearestInt();

        setBounds (mainMon.getX() + mainMon.getWidth() / 4,
                   mainMon.getY() + mainMon.getHeight() / 4,
                   0, 0);

        setOpaque (true);
        setAlwaysOnTop (WindowUtils::areThereAnyAlwaysOnTopWindows());
        addToDesktop (0);
    }

    ~Native() override
    {
        exitModalState (0);
        nativeFileChooser->cancel();
    }

    void launch() override
    {
        std::weak_ptr<Native> safeThis = shared_from_this();

        enterModalState (true, ModalCallbackFunction::create ([safeThis] (int)
        {
            if (auto locked = safeThis.lock())
                locked->owner.finished (locked->nativeFileChooser->results);
        }));

        nativeFileChooser->open (true);
    }

    void runModally() override
    {
       #if JUCE_MODAL_LOOPS_PERMITTED
        enterModalState (true);
        nativeFileChooser->open (false);
        exitModalState (nativeFileChooser->results.size() > 0 ? 1 : 0);
        nativeFileChooser->cancel();

        owner.finished (nativeFileChooser->results);
       #else
        jassertfalse;
       #endif
    }

    bool canModalEventBeSentToComponent (const Component* targetComponent) override
    {
        if (targetComponent == nullptr)
            return false;

        if (targetComponent == nativeFileChooser->getCustomComponent())
            return true;

        return targetComponent->findParentComponentOfClass<FilePreviewComponent>() != nullptr;
    }

    void inputAttemptWhenModal() override {}

private:
    FileChooser& owner;
    std::shared_ptr<Win32NativeFileChooser> nativeFileChooser;
};

//==============================================================================
bool FileChooser::isPlatformDialogAvailable()
{
   #if JUCE_DISABLE_NATIVE_FILECHOOSERS
    return false;
   #else
    return true;
   #endif
}

std::shared_ptr<FileChooser::Pimpl> FileChooser::showPlatformDialog (FileChooser& owner, int flags,
                                                                     FilePreviewComponent* preview)
{
    return std::make_shared<FileChooser::Native> (owner, flags, preview);
}

} // namespace juce
