/**
 * @file
 * Monitor files for changes
 *
 * @authors
 * Copyright (C) 2018 Gero Treuer <gero@70t.de>
 * Copyright (C) 2018-2024 Richard Russon <rich@flatcap.org>
 * Copyright (C) 2019 Ian Zimmerman <itz@no-use.mooo.com>
 * Copyright (C) 2020 Pietro Cerutti <gahr@gahr.ch>
 * Copyright (C) 2020 R Primus <rprimus@gmail.com>
 *
 * @copyright
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 2 of the License, or (at your option) any later
 * version.
 *
 * 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.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * @page neo_monitor Monitor files for changes
 *
 * Monitor files for changes
 */

#include "config.h"
#include <errno.h>
#include <limits.h>
#include <poll.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "mutt/lib.h"
#include "core/lib.h"
#include "monitor.h"
#include "index/lib.h"
#ifndef HAVE_INOTIFY_INIT1
#include <fcntl.h>
#endif

/// Set to true when a monitored file has changed
bool MonitorFilesChanged = false;
/// Set to true when the current mailbox has changed
bool MonitorCurMboxChanged = false;

/// Inotify file descriptor
static int INotifyFd = -1;
/// Linked list of monitored Mailboxes
static struct Monitor *Monitor = NULL;
/// Number of used entries in the #PollFds array
static size_t PollFdsCount = 0;
/// Size of #PollFds array
static size_t PollFdsLen = 0;
/// Array of monitored file descriptors
static struct pollfd *PollFds = NULL;
/// Monitor file descriptor of the current mailbox
static int MonitorCurMboxDescriptor = -1;

#define INOTIFY_MASK_DIR (IN_MOVED_TO | IN_ATTRIB | IN_CLOSE_WRITE | IN_ISDIR)
#define INOTIFY_MASK_FILE IN_CLOSE_WRITE

#define EVENT_BUFLEN MAX(4096, sizeof(struct inotify_event) + NAME_MAX + 1)

/**
 * enum ResolveResult - Results for the Monitor functions
 */
enum ResolveResult
{
  RESOLVE_RES_FAIL_NOMAILBOX = -3, ///< No Mailbox to work on
  RESOLVE_RES_FAIL_NOTYPE = -2,    ///< Can't identify Mailbox type
  RESOLVE_RES_FAIL_STAT = -1,      ///< Can't stat() the Mailbox file
  RESOLVE_RES_OK_NOTEXISTING = 0,  ///< File exists, no monitor is attached
  RESOLVE_RES_OK_EXISTING = 1,     ///< File exists, monitor is already attached
};

/**
 * struct Monitor - A watch on a file
 */
struct Monitor
{
  struct Monitor *next; ///< Linked list
  char *mh_backup_path;
  dev_t st_dev;
  ino_t st_ino;
  enum MailboxType type;
  int desc;
};

/**
 * struct MonitorInfo - Information about a monitored file
 */
struct MonitorInfo
{
  enum MailboxType type;
  bool is_dir;
  const char *path;
  dev_t st_dev;
  ino_t st_ino;
  struct Monitor *monitor;
  struct Buffer path_buf; ///< access via path only (maybe not initialized)
};

/**
 * mutt_poll_fd_add - Add a file to the watch list
 * @param fd     File to watch
 * @param events Events to listen for, e.g. POLLIN
 */
static void mutt_poll_fd_add(int fd, short events)
{
  int i = 0;
  for (; (i < PollFdsCount) && (PollFds[i].fd != fd); i++)
    ; // do nothing

  if (i == PollFdsCount)
  {
    if (PollFdsCount == PollFdsLen)
    {
      PollFdsLen += 2;
      MUTT_MEM_REALLOC(&PollFds, PollFdsLen, struct pollfd);
    }
    PollFdsCount++;
    PollFds[i].fd = fd;
    PollFds[i].events = events;
  }
  else
  {
    PollFds[i].events |= events;
  }
}

/**
 * mutt_poll_fd_remove - Remove a file from the watch list
 * @param fd File to remove
 * @retval  0 Success
 * @retval -1 Error
 */
static int mutt_poll_fd_remove(int fd)
{
  int i = 0;
  for (; (i < PollFdsCount) && (PollFds[i].fd != fd); i++)
    ; // do nothing

  if (i == PollFdsCount)
    return -1;
  int d = PollFdsCount - i - 1;
  if (d != 0)
    memmove(&PollFds[i], &PollFds[i + 1], d * sizeof(struct pollfd));
  PollFdsCount--;
  return 0;
}

/**
 * monitor_init - Set up file monitoring
 * @retval  0 Success
 * @retval -1 Error
 */
static int monitor_init(void)
{
  if (INotifyFd != -1)
    return 0;

#ifdef HAVE_INOTIFY_INIT1
  INotifyFd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
  if (INotifyFd == -1)
  {
    mutt_debug(LL_DEBUG2, "inotify_init1 failed, errno=%d %s\n", errno, strerror(errno));
    return -1;
  }
#else
  INotifyFd = inotify_init();
  if (INotifyFd == -1)
  {
    mutt_debug(LL_DEBUG2, "monitor: inotify_init failed, errno=%d %s\n", errno,
               strerror(errno));
    return -1;
  }
  fcntl(INotifyFd, F_SETFL, O_NONBLOCK);
  fcntl(INotifyFd, F_SETFD, FD_CLOEXEC);
#endif
  mutt_poll_fd_add(0, POLLIN);
  mutt_poll_fd_add(INotifyFd, POLLIN);

  return 0;
}

/**
 * monitor_check_cleanup - Close down file monitoring
 */
static void monitor_check_cleanup(void)
{
  if (!Monitor && (INotifyFd != -1))
  {
    mutt_poll_fd_remove(INotifyFd);
    close(INotifyFd);
    INotifyFd = -1;
    MonitorFilesChanged = false;
  }
}

/**
 * monitor_new - Create a new file monitor
 * @param info       Details of file to monitor
 * @param descriptor Watch descriptor
 * @retval ptr Newly allocated Monitor
 */
static struct Monitor *monitor_new(struct MonitorInfo *info, int descriptor)
{
  struct Monitor *monitor = MUTT_MEM_CALLOC(1, struct Monitor);
  monitor->type = info->type;
  monitor->st_dev = info->st_dev;
  monitor->st_ino = info->st_ino;
  monitor->desc = descriptor;
  monitor->next = Monitor;
  if (info->type == MUTT_MH)
    monitor->mh_backup_path = mutt_str_dup(info->path);

  Monitor = monitor;

  return monitor;
}

/**
 * monitor_info_free - Shutdown a file monitor
 * @param info Monitor to shut down
 */
static void monitor_info_free(struct MonitorInfo *info)
{
  buf_dealloc(&info->path_buf);
}

/**
 * monitor_delete - Free a file monitor
 * @param monitor Monitor to free
 */
static void monitor_delete(struct Monitor *monitor)
{
  if (!monitor)
    return;

  struct Monitor **ptr = &Monitor;

  while (true)
  {
    if (!*ptr)
      return;
    if (*ptr == monitor)
      break;
    ptr = &(*ptr)->next;
  }

  FREE(&monitor->mh_backup_path);
  monitor = monitor->next;
  FREE(ptr);
  *ptr = monitor;
}

/**
 * monitor_handle_ignore - Listen for when a backup file is closed
 * @param desc Watch descriptor
 * @retval >=0 New descriptor
 * @retval  -1 Error
 */
static int monitor_handle_ignore(int desc)
{
  int new_desc = -1;
  struct Monitor *iter = Monitor;
  struct stat st = { 0 };

  while (iter && (iter->desc != desc))
    iter = iter->next;

  if (iter)
  {
    if ((iter->type == MUTT_MH) && (stat(iter->mh_backup_path, &st) == 0))
    {
      new_desc = inotify_add_watch(INotifyFd, iter->mh_backup_path, INOTIFY_MASK_FILE);
      if (new_desc == -1)
      {
        mutt_debug(LL_DEBUG2, "inotify_add_watch failed for '%s', errno=%d %s\n",
                   iter->mh_backup_path, errno, strerror(errno));
      }
      else
      {
        mutt_debug(LL_DEBUG3, "inotify_add_watch descriptor=%d for '%s'\n",
                   desc, iter->mh_backup_path);
        iter->st_dev = st.st_dev;
        iter->st_ino = st.st_ino;
        iter->desc = new_desc;
      }
    }
    else
    {
      mutt_debug(LL_DEBUG3, "cleanup watch (implicitly removed) - descriptor=%d\n", desc);
    }

    if (MonitorCurMboxDescriptor == desc)
      MonitorCurMboxDescriptor = new_desc;

    if (new_desc == -1)
    {
      monitor_delete(iter);
      monitor_check_cleanup();
    }
  }

  return new_desc;
}

/**
 * monitor_resolve - Get the monitor for a mailbox
 * @param[out] info Details of the mailbox's monitor
 * @param[in]  m    Mailbox
 * @retval >=0 mailbox is valid and locally accessible:
 *               0: no monitor / 1: preexisting monitor
 * @retval  -3 no mailbox (MonitorInfo: no fields set)
 * @retval  -2 type not set
 * @retval  -1 stat() failed (see errno; MonitorInfo fields: type, is_dir, path)
 *
 * If m is NULL, try to get the current mailbox from the Index.
 */
static enum ResolveResult monitor_resolve(struct MonitorInfo *info, struct Mailbox *m)
{
  char *fmt = NULL;
  struct stat st = { 0 };

  struct Mailbox *m_cur = get_current_mailbox();
  if (m)
  {
    info->type = m->type;
    info->path = m->realpath;
  }
  else if (m_cur)
  {
    info->type = m_cur->type;
    info->path = m_cur->realpath;
  }
  else
  {
    return RESOLVE_RES_FAIL_NOMAILBOX;
  }

  if (info->type == MUTT_UNKNOWN)
  {
    return RESOLVE_RES_FAIL_NOTYPE;
  }
  else if (info->type == MUTT_MAILDIR)
  {
    info->is_dir = true;
    fmt = "%s/new";
  }
  else
  {
    info->is_dir = false;
    if (info->type == MUTT_MH)
      fmt = "%s/.mh_sequences";
  }
  if (fmt)
  {
    buf_printf(&info->path_buf, fmt, info->path);
    info->path = buf_string(&info->path_buf);
  }
  if (stat(info->path, &st) != 0)
    return RESOLVE_RES_FAIL_STAT;

  struct Monitor *iter = Monitor;
  while (iter && ((iter->st_ino != st.st_ino) || (iter->st_dev != st.st_dev)))
    iter = iter->next;

  info->st_dev = st.st_dev;
  info->st_ino = st.st_ino;
  info->monitor = iter;

  return iter ? RESOLVE_RES_OK_EXISTING : RESOLVE_RES_OK_NOTEXISTING;
}

/**
 * mutt_monitor_poll - Check for filesystem changes
 * @retval -3 unknown/unexpected events: poll timeout / fds not handled by us
 * @retval -2 monitor detected changes, no STDIN input
 * @retval -1 error (see errno)
 * @retval  0 (1) input ready from STDIN, or (2) monitoring inactive -> no poll()
 *
 * Wait for I/O ready file descriptors or signals.
 *
 * MonitorFilesChanged also reflects changes to monitored files.
 *
 * Only STDIN and INotify file handles currently expected/supported.
 * More would ask for common infrastructure (sockets?).
 */
int mutt_monitor_poll(void)
{
  int rc = 0;
  char buf[EVENT_BUFLEN]
      __attribute__((aligned(__alignof__(struct inotify_event)))) = { 0 };

  MonitorFilesChanged = false;

  if (INotifyFd != -1)
  {
    int fds = poll(PollFds, PollFdsCount, 1000); // 1 Second

    if (fds == -1)
    {
      rc = -1;
      if (errno != EINTR)
      {
        mutt_debug(LL_DEBUG2, "poll() failed, errno=%d %s\n", errno, strerror(errno));
      }
    }
    else
    {
      bool input_ready = false;
      for (int i = 0; fds && (i < PollFdsCount); i++)
      {
        if (PollFds[i].revents)
        {
          fds--;
          if (PollFds[i].fd == 0)
          {
            input_ready = true;
          }
          else if (PollFds[i].fd == INotifyFd)
          {
            MonitorFilesChanged = true;
            mutt_debug(LL_DEBUG3, "file change(s) detected\n");
            char *ptr = buf;
            const struct inotify_event *event = NULL;

            while (true)
            {
              int len = read(INotifyFd, buf, sizeof(buf));
              if (len == -1)
              {
                if (errno != EAGAIN)
                {
                  mutt_debug(LL_DEBUG2, "read inotify events failed, errno=%d %s\n",
                             errno, strerror(errno));
                }
                break;
              }

              while (ptr < (buf + len))
              {
                event = (const struct inotify_event *) ptr;
                mutt_debug(LL_DEBUG3, "+ detail: descriptor=%d mask=0x%x\n",
                           event->wd, event->mask);
                if (event->mask & IN_IGNORED)
                  monitor_handle_ignore(event->wd);
                else if (event->wd == MonitorCurMboxDescriptor)
                  MonitorCurMboxChanged = true;
                ptr += sizeof(struct inotify_event) + event->len;
              }
            }
          }
        }
      }
      if (!input_ready)
        rc = MonitorFilesChanged ? -2 : -3;
    }
  }

  return rc;
}

/**
 * mutt_monitor_add - Add a watch for a mailbox
 * @param m Mailbox to watch
 * @retval  0 success: new or already existing monitor
 * @retval -1 failed:  no mailbox, inaccessible file, create monitor/watcher failed
 *
 * If m is NULL, try to get the current mailbox from the Index.
 */
int mutt_monitor_add(struct Mailbox *m)
{
  struct MonitorInfo info = { 0 };

  int rc = 0;
  enum ResolveResult desc = monitor_resolve(&info, m);
  if (desc != RESOLVE_RES_OK_NOTEXISTING)
  {
    if (!m && (desc == RESOLVE_RES_OK_EXISTING))
      MonitorCurMboxDescriptor = info.monitor->desc;
    rc = (desc == RESOLVE_RES_OK_EXISTING) ? 0 : -1;
    goto cleanup;
  }

  uint32_t mask = info.is_dir ? INOTIFY_MASK_DIR : INOTIFY_MASK_FILE;
  if (((INotifyFd == -1) && (monitor_init() == -1)) ||
      ((desc = inotify_add_watch(INotifyFd, info.path, mask)) == -1))
  {
    mutt_debug(LL_DEBUG2, "inotify_add_watch failed for '%s', errno=%d %s\n",
               info.path, errno, strerror(errno));
    rc = -1;
    goto cleanup;
  }

  mutt_debug(LL_DEBUG3, "inotify_add_watch descriptor=%d for '%s'\n", desc, info.path);
  if (!m)
    MonitorCurMboxDescriptor = desc;

  monitor_new(&info, desc);

cleanup:
  monitor_info_free(&info);
  return rc;
}

/**
 * mutt_monitor_remove - Remove a watch for a mailbox
 * @param m Mailbox
 * @retval 0 monitor removed (not shared)
 * @retval 1 monitor not removed (shared)
 * @retval 2 no monitor
 *
 * If m is NULL, try to get the current mailbox from the Index.
 */
int mutt_monitor_remove(struct Mailbox *m)
{
  struct MonitorInfo info = { 0 };
  struct MonitorInfo info2 = { 0 };
  int rc = 0;

  if (!m)
  {
    MonitorCurMboxDescriptor = -1;
    MonitorCurMboxChanged = false;
  }

  if (monitor_resolve(&info, m) != RESOLVE_RES_OK_EXISTING)
  {
    rc = 2;
    goto cleanup;
  }

  struct Mailbox *m_cur = get_current_mailbox();
  if (m_cur)
  {
    if (m)
    {
      if ((monitor_resolve(&info2, NULL) == RESOLVE_RES_OK_EXISTING) &&
          (info.st_ino == info2.st_ino) && (info.st_dev == info2.st_dev))
      {
        rc = 1;
        goto cleanup;
      }
    }
    else
    {
      if (mailbox_find(m_cur->realpath))
      {
        rc = 1;
        goto cleanup;
      }
    }
  }

  inotify_rm_watch(info.monitor->desc, INotifyFd);
  mutt_debug(LL_DEBUG3, "inotify_rm_watch for '%s' descriptor=%d\n", info.path,
             info.monitor->desc);

  monitor_delete(info.monitor);
  monitor_check_cleanup();

cleanup:
  monitor_info_free(&info);
  monitor_info_free(&info2);
  return rc;
}
