/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

/* See `fsnotify_win/fsnotify.ml` for a general description. */

#include <caml/alloc.h>
#include <caml/callback.h>
#include <caml/custom.h>
#include <caml/fail.h>
#include <caml/memory.h>
#include <caml/mlvalues.h>
#include <caml/signals.h>
#include <caml/unixsupport.h>

#include <Windows.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

// Terrible guesstimate of ~1k files, highly unlikely,
// but it's ~16kb buffer.
#define FILE_NOTIFY_BUFFER_LENGTH ((sizeof(DWORD) * 4) * 1000)
#define FILE_NOTIFY_CONDITIONS                                  \
  (FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | \
   FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE |    \
   FILE_NOTIFY_CHANGE_CREATION)

// list of event
struct events {
  struct events* next;
  char* wpath; // the root directory
  FILE_NOTIFY_INFORMATION* info; // the list of changed files
};

// OCaml: Fsnotify.fsenv
struct fsenv {
  struct events* events; // the lock-less list of events
  HANDLE pipe; // the pipe to wakeup the OCaml program
};

// Parameter to the watching thread
struct wenv {
  char* wpath; // the directory to be watched
  struct fsenv* fsenv; // where to store the events.
};

// The pipes are only used signal the other thread that something is ready. So
// writing '.' to the pipe is sufficient.
static void signal_on_pipe(struct fsenv* fsenv) {
  char dot = '.';
  DWORD bytesWritten;
  WriteFile(fsenv->pipe, &dot, sizeof(dot), &bytesWritten, NULL);
  // If `WriteFail` fails, that's probably because the pipe is full,
  // or closed. Then we don't care.
}

// Push a new event in the shared list and wakeup the OCaml thread
static void push_info(struct wenv* wenv, FILE_NOTIFY_INFORMATION* info) {
  struct events* ev = (struct events*)malloc(sizeof(struct events));
  struct fsenv* fsenv = wenv->fsenv;
  ev->info = info;
  ev->wpath = wenv->wpath;
  do {
    ev->next = fsenv->events;
  } while (InterlockedCompareExchangePointer(
               (void**)&(fsenv->events), ev, ev->next) != ev->next);
  signal_on_pipe(fsenv);
}

// Pop all events from the shared list
static struct events* pop_events(struct fsenv* fsenv) {
  struct events* res;
  do {
    res = fsenv->events;
  } while (InterlockedCompareExchangePointer(
               (void**)&(fsenv->events), NULL, res) != res);
  return res;
}

// Converts a `struct events *` in an OCaml value of type `Fsnotify.event list`
static value parse_events(struct events* events) {
  CAMLparam0();
  CAMLlocal4(res, cell, ev, wpath);
  res = Val_unit;
  for (;;) {
    if (events == NULL)
      break;
    FILE_NOTIFY_INFORMATION* fileInfo = events->info;
    wpath = caml_copy_string(events->wpath);
    for (;;) {
      // Forcefully null terminate the filename.
      char* modifiedFilename = (char*)malloc(fileInfo->FileNameLength);
      wcstombs_s(
          NULL,
          modifiedFilename,
          fileInfo->FileNameLength,
          fileInfo->FileName,
          fileInfo->FileNameLength / sizeof(wchar_t));
      // Allocate 'Fsnotify.events'
      ev = caml_alloc_tuple(2);
      Store_field(ev, 0, caml_copy_string(modifiedFilename));
      Store_field(ev, 1, wpath);
      // Allocate list cell
      cell = caml_alloc_tuple(2);
      Store_field(cell, 0, ev);
      Store_field(cell, 1, res);
      res = cell;
      // Next info
      if (fileInfo->NextEntryOffset == 0)
        break;
      FILE_NOTIFY_INFORMATION* old = fileInfo;
      fileInfo =
          (FILE_NOTIFY_INFORMATION*)((char*)fileInfo + fileInfo->NextEntryOffset);
      free(old);
    }
    // Next event
    struct events* old = events;
    events = events->next;
    free(old);
  }
  CAMLreturn(res);
}

// Main function for the watching thread
static DWORD watcher_thread_main(LPVOID param) {
  char* buf;
  struct wenv* wenv = (struct wenv*)param;
  HANDLE dir_handle = CreateFile(
      wenv->wpath,
      FILE_LIST_DIRECTORY,
      FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
      NULL,
      OPEN_EXISTING,
      FILE_FLAG_BACKUP_SEMANTICS,
      NULL);

  if (dir_handle == INVALID_HANDLE_VALUE) {
    win32_maperr(GetLastError());
    uerror("CreateFile", Nothing);
  }

  while (TRUE) {
    DWORD size;
    buf = malloc(FILE_NOTIFY_BUFFER_LENGTH);
    if (!ReadDirectoryChangesW(
            dir_handle,
            buf,
            FILE_NOTIFY_BUFFER_LENGTH,
            TRUE,
            FILE_NOTIFY_CONDITIONS,
            &size,
            NULL,
            NULL)) {
      fprintf(stderr, "FATAL ERROR\n");
      fflush(stderr);
      break;
    };
    push_info(wenv, (FILE_NOTIFY_INFORMATION*)buf);
  }

  free(buf);
  CloseHandle(dir_handle);
  return 0;
}

// Stub: create a watching thread for a given directory
value caml_fsnotify_add_watch(value vfsenv, value path) {
  CAMLparam2(vfsenv, path);
  char output[_MAX_PATH];
  struct fsenv* fsenv = (struct fsenv*)vfsenv;
  struct wenv* wenv = malloc(sizeof(struct wenv));
  wenv->wpath = _strdup(_fullpath(output, String_val(path), _MAX_PATH));
  wenv->fsenv = fsenv;
  HANDLE th = CreateThread(NULL, 0, watcher_thread_main, wenv, 0, NULL);
  if (th == INVALID_HANDLE_VALUE) {
    win32_maperr(GetLastError());
    uerror("CreateThread", Nothing);
  }
  CAMLreturn(Val_long(th));
}

// Stub: read all events from the shared list
value caml_fsnotify_read_events(value vfsenv) {
  CAMLparam1(vfsenv);
  struct fsenv* fsenv = (struct fsenv*)vfsenv;
  struct events* events = pop_events(fsenv);
  CAMLreturn(parse_events(events));
}

// Stub: initialize the environment
value caml_fsnotify_init(value pipe) {
  CAMLparam1(pipe);
  struct fsenv* fsenv = malloc(sizeof(fsenv));
  fsenv->pipe = Handle_val(pipe);
  fsenv->events = NULL;
  CAMLreturn((value)fsenv);
}
