#include "plugin.h"

#include "cstring.h"
#include "list.h"
#include "log/log.h" // TODO: Use the error library instead. Move it to clib.

#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>

#include <errno.h>
#include <linux/limits.h>
#include <poll.h>
#include <sys/inotify.h>
#include <unistd.h>

// Watching for IN_CREATE leads the plugin engine to try to reload a plugin's
// shared library before the compiler has fully written to it.
static const int WATCH_MASK = IN_CLOSE_WRITE;

/// Plugin state.
///
/// Each Plugin object points to a shared library and holds a state object.
///
/// The same shared library can be loaded multiple times, resulting in multiple
/// Plugin instances, each with its own internal state.
typedef struct Plugin {
  void* handle;      // First member so that Plugin can be cast to handle.
  void* state;       // Plugin's internal state.
  bool  reloaded;    // Whether the plugin has been reloaded, state needs to be
                     // re-created.
  PluginEngine* eng; // So that the public API can do stuff with just a Plugin*.
  mstring       filename;
} Plugin;

DEF_LIST(Plugin, Plugin);

/// Plugin engine.
typedef struct PluginEngine {
  int         inotify_instance;
  int         dir_watch; // inotify watch on the plugins directory.
  Plugin_list plugins;
  mstring     plugins_dir;
} PluginEngine;

// -----------------------------------------------------------------------------
// Plugin.
// -----------------------------------------------------------------------------

static mstring plugin_lib_name(const Plugin* plugin) {
  return mstring_concat(
      mstring_make("lib"), mstring_concat_cstr(plugin->filename, ".so"));
}

static mstring plugin_lib_path(const Plugin* plugin) {
  return mstring_concat(plugin->eng->plugins_dir, plugin_lib_name(plugin));
}

static bool load_library(Plugin* plugin) {
  assert(plugin);
  assert(plugin->eng);

  // Handle reloading a previously-loaded library.
  if (plugin->handle) {
    dlclose(plugin->handle);
    plugin->handle = 0;
  }

  const mstring lib = plugin_lib_path(plugin);

  // If the plugin fails to load, make sure to keep the plugin's old handle to
  // handle the error gracefully. This handles reload failures, specifically.
  void* handle = 0;
  if ((handle = dlopen(mstring_cstr(&lib), RTLD_NOW))) {
    LOGD("Plugin [%s] loaded successfully", mstring_cstr(&plugin->filename));
    plugin->handle = handle;
    return true;
  } else {
    LOGE("dlopen() failed: %s", dlerror());
  }

  return false;
}

static void delete_plugin_state(Plugin* plugin) {
  if (plugin->state) {
    free(plugin->state);
    plugin->state = 0;
  }
}

void set_plugin_state(Plugin* plugin, void* state) {
  assert(plugin);
  delete_plugin_state(plugin);
  plugin->state = state;
}

void* get_plugin_state(Plugin* plugin) {
  assert(plugin);
  return plugin->state;
}

static void destroy_plugin(Plugin* plugin) {
  if (plugin) {
    if (plugin->handle) {
      dlclose(plugin->handle);
      plugin->handle = 0;
    }
    delete_plugin_state(plugin);
  }
}

Plugin* load_plugin(PluginEngine* eng, const char* filename) {
  assert(eng);
  assert(filename);

  Plugin plugin = (Plugin){.eng = eng, .filename = mstring_make(filename)};

  if (!load_library(&plugin)) {
    return 0;
  }

  list_add(eng->plugins, plugin);
  return &eng->plugins.head->val;
}

void delete_plugin(Plugin** pPlugin) {
  assert(pPlugin);
  Plugin* plugin = *pPlugin;
  if (plugin) {
    assert(plugin->eng);
    destroy_plugin(plugin);
    list_remove_ptr(plugin->eng->plugins, plugin);
    *pPlugin = 0;
  }
}

bool plugin_reloaded(Plugin* plugin) {
  assert(plugin);
  const bool reloaded = plugin->reloaded;
  plugin->reloaded    = false;
  return reloaded;
}

// -----------------------------------------------------------------------------
// Plugin Engine.
// -----------------------------------------------------------------------------

PluginEngine* new_plugin_engine(const PluginEngineDesc* desc) {
  PluginEngine* eng = 0;

  if (!(eng = calloc(1, sizeof(PluginEngine)))) {
    goto cleanup;
  }
  eng->plugins     = make_list(Plugin);
  eng->plugins_dir = mstring_concat_cstr(mstring_make(desc->plugins_dir), "/");

  LOGD("Watch plugins directory: %s", mstring_cstr(&eng->plugins_dir));

  if ((eng->inotify_instance = inotify_init()) == -1) {
    LOGE("Failed to create inotify instance");
    goto cleanup;
  }
  if ((eng->dir_watch = inotify_add_watch(
           eng->inotify_instance, mstring_cstr(&eng->plugins_dir),
           WATCH_MASK)) == -1) {
    LOGE("Failed to watch directory: %s", mstring_cstr(&eng->plugins_dir));
    goto cleanup;
  }

  return eng;

cleanup:
  delete_plugin_engine(&eng);
  return 0;
}

void delete_plugin_engine(PluginEngine** pEng) {
  assert(pEng);
  PluginEngine* eng = *pEng;
  if (eng) {
    list_foreach_mut(eng->plugins, plugin, { destroy_plugin(&plugin); });
    del_list(eng->plugins);
    if (eng->dir_watch != -1) {
      inotify_rm_watch(eng->dir_watch, eng->inotify_instance);
      close(eng->dir_watch);
      eng->dir_watch = 0;
    }
    if (eng->inotify_instance != -1) {
      close(eng->inotify_instance);
    }
    free(eng);
    *pEng = 0;
  }
}

void plugin_engine_update(PluginEngine* eng) {
  assert(eng);

  struct pollfd pollfds[1] = {
      {eng->inotify_instance, POLLIN, 0}
  };

  int ret = 0;
  while ((ret = poll(pollfds, 1, 0)) != 0) {
    if (ret > 0) {
      const struct pollfd* pfd = &pollfds[0];
      if (pfd->revents & POLLIN) {
        // inotify instances don't like to be partially read, and the events,
        // when watching a directory, have a variable-length file name.
        uint8_t buf[sizeof(struct inotify_event) + NAME_MAX + 1] = {0};
        ssize_t length = read(eng->inotify_instance, &buf, sizeof(buf));
        if (length == -1) {
          LOGE(
              "read() on inotify instance failed with error [%d]: %s", errno,
              strerror(errno));
          break;
        }
        const uint8_t* next = buf;
        const uint8_t* end  = buf + sizeof(buf);
        while (next < end) {
          const struct inotify_event* event = (const struct inotify_event*)next;
          if (event->mask & WATCH_MASK) {
            if (event->wd == eng->dir_watch) {
              if (event->len > 0) {
                // Name does not include directory, e.g., libfoo.so
                const mstring file = mstring_make(event->name);
                list_foreach_mut(eng->plugins, plugin, {
                  if (mstring_eq(file, plugin_lib_name(&plugin))) {
                    if (load_library(&plugin)) {
                      plugin.reloaded = true;
                    }
                    break;
                  }
                });
              }
            }
          }
          next += sizeof(struct inotify_event) + event->len;
        }
      }
      if ((pfd->revents & POLLERR) || (pfd->revents & POLLHUP) ||
          (pfd->revents & POLLNVAL)) {
        LOGE("inotify instance is in a bad state");
        break;
      }
    } else if (ret == -1) {
      LOGE("poll() failed with error [%d]: %s", errno, strerror(errno));
      break;
    }
  }
}