#include "ka_demux.h"

#include <string.h>
#include <assert.h>

#include "config.h"
#include "ka_codecs.h"
#include "ka_log.h"
#include "ka_mem.h"

//trak -> stsd -> mp4a (or others, see ffmpef:libavformat isom_tags/c -> ff_codec_movaudio_tags)
//stco -> 0, nr entries, chunks offsets from start of file

typedef union
{
  char   c[4];
  int32_t  i;
  uint32_t u;
} mp4_tag_t;

static const mp4_tag_t TagFTYP = {{'f','t','y','p'}};
static const mp4_tag_t TagMOOV = {{'m','o','o','v'}};
static const mp4_tag_t TagMDAT = {{'m','d','a','t'}};
static const mp4_tag_t TagDATA = {{'d','a','t','a'}};
static const mp4_tag_t TagTRAK = {{'t','r','a','k'}};

#define TYP_CONTAINER 0
#define TYP_META      1
#define TYP_MDHD      2
#define TYP_STSD      3
#define TYP_ESDS      4
#define TYP_STTS      5
#define TYP_STSZ      6
#define TYP_STSC      7
#define TYP_STCO      8
#define TYP_MP4A     10
#define TYP_AC3      11

typedef struct Mp4Atom
{
  const mp4_tag_t tag;
  unsigned int metaId;
  const struct Mp4Atom* atoms;
  unsigned int type; // metaId > 0 : 0 string, 1 integer ; metaId = 0 : 0 container, 1 tag meta
} Mp4Atom;

typedef struct
{
  uint32_t framesCount;
  uint32_t samples;
} Mp4STTSEntry;

typedef struct
{
  uint32_t size;
  Mp4STTSEntry* table;
} Mp4STTS;

typedef struct
{
  uint32_t firstChunk;      // starts with 1
  uint32_t framesPerChunk;
  uint32_t chunkDescriptorId;
} Mp4STSCEntry;

typedef struct
{
  uint32_t size;
  Mp4STSCEntry* table;
} Mp4STSC;

typedef struct
{
  uint32_t size;
  uint32_t defaultSize;
  uint32_t* table;
} Mp4STSZ;

typedef struct
{
  uint32_t size;
  uint32_t* table;
} Mp4STCO;

static const Mp4Atom WaveAtoms[] =
{ {{'e','s','d','s'}, 0, NULL, TYP_ESDS}
, {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const Mp4Atom Mp4aAtoms[] =
{ {{'e','s','d','s'}, 0, NULL, TYP_ESDS}
, {{'w','a','v','e'}, 0, WaveAtoms, TYP_CONTAINER}
, {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const Mp4Atom StsdAtoms[] =
{ {{'m','p','4','a'}, 0, Mp4aAtoms, TYP_MP4A}
, {{'a','c','-','3'}, 0, Mp4aAtoms, TYP_AC3}
, {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const Mp4Atom StblAtoms[] =
{ {{'s','t','c','o'}, 0, NULL, TYP_STCO}
, {{'s','t','s','c'}, 0, NULL, TYP_STSC}
, {{'s','t','s','d'}, 0, StsdAtoms, TYP_STSD}
, {{'s','t','s','z'}, 0, NULL, TYP_STSZ}
, {{'s','t','t','s'}, 0, NULL, TYP_STTS}
, {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const Mp4Atom MinfAtoms[] =
{ {{'s','t','b','l'}, 0, StblAtoms, TYP_CONTAINER}
, {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const Mp4Atom MdiaAtoms[] =
{ {{'m','d','h','d'}, 0, MinfAtoms, TYP_MDHD}
, {{'m','i','n','f'}, 0, MinfAtoms, TYP_CONTAINER}
, {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const Mp4Atom TrefAtoms[] =
{ {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const Mp4Atom TrakAtoms[] =
{ {{'m','d','i','a'}, 0, MdiaAtoms, TYP_CONTAINER}
, {{'t','r','e','f'}, 0, TrefAtoms, TYP_CONTAINER}
, {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const Mp4Atom IlstAtoms[] =
{ {{'g','n','r','e'}, EMeta_StreamGenre, NULL, 0}
, {{'t','r','k','n'}, EMeta_StreamTrackNumber, NULL, 1}
, {{'©','A','R','T'}, EMeta_StreamAuthor, NULL, 0}
, {{'©','a','l','b'}, EMeta_StreamAlbum, NULL, 0}
, {{'©','c','m','t'}, EMeta_StreamComment, NULL, 0}
, {{'©','d','a','y'}, EMeta_StreamDate, NULL, 0}
, {{'©','g','e','n'}, EMeta_StreamGenre, NULL, 0}
, {{'©','n','a','m'}, EMeta_StreamTitle, NULL, 0}
, {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const Mp4Atom MetaAtoms[] =
{ {{'i','l','s','t'}, 0, IlstAtoms, TYP_CONTAINER}
, {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const Mp4Atom UdtaAtoms[] =
{ {{'m','e','t','a'}, 0, MetaAtoms, TYP_META}
, {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const Mp4Atom MoovAtoms[] =
{ {{'t','r','a','k'}, 0, TrakAtoms, TYP_CONTAINER}
, {{'u','d','t','a'}, 0, UdtaAtoms, TYP_CONTAINER}
, {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const Mp4Atom RootAtoms[] =
{ {{'m','o','o','v'}, 0, MoovAtoms, TYP_CONTAINER}
, {{  0,  0,  0,  0}, 0, NULL, 0}
};

static const struct
{
  ka_block_id   typ;
  uint8_t       id;
} DecoderConfigDescriptorObjectIds[] =
{ { 0             , 0x40} // Must look into Audio Specific Config
, { KA_ABLOCK_AAC , 0x66} // MPEG-2 AAC Main
, { KA_ABLOCK_AAC , 0x67} // MPEG-2 AAC LC
, { KA_ABLOCK_AAC , 0x68} // MPEG-2 AAC SSR
, { KA_ABLOCK_MPEG, 0x69} // MPEG 13918-3 or 11172-3
, { KA_ABLOCK_MPEG, 0x6B} // MPEG 11172-3
, { KA_ABLOCK_AC3 , 0xA5} // AC3
//, { KA_ABLOCK_EAC3, 0xA6}
//, { KA_ABLOCK_DTS, 0xA9}
//, { KA_ABLOCK_Opus, 0xAD}
//, { KA_ABLOCK_FLAC, 0xC1}
//, { KA_ABLOCK_Vorbis, 0xDD}
, { 0, 0}
};

typedef struct chunck_t
{
  uint32_t     size;     // size of chunk in bytes
  uint32_t     startPos; // absolute position in source
  uint32_t     duration; // chunk duration in codec specific unit
} chunk_t;

typedef union
{
  uint32_t     typ;
  ka_vparams_t vparams;
  ka_aparams_t aparams;
  Mp4STTS      stts;
  Mp4STSZ      stsz;
  Mp4STSC      stsc;
  Mp4STCO      stco;
  uint32_t     timeScale;
  uint64_t     totalDuration;
  uint32_t     channels;
  uint32_t     samplerate;
  uint32_t     avgbitrate;
  void*        specificData;
  uint32_t     specificDataLen;
  struct
  {
    chunk_t*   list;
    uint32_t   count;
  }
} track_t;

static void track_reset(track_t* p)
{
  p->typ = 0;
  p->timeScale = 0;
  p->channels = 0;
  p->samplerate = 0;
  p->blockId = 0;
  p->avgbitrate = 0;
  p->specificDataLen = 0;
  p->stts.size = 0;
  p->stsz.size = 0;
  p->stsc.size = 0;
  p->stco.size = 0;
  if (p->specificData != NULL) ka_mem_free(p->specificData);
  if (p->stts.table   != NULL) ka_mem_free(p->stts.table);
  if (p->stsz.table   != NULL) ka_mem_free(p->stsz.table);
  if (p->stsc.table   != NULL) ka_mem_free(p->stsc.table);
  if (p->stco.table   != NULL) ka_mem_free(p->stco.table);
  if (t->chunks.list  != NULL) ka_mem_free(t->chunks.list);
}

static void track_free(track_t* p)
{
  if (p != NULL)
  {
    if (p->specificData != NULL) ka_mem_free(p->specificData);
    if (p->stts.table   != NULL) ka_mem_free(p->stts.table);
    if (p->stsz.table   != NULL) ka_mem_free(p->stsz.table);
    if (p->stsc.table   != NULL) ka_mem_free(p->stsc.table);
    if (p->stco.table   != NULL) ka_mem_free(p->stco.table);
    if (t->chunks.list  != NULL) ka_mem_free(t->chunks.list);
  }
}

#define MAX_LEVEL 10
#define MAX_TRACKS 8

typedef struct
{
  int32_t     size;
  mp4_tag_t   tag;
  int32_t     read;
  const struct Mp4Atom* atoms;
} mp4_level_t;

typedef struct
{
  ka_demux_t  hdr;
  ka_input_t* input;
  int32_t     skipsize;
  int32_t     state;
  mp4_level_t levels[MAX_LEVEL + 2];
  int32_t     level;
  int32_t     track;
  uint32_t    mdatPos;
  uint32_t    mdatSize;
  const Mp4Atom* pMeta;

  int        datasize;
  track_t    tracks[MAX_TRACKS];
} data_t;

static int32_t reverse32(int32_t a)
{
  mp4_tag_t x;

  x.i = a;
  return (x.c[0] << 24) + (x.c[1] << 16) + (x.c[2] << 8) + x.c[3];
}

static bool mp4_getRInt(ka_input_t* input, uint32_t* i, int nr)
{
  bool ret = ka_input_read(input, (uint8_t*) i, nr);
  *i = reverse32(*i);
  return ret;
}

static bool mp4_getAtom(ka_input_t* input, mp4_level_t* plevel)
{
  bool ret = ka_input_read(input, (uint8_t*) plevel, 8);
  plevel->size = reverse32(plevel->size);

  return ret;
}

static uint32_t mp4_getDescriptorLength(ka_input_t* input, int max, uint32_t* count)
{
  uint32_t len = 0;
  uint8_t val;
  *count = 0;
  if (max > 4) max = 4;

  for (; max > 0; max--)
  {
    *count += 1;
    ka_input_read(input, &val, 1);
    len <<= 7;
    len += val & 0x7f;
    if (!(val & 0x80))
      break;
  }

  return len;
}

/**
 * Creates a new demuxer.
 *
 * @returns Pointer to demuxer data or NULL if creation failed.
 */
static ka_demux_t* new_demux_mp4(const kav_demux_t* vptr, ka_error_t* pErrorBlock, uint32_t size, ka_input_t* input)
{
  data_t* data = ka_mem_calloc(sizeof(*data));

  if (!data)
  {
    ka_error_fill(pErrorBlock, ka_error_nomem);
    return NULL;
  }

  data->hdr.vptr = vptr;
  data->hdr.pErrorBlock = pErrorBlock;

  data->input = input;

  data->levels[0].size = size;
  data->levels[0].read = 0;
  data->levels[0].tag.c[0] = 'r';
  data->levels[0].tag.c[1] = 'o';
  data->levels[0].tag.c[2] = 'o';
  data->levels[0].tag.c[3] = 't';
  data->levels[0].atoms = RootAtoms;
  data->level = 0;
  data->track = -1;

  data->state = MP4_STATE_WAIT_FTYP;

  return &data->hdr;
}

/**
 * Deletes a demuxer.
 *
 * @param  ppdemux  Address of pointer to demuxer data.
 */
static void delete_demux_mp4(ka_demux_t** ppdemux)
{
  data_t* data = (data_t*) *ppdemux;
  *ppdemux = NULL;

  if (!data)
    return;

  if (data->indexes) ka_mem_free(data->indexes);
  for (int i = 0; i < MAX_TRACKS; i++)
  {
    track_free(&data->track[i]);
  }
  ka_mem_free(data);
}

/**
 * Resets demuxer data after a restart.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  stack   Pointer to stack.
 */
static void demux_mp4_reset(ka_demux_t* pdemux, ka_stack_t* stack)
{
  data_t* data = (data_t*) pdemux;

  stack = stack; // unused

  ka_input_seek(data->input, 0);
  data->state = AVI_STATE_MAIN;
  data->curlevel = data->levels;
  data->datasize = 0;
  data->skipsize = 0;
  data->curtyp = 0;
  memset(&data->avihdr, 0, sizeof(data->avihdr));
  memset(&data->work, 0, sizeof(data->work));
  memset(&data->streams, 0, sizeof(data->streams));
  memset(&data->levels, 0, sizeof(data->levels));

  data->hdr.demuxSection = 0;
}

/**
 * Clears demuxer data after a seek.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  stack   Pointer to stack.
 */
static int demux_mp4_clear(ka_demux_t* pdemux, ka_stack_t* stack)
{
  data_t* data = (data_t*) pdemux;

  stack = stack; // unused

  data->curlevel = data->levels;
  data->datasize = 0;
  data->skipsize = 0;
  data->hdr.demuxSection++;

  return 0;
}

static void demux_mp4_validate_track(data_t* data, int id, track_t* t)
{
  ka_block_id typ = (t->typ & ~KA_BLOCK_STREAMMASK);

  t->typ = 0;

  if (!id)
  {
    if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
      ka_log( ka_log_warning | ka_log_demux
            , "No supported audio track present");
      return;
  }

  if (!t->timeScale)
  {
    if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
      ka_log( ka_log_warning | ka_log_demux
            , "Missing/Invalid 'mdhd' atom");
    return;
  }

  if (!t->stsz.size)
  {
    if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
      ka_log( ka_log_warning | ka_log_demux
            , "Missing/Invalid 'stsz' atom");
    return;
  }

  if (!t->stsc.size)
  {
    if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
      ka_log( ka_log_warning | ka_log_demux
          , "Missing/Invalid 'stsc' atom");
    return;
  }

  if (!t->stco.size)
  {
    if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
      ka_log( ka_log_warning | ka_log_demux
            , "Missing/Invalid 'stco' atom");
    return;
  }

  if (!t->stts.size)
  {
    if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
      ka_log( ka_log_warning | ka_log_demux
            , "Missing/Invalid 'stts' atom");
    return;
  }


  if ((id == KA_ABLOCK_AAC) && (t->specificData == NULL))
  {
    if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
      ka_log( ka_log_warning | ka_log_demux
            , "Could not extract mp4 audio specific config");
    return;
  }

//ka_logn("STSZ %d, STSC %d, STCO %d STTS %d", w->stsz.size, w->stsc.size, w->stco.size, w->stts.size);

  // Chunks table ?
  if ((t->stsz.size > 0)
  &&  (t->stsc.size > 0)
  &&  (t->stco.size > 0)
  &&  (t->stts.size > 0))
  {
    t->chunks.count = t->stsz.size;
    t->chunks.list = ka_mem_alloc(t->chunks.count * sizeof(*t->chunks.list));
    if (t->chunks.list == NULL)
      return;

    chunk_t* chunk = t->chunks.list;
    int its = -1;
    int isc = -1;
    int ico = -1;
    int framesPerChunk = 0;
    int nextsc = 0;
    uint32_t startPos = 0;
    int framesPerTS = 0;
    uint32_t samples = 0;

    // Add one entry per frame
    for (int isz = 0; isz < t->chunks.count; isz++, chunk++)
    {
      framesPerTS--;
      if ((framesPerTS <= 0) && ((its + 1) < t->stts.size))
      {
        its++;
        framesPerTS = t->stts.table[its].framesCount;
        samples = t->stts.table[its].samples;
      }

      // If exceed nr of frames in chunk move to next chunk
      framesPerChunk--;
      if (framesPerChunk <= 0)
      {
        // Next chunk
        ico++;
        // If we exceed the limits of the stco table, exit loop to truncate chunk list
        if (ico >= t->stco.size)
          break;

        // Set new start position to start of chunk (there are gaps between chunks)
        startPos = t->stco.table[ico];

        // If reached next chunk group, nr of frames per chunk will change
        if (ico >= nextsc)
        {
          isc++;
          nextsc = (isc + 1 < t->stsc.size)
                   ? t->stsc.table[isc + 1].firstChunk - 1
                   : t->chunks.count + 1;
        }

        // Reset frame count
        framesPerChunk = t->stsc.table[isc].framesPerChunk;
//ka_logn("STSC %d: isc %d fpc %d, start %08x", ico, isc, framesPerChunk, startPos);
      }

      chunk->duration = samples;
      chunk->startPos = startPos;
      chunk->size = (t->stsz.defaultSize) ? t->stsz.defaultSize : t->stsz.table[isz];
      startPos += chunk->size;

      // Sanitize
      if (data->inputEend <= chunk->startPos)
      {
        // truncate chunk list
        t->chunks.count = chunk - t->chunks.list;
        break;
      }

      if (t->inputEnd < chunk->startPos + chunk->size)
        chunk->size = t->inputEnd - chunk->startPos;

//ka_logn("Chunk %d (%08x + %x)", isz, chunk->startPos, chunk->size);
    }

    // Fixup chunks count
    t->chunks.count = chunk - t->chunks.list;
//ka_logn("Chunks %d", t->chunks.count);
  }
  if (t->chunks.count <= 0)
    return;

  // Valid track
  t->typ = typ | id;
}

/**
 * Parses the input stream to determine positioning points.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  stack   Pointer to stack.
 *
 * @returns 0 if OK, -1 if error and error block is filled.
 */
static int demux_mp4_parse(ka_demux_t* pdemux, ka_stack_t* stack)
{
  data_t* data = (data_t*) pdemux;
  bool bLoop = true;
  int curpos = (int) ka_input_tell(data->input);
  int pos = 0;
  int offset = 0;
  int read = 0;
  int rc;
  uint8_t buf[16];
  uint32_t i, n, nr, tag;
  patans_t* curTrack = &data->tracks[0];

  stack = stack; //Unused

  while (bLoop)
  {
    mp4_level_t* pclevel = &data->levels[data->level];
    mp4_level_t* plevel = &data->levels[data->level + 1];

    // Move to offset, skipping data if necessary
    pos += data->skipsize;
    data->skipsize = 0;
    ka_input_seek(data->input, pos);

    switch(data->state)
    {
      case MP4_STATE_SKIP_TAGEND:
      {
        pos += plevel->size - plevel->read;
        plevel->read = plevel->size;
        data->state = MP4_STATE_WAIT_TAG;
        pclevel->read += plevel->size;
      }
      break;
      case MP4_STATE_WAIT_TAG:
      {
        // First check if we reach end of parent level(s)
        while (bLoop && ((pclevel->size - pclevel->read) < 8))
        {
          if ((pclevel->size - pclevel->read) < 0)
          {
            ka_error_fill( pdemux->pErrorBlock
                         , "Atom '%.4s', pos %08x: Exceeded end of atom by %d bytes"
                         , pclevel->tag.c, pos, pclevel->read - pclevel->size
                         );
            return ka_demux_error;
          }

          if ((pclevel->size - pclevel->read) > 0)
          {
            ka_error_fill( pdemux->pErrorBlock
                         , "Atom '%.4s', pos %08x: Skip %d bytes at end of atom"
                         , pclevel->tag.c, pos, pclevel->size - pclevel->read
                         );
            data->skipsize = pclevel->size - pclevel->read;
            pclevel->read = pclevel->size;
            break;
          }

          if (data->level <= 0)
          {
            // End of file
            bLoop = false;
            break;
          }

          data->level--;
          pclevel--;
          plevel--;
          pclevel->read += plevel->size;
        }

        if (!bLoop) break;
        if (data->skipsize) continue;

        plevel->read = 0;

        // Attempt to read tag header
        if (!mp4_readAtom(data->input, plevel))
        {
          bLoop = false;
          break;
        }

        // Truncate if exceed parent tag
        if ((pclevel->size - pclevel->read - plevel->size ) < 0)
        {
          ka_error_fill( pdemux->pErrorBlock
                       , "Atom '%.4s', pos %08x: Bigger than parent, truncating size from %08x to %08x"
                       , plevel->tag.c, pos, plevel->size, pclevel->size - pclevel->read
                       );
          plevel->size = pclevel->size - pclevel->read;
        }

        // Sanity checks
        if (plevel->size < 8)
        {
          ka_error_fill( pdemux->pErrorBlock
                       , "Atom '%.4s', pos %08x: bad child atom of size %08x, skipping %08x to end of atom"
                       , plevel->tag.c, pos, plevel->size, pclevel->size - pclevel->read
                       );

          // Skip to end of parent tag
          data->skipsize = pclevel->size - pclevel->read;
          data->state = MP4_STATE_WAIT_TAG;
          pclevel->read += data->skipsize;
          continue;
        }

//ka_log("%.4s: pos %08x, size %08x, level %d, upread %08x"
//  , plevel->tag.c, pos, plevel->size, data->level, pclevel->read);

        if (plevel->tag.i == TagMDAT.i)
        {
          data->mdatPos = pos;
          data->mdatSize = plevel->size - 8;
        }

        // Metadata tag ?
        for (data->pMeta = pclevel->atoms; data->pMeta->tag.i; data->pMeta++)
        {
          if (plevel->tag.i == data->pMeta->tag.i)
            break;
        }

        if (data->pMeta->tag.i)
        {
          plevel->atoms = data->pMeta->atoms;

          if (data->pMeta->metaId == 0)
          {
            // default
            data->state = MP4_STATE_WAIT_TAG;

            switch(data->pMeta->type)
            {
              case TYP_CONTAINER:
              case TYP_META:
              {
                // parse subtags
                data->state = MP4_STATE_INCREASE_TAG;

                if (data->pMeta->type == TYP_META)
                {
                  uint8_t dummy[4];
                  // Tag 'meta', skip version/flags
                  ka_input_read(data->input, 4);
                  plevel->read += 4;
                }
                else if (data->pMeta->tag.i == TagTRAK.i)
                {
                  data->track++;
                  curTrack = data->tracks[data->track];

                  if (data->audiotrack == -1)
                  {
                    // Reset info collected on previous track
                    track_reset(&curTrack);
                  }
                }

                data->pMeta = NULL;

                continue;
              }
              break;
              case TYP_MDHD:
              {
                if ((data->track == data->audiotrack)
                ||  (-1 == data->audiotrack)) // may be present before stsd atom
                  data->state = MP4_STATE_WAIT_MDHD;
              }
              break;
              case TYP_STSD:
              {
                // Only if no AAC track found yet
                if (data->audiotrack == -1)
                  data->state = MP4_STATE_WAIT_STSD;
              }
              break;
              case TYP_MP4A:
              {
                if  (-1 == data->audiotrack)
                  data->state = MP4_STATE_WAIT_MP4A;
              }
              break;
              case TYP_ESDS:
              {
                if (-1 == data->audiotrack)
                  data->state = MP4_STATE_WAIT_ESDS;
              }
              break;
              case TYP_AC3:
              {
                if  (-1 == data->audiotrack)
                {
                  data->audiotrack = data->track;
                  curTrack->typ = KA_ABLOCK_AC3;
                }
              }
              break;
              case TYP_STTS:
              {
                if ((data->track == data->audiotrack)
                ||  (-1 == data->audiotrack)) // may be present before stsd atom
                  data->state = MP4_STATE_WAIT_STTS;
              }
              break;
              case TYP_STSZ:
              {
                if ((data->track == data->audiotrack)
                ||  (-1 == data->audiotrack)) // may be present before stsd atom
                  data->state = MP4_STATE_WAIT_STSZ;
              }
              break;
              case TYP_STSC:
              {
                if ((data->track == data->audiotrack)
                ||  (-1 == data->audiotrack)) // may be present before stsd atom
                  data->state = MP4_STATE_WAIT_STSC;
              }
              break;
              case TYP_STCO:
              {
                if ((data->track == data->audiotrack)
                ||  (-1 == data->audiotrack)) // may be present before stsd atom
                  data->state = MP4_STATE_WAIT_STCO;
              }
              break;
            }
          }
          else
          {
            // parse metadata
            data->state = MP4_STATE_WAIT_META;
          }

          if (data->state != MP4_STATE_WAIT_TAG)
            continue;
        }
        else
          data->pMeta = NULL;

        // Skip tag
        data->state = MP4_STATE_SKIP_TAGEND;
      }
      break;
      case MP4_STATE_INCREASE_TAG:
      {
        data->state = MP4_STATE_WAIT_TAG;

        if (data->level >= MAX_LEVEL)
        {
          ka_error_fill( pdemux->pErrorBlock
                       , "Atom '%.4s', pos %08x: Exceed max ATOM depth of %d"
                       , pclevel->tag.c, ka_input_tell(data->input) - 8, data->level
                       );

          bLoop = false;
          continue;
        }
        data->level++;
        pclevel++;
        plevel++;
      }
      break;
      case MP4_STATE_WAIT_MDHD:
      {
        // Attempt to decode MDHD
        rc = ka_input_hasBytes(data->input, plevel->size * 32);
        if (rc <= 0)
        {
          // Wait for buffer to be filled
          if (rc < 0)
            *bContinue = true;

          bLoop = false;
          break;
        }

        // Version, flags, creation time,modification time
        ka_input_skip(data->input, 12);
        plevel->read += 12;

        // Time scale
        mp4_getRInt(data->input, &curTrack->timeScale, 4);
        plevel->read += 4;

        data->state = MP4_STATE_SKIP_TAGEND;
      }
      break;
      case MP4_STATE_WAIT_STSD:
      {
        // Attempt to decode STSD
        rc = ka_input_hasBytes(data->input, plevel->size * 8);
        if (rc <= 0)
        {
          // Wait for buffer to be filled
          if (rc < 0)
            *bContinue = true;

          bLoop = false;
          break;
        }

        // Version, flags, nr of entries
        ka_input_skip(data->input, 8);
        plevel->read += 8;

        // Read sub tags looking for MP4A
        data->state = MP4_STATE_INCREASE_TAG;
      }
      break;
      case MP4_STATE_WAIT_MP4A:
      {
        uint32_t version;

        // Attempt to decode MP4A
        rc = ka_input_hasBytes(data->input, plevel->size * 8);
        if (rc <= 0)
        {
          // Wait for buffer to be filled
          if (rc < 0)
            *bContinue = true;

          bLoop = false;
          break;
        }
        ka_input_skip(data->input, 8);
        plevel->read += 8;
        mp4_getRInt(data->input, &version, 2);
        plevel->read += 2;
        if (version > 1)
        {
          ka_error_fill( pdemux->pErrorBlock
                       , "Atom '%.4s', pos %08x: Unsupported Sound Sample Description version %x"
                       , plevel->tag.c, ka_input_tell(data->input) - 2, version
                       );

          bLoop = false;
          continue;
        }
        ka_input_skip(data->input, 6);
        plevel->read += 6;
        mp4_getRInt(data->input, &curTrack->channels, 2);
        plevel->read += 2;
        ka_input_skip(data->input, 6);
        plevel->read += 6;
        mp4_getRInt(data->input, &curTrack->samplerate, 4);
        plevel->read += 4;
        curTrack->samplerate >>= 16;

        if (version == 1)
        {
          ka_input_skip(data->input, 16);
          plevel->read += 16;
        }

        // Read sub tags looking for ESDS
        data->state = MP4_STATE_INCREASE_TAG;
      }
      break;
      case MP4_STATE_WAIT_ESDS:
      {
        uint32_t val, type, len, read;

        // Attempt to decode ESDS
        rc = ka_input_hasBytes(data->input, plevel->size * 8);
        if (rc <= 0)
        {
          // Wait for buffer to be filled
          if (rc < 0)
            *bContinue = true;

          bLoop = false;
          break;
        }

        // Version & flags
        ka_input_skip(data->input, 4);
        plevel->read += 4;

        // Is ES_Descriptor (ISO-14496-1)
        mp4_getRInt(data->input, &val, 1);
        plevel->read += 1;

        if (val != 0x03)
          goto skip_esds;

        len = mp4_getDescriptorLength(data->input, plevel->size - plevel->read, &read);
        plevel->read += read;

        if (len > plevel->size - plevel->read)
        {
          if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
            ka_log( ka_log_warning | ka_log_demux
                  , "Atom '%.4s', pos %08x: Corrupted ES_Descriptor 03, reducing size from %d to %d"
                  , plevel->tag.c, ka_input_tell(data->input) - read, len, plevel->size - plevel->read
                  );
          len = plevel->size - plevel->read;
        }

        if (len < 14)
          goto corrupted_esds;

        ka_input_skip(data->input, 2); // ES_ID
        mp4_getRInt(data->input, &val, 1); // flags
        plevel->read += 3;
        read = 0;
        if (val & 0x80) // dependsOn_ES_ID
        {
          ka_input_skip(data->input, 2);
          plevel->read += 2;
        }
        if (val & 0x40) // URL
        {
          len = mp4_getRInt(data->input, &val, 1);
          if ((len + 14) > (plevel->size - plevel->read))
            goto corrupted_esds;
          ka_input_skip(data->input, len);
          plevel->read += 1 + len;
        }
        if (val & 0x20) // OCR_ES_ID
        {
          ka_input_skip(data->input, 2);
          plevel->read += 2;
        }

        // DecoderConfigDescriptor
        mp4_getRInt(data->input, &val, 1);
        plevel->read += 1;
        if (val != 0x04)
          goto corrupted_esds;

        len = mp4_getDescriptorLength(data->input, plevel->size - plevel->read, &read);
        plevel->read += read;

        if (len > plevel->size - plevel->read)
        {
          if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
            ka_log( ka_log_warning | ka_log_demux
                  , "Atom '%.4s', pos %08x: Corrupted ES_Descriptor 04, reducing size from %d to %d"
                  , plevel->tag.c, ka_input_tell(data->input) - read, len, plevel->size - plevel->read
                  );
          len = plevel->size - plevel->read;
        }
        if (len < 13)
          goto corrupted_esds;

        mp4_getRInt(data->input, &type, 1);
        plevel->read += 1;

        for (int i = 0; DecoderConfigDescriptorObjectIds[i].fn != NULL; i++)
        {
          if (DecoderConfigDescriptorObjectIds[i].id == type)
          {
            curTrack->typ = DecoderConfigDescriptorObjectIds[i].typ;
            break;
          }
        }

        // Unsupported codec ? Specified in decoder specific info
        if ((curTrack->typ == 0) && (type != 0x40))
          goto skip_esds;

        // No more info here for MP3
        if (curTrack->typ == KA_ABLOCK_MPEG)
          goto skip_esds;

        ka_input_skip(data->input, 8); // stream type, buffersizeDB
        mp4_getRInt(data->input, &val, 4);
        plevel->read += 12;
        curTrack->avgbitrate = (1023 + val) / 1024;

        if (len > 13)
        {
          // DecoderSpecificInfo ?
          mp4_getRInt(data->input, &val, 1);
          plevel->read += 1;
          if (val != 0x05)
            goto skip_esds;

          len = mp4_getDescriptorLength(data->input, plevel->size - plevel->read, &read);
          plevel->read += read;

          if (len > plevel->size - plevel->read)
          {
            if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
              ka_log (ka_log_warning | ka_log_demux
                    , "Atom '%.4s', pos %08x: Corrupted ES_Descriptor 05, reducing size from %d to %d"
                    , plevel->tag.c, ka_input_tell(data->input) - read, len, plevel->size - plevel->read
                    );
            len = plevel->size - plevel->read;
          }
          if (len < 1)
            goto corrupted_esds;

          curTrack->specificData = ka_mem_calloc(len);
          if (!curTrack->specificData)
          {
            ka_error_fill(pdemux->pErrorBlock, ka_error_nomem);
            return ka_demux_error;
          }
          ka_input_read(data->input, curTrack->specificData, len);
          plevel->read += len;
          curTrack->specificDataLen = len;

          if (curTrack->typ == 0)
          {
            int aot = (((uint8_t*) curTrack->specificData)[0]) >> 3;

            // Assume AAC by default
            curTrack->typ = KA_ABLOCK_AAC;

            // AOT_PS or old mp3on4 draft?
            if (aot == 29)
            {
              if (len <= 2) // specific info too short for AOT_PS
              {
                // Does not seem to work (MP3 without sync maybe?)
                // curTrack->typ = KA_ABLOCK_MPEG;
                curTrack->typ = 0;
              }
            }
          }
        }

skip_esds:
        if (curTrack->typ != 0)
          data->audiotrack = data->track;

        data->state = MP4_STATE_SKIP_TAGEND;
        break;

corrupted_esds:
        ka_error_fill( pdemux->pErrorBlock
                     , "Atom '%.4s', pos %08x: Atom corrupted"
                     , plevel->tag.c, ka_input_tell(data->input)
                     );

        bLoop = false;
        continue;
      }
      break;
      case MP4_STATE_WAIT_STTS:
      {
        if (plevel->read < 16)
        {
          // read header
          if (plevel->size < 16)
          {
            ka_error_fill( pdemux->pErrorBlock
                         , "Atom '%.4s', pos %08x: Size should be at least 16"
                         , plevel->tag.c, ka_input_tell(data->input)
                       );

            bLoop = false;
            continue;
          }

          rc = ka_input_hasBytes(data->input, 8*8);
          if (rc <= 0)
          {
            // Wait for buffer to be filled
            if (rc < 0)
              *bContinue = true;

            bLoop = false;
            break;
          }

          ka_input_skip(data->input, 4);
          mp4_getRInt(data->input, &curTrack->stts.size, 4);
          plevel->read += 8;
          // truncate if not coherent
          if (plevel->read + curTrack->stts.size * 8 > plevel->size)
          {
            if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
              ka_log( ka_log_warning | ka_log_demux
                    , "Truncating STTS table at pos %08x by %x bytes"
                    , ka_input_tell(data->input) - 8, plevel->read + curTrack->stts.size * 8 - plevel->size
                    );
            curTrack->stts.size = (plevel->size - plevel->read) / 8;
          }

          ka_mem_free(curTrack->stts.table);
          curTrack->stts.table = NULL;
          if (curTrack->stts.size > 0)
          {
            curTrack->stts.table = ka_mem_calloc(curTrack->stts.size * sizeof(*curTrack->stts.table));
            if (!curTrack->stts.table)
            {
              ka_error_fill(pdemux->pErrorBlock, ka_error_nomem);
              return ka_demux_error;
            }
          }

          // loop back here to read table
          continue;
        }
        else
        {
          int count = (plevel->size - plevel->read) / 8;

          if (count)
          {
            int index = (plevel->read - 16) / 8;
            if (count > 64) count = 64;

            rc = ka_input_hasBytes(data->input, count * 8 * 8);
            if (rc <= 0)
            {
              // Wait for buffer to be filled
              if (rc < 0)
                *bContinue = true;

              bLoop = false;
              break;
            }

            for (; count > 0; count--, index++)
            {
              mp4_getRInt(data->input, &curTrack->stts.table[index].framesCount, 4);
              mp4_getRInt(data->input, &curTrack->stts.table[index].samples, 4);
              plevel->read += 8;
            }

            // loop back here to read more of table
            continue;
          }
          else
          {
            data->state = MP4_STATE_SKIP_TAGEND;
          }
        }
      }
      break;
      case MP4_STATE_WAIT_STSZ:
      {
        if (plevel->read < 20)
        {
          // read header
          if (plevel->size < 20)
          {
            ka_error_fill( pdemux->pErrorBlock
                         , "Atom '%.4s', pos %08x: Size should be at least 20"
                         , plevel->tag.c, ka_input_tell(data->input)
                         );

            bLoop = false;
            continue;
          }

          rc = ka_input_hasBytes(data->input,  < 12*8);
          if (rc <= 0)
          {
            // Wait for buffer to be filled
            if (rc < 0)
              *bContinue = true;

            bLoop = false;
            break;
          }

          ka_input_skip(data->input, 4);
          mp4_getRInt(data->input, &curTrack->stsz.defaultSize, 4);
          mp4_getRInt(data->input, &curTrack->stsz.size, 4);
          plevel->read += 12;
          if (!curTrack->stsz.defaultSize)
          {
            // truncate if not coherent
            if (plevel->read + curTrack->stsz.size * 4 > plevel->size)
            {
              if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
                ka_log( ka_log_warning | ka_log_demux
                      , "Truncating STSZ table by %x bytes"
                      , plevel->read + curTrack->stsz.size * 4 - plevel->size
                      );
              curTrack->stsz.size = (plevel->size - plevel->read) / 4;
            }

            ka_mem_free(curTrack->stsz.table);
            curTrack->stsz.table = NULL;
            if (curTrack->stsz.size > 0)
            {
              curTrack->stsz.table = ka_mem_calloc(curTrack->stsz.size * sizeof(*curTrack->stsz.table));
              if (!curTrack->stts.table)
              {
                ka_error_fill(pdemux->pErrorBlock, ka_error_nomem);
                return ka_demux_error;
              }
            }

            // loop back here to read table
            continue;
          }
          else
          {
            // No table, just a constant size
            data->state = MP4_STATE_SKIP_TAGEND;
          }
        }
        else
        {
          int count = (plevel->size - plevel->read) / 4;

          if (count)
          {
            int index = (plevel->read - 20) / 4;
            if (count > 64) count = 64;

            rc = ka_input_hasBytes(data->input, < count * 4 * 8);
            if (rc <= 0)
            {
              // Wait for buffer to be filled
              if (rc < 0)
                *bContinue = true;

              bLoop = false;
              break;
            }

            for (; count > 0; count--, index++)
            {
              mp4_getRInt(data->input, &curTrack->stsz.table[index], 4);
              plevel->read += 4;
            }

            // loop back here to read more of table
            continue;
          }
          else
          {
            data->state = MP4_STATE_SKIP_TAGEND;
          }
        }
      }
      break;
      case MP4_STATE_WAIT_STSC:
      {
        if (plevel->read < 16)
        {
          // read header
          if (plevel->size < 16)
          {
            ka_error_fill( pdemux->pErrorBlock
                         , "Atom '%.4s', pos %08x: Size should be at least 16"
                         , plevel->tag.c, ka_input_tell(data->input)
                         );

            bLoop = false;
            continue;
          }

          rc = ka_input_hasBytes(data->input, 8*8);
          if (rc <= 0
          {
            // Wait for buffer to be filled
            if (rc < 0)
              *bContinue = true;

            bLoop = false;
            break;
          }

          ka_input_skip(data->input, 4);
          mp4_getRInt(data->input, &curTrack->stsc.size, 4);
          plevel->read += 8;
          // truncate if not coherent
          if (plevel->read + curTrack->stsc.size * 12 > plevel->size)
          {
              if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
                ka_log( ka_log_warning | ka_log_demux
                      , "Truncating STSC table by %x bytes"
                      , plevel->read + curTrack->stsc.size * 12 - plevel->size
                      );
            curTrack->stsc.size = (plevel->size - plevel->read) / 12;
          }

          ka_mem_free(curTrack->stsc.table);
          curTrack->stsc.table = NULL;
          if (curTrack->stsc.size > 0)
          {
            curTrack->stsc.table = ka_mem_alloc(curTrack->stsc.size * sizeof(*curTrack->stsc.table));
            if (!curTrack->stsc.table)
            {
              ka_error_fill(pdemux->pErrorBlock, ka_error_nomem);
              return ka_demux_error;
            }
          }

          // loop back here to read table
          continue;
        }
        else
        {
          int count = (plevel->size - plevel->read) / 12;

          if (count)
          {
            int index = (plevel->read - 16) / 12;
            if (count > 64) count = 64;

            rc = ka_input_hasBytes(data->input, count * 12 * 8);
            if (rc <= 0)
            {
              // Wait for buffer to be filled
              if (rc < 0)
                *bContinue = true;

              bLoop = false;
              break;
            }

            for (; count > 0; count--, index++)
            {
              mp4_getRInt(data->input, &curTrack->stsc.table[index].firstChunk, 4);
              mp4_getRInt(data->input, &curTrack->stsc.table[index].framesPerChunk, 4);
              ka_input_skip(data->input, 4);
              plevel->read += 12;
            }

            // loop back here to read more of table
            continue;
          }
          else
          {
            data->state = MP4_STATE_SKIP_TAGEND;
          }
        }
      }
      break;
      case MP4_STATE_WAIT_STCO:
      {
        if (plevel->read < 16)
        {
          if (plevel->size < 16)
          {
            ka_error_fill( pdemux->pErrorBlock
                         , "Atom '%.4s', pos %08x: Size should be at least 16"
                         , plevel->tag.c, ka_input_tell(data->input)
                         );

            bLoop = false;
            continue;
          }

          // read header
          rc = ka_input_hasBytes(data->input, 8*8);
          if (tc <= 0)
          {
            // Wait for buffer to be filled
            if (rc < 0)
              *bContinue = true;

            bLoop = false;
            break;
          }

          ka_input_skip(data->input, 4);
          mp4_getRInt(data->input, &curTrack->stco.size, 4);
          plevel->read += 8;
          // truncate if not coherent
          if (plevel->read + curTrack->stco.size * 4 > plevel->size)
          {
            if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
              ka_log( ka_log_warning | ka_log_demux
                    , "Truncating STCO table by %x bytes"
                    , plevel->read + curTrack->stsz.size * 4 - plevel->size
                    );
            curTrack->stco.size = (plevel->size - plevel->read) / 4;
          }

          ka_mem_free(curTrack->stco.table);
          curTrack->stco.table = NULL;
          if (curTrack->stco.size > 0)
          {
            curTrack->stco.table = ka_mem_alloc(curTrack->stco.size * sizeof(*curTrack->stco.table));
            if (!curTrack->stco.table)
            {
              ka_error_fill(pdemux->pErrorBlock, ka_error_nomem);
              return ka_demux_error;
            }
          }

          // loop back here to read table
          continue;
        }
        else
        {
          int count = (plevel->size - plevel->read) / 4;

          if (count)
          {
            int index = (plevel->read - 16) / 4;
            if (count > 64) count = 64;

            rc = ka_input_hasBytes(data->input, count * 4 * 8);
            if (rc <= 0)
            {
              // Wait for buffer to be filled
              if (rc < 0)
                *bContinue = true;

              bLoop = false;
              break;
            }

            for (; count > 0; count--, index++)
            {
              mp4_getRInt(data->input, &curTrack->stco.table[index], 4);
              plevel->read += 4;
            }

            // loop back here to read more of table
            continue;
          }
          else
          {
            data->state = MP4_STATE_SKIP_TAGEND;
          }
        }
      }
      break;
      case MP4_STATE_WAIT_META:
      {
        char* pString = NULL;

        // Attempt to string
        rc = ka_input_hasBytes(data->input, plevel->size * 8);
        if (rc <= 0)
        {
          // Wait for buffer to be filled
          if (rc < 0)
            *bContinue = true;

          bLoop = false;
          break;
        }

        if (plevel->size > 16)
        {
          mp4_level_t sublevel;

          // Data sub tag ?
          mp4_getAtom(data->input, &sublevel);
          plevel->read += 8;

          if ((sublevel.size <= (plevel->size + 8 - plevel->read))
          &&  (sublevel.tag.i == TagDATA.i))
          {
            uint32_t flags;
            mp4_getRInt(data->input, &flags, 4);
            ka_input_skip(data->input, 4);
            plevel->read += 8;

            if ((flags == 1) && (data->pMeta->type == 0))
            {
              int slen = sublevel.size - 16;
              uint8_t* pString = ka_mem_alloc(slen + 1);
              if (pString != NULL)
              {
                ka_input_read(data->input, pString, slen);
                plevel->read += slen;
                pString[slen] = '\0';
                // e = IStream_SetUtf8(s, 1, data->pMeta->metaId, pString, slen);
                ka_mem_free(pString);
              }
            }
            else if ((flags == 0) && (data->pMeta->type == 1))
            {
              if (sublevel.size >= 20)
              {
                uint32_t val;
                mp4_getRInt(data->input, &val, 4);
                plevel->read += 4;
                // e = IStream_SetMetadata(s, 1, data->pMeta->metaId, &val, sizeof(int));
              }
            }
            //if (e)
            //{
            //  bLoop = false;
            //  break;
            //}
          }
        }
        data->state = MP4_STATE_SKIP_TAGEND;
      }
      break;
    }
  }

  // No MP4 header?
  if ((data->state == MP4_STATE_WAIT_FTYP)
  ||  (!data->mdatPos))
  {
    ka_error_fill("Missing 'mdat' atom");
    return -1;
  }

  // Done parsing MP4, validate tracks
  for (int i = 0; i < MAX_TRACKS; i++)
    demux_mp4_validate_track(pdemux, i, &data->track[i]);

  return 0;
}

/**
 * Returns current/max position.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  pbuffer Pointer to stack buffer.
 * @param  curpos  Pointer to current position to fill.
 * @param  endpos  Pointer to max position to fill.
 */

static void demux_mp4_getPos(const ka_demux_t* pdemux, ka_buffer_t* pbuffer, ka_demux_pos_t* pos)
{
  const data_t* data = (data_t*) pdemux;

  pos->titleNr = 0;
  pos->chapterNr = 0;
  pos->curPos = ka_input_tell(data->input) - (uint64_t) ka_buffer_getByteCount(pbuffer);
  pos->endPos = ka_input_getLen(data->input);
}

/**
 * Set position.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  pos     Raw offset in input.
 *
 * @returns Offset in input aligned to nearest block
 *          or -1 if not possible to set/determine a position.
 */
static int64_t demux_mp4_setPos(ka_demux_t* pdemux, int64_t pos)
{
  data_t* data = (data_t*) pdemux;

  ka_input_seek(data->input, pos);
  return pos;
}

/**
 * Returns current/max angles.
 *
 * @param  pdemux     Pointer to demuxer data.
 * @param  pCurAngle  Pointer to current angle to fill.
 * @param  pMaxAngles Pointer to max angle to fill.
 */

static void demux_mp4_getAngle(const ka_demux_t* pdemux, ka_demux_angle_info_t* pInfo)
{
  pdemux = pdemux; // Unused

  pInfo->angleId = 1;
  pInfo->nrAngles = 1;
}

/**
 * Set current angle.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  angle   New angle.
 */
static int demux_mp4_setAngle(ka_demux_t* pdemux, uint32_t angle)
{
  pdemux = pdemux; // Unused
  angle = angle;   // Unused

  return 1;
}

/**
 * Fill in buffer.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  buffer  Buffer handle.
 * @param  pdone   Pointer to EOF to fill on exit.
 *
 * Returns number of bytes read.
 */
static int demux_mp4_fillBuffer(ka_demux_t* pdemux, ka_buffer_t* buffer, int* pdone)
{
  data_t* data = (data_t*) pdemux;

  return ka_input_buffer(data->input, buffer, pdone);
}

/**
 * Analyses part of the input buffer and put it on stack.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  stack   Pointer to stack on which to push data block.
 * @param  buf     Pointer to start of data to parse.
 * @param  end     Pointer to end of data to parse.
 *
 * @returns Pointer to remaining unused data or ka_demux_error on error.
 */
static const uint8_t* demux_avi_push(ka_demux_t* pdemux, ka_stack_t* stack
                                   , const uint8_t* buf, const uint8_t* end)
{
  data_t* data = (data_t*) pdemux;
  int stop = 0;
  int size;

  ka_stack_setProperties(stack, ps_stack_prop_allowvideo
                       | ps_stack_prop_allowaudio
                       | ps_stack_prop_notimestamp);

  while ((buf < end) && !stop)
  {
    size = end - buf;

    // data packet?
    if (data->datasize)
    {
      if (ka_stack_isFull(stack))
        break;

      if (size >= data->datasize)
      {
        // Enough data to skip
        ka_stack_idInfo(stack, data->curtyp, data->curtyp);
        ka_stack_push(stack, data->curtyp, data->hdr.demuxSection, 0, 0, 0, buf, buf + data->datasize);
        data->state = AVI_STATE_PACKETS;
        buf += data->datasize;
        size -= data->datasize;
        data->datasize = 0;
      }
      else
      {
        // Skip present data and retry later
        ka_stack_idInfo(stack, data->curtyp, data->curtyp);
        ka_stack_push(stack, data->curtyp, data->hdr.demuxSection, 0, 0, 0, buf, buf + size);
        buf += size;
        data->datasize -= size;
        break;
      }
    }

    // skip data?
    if (data->skipsize)
    {
      if (size >= data->skipsize)
      {
        // Enough data to skip
        buf += data->skipsize;
        size -= data->skipsize;
        data->skipsize = 0;
      }
      else
      {
        // Skip present data and retry later
        buf += size;
        data->skipsize -= size;
        break;
      }
    }

    if (data->state == AVI_STATE_MAIN)
    {
      uint32_t n;

      // wait till enough data in buffer
      if (size < 12)
        break;

      n = avi_uint32(buf + 4);
      if (n & 1) n += 1;
      data->curlevel->totalsize = data->curlevel->cursize = n - 4;
      data->curlevel->tag = MKTAG(buf[8], buf[9], buf[10], buf[11]);

      if (config.debug & cfg_printdemuxstats)
        ka_log(ka_log_note | ka_log_demux
             , "Chunk %c%c%c%c %c%c%c%c, size %08x"
             , buf[0], buf[1], buf[2], buf[3]
             , buf[8], buf[9], buf[10], buf[11]
             , n + 8);

      buf += 12;
      data->state = AVI_STATE_AVIHDR;
      continue;
    }
    else
    {
      uint32_t n, nr, tag = 0;

      // wait till enough data in buffer
      if (size < 8)
        break;

      nr = n = avi_uint32(buf + 4);
      if (nr & 1) nr++;

      if ((nr + 8) > data->curlevel->cursize)
      {
        if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
          ka_log(ka_log_error | ka_log_demux
               , "Size for %c%c%c%c chunk exceeds size of parent chunk"
               , buf[0], buf[1], buf[2], buf[3]);
        return ka_demux_error;
      }

      tag = MKTAG(buf[0], buf[1], buf[2], buf[3]);

      switch(tag)
      {
        case MKTAG('L', 'I', 'S', 'T'): // container
        {
          // wait till enough data in buffer
          if (size < 12)
          {
            stop = 1;
            continue;
          }

          if (n < 4)
          {
            if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
              ka_log(ka_log_error | ka_log_demux
                   , "Invalid size for LIST chunk");
            return ka_demux_error;
          }

          if (n > 4)
          {
            if (config.debug & cfg_printdemuxstats)
              ka_log(ka_log_note | ka_log_demux
                   , "%.*sChunk LIST %c%c%c%c, size %08x"
                   , data->curlevel - data->levels, "        "
                   , buf[8], buf[9], buf[10], buf[11]
                   , nr);

            data->curlevel->cursize -= 12;
            data->curlevel++;
            data->curlevel->tag = MKTAG(buf[8], buf[9], buf[10], buf[11]);
            data->curlevel->totalsize = data->curlevel->cursize = nr - 4;

            buf += 12;
            continue;
          }

          buf += 8;
          data->skipsize = nr;
        }
        break;
        case MKTAG('a', 'v', 'i', 'h'):
        {
          const int len = sizeof(data->avihdr);

          if (n < len)
          {
            if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
              ka_log(ka_log_error | ka_log_demux
                   , "Invalid size for avih chunk");
            return ka_demux_error;
          }

          // only treat the first occurence
          if (data->state == AVI_STATE_AVIHDR)
          {
            if (size < 8 + len)
            {
              stop = 1;
              continue;
            }

            log_chunk(data, buf, n, 1);

            memcpy(&data->avihdr, buf + 8, len);
            data->state = AVI_STATE_STREAMDEFS;
            data->curstrh = 0;
            data->curstrf = 0;
          }
          else
          {
            log_chunk(data, buf, n, 0);

            if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
              ka_log(ka_log_warning | ka_log_demux
                   , "Extra avih chunk ignored");
          }

          buf += 8;
          data->skipsize = nr;
        }
        break;
        case MKTAG('s', 't', 'r', 'h'):
        {
          const int len = sizeof(data->work.shdr);

          if (n < len)
          {
            if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
              ka_log(ka_log_error | ka_log_demux
                   , "Invalid size for strh chunk");
            return ka_demux_error;
          }

          // only treat as many occurences as there are streams
          if ((data->state == AVI_STATE_STREAMDEFS)
          &&  (data->curstrh < data->avihdr.Streams))
          {
            if (size < 8 + len)
            {
              stop = 1;
              continue;
            }

            log_chunk(data, buf, n, 1);

            memcpy(&data->work.shdr, buf + 8, len);
            {
              char* tag = data->work.shdr.Type;

              if (!strncmp(tag, "vids", 4))
              {
                ka_vparams_t* params = &data->tracks[data->curstrh].vparams;
                tag = data->work.shdr.Handler;

                params->typ = ka_tag_to_video(MKTAG(tag[0], tag[1], tag[2], tag[3]));
                if (data->work.shdr.Scale && data->work.shdr.Rate)
                  params->period = (int) ((double) (90000.*data->work.shdr.Scale)
                                         /(double) data->work.shdr.Rate);
                else
                  params->period = 9000;
              }
              else if (!strncmp(tag, "auds", 4))
              {
                ka_aparams_t* params = &data->streams[data->curstrh].aparams;

                params->typ = KA_ABLOCK_UNKNOWN;
              }
              else
              {
                data->streams[data->curstrh].typ = 0;

                if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
                  ka_log(ka_log_warning | ka_log_demux
                       , "strh chunk of type %c%c%c%c ignored"
                       , tag[0], tag[1], tag[2], tag[3]);
              }
            }
            data->curstrh++;
          }
          else
          {
            log_chunk(data, buf, n, 0);

            if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
              ka_log(ka_log_warning | ka_log_demux
                   , "Extra strh chunk ignored");
          }

          buf += 8;
          data->skipsize = nr;
        }
        break;
        case MKTAG('s', 't', 'r', 'f'):
        {
/*          const int len = sizeof(data->shdr[0]);

          if (n < len)
          {
            if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
              ka_log(ka_log_error | ka_log_demux
                   , "Invalid size for strh chunk");
            return ka_demux_error;
          }
*/
          // only treat the first occurence
          if ((data->state == AVI_STATE_STREAMDEFS)
          &&  (data->curstrf < data->curstrh))
          {
            if (size < n)
            {
              stop = 1;
              continue;
            }

            log_chunk(data, buf, n, 1);

            if (data->streams[data->curstrf].typ & KA_BLOCK_TYPE_AUDIO)
            {
              ka_aparams_t* params = &data->streams[data->curstrf].aparams;
              int len = sizeof(data->work.ahdr);
              char* tag = data->work.ahdr.format;

              if (n < len)
              {
                if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
                  ka_log(ka_log_error | ka_log_demux
                       , "Invalid size for strh chunk");
                return ka_demux_error;
              }

              memcpy(&data->work.ahdr, buf + 8, len);
              params->typ = ka_tag_to_audio(MKTAG(tag[0], tag[1], 0, 0));
              params->channels = data->work.ahdr.channels;
              params->samplerate = data->work.ahdr.samplerate;
              params->bitrate = 8*data->work.ahdr.bytespersec;
              params->blocksize = data->work.ahdr.blocksize;
              params->bitspersample = data->work.ahdr.bitspersample;
              if (params->typ == KA_ABLOCK_LPCM_LE)
              {
                if (params->bitspersample & 7)
                {
                  params->bitspersample &= ~7;
                  params->bitspersample += 8;
                }
                params->blocksize = 0;
                if (params->bitspersample <= 8)
                  params->typ = KA_ABLOCK_LPCM_LE_U;
              }
            }
            else if (data->streams[data->curstrf].typ & KA_BLOCK_TYPE_VIDEO)
            {
              ka_vparams_t* params = &data->streams[data->curstrf].vparams;
              int len = sizeof(data->work.vhdr);
              char* tag = data->work.vhdr.compression;

              if (n < len)
              {
                if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
                  ka_log(ka_log_error | ka_log_demux
                       , "Invalid size for strh chunk");
                return ka_demux_error;
              }

              memcpy(&data->work.vhdr, buf + 8, len);
              params->typ = ka_tag_to_video(MKTAG(tag[0], tag[1], tag[2], tag[3]));
              params->width = data->work.vhdr.width;
              params->height = data->work.vhdr.height;
//              ka_log(ka_log_error, "Size: %dx%d", params->width, params->height);
            }

            data->curstrf++;
            if (data->curstrf == data->avihdr.Streams)
            {
              data->state = AVI_STATE_PACKETS;
              data->curstrh = 0;
            }
          }
          else
          {
            log_chunk(data, buf, n, 0);

            if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
              ka_log(ka_log_warning | ka_log_demux
                   , "Extra strf chunk ignored");
          }

          buf += 8;
          data->skipsize = nr;
        }
        break;
        default:
        {
          // Data packet
          if (((buf[0] >= '0') && (buf[0] <= '9'))
          &&  ((buf[1] >= '0') && (buf[1] <= '9'))
          &&  (   ((buf[2] == 'd') && (buf[3] == 'b'))
               || ((buf[2] == 'd') && (buf[3] == 'c'))
               || ((buf[2] == 'w') && (buf[3] == 'b')))
          &&  (n > 0))
          {
            data->curstrh = (buf[0] - '0') * 10 + (buf[1] - '0');

            if (data->curstrh < data->avihdr.Streams)
            {
              log_chunk(data, buf, n, 1);

              data->curtyp = data->streams[data->curstrh].typ;

              // Data is to be stacked
              buf += 8;
              data->datasize = n;
              data->skipsize = nr - n;
            }
            else
            {
              log_chunk(data, buf, n, 0);

              if (config.debug & (cfg_printwarnings | cfg_printdemuxstats))
                ka_log(ka_log_warning | ka_log_demux
                     , "Packet with incorrect id ignored");

              buf += 8;
              data->skipsize = nr;
            }
          }
          else
          {
            log_chunk(data, buf, n, 0);

            buf += 8;
            data->skipsize = nr;
          }
        }
      }

//      assert(data->curlevel->cursize >= (8 + nr));

      data->curlevel->cursize -= 8 + nr;

      // If not enough size for at least a chunk header at this level
      // drop (possibly recursively from level)
      while ((data->curlevel->cursize < 8)
          && (data->curlevel != data->levels))
      {
        data->curlevel--;

//        assert(data->curlevel->cursize >= data->curlevel[1].totalsize);
        data->curlevel->cursize -= data->curlevel[1].totalsize;
      }
    }
  }

  return buf;
}

/**
 * Returns audio/video stream parameters.
 */
static const void* demux_mp4_params(const ka_demux_t* pdemux, uint32_t typ)
{
  const data_t* data = (data_t*) pdemux;
  int i;

  for (i = 0; i < data->avihdr.Streams; i++)
  {
    if (data->streams[i].typ == typ)
      return &data->streams[i];
  }

  return NULL;
}

static const kav_demux_t kav_demux_mp4 =
{ .name = "MP4"
, .FNdelete = delete_demux_mp4
, .FNreset  = demux_mp4_reset
, .FNclear  = demux_mp4_clear
, .FNparse  = demux_mp4_parse
, .FNgetPos = demux_mp4_getPos
, .FNsetPos = demux_mp4_setPos
, .FNgetAngle = demux_mp4_getAngle
, .FNsetAngle = demux_mp4_setAngle
, .FNfillBuffer = demux_mp4_fillBuffer
, .FNpush   = demux_mp4_push
, .FNparams = demux_mp4_params
};

/**
 * Checks for MP4 header.
 *
 * @param  ppdemux  Pointer where to fill in with address of MP4 demuxer.
 * @param  param    Pameters (error block, input source, filename, stack, work buffer).
 *
 * Returns
 *   ka_demux_select_found if MP4
 *   ka_demux_select_none  if not an MP4
 *   ka_demux_select_error if error occured
 */
ka_demux_select demux_selector_mp4(ka_demux_t** ppdemux, ka_demux_params_t* params)
{
  const uint8_t *buf, *end;
  int done;

  ka_input_seek(params->pInput, 0);
  ka_buffer_clear(params->pBuffer);

  // While not enough data
  do
  {
    if (ka_input_buffer(params->pInput, params->pBuffer, &done) < 0)
      return ka_demux_select_error;

    if (done)
      return ka_demux_select_none;

    if (!ka_buffer_getUnstackedBlock(params->pBuffer, &buf, &end))
      continue;
  }
  while ((end - buf) < 8;

  mp4_level_t* pclevel = &data->levels[data->level];
  mp4_level_t* plevel = &data->levels[data->level + 1];

  // Check for header
  mp4_getAtom(params->pInput, plevel);
  // No ftyp, exit
  if ((plevel->tag.i != TagFTYP.i)
  &&  (plevel->tag.i != TagMOOV.i)
  &&  (plevel->tag.i != TagMDAT.i))
    return ka_demux_select_none;

  uint32_t size = (uint32_t) ka_input_getLen(params->pInput);
  *ppdemux = new_demux_mp4(&kav_demux_mp4, params->pErrorBlock, size, params->pInput);
  if (*ppdemux == NULL)
    return ka_demux_select_error;

  return ka_demux_select_found;
}
