/*
  Copyright (C) 2003-2006, 2008, 2011, 2019 Rocky Bernstein
  <rocky@gnu.org>

  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 3 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, see <http://www.gnu.org/licenses/>.
*/

/* Program to debug read routines audio, mode1, mode2 forms 1 & 2. */

#include "util.h"
#include <cdio/mmc.h>

#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif

#include "getopt.h"

#ifndef O_BINARY
#define O_BINARY 0
#endif

/* Configuration option codes */
enum {
  OP_HANDLED = 0,

  /* NOTE: libpopt version associated these with drivers.  That
     appeared to be an unused historical artifact.
  */
  OP_SOURCE_AUTO,
  OP_SOURCE_BIN,
  OP_SOURCE_CUE,
  OP_SOURCE_NRG,
  OP_SOURCE_CDRDAO,
  OP_SOURCE_DEVICE,

  OP_USAGE,

  /* These are the remaining configuration options */
  OP_READ_MODE,
  OP_VERSION,

};

typedef enum
{
  READ_AUDIO = CDIO_READ_MODE_AUDIO,
  READ_M1F1  = CDIO_READ_MODE_M1F1,
  READ_M1F2  = CDIO_READ_MODE_M1F2,
  READ_M2F1  = CDIO_READ_MODE_M2F1,
  READ_M2F2  = CDIO_READ_MODE_M2F2,
  READ_MODE_UNINIT,
  READ_ANY
} read_mode_t;

/* Structure used so we can binary sort and set the --mode switch. */
typedef struct
{
  char name[30];
  read_mode_t read_mode;
} subopt_entry_t;


/* Sub-options for --mode.  Note: entries must be sorted! */
static const subopt_entry_t modes_sublist[] = {
  {"any",        READ_ANY},
  {"audio",      READ_AUDIO},
  {"m1f1",       READ_M1F1},
  {"m1f2",       READ_M1F2},
  {"m2f1",       READ_M2F1},
  {"m2f2",       READ_M2F2},
  {"mode1form1", READ_M1F1},
  {"mode1form2", READ_M1F2},
  {"mode2form1", READ_M2F1},
  {"mode2form2", READ_M2F2},
  {"red",        READ_AUDIO},
};

/* Used by `main' to communicate with `parse_opt'. And global options
 */
static struct arguments
{
  char          *access_mode; /* Access method driver should use for control */
  char          *output_file; /* file to output blocks if not NULL. */
  int            debug_level;
  int            hexdump;     /* Show output as a hexdump */
  int            nohexdump;   /* Don't output as a hexdump. I don't know
				 how to get popt to combine these as
				 one variable.
			       */
  int            just_hex;    /* Don't try to print "printable" characters
				 in hex dump.    */
  read_mode_t    read_mode;
  int            version_only;
  int            no_header;
  int            print_iso9660;
  source_image_t source_image;
  lsn_t          start_lsn;
  lsn_t          end_lsn;
  int            num_sectors;
} opts;

static void
hexdump (FILE *stream,  uint8_t * buffer, unsigned int len,
	 int just_hex)
{
  unsigned int i;
  for (i = 0; i < len; i++, buffer++)
    {
      if (i % 16 == 0)
	fprintf (stream, "0x%04x: ", i);
      fprintf (stream, "%02x", *buffer);
      if (i % 2 == 1)
	fprintf (stream, " ");
      if (i % 16 == 15) {
	if (!just_hex) {
	  uint8_t *p;
	  fprintf (stream, "  ");
	  for (p=buffer-15; p <= buffer; p++) {
	    fprintf(stream, "%c", isprint(*p) ?  *p : '.');
	  }
	}
	fprintf (stream, "\n");
      }
    }
  fprintf (stream, "\n");
  fflush (stream);
}

/* Comparison function called by bearch() to find sub-option record. */
static int
compare_subopts(const void *key1, const void *key2)
{
  subopt_entry_t *a = (subopt_entry_t *) key1;
  subopt_entry_t *b = (subopt_entry_t *) key2;
  return (strncmp(a->name, b->name, 30));
}

/* Do processing of a --mode sub option.
   Basically we find the option in the array, set it's corresponding
   flag variable to true as well as the "show.all" false.
*/
static void
process_suboption(const char *subopt, const subopt_entry_t *sublist, const int num,
                  const char *subopt_name)
{
  subopt_entry_t *subopt_rec =
    bsearch(subopt, sublist, num, sizeof(subopt_entry_t),
            &compare_subopts);
  if (subopt_rec != NULL) {
    opts.read_mode = subopt_rec->read_mode;
    return;
  } else {
    unsigned int i;
    bool is_help=strcmp(subopt, "help")==0;
    if (is_help) {
      report( stderr, "The list of sub options for \"%s\" are:\n",
	      subopt_name );
    } else {
      report( stderr, "Invalid option following \"%s\": %s.\n",
	      subopt_name, subopt );
      report( stderr, "Should be one of: " );
    }
    for (i=0; i<num-1; i++) {
      report( stderr, "%s, ", sublist[i].name );
    }
    report( stderr, "or %s.\n", sublist[num-1].name );
    exit (is_help ? EXIT_SUCCESS : EXIT_FAILURE);
  }
}


/* Parse source options. */
static void
parse_source(int opt)
{
  /* NOTE: The libpopt version made use of an extra temporary
     variable (psz_my_source) for all sources _except_ devices.
     This distinction seemed to serve no purpose.
  */
  /* NOTE: The libpopt version had a bug which kept it from
     processing toc-file inputs
  */

  if (opts.source_image != INPUT_UNKNOWN) {
    report( stderr, "%s: another source type option given before.\n",
	    program_name );
    report( stderr, "%s: give only one source type option.\n",
	    program_name );
    return;
  }

  /* For all input sources which are not a DEVICE, we need to make
     a copy of the string; for a DEVICE the fill-out routine makes
     the copy.
  */
  if (OP_SOURCE_DEVICE != opt)
    if (optarg != NULL) source_name = strdup(optarg);

  switch (opt) {
  case OP_SOURCE_BIN:
    opts.source_image  = INPUT_BIN;
    break;
  case OP_SOURCE_CUE:
    opts.source_image  = INPUT_CUE;
    break;
  case OP_SOURCE_NRG:
    opts.source_image  = INPUT_NRG;
    break;
  case OP_SOURCE_AUTO:
    opts.source_image  = INPUT_AUTO;
    break;
  case OP_SOURCE_DEVICE:
    opts.source_image  = INPUT_DEVICE;
    if (optarg != NULL) source_name = fillout_device_name(optarg);
    break;
  }
}


/* Parse a options. */
static bool
parse_options (int argc, char *argv[])
{
  int opt;
  int rc = EXIT_FAILURE;

  static const char helpText[] =
    "Usage: %s [OPTION...]\n"
    "  -a, --access-mode=STRING        Set CD control access mode\n"
    "  -m, --mode=MODE-TYPE            set CD-ROM read mode (audio, m1f1, m1f2,\n"
    "                                  m2mf1, m2f2)\n"
    "  -d, --debug=INT                 Set debugging to LEVEL\n"
    "  -x, --hexdump                   Show output as a hex dump. The default is a\n"
    "                                  hex dump when output goes to stdout and no\n"
    "                                  hex dump when output is to a file.\n"
    "  -j, --just-hex                  Don't display printable chars on hex\n"
    "                                  dump. The default is print chars too.\n"
    "  --no-header                     Don't display header and copyright (for\n"
    "                                  regression testing)\n"
    "  --no-hexdump                    Don't show output as a hex dump.\n"
    "  -s, --start=INT                 Set LBA to start reading from\n"
    "  -e, --end=INT                   Set LBA to end reading from\n"
    "  -n, --number=INT                Set number of sectors to read\n"
    "  -b, --bin-file[=FILE]           set \"bin\" CD-ROM disk image file as source\n"
    "  -c, --cue-file[=FILE]           set \"cue\" CD-ROM disk image file as source\n"
    "  -i, --input[=FILE]              set source and determine if \"bin\" image or\n"
    "                                  device\n"
    "  -C, --cdrom-device[=DEVICE]     set CD-ROM device as source\n"
    "  -N, --nrg-file[=FILE]           set Nero CD-ROM disk image file as source\n"
    "  -t, --toc-file[=FILE]           set \"TOC\" CD-ROM disk image file as source\n"
    "  -o, --output-file=FILE          Output blocks to file rather than give a\n"
    "                                  hexdump.\n"
    "  -V, --version                   display version and copyright information\n"
    "                                  and exit\n"
    "\n"
    "Help options:\n"
    "  -?, --help                      Show this help message\n"
    "  --usage                         Display brief usage message\n";

  static const char usageText[] =
    "Usage: %s [-a|--access-mode STRING] [-m|--mode MODE-TYPE]\n"
    "        [-d|--debug INT] [-x|--hexdump] [--no-header] [--no-hexdump]\n"
    "        [-s|--start INT] [-e|--end INT] [-n|--number INT] [-b|--bin-file FILE]\n"
    "        [-c|--cue-file FILE] [-i|--input FILE] [-C|--cdrom-device DEVICE]\n"
    "        [-N|--nrg-file FILE] [-t|--toc-file FILE] [-o|--output-file FILE]\n"
    "        [-V|--version] [-?|--help] [--usage]\n";

  /* Command-line options */
  static const char optionsString[] = "a:m:d:xjs:e:n:b::c::i::C::N::t::o:V?";
  static const struct option optionsTable[] = {

    {"access-mode", required_argument, NULL, 'a'},
    {"mode", required_argument, NULL, 'm'},
    {"debug", required_argument, NULL, 'd'},
    {"hexdump", no_argument, NULL, 'x'},
    {"no-header", no_argument, &opts.no_header, 1},
    {"no-hexdump", no_argument, &opts.nohexdump, 1},
    {"just-hex", no_argument, &opts.just_hex, 'j'},
    {"start", required_argument, NULL, 's'},
    {"end", required_argument, NULL, 'e'},
    {"number", required_argument, NULL, 'n'},
    {"bin-file", optional_argument, NULL, 'b'},
    {"cue-file", optional_argument, NULL, 'c'},
    {"input", optional_argument, NULL, 'i'},
    {"cdrom-device", optional_argument, NULL, 'C'},
    {"nrg-file", optional_argument, NULL, 'N'},
    {"toc-file", optional_argument, NULL, 't'},
    {"output-file", required_argument, NULL, 'o'},
    {"version", no_argument, NULL, 'V'},

    {"help", no_argument, NULL, '?' },
    {"usage", no_argument, NULL, OP_USAGE },
    { NULL, 0, NULL, 0 }
  };

  const char *program_name2 = strrchr(argv[0],'/');
  program_name = program_name2 ? strdup(program_name2+1) : strdup(argv[0]);

  while ((opt = getopt_long(argc, argv, optionsString, optionsTable, NULL)) >= 0)
    switch (opt)
      {
      case 'a': opts.access_mode = strdup(optarg); break;
      case 'd': opts.debug_level = atoi(optarg); break;
      case 'x': opts.hexdump = 1; break;
      case 's': opts.start_lsn = atoi(optarg); break;
      case 'e': opts.end_lsn = atoi(optarg); break;
      case 'n': opts.num_sectors = atoi(optarg); break;
      case 'b': parse_source(OP_SOURCE_BIN); break;
      case 'c': parse_source(OP_SOURCE_CUE); break;
      case 'i': parse_source(OP_SOURCE_AUTO); break;
      case 'C': parse_source(OP_SOURCE_DEVICE); break;
      case 'N': parse_source(OP_SOURCE_NRG); break;
      case 't': parse_source(OP_SOURCE_CDRDAO); break;
      case 'o': opts.output_file = strdup(optarg); break;

      case 'm':
	process_suboption(optarg, modes_sublist,
			  sizeof(modes_sublist) / sizeof(subopt_entry_t),
                            "--mode");
        break;

      case 'V':
        print_version(program_name, VERSION, 0, true);
	rc = EXIT_SUCCESS;
	goto error_exit;

      case '?':
	fprintf(stdout, helpText, program_name);
	rc = EXIT_INFO;
	goto error_exit;

      case OP_USAGE:
	fprintf(stderr, usageText, program_name);
	goto error_exit;

      case OP_HANDLED:
	break;
      }

  if (optind < argc) {
    const char *remaining_arg = argv[optind++];

    /* NOTE: A bug in the libpopt version checked source_image, which
       rendered the subsequent source_image test useless.
    */
    if (source_name != NULL) {
      report( stderr, "%s: Source specified in option %s and as %s\n",
	      program_name, source_name, remaining_arg );
      goto error_exit;
    }

    if (opts.source_image == INPUT_DEVICE)
      source_name = fillout_device_name(remaining_arg);
    else
      source_name = strdup(remaining_arg);

    if (optind < argc) {
      report( stderr, "%s: Source specified in previously %s and %s\n",
	      program_name, source_name, remaining_arg );
      goto error_exit;
    }
  }

  if (opts.debug_level == 3) {
    cdio_loglevel_default = CDIO_LOG_INFO;
  } else if (opts.debug_level >= 4) {
    cdio_loglevel_default = CDIO_LOG_DEBUG;
  }

  if (opts.read_mode == READ_MODE_UNINIT) {
    report( stderr,
	    "%s: Need to give a read mode "
	    "(audio, m1f1, m1f2, m2f1, m2f2, or auto)\n",
	    program_name );
    rc = 10;
    goto error_exit;
  }

  /* Check consistency between start_lsn, end_lsn and num_sectors. */

  if (opts.nohexdump && opts.hexdump != 2) {
    report( stderr,
	    "%s: don't give both --hexdump and --no-hexdump together\n",
	    program_name );
    rc = 11;
    goto error_exit;
  }

  if (opts.nohexdump) opts.hexdump = 0;

  if (opts.start_lsn == CDIO_INVALID_LSN) {
    /* Maybe we derive the start from the end and num sectors. */
    if (opts.end_lsn == CDIO_INVALID_LSN) {
      /* No start or end LSN, so use 0 for the start */
      opts.start_lsn = 0;
      if (opts.num_sectors == 0) opts.num_sectors = 1;
    } else if (opts.num_sectors != 0) {
      if (opts.end_lsn <= opts.num_sectors) {
	report( stderr, "%s: end LSN (%lu) needs to be greater than "
		" the sector to read (%lu)\n",
		program_name, (unsigned long) opts.end_lsn,
		(unsigned long) opts.num_sectors );
	rc = 12;
	goto error_exit;
      }
      opts.start_lsn = opts.end_lsn - opts.num_sectors + 1;
    }
  }

  /* opts.start_lsn has been set somehow or we've aborted. */

  if (opts.end_lsn == CDIO_INVALID_LSN) {
    if (0 == opts.num_sectors) opts.num_sectors = 1;
    opts.end_lsn = opts.start_lsn + opts.num_sectors - 1;
  } else {
    /* We were given an end lsn. */
    if (opts.end_lsn < opts.start_lsn) {
      report( stderr,
	      "%s: end LSN (%lu) needs to be grater than start LSN (%lu)\n",
	      program_name, (unsigned long) opts.start_lsn,
	      (unsigned long) opts.end_lsn );
      rc = 13;
      goto error_exit;
    }
    if (opts.num_sectors != opts.end_lsn - opts.start_lsn + 1)
      if (opts.num_sectors != 0) {
	 report( stderr,
		 "%s: inconsistency between start LSN (%lu), end (%lu), "
		 "and count (%d)\n",
		 program_name, (unsigned long) opts.start_lsn,
		 (unsigned long) opts.end_lsn, opts.num_sectors );
	 rc = 14;
	 goto error_exit;
	}
    opts.num_sectors = opts.end_lsn - opts.start_lsn + 1;
  }

  return true;
 error_exit:
  free(program_name);
  exit(rc);
}

static void
log_handler (cdio_log_level_t level, const char message[])
{
  if (level == CDIO_LOG_DEBUG && opts.debug_level < 2)
    return;

  if (level == CDIO_LOG_INFO  && opts.debug_level < 1)
    return;

  if (level == CDIO_LOG_WARN  && opts.debug_level < 0)
    return;

  gl_default_cdio_log_handler (level, message);
}


static void
init(void)
{
  opts.debug_level   = 0;
  opts.start_lsn     = CDIO_INVALID_LSN;
  opts.end_lsn       = CDIO_INVALID_LSN;
  opts.num_sectors   = 0;
  opts.read_mode     = READ_MODE_UNINIT;
  opts.source_image  = INPUT_UNKNOWN;
  opts.hexdump       = 2;         /* Not set. */

  gl_default_cdio_log_handler = cdio_log_set_handler (log_handler);
}

int
main(int argc, char *argv[])
{
  uint8_t buffer[CDIO_CD_FRAMESIZE_RAW] = { 0, };
  unsigned int blocklen=CDIO_CD_FRAMESIZE_RAW;
  CdIo *p_cdio=NULL;
  int output_fd=-1;
  FILE *output_stream;

  init();

  /* Parse our arguments; every option seen by `parse_opt' will
     be reflected in `arguments'. */
  parse_options(argc, argv);

  print_version(program_name, VERSION, opts.no_header, opts.version_only);

  p_cdio = open_input(source_name, opts.source_image, opts.access_mode);

  if (opts.output_file!=NULL) {

    /* If hexdump not explicitly set, then don't produce hexdump
       when writing to a file.
     */
    if (opts.hexdump == 2) opts.hexdump = 0;

    output_fd = open(opts.output_file, O_WRONLY|O_CREAT|O_TRUNC|O_BINARY, 0644);
    if (-1 == output_fd) {
      err_exit("Error opening output file %s: %s\n",
	       opts.output_file, strerror(errno));

    }
  } else
    /* If we are writing to stdout, then the default is to produce
       a hexdump.
     */
    if (opts.hexdump == 2) opts.hexdump = 1;


  for ( ; opts.start_lsn <= opts.end_lsn; opts.start_lsn++ ) {
    switch (opts.read_mode) {
    case READ_AUDIO:
    case READ_M1F1:
    case READ_M1F2:
    case READ_M2F1:
    case READ_M2F2:
      if (DRIVER_OP_SUCCESS !=
	  cdio_read_sector(p_cdio, &buffer,
			   opts.start_lsn,
			   (cdio_read_mode_t) opts.read_mode)) {
	report( stderr, "error reading block %u\n",
		(unsigned int) opts.start_lsn );
	blocklen = 0;
      } else {
	switch (opts.read_mode) {
	case READ_M1F1:
	  blocklen=CDIO_CD_FRAMESIZE;
	  break;
	case READ_M1F2:
	  blocklen=M2RAW_SECTOR_SIZE;
	  break;
	case READ_M2F1:
	  blocklen=CDIO_CD_FRAMESIZE;
	  break;
	case READ_M2F2:
	  blocklen=M2F2_SECTOR_SIZE;
	  break;
	default: ;
	}
      }
      break;

    case READ_ANY:
      {
	driver_id_t driver_id = cdio_get_driver_id(p_cdio);
	if (cdio_is_device(source_name, driver_id)) {
	  if (DRIVER_OP_SUCCESS !=
	      mmc_read_sectors(p_cdio, &buffer,
			       opts.start_lsn, CDIO_MMC_READ_TYPE_ANY, 1)) {
	    report( stderr, "error reading block %u\n",
		    (unsigned int) opts.start_lsn );
	    blocklen = 0;
	  }
	} else {
	  err_exit(
		   "%s: mode 'any' must be used with a real CD-ROM, not an image file.\n", program_name);
	}
      }
      break;

      case READ_MODE_UNINIT:
      err_exit("%s: Reading mode not set\n", program_name);
      break;
    }

    if (!opts.output_file) {
      output_stream = stdout;
    } else {
      output_stream = fdopen(output_fd, "w");
    }

    if (opts.hexdump)
      hexdump(output_stream, buffer, blocklen, opts.just_hex);
    else if (opts.output_file) {
      ssize_t bytes_ret;
      bytes_ret = write(output_fd, buffer, blocklen);
      (void)bytes_ret; /* Silence unused warnings */
    } else {
      unsigned int i;
      for (i=0; i<blocklen; i++) printf("%c", buffer[i]);
    }

  }

  if (opts.output_file) close(output_fd);

  myexit(p_cdio, EXIT_SUCCESS);
  /* Not reached:*/
  return(EXIT_SUCCESS);

}
