/*
 * Copyright 2001 Free Software Foundation, Inc.
 * 
 * This file is part of GNU Radio
 * 
 * GNU Radio 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, or (at your option)
 * any later version.
 * 
 * GNU Radio 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 GNU Radio; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */
#include <stdio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include "mc4020.h"
#include <getopt.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
#include <assert.h>
#include <sys/mman.h>
#include <sys/times.h>

#define	TRUE	1
#define	FALSE	0

#define	DEFAULT_CHANNEL		MCC_CH0_EN

#define	BUFFERSIZE_IN_SECONDS	(0.5)

char *dev = "/dev/mc4020_0";

#define	INFINITE_COUNT ((long long) -1)


struct optvals {
  int		source;
  int		range;
  int		chan[4];
  unsigned long	freq;
  long long	count;		// # of samples
  int		binary_p;	// TRUE if binary, else ascii
  int		fix_offset_p;	// TRUE if fix_offset, else dont
  int		verbose;
  int		help;
  int		threshold;
  int		clock_source;
  int		acquire_to_mem;  // TRUE if we should acquire all data
				 // to mem prior to writing to disk
};


inline static long
lmin (long a, long b)
{
  return a < b ? a : b;
}

static void
usage (struct option *option)
{
  int	i;
  int	len = 0;
  
  fprintf (stderr,
	   "usage: mc4020-read-adc [-a] [-b] [-v] [-c <count>] [-f <freq>]\n");
  for (i = 0; option[i].name != 0; i++){
    if (len == 0){
      fprintf (stderr, "\t");
      len = 8;
    }
    fprintf (stderr, "[--%s", option[i].name);
    len += strlen (option[i].name) + 1;
    switch (option[i].has_arg){

    default:
    case no_argument:
      fprintf (stderr, "] ");
      len += 2;
      break;

    case required_argument:
      fprintf (stderr, "=<value>] ");
      len += 10;
      break;

    case optional_argument:
      fprintf (stderr, "[=<value>]]");
      len += 12;
      break;
    }
    if (len >= 64){
      fprintf (stderr, "\n");
      len = 0;
    }
  }
  if (len != 0)
    fprintf (stderr, "\n");
}

// return pointer to statically allocated optvals struct iff successful,
// else NULL

struct optvals *
parse_args (int argc, char **argv)
{
  static struct optvals	opt;
  static struct option long_options[] = {
    // these options set a flag
    { "src-bnc",   no_argument,	&opt.source, MCC_ASRC_BNC },
    { "src-gnd",   no_argument,	&opt.source, MCC_ASRC_CAL_AGND },
    { "src-0.625", no_argument,	&opt.source, MCC_ASRC_CAL_0_625 },
    { "src-4.375", no_argument,	&opt.source, MCC_ASRC_CAL_4_375 },
    { "src-hdr",   no_argument,	&opt.source, MCC_ASRC_CAL_HDR },
    { "ch0",	     no_argument,	&opt.chan[0], 1 },
    { "ch1",	     no_argument,	&opt.chan[1], 1 },
    { "ch2",	     no_argument,	&opt.chan[2], 1 },
    { "ch3",	     no_argument,	&opt.chan[3], 1 },
    { "5v",	     no_argument,	&opt.range, MCC_ALL_5V },
    { "1v",	     no_argument,	&opt.range, MCC_ALL_1V },
    { "ascii",     no_argument,	&opt.binary_p, FALSE },
    { "binary",    no_argument,	&opt.binary_p, TRUE },
    { "fix-offset",no_argument,	&opt.fix_offset_p, TRUE },
    { "nofix-offset",no_argument,	&opt.fix_offset_p, FALSE },
    { "help",	     no_argument,	&opt.help, TRUE },
    { "verbose",   no_argument,	&opt.verbose, TRUE },
    { "clk-internal", 		no_argument, &opt.clock_source, MCC_CLK_INTERNAL },
    { "clk-ext-bnc",  		no_argument, &opt.clock_source, MCC_CLK_EXT_BNC  },
    { "clk-ad-start-trig-in", 	no_argument, &opt.clock_source, MCC_CLK_AD_START_TRIG_IN  },
    { "ext-bnc-threshold-0v",	no_argument, &opt.threshold,    MCC_EXT_BNC_THRESH_ZERO },
    { "ext-bnc-threshold-2.5v",	no_argument, &opt.threshold,    MCC_EXT_BNC_THRESH_2_5V },
    { "acquire-to-mem-then-write",	no_argument, &opt.acquire_to_mem, TRUE },
    { "noacquire-to-mem-then-write",	no_argument, &opt.acquire_to_mem, FALSE },
    // these options take an arg
    { "count",       required_argument, 0, 'c' },
    { "freq",	     required_argument, 0, 'f' },
    { 0, 0, 0, 0 }
  };

  int		c;

  memset (&opt, 0, sizeof (opt));
  
  opt.source = MCC_ASRC_BNC;
  opt.range = MCC_ALL_5V;
  opt.freq  = 20000000;
  opt.count = INFINITE_COUNT;
  opt.binary_p = FALSE;
  opt.fix_offset_p = TRUE;
  opt.verbose = FALSE;
  opt.clock_source = MCC_CLK_INTERNAL;
  opt.threshold = MCC_EXT_BNC_THRESH_ZERO;


  while (1){
    int option_index = 0;

    c = getopt_long (argc, argv, "abvf:c:",
		     long_options, &option_index);

    if (c == -1)	// end of options
      break;

    switch (c){
    case 0:
      // if this option sets a flag, do nothing else now.
      break;

    case 'a':
      opt.binary_p = FALSE;
      break;
      
    case 'b':
      opt.binary_p = TRUE;
      break;
      
    case 'v':
      opt.verbose = TRUE;
      break;
      
    case 'f':
      opt.freq = strtol (optarg, 0, 0);
      break;
      
    case 'c':
      opt.count = (long long) strtod (optarg, 0);
      break;
      
    case '?':
      // getopt_long already printed an error message
      usage (long_options);
      return 0;

    default:
      abort ();
    }
  }

  if (opt.help){
    usage (long_options);
    return 0;
  }

  if (opt.count == INFINITE_COUNT && opt.acquire_to_mem){
    fprintf (stderr, "can't use --acquire-to-mem-then-write without specifying --count\n");
    usage (long_options);
    return 0;
  }

  return &opt;
}


static void
fill_config (struct mc4020_config *c, int source, int range,
	     int chan[4], unsigned long freq, int clock_source, int threshold)
{
  memset (c, 0, sizeof (*c));
  c->scan_rate = freq;
  c->bitmask = source | range | clock_source | threshold;

  if (chan[0]) c->bitmask |= MCC_CH0_EN;
  if (chan[1]) c->bitmask |= MCC_CH1_EN;
  if (chan[2]) c->bitmask |= MCC_CH2_EN;
  if (chan[3]) c->bitmask |= MCC_CH3_EN;

  if ((c->bitmask & (MCC_CH0_EN | MCC_CH1_EN | MCC_CH2_EN | MCC_CH3_EN)) == 0)
    c->bitmask |= DEFAULT_CHANNEL;
}

static int
double_mmap (int fd, unsigned long bufsize, void **bufptr)
{
  int	fdz;
  char	*start = 0;

  fdz = open ("/dev/zero", O_RDONLY);
  if (fdz < 0){
    perror ("/dev/zero");
    return 0;
  }

  start = (char *) mmap (0, bufsize * 2, PROT_NONE, MAP_SHARED, fdz, 0);
  if (start == MAP_FAILED || munmap (start, bufsize * 2) == -1){
    perror ("Could not allocate mmap buffer");
    return 0;
  }
  close (fdz);

  if ((mmap (start, bufsize, PROT_READ,
	     MAP_FIXED | MAP_SHARED, fd, 0)) == MAP_FAILED
      || (mmap (start + bufsize, bufsize, PROT_READ,
		MAP_FIXED | MAP_SHARED, fd, 0)) == MAP_FAILED){
    perror ("Could not mmap ADC buffer");
    return 0;
  }
    
  *bufptr = start;
  return 1;
}


typedef int (*formatter_t)(int outfd,
			   FILE *outfp,
			   const char *buf,
			   unsigned long count);


int 
format_binary_nofix_offset (int outfd, FILE *outfp, const char *buf,
			    unsigned long count)
{
  int		n;
  while (count > 0){
    n = write (outfd, buf, count);
    if (n < 0){
      perror ("write error");
      return FALSE;
    }
    count -= n;
    buf += n;
  }
  return TRUE;
}

#define	TMPSIZE	(32 * 1024)

int 
format_binary_fix_offset (int outfd, FILE *outfp, const char *buf,
			  unsigned long count)
{
  int		n, m, i;
  const short	*sp;
  short		tmp[TMPSIZE];
    

  while (count > 0){
    m = lmin (sizeof (short) * TMPSIZE, count);
    sp = (const short *) buf;

    for (i = 0; i < m / sizeof (short); i++)
      tmp[i] = sp[i] - 0x0800;
    
    n = write (outfd, tmp, m);
    if (n < 0){
      perror ("write error");
      return FALSE;
    }
    count -= n;
    buf += n;
  }
  return TRUE;
}

#if 0		// these work but are really slooooow

int 
format_ascii_nofix_offset (int outfd, FILE *outfp, const char *buf,
			   unsigned long count)
{
  int	i;
  const short *sp = (const short *) buf;
  for (i = 0; i < count / sizeof (short); i++)
    fprintf (outfp, "%4d\n", sp[i]);

  return TRUE;
}

int 
format_ascii_fix_offset (int outfd, FILE *outfp, const char *buf,
			 unsigned long count)
{
  int	i;
  const short *sp = (const short *) buf;
  for (i = 0; i < count / sizeof (short); i++)
    fprintf (outfp, "%5d\n", sp[i] - 0x0800);

  return TRUE;
}

#else

// convert a signed integer into ascii.
// The integer is known to be in the range -9999 <= v <= 9999
//
// writes 6 bytes into buf.
// format is equivalent to sprintf (buf, "%5d\n", v) but without
// the trailing null

inline static void 
convert_1 (char *buf, int v)
{
  unsigned	u, r;
  int		i;
  char		sign;

  buf[5] = '\n';

  if (v == 0){
    buf[0] = ' ';
    buf[1] = ' ';
    buf[2] = ' ';
    buf[3] = ' ';
    buf[4] = '0';
    return;
  }

  sign = ' ';
  u = v;
  if (v < 0){
    u = -v;
    sign = '-';
  }

  i = 4;
  while (u != 0){
    r = u % 10;
    u = u / 10;
    buf[i--] = r + '0';
  }

  buf[i--] = sign;

  while (i >= 0)
    buf[i--] = ' ';
}

#define	SAMPLES_PER_BLK	4096

int 
format_ascii_fix_either (int outfd, FILE *outfp,
			 const char *inbuf, unsigned long count,
			 int offset)
{
  int		i;
  const short 	*sp = (const short *) inbuf;
  char 		outbuf[6*SAMPLES_PER_BLK];
  char		*op;

  count /= sizeof (short);	// convert to sample count
  
  // do a block's worth

  while (count >= SAMPLES_PER_BLK){

    op = outbuf;

    for (i = 0; i < SAMPLES_PER_BLK; i++){
      convert_1 (op, (sp[i] & 0x0fff) - offset);
      op += 6;
    }

    if (!format_binary_nofix_offset (outfd, outfp, outbuf, sizeof (outbuf)))
      return FALSE;
    
    count -= SAMPLES_PER_BLK;
    sp += SAMPLES_PER_BLK;
  }

  // finish up any fragment of a block

  if (count > 0){
    op = outbuf;
    for (i = 0; i < count; i++){
      convert_1 (op, (sp[i] & 0x0fff) - offset);
      op += 6;
    }

    if (!format_binary_nofix_offset (outfd, outfp,
				     outbuf, count * 6))
      return FALSE;
  }
  
  return TRUE;
}

int 
format_ascii_nofix_offset (int outfd, FILE *outfp, const char *buf,
			   unsigned long count)
{
  return format_ascii_fix_either (outfd, outfp, buf, count, 0);
}

int 
format_ascii_fix_offset (int outfd, FILE *outfp, const char *buf,
			 unsigned long count)
{
  return format_ascii_fix_either (outfd, outfp, buf, count, 0x0800);
}

#endif

static char *
allocate_buffer (size_t size, int verbose)
{
  char		*ram_buf = 0;
  size_t	i;
  int		page_size = getpagesize ();

  ram_buf = malloc (size);
  if (ram_buf == 0){
    perror ("Can't allocate ram buffer");
    return 0;
  }

  if (mlock (ram_buf, size) == -1){
    if (errno == EPERM)
      fprintf (stderr,
       "Unable to mlock buffer.  Run as root if you're getting overruns.\n");
    else
      perror ("mlock");

    /*
     * We failed to mlock, at least try to fault the buffer in...
     */
    if (verbose)
      fprintf (stderr, "Faulting in ram buffer...");

    for (i = 0; i < size; i += page_size)
      ram_buf[i] = 1;

    if (verbose)
      fprintf (stderr, "Done!\n");
  }
  return ram_buf;
}

#if 0
static void
deallocate_buffer (char *buf, size_t size)
{
  munlock (buf, size);
  free (buf);
}
#endif

static int
drop_privs (void)
{
  if (setgid (getgid ()) < 0){
    perror ("setgid");
    return FALSE;
  }

  if (setuid (getuid ()) < 0){
    perror ("setuid");
    return FALSE;
  }
  return TRUE;
}

int
main_loop (int infd, int outfd,
	   char *buf, long long total_count, int verbose,
	   formatter_t formatter, char *ram_buf)
{
  struct mc4020_status	status;
  clock_t		start_ticks, stop_ticks;
  struct tms		start_tms, stop_tms;
  long			ticks_per_second = sysconf (_SC_CLK_TCK);
  int			page_size = getpagesize ();
  double		elapsed_time;
  char			*p;
  FILE			*outfp;
  long long		count = 0;
  long			total_lost = 0;


  if ((outfp = fdopen (outfd, "w")) == 0){
    perror ("fdopen");
    return FALSE;
  }

  memset (&status, 0, sizeof (status));
  
  start_ticks = times (&start_tms);
  
  while (total_count == INFINITE_COUNT || count < total_count){
    unsigned long	n;
    
    if (ioctl (infd, GIOCSETGETSTATUS, &status) < 0){
      perror ("giocsetgetstatus");
      return FALSE;
    }

    if (status.lost){
      total_lost++;
      if (verbose)
	fprintf (stderr, "O");
    }

    if (status.num){
      
      // printf ("%6d\t%6d\t%d\n", status.index, status.num, status.lost);

      p = &buf[status.index * page_size];
      n = status.num * page_size;

      if (total_count != INFINITE_COUNT){
	if (total_count - count < n)
	  n = total_count - count;
      }

      if (ram_buf){
	// defer formatting and i/o, just copy to ram
	memcpy (&ram_buf[count], p, n);
      }
      else {
	if (!formatter (outfd, outfp, p, n))
	  return FALSE;
      }

      count += n;

      /* fprintf (stderr, status.lost ? "n" : "N"); */
    }
    else {	// status.num == 0
      /* fprintf (stderr, status.lost ? "z" : "Z"); */
    }
  }

  stop_ticks = times (&stop_tms);
  elapsed_time = (stop_ticks - start_ticks) / (double) ticks_per_second;

  if (verbose){
    fprintf (stderr, 
	     "transfered %lld bytes in %.2f seconds,  %.3g bytes/sec\n",
	     count, elapsed_time, count / elapsed_time);
  }

  if (total_lost != 0){			// always show overruns

    if (total_lost == 1)
      fprintf (stderr, "warning: 1 overrun\n");
    else
      fprintf (stderr, "warning: %ld overruns\n", total_lost);
  }

  if (ram_buf){
    // now handle deferred formatting and i/o
    if (!formatter (outfd, outfp, ram_buf, count))
      return FALSE;
  }
  
  return total_lost == 0;
}

int 
main (int argc, char **argv)
{
  int			fd;
  struct mc4020_config	config;
  char			*data;
  struct optvals	*opt;
  long			bufsize;
  int			nchans;
  formatter_t		formatter;
  char			*ram_buf = 0;


  opt = parse_args (argc, argv);
  if (opt == 0)
    return 1;

  fill_config (&config, opt->source, opt->range, opt->chan, opt->freq,
	       opt->clock_source, opt->threshold);

  fd = open (dev, O_RDONLY);
  if (fd < 0){
    perror (dev);
    return 1;
  }

  if (ioctl (fd, GIOCSETCONFIG, &config) < 0){
    perror ("giocsetconfig");
    return 1;
  }
  
  if (opt->clock_source != MCC_CLK_INTERNAL)
    opt->freq = 20000000;	// use this value for bufsize calculation

  // compute a reasonable buffersize ...

  nchans = opt->chan[0] + opt->chan[1] + opt->chan[2] + opt->chan[3];
  bufsize = nchans * opt->freq * sizeof (short) * BUFFERSIZE_IN_SECONDS;
  bufsize = (bufsize + MCBUF_MULTIPLE - 1) & ~(MCBUF_MULTIPLE - 1);
  if (bufsize < MCBUF_MINIMUM)
    bufsize = MCBUF_MINIMUM;

  if (opt->verbose)
    fprintf (stderr, "bufsize: %ld (0x%lx)\n", bufsize, bufsize);
  
  if (ioctl (fd, GIOCSETBUFSIZE, bufsize) < 0){
    perror ("setbufsize");
    return 1;
  }

  /*
   * Ideally, we'd like to do this up top, and then drop privs earlier,
   * but it turns out that we really need to get the kernel buffers allocated
   * in GIOCSETBUFSIZE before me malloc and mlock a GB or so of memory.
   * [Some of the driver's kernel buffers need to be contiguous.]
   */

  if (opt->acquire_to_mem){
    assert (opt->count >= 0);
    if (opt->count >= ((long long) 1) << 31){
      /* I'm sure this will be proven wrong eventually... */
      fprintf (stderr, "When using acquire_to_mem, count must be <= 2^31\n");
      exit (1);
    }
    ram_buf = allocate_buffer (opt->count * sizeof (short), opt->verbose);
    if (ram_buf == 0)
      exit (1);
  }

  if (!drop_privs ())	// drop any setuid / setgid privs
    exit (1);

  if (!double_mmap (fd, bufsize, (void **) &data))
    return 1;

  if (opt->binary_p){
    if (opt->fix_offset_p)
      formatter = format_binary_fix_offset;
    else
      formatter = format_binary_nofix_offset;
  }
  else {
    if (opt->fix_offset_p)
      formatter = format_ascii_fix_offset;
    else
      formatter = format_ascii_nofix_offset;
  }

  if (ioctl (fd, GIOCSTART, 0) < 0){
    perror ("giocstart");
    return 1;
  }

  {
    long long byte_count = opt->count;

    if (byte_count != INFINITE_COUNT)
      byte_count *= sizeof (short);

    if (main_loop (fd, 1, data, byte_count, opt->verbose, formatter, ram_buf))
      return 0;
    else
      return 1;
  }
}
