/* randomize:  Randomizes lines from a file or from stdin.

   Copyright (C) 2004-2026 by Brian Lindholm.  This file is part of the
   littleutils utility set.

   The randomize utility 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 3, or (at your option) any
   later version.

   The randomize utility 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
   the littleutils.  If not, see <https://www.gnu.org/licenses/>. */


#include <config.h>

#include <fcntl.h>
#ifdef HAVE_STDIO_H
# include <stdio.h>
#endif
#ifdef HAVE_STDLIB_H
# include <stdlib.h>
#endif
#ifdef HAVE_STRING_H
# include <string.h>
#endif
#ifdef HAVE_SYS_RANDOM_H
# include <sys/random.h>
#endif
#ifdef HAVE_SYS_STAT_H
# include <sys/stat.h>
#endif

#ifdef HAVE_UNISTD_H
# include <unistd.h>
# define OPTEND -1
#else
# define OPTEND EOF
#endif
#ifdef HAVE_GETOPT_H
# include <getopt.h>
#endif

#include "rand_funcs.h"

#ifdef __MINGW32__
extern int getopt(int argc, char * const *argv, const char *optstring);
extern char *optarg;
extern int optind;
#endif

#ifdef DJGPP
/* speed up stat command for DJGPP */
unsigned short _djstat_flags = 63;
#endif

#ifndef BLOCKSIZE
# define BLOCKSIZE 8192
#endif

#ifndef PATH_MAX
# define PATH_MAX 256
#endif

/* help function */
static void help(FILE *where)
{
  fprintf(where, "randomize " PACKAGE_VERSION "\n"
    "usage: randomize [-0/-z(ero_terminated)] [file...]\n");
}

/* read stdin into a buffer allocated here, returning number of bytes read */
static size_t load_stdin(char **buffer, char delimiter)
{
  char *tmp_buffer;
  size_t buffer_size, last_read, size;

  /* set up initial block */
  buffer_size = BLOCKSIZE;
  tmp_buffer = (char *) malloc(buffer_size * sizeof(char));
  if (tmp_buffer == NULL) {
    fprintf(stderr, "randomize error: out of memory A!\n");
    exit(4);
  }
  /* keep reading, doubling the memory allocation along the way if necessary */
  size = 0;
  while (!feof(stdin)) {
    last_read = fread(&tmp_buffer[size], sizeof(char), BLOCKSIZE, stdin);
    size += last_read * sizeof(char);
    if ((!feof(stdin)) && ((size + BLOCKSIZE) >= buffer_size)) {
      buffer_size *= 2;
      tmp_buffer = realloc(tmp_buffer, buffer_size);
      if (tmp_buffer == NULL) {
        fprintf(stderr, "randomize error: out of memory B!\n");
        exit(4);
      }
    }
  }
  /* ensure that the last character of non-empty input is a delimiter character, appending it if necessary */
  if ((size > 0) && (tmp_buffer[size - 1] != delimiter)) {
    if (size == buffer_size) {
      tmp_buffer = realloc(tmp_buffer, buffer_size + 1);
      if (tmp_buffer == NULL) {
        fprintf(stderr, "randomize error: out of memory B!\n");
        exit(4);
      }
    }
    tmp_buffer[size] = delimiter;
    size += 1;
  }
  *buffer = tmp_buffer;
  return(size);
}

/* allocate memory for files function, returning a pointer to a buffer */
static void prep_buffer(char **buffer, size_t size)
{
  char *tmp_buffer;

  tmp_buffer = (char *) malloc(size * sizeof(char));
  if (tmp_buffer == NULL) {
    fprintf(stderr, "randomize error: out of memory C!\n");
    exit(4);
  }
  *buffer = tmp_buffer;
}

/* read a file into a pre-allocated buffer, returning number of bytes read from file */
static size_t load_file(char *buffer, char *filename, size_t size, size_t index, char delimiter)
{
  size_t qty_read;
  FILE *input_file;

  input_file = fopen(filename, "r");
  if (input_file == NULL) {
    fprintf(stderr, "randomize error: can't open %s!\n", filename);
    exit(3);
  }
  /* read in the file */
  qty_read = fread(&buffer[index], sizeof(char), size, input_file);
  if (qty_read != size) {
    fprintf(stderr, "randomize error: can't read all of %s!\n", filename);
    exit(3);
  }
  /* ensure that the last character of non-empty input is a delimiter character, appending it if necessary */
  if (((index + size) > 0) && (buffer[index + size - 1] != delimiter)) {
    buffer[index + size] = delimiter;
    size += 1;
  }
  (void) fclose(input_file);
  return(size);
}

/* scan buffer for substrings, returning arrays of pointers and lengths */
static void scan_buffer(char *buffer, size_t **indices, size_t **lengths, size_t size, size_t delimiter_qty,
  char delimiter)
{
  size_t i, index, previous_end;
  size_t *tmp_indices, *tmp_lengths;

  /* allocate access arrays */
  tmp_indices = (size_t *) malloc((delimiter_qty + 1) * sizeof(size_t));
  if (tmp_indices == NULL) {
    fprintf(stderr, "randomize error: out of memory D\n");
    exit(4);
  }
  tmp_lengths = (size_t *) malloc((delimiter_qty + 1) * sizeof(size_t));
  if (tmp_lengths == NULL) {
    fprintf(stderr, "randomize error: out of memory E\n");
    exit(4);
  }
  /* find first line */
  index = 0;
  tmp_indices[index] = 0;
  i = 0;
  while ((buffer[i] != delimiter) && (i < size)) i++;
  tmp_lengths[index] = i;
  previous_end = i;
  /* find remaining lines */
  for (i = previous_end + 1; i < size; i++) if (buffer[i] == delimiter) {
    index++;
    tmp_indices[index] = previous_end + 1;
    tmp_lengths[index] = i - previous_end - 1;
    previous_end = i;
  }
  /* sanity check */
  if (index != (delimiter_qty - 1)) {
    fprintf(stderr, "randomize error: count mismatch %u vs. %u!\n", (unsigned int) index,
      (unsigned int) delimiter_qty - 1);
    exit(3);
  }
  *indices = tmp_indices;
  *lengths = tmp_lengths;
}

/* main program */
int main(int argc, char *argv[])
{
  char *buffer, delimiter, *filename;
  int argn, opt;
  size_t delimiter_qty, i, index, *indices, j, k, length, *lengths, size, sum;
  struct stat file_stats;

  /* parse options */
  delimiter = '\n';
  while ((opt = getopt (argc, argv, "0hz")) != OPTEND) switch (opt) {
    case '0':
      delimiter = '\0';
      break;
    case 'h':
      help(stdout);
      return(0);
    case 'z':
      delimiter = '\0';
      break;
    case '?':
      help(stderr);
      return(1);
  }
  /* read standard input or files into buffer */
  if ((argc - optind) == 0)
    size = load_stdin(&buffer, delimiter);
  else {
    sum = 0;
    for (argn = optind; argn < argc; argn++) {
      filename = argv[argn];
      if (stat(filename, &file_stats)) {
        fprintf(stderr, "randomize error: can't determine size of %s\n", filename);
        exit(1);
      }
      if (((file_stats.st_mode & S_IFDIR) != S_IFDIR) && ((file_stats.st_mode & S_IFREG) == S_IFREG))
        sum += (size_t) file_stats.st_size + 1;
      else
        fprintf(stderr, "randomize warning: skipping non-regular file %s \n", filename);
    }
    prep_buffer(&buffer, sum);
    index = 0;
    for (argn = optind; argn < argc; argn++) {
      filename = argv[argn];
      (void) stat(filename, &file_stats);
      if (((file_stats.st_mode & S_IFDIR) != S_IFDIR) && ((file_stats.st_mode & S_IFREG) == S_IFREG))
        index += load_file(buffer, filename, (size_t) file_stats.st_size, index, delimiter);
    }
    size = index;
  }
  if (size > 0) {
    /* count delimiters */
    delimiter_qty = 0;
    for (i = 0; i < size; i++) if (buffer[i] == delimiter) delimiter_qty++;
    /* create access arrays for index and length */
    scan_buffer(buffer, &indices, &lengths, size, delimiter_qty, delimiter);
    rand_seed();
    /* shuffle the list twice */
    for (k = 0; k < 2; k++) for (i = 0; i < delimiter_qty; i++) {
      j = rand_int(delimiter_qty);
      index = indices[i];
      indices[i] = indices[j];
      indices[j] = index;
      length = lengths[i];
      lengths[i] = lengths[j];
      lengths[j] = length;
    }
    /* print the list using the randomized pointers */
    for (i = 0; i < delimiter_qty; i++) (void) fwrite(&buffer[indices[i]], sizeof(char), lengths[i] + 1, stdout);
    /* free up arrays */
    free(lengths);
    free(indices);
  }
  /* free up buffer */
  free(buffer);
  /* indicate successful finish */
  return(0);
}
