/***************************************************************************
 * sac2mseed.c
 *
 * Simple waveform data conversion from SAC timeseries to miniSEED.
 *
 * Only waveform data with NVHD = 6 is supported.  The later NVHD = 7,
 * spectral data or or generic X-Y data are not supported.
 *
 * Written by Chad Trabant, IRIS Data Management Center
 ***************************************************************************/

#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <libmseed.h>

#include "sacformat.h"

#define VERSION "1.13"
#define PACKAGE "sac2mseed"

#if defined(LWP_WIN)
#define strtoull _strtoui64
#endif

struct listnode
{
  char *key;
  char *data;
  struct listnode *next;
};

static void packtraces (flag flush);
static int sac2group (char *sacfile, MSTraceGroup *mstg);
static int parsesac (FILE *ifp, struct SACHeader *sh, float **data, int format,
                     int verbose, char *sacfile);
static int readbinaryheader (FILE *ifp, struct SACHeader *sh, int *format,
                             int *swapflag, int verbose, char *sacfile);
static int readbinarydata (FILE *ifp, float *data, int datacnt,
                           int swapflag, int verbose, char *sacfile);
static int readalphaheader (FILE *ifp, struct SACHeader *sh);
static int readalphadata (FILE *ifp, float *data, int datacnt);
static int swapsacheader (struct SACHeader *sh);
static int writemetadata (struct SACHeader *sh, char *network, char *station,
                          char *location, char *channel, hptime_t starttime,
                          int expanded);
static int parameter_proc (int argcount, char **argvec);
static char *getoptval (int argcount, char **argvec, int argopt);
static int readlistfile (char *listfile);
static void addnode (struct listnode **listroot, char *key, char *data);
static void record_handler (char *record, int reclen, void *handlerdata);
static void usage (void);

static int verbose               = 0;
static int packreclen            = -1;
static int encoding              = 11;
static int byteorder             = -1;
static int sacformat             = 0;
static int expmeta               = 0;
static char srateblkt            = 0;
static char *forcenet            = 0;
static char *forcesta            = 0;
static char *forceloc            = 0;
static char *forcechan           = 0;
static char *outputfile          = 0;
static FILE *ofp                 = 0;
static char *metafile            = 0;
static FILE *mfp                 = 0;
static long long int datascaling = 0;

/* A list of input files */
struct listnode *filelist = 0;

static MSTraceGroup *mstg = 0;

static int packedtraces      = 0;
static int64_t packedsamples = 0;
static int64_t packedrecords = 0;

int
main (int argc, char **argv)
{
  struct listnode *flp;

  /* Process given parameters (command line and parameter file) */
  if (parameter_proc (argc, argv) < 0)
    return -1;

  /* Init MSTraceGroup */
  mstg = mst_initgroup (mstg);

  /* Open the output file if specified */
  if (outputfile)
  {
    if (strcmp (outputfile, "-") == 0)
    {
      ofp = stdout;
    }
    else if ((ofp = fopen (outputfile, "wb")) == NULL)
    {
      fprintf (stderr, "Cannot open output file: %s (%s)\n",
               outputfile, strerror (errno));
      return -1;
    }
  }

  /* Open the metadata output file if specified */
  if (metafile)
  {
    if (strcmp (metafile, "-") == 0)
    {
      mfp = stdout;
    }
    else if ((mfp = fopen (metafile, "wb")) == NULL)
    {
      fprintf (stderr, "Cannot open metadata output file: %s (%s)\n",
               metafile, strerror (errno));
      return -1;
    }
  }

  /* Read input SAC files into MSTraceGroup */
  flp = filelist;
  while (flp != 0)
  {
    if (verbose)
      fprintf (stderr, "Reading %s\n", flp->data);

    sac2group (flp->data, mstg);

    flp = flp->next;
  }

  fprintf (stderr, "Packed %d trace(s) of %lld samples into %lld records\n",
           packedtraces, (long long int)packedsamples, (long long int)packedrecords);

  /* Make sure everything is cleaned up */
  if (ofp)
    fclose (ofp);

  if (mfp)
    fclose (mfp);

  return 0;
} /* End of main() */

/***************************************************************************
 * packtraces:
 *
 * Pack all traces in a group using per-MSTrace templates.
 *
 * Returns 0 on success, and -1 on failure
 ***************************************************************************/
static void
packtraces (flag flush)
{
  MSTrace *mst;
  int64_t trpackedsamples = 0;
  int64_t trpackedrecords = 0;

  mst = mstg->traces;
  while (mst)
  {

    if (mst->numsamples <= 0)
    {
      mst = mst->next;
      continue;
    }

    trpackedrecords = mst_pack (mst, &record_handler, 0, packreclen, encoding, byteorder,
                                &trpackedsamples, flush, verbose - 2, (MSRecord *)mst->prvtptr);

    if (trpackedrecords < 0)
    {
      fprintf (stderr, "Error packing data\n");
    }
    else
    {
      packedrecords += trpackedrecords;
      packedsamples += trpackedsamples;
    }

    mst = mst->next;
  }
} /* End of packtraces() */

/***************************************************************************
 * sac2group:
 * Read a SAC file and add data samples to a MSTraceGroup.  As the SAC
 * data is read in a MSRecord struct is used as a holder for the input
 * information.
 *
 * Returns 0 on success, and -1 on failure
 ***************************************************************************/
static int
sac2group (char *sacfile, MSTraceGroup *mstg)
{
  FILE *ifp     = 0;
  MSRecord *msr = 0;
  MSTrace *mst;
  struct blkt_1000_s Blkt1000;
  struct blkt_1001_s Blkt1001;
  struct blkt_100_s Blkt100;

  struct SACHeader sh;
  float *fdata   = 0;
  int32_t *idata = 0;
  int dataidx;
  int datacnt;
  long long int scaling = datascaling;

  /* Open input file */
  if ((ifp = fopen (sacfile, "rb")) == NULL)
  {
    fprintf (stderr, "Cannot open input file: %s (%s)\n",
             sacfile, strerror (errno));
    return -1;
  }

  /* Parse input SAC file into a header structure and data buffer */
  if ((datacnt = parsesac (ifp, &sh, &fdata, sacformat, verbose, sacfile)) < 0)
  {
    fprintf (stderr, "Error parsing %s\n", sacfile);

    return -1;
  }

  /* Open output file if needed */
  if (!ofp)
  {
    char mseedoutputfile[1024];
    int namelen;
    strncpy (mseedoutputfile, sacfile, sizeof (mseedoutputfile) - 6);
    namelen = strlen (sacfile);

    /* Truncate file name if .sac is at the end */
    if (namelen > 4)
      if ((*(mseedoutputfile + namelen - 1) == 'c' || *(mseedoutputfile + namelen - 1) == 'C') &&
          (*(mseedoutputfile + namelen - 2) == 'a' || *(mseedoutputfile + namelen - 2) == 'A') &&
          (*(mseedoutputfile + namelen - 3) == 's' || *(mseedoutputfile + namelen - 3) == 'S') &&
          (*(mseedoutputfile + namelen - 4) == '.'))

      {
        *(mseedoutputfile + namelen - 4) = '\0';
      }

    /* Add .mseed to the file name */
    strcat (mseedoutputfile, ".mseed");

    if ((ofp = fopen (mseedoutputfile, "wb")) == NULL)
    {
      fprintf (stderr, "Cannot open output file: %s (%s)\n",
               mseedoutputfile, strerror (errno));
      return -1;
    }
  }

  if (!(msr = msr_init (msr)))
  {
    fprintf (stderr, "Cannot initialize MSRecord strcture\n");
    return -1;
  }

  /* Determine autoscaling */
  if (scaling == 0 && encoding != 4)
  {
    float datamin, datamax;
    int fractional;
    long long int autoscale;

    /* Determine data sample minimum and maximum
       * Detect if scaling by 1 will result in truncation (fractional=1) */
    datamin = datamax = *fdata;
    fractional        = 0;
    for (dataidx = 1; dataidx < datacnt; dataidx++)
    {
      if (*(fdata + dataidx) < datamin)
        datamin = *(fdata + dataidx);
      if (*(fdata + dataidx) > datamax)
        datamax = *(fdata + dataidx);

      if (!fractional)
        if (*(fdata + dataidx) - (int)*(fdata + dataidx) > 0.000001)
          fractional = 1;
    }

    autoscale = 1;

    if (fractional)
    {
      for (autoscale = 1; abs ((int32_t)(datamax * autoscale)) < 100000; autoscale *= 10)
      {
      }

      if (abs ((int32_t)(datamin * autoscale)) < 10)
        fprintf (stderr, "WARNING Large sample value range (%g/%g), autoscaling might be a bad idea\n",
                 datamax, datamin);
    }

    scaling = autoscale;
  }

  /* Populate MSRecord structure with header details */
  if (strncmp (SUNDEF, sh.knetwk, 8))
    ms_strncpclean (msr->network, sh.knetwk, 2);
  if (strncmp (SUNDEF, sh.kstnm, 8))
    ms_strncpclean (msr->station, sh.kstnm, 5);
  if (strncmp (SUNDEF, sh.khole, 8))
    ms_strncpclean (msr->location, sh.khole, 2);
  if (strncmp (SUNDEF, sh.kcmpnm, 8))
    ms_strncpclean (msr->channel, sh.kcmpnm, 3);

  if (forcenet)
    ms_strncpclean (msr->network, forcenet, 2);

  if (forcesta)
    ms_strncpclean (msr->station, forcesta, 5);

  if (forceloc)
    ms_strncpclean (msr->location, forceloc, 2);

  if (forcechan)
  {
    int idx = 0;
    while (forcechan[idx] && idx < (sizeof (msr->channel) - 1))
    {
      if (forcechan[idx] != '.')
        msr->channel[idx] = forcechan[idx];
      idx++;
    }
    msr->channel[idx] = '\0';
  }

  msr->starttime = ms_time2hptime (sh.nzyear, sh.nzjday, sh.nzhour, sh.nzmin, sh.nzsec, sh.nzmsec * 1000);

  /* Adjust for Begin ('B' SAC variable) time offset */
  if (sh.b != FUNDEF)
    msr->starttime += (double)sh.b * HPTMODULUS;

  /* Calculate sample rate from interval(period) rounding to nearest 0.000001 Hz */
  msr->samprate = (double)((int)((1 / sh.delta) * 100000 + 0.5)) / 100000;

  msr->samplecnt = msr->numsamples = datacnt;

  /* Data sample type and sample array */
  if (encoding == 4)
  {
    msr->sampletype  = 'f';
    msr->datasamples = fdata;
  }
  else
  {
    /* Create an array of scaled integers */
    idata = (int32_t *)malloc (datacnt * sizeof (int32_t));

    if (verbose)
      fprintf (stderr, "[%s] Creating integer data scaled by: %lld\n", sacfile, scaling);

    for (dataidx = 0; dataidx < datacnt; dataidx++)
      *(idata + dataidx) = (int32_t)(*(fdata + dataidx) * scaling);

    msr->sampletype  = 'i';
    msr->datasamples = idata;
  }

  if (verbose >= 1)
  {
    fprintf (stderr, "[%s] %lld samps @ %.6f Hz for N: '%s', S: '%s', L: '%s', C: '%s'\n",
             sacfile, (long long int)msr->numsamples, msr->samprate,
             msr->network, msr->station, msr->location, msr->channel);
  }

  if (!(mst = mst_addmsrtogroup (mstg, msr, 0, -1.0, -1.0)))
  {
    fprintf (stderr, "[%s] Error adding samples to MSTraceGroup\n", sacfile);
  }

  /* Create an MSRecord template for the MSTrace by copying the current holder */
  if (!mst->prvtptr)
  {
    mst->prvtptr = msr_duplicate (msr, 0);

    if (!mst->prvtptr)
    {
      fprintf (stderr, "[%s] Error duplicate MSRecord for template\n", sacfile);
      return -1;
    }

    /* Add blockettes 1000 & 1001 to template */
    memset (&Blkt1000, 0, sizeof (struct blkt_1000_s));
    msr_addblockette ((MSRecord *)mst->prvtptr, (char *)&Blkt1000,
                      sizeof (struct blkt_1001_s), 1000, 0);
    memset (&Blkt1001, 0, sizeof (struct blkt_1001_s));
    msr_addblockette ((MSRecord *)mst->prvtptr, (char *)&Blkt1001,
                      sizeof (struct blkt_1001_s), 1001, 0);

    /* Add blockette 100 to template if requested */
    if (srateblkt)
    {
      memset (&Blkt100, 0, sizeof (struct blkt_100_s));
      Blkt100.samprate = (float)msr->samprate;
      msr_addblockette ((MSRecord *)mst->prvtptr, (char *)&Blkt100,
                        sizeof (struct blkt_100_s), 100, 0);
    }
  }

  packtraces (1);
  packedtraces += mstg->numtraces;

  /* Write metadata to file if requested */
  if (mfp)
  {
    if (verbose)
      fprintf (stderr, "[%s] Writing metadata to %s\n", sacfile, metafile);

    if (writemetadata (&sh, msr->network, msr->station, msr->location, msr->channel,
                       msr->starttime, expmeta))
    {
      fprintf (stderr, "Error writing metadata to file '%s'\n", metafile);

      return -1;
    }
  }

  /* Cleanup */
  fclose (ifp);

  if (ofp && !outputfile)
  {
    fclose (ofp);
    ofp = 0;
  }

  if (fdata)
    free (fdata);

  if (idata)
    free (idata);

  msr->datasamples = 0;

  if (msr)
    msr_free (&msr);

  return 0;
} /* End of sac2group() */

/***************************************************************************
 * parsesac:
 *
 * Parse a SAC file, autodetecting format dialect (ALPHA,
 * binary, big or little endian).  Results will be placed in the
 * supplied SAC header struct and data (float sample array in host
 * byte order).  The data array will be allocated by this routine and
 * must be free'd by the caller.  The data array will contain the
 * number of samples indicated in the SAC header (sh->npts).
 *
 * The format argument is interpreted as:
 * 0 : Unknown, detection needed
 * 1 : ALPHA
 * 2 : Binary, byte order detection needed
 * 3 : Binary, little endian
 * 4 : Binary, big endian
 *
 * Returns number of data samples in file or -1 on failure.
 ***************************************************************************/
static int
parsesac (FILE *ifp, struct SACHeader *sh, float **data, int format,
          int verbose, char *sacfile)
{
  char fourc[4];
  int swapflag = 0;
  int rv;

  /* Argument sanity */
  if (!ifp || !sh || !data)
    return -1;

  /* Read the first 4 characters */
  if (fread (&fourc, 4, 1, ifp) < 1)
    return -1;

  /* Determine if the file is ALPHA or binary SAC,
   * if the first 4 characters are spaces assume ALPHA SAC */
  if (format == 0)
  {
    if (fourc[0] == ' ' && fourc[1] == ' ' && fourc[2] == ' ' && fourc[3] == ' ')
      format = 1;
    else
      format = 2; /* Byte order detection will occur below */
  }

  /* Rewind the file position pointer to the beginning */
  rewind (ifp);

  /* Read the header */
  if (format == 1) /* Process SAC ALPHA header */
  {
    if ((rv = readalphaheader (ifp, sh)))
    {
      fprintf (stderr, "[%s] Error parsing SAC ALPHA header at line %d\n",
               sacfile, rv);
      return -1;
    }
  }
  else if (format >= 2 && format <= 4) /* Process SAC binary header */
  {
    if (readbinaryheader (ifp, sh, &format, &swapflag, verbose, sacfile))
    {
      fprintf (stderr, "[%s] Error parsing SAC header\n", sacfile);
      return -1;
    }
  }
  else
  {
    fprintf (stderr, "[%s] Unrecognized format value: %d\n", sacfile, format);
    return -1;
  }

  /* Fix up underspecified year values by adding 1900 */
  if (sh->nzyear >= 0 && sh->nzyear <= 200)
  {
    if (verbose)
      fprintf (stderr, "[%s] Adding 1900 to underspecified year value (%d)\n", sacfile, sh->nzyear);

    sh->nzyear += 1900;
  }

  /* Sanity check the start time */
  if (sh->nzyear < 1900 || sh->nzyear > 3000 ||
      sh->nzjday < 1 || sh->nzjday > 366 ||
      sh->nzhour < 0 || sh->nzhour > 23 ||
      sh->nzmin < 0 || sh->nzmin > 59 ||
      sh->nzsec < 0 || sh->nzsec > 60 ||
      sh->nzmsec < 0 || sh->nzmsec > 999999)
  {
    fprintf (stderr, "[%s] Unrecognized format (not SAC?)\n", sacfile);
    return -1;
  }

  if (verbose)
  {
    if (format == 1)
      fprintf (stderr, "[%s] Reading SAC ALPHA format\n", sacfile);
    if (format == 3)
      fprintf (stderr, "[%s] Reading SAC binary format (little-endian)\n", sacfile);
    if (format == 4)
      fprintf (stderr, "[%s] Reading SAC binary format (big-endian)\n", sacfile);
  }

  if (verbose > 2)
    fprintf (stderr, "[%s] SAC header version number: %d\n", sacfile, sh->nvhdr);

  if (sh->nvhdr != 6)
  {
    fprintf (stderr, "[%s] ERROR SAC header version (%d) not supported\n",
             sacfile, sh->nvhdr);
    return -1;
  }

  if (sh->npts <= 0)
  {
    fprintf (stderr, "[%s] No data, number of samples: %d\n", sacfile, sh->npts);
    return -1;
  }

  if (sh->iftype != ITIME)
  {
    fprintf (stderr, "[%s] Data is not time series (IFTYPE=%d), cannot convert other types\n",
             sacfile, sh->iftype);
    return -1;
  }

  if (!sh->leven)
  {
    fprintf (stderr, "[%s] Data is not evenly spaced (LEVEN not true), cannot convert\n", sacfile);
    return -1;
  }

  /* Allocate space for data samples */
  *data = (float *)malloc (sizeof (float) * sh->npts);
  memset (*data, 0, (sizeof (float) * sh->npts));

  /* Read the data samples */
  if (format == 1) /* Process SAC ALPHA data */
  {
    if ((rv = readalphadata (ifp, *data, sh->npts)))
    {
      fprintf (stderr, "[%s] Error parsing SAC ALPHA data at line %d\n",
               sacfile, rv);
      return -1;
    }
  }
  else if (format >= 2 && format <= 4) /* Process SAC binary data */
  {
    if (readbinarydata (ifp, *data, sh->npts, swapflag, verbose, sacfile))
    {
      fprintf (stderr, "[%s] Error reading SAC data samples\n", sacfile);
      return -1;
    }
  }
  else
  {
    fprintf (stderr, "[%s] Unrecognized format value: %d\n", sacfile, format);
    return -1;
  }

  return sh->npts;
} /* End of parsesac() */

/***************************************************************************
 * readbinaryheader:
 *
 * Read a binary header from a file and parse into a SAC header
 * struct.  Also determines byte order and sets the swap flag unless
 * already dictated by the format.
 *
 * Returns 0 on sucess or -1 on failure.
 ***************************************************************************/
static int
readbinaryheader (FILE *ifp, struct SACHeader *sh, int *format,
                  int *swapflag, int verbose, char *sacfile)
{
  int bigendianhost;
  int32_t hdrver;

  /* Read the binary header into memory */
  if (fread (sh, sizeof (struct SACHeader), 1, ifp) != 1)
  {
    fprintf (stderr, "[%s] Could not read SAC header from file\n", sacfile);

    if (ferror (ifp))
      fprintf (stderr, "[%s] Error reading from file\n", sacfile);

    return -1;
  }

  /* Determine if host is big-endian */
  bigendianhost = ms_bigendianhost ();

  *swapflag = 0;

  /* Test byte order using the header version if unknown */
  /* Also set the swapflag appropriately */
  if (*format == 2)
  {
    memcpy (&hdrver, &sh->nvhdr, 4);
    if (hdrver < 1 || hdrver > 10)
    {
      ms_gswap4 (&hdrver);
      if (hdrver < 1 || hdrver > 10)
      {
        fprintf (stderr, "[%s] Cannot determine byte order (not SAC?)\n", sacfile);
        return -1;
      }

      *format   = (bigendianhost) ? 3 : 4;
      *swapflag = 1;
    }
    else
    {
      *format = (bigendianhost) ? 4 : 3;
    }
  }
  else if (*format == 3 && bigendianhost)
    *swapflag = 1;
  else if (*format == 4 && !bigendianhost)
    *swapflag = 1;

  if (verbose > 1)
  {
    if (*swapflag)
      fprintf (stderr, "[%s] Byte swapping required\n", sacfile);
    else
      fprintf (stderr, "[%s] Byte swapping NOT required\n", sacfile);
  }

  /* Byte swap all values in header */
  if (*swapflag)
    swapsacheader (sh);

  return 0;
} /* End of readbinaryheader() */

/***************************************************************************
 * readbinarydata:
 *
 * Read binary data from a file and add to an array, the array
 * must already be allocated with datacnt floats.
 *
 * Returns 0 on sucess or -1 on failure.
 ***************************************************************************/
static int
readbinarydata (FILE *ifp, float *data, int datacnt, int swapflag,
                int verbose, char *sacfile)
{
  int samplesread = 0;
  int dataidx;

  /* Read in data samples */
  if ((samplesread = fread (data, sizeof (float), datacnt, ifp)) != datacnt)
  {
    fprintf (stderr, "[%s] Only read %d of %d expected data samples\n",
             sacfile, samplesread, datacnt);
    return -1;
  }

  /* Swap data samples */
  if (swapflag)
  {
    for (dataidx = 0; dataidx < datacnt; dataidx++)
    {
      ms_gswap4 (data + dataidx);
    }
  }

  return 0;
} /* End of readbinarydata() */

/***************************************************************************
 * readalphaheader:
 *
 * Read a alphanumeric header from a file and parse into a SAC header
 * struct.
 *
 * Returns 0 on sucess or a positive number indicating line number of
 * parsing failure.
 ***************************************************************************/
static int
readalphaheader (FILE *ifp, struct SACHeader *sh)
{
  char line[1025];
  int linecnt = 1; /* The header starts at line 1 */
  int lineidx;
  int count;
  int hvidx = 0;
  char *cp;

  if (!ifp || !sh)
    return -1;

  /* The first 14 lines x 5 values are floats */
  for (lineidx = 0; lineidx < 14; lineidx++)
  {
    if (!fgets (line, sizeof (line), ifp))
      return linecnt;

    count = sscanf (line, " %f %f %f %f %f ", (float *)sh + hvidx,
                    (float *)sh + hvidx + 1, (float *)sh + hvidx + 2,
                    (float *)sh + hvidx + 3, (float *)sh + hvidx + 4);

    if (count != 5)
      return linecnt;

    hvidx += 5;
    linecnt++;
  }

  /* The next 8 lines x 5 values are integers */
  for (lineidx = 0; lineidx < 8; lineidx++)
  {
    if (!fgets (line, sizeof (line), ifp))
      return linecnt;

    count = sscanf (line, " %d %d %d %d %d ", (int32_t *)sh + hvidx,
                    (int32_t *)sh + hvidx + 1, (int32_t *)sh + hvidx + 2,
                    (int32_t *)sh + hvidx + 3, (int32_t *)sh + hvidx + 4);

    if (count != 5)
      return linecnt;

    hvidx += 5;
    linecnt++;
  }

  /* Set pointer to start of string variables */
  cp = (char *)sh + (hvidx * 4);

  /* The next 8 lines each contain 24 bytes of string data */
  for (lineidx = 0; lineidx < 8; lineidx++)
  {
    memset (line, 0, sizeof (line));
    if (!fgets (line, sizeof (line), ifp))
      return linecnt;

    memcpy (cp, line, 24);
    cp += 24;

    linecnt++;
  }

  /* Make sure each of the 23 string variables are left justified */
  cp = (char *)sh + (hvidx * 4);
  for (count = 0; count < 24; count++)
  {
    int ridx, widx, width;
    char *fcp;

    /* Each string variable is 8 characters with one exception */
    if (count != 1)
    {
      width = 8;
    }
    else
    {
      width = 16;
      count++;
    }

    /* Pointer to field */
    fcp = cp + (count * 8);

    /* Find first character that is not a space */
    ridx = 0;
    while (*(fcp + ridx) == ' ')
      ridx++;

    /* Remove any leading spaces */
    if (ridx > 0)
    {
      for (widx = 0; widx < width; widx++, ridx++)
      {
        if (ridx < width)
          *(fcp + widx) = *(fcp + ridx);
        else
          *(fcp + widx) = ' ';
      }
    }
  }

  return 0;
} /* End of readalphaheader() */

/***************************************************************************
 * readalphadata:
 *
 * Read a alphanumeric data from a file and add to an array, the array
 * must already be allocated with datacnt floats.
 *
 * Returns 0 on sucess or a positive number indicating line number of
 * parsing failure.
 ***************************************************************************/
static int
readalphadata (FILE *ifp, float *data, int datacnt)
{
  char line[1025];
  int linecnt     = 31; /* Data samples start on line 31 */
  int samplesread = 0;
  int count;
  int dataidx = 0;

  if (!ifp || !data || !datacnt)
    return -1;

  /* Each data line should contain 5 floats unless the last */
  for (;;)
  {
    if (!fgets (line, sizeof (line), ifp))
      return linecnt;

    count = sscanf (line, " %f %f %f %f %f ", (float *)data + dataidx,
                    (float *)data + dataidx + 1, (float *)data + dataidx + 2,
                    (float *)data + dataidx + 3, (float *)data + dataidx + 4);

    samplesread += count;

    if (samplesread >= datacnt)
      break;
    else if (count != 5)
      return linecnt;

    dataidx += 5;
    linecnt++;
  }

  return 0;
} /* End of readalphadata() */

/***************************************************************************
 * swapsacheader:
 *
 * Byte swap all multi-byte quantities (floats and ints) in SAC header
 * struct.
 *
 * Returns 0 on sucess and -1 on failure.
 ***************************************************************************/
static int
swapsacheader (struct SACHeader *sh)
{
  int32_t *ip;
  int idx;

  if (!sh)
    return -1;

  for (idx = 0; idx < (NUMFLOATHDR + NUMINTHDR); idx++)
  {
    ip = (int32_t *)sh + idx;
    ms_gswap4 (ip);
  }

  return 0;
} /* End of swapsacheader() */

/***************************************************************************
 * writemetadata:
 *
 * Write a single line of metadata into the metadata output file
 * containing the following fields comma-separated in this order:
 *
 *   Network (supplied, originally knetwk)
 *   Station (supplied, originally kstnm)
 *   Location (supplied, originally khole)
 *   Channel (supplied, originally kcmpnm)
 *   Latitude (stla)
 *   Longitude (stlo)
 *   Elevation (stel) [not currently used by SAC]
 *   Depth (stdp) [not currently used by SAC]
 *   Component Azimuth (cmpaz), degrees clockwise from north
 *   Component Incident Angle (cmpinc), degrees from vertical
 *   Instrument Name (kinst)
 *   Scale Factor (scale)
 *   Scale Frequency, unknown, will be empty
 *   Scale Units, unknown, will be empty
 *   Sampling rate, in samples per second (1/delta)
 *   Start time, set to start time
 *   End time, set to end time
 *
 * If the expanded argument is true the following fields will also be
 * included:
 *
 *   Event Name (kevnm)
 *   User string 0 (kuser0)
 *   User string 1 (kuser1)
 *   User string 2 (kuser2)
 *
 * Returns 0 on sucess and -1 on failure.
 ***************************************************************************/
static int
writemetadata (struct SACHeader *sh, char *network, char *station,
               char *location, char *channel, hptime_t starttime, int expanded)
{
  static flag wroteheader = 0;
  hptime_t endtime;
  char string[50];
  char *cp;

  if (!sh || !mfp)
    return -1;

  if (!wroteheader)
  {
    wroteheader = 1;

    if (!fprintf (mfp, "#Net,Sta,Loc,Chan,Lat,Lon,Elev,Depth,Az,Inc,Inst,Scale,ScaleFreq,ScaleUnits,SampleRate,Start,End"))
    {
      fprintf (stderr, "Error writing to metadata output file\n");
      return -1;
    }

    if (expanded)
      if (!fprintf (mfp, ",Event,String0,String1,String2"))
      {
        fprintf (stderr, "Error writing to metadata output file\n");
        return -1;
      }

    fprintf (mfp, "\n");
  }

  /* LINE: Net,Sta,Loc,Chan,Lat,Lon,Elev,Dep,Az,Inc,Inst,Scale,ScaleFreq,ScaleUnits,SampleRate,Start,End[,Event,String0,String1,String2] */

  /* Write the source parameters */
  if (!fprintf (mfp, "%s,%s,%s,%s,", network, station, location, channel))
  {
    fprintf (stderr, "Error writing to metadata output file\n");
    return -1;
  }

  /* Write lat, lon, elev, depth, azimuth, incident, instrument, scale */
  if (sh->stla != FUNDEF)
    fprintf (mfp, "%.5f,", sh->stla);
  else
    fprintf (mfp, ",");
  if (sh->stlo != FUNDEF)
    fprintf (mfp, "%.5f,", sh->stlo);
  else
    fprintf (mfp, ",");
  if (sh->stel != FUNDEF)
    fprintf (mfp, "%g,", sh->stel);
  else
    fprintf (mfp, ",");
  if (sh->stdp != FUNDEF)
    fprintf (mfp, "%g,", sh->stdp);
  else
    fprintf (mfp, ",");
  if (sh->cmpaz != FUNDEF)
    fprintf (mfp, "%g,", sh->cmpaz);
  else
    fprintf (mfp, ",");
  if (sh->cmpinc != FUNDEF)
    fprintf (mfp, "%g,", sh->cmpinc);
  else
    fprintf (mfp, ",");
  if (strncmp (SUNDEF, sh->kinst, 6))
  {
    memcpy (string, sh->kinst, 8);
    string[8] = '\0';
    for (cp = &string[7]; cp >= string && *cp == ' '; cp--)
    {
      *cp = '\0';
    }
    fprintf (mfp, "%s,", string);
  }
  else
    fprintf (mfp, ",");
  if (sh->scale != FUNDEF)
    fprintf (mfp, "%.g,", sh->scale);
  else
    fprintf (mfp, ",");

  /* Add blanks for ScaleFreq and ScaleUnits which we do not know */
  fprintf (mfp, ",,");

  /* Add sampling rate */
  if (sh->delta != FUNDEF)
    fprintf (mfp, "%g,", 1.0 / sh->delta);
  else
    fprintf (mfp, ",");

  /* Add start time */
  ms_hptime2isotimestr (starttime, string, 0);
  fprintf (mfp, "%s,", string);

  /* Calculate and add end time */
  endtime = starttime + (((sh->npts - 1) * sh->delta) * HPTMODULUS);
  ms_hptime2isotimestr (endtime, string, 0);
  fprintf (mfp, "%s", string);

  /* Add expanded set of fields if requested */
  if (expanded)
  {
    fprintf (mfp, ",");
    if (strncmp (SUNDEF, sh->kevnm, 6))
    {
      memcpy (string, sh->kevnm, 16);
      string[16] = '\0';
      for (cp = &string[15]; cp >= string && *cp == ' '; cp--)
      {
        *cp = '\0';
      }
      fprintf (mfp, "%s,", string);
    }
    else
      fprintf (mfp, ",");
    if (strncmp (SUNDEF, sh->kuser0, 6))
    {
      memcpy (string, sh->kuser0, 8);
      string[8] = '\0';
      for (cp = &string[7]; cp >= string && *cp == ' '; cp--)
      {
        *cp = '\0';
      }
      fprintf (mfp, "%s,", string);
    }
    else
      fprintf (mfp, ",");
    if (strncmp (SUNDEF, sh->kuser1, 6))
    {
      memcpy (string, sh->kuser1, 8);
      string[8] = '\0';
      for (cp = &string[7]; cp >= string && *cp == ' '; cp--)
      {
        *cp = '\0';
      }
      fprintf (mfp, "%s,", string);
    }
    else
      fprintf (mfp, ",");
    if (strncmp (SUNDEF, sh->kuser2, 6))
    {
      memcpy (string, sh->kuser2, 8);
      string[8] = '\0';
      for (cp = &string[7]; cp >= string && *cp == ' '; cp--)
      {
        *cp = '\0';
      }
      fprintf (mfp, "%s", string);
    }
  }

  fprintf (mfp, "\n");

  return 0;
} /* End of writemetadata() */

/***************************************************************************
 * parameter_proc:
 * Process the command line parameters.
 *
 * Returns 0 on success, and -1 on failure.
 ***************************************************************************/
static int
parameter_proc (int argcount, char **argvec)
{
  int optind;

  /* Process all command line arguments */
  for (optind = 1; optind < argcount; optind++)
  {
    if (strcmp (argvec[optind], "-V") == 0)
    {
      fprintf (stderr, "%s version: %s\n", PACKAGE, VERSION);
      exit (0);
    }
    else if (strcmp (argvec[optind], "-h") == 0)
    {
      usage ();
      exit (0);
    }
    else if (strncmp (argvec[optind], "-v", 2) == 0)
    {
      verbose += strspn (&argvec[optind][1], "v");
    }
    else if (strcmp (argvec[optind], "-S") == 0)
    {
      srateblkt = 1;
    }
    else if (strcmp (argvec[optind], "-n") == 0)
    {
      forcenet = getoptval (argcount, argvec, optind++);
    }
    else if (strcmp (argvec[optind], "-t") == 0)
    {
      forcesta = getoptval (argcount, argvec, optind++);
    }
    else if (strcmp (argvec[optind], "-l") == 0)
    {
      forceloc = getoptval (argcount, argvec, optind++);
    }
    else if (strcmp (argvec[optind], "-c") == 0)
    {
      forcechan = getoptval (argcount, argvec, optind++);
    }
    else if (strcmp (argvec[optind], "-r") == 0)
    {
      packreclen = strtoul (getoptval (argcount, argvec, optind++), NULL, 10);
    }
    else if (strcmp (argvec[optind], "-e") == 0)
    {
      encoding = strtoul (getoptval (argcount, argvec, optind++), NULL, 10);
    }
    else if (strcmp (argvec[optind], "-b") == 0)
    {
      byteorder = strtoul (getoptval (argcount, argvec, optind++), NULL, 10);
    }
    else if (strcmp (argvec[optind], "-o") == 0)
    {
      outputfile = getoptval (argcount, argvec, optind++);
    }
    else if (strcmp (argvec[optind], "-m") == 0)
    {
      metafile = getoptval (argcount, argvec, optind++);
    }
    else if (strcmp (argvec[optind], "-me") == 0)
    {
      expmeta = 1;
    }
    else if (strcmp (argvec[optind], "-s") == 0)
    {
      datascaling = strtoull (getoptval (argcount, argvec, optind++), NULL, 10);
    }
    else if (strcmp (argvec[optind], "-f") == 0)
    {
      sacformat = strtoul (getoptval (argcount, argvec, optind++), NULL, 10);
    }
    else if (strncmp (argvec[optind], "-", 1) == 0 &&
             strlen (argvec[optind]) > 1)
    {
      fprintf (stderr, "Unknown option: %s\n", argvec[optind]);
      exit (1);
    }
    else
    {
      addnode (&filelist, NULL, argvec[optind]);
    }
  }

  /* Make sure an input files were specified */
  if (filelist == 0)
  {
    fprintf (stderr, "No input files were specified\n\n");
    fprintf (stderr, "%s version %s\n\n", PACKAGE, VERSION);
    fprintf (stderr, "Try %s -h for usage\n", PACKAGE);
    exit (1);
  }

  /* Report the program version */
  if (verbose)
    fprintf (stderr, "%s version: %s\n", PACKAGE, VERSION);

  /* Check the input files for any list files, if any are found
   * remove them from the list and add the contained list */
  if (filelist)
  {
    struct listnode *prevln, *ln;
    char *lfname;

    prevln = ln = filelist;
    while (ln != 0)
    {
      lfname = ln->data;

      if (*lfname == '@')
      {
        /* Remove this node from the list */
        if (ln == filelist)
          filelist = ln->next;
        else
          prevln->next = ln->next;

        /* Skip the '@' first character */
        if (*lfname == '@')
          lfname++;

        /* Read list file */
        readlistfile (lfname);

        /* Free memory for this node */
        if (ln->key)
          free (ln->key);
        free (ln->data);
        free (ln);
      }
      else
      {
        prevln = ln;
      }

      ln = ln->next;
    }
  }

  return 0;
} /* End of parameter_proc() */

/***************************************************************************
 * getoptval:
 *
 * Return the value to a command line option; checking that the value is
 * itself not an option (starting with '-') and is not past the end of
 * the argument list.
 *
 * argcount: total arguments in argvec
 * argvec: argument list
 * argopt: index of option to process, value is expected to be at argopt+1
 *
 * Returns value on success and exits with error message on failure
 ***************************************************************************/
static char *
getoptval (int argcount, char **argvec, int argopt)
{
  if (argvec == NULL || argvec[argopt] == NULL)
  {
    fprintf (stderr, "getoptval(): NULL option requested\n");
    exit (1);
    return 0;
  }

  /* Special case of '-o -' usage */
  if ((argopt + 1) < argcount && strcmp (argvec[argopt], "-o") == 0)
    if (strcmp (argvec[argopt + 1], "-") == 0)
      return argvec[argopt + 1];

  if ((argopt + 1) < argcount && *argvec[argopt + 1] != '-')
    return argvec[argopt + 1];

  fprintf (stderr, "Option %s requires a value\n", argvec[argopt]);
  exit (1);
  return 0;
} /* End of getoptval() */

/***************************************************************************
 * readlistfile:
 *
 * Read a list of files from a file and add them to the filelist for
 * input data.  The filename is expected to be the last
 * space-separated field on the line.
 *
 * Returns the number of file names parsed from the list or -1 on error.
 ***************************************************************************/
static int
readlistfile (char *listfile)
{
  FILE *fp;
  char line[1024];
  char *ptr;
  int filecnt = 0;

  char filename[1024];
  char *lastfield = 0;
  int fields      = 0;
  int wspace;

  /* Open the list file */
  if ((fp = fopen (listfile, "rb")) == NULL)
  {
    if (errno == ENOENT)
    {
      fprintf (stderr, "Could not find list file %s\n", listfile);
      return -1;
    }
    else
    {
      fprintf (stderr, "Error opening list file %s: %s\n",
               listfile, strerror (errno));
      return -1;
    }
  }

  if (verbose)
    fprintf (stderr, "Reading list of input files from %s\n", listfile);

  while ((fgets (line, sizeof (line), fp)) != NULL)
  {
    /* Truncate line at first \r or \n, count space-separated fields
       * and track last field */
    fields = 0;
    wspace = 0;
    ptr    = line;
    while (*ptr)
    {
      if (*ptr == '\r' || *ptr == '\n' || *ptr == '\0')
      {
        *ptr = '\0';
        break;
      }
      else if (*ptr != ' ')
      {
        if (wspace || ptr == line)
        {
          fields++;
          lastfield = ptr;
        }
        wspace = 0;
      }
      else
      {
        wspace = 1;
      }

      ptr++;
    }

    /* Skip empty lines */
    if (!lastfield)
      continue;

    if (fields >= 1 && fields <= 3)
    {
      fields = sscanf (lastfield, "%s", filename);

      if (fields != 1)
      {
        fprintf (stderr, "Error parsing file name from: %s\n", line);
        continue;
      }

      if (verbose > 1)
        fprintf (stderr, "Adding '%s' to input file list\n", filename);

      addnode (&filelist, NULL, filename);
      filecnt++;

      continue;
    }
  }

  fclose (fp);

  return filecnt;
} /* End readlistfile() */

/***************************************************************************
 * addnode:
 *
 * Add node to the specified list.
 ***************************************************************************/
static void
addnode (struct listnode **listroot, char *key, char *data)
{
  struct listnode *lastlp, *newlp;

  if (data == NULL)
  {
    fprintf (stderr, "addnode(): No file name specified\n");
    return;
  }

  lastlp = *listroot;
  while (lastlp != 0)
  {
    if (lastlp->next == 0)
      break;

    lastlp = lastlp->next;
  }

  newlp = (struct listnode *)malloc (sizeof (struct listnode));
  memset (newlp, 0, sizeof (struct listnode));
  if (key)
    newlp->key = strdup (key);
  else
    newlp->key = key;
  if (data)
    newlp->data = strdup (data);
  else
    newlp->data = data;
  newlp->next = 0;

  if (lastlp == 0)
    *listroot = newlp;
  else
    lastlp->next = newlp;

} /* End of addnode() */

/***************************************************************************
 * record_handler:
 * Saves passed records to the output file.
 ***************************************************************************/
static void
record_handler (char *record, int reclen, void *handlerdata)
{
  if (fwrite (record, reclen, 1, ofp) != 1)
  {
    fprintf (stderr, "Error writing to output file\n");
  }
} /* End of record_handler() */

/***************************************************************************
 * usage:
 * Print the usage message and exit.
 ***************************************************************************/
static void
usage (void)
{
  fprintf (stderr, "%s version: %s\n\n", PACKAGE, VERSION);
  fprintf (stderr, "Convert SAC waveform data to Mini-SEED.\n\n");
  fprintf (stderr, "Usage: %s [options] file1 [file2 file3 ...]\n\n", PACKAGE);
  fprintf (stderr,
           " ## Options ##\n"
           " -V             Report program version\n"
           " -h             Show this usage message\n"
           " -v             Be more verbose, multiple flags can be used\n"
           " -S             Include SEED blockette 100 for very irrational sample rates\n"
           " -n netcode     Specify the SEED network code, default is KNETWK header value\n"
           " -t stacode     Specify the SEED station code, default is KSTNM header value\n"
           " -l locid       Specify the SEED location ID, default is KHOLE header value\n"
           " -c chancodes   Specify the SEED channel codes, default is KCMPNM header value\n"
           " -r bytes       Specify record length in bytes for packing, default: 4096\n"
           " -e encoding    Specify SEED encoding format for packing, default: 11 (Steim2)\n"
           " -b byteorder   Specify byte order for packing, MSBF: 1 (default), LSBF: 0\n"
           " -o outfile     Specify the output file, default is <inputfile>.mseed\n"
           " -m metafile    Specify the metadata output file\n"
           " -me            Write additional fields into the metadata output\n"
           " -s factor      Specify scaling factor for sample values, default is autoscale\n"
           " -f format      Specify input SAC file format (default is autodetect):\n"
           "                  0=autodetect, 1=alpha, 2=binary (detect byte order),\n"
           "                  3=binary (little-endian), 4=binary (big-endian)\n"
           "\n"
           " file(s)        File(s) of SAC input data\n"
           "                  If a file is prefixed with an '@' it is assumed to contain\n"
           "                  a list of data files to be read\n"
           "\n"
           "Supported Mini-SEED encoding formats:\n"
           " 3  : 32-bit integers, scaled\n"
           " 4  : 32-bit floats (C float)\n"
           " 10 : Steim 1 compression of scaled 32-bit integers\n"
           " 11 : Steim 2 compression of scaled 32-bit integers\n"
           "\n"
           "For any of the non-floating point encoding formats the data samples\n"
           "will be scaled either by the specified scaling factor or autoscaling\n"
           "where the magnitude of the maximum sample will be 6 digits.\n"
           "\n");
} /* End of usage() */
