/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#include "system-precomp.h"  // Precompiled headers
//
#include <mrpt/config.h>

#ifdef _WIN32
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0400
#endif
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#if MRPT_HAS_INOTIFY
#include <sys/inotify.h>
#endif

#if HAVE_SYS_TIME_H
#include <sys/time.h>
#endif

#include <unistd.h>

#include <cerrno>
#include <cstdio>
#endif

#include <mrpt/core/exceptions.h>  // for THROW_EXCEPTION, ASSERT_
#include <mrpt/system/CFileSystemWatcher.h>
#include <mrpt/system/filesystem.h>  // for directoryExists()

#include <cstring>  // for NULL, memcpy

using namespace mrpt::system;
using namespace std;

/*---------------------------------------------------------------
          Constructor
 ---------------------------------------------------------------*/
CFileSystemWatcher::CFileSystemWatcher(const std::string& path) : m_watchedDirectory(path)
{
  MRPT_START
  ASSERT_(!path.empty());

  if (m_watchedDirectory[m_watchedDirectory.size() - 1] != '/' &&
      m_watchedDirectory[m_watchedDirectory.size() - 1] != '\\')
    m_watchedDirectory.push_back('/');

#ifdef _WIN32
  // Windows version:
  HANDLE hDir = CreateFileA(
      path.c_str(), FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
      nullptr,  // security attributes
      OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
  if (hDir == INVALID_HANDLE_VALUE)
  {
    m_hNotif = nullptr;
    THROW_EXCEPTION("FindFirstChangeNotificationA returned error!");
  }

  m_hNotif = static_cast<void*>(hDir);

  m_watchThread = std::thread(&CFileSystemWatcher::thread_win32_watch, this);

#else
#if MRPT_HAS_INOTIFY
  // Linux version:
  m_wd = -1;

  m_fd = inotify_init();
  if (m_fd < 0) THROW_EXCEPTION("inotify_init returned error!");

  // Create watcher:
  m_wd = inotify_add_watch(
      m_fd, path.c_str(),
      IN_CLOSE_WRITE | IN_DELETE | IN_MOVED_TO | IN_MOVED_FROM | IN_CREATE | IN_ACCESS);

  if (m_wd < 0) THROW_EXCEPTION("inotify_add_watch returned error!");
#endif
#endif
  MRPT_END
}

/*---------------------------------------------------------------
          Destructor
 ---------------------------------------------------------------*/
CFileSystemWatcher::~CFileSystemWatcher()
{
#ifdef _WIN32
  // Windows version:
  if (m_hNotif)
  {
    // Kill thread:
    CloseHandle(HANDLE(m_hNotif));
    m_hNotif = nullptr;
  }
#else
#if MRPT_HAS_INOTIFY
  // Linux version:
  if (m_fd >= 0)
  {
    close(m_fd);
    m_fd = -1;
    if (m_wd >= 0) inotify_rm_watch(m_fd, m_wd);
  }
#endif
#endif
}

/*---------------------------------------------------------------
          getChanges
 ---------------------------------------------------------------*/
void CFileSystemWatcher::getChanges(TFileSystemChangeList& out_list)
{
  out_list.clear();

#ifdef _WIN32
  // Windows version:
  ASSERTMSG_(m_hNotif != nullptr, "CFileSystemWatcher was not initialized correctly.");

  // Work is done in thread_win32_watch().
  // Just check for incoming mail:
  while (!m_queue_events_win32_msgs.empty())
  {
    TFileSystemChange* obj = nullptr;
    {
      std::lock_guard<std::mutex> lock(m_queue_events_win32_cs);
      if (!m_queue_events_win32_msgs.empty())
      {
        auto ret = m_queue_events_win32_msgs.front();
        m_queue_events_win32_msgs.pop();
        obj = ret;
      }
    }
    if (obj)
    {
      out_list.push_back(*obj);
      delete obj;
    }
  }

#else
#if MRPT_HAS_INOTIFY
  if (m_fd < 0) return;  // Not open?

  // Linux version:
  // Refer to:
  //  http://www.linuxjournal.com/article/8478
  //  http://inotify.aiken.cz/?section=common&page=home&lang=en
  struct timeval time
  {
  };
  fd_set rfds;
  int ret;

  // timeout
  time.tv_sec = 0;
  time.tv_usec = 100;

  // zero-out the fd_set
  FD_ZERO(&rfds);

  // Add inotify fd
  FD_SET(m_fd, &rfds);

  ret = select(m_fd + 1, &rfds, nullptr, nullptr, &time);
  if (ret < 0)
  {
    perror("[CFileSystemWatcher::getChanges] select");
    return;
  }

  else if (!ret)
  {
    // timed out!
  }
  else if (FD_ISSET(m_fd, &rfds))
  {
    // inotify events are available! : Read them!

    /* size of the event structure, not counting name */
    constexpr size_t EVENT_SIZE = (sizeof(struct inotify_event));

    /* reasonable guess as to size of 1024 events */
    constexpr size_t BUF_LEN = (1024 * (EVENT_SIZE + 16));

    char buf[BUF_LEN];
    ssize_t i = 0;

    const auto len = read(m_fd, buf, BUF_LEN);
    if (len < 0)
    {
      if (errno == EINTR)
      { /* need to reissue system call */
      }
      else
        perror("[CFileSystemWatcher::getChanges] read");
    }
    else if (!len)
    { /* BUF_LEN too small? */
    }

    while (i < len)
    {
      const inotify_event* event = reinterpret_cast<const inotify_event*>(&buf[i]);

      i += EVENT_SIZE + event->len;

      // printf ("wd=%d mask=%u cookie=%u len=%u\n",event->wd,
      // event->mask,event->cookie, event->len);

      string eventName;
      if (event->len) eventName = event->name;

      // Add event to output list:
      // ---------------------------------------------
      if (0 == (event->mask & IN_UNMOUNT) && 0 == (event->mask & IN_Q_OVERFLOW) &&
          0 == (event->mask & IN_IGNORED))
      {
        TFileSystemChange newEntry;

        newEntry.path = m_watchedDirectory + eventName;
        newEntry.isDir = event->mask & IN_ISDIR;
        newEntry.eventModified = event->mask & IN_MODIFY;
        newEntry.eventCloseWrite = event->mask & IN_CLOSE_WRITE;
        newEntry.eventDeleted = event->mask & IN_DELETE;
        newEntry.eventMovedTo = event->mask & IN_MOVED_TO;
        newEntry.eventMovedFrom = event->mask & IN_MOVED_FROM;
        newEntry.eventCreated = event->mask & IN_CREATE;
        newEntry.eventAccessed = event->mask & IN_ACCESS;

        out_list.push_back(newEntry);
      }
    }
  }
#endif
#endif
}

#ifdef _WIN32

void CFileSystemWatcher::thread_win32_watch()
{
  uint8_t buf[8 * 1024];
  DWORD dwRead = 0;

  while (ReadDirectoryChangesW(
      HANDLE(m_hNotif), buf, sizeof(buf),
      false,  // No subtree
      FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES |
          FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_LAST_ACCESS |
          FILE_NOTIFY_CHANGE_CREATION,
      &dwRead, nullptr, nullptr))
  {
    // Interpret read data as FILE_NOTIFY_INFORMATION:
    // There might be several notifications in the same data block:
    size_t idx = 0;
    for (;;)
    {
      // Yep... this is MS's idea of a beautiful and easy way to return
      // data:
      FILE_NOTIFY_INFORMATION* fni = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(&buf[idx]);

      // Extract the name (stored as a WCHAR*) into a UTF-8 std::string:
      ASSERTMSG_(fni->FileNameLength < 10000, "Name length >10K... this is probably an error");

      int reqLen = WideCharToMultiByte(
          CP_UTF8, 0, fni->FileName, fni->FileNameLength >> 1, nullptr, 0, nullptr, nullptr);
      std::vector<char> tmpBuf(reqLen);
      int actLen = WideCharToMultiByte(
          CP_UTF8, 0, fni->FileName, fni->FileNameLength >> 1, &tmpBuf[0], tmpBuf.size(), nullptr,
          nullptr);
      ASSERTMSG_(actLen > 0, "Error converting filename from WCHAR* to UTF8");

      const std::string filName(&tmpBuf[0], actLen);

      TFileSystemChange newEntry;
      newEntry.path = m_watchedDirectory + filName;
      newEntry.isDir = mrpt::system::directoryExists(newEntry.path);

      // Fill out the new entry:
      std::lock_guard<std::mutex> lock(m_queue_events_win32_cs);
      switch (fni->Action)
      {
        case FILE_ACTION_ADDED:
        {
          newEntry.eventCreated = true;
          m_queue_events_win32_msgs.push(new TFileSystemChange(newEntry));
        }
        break;
        case FILE_ACTION_REMOVED:
        {
          newEntry.eventDeleted = true;
          m_queue_events_win32_msgs.push(new TFileSystemChange(newEntry));
        }
        break;
        case FILE_ACTION_MODIFIED:
        {
          newEntry.eventModified = true;
          m_queue_events_win32_msgs.push(new TFileSystemChange(newEntry));
        }
        break;
        case FILE_ACTION_RENAMED_OLD_NAME:
        {
          newEntry.eventMovedFrom = true;
          m_queue_events_win32_msgs.push(new TFileSystemChange(newEntry));
        }
        break;
        case FILE_ACTION_RENAMED_NEW_NAME:
        {
          newEntry.eventMovedTo = true;
          m_queue_events_win32_msgs.push(new TFileSystemChange(newEntry));
        }
        break;
      }

      // Next entry?
      if (fni->NextEntryOffset > 0)
        idx += fni->NextEntryOffset;
      else
        break;  // done
    }
  }

  printf("Done!");
}
#endif
