/////////////////////////////////////////////////////////////////////////////
// Name:        wx/msw/private/fswatcher.h
// Purpose:     File system watcher impl classes
// Author:      Bartosz Bekier
// Created:     2009-05-26
// Copyright:   (c) 2009 Bartosz Bekier <bartosz.bekier@gmail.com>
// Licence:     wxWindows licence
/////////////////////////////////////////////////////////////////////////////

#ifndef WX_MSW_PRIVATE_FSWATCHER_H_
#define WX_MSW_PRIVATE_FSWATCHER_H_

#include "wx/filename.h"
#include "wx/vector.h"
#include "wx/msw/private.h"

// ============================================================================
// wxFSWatcherEntry implementation & helper declarations
// ============================================================================

class wxFSWatcherImplMSW;

class wxFSWatchEntryMSW : public wxFSWatchInfo
{
public:
    enum
    {
        BUFFER_SIZE = 4096   // TODO parametrize
    };

    wxFSWatchEntryMSW(const wxFSWatchInfo& winfo) :
        wxFSWatchInfo(winfo)
    {
        // get handle for this path
        m_handle = OpenDir(m_path);
        m_overlapped = (OVERLAPPED*)calloc(1, sizeof(OVERLAPPED));
        wxZeroMemory(m_buffer);
    }

    virtual ~wxFSWatchEntryMSW()
    {
        wxLogTrace(wxTRACE_FSWATCHER, "Deleting entry '%s'", m_path);

        if (m_handle != INVALID_HANDLE_VALUE)
        {
            if (!CloseHandle(m_handle))
            {
                wxLogSysError(_("Unable to close the handle for '%s'"),
                                m_path);
            }
        }
        free(m_overlapped);
    }

    bool IsOk() const
    {
        return m_handle != INVALID_HANDLE_VALUE;
    }

    HANDLE GetHandle() const
    {
        return m_handle;
    }

    void* GetBuffer()
    {
        return m_buffer;
    }

    OVERLAPPED* GetOverlapped() const
    {
        return m_overlapped;
    }

private:
    // opens dir with all flags, attributes etc. necessary to be later
    // asynchronous watched with ReadDirectoryChangesW
    static HANDLE OpenDir(const wxString& path)
    {
        HANDLE handle = CreateFile(path.t_str(),
                                   FILE_LIST_DIRECTORY,
                                   FILE_SHARE_READ |
                                   FILE_SHARE_WRITE |
                                   FILE_SHARE_DELETE,
                                   nullptr,
                                   OPEN_EXISTING,
                                   FILE_FLAG_BACKUP_SEMANTICS |
                                   FILE_FLAG_OVERLAPPED,
                                   nullptr);
        if (handle == INVALID_HANDLE_VALUE)
        {
            wxLogSysError(_("Failed to open directory \"%s\" for monitoring."),
                            path);
        }

        return handle;
    }

    HANDLE m_handle;             // handle to opened directory
    char m_buffer[BUFFER_SIZE];  // buffer for fs events
    OVERLAPPED* m_overlapped;

    wxDECLARE_NO_COPY_CLASS(wxFSWatchEntryMSW);
};

// ============================================================================
// wxFSWatcherImplMSW helper classes implementations
// ============================================================================

class wxIOCPService
{
public:
    wxIOCPService() :
        m_iocp(INVALID_HANDLE_VALUE)
    {
        Init();
    }

    ~wxIOCPService()
    {
        if (m_iocp != INVALID_HANDLE_VALUE)
        {
            if (!CloseHandle(m_iocp))
            {
                wxLogSysError(_("Unable to close I/O completion port handle"));
            }
        }
        m_watches.clear();
    }

    // associates a wxFSWatchEntryMSW with completion port
    bool Add(wxSharedPtr<wxFSWatchEntryMSW> watch)
    {
        wxCHECK_MSG( m_iocp != INVALID_HANDLE_VALUE, false, "IOCP not init" );
        wxCHECK_MSG( watch->IsOk(), false, "Invalid watch" );

        // associate with IOCP
        HANDLE ret = CreateIoCompletionPort(watch->GetHandle(), m_iocp,
                                            (ULONG_PTR)watch.get(), 0);
        if (ret == nullptr)
        {
            wxLogSysError(_("Unable to associate handle with "
                            "I/O completion port"));
            return false;
        }
        else if (ret != m_iocp)
        {
            wxFAIL_MSG(_("Unexpectedly new I/O completion port was created"));
            return false;
        }

        // add to watch map
        wxFSWatchEntries::value_type val(watch->GetPath(), watch);
        return m_watches.insert(val).second;
    }

    // Removes a watch we're currently using. Notice that this doesn't happen
    // immediately, CompleteRemoval() must be called later when it's really
    // safe to delete the watch, i.e. after completion of the IO operation
    // using it.
    bool ScheduleForRemoval(const wxSharedPtr<wxFSWatchEntryMSW>& watch)
    {
        wxCHECK_MSG( m_iocp != INVALID_HANDLE_VALUE, false, "IOCP not init" );
        wxCHECK_MSG( watch->IsOk(), false, "Invalid watch" );

        const wxString path = watch->GetPath();
        wxFSWatchEntries::iterator it = m_watches.find(path);
        wxCHECK_MSG( it != m_watches.end(), false,
                     "Can't remove a watch we don't use" );

        // We can't just delete the watch here as we can have pending events
        // for it and if we destroyed it now, we could get a dangling (or,
        // worse, reused to point to another object) pointer in ReadEvents() so
        // just remember that this one should be removed when CompleteRemoval()
        // is called later.
        m_removedWatches.push_back(watch);
        m_watches.erase(it);

        return true;
    }

    // Really remove the watch previously passed to ScheduleForRemoval().
    //
    // It's ok to call this for a watch that hadn't been removed before, in
    // this case we'll just return false and do nothing.
    bool CompleteRemoval(wxFSWatchEntryMSW* watch)
    {
        for ( Watches::iterator it = m_removedWatches.begin();
              it != m_removedWatches.end();
              ++it )
        {
            if ( (*it).get() == watch )
            {
                // Removing the object from here will result in deleting the
                // watch itself as it's not referenced from anywhere else now.
                m_removedWatches.erase(it);
                return true;
            }
        }

        return false;
    }

    // post completion packet
    bool PostEmptyStatus()
    {
        wxCHECK_MSG( m_iocp != INVALID_HANDLE_VALUE, false, "IOCP not init" );

        // The special values of 0 will make GetStatus() return Status_Exit.
        const int ret = PostQueuedCompletionStatus(m_iocp, 0, 0, nullptr);
        if (!ret)
        {
            wxLogSysError(_("Unable to post completion status"));
        }

        return ret != 0;
    }

    // Possible return values of GetStatus()
    enum Status
    {
        // Status successfully retrieved into the provided arguments.
        Status_OK,

        // Special status indicating that we should exit retrieved.
        Status_Exit,

        // An error occurred because the watched directory was deleted.
        Status_Deleted,

        // Some other error occurred.
        Status_Error
    };

    // Wait for completion status to arrive.
    // This function can block forever in it's wait for completion status.
    // Use PostEmptyStatus() to wake it up (and end the worker thread)
    Status
    GetStatus(DWORD* count, wxFSWatchEntryMSW** watch,
              OVERLAPPED** overlapped)
    {
        wxCHECK_MSG( m_iocp != INVALID_HANDLE_VALUE, Status_Error,
                     "Invalid IOCP object" );
        wxCHECK_MSG( count && watch && overlapped, Status_Error,
                     "Output parameters can't be null" );

        const int ret = GetQueuedCompletionStatus(m_iocp, count, (ULONG_PTR *)watch,
                                            overlapped, INFINITE);
        if ( ret != 0 )
        {
            return *count || *watch || *overlapped ? Status_OK : Status_Exit;
        }

        // An error is returned if the underlying directory has been deleted,
        // but this is not really an unexpected failure, so handle it
        // specially.
        if ( wxSysErrorCode() == ERROR_ACCESS_DENIED &&
                *watch && !wxFileName::DirExists((*watch)->GetPath()) )
            return Status_Deleted;

        // Some other error, at least log it.
        wxLogSysError(_("Unable to dequeue completion packet"));

        return Status_Error;
    }

protected:
    bool Init()
    {
        m_iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0);
        if (m_iocp == nullptr)
        {
            wxLogSysError(_("Unable to create I/O completion port"));
        }
        return m_iocp != nullptr;
    }

    HANDLE m_iocp;

    // The hash containing all the wxFSWatchEntryMSW objects currently being
    // watched keyed by their paths.
    wxFSWatchEntries m_watches;

    // Contains the watches which had been removed but are still pending.
    typedef wxVector< wxSharedPtr<wxFSWatchEntryMSW> > Watches;
    Watches m_removedWatches;
};


class wxIOCPThread : public wxThread
{
public:
    wxIOCPThread(wxFSWatcherImplMSW* service, wxIOCPService* iocp);

    // finishes this thread
    bool Finish();

protected:
    // structure to hold information needed to process one native event
    // this is just a dummy holder, so it doesn't take ownership of its data
    struct wxEventProcessingData
    {
        wxEventProcessingData(const FILE_NOTIFY_INFORMATION* ne,
                              const wxFSWatchEntryMSW* watch_) :
            nativeEvent(ne), watch(watch_)
        {}

        const FILE_NOTIFY_INFORMATION* nativeEvent;
        const wxFSWatchEntryMSW* watch;
    };

    virtual ExitCode Entry() override;

    // wait for events to occur, read them and send to interested parties
    // returns false it empty status was read, which means we would exit
    //         true otherwise
    bool ReadEvents();

    void ProcessNativeEvents(wxVector<wxEventProcessingData>& events);

    void SendEvent(wxFileSystemWatcherEvent& evt);

    static int Native2WatcherFlags(int flags);

    static wxString FileNotifyInformationToString(
                                            const FILE_NOTIFY_INFORMATION& e);

    static wxFileName GetEventPath(const wxFSWatchEntryMSW& watch,
                                   const FILE_NOTIFY_INFORMATION& e);

    wxFSWatcherImplMSW* m_service;
    wxIOCPService* m_iocp;
};

#endif /* WX_MSW_PRIVATE_FSWATCHER_H_ */
