/**
 * =========================================================================
 * File        : wdir_watch.cpp
 * Project     : 0 A.D.
 * Description : Win32 directory change notification
 *
 * @author Jan.Wassenberg@stud.uni-karlsruhe.de
 * =========================================================================
 */

/*
 * Copyright (c) 2004 Jan Wassenberg
 *
 * Redistribution and/or modification are also permitted under the
 * terms of the GNU General Public License as published by the
 * Free Software Foundation (version 2 or later, at your option).
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 */

#include "precompiled.h"

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

#include "lib/lib.h"
#include "lib/path_util.h"
#include "win_internal.h"
#include "lib/res/file/file.h"	// path_is_subpath


#pragma data_seg(WIN_CALLBACK_POST_ATEXIT(x))
WIN_REGISTER_FUNC(wdir_watch_shutdown);
#pragma data_seg()


// 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.


// list of all active watches. required, since we have to be able to
// cancel watches; also makes detecting duplicates possible.
//
// only store pointer in container - they're not copy-equivalent
// (dtor would close hDir).
//
// key is intptr_t "reqnum"; they aren't reused to avoid problems
// with stale reqnums after cancelling;
// hence, map instead of vector and freelist.
struct Watch;
typedef std::map<intptr_t, Watch*> Watches;
typedef Watches::iterator WatchIt;

// list of all active watches to detect duplicates and for easier cleanup.
// only store pointer in container - they're not copy-equivalent.
static Watches watches;

// 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

		watches[reqnum] = this;
	}

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

		watches[reqnum] = 0;
	}
};


// global state
static HANDLE hIOCP = 0;
	// not INVALID_HANDLE_VALUE! (CreateIoCompletionPort requirement)

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


typedef std::list<std::string> Events;
static Events pending_events;
	// 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.


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

	// free all (dynamically allocated) Watch objects
	for(WatchIt it = watches.begin(); it != watches.end(); ++it)
		delete it->second;
	watches.clear();

	return INFO_OK;
}


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


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

// 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* _reqnum)
{
	LibError err = ERR_FAIL;
	WIN_SAVE_LAST_ERROR;	// Create*

	intptr_t reqnum;
	*_reqnum = 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);

		// 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;
	*_reqnum = 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 = find_watch(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);

	delete w;
	watches[reqnum] = 0;
	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 = find_watch(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;
}
