/*
 *  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 "plugins.h"
#include "brandom.h"
#include "constants.h"
#include "biniou.h"
#include "settings.h"


static PluginType_t pTypes[MAX_OPTIONS];
static json_t *plugins_j = NULL;


Plugins_t *
Plugins_new(const char *path)
{
  Plugins_t *p = NULL;

  if (NULL == path) {
    path = ".";
  }

  p = xcalloc(1, sizeof(Plugins_t));

  p->path = strdup(path);
  p->plugins = NULL;
  p->size = 0;
  p->selected_idx = 0;
  p->selected = NULL;

  plugins_j = json_array();
  
  return p;
}


json_t *
Plugins_delete(Plugins_t *p)
{
  if (NULL == p) {
    return NULL;
  } else {
    json_t *stats = json_object();

    for (uint16_t i = 0; i < p->size; i++) {
      Plugin_t *_p = p->plugins[i];
      char *name = strdup(_p->name);
      uint32_t calls = Plugin_delete(_p);

      if (calls) {
        json_object_set_new(stats, name, json_integer(calls));
      }
      xfree(name);
    }
    xfree(p->plugins);
    xfree(p->path);
    xfree(p);
    json_decref(plugins_j);

    return stats;
  }
}


void
Plugins_init(Plugins_t *ps)
{
  uint16_t i;
  uint16_t old_size = ps->size;

  VERBOSE(printf("[+] Initializing %d plugin%c\n", ps->size, (ps->size == 1) ? ' ' : 's'));
  for (i = 0; i < old_size; i++) {
    Plugin_t *p = ps->plugins[i];

    if (!Plugin_init(p)) {
      VERBOSE(printf("[!] Failed to initialize %s\n", p->name));

      /* plugin types stuff */
      for (int t = 0; t < MAX_OPTIONS; t++)
        if (*p->options & (1 << t)) {
#ifdef DEBUG
          // printf("[T] Has type %s\n", pTypes[t].name);
#endif
          pTypes[t].count--;
        }

      if (plugins->selected == p) {
        plugins->selected = NULL;
        plugins->selected_idx = 0;
      }
      Plugin_delete(p);
      ps->plugins[i] = NULL;
      ps->size--;
    }
  }

  // clean up disabled plugins
  if (ps->size != old_size) {
    Plugin_t **res = xcalloc(ps->size, sizeof(Plugin_t *));
    uint16_t idx = 0;

    for (i = 0; i < old_size; i++) {
      if (NULL != ps->plugins[i]) {
        res[idx++] = ps->plugins[i];
      }
    }

    xfree(ps->plugins);
    ps->plugins = res;
  }
}


/*
 * Check if a directory is a plugin directory.
 * Convention is that you MUST name your .so and directory
 * with your plugin name. eg: you have a plugin called Foo.
 * It MUST be in /path/to/plugins/Foo/
 * This directory MUST contain /path/to/plugins/Foo/Foo.so
 */
static int
isPluginDir(const char *dir, const char *name)
{
  struct stat istics;
  int rc = -1;
  gchar *soname = NULL;

  if (name[0] == '.') {
    return 0;
  }

  /* XXX hardcoded main */
  soname = g_strdup_printf("%s/main/%s/%s.so", dir, name, name);

  rc = stat(soname, &istics);
  g_free(soname);
  if (rc == -1) {
    return 0;
  }

  return ((rc == 0) && S_ISREG(istics.st_mode));
}


void
Plugins_select(Plugins_t *ps, const Plugin_t *p)
{
  for (uint16_t i = 0; i < ps->size; i++)
    if (ps->plugins[i] == p) {
      ps->selected_idx = i;
      ps->selected = (Plugin_t *)p;

      return;
    }

  xerror("Plugins_select failed");
}


static int
Plugins_compare(const void *_p0, const void *_p1)
{
  const Plugin_t **p0, **p1;

  p0 = (const Plugin_t **)(_p0);
  p1 = (const Plugin_t **)(_p1);

  return strcasecmp((*p0)->dname, (*p1)->dname);
}


void
Plugins_load(Plugins_t *ps, const Context_t *ctx)
{
  DIR *da_dir;
  struct dirent *da_ent;
  uint16_t i, old_size;
  gchar *blah;
  json_t *plugins_json = json_array();

  blah = g_strdup_printf("%s/main", ps->path);
  VERBOSE(printf("[i] Loading plugins from %s\n", blah));

  if (NULL == (da_dir = opendir(blah))) {
    xperror("opendir");
  }

  g_free(blah);
  while (NULL != (da_ent = readdir(da_dir)))
    if (isPluginDir(ps->path, da_ent->d_name)) {
      ps->size++;
    }

  if (!ps->size) {
    xerror("Won't do anything without a plugin, stupid\n");
  }
  ps->plugins = xcalloc(ps->size, sizeof(Plugin_t *));
  old_size = ps->size;

  rewinddir(da_dir);
  i = 0;

  while (NULL != (da_ent = readdir(da_dir))) {
    Plugin_t *p;

    const char *name = da_ent->d_name;

    if (!isPluginDir(ps->path, name)) {
      continue;
    }

    /* load the plugin */
    p = ps->plugins[i] = Plugin_new(ps->path, name, PL_MAIN);

    if (NULL != p) {
      json_t *settings_plugins = Settings_get_plugins();
      uint8_t black = 0; // all plugins enabled by default

      if (NULL != settings_plugins) {
        // enabled plugins are in the JSON config
        size_t index;
        json_t *value;

        json_array_foreach(settings_plugins, index, value) {
          if (is_equal(json_string_value(json_object_get(value, "name")), name)) {
            black = !json_boolean_value(json_object_get(value, "enabled"));
            break;
          }
        }
      }
      if (NULL == settings_plugins) {
        json_t *plugin_j = json_pack("{sssssbsb}",
                                     "name", name,
                                     "displayName", p->dname,
                                     "enabled", !black,
                                     "favorite", 0);

        json_array_append_new(plugins_json, plugin_j);
      } else {
        json_decref(settings_plugins);
      }
      json_array_append_new(plugins_j, json_pack("{sssssi}",
                                                 "name", name,
                                                 "displayName", p->dname,
                                                 "options", *p->options));

      if (black) {
#ifdef DEBUG
        printf("[-] Skipping blacklisted plugin: %s\n", name);
#endif
        Plugin_delete(p);
        ps->size--;
        continue;
      }

      if (
        // check if plugin needs input
        ((NULL != ctx->input) ||
         ((NULL == ctx->input) && (!(*p->options & BO_SFX))))
        &&
        // check if plugin needs images
        ((NULL != ctx->imgf) ||
         ((NULL == ctx->imgf) && (!(*p->options & BO_IMAGE))))
        &&
        // check if plugin needs webcam
        ((ctx->webcams > 0) ||
         (!ctx->webcams && (!(*p->options & BO_WEBCAM))))
      ) {
        if (libbiniou_verbose) {
          if (NULL != p->desc) {
            printf("        %s\n", p->desc);
          } else {
            fprintf(stderr, "[!] FIXME: %s has no description\n", name);
          }
        }

        // plugin types stuff
        for (uint8_t t = 0; t < MAX_OPTIONS; t++)
          if (*p->options & (1 << t)) {
#ifdef DEBUG
            // printf("[T] Has type %s\n", pTypes[t].name);
#endif
            pTypes[t].count++;
          }
#ifdef DEBUG
        printf("\n");
#endif
        i++;
      } else {
        VERBOSE(printf("\n"));
        ps->plugins[i] = NULL;
        Plugin_delete(p);
        ps->size--;
      }
    } else {
      ps->size--;
    }
  }
  closedir(da_dir);

  if (!ps->size) {
    fprintf(stderr, "[!] Fatal error: no plugins loaded. Please check your configuration\n");
    fprintf(stderr, "    or delete your (probably corrupted) '%s'\n", Settings_get_configuration_file());
    exit(1);
  }

  Plugin_t **res = xcalloc(ps->size, sizeof(Plugin_t *));
  int idx = 0;

  for (i = 0; i < old_size; i++) {
    if (NULL != ps->plugins[i]) {
      res[idx++] = ps->plugins[i];
    }
  }
  xfree(ps->plugins);
  ps->plugins = res;

  /* sort the plugins array */
  qsort((void *)ps->plugins, (size_t)ps->size, (size_t)sizeof(Plugin_t *), &Plugins_compare);

  if (libbiniou_verbose) {
    printf("[+] Loaded %d plugin%c\n", ps->size, (ps->size == 1 ? ' ' : 's'));
  }

  Plugins_select(ps, ps->plugins[0]);
  if (json_array_size(plugins_json)) {
#ifdef DEBUG
    printf("[i] Migrating settings to JSON\n");
#endif
    Settings_set_plugins(plugins_json);
  } else {
#ifdef DEBUG
    printf("[i] Plugins are in the JSON configuration file\n");
#endif
  }
  json_decref(plugins_json);
}


Plugin_t *
Plugins_find(const char *name)
{
  for (uint16_t i = 0; i < plugins->size; i++)
    if (is_equal(plugins->plugins[i]->name, name)) {
      return plugins->plugins[i];
    }

  return NULL;
}


Plugin_t *
Plugins_get_random(const enum PluginOptions feat, const Plugin_t *locked)
{
  uint8_t i;
  uint16_t rand = 0;
  Plugin_t *chosen = NULL;

  /* check if we can select at random or not */
  for (i = 0; i < MAX_OPTIONS; i++)
    if (pTypes[i].count && ((1 << i) & feat)) {
      break;
    }

  if (i == MAX_OPTIONS) {
    /* TODO option_to_string */
    /* printf("[!] Failed to find plugin with feature %d\n", feat); */
    return NULL;
  }
  // else printf("pTypes[%d].count= %d\n", i, pTypes[i].count);

  uint8_t ignore_beq_norandom = (NULL != getenv("LEBINIOU_IGNORE_BO_NORANDOM"));
  int retry;

  do {
    retry = 0;
    rand = b_rand_uint32_range(0, plugins->size);
    chosen = plugins->plugins[rand];
    if (!(*chosen->options & feat)) {
      retry = 1;
    } else {
      if (!ignore_beq_norandom && (*chosen->options & BO_NORANDOM) && (chosen != locked)) {
        retry = 1;
      }
    }
  } while (retry);

  return chosen;
}


void
Plugins_next(void)
{
  if (++plugins->selected_idx == plugins->size) {
    plugins->selected_idx = 0;
  }
  plugins->selected = plugins->plugins[plugins->selected_idx];
  VERBOSE(printf("[i] Selected plugin: '%s' (%s)\n", plugins->selected->name, plugins->selected->dname));
}


void
Plugins_prev(void)
{
  if (!plugins->selected_idx) {
    plugins->selected_idx = plugins->size - 1;
  } else {
    plugins->selected_idx--;
  }
  plugins->selected = plugins->plugins[plugins->selected_idx];
  VERBOSE(printf("[i] Selected plugin: '%s' (%s)\n", plugins->selected->name, plugins->selected->dname));
}


void
Plugins_next_n(const uint16_t n)
{
  for (uint16_t i = 0; i < n; i++) {
    Plugins_next();
  }
}


void
Plugins_prev_n(const uint16_t n)
{
  for (uint16_t i = 0; i < n; i++) {
    Plugins_prev();
  }
}


void
Plugins_reload_selected(Plugins_t *plugins)
{
  Plugin_reload(plugins->selected);
}


json_t *
Plugins_get(void)
{
  return plugins_j;
}
