#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>

#include "tsadc.h"

/* Possible improvements:
 * - expand for use with TS-ADC24
 * - add support for TS-8100
 * - replace assert statements with better error handling
 */

/* After initialization, these macros are used to access hardware
 * registers.  ADC(x) where x ranges from 0 to 0x1E will access the 
 * TS-ADC16 control registers as defined in the getting started sheet.
 * ADC(x) is for 16 bit access.  For certain operations 8 bit access
 * is required, hence ADC8(x).
 */
#define ADC(x) (*(adc.io + (x)/sizeof(unsigned short)))
#define CONFIG(x) (*(config + (x)/sizeof(unsigned short)))
#define ADC8(x) (*(adc.io8 + (x)))

/* ADC16 register offsets: */
#define BID_REG        0x0
#define CFG_REG        0x2
#define DLY_MSB_REG    0x4
#define DLY_LSB_REG    0x6
#define STAT_REG       0x8
#define FIFO_REG       0xA
#define DIO_REG        0x18

#define FFCHAN ((ADC(STAT_REG) >> 1) & 0x1f)
#define FFCOUNT (ADC(STAT_REG) >> 6)

/* PC/104 IO space for different models: */
#ifdef TS7200
#define IO_BASE_8   0x11E00000
#define IO_BASE_16  0x21E00000
#define IO_OFFSET   0x100
#endif

#ifdef TS7350
#define IO_BASE_8   0x600FF000
#define IO_BASE_16  0x600FF000
#define IO_OFFSET   0x900
#endif

#ifdef TS7800
#define IO_BASE_8   0xEE000000
#define IO_BASE_16  0xEF000000
#define IO_OFFSET   0x100
#endif

#define CUSHION 16

/* adc is a global variable used primarily to hold pointers into 
 * memory space.  These are set up by tsadc_init.
 */
typedef struct {
  unsigned char status; /* 1 = useable, 0 = uninitialized */
  volatile unsigned short *io; /* pointer to adc board io space */
  volatile unsigned char *io8; /* 8 bit access is required for certain operations */
  int fd; /* file descriptor used to open /dev/mem */
} tsadc;

tsadc adc;

/* tsadc_init will attempt to initialize the adc.
   on success it will return 0 and adc will be able
   to be used.  Otherwise it will return -1
*/
int tsadc_init() {
  volatile unsigned short *config;
  unsigned int offset;
  int success;

  /* This block of code makes it so that config is a pointer into
   * memory space.
   */
  adc.fd = open("/dev/mem", O_RDWR|O_SYNC);
  assert(adc.fd != -1);
  config = (unsigned short *)mmap(0, getpagesize(),
    PROT_READ|PROT_WRITE, MAP_SHARED, adc.fd, IO_BASE_16);
  assert(config != MAP_FAILED);

#ifdef TS7350
  /* Configure PC/104 connector for PC/104: */
  CONFIG(0x98) = 0x5555;
  CONFIG(0x9a) = 0x5555;
  CONFIG(0x9c) = 0x5555;
  CONFIG(0x9e) = 0x5555;
  /* Configure ISA bus for 16 bit ARM mode: */
  CONFIG(0xd4) |= 0x0800;
#endif

#ifdef TS7800
  /* Configure PC/104 connector for PC/104: */
  CONFIG(0x30) = 0x5555;
  CONFIG(0x32) = 0x5555;
  CONFIG(0x34) = 0x5555;
  CONFIG(0x36) = 0x5555;
  /* Configure ISA bus for 16 bit ARM mode: */
  CONFIG(0x0c) |= 0x0800;
#endif
  
  adc.io = config + IO_OFFSET/(sizeof(unsigned short));
  adc.status = 0;
  adc.io8 = (unsigned char *)mmap(0, getpagesize(),
    PROT_READ|PROT_WRITE, MAP_SHARED, adc.fd, IO_BASE_8);
  assert(adc.io8 != MAP_FAILED);
  adc.io8 += IO_OFFSET;
  
  success = 0;
  offset = 0;
  /* Automatically detect memory space set by JP1 and JP2.
   * checking only for TS-ADC16 at this time: */
  while (offset < 0x80 && !success) {
    //fprintf(stderr, "trying offset 0x%x\n", offset);
    if(ADC8(offset) == 0x3e){
      adc.io += offset/sizeof(unsigned short);
      adc.io8 += offset;
      adc.status = 1;
      success = 1;
      //fprintf(stderr, "ADC16 found at offset 0x%x\n", offset);
    }
    offset += 0x20;
  }
  
  if(!success)
    return -1;
    
  if(ADC8(1) & 0x40) /* JP3 must be ON */ 
    return 0;
  else
    return -1;
}

int adc_digital_out(unsigned char output) {
  
  if(adc.status == 0)
    tsadc_init();
  if(adc.status == 0){
    fprintf(stderr, "Aborted adc_digital_out: unable to find TS-ADC16.\n");
    return -1;
  }
  
  /* set or clear digital output bit: */
  if(output)
    ADC(DIO_REG) |= 0x10;
  else
    ADC(DIO_REG) &= ~0x10;
  
  return 0;

}

unsigned char adc_digital_in() {

  if(adc.status == 0)
    tsadc_init();
  if(adc.status == 0){
    fprintf(stderr, "Aborted adc_digital_in: unable to find TS-ADC16.\n");
    return -1;
  }
  
  return ADC(DIO_REG) & 0xf;
}

unsigned short adc_counter(unsigned char counter_id) {

  if(adc.status == 0)
    tsadc_init();
  if(adc.status == 0){
    fprintf(stderr, "Aborted adc_counter: unable to find TS-ADC16.\n");
    return -1;
  }

  int offset = 0x10 + counter_id * 2;

  return(ADC(offset));

}

int dac_set(unsigned char dac_id, unsigned short value) {

  if(adc.status == 0)
    tsadc_init();
  if(adc.status == 0){
    fprintf(stderr, "Aborted dac_set: unable to find TS-ADC16.\n");
    return -1;
  }
  
  if (value > 0xfff) return -1;
  
  value |= (((unsigned short)dac_id) << 14);
  
  value |= 0x2000;
  
  //fprintf(stderr, "value=0x%x\n", value);
  
  ADC(0xE) = value;
  ADC8(0xF) = value >> 8;
  
  return 0;

}

/* adc_acquire will attempt to acquire n_samples samples at s_rate Hz.
   It will return the number of samples actually acquired.
*/
int32_t adc_acquire(int8_t channel_id, int32_t s_rate, int32_t n_samples,  uint16_t *data1, uint16_t *data2) {
  
  unsigned int pacing, chan, prevchan;
  unsigned short adccfg, result, ffcount;
  int i1, i2, okay;

  if(adc.status == 0)
    tsadc_init();
  if(adc.status == 0){
    fprintf(stderr, "Aborted adc_acquire: unable to find TS-ADC16.\n");
    return -1;
  }
  
  /* set config for 0-5V and all channels up to channel_id */
  adccfg = 0x160 | ((unsigned char)channel_id << 1); 
  ADC(CFG_REG) = 1; /* start and stop to reset ADC16 */
  ADC(CFG_REG) = adccfg;
  pacing = 32000000/s_rate;
  if (pacing < 0x140) pacing = 0;
  ADC(DLY_LSB_REG) = pacing & 0xffff;
  ADC(DLY_MSB_REG) = pacing >> 16;
  ADC(CFG_REG) = adccfg;
  //fprintf(stderr, "n_samples=%d\n", n_samples);
  //fprintf(stderr, "pacing=0x%x\n", pacing);
  //fprintf(stderr, "adccfg=0x%x\n", ADC(CFG_REG));
  
  /* Start ADC machine: */
  ADC(CFG_REG) = adccfg | 1;
  
  /* Loop variables:
   * i1 = current index for data1
   * i2 = current index for data2
   * ffcount = number of samples known to be available in FIFO
   * chan = channel id (0 to 15) of next sample to be read
   */
  i1 = 0;
  i2 = 0;
  okay = 1;
  chan = 2 * channel_id + 1;
  while (((i1 < n_samples) || (i2 < n_samples)) && okay) {
    ffcount = FFCOUNT;
    if (ffcount > CUSHION) {
      while (ffcount > CUSHION) {
        /* take a sample: */
        prevchan = chan;
        chan = FFCHAN;
        if (chan == prevchan){ /* this should never happen */
          fprintf(stderr, "WARNING: at sample %d: two results in a row from channel %d.\n", i1, chan);
          result = ADC(FIFO_REG);
          if (chan & 1) i2--;
          else i1--;
          ffcount--;
        } else {
          result = ADC(FIFO_REG); 
          /* store results only if we are on the correct channel: */
          if ((((unsigned char)channel_id * 2) == chan) && (i1 < n_samples)) {
            data1[i1] = result;
            i1++;
          }
          if ((((unsigned char)channel_id * 2 + 1) == chan) && (i2 < n_samples)) {
            if(data2 != NULL) data2[i2] = result;
            i2++;
          }
          ffcount--;
        }
      }
    } else if (!(ADC(CFG_REG) & 1)) {
      ffcount = FFCOUNT; /* catering to a case that is almost impossible */
      if (ffcount == CUSHION) {
        //fprintf(stderr, "ADC machine stopped prematurely due to FIFO overflow.\n");
        okay = 0;
      }
    }
  }
  if (okay) {
    ADC(CFG_REG) |= ~1; /* stop ADC machine */
    //fprintf(stderr, "ADC machine stopped normally\n");
  }  
  if(data2 == NULL)
    return i1;
  else
    return i1 + i2;
}

int adc_sample(int max_chan, int s_rate, int n_discard, int n_accum, unsigned int *data) {

  int i, est_time, n_samples, collected, n_chan;
  unsigned int chan, pacing;
  unsigned short adccfg;

  if(adc.status == 0)
    tsadc_init();
  if(adc.status == 0){
    fprintf(stderr, "Aborted adc_sample: unable to find TS-ADC16.\n");
    return -1;
  }
  
  adccfg = 0x160 | ((unsigned char)max_chan << 1); 
  ADC(CFG_REG) = 1; /* start and stop to reset ADC16 */
  ADC(CFG_REG) = 0;
  pacing = 32000000/s_rate;
  if (pacing < 0x140) pacing = 0;
  ADC(DLY_LSB_REG) = pacing & 0xffff;
  ADC(DLY_MSB_REG) = pacing >> 16;
  ADC(CFG_REG) = adccfg;
  est_time = (pacing >> 5) * (n_discard + n_accum - 1) + 1;
  n_chan = 2 * (max_chan + 1);
  n_samples = (n_discard + n_accum) * n_chan;
  
  /* By assuming the FIFO will not be filled we simplify this function.
   * If paramters are assigned so that it needs more than 512 samples,
   * it fails here.
   */
  if (n_samples > 512) return -1;
  
  /* Start ADC machine: */
  ADC(CFG_REG) = adccfg | 1;
  usleep(est_time);
  
  /* Wait for sampling to be complete: */
  while(FFCOUNT <= n_samples) usleep(1);
  
  collected = 0;
  i = 0;
  while(collected < n_chan*(n_discard + n_accum)) {
    chan = FFCHAN;
    if(chan == i) { /* This check is here in case samples don't come out in order */
      /* The first n_discard samples per channel get overwritten: */
      if(collected < n_chan*(n_discard + 1)) 
        data[i] = ADC(FIFO_REG);
      else /* n_accum samples per channel get added up */
        data[i] += ADC(FIFO_REG);
      collected++;
      i++;
      if(i == n_chan) i = 0; /* i ranges from 0 to n_chan - 1 */
    } else { /* This should never happen: */
      ADC(FIFO_REG);
      fprintf(stderr, "Had to skip data on channel %d\n", i);
    }
  }
  ADC(CFG_REG) |= ~1; /* stop ADC machine */

  return 0;
}
