/*
 *  Copyright 1994-2022 Olivier Girondel
 *
 *  This file is part of lebiniou.
 *
 *  lebiniou 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.
 *
 *  lebiniou 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 lebiniou. If not, see <http://www.gnu.org/licenses/>.
 */

#include "biniou.h"
#include "plugin.h"
#include "pthread_utils.h"

/* Note: dlsym prevents us from fully compiling in -pedantic..
 * see http://www.trilithium.com/johan/2004/12/problem-with-dlsym/
 */

#ifdef DEBUG
#define B_DLSYM(VAR, SYM)                                         \
  (VAR) = (void (*)(struct Context_s *)) dlsym(p->handle, (SYM)); \
  if (libbiniou_verbose && (NULL != (VAR)))                       \
    printf("[p] >> %s has '%s'\n", p->name, (SYM))
#define B_GOTSYM(VAR, SYM) if (NULL != (VAR))                     \
    printf("[p] >> %s has '%s'\n", p->name, (SYM));
#else
#define B_DLSYM(VAR, SYM)                                         \
  (VAR) = (void (*)(struct Context_s *)) dlsym(p->handle, (SYM))
#define B_GOTSYM(VAR, SYM) { }
#endif


static Plugin_t *
Plugin_load(Plugin_t *p)
{
  const char *error = NULL;
  uint32_t *_tmp;
  enum LayerMode *_mode;

  assert(NULL != p);

  p->handle = dlopen(p->file, RTLD_NOW);

  if (NULL == p->handle) {
    error = dlerror();
    VERBOSE(fprintf(stderr, "[!] Failed to load plugin '%s': %s\n", p->name, error));
    xfree(p->name);
    xfree(p->file);
    xfree(p);

    return NULL;
  } else {
    VERBOSE(printf("[p] Loading plugin '%s'\n", p->name));
  }

  uint32_t *version_ptr = (uint32_t *) dlsym(p->handle, "version");
  if (NULL != version_ptr) {
    p->version = *version_ptr;
#ifdef DEBUG
    VERBOSE(printf("[i] Plugin version: %d\n", p->version));
#endif
  } else {
    xerror("Plugin '%s' MUST define a version\n", p->name);
  }

  _tmp = (uint32_t *) dlsym(p->handle, "options");
  if (NULL == _tmp) {
    error = dlerror();
    xerror("Plugin MUST define options (%s)\n", error);
  } else {
    p->options = _tmp;
  }

  _mode = (enum LayerMode *) dlsym(p->handle, "mode");
  p->mode = _mode;

  /* get display name */
  p->dname = (char *) dlsym(p->handle, "dname");
  B_GOTSYM(p->dname, "dname");
  if (NULL == p->dname) {
    p->dname = p->name;
  }

  /* get description */
  p->desc = (char *) dlsym(p->handle, "desc");
  B_GOTSYM(p->desc, "desc");

  /* get HTML description */
  p->html_description = (char *) dlsym(p->handle, "html_description");
  B_GOTSYM(p->html_description, "html_description");

  p->create = (int8_t (*)(struct Context_s *)) dlsym(p->handle, "create");
  B_DLSYM(p->destroy, "destroy");
  p->check_version = (int8_t (*)(uint32_t)) dlsym(p->handle, "check_version");
  B_DLSYM(p->run, "run");
  B_DLSYM(p->on_switch_on, "on_switch_on");
  B_DLSYM(p->on_switch_off, "on_switch_off");

  /* Output plugin stuff */
  p->fullscreen = (void (*)(int)) dlsym(p->handle, "fullscreen");
  B_GOTSYM(p->fullscreen, "fullscreen");
  p->switch_cursor = (void (*)(void)) dlsym(p->handle, "switch_cursor");
  B_GOTSYM(p->switch_cursor, "switch_cursor");

  /* Input plugin stuff (? mainly -to check --oliv3) */
  p->jthread = (void *(*)(void *)) dlsym(p->handle, "jthread");
  B_GOTSYM(p->jthread, "jthread");

  p->parameters = (json_t *(*)(struct Context_s *, const json_t *, const uint8_t)) dlsym(p->handle, "parameters");
  B_GOTSYM(p->parameters, "parameters");
  p->command = (json_t *(*)(struct Context_s *, const json_t *)) dlsym(p->handle, "command");
  B_GOTSYM(p->command, "command");

  return p;
}


static uint32_t
Plugin_unload(Plugin_t *p)
{
  assert (NULL != p);
  uint32_t calls = 0;

  /* FIXME error checking there, ie if plugin fails to destroy */
  if (NULL != p->jthread) {
    VERBOSE(printf("[p] Joining thread from plugin '%s'... ", p->name));
    xpthread_join(p->thread, NULL);
  } else {
    calls = p->calls;
    if (p->calls) {
      VERBOSE(printf("[p] Unloading plugin '%s' (%d call%s)... ", p->name, p->calls,
                     ((p->calls == 1) ? "" : "s")));
    } else {
      VERBOSE(printf("[p] Unloading plugin '%s'... ", p->name));
    }
  }

  if (NULL != p->destroy) {
    p->destroy(context);
  }

#ifndef DISABLE_DLCLOSE
  VERBOSE(printf("dlclose... "));
  dlclose(p->handle);
#endif
  VERBOSE(printf("done.\n"));

  return calls;
}


Plugin_t *
Plugin_new(const char *directory, const char *name, const enum PluginType type)
{
  Plugin_t *p = xcalloc(1, sizeof(Plugin_t));

  assert(NULL != name);
  assert(NULL != directory);

  p->name = strdup(name);
  p->calls = 0;

  if (type == PL_INPUT) {
    p->file = g_strdup_printf("%s/input/%s/%s.so", directory, name, name);
  } else if (type == PL_MAIN) {
    p->file = g_strdup_printf("%s/main/%s/%s.so", directory, name, name);
  } else if (type == PL_OUTPUT) {
    p->file = g_strdup_printf("%s/output/%s/%s.so", directory, name, name);
  }

  return Plugin_load(p);
}


uint32_t
Plugin_delete(Plugin_t *p)
{
  assert(NULL != p);
  uint32_t calls = Plugin_unload(p);
  xfree(p->name);
  g_free(p->file);
  xfree(p);

  return calls;
}


void
Plugin_reload(Plugin_t *p)
{
  assert(NULL != p);
  Plugin_unload(p);
  Plugin_load(p);
  VERBOSE(printf("[p] Reloaded plugin '%s'\n", p->name));
}


int8_t
Plugin_init(Plugin_t *p)
{
  int8_t res = 1;
  assert(NULL != p);

  if (NULL != p->create) {
    VERBOSE(printf("[+] Initializing plugin %s\n", p->name));
    res = p->create(context);
  }

  if ((NULL != p->jthread) && res) {
    xpthread_create(&p->thread, NULL, p->jthread, (void *)context);
    VERBOSE(printf("[p] Launched thread %s\n", p->name));
  }

  return res;
}
