/*
 * Copyright (C) 1996-8 Michael R. Elkins <me@cs.hmc.edu>
 * 
 *     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, write to the Free Software
 *     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */ 

#include "mutt.h"
#include "mutt_curses.h"
#include "mutt_menu.h"
#include "buffy.h"
#include "sort.h"
#include "mailbox.h"

#include <stdlib.h>
#include <dirent.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <pwd.h>
#include <grp.h>

/* HP-UX and ConvexOS don't have this macro */
#ifndef S_ISLNK
#define S_ISLNK(x) (((x) & S_IFMT) == S_IFLNK ? 1 : 0)
#endif

struct folder_file
{
  mode_t mode;
  time_t mtime;
  off_t size;
  char *name;
  char *desc;
};

struct browser_state
{
  struct folder_file *entry;
  short entrylen; /* number of real entries */
  short entrymax;  /* max entry */
};

static struct mapping_t FolderHelp[] = {
  { "Exit",  OP_EXIT },
  { "Chdir", OP_CHANGE_DIRECTORY },
  { "Mask",  OP_ENTER_MASK },
  { "Help",  OP_HELP },
  { NULL }
};

typedef struct folder_t
{
  const char *name;
  const struct stat *f;
  int new;
} FOLDER;

static char LastDir[_POSIX_PATH_MAX] = "";

/* Frees up the memory allocated for the local-global variables.  */
static void destroy_state (struct browser_state *state)
{
  int c;

  for (c = 0; c < state->entrylen; c++)
  {
    safe_free ((void **) &((state->entry)[c].name));
    safe_free ((void **) &((state->entry)[c].desc));
  }
  safe_free ((void **) &state->entry);
}

static int browser_compare_subject (const void *a, const void *b)
{
  struct folder_file *pa = (struct folder_file *) a;
  struct folder_file *pb = (struct folder_file *) b;

  int r = strcmp (pa->name, pb->name);

  return ((BrowserSort & SORT_REVERSE) ? -r : r);
}

static int browser_compare_date (const void *a, const void *b)
{
  struct folder_file *pa = (struct folder_file *) a;
  struct folder_file *pb = (struct folder_file *) b;

  int r = pa->mtime - pb->mtime;

  return ((BrowserSort & SORT_REVERSE) ? -r : r);
}

static int browser_compare_size (const void *a, const void *b)
{
  struct folder_file *pa = (struct folder_file *) a;
  struct folder_file *pb = (struct folder_file *) b;

  int r = pa->size - pb->size;

  return ((BrowserSort & SORT_REVERSE) ? -r : r);
}

static void browser_sort (struct browser_state *state)
{
  int (*f) (const void *, const void *);

  switch (BrowserSort & SORT_MASK)
  {
    case SORT_ORDER:
      return;
    case SORT_DATE:
      f = browser_compare_date;
      break;
    case SORT_SIZE:
      f = browser_compare_size;
      break;
    case SORT_SUBJECT:
    default:
      f = browser_compare_subject;
      break;
  }
  qsort (state->entry, state->entrylen, sizeof (struct folder_file), f);
}

static int link_is_dir (const char *path)
{
  struct stat st;

  if (stat (path, &st) == 0)
    return (S_ISDIR (st.st_mode));
  else
    return (-1);
}

static const char *
folder_format_str (char *dest, size_t destlen, char op, const char *src,
		   const char *fmt, const char *ifstring, const char *elsestring,
		   unsigned long data, format_flag flags)
{
  char fn[SHORT_STRING], tmp[SHORT_STRING], permission[10];
  char date[16], *t_fmt;
  time_t tnow;
  FOLDER *folder = (FOLDER *) data;
  struct passwd *pw;
  struct group *gr;
 
  switch (op)
  {
    case 'd':
      tnow = time (NULL);
      t_fmt = tnow - folder->f->st_mtime < 31536000 ? "%b %d %H:%M" : "%b %d  %Y";
      strftime (date, sizeof (date), t_fmt, localtime (&folder->f->st_mtime));
      snprintf (tmp, sizeof (tmp), "%%%ss", fmt);
      snprintf (dest, destlen, tmp, date);
      break;
    case 'f':
      strncpy (fn, folder->name, sizeof(fn) - 1);
      strcat (fn, S_ISLNK (folder->f->st_mode) ? "@" : 
	      (S_ISDIR (folder->f->st_mode) ? "/" : 
	       ((folder->f->st_mode & S_IXUSR) != 0 ? "*" : "")));
      snprintf (tmp, sizeof (tmp), "%%%ss", fmt);
      snprintf (dest, destlen, tmp, fn);
      break;
    case 'F':
      sprintf (permission, "%c%c%c%c%c%c%c%c%c%c",
	       S_ISDIR(folder->f->st_mode) ? 'd' : (S_ISLNK(folder->f->st_mode) ? 'l' : '-'),
	       (folder->f->st_mode & S_IRUSR) != 0 ? 'r': '-',
	       (folder->f->st_mode & S_IWUSR) != 0 ? 'w' : '-',
	       (folder->f->st_mode & S_ISUID) != 0 ? 's' : (folder->f->st_mode & S_IXUSR) != 0 ? 'x': '-',
	       (folder->f->st_mode & S_IRGRP) != 0 ? 'r' : '-',
	       (folder->f->st_mode & S_IWGRP) != 0 ? 'w' : '-',
	       (folder->f->st_mode & S_ISGID) != 0 ? 's' : (folder->f->st_mode & S_IXGRP) != 0 ? 'x': '-',
	       (folder->f->st_mode & S_IROTH) != 0 ? 'r' : '-',
	       (folder->f->st_mode & S_IWOTH) != 0 ? 'w' : '-',
	       (folder->f->st_mode & S_ISVTX) != 0 ? 't' : (folder->f->st_mode & S_IXOTH) != 0 ? 'x': '-');
      snprintf (tmp, sizeof (tmp), "%%%ss", fmt);
      snprintf (dest, destlen, tmp, permission);
      break;
    case 'g':
      if ((gr = getgrgid (folder->f->st_gid)))
      {
	snprintf (tmp, sizeof (tmp), "%%%ss", fmt);
	snprintf (dest, destlen, tmp, gr->gr_name);
      }
      else
      {
	snprintf (tmp, sizeof (tmp), "%%%sld", fmt);
	snprintf (dest, destlen, tmp, folder->f->st_gid);
      }
      break;
    case 'l':
      snprintf (tmp, sizeof (tmp), "%%%sd", fmt);
      snprintf (dest, destlen, tmp, folder->f->st_nlink);
      break;
    case 'N':
      snprintf (tmp, sizeof (tmp), "%%%sc", fmt);
      snprintf (dest, destlen, tmp, folder->new ? 'N' : ' ');
      break;
    case 's':
      snprintf (tmp, sizeof (tmp), "%%%sld", fmt);
      snprintf (dest, destlen, tmp, (long) folder->f->st_size);
      break;
    case 'u':
      if ((pw = getpwuid (folder->f->st_uid)))
      {
	snprintf (tmp, sizeof (tmp), "%%%ss", fmt);
	snprintf (dest, destlen, tmp, pw->pw_name);
      }
      else
      {
	snprintf (tmp, sizeof (tmp), "%%%sld", fmt);
	snprintf (dest, destlen, tmp, folder->f->st_uid);
      }
      break;
  }
  return (src);
}

static void add_folder (MUTTMENU *m, struct browser_state *state,
			const char *name, const struct stat *s, int new)
{
  char buffer[_POSIX_PATH_MAX + SHORT_STRING];
  FOLDER folder;

  folder.name = name;
  folder.f = s;
  folder.new = new;
  mutt_FormatString (buffer, sizeof (buffer), FolderFormat, folder_format_str,
		     (unsigned long) &folder, 0);

  if (state->entrylen == state->entrymax)
  {
    /* need to allocate more space */
    safe_realloc ((void **) &state->entry,
		  sizeof (struct folder_file) * (state->entrymax += 256));
    if (m)
      m->data = state->entry;
  }

  (state->entry)[state->entrylen].mode = s->st_mode;
  (state->entry)[state->entrylen].mtime = s->st_mtime;
  (state->entry)[state->entrylen].size = s->st_size;
  (state->entry)[state->entrylen].name = safe_strdup (name);
  (state->entry)[state->entrylen].desc = safe_strdup (buffer);

  (state->entrylen)++;
}

static void init_state (struct browser_state *state, MUTTMENU *menu)
{
  state->entrylen = 0;
  state->entrymax = 256;
  state->entry = (struct folder_file *) safe_malloc (sizeof (struct folder_file) * state->entrymax);
  if (menu)
    menu->data = state->entry;
}

static int examine_directory (MUTTMENU *menu, struct browser_state *state,
			      const char *d, const char *prefix)
{
  struct stat s;
  DIR *dp;
  struct dirent *de;
  char buffer[_POSIX_PATH_MAX + SHORT_STRING];
  BUFFY *tmp;

  if (stat (d, &s) == -1)
  {
    mutt_perror (d);
    return (-1);
  }

  if (!S_ISDIR (s.st_mode))
  {
    mutt_error ("%s is not a directory", d);
    return (-1);
  }

  mutt_buffy_check (0);

  if ((dp = opendir (d)) == NULL)
  {
    mutt_perror (d);
    return (-1);
  }

  init_state (state, menu);

  while ((de = readdir (dp)) != NULL)
  {
    if (strcmp (de->d_name, ".") == 0)
      continue;    /* we don't need . */
    
    if (prefix && *prefix && strncmp (prefix, de->d_name, strlen (prefix)) != 0)
      continue;
    if (regexec (Mask.rx, de->d_name, 0, NULL, 0) != 0)
      continue;

    snprintf (buffer, sizeof (buffer), "%s/%s", d, de->d_name);
    if (lstat (buffer, &s) == -1)
      continue;
    
    if ((! S_ISREG (s.st_mode)) && (! S_ISDIR (s.st_mode)) &&
	(! S_ISLNK (s.st_mode)))
      continue;
    
    tmp = Incoming;
    while (tmp && strcmp (buffer, tmp->path))
      tmp = tmp->next;
    add_folder (menu, state, de->d_name, &s, (tmp) ? tmp->new : 0);
  }
  closedir (dp);  
  browser_sort (state);
  return 0;
}

static int examine_mailboxes (MUTTMENU *menu, struct browser_state *state)
{
  struct stat s;
  char buffer[LONG_STRING];
  BUFFY *tmp = Incoming;

  if (!Incoming)
    return (-1);
  mutt_buffy_check (0);

  init_state (state, menu);

  do
  {
    if (lstat (tmp->path, &s) == -1)
      continue;

    if ((! S_ISREG (s.st_mode)) && (! S_ISDIR (s.st_mode)) &&
	(! S_ISLNK (s.st_mode)))
      continue;
    
    strfcpy (buffer, tmp->path, sizeof (buffer));
    mutt_pretty_mailbox (buffer);

    add_folder (menu, state, buffer, &s, tmp->new);
  }
  while ((tmp = tmp->next));
  browser_sort (state);
  return 0;
}

int select_file_search (MUTTMENU *menu, regex_t *re, int n)
{
  return (regexec (re, ((struct folder_file *) menu->data)[n].name, 0, NULL, 0));
}

void folder_entry (char *s, size_t slen, MUTTMENU *menu, int num)
{
  snprintf (s, slen, "%2d %s", num + 1, ((struct folder_file *) menu->data)[num].desc);
}

static void init_menu (struct browser_state *state, MUTTMENU *menu, char *title,
		       size_t titlelen, int buffy)
{
  char path[_POSIX_PATH_MAX];

  menu->current = 0;
  menu->top = 0;
  menu->max = state->entrylen;
  if (buffy)
    snprintf (title, titlelen, "Mailboxes [%d]", mutt_buffy_check (0));
  else
  {
    strfcpy (path, LastDir, sizeof (path));
    mutt_pretty_mailbox (path);
    snprintf (title, titlelen, "Directory [%s], File mask: %s",
	      path, Mask.pattern);
  }
  menu->redraw = REDRAW_FULL;
}

void mutt_select_file (char *f, size_t flen, int buffy)
{
  char buf[STRING];
  char prefix[_POSIX_PATH_MAX] = "";
  char helpstr[SHORT_STRING];
  char title[STRING];
  struct browser_state state;
  MUTTMENU *menu;
  struct stat st;
  int i, killPrefix = 0;

  memset (&state, 0, sizeof (struct browser_state));

  if (*f)
  {
    mutt_expand_path (f, flen);
    for (i = strlen (f) - 1; i > 0 && f[i] != '/' ; i--);
    if (i > 0)
    {
      if (f[0] == '/')
      {
	if (i > sizeof (LastDir) - 1) i = sizeof (LastDir) - 1;
	strncpy (LastDir, f, i);
	LastDir[i] = 0;
      }
      else
      {
	getcwd (LastDir, sizeof (LastDir));
	strcat (LastDir, "/");
	strncat (LastDir, f, i);
      }
    }
    else
    {
      if (f[0] == '/')
	strcpy (LastDir, "/");
      else
	getcwd (LastDir, sizeof (LastDir));
    }

    if (i <= 0 && f[0] != '/')
      strfcpy (prefix, f, sizeof (prefix));
    else
      strfcpy (prefix, f + i + 1, sizeof (prefix));
    killPrefix = 1;
  }
  else if (!LastDir[0])
    strfcpy (LastDir, Maildir, sizeof (LastDir));

  *f = 0;

  if (buffy)
  {
    if (examine_mailboxes (NULL, &state) == -1)
      return;
  }
  else if (examine_directory (NULL, &state, LastDir, prefix) == -1)
    return;

  menu = mutt_new_menu ();
  menu->menu = MENU_FOLDER;
  menu->make_entry = folder_entry;
  menu->search = select_file_search;
  menu->title = title;
  menu->data = state.entry;

  menu->help = mutt_compile_help (helpstr, sizeof (helpstr), MENU_FOLDER, FolderHelp);

  init_menu (&state, menu, title, sizeof (title), buffy);

  FOREVER
  {
    switch (i = mutt_menuLoop (menu))
    {
      case OP_GENERIC_SELECT_ENTRY:

	if (!state.entrylen)
	{
	  mutt_error ("No files match the file mask");
	  break;
	}

        if (S_ISDIR (state.entry[menu->current].mode) ||
	    (S_ISLNK (state.entry[menu->current].mode) &&
	    link_is_dir (state.entry[menu->current].name)))
	{
	  /* make sure this isn't a MH or maildir mailbox */
	  if (buffy)
	  {
	    strfcpy (buf, state.entry[menu->current].name, sizeof (buf));
	    mutt_expand_path (buf, sizeof (buf));
	  }
	  else
	    snprintf (buf, sizeof (buf), "%s/%s", LastDir, 
		      state.entry[menu->current].name);

	  if (mx_get_magic (buf) <= 0)
	  {
	    char OldLastDir[_POSIX_PATH_MAX];

	    /* save the old directory */
	    strfcpy (OldLastDir, LastDir, sizeof (OldLastDir));

	    if (strcmp (state.entry[menu->current].name, "..") == 0)
	    {
	      if (strcmp ("..", LastDir + strlen (LastDir) - 2) == 0)
		strcat (LastDir, "/..");
	      else
	      {
		char *p = strrchr (LastDir + 1, '/');

		if (p)
		  *p = 0;
		else
		{
		  if (LastDir[0] == '/')
		    LastDir[1] = 0;
		  else
		    strcat (LastDir, "/..");
		}
	      }
	    }
	    else if (buffy)
	    {
	      sprintf (LastDir, "%s", state.entry[menu->current].name);
	      mutt_expand_path (LastDir, sizeof (LastDir));
	    }
	    else
	      sprintf (LastDir + strlen (LastDir), "/%s",
		       state.entry[menu->current].name);

	    destroy_state (&state);
	    if (killPrefix)
	    {
	      prefix[0] = 0;
	      killPrefix = 0;
	    }
	    buffy = 0;
	    if (examine_directory (menu, &state, LastDir, prefix) == -1)
	    {
	      /* try to restore the old values */
	      strfcpy (LastDir, OldLastDir, sizeof (LastDir));
	      if (examine_directory (menu, &state, LastDir, prefix) == -1)
	      {
		strfcpy (LastDir, Homedir, sizeof (LastDir));
		return;
	      }
	    }
	    init_menu (&state, menu, title, sizeof (title), buffy);
	    break;
	  }
	}

	if (buffy)
	{
	  strfcpy (f, state.entry[menu->current].name, flen);
	  mutt_expand_path (f, flen);
	}
	else
	  snprintf (f, flen, "%s/%s", LastDir, state.entry[menu->current].name);

	/* Fall through to OP_EXIT */

      case OP_EXIT:

	destroy_state (&state);
	mutt_menuDestroy (&menu);
	return;

      case OP_CHANGE_DIRECTORY:

	strfcpy (buf, LastDir, sizeof (buf));
	if (mutt_get_field ("Chdir to: ", buf, sizeof (buf), M_FILE) == 0 &&
	    buf[0])
	{
	  buffy = 0;	  
	  mutt_expand_path (buf, sizeof (buf));
	  if (stat (buf, &st) == 0)
	  {
	    if (S_ISDIR (st.st_mode))
	    {
	      strfcpy (LastDir, buf, sizeof (LastDir));
	      destroy_state (&state);
	      if (examine_directory (menu, &state, LastDir, prefix) == 0)
		init_menu (&state, menu, title, sizeof (title), buffy);
	      else
	      {
		mutt_error ("Error scanning directory.");
		destroy_state (&state);
		mutt_menuDestroy (&menu);
		return;
	      }
	    }
	    else
	      mutt_error ("%s is not a directory.", buf);
	  }
	  else
	    mutt_perror (buf);
	}
	MAYBE_REDRAW (menu->redraw);
	break;
	
      case OP_ENTER_MASK:

	strfcpy (buf, Mask.pattern, sizeof (buf));
	if (mutt_get_field ("File Mask: ", buf, sizeof (buf), 0) == 0)
	{
	  regex_t *rx = (regex_t *) safe_malloc (sizeof (regex_t));
	  int err;

	  buffy = 0;
	  /* assume that the user wants to see everything */
	  if (!buf[0])
	    strfcpy (buf, ".", sizeof (buf));

	  if ((err = REGCOMP (rx, buf, REG_NOSUB | mutt_which_case (buf))) != 0)
	  {
	    regerror (err, rx, buf, sizeof (buf));
	    regfree (rx);
	    safe_free ((void **) &rx);
	    mutt_error ("%s", buf);
	  }
	  else
	  {
	    safe_free ((void **) &Mask.pattern);
	    regfree (Mask.rx);
	    safe_free ((void **) &Mask.rx);
	    Mask.pattern = safe_strdup (buf);
	    Mask.rx = rx;

	    destroy_state (&state);
	    if (examine_directory (menu, &state, LastDir, NULL) == 0)
	      init_menu (&state, menu, title, sizeof (title), buffy);
	    else
	    {
	      mutt_error ("Error scanning directory.");
	      mutt_menuDestroy (&menu);
	      return;
	    }
	    killPrefix = 0;
	  }
	}
	MAYBE_REDRAW (menu->redraw);
	break;

      case OP_SORT:
      case OP_SORT_REVERSE:

	{
	  int reverse = 0;

	  move (LINES - 1, 0);
	  if (i == OP_SORT_REVERSE)
	  {
	    reverse = SORT_REVERSE;
	    addstr ("Reverse ");
	  }
	  addstr ("Sort by (d)ate, (a)lpha, si(z)e or do(n)'t sort? ");
	  clrtoeol ();

	  while ((i = mutt_getch ()) != EOF && i != 'a' && i != 'd' && i != 'z'
		 && i != 'n')
	  {
	    if (i == ERR || CI_is_return (i))
	      break;
	    else
	      BEEP ();
	  }

	  if (i != EOF)
	  {
	    switch (i)
	    {
	      case 'a': 
	        BrowserSort = reverse | SORT_SUBJECT;
		break;
	      case 'd':
	        BrowserSort = reverse | SORT_DATE;
		break;
	      case 'z': 
	        BrowserSort = reverse | SORT_SIZE;
		break;
	      case 'n': 
	        BrowserSort = SORT_ORDER;
		break;
	    }
	    browser_sort (&state);
	    menu->redraw = REDRAW_FULL;
	  }
	}

	break;

      case OP_CHECK_NEW:

	buffy = 1 - buffy;
	destroy_state (&state);
	prefix[0] = 0;
	killPrefix = 0;
	if (buffy)
	{
	  if (examine_mailboxes (menu, &state) == -1)
	    return;
	}
	else if (examine_directory (menu, &state, LastDir, prefix) == -1)
	  return;
	init_menu (&state, menu, title, sizeof (title), buffy);
	break;

      case OP_BROWSER_NEW_FILE:

	snprintf (buf, sizeof (buf), "%s/", LastDir);
	if (mutt_get_field ("New file name: ", buf, sizeof (buf), M_FILE) == 0)
	{
	  strfcpy (f, buf, flen);
	  destroy_state (&state);
	  mutt_menuDestroy (&menu);
	  return;
	}
	MAYBE_REDRAW (menu->redraw);
	break;
    }
  }
  /* not reached */
}
