#define _LARGEFILE_SOURCE
#define _FILE_OFFSET_BITS 64
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <unistd.h>
#include <sys/types.h>

/** Echo digital video frames to stdout
 * @param smil		SMIL file name (for diagnostics)
 * @param lineno	SMIL file line number
 * @param dvname	digital video file name
 * @param begin		first frame to display
 * @param end		last frame to display
 * @return		zero on success, nonzero on error
 */
static int
dvread (const char* smil, unsigned lineno,
	const char* dvname, unsigned begin, unsigned end)
{
  FILE* f;
  static char frame[144000];
  unsigned framelen = 144000;
  /** current frame */
  unsigned curr = 0;
  if (end < begin) {
    fprintf (stderr, "%s:%u: src=\"%s\", clipBegin>clipEnd: %u>%u\n",
	     smil, lineno, dvname, begin, end);
    return 1;
  }
  f = fopen (dvname, "rbm");
  if (!f) {
    fprintf (stderr, "%s:%u: cannot open %s: %m\n", smil, lineno, dvname);
    return 2;
  }
  if (4 != fread (frame, 1, 4, f)) {
  freadError:
    if (feof (f)) {
      fprintf (stderr, "%s:%u: unexpected end of file %s at %u frames\n",
	       smil, lineno, dvname, curr);
      return 0;
    }
    else
      fprintf (stderr, "%s:%u: reading frame %u from %s: %m\n",
	       smil, lineno, curr, dvname);
    fclose (f);
    return 2;
  }
  if (memcmp (frame, "\037\7", 3)) {
    fprintf (stderr, "%s:%u: file %s is not in DIF format\n",
	     smil, lineno, dvname);
    fclose (f);
    return 3;
  }

  /*
   * Determine the number of bytes per frame,
   * assuming that all frames in the file are in the same format.
   */
  if (!(frame[3] & 0x80))
    framelen = 120000; /* NTSC format */

  /* skip the first frames by seeking, fall back to reading */
  if (!fseeko (f, (off_t) framelen * begin, SEEK_SET))
    curr = begin;
  else {
    if (framelen - 4 != fread (frame + 4, 1, framelen - 4, f))
      goto freadError;
    while (++curr <= begin)
      if (framelen != fread (frame, 1, framelen, f))
	goto freadError;
    goto writeFrame;
  }

  /* copy frames until end of file */
  while (curr <= end) {
    if (framelen != fread (frame, 1, framelen, f))
      goto freadError;
    curr++;
  writeFrame:
    if (framelen != write (STDOUT_FILENO, frame, framelen)) {
      fprintf (stderr, "%s:%u: writing frame %u of %s: %m\n",
	       smil, lineno, curr, dvname);
      fclose (f);
      return 4;
    }
  }
  fclose (f);
  return 0;
}

/** The main program.
 * Read the SMIL files specified as command line arguments and output
 * the referred DV streams on standard output.
 * @param argc	number of command line arguments
 * @param argv	the command line arguments
 */
int
main (int argc, char** argv)
{
  char** arg;
  /** string buffer */
  char* buf = malloc (16);
  /** allocated length of buf */
  unsigned buflen = 16;
  
  for (arg = argv; ++arg != argv + argc; ) {
    FILE* f = fopen (*arg, "rt");
    if (!f) {
      fprintf (stderr, "%s: fopen (\"%s\"): %m\n", *argv, *arg);
      free (buf);
      return 1;
    }
    else {
      /** current line number */
      unsigned lineno = 1;
      /** lexer state */
      enum { Normal, Tag, Attr, Equals, Quote } state = Normal;
      /** string being captured (same order as attrs[] below) */
      enum { sSrc, sBegin, sEnd, sNone } capture = sNone;
      /** video source file (pointer to last character, or 0 if not reading) */
      char* src = 0;
      /** begin frame */
      unsigned begin = UINT_MAX;
      /** end frame */
      unsigned end = UINT_MAX;
      /** the tag to look for */
      static const char* const tag = "video";
      /** next expected character of the magic tag (0=no match) */
      const char* tnext = 0;
      /** the attributes to look for */
      static const char* const attrs[] = {
	"src", "clipBegin", "clipEnd", 0
      };
      /** next expected characters of the attributes */
      const char* anext[sizeof (attrs) / sizeof *attrs - 1];

      while (f) {
	int c = getc (f);
	switch (c) {
	case EOF:
	  if (state)
	    fprintf (stderr, "%s:%u: unexpected end of file\n",
		     *arg, lineno);
	  fclose (f), f = 0;
	  continue;
	  /* to do: process XML entities &lt; &gt;, &amp;, &quot;, etc.
	case '&': ...; break;
	case ';': ...; break;
	  */
	case '=':
	  switch (state) {
	  case Normal:
	    break;
	  case Quote:
	    goto append;
	  case Tag: case Equals:
	    fprintf (stderr, "%s:%u: unexpected '='\n",
		     *arg, lineno);
	    tnext = 0;
	    /* fall through */
	  case Attr:
	    state = Equals;
	    break;
	  }
	  break;
	case '"':
	  capture = sNone;
	  switch (state) {
	  case Normal:
	    break;
	  case Tag:
	  case Attr:
	    fprintf (stderr, "%s:%u: unexpected '\"'\n",
		     *arg, lineno);
	    state = Quote;
	    tnext = 0;
	    src = 0;
	    break;
	  case Equals:
	    if (tnext && !*tnext) {
	      unsigned a;
	      for (a = 0; attrs[a]; a++)
		if (anext[a] && !*anext[a])
		  capture = a;
	      switch (capture) {
	      case sNone: break;
	      case sSrc: src = 0; break;
	      case sBegin: begin = UINT_MAX; break;
	      case sEnd: end = UINT_MAX; break;
	      }
	    }
	    state = Quote;
	    break;
	  case Quote:
	    state = Attr;
	    memcpy (anext, attrs, sizeof anext);
	    break;
	  }
	  break;
	case '<':
	  if (state != Normal)
	    fprintf (stderr, "%s:%u: '<' without '>'\n",
		     *arg, lineno);
	  state = Tag;
	  tnext = tag;
	  break;
	case '>':
	  if (state == Quote)
	    fprintf (stderr, "%s:%u: unterminated '\"' before '>'\n",
		     *arg, lineno);
	  else if (src && begin != UINT_MAX && end != UINT_MAX) {
	    *src = 0;
	    if (dvread (*arg, lineno, buf, begin, end)) {
	      free (buf);
	      fclose (f);
	      return 2;
	    }
	  }
	  state = Normal;
	  tnext = 0;
	  src = 0;
	  begin = UINT_MAX;
	  end = UINT_MAX;
	  break;
	case '\n':
	  lineno++;
	case '\v': case '\f': case '\t': case ' ':
	  switch (state) {
	  case Normal: case Attr: case Equals:
	    break;
	  case Quote:
	    goto append;
	  case Tag:
	    if (tnext != tag) {
	      /* got a non-whitespace character after '<' */
	      state = Attr;
	      memcpy (anext, attrs, sizeof anext);
	    }
	  }
	  break;
	case '\0':
	  fprintf (stderr, "%s: %u: ignoring NUL character\n",
		   *arg, lineno);
	  break;
	default:
	  switch (state) {
	  case Tag:
	    if (tnext && (char) (unsigned char) c != *tnext++)
	      tnext = 0;
	    break;
	  case Attr:
	    if (tnext && !*tnext) {
	      unsigned a;
	      for (a = 0; attrs[a]; a++)
		if (anext[a] && (char) (unsigned char) c != *anext[a]++)
		  anext[a] = 0;
	    }
	    break;
	  case Normal:
	  case Equals:
	    break;
	  case Quote:
	  append:
	    switch (capture) {
	    case sNone:
	      break;
	    case sSrc:
	      if (!src)
		src = buf;
	      *src++ = c;
	      if (src == buf + buflen) {
		buf = realloc (buf, buflen << 1);
		src = buf + buflen;
		buflen <<= 1;
	      }
	      break;
	    case sBegin:
	      if (c < '0' || c > '9')
		fprintf (stderr, "%s: %u: ignoring '%c' in clipBegin\n",
			 *arg, lineno, (unsigned char) c);
	      else if (begin == UINT_MAX)
		begin = c - '0';
	      else
		begin *= 10, begin += c - '0';
	      break;
	    case sEnd:
	      if (c < '0' || c > '9')
		fprintf (stderr, "%s: %u: ignoring '%c' in clipEnd\n",
			 *arg, lineno, (unsigned char) c);
	      else if (end == UINT_MAX)
		end = c - '0';
	      else
		end *= 10, end += c - '0';
	      break;
	    }
	    break;
	  }
	}
      }
    }
  }

  free (buf);
  return 0;
}

