/*
 *  wmfs2 by Martin Duquesnoy <xorg62@gmail.com> { for(i = 2011; i < 2111; ++i) ©(i); }
 *  For license, see COPYING.
 */

#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <X11/Xutil.h>

#include "wmfs.h"
#include "event.h"
#include "util.h"
#include "infobar.h"
#include "config.h"

static int
qsort_string_compare(const void * a, const void * b)
{
     return (strcmp(*(char **)a, *(char **)b));
}

static char **
complete_on_command(char *start)
{
     struct dirent *content;
     DIR *dir;
     char **paths, *path, *p, **namelist = NULL;
     int i, count;

     if(!(path = getenv("PATH")) || !start)
          return NULL;

     /* split PATH into paths */
     path = p = xstrdup(path);

     for(count = 1, p = path; strchr(p, ':'); ++p, ++count);

     paths = xcalloc(count, sizeof(*paths));

     for(paths[0] = p = path, count = 1; (p = strchr(p, ':')); ++p, ++count)
     {
          paths[count] = p + 1;
          *p = '\0';
     }

     paths[count] = NULL;

     /* recursively open PATH */
     for(i = count = 0; paths[i]; ++i)
     {
          if(!(dir = opendir(paths[i])))
               continue;

          while((content = readdir(dir)))
          {
               if(strncmp(content->d_name, ".", 1)
                  && !strncmp(content->d_name, start, strlen(start)))
               {
                    namelist = xrealloc(namelist, ++count, sizeof(*namelist));
                    namelist[count - 1] = xstrdup(content->d_name + strlen(start));
               }
          }
          closedir(dir);
     }

     if(count)
     {
          qsort(namelist, count, sizeof(char *), qsort_string_compare);
          namelist = xrealloc(namelist, ++count, sizeof(*namelist));
          namelist[count - 1] = NULL;
     }

     free(paths);
     free(path);

     return namelist;
}

/*
 * Complete a filename or directory name.
 * works like complete_on_command.
 */
static char **
complete_on_files(char *start)
{
     struct dirent *content = NULL;
     struct stat st;
     DIR *dir;
     char *home, *path, *dirname = NULL;
     char **namelist = NULL, *filepath, *p = start;
     int count = 0;

     /*
      * Search the directory to open and set
      * the beginning of file to complete on pointer 'p'.
      */
     if(*p == '\0' || !strrchr(p, '/'))
          path = xstrdup(".");
     else
     {
          if(!(home = getenv("HOME")))
               return NULL;

          /* remplace ~ by $HOME in dirname */
          if(!strncmp(p, "~/", 2) && home)
               xasprintf(&dirname, "%s%s", home, p+1);
          else
               dirname = xstrdup(p);

          /* Set p to filename to be complete
           * and path the directory containing the file
           * /foooooo/baaaaaar/somethinglikethis<tab>
           * <---- path - ---><------- p ------>
           */
          p = strrchr(dirname, '/');
          if(p != dirname)
          {
               *(p++) = '\0';
               path = xstrdup(dirname);
          }
          else
          {
               path = xstrdup("/");
               p++;
          }
     }

     if((dir = opendir(path)))
     {
          while((content = readdir(dir)))
          {
               if(!strcmp(content->d_name, ".")
                  || !strcmp(content->d_name, "..")
                  || strncmp(content->d_name, p, strlen(p)))
                    continue;

               /* If it's a directory append '/' to the completion */
               xasprintf(&filepath, "%s/%s", path, content->d_name);

               if(filepath && stat(filepath, &st) != -1)
               {
                    namelist = xrealloc(namelist, ++count, sizeof(*namelist));

                    if(S_ISDIR(st.st_mode))
                         xasprintf(&namelist[count - 1], "%s/", content->d_name + strlen(p));
                    else
                         namelist[count - 1] = xstrdup(content->d_name + strlen(p));
               }
               else
                    warnl("%s", filepath);

               free(filepath);
          }
          closedir(dir);
     }

     if(count)
     {
          namelist = xrealloc(namelist, ++count, sizeof(*namelist));
          namelist[count - 1] = NULL;
     }

     free(dirname);
     free(path);

     return namelist;
}

static void
complete_cache_free(struct launcher_ccache *cache)
{
     int i;

     /* release memory */
     free(cache->start);

     if(cache->namelist)
     {
          for(i = 0; cache->namelist[i]; i++)
               free(cache->namelist[i]);
          free(cache->namelist);
     }

     /* init */
     cache->hits = 0;
     cache->start = NULL;
     cache->namelist = NULL;
}

static char *
complete(struct launcher_ccache *cache, char *start)
{
     char *p = NULL, *comp = NULL;

     if(!start || !cache)
          return NULL;

     if((p = strrchr(start, ' ')))
          p++;
     else
          p = start;

     if(cache->start && !strcmp(cache->start, start))
     {
          if(cache->namelist && !cache->namelist[cache->hits])
               cache->hits = 0;
     }
     else
     {
          complete_cache_free(cache);
          cache->start = xstrdup(start);

          if(p == start)
               cache->namelist = complete_on_command(p);
          else
               cache->namelist = complete_on_files(p);
     }

     if(cache->namelist && cache->namelist[cache->hits])
          comp = cache->namelist[cache->hits];

     return comp;
}

#define LAUNCHER_INIT_ELEM(width)                       \
     SLIST_FOREACH(ib, &W->screen->infobars, next)      \
     {                                                  \
          TAILQ_FOREACH(e, &ib->elements, next)         \
               if(e->type == ElemLauncher)              \
               {                                        \
                    e->geo.w = width;                   \
                    e->data = data;                     \
               }                                        \
          infobar_elem_reinit(ib);                      \
     }

static void
launcher_process(struct launcher *l)
{
     struct infobar *ib;
     struct element *e;
     struct launcher_ccache cache = {NULL, NULL, 0};
     bool loop = true, found = false, lastwastab = false;
     char tmpbuf[512] = { 0 }, buf[512] = { 0 };
     char tmp[32] = { 0 };
     char *p, *data, *arg, *end, *cmd = xstrdup(l->command);
     int i, pos = 0, histpos = 0;
     void (*func)(Uicb);
     XEvent ev;
     KeySym ks;

     W->flags |= WMFS_LAUNCHER;

     /* Prepare elements */
     xasprintf(&data, "%s ", l->prompt);
     LAUNCHER_INIT_ELEM(l->width);

     XGrabKeyboard(W->dpy, W->root, true, GrabModeAsync, GrabModeAsync, CurrentTime);

     while(loop)
     {
          XNextEvent(W->dpy, &ev);

          if(ev.type != KeyPress)
          {
               EVENT_HANDLE(&ev);
               continue;
          }

          /* Get pressed key */
          XLookupString(&ev.xkey, tmp, sizeof(tmp), &ks, 0);

          /* Check Ctrl-c / Ctrl-d */
          if(ev.xkey.state & ControlMask)
          {
               switch(ks)
               {
               case XK_c:
               case XK_d:
                    ks = XK_Escape;
                    break;
               case XK_p:
                    ks = XK_Up;
                    break;
               case XK_n:
                    ks = XK_Down;
                    break;
               }
          }

          /* Check if there is a keypad */
          if(IsKeypadKey(ks) && ks == XK_KP_Enter)
               ks = XK_Return;

          /* Manage pressed keys */
          switch(ks)
          {
          case XK_Up:
               if(l->nhisto)
               {
                    if(histpos >= (int)l->nhisto)
                         histpos = 0;
                    strncpy(buf, l->histo[l->nhisto - ++histpos], sizeof(buf));
                    pos = strlen(buf);
               }
               break;

          case XK_Down:
               if(l->nhisto && histpos > 0 && histpos < (int)l->nhisto)
               {
                    strncpy(buf, l->histo[l->nhisto - --histpos], sizeof(buf));
                    pos = strlen(buf);
               }
               break;

          case XK_Return:
               /* Get function name only, if cmds are added in command */
               arg = NULL;
               if((p = strchr(cmd, ' ')))
               {
                    *p = '\0';
                    xasprintf(&arg, "%s %s", p + 1, buf);
               }

               if((func = uicb_name_func(cmd)))
               {
                    if(arg)
                    {
                         func(arg);
                         free(arg);
                    }
                    else
                         func(buf);
               }

               /* Histo */
               if(l->nhisto + 1 > HISTOLEN)
               {
                    for(i = l->nhisto - 1; i > 1; --i)
                         strncpy(l->histo[i], l->histo[i - 1], sizeof(l->histo[i]));

                    l->nhisto = 0;
               }
               /* Store in histo array */
               strncpy(l->histo[l->nhisto++], buf, sizeof(buf));

               loop = false;
               break;

          case XK_Escape:
               loop = false;
               break;

          /* Completion */
          case XK_Tab:
               buf[pos] = '\0';
               if(lastwastab)
                    ++cache.hits;
               else
               {
                    cache.hits = 0;
                    strncpy(tmpbuf, buf, sizeof(tmpbuf));
               }

               if(pos && (end = complete(&cache, tmpbuf)))
               {
                    strncpy(buf, tmpbuf, sizeof(buf));
                    strncat(buf, end, sizeof(buf));
                    found = true;
               }

               lastwastab = true;

               /* start a new round of tabbing */
               if(!found)
                    cache.hits = 0;

               pos = strlen(buf);
               break;

          case XK_BackSpace:
               lastwastab = false;
               if(pos)
                    buf[--pos] = '\0';
               break;

          default:
               lastwastab = false;
               strncat(buf, tmp, sizeof(tmp));
               ++pos;
               break;
          }

          free(data);
          xasprintf(&data, "%s %s", l->prompt, buf);

          /* Update EVERY launcher element of the screen */
          SLIST_FOREACH(ib, &W->screen->infobars, next)
          {
               TAILQ_FOREACH(e, &ib->elements, next)
               {
                    if(e->type != ElemLauncher)
                         continue;

                    e->data = data;
                    e->func_update(e);
               }
          }
     }

     XUngrabKeyboard(W->dpy, CurrentTime);

     complete_cache_free(&cache);
     free(cmd);
     free(data);

     /* 'Close' launcher elements */
     W->flags ^= WMFS_LAUNCHER;
     data = NULL;
     LAUNCHER_INIT_ELEM(1);
}

void
uicb_launcher(Uicb cmd)
{
     struct launcher *l;

     if(!cmd)
          return;

     SLIST_FOREACH(l, &W->h.launcher, next)
          if(!strcmp(l->name, cmd))
          {
               launcher_process(l);
               break;
          }
}
