#include <stdlib.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
#include "conf.h"
#include "dir.h"
#include "err.h"
#include "mem.h"


/* The reason for this routine is that xterm/ curses fucks
 * up on some configurations if feed with non ascii characters.
 */

static void ascii_memcpy (char *to, char *from, int n)
{
  int c;
  
  for (;n>0;n--) {
    c = *from++;
    if (c < 32 || c > 126)
      c = '?';
    *to++ = c;
  }
}


/* Returns ERROR for no match
 */

static int match_format (struct CONFIG *cfg, char *s)
{
  int l, ml, i, j;
  char *s2, *m;
  
  l = strlen (s);
  for (i=0; i<cfg->formats; i++) {
    m = cfg->format[i].strings;
    ml = cfg->format[i].match_len;
    if (l >= ml) {
      s2 = s + l - ml;
      for (j=0; j<ml; j++) {
	if (*s2++ != m[j])
	  break;
      }
      if (j == ml)
	break;
    }
  }
  if (i == cfg->formats)
    i = ERROR;
  
  return i;
}


static int is_dir (char *path)
{
  struct stat buf;
  
  if (!stat (path, &buf)) {
    if (buf.st_mode &= S_IFDIR)
      return 1;
  }
  return 0;
}


static int is_file (char *path)
{
  struct stat buf;
  
  if (!stat (path, &buf)) {
    if (buf.st_mode &= S_IFREG)
      return 1;
  }
  return 0;
}


/* Return 1 if string b is less than string a.
 * a and b are not null terminated so I can't use strcmp.
 */

static int swap (char *a, char *b, int as, int bs)
{
  int i, f = 0, n;
  
  if (as < bs)
    n = as;
  else
    n = bs;
  
  for (i=0;i<n;i++) {
    if (a[i] > b[i]) {
      f = 1;
      break;
    }
    if (a[i] < b[i])
      break;
  }
  if (f)
    return 1;
  
  if (i == n) {
    if (as > bs)
      return 1;
  }
  
  return 0;
}


/* Sort directory by type and alphabeticaly.
 */

static void sort (struct DIR_INFO *dinfo, int parent)
{
  char *a, *b;
  int *list_old = 0, *list_new = 0, items, i, j, k, f, t, as, bs;
  struct DIR_ITEM *dir_items = 0;
  
  items = dir_count_parent (dinfo, parent);
  if (!items)
    return;
  mem_resize ((void **)&list_old, sizeof(int) * items);
  mem_resize ((void **)&list_new, sizeof(int) * items);
  mem_resize ((void **)&dir_items, sizeof(struct DIR_ITEM) * items);
  
  i = 0;
  j = 0;
  while ((i = dir_match_parent (dinfo, parent, i)) != -1) {
    list_old[j] = i;
    list_new[j++] = i++;
  }
  
  do {
    f=0;
    for (i=0; i<items-1; i++) {
      j = list_new[i];
      k = list_new[i+1];
      
      if (dinfo->item[j].type > dinfo->item[k].type) {
	t = list_new[i];
	list_new[i] = list_new[i+1];
	list_new[i+1] = t;
	f = 1;
      } 
    }
  } while (f);
  
  do {
    f = 0;
    for (i=0; i<items-1; i++) {
      j = list_new[i];
      k = list_new[i+1];
      
      if (dinfo->item[j].type == dinfo->item[k].type) {
	a = dinfo->strings + dinfo->item[j].offset;
	b = dinfo->strings + dinfo->item[k].offset;
	as = dinfo->item[j].size;
	bs = dinfo->item[k].size;
	
	if (swap (a, b, as, bs)) {
	  t = list_new[i];
	  list_new[i] = list_new[i+1];
	  list_new[i+1] = t;
	  f = 1;
	}
      } 
    }
  } while (f);
  
  for (i=0; i<items; i++)
    dir_items[i] = dinfo->item[list_new[i]];
  
  for (i=0; i<items; i++)
    dinfo->item[list_old[i]] = dir_items[i];
  
  free (list_old);
  free (list_new);
  free (dir_items);
}


/* Returns position of added entry.
 */

static int add_item (struct DIR_INFO *dinfo, char *name,
		     int type, int tree, int level, int parent)
{
  int num, size, old_size;
  struct DIR_ITEM *ditem;
  
  num = dinfo->items;
  size = sizeof(struct DIR_ITEM) * (num + 1);
  mem_resize ((void *)&dinfo->item, size);
  ditem = &dinfo->item[num];
  dinfo->items = num + 1;
  
  size = strlen (name);
  old_size = dinfo->size;
  dinfo->size += size;
  mem_resize ((void *)&dinfo->strings, dinfo->size);
  
  ascii_memcpy (dinfo->strings + old_size, name, size);
  
  ditem->queued = 0;
  ditem->type = type;
  ditem->tree = tree;
  ditem->level = level;
  ditem->parent = parent;
  ditem->offset = old_size;
  ditem->size = size;
  
  return num;
}


/* Return -1 for error
 */

static int add_items (struct DIR_INFO *dinfo, struct CONFIG *cfg,
		      char *path, int tree, int level, int parent)
{
  int format;
  DIR *dir;
  struct dirent *d;
  
  if (!(dir = opendir (path)))
    return ERROR;
  
  if (chdir (path))
    return ERROR;
  
  while ((d = readdir (dir))) {
    if (is_dir (d->d_name) & (d->d_name[0] != '.')) {
      add_item (dinfo, d->d_name, 0, tree, level, parent);
    } else {
      if (is_file (d->d_name)) {
	format = match_format (cfg, d->d_name);
	if (format != ERROR) {
	  add_item (dinfo, d->d_name, format + 1, tree, level, parent);
	  dinfo->files++;
	}  
      }
    }
  }
  closedir (dir);
  sort (dinfo, parent);
  return E_OK;
}


/* Return position or -1 for not found
 */

static int find_dir (struct DIR_INFO *dinfo,
		     int tree, int level, int start_dir)
{
  int i;
  
  for (i=start_dir; i<dinfo->items; i++) {
    if (dinfo->item[i].tree == tree) {
      if (dinfo->item[i].level == level) {
	if (! dinfo->item[i].type)
	  return i;
      }
    }
  }
  return ERROR;
}


/* Builds path to item, returns malloced memory
 */

char *dir_build_path (struct DIR_INFO *dinfo, int item)
{
  char *str = 0, *str2, *str3;
  int db_n = 0, str_s = 0, i, j;
  struct DIR_BUILD *db = 0;
  struct DIR_ITEM *di = &dinfo->item[item];
  
  db_n = di->level + 1;
  mem_resize ((void *)&db, db_n * sizeof(struct DIR_BUILD));
  
  for (i=0;i<db_n;i++) {
    db[i].name = dinfo->strings + di->offset;
    db[i].size = di->size;
    str_s += di->size + 1;
    di = &dinfo->item[di->parent];
  }
  
  str_s++;
  mem_resize ((void *)&str, str_s);
  str2 = str;
  
  for (i=db_n-1;i>=0;i--) {
    str3 = db[i].name;
    
    for (j=db[i].size;j>0;j--)
      *str2++ = *str3++;
    
    if (i) {
      *str2++ = '/';
    }
  }
  *str2 = 0;
  free (db);
  return str;
}


/* Returns -1 if path can't be opened
 */

int dir_recurse (struct DIR_INFO *dinfo, struct CONFIG *cfg,
		 char *path, int tree)
{
  int level = 0, level_hits = 0, epos;
  char *s, opath[1025];
  DIR *dir;
  
  dir = opendir (path);
  if (!dir)
    return ERROR;
  
  epos = add_item (dinfo, path, 0, tree, level, -1);
  closedir (dir);
  
  if (! getcwd (opath, 1025))  /* should use PATH_MAX */
    return ERROR;
  
  do {
    if (epos == ERROR) {
      level++;
      level_hits = 0;
      epos = 0;
    }
    epos = find_dir (dinfo, tree, level, epos);    
    if (epos != ERROR) {
      s = dir_build_path (dinfo, epos);
      add_items (dinfo, cfg, s, tree, level + 1, epos);
      free (s);
      chdir (opath);
      level_hits++;
      epos++;
    }
  } while (level_hits);
  
  return E_OK;
}


/* Return number of items matched by parent
 */

int dir_count_parent (struct DIR_INFO *dinfo, int parent)
{
  int i, num = 0;
  
  for (i=0; i<dinfo->items; i++) {
    if (dinfo->item[i].parent == parent) {
      num++;
    }
  }
  return num;
}


/*  Return item matched by parent
 */

int dir_match_parent (struct DIR_INFO *dinfo, int parent, int start_dir)
{
  int i;
  
  for (i=start_dir; i<dinfo->items; i++) {
    if (dinfo->item[i].parent == parent) {
      return i;
    }
  }
  return ERROR;
}


/* Return item matched by level == 0
 */

int dir_match_null_level (struct DIR_INFO *dinfo, int start_dir)
{
  int i;
  
  for (i=start_dir; i<dinfo->items; i++) {
    if (!dinfo->item[i].level) {
      return i;
    }
  }
  return ERROR;
}
