/**
 * =========================================================================
 * File        : wdir_watch.cpp
 * Project     : 0 A.D.
 * Description : Win32 directory change notification
 * =========================================================================
 */

// license: GPL; see lib/license.txt

#include "precompiled.h"
#include "lib/sysdep/dir_watch.h"

#include <string>
#include <map>
#include <list>

#include "lib/path_util.h"
#include "lib/allocators.h"
#include "lib/res/file/file.h"  // path_is_subpath
#include "win.h"
#include "winit.h"
#include "wutil.h"

WINIT_REGISTER_MAIN_INIT(wdir_watch_Init);
WINIT_REGISTER_MAIN_SHUTDOWN(wdir_watch_Shutdown);

// rationale for polling:
// much simpler than pure asynchronous notification: no need for a
// worker thread, mutex, and in/out queues. polling isn't inefficient:
// we do not examine each file; we only need to check if Windows
// has sent a change notification via ReadDirectoryChangesW.
//
// the main reason, however, is that user code will want to poll anyway,
// instead of select() from a worker thread: handling asynchronous file
// changes is much more work, requiring everything to be thread-safe.
// we currently poll once a frame, so that file changes will happen
// at a defined time.


// rationale for using I/O completion ports for notification:
// alternatives:
// - multiple threads with blocking I/O. a good many mount points
//   and therefore directory watches are possible, so this is out.
// - normal overlapped I/O: build a contiguous array of the hEvents
//   in all OVERLAPPED structures, and WaitForMultipleObjects.
//   having to (re)build the array after every watch add/remove sucks.
// - callback notification: notification function is called when the thread
//   that initiated the I/O (ReadDirectoryChangesW) enters an alertable
//   wait state (e.g. with SleepEx). we need to poll for notifications
//   from the mainline (see above). unfortunately, cannot come up with
//   a robust yet quick way of working off all pending APCs -
//   SleepEx(1) is a hack. even worse, it was noted in a previous project
//   that APCs are sometimes delivered from within Windows APIs, without
//   having used SleepEx (it seems threads enter an "AWS" sometimes when
//   calling the kernel).
//
// IOCPs work well and are elegant; have not yet noticed any drawbacks.
// the completion key is used to associate Watch with the directory handle.


// don't worry about size; heap-allocated.
struct Watch
{
    intptr_t reqnum;

    // (refcounted, since dir_add_watch reuses existing Watches)
    int refs;

    std::string dir_name;
    HANDLE hDir;

    // storage for RDC lpBytesReturned to avoid BoundsChecker warning
    // (dox are unclear on whether the pointer must be valid).
    DWORD dummy_nbytes;

    // fields aren't used.
    // overlapped I/O completation notification is via IOCP.
    OVERLAPPED ovl;

    // if too small, the current FILE_NOTIFY_INFORMATION is lost!
    // this is enough for ~7 packets (worst case) - should be enough,
    // since the app polls once a frame. we don't want to waste too much
    // memory. size chosen such that sizeof(Watch) = 4KiB.
    // issue code uses sizeof(change_buf) to determine size.
    //
    // note: we can't share one central buffer: the individual watches
    // are independent, and may be triggered 'simultaneously' before
    // the next app poll, so they'd overwrite one another.
    char change_buf[4096-58];


    Watch(intptr_t _reqnum, const std::string& _dir_name, HANDLE _hDir)
        : reqnum(_reqnum), refs(1), dir_name(_dir_name), hDir(_hDir)
    {
        memset(&ovl, 0, sizeof(ovl));
        // change_buf[] doesn't need init
    }

    ~Watch()
    {
        CloseHandle(hDir);
        hDir = INVALID_HANDLE_VALUE;
    }
};


//-----------------------------------------------------------------------------
// list of active watches

// we need to be able to cancel watches, which requires a 'list' of them.
// this also makes detecting duplicates possible and simplifies cleanup.
//
// key is intptr_t "reqnum"; they aren't reused to avoid problems with
// stale reqnums after canceling; hence, use map instead of array.
//
// only store pointer in container - they're not copy-equivalent
// (dtor would close hDir).
typedef std::map<intptr_t, Watch*> Watches;
typedef Watches::iterator WatchIt;
static Watches* watches;

static void FreeAllWatches()
{
    for(WatchIt it = watches->begin(); it != watches->end(); ++it)
    {
        Watch* w = it->second;
        delete w;
    }
    watches->clear();
}

static Watch* WatchFromReqnum(intptr_t reqnum)
{
    return (*watches)[reqnum];
}


//-----------------------------------------------------------------------------
// event queue

// rationale:
// we need a queue, instead of just taking events from the change_buf,
// because we need to re-issue the watch immediately after it returns
// data. of course we can't have the app read from the buffer while
// waiting for RDC to write to the buffer - race condition.
// an alternative to a queue would be to allocate another buffer,
// but that's more complicated, and this way is cleaner anyway.
typedef std::list<std::string> Events;
static Events* pending_events;


//-----------------------------------------------------------------------------
// allocate static objects

// manual allocation and construction/destruction of static objects is
// required because winit calls us before static ctors and after dtors.

static void AllocStaticObjects()
{
    STATIC_STORAGE(ss, 200);
#include "lib/nommgr.h"
    void* addr1 = static_calloc(&ss, sizeof(Watches));
    watches = new(addr1) Watches;

    void* addr2 = static_calloc(&ss, sizeof(Events));
    pending_events = new(addr2) Events;
#include "lib/mmgr.h"
}

static void FreeStaticObjects()
{
    watches->~Watches();
    pending_events->~Events();
}


//-----------------------------------------------------------------------------
// global state

static HANDLE hIOCP = 0;    // CreateIoCompletionPort requires 0, not INVALID_HANDLE_VALUE

// note: the start value provides a little protection against bogus reqnums
// (we don't bother using a tag for safety though - it isn't important)
static intptr_t last_reqnum = 1000;


// HACK - see call site
static void get_packet();



// path: portable and relative, must add current directory and convert to native
// better to use a cached string from rel_chdir - secure
LibError dir_add_watch(const char* dir, intptr_t* preqnum)
{
    LibError err = ERR::FAIL;
    WIN_SAVE_LAST_ERROR;    // Create*

    intptr_t reqnum;
    *preqnum = 0;

    {
    const std::string dir_s(dir);

    // check if this is a subdirectory of an already watched dir tree
    // (much faster than issuing a new watch for every subdir).
    // this also prevents watching the same directory twice.
    for(WatchIt it = watches->begin(); it != watches->end(); ++it)
    {
        Watch* const w = it->second;
        if(!w)
            continue;
        const char* old_dir = w->dir_name.c_str();
        if(path_is_subpath(dir, old_dir))
        {
            reqnum = w->reqnum;
            w->refs++;
            goto done;
        }
    }

    // open handle to directory
    const DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
    const DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED;
    const HANDLE hDir = CreateFile(dir, FILE_LIST_DIRECTORY, share, 0, OPEN_EXISTING, flags, 0);
    if(hDir == INVALID_HANDLE_VALUE)
        goto fail;

    // assign a new (unique) request number. don't do this earlier - prevents
    // DOS via wasting reqnums due to invalid directory parameters.
    // need it before binding dir to IOCP because it is our "key".
    if(last_reqnum == INT_MAX)
    {
        debug_warn("request numbers are no longer unique");
        CloseHandle(hDir);
        goto fail;
    }
    reqnum = ++last_reqnum;

    // associate Watch* with the directory handle. when we receive a packet
    // from the IOCP, we will need to re-issue the watch.
    const ULONG_PTR key = (ULONG_PTR)reqnum;

    // create IOCP (if not already done) and attach hDir to it
    hIOCP = CreateIoCompletionPort(hDir, hIOCP, key, 0);
    if(hIOCP == 0 || hIOCP == INVALID_HANDLE_VALUE)
    {
        CloseHandle(hDir);
        goto fail;
    }

    // allocate watch, add to list, associate with reqnum
    // note: can't use SAFE_NEW due to ctor params.
    try
    {
        Watch* w = new Watch(reqnum, dir_s, hDir);
        (*watches)[reqnum] = w;

        // add trailing \ if not already there
        if(dir_s[dir_s.length()-1] != '\\')
            w->dir_name += '\\';
    }
    catch(std::bad_alloc)
    {
        goto fail;
    }


    // post a dummy kickoff packet; the IOCP polling code will "re"issue
    // the corresponding watch. this keeps the ReadDirectoryChangesW call
    // and directory <--> Watch association code in one place.
    //
    // we call get_packet so that it's issued immediately,
    // instead of only at the next call to dir_get_changed_file.
    PostQueuedCompletionStatus(hIOCP, 0, key, 0);
    get_packet();
    }

done:
    err = INFO::OK;
    *preqnum = reqnum;

fail:
    WIN_RESTORE_LAST_ERROR;
    return err;
}


LibError dir_cancel_watch(const intptr_t reqnum)
{
    if(reqnum <= 0)
        WARN_RETURN(ERR::INVALID_PARAM);

    Watch* w = WatchFromReqnum(reqnum);
    // watches[reqnum] is invalid - big trouble
    if(!w)
        WARN_RETURN(ERR::FAIL);

    // we're freeing a reference - done.
    debug_assert(w->refs >= 1);
    if(--w->refs != 0)
        return INFO::OK;

    // contrary to dox, the RDC IOs do not issue a completion notification.
    // no packet was received on the IOCP while or after cancelling in a test.
    //
    // if cancel somehow fails though, no matter - the Watch is freed, and
    // its reqnum isn't reused; if we receive a packet, it's ignored.
    BOOL ok = CancelIo(w->hDir);

    (*watches)[reqnum] = 0;
    delete w;
    return LibError_from_win32(ok);
}


static void extract_events(Watch* w)
{
    debug_assert(w);

    // points to current FILE_NOTIFY_INFORMATION;
    // char* simplifies advancing to the next (variable length) FNI.
    char* pos = w->change_buf;

    // for every packet in buffer: (there's at least one)
    for(;;)
    {
        const FILE_NOTIFY_INFORMATION* fni = (const FILE_NOTIFY_INFORMATION*)pos;

        // convert filename from Windows BSTR
        // (can't use wcstombs - FileName isn't 0-terminated)
        std::string fn = w->dir_name;
        for(int i = 0; i < (int)fni->FileNameLength/2; i++)
            fn += (char)fni->FileName[i];

        pending_events->push_back(fn);

        // advance to next entry in buffer (variable length)
        const DWORD ofs = fni->NextEntryOffset;
        // .. this one was the last - done.
        if(!ofs)
            break;
        pos += ofs;
    }
}


// if a packet is pending, extract its events, post them in the queue and
// re-issue its watch.
static void get_packet()
{
    // poll for change notifications from all pending watches
    DWORD bytes_transferred;    // used to determine if packet is valid or a kickoff
    ULONG_PTR key;
    OVERLAPPED* povl;
    BOOL got_packet = GetQueuedCompletionStatus(hIOCP, &bytes_transferred, &key, &povl, 0);
    if(!got_packet) // no new packet - done
        return;

    const intptr_t reqnum = (intptr_t)key;
    Watch* const w = WatchFromReqnum(reqnum);
    // watch was subsequently removed - ignore the error.
    if(!w)
        return;

    // this is an actual packet, not just a kickoff for issuing the watch.
    // extract the events and push them onto AppState's queue.
    if(bytes_transferred != 0)
        extract_events(w);

    // (re-)issue change notification request.
    // it's safe to reuse Watch.change_buf, because we copied out all events.
    const DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME |
                         FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE |
                         FILE_NOTIFY_CHANGE_CREATION;
    const DWORD buf_size = sizeof(w->change_buf);
    memset(&w->ovl, 0, sizeof(w->ovl));
    BOOL watch_subtree = TRUE;
        // much faster than watching every dir separately. see dir_add_watch.
    BOOL ok = ReadDirectoryChangesW(w->hDir, w->change_buf, buf_size, watch_subtree, filter, &w->dummy_nbytes, &w->ovl, 0);
    WARN_IF_FALSE(ok);
}


// if a file change notification is pending, store its filename in <fn> and
// return INFO::OK; otherwise, return ERR::AGAIN ('none currently pending') or
// a negative error code.
// <fn> must hold at least PATH_MAX chars.
LibError dir_get_changed_file(char* fn)
{
    // may or may not queue event(s).
    get_packet();

    // nothing to return; call again later.
    if(pending_events->empty())
        return ERR::AGAIN;  // NOWARN

    const std::string& fn_s = pending_events->front();
    strcpy_s(fn, PATH_MAX, fn_s.c_str());
    pending_events->pop_front();

    return INFO::OK;
}


//-----------------------------------------------------------------------------

static LibError wdir_watch_Init()
{
    AllocStaticObjects();
    return INFO::OK;
}

static LibError wdir_watch_Shutdown()
{
    CloseHandle(hIOCP);
    hIOCP = INVALID_HANDLE_VALUE;

    FreeAllWatches();
    FreeStaticObjects();

    return INFO::OK;
}