// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.

#if PLATFORM_WINDOWS

#include "WindowsFileSystem.h"
#include "Engine/Platform/File.h"
#include "Engine/Platform/Window.h"
#include "Engine/Platform/Windows/ComPtr.h"
#include "Engine/Platform/CreateProcessSettings.h"
#include "Engine/Core/Types/StringView.h"
#include "Engine/Core/Collections/Array.h"
#include "../Win32/IncludeWindowsHeaders.h"

// Hack this stuff (the problem is that GDI has function named Rectangle -> like one of the Flax core types)
#define _PRSHT_H_
#define __shobjidl_h__
#define LPFNADDPROPSHEETPAGE void*
#include <ShlObj.h>
#include <ShellAPI.h>
#include <KnownFolders.h>
#undef ShellExecute

namespace Windows
{
    typedef UINT_PTR (CALLBACK *LPOFNHOOKPROC)(HWND, UINT, WPARAM, LPARAM);

    typedef struct tagOFNW
    {
        DWORD lStructSize;
        HWND hwndOwner;
        HINSTANCE hInstance;
        LPCWSTR lpstrFilter;
        LPWSTR lpstrCustomFilter;
        DWORD nMaxCustFilter;
        DWORD nFilterIndex;
        LPWSTR lpstrFile;
        DWORD nMaxFile;
        LPWSTR lpstrFileTitle;
        DWORD nMaxFileTitle;
        LPCWSTR lpstrInitialDir;
        LPCWSTR lpstrTitle;
        DWORD Flags;
        WORD nFileOffset;
        WORD nFileExtension;
        LPCWSTR lpstrDefExt;
        LPARAM lCustData;
        LPOFNHOOKPROC lpfnHook;
        LPCWSTR lpTemplateName;
        void* pvReserved;
        DWORD dwReserved;
        DWORD FlagsEx;
    } OPENFILENAMEW, *LPOPENFILENAMEW;

    typedef OPENFILENAMEW OPENFILENAME;

    WIN_API BOOL WIN_API_CALLCONV GetOpenFileNameW(LPOPENFILENAMEW);
    WIN_API BOOL WIN_API_CALLCONV GetSaveFileNameW(LPOPENFILENAMEW);

#define OFN_READONLY                 0x00000001
#define OFN_OVERWRITEPROMPT          0x00000002
#define OFN_HIDEREADONLY             0x00000004
#define OFN_NOCHANGEDIR              0x00000008
#define OFN_SHOWHELP                 0x00000010
#define OFN_ENABLEHOOK               0x00000020
#define OFN_ENABLETEMPLATE           0x00000040
#define OFN_ENABLETEMPLATEHANDLE     0x00000080
#define OFN_NOVALIDATE               0x00000100
#define OFN_ALLOWMULTISELECT         0x00000200
#define OFN_EXTENSIONDIFFERENT       0x00000400
#define OFN_PATHMUSTEXIST            0x00000800
#define OFN_FILEMUSTEXIST            0x00001000
#define OFN_CREATEPROMPT             0x00002000
#define OFN_SHAREAWARE               0x00004000
#define OFN_NOREADONLYRETURN         0x00008000
#define OFN_NOTESTFILECREATE         0x00010000
#define OFN_NONETWORKBUTTON          0x00020000
#define OFN_NOLONGNAMES              0x00040000
#define OFN_EXPLORER                 0x00080000
#define OFN_NODEREFERENCELINKS       0x00100000
#define OFN_LONGNAMES                0x00200000
#define OFN_ENABLEINCLUDENOTIFY      0x00400000
#define OFN_ENABLESIZING             0x00800000
#define OFN_DONTADDTORECENT          0x02000000
#define OFN_FORCESHOWHIDDEN          0x10000000
}

static int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
{
    if (uMsg == BFFM_INITIALIZED)
    {
        if (lParam)
        {
            SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lParam);
        }
    }

    return 0;
}

bool WindowsFileSystem::MoveFileToRecycleBin(const StringView& path)
{
    ASSERT(path.Length() < MAX_PATH);
    Char pathNullNull[MAX_PATH + 1];
    Platform::MemoryCopy(pathNullNull, *path, sizeof(Char) * path.Length() + 1);
    pathNullNull[path.Length()] = 0;
    pathNullNull[path.Length() + 1] = 0;

    SHFILEOPSTRUCT op;
    op.hwnd = nullptr;
    op.wFunc = FO_DELETE;
    op.pFrom = pathNullNull;
    op.pTo = nullptr;
    op.fFlags = FOF_ALLOWUNDO | FOF_NO_UI;
    op.fAnyOperationsAborted = FALSE;
    op.hNameMappings = nullptr;
    op.lpszProgressTitle = nullptr;

    return SHFileOperation(&op) != 0;
}

bool SameFile(HANDLE h1, HANDLE h2)
{
    BY_HANDLE_FILE_INFORMATION bhfi1 = { 0 };
    BY_HANDLE_FILE_INFORMATION bhfi2 = { 0 };

    if (::GetFileInformationByHandle(h1, &bhfi1) && ::GetFileInformationByHandle(h2, &bhfi2))
    {
        return ((bhfi1.nFileIndexHigh == bhfi2.nFileIndexHigh) && (bhfi1.nFileIndexLow == bhfi2.nFileIndexLow) && (bhfi1.dwVolumeSerialNumber == bhfi2.dwVolumeSerialNumber));
    }

    return false;
}

bool WindowsFileSystem::AreFilePathsEqual(const StringView& path1, const StringView& path2)
{
    if (path1.Compare(path2, StringSearchCase::CaseSensitive) == 0)
        return true;

    // Normalize file paths
    String filename1(path1);
    String filename2(path2);
    NormalizePath(filename1);
    NormalizePath(filename2);

    HANDLE file1 = CreateFileW(*filename1, GENERIC_READ, (DWORD)FileShare::All, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
    HANDLE file2 = CreateFileW(*filename2, GENERIC_READ, (DWORD)FileShare::All, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);

    bool result = SameFile(file1, file2);

    CloseHandle(file1);
    CloseHandle(file2);

    return result;
}

void WindowsFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& result)
{
    KNOWNFOLDERID rfid;
    switch (type)
    {
    case SpecialFolder::Desktop:
        rfid = FOLDERID_Desktop;
        break;
    case SpecialFolder::Documents:
        rfid = FOLDERID_Documents;
        break;
    case SpecialFolder::Pictures:
        rfid = FOLDERID_Pictures;
        break;
    case SpecialFolder::AppData:
        rfid = FOLDERID_RoamingAppData;
        break;
    case SpecialFolder::LocalAppData:
        rfid = FOLDERID_LocalAppData;
        break;
    case SpecialFolder::ProgramData:
        rfid = FOLDERID_ProgramData;
        break;
    case SpecialFolder::Temporary:
    {
        Char buffer[MAX_PATH];
        if (GetTempPathW(MAX_PATH, buffer))
        {
            result = buffer;
            NormalizePath(result);
        }
        return;
    }
    }

    PWSTR path = nullptr;
    HRESULT hr = SHGetKnownFolderPath(rfid, 0, nullptr, &path);
    if (SUCCEEDED(hr))
    {
        result = path;
        NormalizePath(result);
    }

    CoTaskMemFree(path);
}

bool WindowsFileSystem::ShowOpenFileDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& filter, bool multiSelect, const StringView& title, Array<String, HeapAllocation>& filenames)
{
    bool result = true;

    // Allocate memory for the filenames
    const int32 maxFilenamesSize = (multiSelect ? 200 : 2) * MAX_PATH;
    Array<Char> fileNamesBuffer;
    fileNamesBuffer.Resize(maxFilenamesSize);
    fileNamesBuffer[0] = 0;

    // Setup description
    Windows::OPENFILENAME of;
    ZeroMemory(&of, sizeof(of));
    of.lStructSize = sizeof(of);
    of.lpstrFilter = filter.HasChars() ? filter.Get() : nullptr;
    of.lpstrFile = fileNamesBuffer.Get();
    of.nMaxFile = maxFilenamesSize;
    of.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_ENABLESIZING | OFN_NOCHANGEDIR;
    of.lpstrTitle = title.HasChars() ? title.Get() : nullptr;
    of.lpstrInitialDir = initialDirectory.HasChars() ? initialDirectory.Get() : nullptr;
    if (parentWindow)
        of.hwndOwner = static_cast<HWND>(parentWindow->GetNativePtr());
    if (multiSelect)
        of.Flags |= OFN_ALLOWMULTISELECT;

    // Show dialog
    if (GetOpenFileNameW(&of) != 0)
    {
        // Get filenames
        Char* ptr = of.lpstrFile;
        ptr[of.nFileOffset - 1] = 0;
        String directory = String(ptr);
        ptr += of.nFileOffset;
        while (*ptr)
        {
            filenames.Add(directory / ptr);
            ptr += lstrlenW(ptr);
            if (multiSelect)
                ptr++;
        }

        result = false;
    }

    return result;
}

bool WindowsFileSystem::ShowSaveFileDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& filter, bool multiSelect, const StringView& title, Array<String, HeapAllocation>& filenames)
{
    bool result = true;

    // Allocate memory for the filenames
    int32 maxFilenamesSize = (multiSelect ? 200 : 2) * MAX_PATH;
    Array<Char> fileNamesBuffer;
    fileNamesBuffer.Resize(maxFilenamesSize);
    fileNamesBuffer[0] = 0;

    // Setup description
    Windows::OPENFILENAME of;
    ZeroMemory(&of, sizeof(of));
    of.lStructSize = sizeof(of);
    of.lpstrFilter = filter.HasChars() ? filter.Get() : nullptr;
    of.lpstrFile = fileNamesBuffer.Get();
    of.nMaxFile = maxFilenamesSize;
    of.Flags = OFN_EXPLORER | OFN_ENABLESIZING | OFN_OVERWRITEPROMPT | OFN_NOCHANGEDIR;
    of.lpstrTitle = title.HasChars() ? title.Get() : nullptr;
    of.lpstrInitialDir = initialDirectory.HasChars() ? initialDirectory.Get() : nullptr;
    if (parentWindow)
        of.hwndOwner = static_cast<HWND>(parentWindow->GetNativePtr());
    if (multiSelect)
        of.Flags |= OFN_ALLOWMULTISELECT;

    // Show dialog
    if (GetSaveFileNameW(&of) != 0)
    {
        // Get filenames
        Char* ptr = of.lpstrFile;
        ptr[of.nFileOffset - 1] = 0;
        String directory = String(ptr);
        ptr += of.nFileOffset;
        while (*ptr)
        {
            filenames.Add(directory / ptr);
            ptr += lstrlenW(ptr);
            if (multiSelect)
                ptr++;
        }

        result = false;
    }

    return result;
}

bool WindowsFileSystem::ShowBrowseFolderDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& title, String& path)
{
    bool result = true;

    // Randomly generated GUID used for storing the last location of this dialog
    const Guid folderGuid(0x53890ed9, 0xa55e47ba, 0xa970bdae, 0x72acedff);

    ComPtr<IFileOpenDialog> fd;
    if (SUCCEEDED(CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&fd))))
    {
        DWORD options;
        fd->GetOptions(&options);
        fd->SetOptions(options | FOS_PICKFOLDERS | FOS_NOCHANGEDIR);

        if (title.HasChars())
            fd->SetTitle(title.Get());

        // Associate the last selected folder with this GUID instead of overwriting the global one
        fd->SetClientGuid(*reinterpret_cast<const GUID*>(&folderGuid));

        ComPtr<IShellItem> defaultFolder;
        if (SUCCEEDED(SHCreateItemFromParsingName(initialDirectory.Get(), NULL, IID_PPV_ARGS(&defaultFolder))))
            fd->SetFolder(defaultFolder);

        HWND hwndOwner = parentWindow ? parentWindow->GetHWND() : NULL;
        if (SUCCEEDED(fd->Show(hwndOwner)))
        {
            ComPtr<IShellItem> si;
            if (SUCCEEDED(fd->GetResult(&si)))
            {
                LPWSTR resultPath;
                if (SUCCEEDED(si->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &resultPath)))
                {
                    path = resultPath;
                    CoTaskMemFree(resultPath);
                    result = false;
                }
            }
        }
    }

    return result;
}

bool WindowsFileSystem::ShowFileExplorer(const StringView& path)
{
    CreateProcessSettings procSettings;
    procSettings.FileName = path;
    procSettings.FileName = path;
    procSettings.HiddenWindow = false;
    procSettings.WaitForEnd = false;
    procSettings.LogOutput = false;
    procSettings.ShellExecute = true;
    return Platform::CreateProcess(procSettings) != 0;
}

#endif
