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

#if PLATFORM_WINDOWS

#include "WindowsFileSystemWatcher.h"
#include "Engine/Platform/CriticalSection.h"
#include "Engine/Platform/Thread.h"
#include "Engine/Threading/ThreadSpawner.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Core/Collections/Array.h"
#include "../Win32/IncludeWindowsHeaders.h"

BOOL RefreshWatch(WindowsFileSystemWatcher* watcher);

namespace FileSystemWatchers
{
    CriticalSection Locker;
    Array<WindowsFileSystemWatcher*, InlinedAllocation<16>> Watchers;
    Win32Thread* Thread = nullptr;
    bool ThreadActive;

    int32 Run()
    {
        while (ThreadActive)
        {
            SleepEx(INFINITE, true);
        }
        return 0;
    }

    static void CALLBACK StopProc(ULONG_PTR arg)
    {
        ThreadActive = false;
    }

    static void CALLBACK AddDirectoryProc(ULONG_PTR arg)
    {
        const auto watcher = (FileSystemWatcher*)arg;
        RefreshWatch(watcher);
    }
};

VOID CALLBACK NotificationCompletion(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped)
{
    auto watcher = (FileSystemWatcher*)lpOverlapped->hEvent;
    if (dwErrorCode == ERROR_OPERATION_ABORTED ||
        dwNumberOfBytesTransfered <= 0 ||
        !watcher)
    {
        return;
    }

    // Swap buffers
    watcher->CurrentBuffer = (watcher->CurrentBuffer + 1) % 2;

    // Get the new read issued as fast as possible
    if (!watcher->StopNow)
    {
        RefreshWatch(watcher);
    }

    // Process notifications
    auto notify = (FILE_NOTIFY_INFORMATION*)watcher->Buffer[(watcher->CurrentBuffer + 1) % 2];
    do
    {
        // Convert action type
        auto action = FileSystemAction::Unknown;
        switch (notify->Action)
        {
        case FILE_ACTION_RENAMED_NEW_NAME:
        case FILE_ACTION_ADDED:
            action = FileSystemAction::Create;
            break;
        case FILE_ACTION_RENAMED_OLD_NAME:
        case FILE_ACTION_REMOVED:
            action = FileSystemAction::Delete;
            break;
        case FILE_ACTION_MODIFIED:
            action = FileSystemAction::Modify;
            break;
        default:
            action = FileSystemAction::Unknown;
            break;
        }
        if (action != FileSystemAction::Unknown)
        {
            // Build path
            String path(notify->FileName, notify->FileNameLength / sizeof(WCHAR));
            path = watcher->GetDirectory() / path;

            // Send event
            watcher->OnEvent(path, action);
        }

        // Move to the next notify
        notify = (FILE_NOTIFY_INFORMATION*)((byte*)notify + notify->NextEntryOffset);
    } while (notify->NextEntryOffset != 0);
}

// Refreshes the directory monitoring
BOOL RefreshWatch(WindowsFileSystemWatcher* watcher)
{
    DWORD dwBytesReturned = 0;
    return ReadDirectoryChangesW(
        watcher->DirectoryHandle,
        watcher->Buffer[watcher->CurrentBuffer],
        FileSystemWatcher::BufferSize,
        watcher->WithSubDirs() ? TRUE : FALSE,
        FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_FILE_NAME,
        &dwBytesReturned,
        (OVERLAPPED*)&watcher->Overlapped,
        NotificationCompletion
    );
}

WindowsFileSystemWatcher::WindowsFileSystemWatcher(const String& directory, bool withSubDirs)
    : FileSystemWatcherBase(directory, withSubDirs)
    , StopNow(false)
    , CurrentBuffer(0)
{
    // Setup
    Platform::MemoryClear(&Overlapped, sizeof(Overlapped));
    ((OVERLAPPED&)Overlapped).hEvent = this;

    // Create directory handle for events handling
    DirectoryHandle = CreateFileW(
        *directory,
        FILE_LIST_DIRECTORY,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        nullptr,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
        nullptr
    );
    if (DirectoryHandle == INVALID_HANDLE_VALUE)
    {
        LOG_WIN32_LAST_ERROR;
        return;
    }

    // Register
    FileSystemWatchers::Locker.Lock();
    FileSystemWatchers::Watchers.Add(this);
    if (!FileSystemWatchers::Thread)
    {
        FileSystemWatchers::ThreadActive = true;
        FileSystemWatchers::Thread = ThreadSpawner::Start(FileSystemWatchers::Run, TEXT("File System Watchers"), ThreadPriority::BelowNormal);
    }
    FileSystemWatchers::Locker.Unlock();

    // Issue the first read
    QueueUserAPC(FileSystemWatchers::AddDirectoryProc, FileSystemWatchers::Thread->GetHandle(), (ULONG_PTR)this);
}

WindowsFileSystemWatcher::~WindowsFileSystemWatcher()
{
    FileSystemWatchers::Locker.Lock();
    FileSystemWatchers::Watchers.Remove(this);
    FileSystemWatchers::Locker.Unlock();

    if (DirectoryHandle != INVALID_HANDLE_VALUE)
    {
        StopNow = true;

#if WINVER >= 0x600
        CancelIoEx(DirectoryHandle, (OVERLAPPED*)&Overlapped);
#else
        CancelIo(DirectoryHandle);
#endif

        const HANDLE handle = DirectoryHandle;
        DirectoryHandle = INVALID_HANDLE_VALUE;
        WaitForSingleObjectEx(handle, 0, true);

        CloseHandle(handle);
    }

    FileSystemWatchers::Locker.Lock();
    if (FileSystemWatchers::Watchers.IsEmpty() && FileSystemWatchers::Thread)
    {
        FileSystemWatchers::ThreadActive = false;
        QueueUserAPC(FileSystemWatchers::StopProc, FileSystemWatchers::Thread->GetHandle(), 0);
        FileSystemWatchers::Thread->Join();
        FileSystemWatchers::Thread = nullptr;
    }
    FileSystemWatchers::Locker.Unlock();
}

#endif
