/* 
 * $Id: shuffle.c,v 1.12 1999/02/01 22:40:49 drz Exp drz $
 * Most code by Tim Newsome <nuisance@cmu.edu>
 * Source flag code by Jay Kominek <jkominek@wiw.org>
 */

#include <config.h>

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if TIME_WITH_SYS_TIME
#include <sys/time.h>
#include <time.h>
#else
#if HAVE_SYS_TIME_H
#include <sys/time.h>
#else
#include <time.h>
#endif  /* HAVE_SYS_TIME_H */
#endif  /* TIME_WITH_SYS_TIME */
#include <unistd.h>
#ifdef HAVE_GETOPT_H
#include <getopt.h>
#endif
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include "shuffle.h"

static int shuffle_commandline(int n, int argc, char *argv[], int source);
static int shuffle_stream(FILE *stream, int n, int source);
static int randomize();
static void display_help();
static void display_version();
inline static int add_to_data(char *c, int length, char **data_start,
                              long *data_size, long *data_index);
inline static int add_to_index(long data_index, long **index_start,
                               long *index_size, long *index_index);
static unsigned long get_random(int source);

int main(int argc, char *argv[]) {
  int ret;
  int c;
  /**
   * The amount of elements to output.
   */
  int n = -1;

  /* File to read random numbers from. */
  char *source = NULL;
  int sourcefd;

#ifdef HAVE_GETOPT_LONG
  int option_index;
#endif

  /*
   * Parse any commandline arguments.
   */
#ifdef HAVE_GETOPT_LONG
  static struct option long_options[] =
  {
    {"version", no_argument, 0, 'v'},
    {"help",    no_argument, 0, 'h'},
    {"number", required_argument, 0, 'n'},
    {"source", required_argument, 0, 's'},
    {0, 0, 0, 0}};
  option_index = 0;
#endif

#ifdef HAVE_GETOPT_LONG
  while ((c = getopt_long(argc, argv, "s:n:vh", long_options, &option_index))
         != EOF) {
#else
  while ((c = getopt(argc, argv, "s:n:vh")) != EOF) {
#endif
    switch (c) {

    case 'n':
      n = atoi(optarg);
      break;

    case 'h':
      display_help();
      return 0;

    case 'v':
      display_version();
      return 0;

    case 's':
            source = strdup(optarg);
      break;

    default:
      return 1;
    }
  }

  /*
   * Hide the arguments from the actual shuffling program.
   */
  argc -= optind - 1;
  argv += optind - 1;

  /*
   * Seed the random number generator. 
   */
  sourcefd = randomize(source);

  /*
   * If there are no arguments. 
   */
  if (argc == 1) {
    ret = shuffle_stream(stdin, n, sourcefd);
  } else {
    /*
     * Shuffle the commandline, not including the name of the program. 
     */
    ret = shuffle_commandline(n, --argc, ++argv, sourcefd);
  }

  switch (ret) {
  case SE_NOMEM:
    fprintf(stderr, "%s: %s\n", argv[0], SE_NOMEM_STR);
    break;
  }

  if(sourcefd>=0)
    close(sourcefd);

  return ret;
}

/* 
 * This function is called to shuffle the commandline, which we already
 * have in the form of an index to an array of strings.
 */
int shuffle_commandline(int n, int argc, char *argv[], int source) {
  unsigned long r;

  while (argc && n--) {
    r = get_random(source) % argc;           /* Pick a random argument. */
    fputs(argv[r], stdout);      /* Print it. */
    putchar(' ');                /* Add a space. */
    /*
     * Move the last element from the list onto the argument we just
     * printed and decrement argc. 
     */
    argv[r] = argv[--argc];
  }
  return SE_NOERR;
}

int read_stream(FILE *stream, char **data_start, long *data_size) {
  long data_index = 0;          /* Which entry we're currently looking at. */
  char c;
  int ret;
  char buffer[READ_BUFFER_SIZE];
  size_t bytes_read;

  /*
   * First, get some memory to start of with.
   */
  *data_size = DATA_INCREMENT;
  *data_start = malloc(*data_size * sizeof(char));
  if (*data_start == NULL)
    return SE_NOMEM;

  while ((bytes_read = fread(buffer, 1, READ_BUFFER_SIZE, stream)) > 0) {
    ret = add_to_data(buffer, bytes_read, data_start, data_size, &data_index);
    if (ret != SE_NOERR) {
      free(*data_start);
      return ret;
    }
  }                              /* EOF */

  if (ferror(stdin)) {
    free(*data_start);
    return SE_READERR;
  }
  /*
   * Stick a separator at the end of our data, so we don't walk out of our
   * memory when we print out the last element.
   */
  if ((*data_start)[data_index - 1] != SEPARATOR) {
    c = SEPARATOR;
    ret = add_to_data(&c, 1, data_start, data_size, &data_index);
    if (ret != SE_NOERR) {
      free(*data_start);
      return ret;
    }
  }
  return SE_NOERR;
}

int build_index(const char *data_start, const long data_size,
                long **index_start, long *index_size) {
  long index_index;
  long data_index;
  int ret;

  *index_size = INDEX_INCREMENT;
  *index_start = malloc(*index_size * sizeof(char *));
  if (*index_start == NULL)
    return SE_NOMEM;

  /*
   * First entry points to the very first element.
   */
  (*index_start)[0] = 0;
  index_index = 1;

  for (data_index = 0; data_index < data_size; data_index++) {
    if (data_start[data_index] == SEPARATOR) {
      ret = add_to_index(data_index + 1, index_start, index_size, &index_index);
      if (ret != SE_NOERR) {
        free(index_start);
        return ret;
      }
    }
  }

  /*
   * Get rid of the last entry, which basically points to EOF.
   */
  *index_size = index_index - 1;

  return SE_NOERR;
}

int shuffle_memory(int n, const char *data_start, long *index_start,
       long index_size, int source) {
  unsigned long r;
  const char *p;

  /*
   * Shuffle the stuff and print it as we go.
   */

  while (index_size && n--) {
    r = get_random(source) % index_size;  /* Pick a random argument. */
    /*
     * Print it.
     */
    p = data_start + index_start[r];
    while (*p != SEPARATOR) {
      putchar(*p++);
    }
    putchar(SEPARATOR);
    /*
     * Move the last element from the list onto the argument we just
     * printed. 
     */
    index_start[r] = index_start[--index_size];
  }

  return SE_NOERR;
}

#if 0
/*
 * Useful for when/if I support shuffling a file.
 */
int fseekable(FILE *stream) {
  /* Definitely not possible on a non-seekable stream. */
  if (fseek(stream, 0, SEEK_END) || fseek(stream, 0, SEEK_SET))
    return (errno == EBADF);

  return 0;
}
#endif

int shuffle_stream(FILE *stream, int n, int source) {
  long *index_start;            /* Where our index starts. */
  long index_size;
  char *data_start;              /* Where our data starts. */
  long data_size;
  int ret;

  ret = read_stream(stream, &data_start, &data_size);
  if (ret != SE_NOERR)
    return ret;

  ret = build_index(data_start, data_size, &index_start, &index_size);
  if (ret != SE_NOERR) {
    free(data_start);
    return ret;
  }
  shuffle_memory(n, data_start, index_start, index_size, source);

  /*
   * Free up the memory again.
   */
  free(data_start);
  free(index_start);

  return SE_NOERR;
}

inline static int add_to_data(char *c, int length, char **data_start,
            long *data_size, long *data_index) {
  char *new_data = NULL;
  long new_size;

  new_size = *data_index + length;

  if (new_size >= *data_size) {
    /*
     * Get more data memory. 
     */
    *data_size = new_size * 2;
    new_data = realloc(*data_start, *data_size * sizeof(char));
    if (new_data == NULL)
      return SE_NOMEM;

    *data_start = new_data;
  }
  memcpy(*data_start + *data_index, c, length * sizeof(char));
  *data_index += length;

  return SE_NOERR;
}

inline static int add_to_index(long data_index, long **index_start,
                               long *index_size, long *index_index) {
  long *new_index = NULL;

  if (*index_index >= *index_size) {
    /*
     * Add more index memory. 
     */
    *index_size *= 2;
    new_index = realloc(*index_start, *index_size * sizeof(char *));
    if (new_index == NULL)
      return SE_NOMEM;

    *index_start = new_index;
  }
  (*index_start)[(*index_index)++] = data_index;

  return SE_NOERR;
}

static int randomize(const char *source) {
  unsigned int seed;
  int fd;

#ifdef HAVE_GETTIMEOFDAY
  struct timeval tv;
  struct timezone tz;
  long lseed;
#else
  time_t tseed;
#endif

  if (source && (fd = open(source, O_RDONLY)) >= 0) {
    return fd;

  } else {

#ifdef HAVE_GETTIMEOFDAY
    gettimeofday(&tv, &tz);
    lseed = tv.tv_usec & ((unsigned int) -1);
    seed = (unsigned int) lseed;

#else
    tseed = time(NULL) & ((unsigned int) -1);
    seed = (unsigned int) tseed;
#endif

    seed = seed ^ (unsigned int) getpid();
    srandom(seed);
    return -1;
  }
}

static unsigned long get_random(int source) {
  unsigned long tmp = 0;
  if (source >= 0) {
    read(source, &tmp, sizeof(unsigned long));
  } else {
    tmp = random();
  }
  return tmp;
}

static void display_version() {
  fprintf(stderr, VERSION);
}

static void display_help() {
  printf(HELP);
}
