#include "cache.h"
#include "config.h"
#include "fsmonitor.h"
#include "fsm-health.h"
#include "fsmonitor--daemon.h"

/*
 * Every minute wake up and test our health.
 */
#define WAIT_FREQ_MS (60 * 1000)

/*
 * State machine states for each of the interval functions
 * used for polling our health.
 */
enum interval_fn_ctx {
	CTX_INIT = 0,
	CTX_TERM,
	CTX_TIMER
};

typedef int (interval_fn)(struct fsmonitor_daemon_state *state,
			  enum interval_fn_ctx ctx);

struct fsm_health_data
{
	HANDLE hEventShutdown;

	HANDLE hHandles[1]; /* the array does not own these handles */
#define HEALTH_SHUTDOWN 0
	int nr_handles; /* number of active event handles */

	struct wt_moved
	{
		wchar_t wpath[MAX_PATH + 1];
		BY_HANDLE_FILE_INFORMATION bhfi;
	} wt_moved;
};

/*
 * Lookup the system unique ID for the path.  This is as close as
 * we get to an inode number, but this also contains volume info,
 * so it is a little stronger.
 */
static int lookup_bhfi(wchar_t *wpath,
		       BY_HANDLE_FILE_INFORMATION *bhfi)
{
	DWORD desired_access = FILE_LIST_DIRECTORY;
	DWORD share_mode =
		FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE;
	HANDLE hDir;

	hDir = CreateFileW(wpath, desired_access, share_mode, NULL,
			   OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
	if (hDir == INVALID_HANDLE_VALUE) {
		error(_("[GLE %ld] health thread could not open '%ls'"),
		      GetLastError(), wpath);
		return -1;
	}

	if (!GetFileInformationByHandle(hDir, bhfi)) {
		error(_("[GLE %ld] health thread getting BHFI for '%ls'"),
		      GetLastError(), wpath);
		CloseHandle(hDir);
		return -1;
	}

	CloseHandle(hDir);
	return 0;
}

/*
 * Compare the relevant fields from two system unique IDs.
 * We use this to see if two different handles to the same
 * path actually refer to the same *instance* of the file
 * or directory.
 */
static int bhfi_eq(const BY_HANDLE_FILE_INFORMATION *bhfi_1,
		   const BY_HANDLE_FILE_INFORMATION *bhfi_2)
{
	return (bhfi_1->dwVolumeSerialNumber == bhfi_2->dwVolumeSerialNumber &&
		bhfi_1->nFileIndexHigh == bhfi_2->nFileIndexHigh &&
		bhfi_1->nFileIndexLow == bhfi_2->nFileIndexLow);
}

/*
 * Shutdown if the original worktree root directory been deleted,
 * moved, or renamed?
 *
 * Since the main thread did a "chdir(getenv($HOME))" and our CWD
 * is not in the worktree root directory and because the listener
 * thread added FILE_SHARE_DELETE to the watch handle, it is possible
 * for the root directory to be moved or deleted while we are still
 * watching it.  We want to detect that here and force a shutdown.
 *
 * Granted, a delete MAY cause some operations to fail, such as
 * GetOverlappedResult(), but it is not guaranteed.  And because
 * ReadDirectoryChangesW() only reports on changes *WITHIN* the
 * directory, not changes *ON* the directory, our watch will not
 * receive a delete event for it.
 *
 * A move/rename of the worktree root will also not generate an event.
 * And since the listener thread already has an open handle, it may
 * continue to receive events for events within the directory.
 * However, the pathname of the named-pipe was constructed using the
 * original location of the worktree root.  (Remember named-pipes are
 * stored in the NPFS and not in the actual file system.)  Clients
 * trying to talk to the worktree after the move/rename will not
 * reach our daemon process, since we're still listening on the
 * pipe with original path.
 *
 * Furthermore, if the user does something like:
 *
 *   $ mv repo repo.old
 *   $ git init repo
 *
 * A new daemon cannot be started in the new instance of "repo"
 * because the named-pipe is still being used by the daemon on
 * the original instance.
 *
 * So, detect move/rename/delete and shutdown.  This should also
 * handle unsafe drive removal.
 *
 * We use the file system unique ID to distinguish the original
 * directory instance from a new instance and force a shutdown
 * if the unique ID changes.
 *
 * Since a worktree move/rename/delete/unmount doesn't happen
 * that often (and we can't get an immediate event anyway), we
 * use a timeout and periodically poll it.
 */
static int has_worktree_moved(struct fsmonitor_daemon_state *state,
			      enum interval_fn_ctx ctx)
{
	struct fsm_health_data *data = state->health_data;
	BY_HANDLE_FILE_INFORMATION bhfi;
	int r;

	switch (ctx) {
	case CTX_TERM:
		return 0;

	case CTX_INIT:
		if (xutftowcs_path(data->wt_moved.wpath,
				   state->path_worktree_watch.buf) < 0) {
			error(_("could not convert to wide characters: '%s'"),
			      state->path_worktree_watch.buf);
			return -1;
		}

		/*
		 * On the first call we lookup the unique sequence ID for
		 * the worktree root directory.
		 */
		return lookup_bhfi(data->wt_moved.wpath, &data->wt_moved.bhfi);

	case CTX_TIMER:
		r = lookup_bhfi(data->wt_moved.wpath, &bhfi);
		if (r)
			return r;
		if (!bhfi_eq(&data->wt_moved.bhfi, &bhfi)) {
			error(_("BHFI changed '%ls'"), data->wt_moved.wpath);
			return -1;
		}
		return 0;

	default:
		die(_("unhandled case in 'has_worktree_moved': %d"),
		    (int)ctx);
	}

	return 0;
}


int fsm_health__ctor(struct fsmonitor_daemon_state *state)
{
	struct fsm_health_data *data;

	CALLOC_ARRAY(data, 1);

	data->hEventShutdown = CreateEvent(NULL, TRUE, FALSE, NULL);

	data->hHandles[HEALTH_SHUTDOWN] = data->hEventShutdown;
	data->nr_handles++;

	state->health_data = data;
	return 0;
}

void fsm_health__dtor(struct fsmonitor_daemon_state *state)
{
	struct fsm_health_data *data;

	if (!state || !state->health_data)
		return;

	data = state->health_data;

	CloseHandle(data->hEventShutdown);

	FREE_AND_NULL(state->health_data);
}

/*
 * A table of the polling functions.
 */
static interval_fn *table[] = {
	has_worktree_moved,
	NULL, /* must be last */
};

/*
 * Call all of the polling functions in the table.
 * Shortcut and return first error.
 *
 * Return 0 if all succeeded.
 */
static int call_all(struct fsmonitor_daemon_state *state,
		    enum interval_fn_ctx ctx)
{
	int k;

	for (k = 0; table[k]; k++) {
		int r = table[k](state, ctx);
		if (r)
			return r;
	}

	return 0;
}

void fsm_health__loop(struct fsmonitor_daemon_state *state)
{
	struct fsm_health_data *data = state->health_data;
	int r;

	r = call_all(state, CTX_INIT);
	if (r < 0)
		goto force_error_stop;
	if (r > 0)
		goto force_shutdown;

	for (;;) {
		DWORD dwWait = WaitForMultipleObjects(data->nr_handles,
						      data->hHandles,
						      FALSE, WAIT_FREQ_MS);

		if (dwWait == WAIT_OBJECT_0 + HEALTH_SHUTDOWN)
			goto clean_shutdown;

		if (dwWait == WAIT_TIMEOUT) {
			r = call_all(state, CTX_TIMER);
			if (r < 0)
				goto force_error_stop;
			if (r > 0)
				goto force_shutdown;
			continue;
		}

		error(_("health thread wait failed [GLE %ld]"),
		      GetLastError());
		goto force_error_stop;
	}

force_error_stop:
	state->health_error_code = -1;
force_shutdown:
	ipc_server_stop_async(state->ipc_server_data);
clean_shutdown:
	call_all(state, CTX_TERM);
	return;
}

void fsm_health__stop_async(struct fsmonitor_daemon_state *state)
{
	SetEvent(state->health_data->hHandles[HEALTH_SHUTDOWN]);
}
