#include "dvd_ifo.h"
#include "dvd_packets.h"

#include <string.h>
#include <stdio.h>
#include "config.h"
#include "ka_mem.h"
#include "ka_log.h"

#define DVD_RAWSIZE_OFFSET 4
#define DVD_RAWSIZE_LB     4
#define PTS_PER_SECOND 90000

/**
 *  Read 32 bit Big Endian.
 */
static uint32_t read32(const uint8_t* p)
{
  uint32_t val = (p[0] << 24) + (p[1] << 16) + (p[2] << 8) + p[3];
  return val;
}

/**
 *  Read 16 bit Big Endian.
 */
static uint16_t read16(const uint8_t* p)
{
  uint16_t val = (p[0] << 8) + p[1];
  return val;
}

/**
 *  Read BCD time.
 */
static void read_time(dvd_time_t* t, const uint8_t* p)
{
  t->hour = 10 * (p[0] >> 4) + (p[0] & 0xf);
  t->min  = 10 * (p[1] >> 4) + (p[1] & 0xf);
  t->sec  = 10 * (p[2] >> 4) + (p[2] & 0xf);
  t->frameinfo = 10 * ((p[3] & 030) >> 4) + (p[3] & 0xf);
  switch(p[3] >> 6)
  {
    case 3: t->fps = 30; break;
    case 1: t->fps = 25; break;
    default: t->fps = 0; break;
  }
}

/**
 *  Copy bytes.
 */
static void readn(uint8_t* d, const uint8_t* p, int n)
{
  while (n > 0)
  {
    n--;
    *d++ = *p++;
  }
}

//--------------
// VMG: TT_SRPT

static void free_table_of_titles(dvd_table_of_titles_t** ptt)
{
  if (*ptt)
  {
    ka_mem_free(*ptt);
    *ptt = NULL;
  }
}

#define DVD_RAWSIZE_TITLE_TABLE 8
#define DVD_RAWSIZE_TITLE 12

static int read_table_of_titles(dvd_table_of_titles_t** ptt, const uint32_t offset, const uint8_t* const buf, const uint8_t* const end)
{
  const uint8_t* p = buf + offset;
  int rc = 0;

  uint32_t nr_titles = read16(p + 0x0000);
  uint32_t size = 1 + read32(p + 0x0004);

  // Must have at least one title
  if (!nr_titles || (nr_titles > 99))
  {
    rc = p - buf;
    goto error;
  }

  // Size must cover all titles and not exceed end of IFO
  if ((size < DVD_RAWSIZE_TITLE_TABLE + DVD_RAWSIZE_TITLE * nr_titles)
  ||  (p + size > end))
  {
    rc = (p + 4) - buf;
    goto error;
  }

  {
    // Allocate structure and table
    dvd_table_of_titles_t* tt = *ptt = ka_mem_calloc(sizeof(*tt) + nr_titles * sizeof(tt->titles[0]));
    if (tt == NULL)
      return -1;

    tt->nr_titles = nr_titles;
    tt->size = size;

    p += DVD_RAWSIZE_TITLE_TABLE;
    dvd_title_t *t = &tt->titles[0];
    for (int i = 0; i < nr_titles; i++, t++, p+= DVD_RAWSIZE_TITLE)
    {
      t->type = *p;
      t->nr_angles = p[1];
      t->nr_chapters = read16(p + 2);
      t->parental_management_mask = read16(p + 4);
      t->vts_nr = p[6];
      t->vts_title_nr = p[7];
      t->lb_vts_ifo_start = read32(p + 8);

      if (!t->nr_angles || (t->nr_angles > 9))
      {
        rc = (p + 1) - buf;
        goto error;
      }
      if (!t->nr_chapters || (t->nr_chapters > 4096)) // Arbitrary
      {
        rc = (p + 2) - buf;
        goto error;
      }
      if (!t->vts_nr || (t->vts_nr > 99))
      {
        rc = (p + 6) - buf;
        goto error;
      }
      if (!t->vts_title_nr || (t->vts_title_nr > 99))
      {
        rc = (p + 7) - buf;
        goto error;
      }
      if (!t->lb_vts_ifo_start)
      {
        rc = (p + 8) - buf;
        goto error;
      }
    }
  }

  return 0;

error:
  if (rc > 0)
  {
    if (config.debug & cfg_printnavstats)
      ka_log(ka_log_note | ka_log_nav
           , "TITLES TABLE        at 0x%05x L 0x%05x: invalid value at offset 0x%05x\n"
           , offset, end - buf - offset, rc - offset);
  }

  return rc;
}

//--------------
// VTS_PTT_SRPT

static void free_title_parts(dvd_table_of_title_parts_t** ptt)
{
  dvd_table_of_title_parts_t* tt = *ptt;
  *ptt = NULL;

  if (tt)
  {
    if (tt->parts)
      ka_mem_free(tt->parts);
    ka_mem_free(tt);
  }
}

#define DVD_RAWSIZE_PART_TABLE 8
#define DVD_RAWSIZE_PART 4

static int read_title_parts(dvd_table_of_title_parts_t** ptt, const uint32_t offset, const uint8_t* const buf, const uint8_t* const end)
{
  const uint8_t* p = buf + offset;
  int rc;

  uint32_t nr_titles = read16(p + 0x0000);
  uint32_t size = 1 + read32(p + 0x0004);
  uint32_t min_size = DVD_RAWSIZE_PART_TABLE + DVD_RAWSIZE_OFFSET * nr_titles;

  // Must have at least one title
  if (!nr_titles)
  {
    rc = p - buf;
    goto error;
  }

  // Size must cover all titles and not exceed end of IFO
  if ((size < min_size + DVD_RAWSIZE_PART)
  ||  (p + size > end))
  {
    rc = (p + 4) - buf;
    goto error;
  }

  {
    // Allocate structure and table
    dvd_table_of_title_parts_t* tt = *ptt = ka_mem_calloc(sizeof(*tt) + nr_titles * sizeof(tt->parts_indexes[0]));
    if (tt == NULL)
      return -1;

    tt->nr_titles = nr_titles;
    tt->size = size;

    p += DVD_RAWSIZE_PART_TABLE;
    for (int i = 0; i < nr_titles; i++, p+= DVD_RAWSIZE_OFFSET)
    {
      uint32_t poffset = read32(p);
      // Offset must fit in table
      if ((poffset < min_size)
      ||  (poffset > size - 4))
      {
        rc = p - buf;
        goto error;
      }
      tt->parts_indexes[i] = (poffset - min_size) / DVD_RAWSIZE_PART;
    }

    // size ot parts
    size -= min_size;
    tt->nr_parts = (size + DVD_RAWSIZE_PART -1 ) / DVD_RAWSIZE_PART;
    dvd_part_t* part = tt->parts = ka_mem_calloc(sizeof(*part) * tt->nr_parts);
    if (part == NULL)
      return -1;
    for (int i = 0; i < tt->nr_parts; i++, part++, p += DVD_RAWSIZE_PART)
    {
      part->program_chain_nr = read16(p);
      part->program_nr = read16(p + 2);

      if (!part->program_chain_nr)
      {
        rc = p - buf;
        goto error;
      }

      if (!part->program_nr)
      {
        rc = (p + 2) - buf;
        goto error;
      }
    }
  }

  return 0;

error:
  if (rc > 0)
  {
    if (config.debug & cfg_printnavstats)
      ka_log(ka_log_note | ka_log_nav
           , "TITLE PARTS TABLE   at 0x%05x L 0x%05x: invalid value at offset 0x%05x\n"
           , offset, end - buf- offset, rc - offset);
  }

  return rc;
}

//-------------------------------
// VMGM_PGC - VTSM_PGC - VTS_PGC

// PGC - Command Table

static void free_command_table(dvd_command_table_t** ppc)
{
  dvd_command_table_t* pc = *ppc;
  *ppc = NULL;

  if (pc)
  {
    ka_mem_free(pc);
  }
}

#define DVD_RAWSIZE_CMD 8
#define DVD_RAWSIZE_CMDTABLE 8

static int read_command_table(dvd_command_table_t** ppc, const uint32_t offset, const uint8_t* const buf, const uint8_t* const end)
{
  const uint8_t* p = buf + offset;
  int rc;
  uint16_t nr_pre_cmds = read16(p);
  uint16_t nr_post_cmds = read16(p + 2);
  uint16_t nr_cell_cmds = read16(p + 4);
  uint32_t size = 1 + read16(p + 6);

  if ((size < DVD_RAWSIZE_CMDTABLE + DVD_RAWSIZE_CMD * (nr_pre_cmds + nr_post_cmds + nr_cell_cmds))
  ||  (p + size > end))
  {
    rc = (p + 6) - buf;
    goto error;
  }

  {
    dvd_command_table_t* pc = *ppc = ka_mem_calloc(sizeof(*pc) + (nr_pre_cmds + nr_post_cmds + nr_cell_cmds) * sizeof(pc->commands[0]));
    if (pc == NULL)
      return -1;

    pc->nr_pre_cmds = nr_pre_cmds;
    pc->nr_post_cmds = nr_post_cmds;
    pc->nr_cell_cmds = nr_cell_cmds;
    pc->table_size = size;

    memcpy(pc->commands, p + 8, DVD_RAWSIZE_CMD * (nr_pre_cmds + nr_post_cmds + nr_cell_cmds));
  }

  return 0;

error:
  if (rc > 0)
  {
    if (config.debug & cfg_printnavstats)
      ka_log(ka_log_note | ka_log_nav
           , "COMMAND TABLE       at 0x%05x L 0x%05x: invalid value at offset 0x%05x\n"
           , offset, end - buf - offset, rc - offset);
  }

  return rc;
}

// PGC - Program Chain

static void free_pgc(dvd_pgc_t** ppgc)
{
  dvd_pgc_t* pgc = *ppgc;
  *ppgc = NULL;

  if (pgc)
  {
    free_command_table(&pgc->commands);
    if (pgc->program_cellnrs) ka_mem_free(pgc->program_cellnrs);
    if (pgc->cell_playbacks) ka_mem_free(pgc->cell_playbacks);
    if (pgc->cells) ka_mem_free(pgc->cells);
    ka_mem_free(pgc);
  }
}

#define DVD_RAWSIZE_CELL_PLAYBACK 24
#define DVD_RAWSIZE_CELL 4
#define DVD_RAWSIZE_PGC 0xEC

static int read_pgc(dvd_pgc_t** ppgc, const uint32_t offset, const uint8_t* const buf, const uint8_t* const end)
{
  const uint8_t* p = buf + offset;
  int rc;

  dvd_pgc_t* pgc = *ppgc = ka_mem_calloc(sizeof(*pgc));
  if (pgc == NULL)
    return -1;

  pgc->nr_programs = p[0x02]; // may be 0
  pgc->nr_cells = p[0x03]; // may be 0
  read_time(&pgc->time, p + 0x04);
  pgc->prohibited_user_ops = read32(p + 0x08);
  for (int i = 0; i < 8; i++)
  {
    pgc->audio_stream[i] = p[0x0C + 2*i];
  }
  for (int i = 0; i < 32; i++)
  {
    pgc->subtitle_stream[i][0] = p[ + 0x1C + 4*i];
    pgc->subtitle_stream[i][1] = p[ + 0x1C + 4*i + 1];
    pgc->subtitle_stream[i][2] = p[ + 0x1C + 4*i + 2];
    pgc->subtitle_stream[i][3] = p[ + 0x1C + 4*i + 3];
  }
  pgc->next_pgcn = read16(p + 0x9C);
  pgc->prev_pgcn = read16(p + 0x9E);
  pgc->goup_pgcn = read16(p + 0xA0);
  pgc->playback_mode = p[0xA2];
  pgc->still_time = p[0xA3];
  memcpy(pgc->color_lookup, p + 0xA4, 16*4);
  pgc->offset_commands = read16(p + 0xE4);
  pgc->offset_program_map = read16(p + 0xE6);
  pgc->offset_cell_playback_table = read16(p + 0xE8);
  pgc->offset_cell_position_table = read16(p + 0xEA);

  // check valid offset to commands
  if (pgc->offset_commands)
  {
    if ((pgc->offset_commands < DVD_RAWSIZE_PGC)
    ||  (pgc->offset_commands > ((end - p) - DVD_RAWSIZE_CMDTABLE)))
    {
      rc = (p + 0xE4) - buf;
      goto error;
    }

    read_command_table(&pgc->commands, offset + pgc->offset_commands, buf, end);
  }

  if (pgc->nr_cells)
  {
    // check valid offset to programmap
    if ((pgc->offset_program_map < DVD_RAWSIZE_PGC)
    ||  (pgc->offset_program_map > ((end - p) - pgc->nr_cells)))
    {
      rc = (p + 0xE6) - buf;
      goto error;
    }
    // check valid offset to cell playback table
    if ((pgc->offset_cell_playback_table < DVD_RAWSIZE_PGC)
    ||  (pgc->offset_cell_playback_table > ((end - p) - pgc->nr_cells * DVD_RAWSIZE_CELL_PLAYBACK)))
    {
      rc = (p + 0xE8) - buf;
      goto error;
    }
    // check valid offset to cell position table
    if ((pgc->offset_cell_position_table < DVD_RAWSIZE_PGC)
    ||  (pgc->offset_cell_position_table > ((end - p) - pgc->nr_cells * DVD_RAWSIZE_CELL)))
    {
      rc = (p + 0xEA) - buf;
      goto error;
    }

    // allocate cell tables
    pgc->program_cellnrs = ka_mem_alloc(pgc->nr_programs * sizeof(*pgc->program_cellnrs));
    if (!pgc->program_cellnrs)
      return -1;
    pgc->cell_playbacks = ka_mem_alloc(pgc->nr_cells * sizeof(*pgc->cell_playbacks));
    if (!pgc->program_cellnrs)
      return -1;
    pgc->cells = ka_mem_alloc(pgc->nr_cells * sizeof(*pgc->cells));
    if (!pgc->program_cellnrs)
      return -1;

    memcpy(pgc->program_cellnrs, p + pgc->offset_program_map, pgc->nr_programs);

    // padding ?
    if (((pgc->nr_programs & 1) == 0)
    &&  (pgc->program_cellnrs[pgc->nr_programs - 1] == 0))
      pgc->nr_programs--;

    // program_cellnrs should start at 1 and be increasing
    if (pgc->program_cellnrs[0] != 1)
    {
      rc = (p + pgc->offset_program_map + 0) - buf;
      goto error;
    }

    int min = 1;
    for(int i = 1; i < pgc->nr_programs; i++)
    {
      if ((pgc->program_cellnrs[i] <= min) || (pgc->program_cellnrs[i] > pgc->nr_cells))
      {
        rc = (p + pgc->offset_program_map + i) - buf;
        goto error;
      }
      min = pgc->program_cellnrs[i];
    }

    {
      const uint8_t* pp = p + pgc->offset_cell_playback_table;
      dvd_cell_playback_t* cp = pgc->cell_playbacks;
      uint32_t rel_time = 0;
      uint32_t rel_size = 0;
      uint32_t lb_start = 0, lb_last = 0;

      for (int i = 0; i < pgc->nr_cells; i++, cp++, pp += DVD_RAWSIZE_CELL_PLAYBACK)
      {
        cp->category = read16(pp);
        cp->still_time = pp[2];
        cp->cell_command_nr = pp[3];
        read_time(&cp->time, pp + 4);
        cp->lb_first_vobu_start = read32(pp + 8);
        cp->lb_first_ilvu_last = read32(pp + 12);
        cp->lb_last_vobu_start = read32(pp + 16);
        cp->lb_last_vobu_last = read32(pp + 20);

        cp->pgc_rel_time = rel_time;
        cp->pgc_rel_lb_size = rel_size;

        if (cp->lb_first_ilvu_last && (cp->lb_first_ilvu_last < cp->lb_first_vobu_start))
        {
          rc = (pp + 12) - buf;
          goto error;
        }
        if (cp->lb_last_vobu_start < cp->lb_first_vobu_start)
        {
          rc = (pp + 16) - buf;
          goto error;
        }
        if (cp->lb_last_vobu_last < cp->lb_last_vobu_start)
        {
          rc = (pp + 20) - buf;
          goto error;
        }

        switch (cp->category & DVD_CELL_TYPE_MASK)
        {
          case DVD_CELL_TYPE_NORMAL:
          {
            rel_size += 1 + cp->lb_last_vobu_last - cp->lb_first_vobu_start;
            rel_time += dvd_time_toRtc(&cp->time);
            if (cp->still_time < 0xff)
              rel_time += PTS_PER_SECOND * cp->still_time;
          }
          break;
          case DVD_CELL_TYPE_FIRST_OF_ANGLE:
          {
            lb_start = cp->lb_first_vobu_start;
            lb_last = cp->lb_last_vobu_last;
          }
          break;
          case DVD_CELL_TYPE_MIDDLE_OF_ANGLE:
          {
            if (lb_start > cp->lb_first_vobu_start)
              lb_start = cp->lb_first_vobu_start;
            if (lb_last < cp->lb_last_vobu_last)
              lb_last = cp->lb_last_vobu_last;
          }
          break;
          case DVD_CELL_TYPE_LAST_OF_ANGLE:
          {
            if (lb_start > cp->lb_first_vobu_start)
              lb_start = cp->lb_first_vobu_start;
            if (lb_last < cp->lb_last_vobu_last)
              lb_last = cp->lb_last_vobu_last;

            rel_size += 1 + lb_last - lb_start;
            rel_time += dvd_time_toRtc(&cp->time);
            if (cp->still_time < 0xff)
              rel_time += PTS_PER_SECOND * cp->still_time;
          }
          break;
        }
      }
      if (pgc->still_time < 0xff)
        rel_time += PTS_PER_SECOND * pgc->still_time;
      pgc->total_time = rel_time;
      pgc->total_lb_size = rel_size;
    }

    {
      const uint8_t* pp = p + pgc->offset_cell_position_table;
      dvd_cell_t* cc = pgc->cells;
      for (int i = 0; i < pgc->nr_cells; i++, cc++, pp += DVD_RAWSIZE_CELL)
      {
        cc->vob_id = read16(pp);
        cc->cell_id = pp[3];

        if (!cc->vob_id)
        {
          rc = pp - buf;
          goto error;
        }
        if (!cc->cell_id)
        {
          rc = (pp + 3) - buf;
          goto error;
        }
      }
    }
  }

  return 0;

error:
  if (rc > 0)
  {
    if (config.debug & cfg_printnavstats)
      ka_log(ka_log_note | ka_log_nav
           , "PGC                 at 0x%05x L 0x%05x: invalid value at offset 0x%05x\n"
           , offset, end - buf - offset, rc - offset);
  }

  return rc;
}

// VMGM_LU - VTSM_LU

#define DVD_RAWSIZE_PGC_LU_TABLE 8
#define DVD_RAWSIZE_PGC_LU 8

static void free_pgc_lu_table(dvd_pgc_lu_table_t** plus)
{
  dvd_pgc_lu_table_t* lus = *plus;
  *plus = NULL;

  if (lus)
  {
    dvd_pgc_lu_t* lu = &lus->pgc_lus[0];
    for (int i = 0; i < lus->nr_pgc_lus; i++, lu++)
    {
      free_pgc(&lu->pgc);
    }

    ka_mem_free(lus);
  }
}

static int read_pgc_lu_table(dvd_pgc_lu_table_t** plus, const uint32_t offset, const uint8_t* const buf, const uint8_t* const end)
{
  const uint8_t* p = buf + offset;
  int rc;

  uint32_t nr_pgc_lus = read16(p + 0x0000);
  uint32_t size = 1 + read32(p + 0x0004);
  uint32_t min_size = DVD_RAWSIZE_PGC_LU_TABLE + DVD_RAWSIZE_PGC_LU * nr_pgc_lus;
  const uint8_t* table_end = p + size;

  // Must have at least one title
  if (!nr_pgc_lus)
  {
    rc = p - buf;
    goto error;
  }

  // Size must cover all titles and not exceed end of IFO
  if ((size < min_size + DVD_RAWSIZE_PGC)
  ||  (p + size > end))
  {
    rc = (p + 4) - buf;
    goto error;
  }

  {
    // Allocate structure and table
    dvd_pgc_lu_table_t* lus = * plus = ka_mem_calloc(sizeof(*lus) + nr_pgc_lus * sizeof(lus->pgc_lus[0]));
    if (lus == NULL)
      return -1;

    lus->nr_pgc_lus = nr_pgc_lus;
    lus->table_size = size;

    p += DVD_RAWSIZE_PGC_LU_TABLE;
    dvd_pgc_lu_t* lu = &lus->pgc_lus[0];
    for (int i = 0; i < nr_pgc_lus; i++, lu++, p+= DVD_RAWSIZE_PGC_LU)
    {
      lu->category = p[0];
      lu->parental_mask = read16(p + 2);
      lu->offset_pgc = read32(p + 4);
      // Offset must fit in table
      if ((lu->offset_pgc < min_size)
      ||  (lu->offset_pgc > size - DVD_RAWSIZE_PGC))
      {
        rc = (p + 4) - buf;
        goto error;
      }

      rc = read_pgc(&lu->pgc, offset + lu->offset_pgc, buf, table_end);
      if (rc)
        goto error;
    }
  }

  return 0;

error:
  if (rc > 0)
  {
    if (config.debug & cfg_printnavstats)
      ka_log(ka_log_note | ka_log_nav
           , "PGC_LU TABLE        at 0x%05x L 0x%05x: invalid value at offset 0x%05x\n"
           , offset, end - buf - offset, rc - offset);
  }

  return rc;
}

// VMGM_PGCI_UT - CTSM_PGCI_UT

#define DVD_RAWSIZE_LANGUAGE_TABLE 8
#define DVD_RAWSIZE_LANGUAGE 8

static void free_table_of_menu_languages(dvd_table_of_menu_languages_t** pmls)
{
  dvd_table_of_menu_languages_t* mls = *pmls;
  *pmls = NULL;

  if (mls)
  {
    dvd_language_t* l = &mls->languages[0];
    for (int i = 0; i < mls->nr_languages; i++, l++)
    {
      free_pgc_lu_table(&l->lut);
    }

    ka_mem_free(mls);
  }
}

static int read_table_of_menu_languages(dvd_table_of_menu_languages_t** pmls, const uint32_t offset, const uint8_t* const buf, const uint8_t* const end)
{
  const uint8_t* p = buf + offset;
  int rc;

  uint32_t nr_languages = read16(p + 0x0000);
  uint32_t size = 1 + read32(p + 0x0004);
  uint32_t min_size = DVD_RAWSIZE_LANGUAGE_TABLE + DVD_RAWSIZE_LANGUAGE * nr_languages;
  const uint8_t* table_end = p + size;

  // Must have at least one title
  if (!nr_languages)
  {
    rc = p - buf;
    goto error;
  }

  // Size must cover all titles and not exceed end of IFO
  if ((size < min_size + DVD_RAWSIZE_PGC_LU_TABLE)
  ||  (p + size > end))
  {
    rc = (p + 4) - buf;
    goto error;
  }

  {
    // Allocate structure and table
    dvd_table_of_menu_languages_t* mls = *pmls = ka_mem_calloc(sizeof(*mls) + nr_languages * sizeof(mls->languages[0]));
    if (mls == NULL)
      return -1;

    mls->nr_languages = nr_languages;
    mls->table_size = size;

    p += DVD_RAWSIZE_LANGUAGE_TABLE;
    dvd_language_t* l = &mls->languages[0];
    for (int i = 0; i < nr_languages; i++, l++, p+= DVD_RAWSIZE_LANGUAGE)
    {
      memcpy(l->iso639code, p, 2);
      l->menu_flags = p[3];
      l->offset_lu = read32(p + 4);
      // Offset must fit in table
      if ((l->offset_lu < min_size)
      ||  (l->offset_lu > size - DVD_RAWSIZE_PGC_LU_TABLE))
      {
        rc = (p + 4) - buf;
        goto error;
      }

      rc = read_pgc_lu_table(&l->lut, offset + l->offset_lu, buf, table_end);
      if (rc)
        goto error;
    }
  }

  return 0;

error:
  if (rc > 0)
  {
    if (config.debug & cfg_printnavstats)
      ka_log(ka_log_note | ka_log_nav
           , "LANGUAGES TABLE     at 0x%05x L 0x%05x: invalid value at offset 0x%05x\n"
           , offset, end - buf - offset, rc - offset);
  }

  return rc;
}

//--------
//

static void free_parental_management_masks(dvd_parental_management_masks_t** pp)
{
  dvd_parental_management_masks_t* p = *pp;
  *pp = NULL;

  if (p)
  {
    ka_mem_free(p);
  }
}

static int read_parental_management_masks(dvd_parental_management_masks_t** pp, const uint32_t offset, const uint8_t* const buf, const uint8_t* const end)
{
  const uint8_t* p = buf + offset;
  int rc;

  return 0;
p = p;pp = pp; goto error;

error:
  if (rc > 0)
  {
    if (config.debug & cfg_printnavstats)
      ka_log(ka_log_note | ka_log_nav
           , "PARENTAL MASKS      at 0x%05x L 0x%05x: invalid value at offset 0x%05x\n"
           , offset, end - buf - offset, rc - offset);
  }

  return rc;
}

//--------
//

static void free_vts_attributes(dvd_vts_attributes_t** pp)
{
  dvd_vts_attributes_t* p = *pp;
  *pp = NULL;

  if (p)
  {
    ka_mem_free(p);
  }
}

static int read_vts_attributes(dvd_vts_attributes_t** pp, const uint32_t offset, const uint8_t* const buf, const uint8_t* const end)
{
  const uint8_t* p = buf + offset;
  int rc;

  return 0;
p = p;pp = pp; goto error;

error:
  if (rc > 0)
  {
    if (config.debug & cfg_printnavstats)
      ka_log(ka_log_note | ka_log_nav
           , "VTS ATTRIBUTES      at 0x%05x L 0x%05x: invalid value at offset 0x%05x\n"
           , offset, end - buf - offset, rc - offset);
  }

  return rc;
}

//--------
//

static void free_text_data(dvd_text_data_t** pp)
{
  dvd_text_data_t* p = *pp;
  *pp = NULL;

  if (p)
  {
    ka_mem_free(p);
  }
}

static int read_text_data(dvd_text_data_t** pp, const uint32_t offset, const uint8_t* const buf, const uint8_t* const end)
{
  const uint8_t* p = buf + offset;
  int rc;

  return 0;
p = p;pp = pp; goto error;

error:
  if (rc > 0)
  {
    if (config.debug & cfg_printnavstats)
      ka_log(ka_log_note | ka_log_nav
           , "TEXT DATA           at 0x%05x L 0x%05x: invalid value at offset 0x%05x\n"
           , offset, end - buf - offset, rc - offset);
  }

  return rc;
}

//-------------------------------------
// VMGM_C_ADT - VTSM_C_ADT - VTS_C_ADT

static void free_cell_address_table(dvd_cell_address_table_t** pp)
{
  dvd_cell_address_table_t* p = *pp;
  *pp = NULL;

  if (p)
  {
    ka_mem_free(p);
  }
}

#define DVD_RAWSIZE_CELL_ADDRESS 12
#define DVD_RAWSIZE_CELL_ADDRESS_TABLE 8

static int read_cell_address_table(dvd_cell_address_table_t** pcas, const uint32_t offset, const uint8_t* const buf, const uint8_t* const end)
{
  const uint8_t* p = buf + offset;
  int rc;

  uint32_t nr_cells = read16(p + 0x0000); // 0 si allowed
  uint32_t size = 1 + read32(p + 0x0004);
  uint32_t min_size = DVD_RAWSIZE_CELL_ADDRESS_TABLE + DVD_RAWSIZE_CELL_ADDRESS * nr_cells;

  // Size must cover the table and not exceed end of IFO
  if ((size < min_size)
  ||  (p + size > end))
  {
    rc = (p + 4) - buf;
    goto error;
  }

  // Found buggy case Escaflowne01
  nr_cells = (size - DVD_RAWSIZE_CELL_ADDRESS_TABLE) / DVD_RAWSIZE_CELL_ADDRESS;

  {
    // Allocate structure and table
    dvd_cell_address_table_t* cas = *pcas = ka_mem_calloc(sizeof(*cas) + nr_cells * sizeof(cas->cells[0]));
    if (cas == NULL)
      return -1;

    cas->nr_cells = nr_cells;
    cas->table_size = size;

    p += DVD_RAWSIZE_CELL_ADDRESS_TABLE;
    dvd_cell_address_t* ca = &cas->cells[0];
    for (int i = 0; i < nr_cells; i++, ca++, p+= DVD_RAWSIZE_CELL_ADDRESS)
    {
      ca->vob_id = read16(p);
      ca->cell_id = p[2];
      ca->lb_vob_start = read32(p + 4);
      ca->lb_vob_last = read32(p + 8);

      if (!ca->vob_id)
      {
        rc = p - buf;
        goto error;
      }
      if (!ca->cell_id)
      {
        rc = (p + 2) - buf;
        goto error;
      }
      if (ca->lb_vob_last < ca->lb_vob_start)
      {
        // rc = (p + 8) - buf;
        // goto error;
        // World War Z DVD
        ca->lb_vob_last = ca->lb_vob_start;
      }
    }
  }

  return 0;

error:
  if (rc > 0)
  {
    if (config.debug & cfg_printnavstats)
      ka_log(ka_log_note | ka_log_nav
           , "CELL ADDRESS TABLE  at 0x%05x L 0x%05x: invalid value at offset 0x%05x\n"
           , offset, end - buf - offset, rc - offset);
  }

  return rc;
}

//----------------------------------------------------
// VMGM_VOBU_ADMAP - VTSM_VOBU_ADMAP - VTS_VOBU_ADMAP

static void free_vobu_address_map(dvd_vobu_address_map_t** pp)
{
  dvd_vobu_address_map_t* p = *pp;
  *pp = NULL;

  if (p)
  {
    ka_mem_free(p);
  }
}

#define DVD_RAWSIZE_VOBU_ADDRESS_MAP 4

static int read_vobu_address_map(dvd_vobu_address_map_t** pvap, const uint32_t offset, const uint8_t* const buf, const uint8_t* const end)
{
  const uint8_t* p = buf + offset;
  int rc;

  uint32_t size = 1 + read32(p);
  uint32_t nr_vobus = (size - DVD_RAWSIZE_VOBU_ADDRESS_MAP) / DVD_RAWSIZE_LB;
  uint32_t min_size = DVD_RAWSIZE_VOBU_ADDRESS_MAP + DVD_RAWSIZE_LB * nr_vobus;

  // Size must cover the table and not exceed end of IFO
  if ((size < min_size)
  ||  (p + size > end))
  {
    rc = p - buf;
    goto error;
  }

  {
    // Allocate structure and table
    dvd_vobu_address_map_t* vap = *pvap = ka_mem_calloc(sizeof(*vap) + nr_vobus * sizeof(vap->lb_vobu_start[0]));
    if (vap == NULL)
      return -1;

    vap->nr_vobus = nr_vobus;

    p += DVD_RAWSIZE_VOBU_ADDRESS_MAP;
    uint32_t* lb = &vap->lb_vobu_start[0];
    uint32_t prev = 0;
    for (int i = 0; i < nr_vobus; i++, lb++, p+= DVD_RAWSIZE_LB)
    {
      *lb = read32(p);
      if (i && (*lb <= prev))
      {
        rc = p - buf;
        goto error;
      }
      prev = *lb;
    }
  }

  return 0;

error:
  if (rc > 0)
  {
    if (config.debug & cfg_printnavstats)
      ka_log(ka_log_note | ka_log_nav
           , "VOBU ADDRESS MAP    at 0x%05x L 0x%05x: invalid value at offset 0x%05x\n"
           , offset, end - buf - offset, rc - offset);
  }

  return rc;
}

//-------------
// VTMS_TMAPTI

#define DVD_RAWSIZE_TIMEMAP 8

static void free_table_of_timemaps(dvd_table_of_timemaps_t** pp)
{
  dvd_table_of_timemaps_t* p = *pp;
  *pp = NULL;

  if (p)
  {
    ka_mem_free(p);
  }
}

static int read_table_of_timemaps(dvd_table_of_timemaps_t** pp, const uint32_t offset, const uint8_t* const buf, const uint8_t* const end)
{
  const uint8_t* p = buf + offset;
  int rc;

  return 0;
p = p;pp = pp; goto error;

error:
  if (rc > 0)
  {
    if (config.debug & cfg_printnavstats)
      ka_log(ka_log_note | ka_log_nav
           , "TIMEMAP             at 0x%05x L 0x%05x: invalid value at offset 0x%05x\n"
           , offset, end - buf - offset, rc - offset);
  }

  return rc;
}

//---------
// VMG_MAT

#define DVD_RAWSIZE_MAT 0x400

static int read_vmg_mat(dvd_vmg_ifo_t* vmg, const uint8_t* p)
{
  int rc = 0;
  uint32_t mat_lb_size;

  if (strncmp("DVDVIDEO-VMG", (char*) p, 12))
  {
    rc = 1;
    goto error;
  }

  vmg->set_lba = 0;
  vmg->set_lb_size = 1 + read32(p + 0x000C);
  vmg->ifo_lb_size = 1 + read32(p + 0x001C);
  vmg->version    = read16(p + 0x0020);
  vmg->category   = read32(p + 0x0022);
  vmg->nr_volumes = read16(p + 0x0026);
  vmg->cur_volume = read16(p + 0x0028);
  vmg->side_id    = p[0x002A];
  vmg->nr_vts     = read16(p + 0x003E);
  readn((uint8_t*) vmg->provider, p + 0x0040, 32);
  readn((uint8_t*) vmg->vmg_pos, p + 0x0060, 8);
  vmg->mat_size = 1 + read32(p + 0x0080);
  vmg->offset_first_play_program_chain = read32(p + 0x0084);

  vmg->lb_offset_menu_vob = read32(p + 0x00C0);
  vmg->lb_offset_table_of_titles = read32(p + 0x00C4);
  vmg->lb_offset_menu_languages_table = read32(p + 0x00C8);
  vmg->lb_offset_parental_management_masks = read32(p + 0x00CC);
  vmg->lb_offset_vts_attributes = read32(p + 0x00D0);
  vmg->lb_offset_text_data = read32(p + 0x00D4);
  vmg->lb_offset_menu_cell_address_table = read32(p + 0x00D8);
  vmg->lb_offset_menu_vobu_address_map = read32(p + 0x00DC);

  vmg->menu_attributes.video_attributes = read16(p + 0x0100);
  vmg->menu_attributes.nr_audio_streams = read16(p + 0x0102);
  readn((uint8_t*) vmg->menu_attributes.audio_attributes, p + 0x0104, 8);
  vmg->menu_attributes.nr_subtitle_streams = read16(p + 0x0154);
  readn((uint8_t*) vmg->menu_attributes.subtitle_attributes, p + 0x0156, 6);

  //
  // Some consistency checks
  //

  // set size is at least IFO + BUP size
  if (vmg->set_lb_size < 2*vmg->ifo_lb_size)
  {
    rc = 0x000C;
    goto error;
  }

  // mat must not exceed IFO size
  mat_lb_size = (vmg->mat_size + DVD_LB_SIZE - 1) / DVD_LB_SIZE;
  if ((vmg->mat_size < DVD_RAWSIZE_MAT)
  ||  (mat_lb_size > vmg->ifo_lb_size))
  {
    rc = 0x0080;
    goto error;
  }

  // first play must be located between mat header and end of mat
//  if (vmg->offset_first_play_program_chain)
  {
    if ((vmg->offset_first_play_program_chain < DVD_RAWSIZE_MAT)
    ||  (vmg->offset_first_play_program_chain >= vmg->mat_size))
    {
      rc = 0x0084;
      goto error;
    }
  }

  // memu_vob must be located between IFO and BUP
  if (vmg->lb_offset_menu_vob)
  {
    if ((vmg->lb_offset_menu_vob < vmg->ifo_lb_size)
    ||  (vmg->lb_offset_menu_vob > vmg->set_lb_size - vmg->ifo_lb_size))
    {
      rc = 0x00C0;
      goto error;
    }
  }

  // other tables must be located between mat and end of IFO

  //  table_of_titles is mandatory
  {
    if ((vmg->lb_offset_table_of_titles >= vmg->ifo_lb_size)
    ||  (vmg->lb_offset_table_of_titles < mat_lb_size))
    {
      rc = 0x00C4;
      goto error;
    }
  }

  if (vmg->lb_offset_menu_languages_table)
  {
    if ((vmg->lb_offset_menu_languages_table >= vmg->ifo_lb_size)
    ||  (vmg->lb_offset_menu_languages_table < mat_lb_size))
    {
      rc = 0x00C8;
      goto error;
    }
  }

  if (vmg->lb_offset_parental_management_masks)
  {
    if ((vmg->lb_offset_parental_management_masks >= vmg->ifo_lb_size)
    ||  (vmg->lb_offset_parental_management_masks < mat_lb_size))
    {
      rc = 0x00CC;
      goto error;
    }
  }

//  if (vmg->lb_offset_vts_attributes)
  {
    if ((vmg->lb_offset_vts_attributes >= vmg->ifo_lb_size)
    ||  (vmg->lb_offset_vts_attributes < mat_lb_size))
    {
      rc = 0x00D0;
      goto error;
    }
  }

  if (vmg->lb_offset_text_data)
  {
    if ((vmg->lb_offset_text_data >= vmg->ifo_lb_size)
    ||  (vmg->lb_offset_text_data < mat_lb_size))
    {
      rc = 0x00D4;
      goto error;
    }
  }

  if (vmg->lb_offset_menu_cell_address_table)
  {
    if ((vmg->lb_offset_menu_cell_address_table >= vmg->ifo_lb_size)
    ||  (vmg->lb_offset_menu_cell_address_table < mat_lb_size))
    {
      rc = 0x00D8;
      goto error;
    }
  }

  if (vmg->lb_offset_menu_vobu_address_map)
  {
    if ((vmg->lb_offset_menu_vobu_address_map >= vmg->ifo_lb_size)
    ||  (vmg->lb_offset_menu_vobu_address_map < mat_lb_size))
    {
      rc = 0x00DC;
      goto error;
    }
  }

  return 0;

error:
  if (config.debug & cfg_printnavstats)
    ka_log(ka_log_note | ka_log_nav, "VMG_MAT, invalid value at offset 0x%05x\n", rc);

  return rc;
}

void dvd_free_vmg_info(dvd_vmg_ifo_t** pvmg)
{
  dvd_vmg_ifo_t* vmg = *pvmg;
  *pvmg = NULL;

  if (vmg)
  {
    free_pgc(&vmg->first_play_pgc);
    free_table_of_titles(&vmg->table_of_titles);
    free_table_of_menu_languages(&vmg->menu_languages);
    free_parental_management_masks(&vmg->parental_management_masks);
    free_vts_attributes(&vmg->vts_attributes);
    free_text_data(&vmg->text_data);
    free_cell_address_table(&vmg->menu_cells);
    free_vobu_address_map(&vmg->menu_vobus);
    ka_mem_free(vmg);
  }
}

int dvd_read_vmg_ifo(ka_error_t* pErrorBlock, dvd_vmg_ifo_t** pvmg, const uint8_t* buf, const uint8_t* end)
{
  dvd_vmg_ifo_t* vmg = ka_mem_calloc(sizeof(*vmg));
  if (vmg == NULL)
  {
    ka_error_fill(pErrorBlock, ka_error_nomem);
    return -1;
  }

  *pvmg = vmg;

  int rc = 0;

  rc = read_vmg_mat(vmg, buf);
  if (!rc && vmg->offset_first_play_program_chain)
    rc = read_pgc(&vmg->first_play_pgc, vmg->offset_first_play_program_chain, buf, end);
  if (!rc && vmg->lb_offset_table_of_titles)
    rc = read_table_of_titles(&vmg->table_of_titles, DVD_LB_SIZE*vmg->lb_offset_table_of_titles, buf, end);
  if (!rc && vmg->lb_offset_menu_languages_table)
    rc = read_table_of_menu_languages(&vmg->menu_languages, DVD_LB_SIZE*vmg->lb_offset_menu_languages_table, buf, end);
  if (!rc && vmg->lb_offset_parental_management_masks)
    rc = read_parental_management_masks(&vmg->parental_management_masks, DVD_LB_SIZE*vmg->lb_offset_parental_management_masks, buf, end);
  if (!rc && vmg->lb_offset_vts_attributes)
    rc = read_vts_attributes(&vmg->vts_attributes, DVD_LB_SIZE*vmg->lb_offset_vts_attributes, buf, end);
  if (!rc && vmg->lb_offset_text_data)
    rc = read_text_data(&vmg->text_data, DVD_LB_SIZE*vmg->lb_offset_text_data, buf, end);
  if (!rc && vmg->lb_offset_menu_cell_address_table)
    rc = read_cell_address_table(&vmg->menu_cells, DVD_LB_SIZE*vmg->lb_offset_menu_cell_address_table, buf, end);
  if (!rc && vmg->lb_offset_menu_vobu_address_map)
    rc = read_vobu_address_map(&vmg->menu_vobus, DVD_LB_SIZE*vmg->lb_offset_menu_vobu_address_map, buf, end);

  if (!rc) return 0;

  if (rc < 0)
  {
    ka_error_fill(pErrorBlock, ka_error_nomem);
    return -1;
  }

  sprintf(&pErrorBlock->errmess[0], "VMG invalid value at offset 0x%05x", rc);
  return -1;
}

//---------
// VTS_MAT

static int read_vts_mat(dvd_vts_ifo_t* vts, const uint8_t* p)
{
  int rc = 0;
  uint32_t mat_lb_size;

  if (strncmp("DVDVIDEO-VTS", (char*) p, 12))
  {
    rc = 1;
    goto error;
  }

  vts->set_lba = 0;
  vts->set_lb_size = 1 + read32(p + 0x000C);
  vts->ifo_lb_size = 1 + read32(p + 0x001C);
  vts->version    = read16(p + 0x0020);
  vts->category   = read32(p + 0x0022);
  vts->mat_size = 1 + read32(p + 0x0080);

  vts->lb_offset_menu_vob = read32(p + 0x00C0);
  vts->lb_offset_title_vob = read32(p + 0x00C4);
  vts->lb_offset_title_parts_table = read32(p + 0x00C8);
  vts->lb_offset_title_program_chains_table = read32(p + 0x00CC);
  vts->lb_offset_menu_languages_table = read32(p + 0x00D0);
  vts->lb_offset_time_map = read32(p + 0x00D4);
  vts->lb_offset_menu_cell_address_table = read32(p + 0x00D8);
  vts->lb_offset_menu_vobu_address_map = read32(p + 0x00DC);
  vts->lb_offset_title_cell_address_table = read32(p + 0x00E0);
  vts->lb_offset_title_vobu_address_map = read32(p + 0x00E4);

  vts->menu_attributes.video_attributes = read16(p + 0x0100);
  vts->menu_attributes.nr_audio_streams = read16(p + 0x0102);
  readn((uint8_t*) vts->menu_attributes.audio_attributes, p + 0x0104, 8);
  vts->menu_attributes.nr_subtitle_streams = read16(p + 0x0154);
  readn((uint8_t*) vts->menu_attributes.subtitle_attributes, p + 0x0156, 6);

  vts->title_attributes.video_attributes = read16(p + 0x0200);
  vts->title_attributes.nr_audio_streams = read16(p + 0x0202);
  readn((uint8_t*) vts->title_attributes.audio_attributes, p + 0x0204, 8*8);
  vts->title_attributes.nr_subtitle_streams = read16(p + 0x0254);
  readn((uint8_t*) vts->title_attributes.subtitle_attributes, p + 0x0256, 32*6);
  readn((uint8_t*) vts->title_attributes.multichannel_extension, p + 0x318, 8*24);

  //
  // Some consistency checks
  //

  // set size is at least IFO + BUP size
  if (vts->set_lb_size < 2*vts->ifo_lb_size)
  {
    rc = 0x000C;
    goto error;
  }

  // mat must not exceed IFO size
  mat_lb_size = (vts->mat_size + DVD_LB_SIZE - 1) / DVD_LB_SIZE;
  if ((vts->mat_size < DVD_RAWSIZE_MAT)
  ||  (mat_lb_size > vts->ifo_lb_size))
  {
    rc = 0x0080;
    goto error;
  }

  // memu_vob must be located between IFO and BUP
  if (vts->lb_offset_menu_vob)
  {
    if ((vts->lb_offset_menu_vob < vts->ifo_lb_size)
    ||  (vts->lb_offset_menu_vob > vts->set_lb_size - vts->ifo_lb_size))
    {
      rc = 0x00C0;
      goto error;
    }
  }

  // title_vob must be located between IFO and BUP
//  if (vts->lb_offset_title_vob)
  {
    if ((vts->lb_offset_title_vob < vts->ifo_lb_size)
    ||  (vts->lb_offset_title_vob > vts->set_lb_size - vts->ifo_lb_size))
    {
      rc = 0x00C4;
      goto error;
    }
  }

  // other tables must be located between mat and end of IFO

  //  title parts table is mandatory
  {
    if ((vts->lb_offset_title_parts_table >= vts->ifo_lb_size)
    ||  (vts->lb_offset_title_parts_table < mat_lb_size))
    {
      rc = 0x00C8;
      goto error;
    }
  }

  //  title program chains table is mandatory
  {
    if ((vts->lb_offset_title_program_chains_table >= vts->ifo_lb_size)
    ||  (vts->lb_offset_title_program_chains_table < mat_lb_size))
    {
      rc = 0x00CC;
      goto error;
    }
  }

  if (vts->lb_offset_menu_languages_table)
  {
    if ((vts->lb_offset_menu_languages_table >= vts->ifo_lb_size)
    ||  (vts->lb_offset_menu_languages_table < mat_lb_size))
    {
      rc = 0x00D0;
      goto error;
    }
  }

  if (vts->lb_offset_time_map)
  {
    if ((vts->lb_offset_time_map >= vts->ifo_lb_size)
    ||  (vts->lb_offset_time_map < mat_lb_size))
    {
      rc = 0x00D4;
      goto error;
    }
  }

  if (vts->lb_offset_menu_cell_address_table)
  {
    if ((vts->lb_offset_menu_cell_address_table >= vts->ifo_lb_size)
    ||  (vts->lb_offset_menu_cell_address_table < mat_lb_size))
    {
      rc = 0x00D8;
      goto error;
    }
  }

  if (vts->lb_offset_menu_vobu_address_map)
  {
    if ((vts->lb_offset_menu_vobu_address_map >= vts->ifo_lb_size)
    ||  (vts->lb_offset_menu_vobu_address_map < mat_lb_size))
    {
      rc = 0x00DC;
      goto error;
    }
  }

//  if (vts->lb_offset_title_cell_address_table)
  {
    if ((vts->lb_offset_title_cell_address_table >= vts->ifo_lb_size)
    ||  (vts->lb_offset_title_cell_address_table < mat_lb_size))
    {
      rc = 0x00E0;
      goto error;
    }
  }

//  if (vts->lb_offset_title_vobu_address_map)
  {
    if ((vts->lb_offset_title_vobu_address_map >= vts->ifo_lb_size)
    ||  (vts->lb_offset_title_vobu_address_map < mat_lb_size))
    {
      rc = 0x00E4;
      goto error;
    }
  }

  return 0;

error:
  if (config.debug & cfg_printnavstats)
    ka_log(ka_log_note | ka_log_nav, "VTS_MAT, invalid value at offset 0x%05x\n", rc);

  return rc;
}

void dvd_free_vts_info(dvd_vts_ifo_t** pvts)
{
  dvd_vts_ifo_t* vts = *pvts;
  *pvts = NULL;

  if (vts)
  {
    free_title_parts(&vts->title_parts);
    free_pgc_lu_table(&vts->title_pgcs);
    free_table_of_menu_languages(&vts->menu_languages);
    free_table_of_timemaps(&vts->time_map);
    free_cell_address_table(&vts->menu_cells);
    free_vobu_address_map(&vts->menu_vobus);
    free_cell_address_table(&vts->title_cells);
    free_vobu_address_map(&vts->title_vobus);
    ka_mem_free(vts);
  }
}

int dvd_read_vts_ifo(ka_error_t* pErrorBlock, dvd_vts_ifo_t** pvts, int id, const uint8_t* buf, const uint8_t* end)
{
  dvd_vts_ifo_t* vts = ka_mem_calloc(sizeof(*vts));
  if (vts == NULL)
  {
    ka_error_fill(pErrorBlock, ka_error_nomem);
    return -1;
  }

  *pvts = vts;

  int rc = 0;
  if (!rc) rc = read_vts_mat(vts, buf);
  if (!rc && vts->lb_offset_title_parts_table)
    rc = read_title_parts(&vts->title_parts, DVD_LB_SIZE*vts->lb_offset_title_parts_table, buf, end);
  if (!rc && vts->lb_offset_title_program_chains_table)
    rc = read_pgc_lu_table(&vts->title_pgcs, DVD_LB_SIZE*vts->lb_offset_title_program_chains_table, buf, end);
  if (!rc && vts->lb_offset_menu_languages_table)
    rc = read_table_of_menu_languages(&vts->menu_languages, DVD_LB_SIZE*vts->lb_offset_menu_languages_table, buf, end);
  if (!rc && vts->lb_offset_time_map)
    rc = read_table_of_timemaps(&vts->time_map, DVD_LB_SIZE*vts->lb_offset_time_map, buf, end);
  if (!rc && vts->lb_offset_menu_cell_address_table)
    rc = read_cell_address_table(&vts->menu_cells, DVD_LB_SIZE*vts->lb_offset_menu_cell_address_table, buf, end);
  if (!rc && vts->lb_offset_menu_vobu_address_map)
    rc = read_vobu_address_map(&vts->menu_vobus, DVD_LB_SIZE*vts->lb_offset_menu_vobu_address_map, buf, end);
  if (!rc && vts->lb_offset_title_cell_address_table)
    rc = read_cell_address_table(&vts->title_cells, DVD_LB_SIZE*vts->lb_offset_title_cell_address_table, buf, end);
  if (!rc && vts->lb_offset_title_vobu_address_map)
    rc = read_vobu_address_map(&vts->title_vobus, DVD_LB_SIZE*vts->lb_offset_title_vobu_address_map, buf, end);

  if (!rc) return 0;

  if (rc < 0)
  {
    ka_error_fill(pErrorBlock, ka_error_nomem);
    return -1;
  }

  sprintf(&pErrorBlock->errmess[0], "VTS %02d invalid value at offset 0x%05x", id, rc);
  return -1;
}

int dvd_read_pci(ka_error_t* pErrorBlock, dvd_pci_t** ppci, const uint8_t* p)
{
  dvd_pci_t* pci = ka_mem_calloc(sizeof(*pci));
  if (pci == NULL)
  {
    ka_error_fill(pErrorBlock, ka_error_nomem);
    return -1;
  }

  pci->vobu.cur_lbn = read32(p + 0x00);
  pci->vobu.category = read32(p + 0x04);
  pci->vobu.userops_ctl = read32(p + 0x08);
  pci->vobu.start_pts = read32(p + 0x0C);
  pci->vobu.end_pts = read32(p + 0x10);
  pci->vobu.seq_end_pts = read32(p + 0x14);
  read_time(&pci->vobu.cell_time, p + 0x18);
  readn((uint8_t*) &pci->vobu.iscr, p + 0x1C, 32);

  for (int i = 0; i < 9; i++)
    pci->angle_rel_lbn[i] = read32(p + 0x3C + 4*i);

  pci->highlight.status = read16(p + 0x60);
  pci->highlight.start_pts = read32(p + 0x62);
  pci->highlight.end_pts = read32(p + 0x66);
  pci->highlight.button_selection_end_pts = read32(p + 0x6A);
  pci->highlight.groups.nr_groups = p[0x6E] >> 4;
  pci->highlight.groups.type[0] = p[0x6E] & 0xf;
  pci->highlight.groups.type[1] = p[0x6F] >> 4;
  pci->highlight.groups.type[2] = p[0x6F] & 0xf;
  pci->highlight.offset_button = p[0x70];
  pci->highlight.nr_buttons = p[0x71];
  pci->highlight.nr_of_selectable_buttons = p[0x72];
  pci->highlight.force_select_button = p[0x74];
  pci->highlight.force_action_button = p[0x75];

  p += 0x76;
  for (int i = 0; i < 3; i++, p+= 8)
  {
    pci->highlight.selection[i].color_emphasis2  = p[0] >> 4;
    pci->highlight.selection[i].color_emphasis1  = p[0] & 0xf;
    pci->highlight.selection[i].color_pattern    = p[1] >> 4;
    pci->highlight.selection[i].color_background = p[1] & 0xf;
    pci->highlight.selection[i].alpha_emphasis2  = p[2] >> 4;
    pci->highlight.selection[i].alpha_emphasis1  = p[2] & 0xf;
    pci->highlight.selection[i].alpha_pattern    = p[3] >> 4;
    pci->highlight.selection[i].alpha_background = p[3] & 0xf;
    pci->highlight.action[i].color_emphasis2  = p[4] >> 4;
    pci->highlight.action[i].color_emphasis1  = p[4] & 0xf;
    pci->highlight.action[i].color_pattern    = p[5] >> 4;
    pci->highlight.action[i].color_background = p[5] & 0xf;
    pci->highlight.action[i].alpha_emphasis2  = p[6] >> 4;
    pci->highlight.action[i].alpha_emphasis1  = p[6] & 0xf;
    pci->highlight.action[i].alpha_pattern    = p[7] >> 4;
    pci->highlight.action[i].alpha_background = p[7] & 0xf;
  }

  for (int i = 0; i < 36; i++, p += 18)
  {
    pci->buttons[i].color_table = p[0] >> 6;
    pci->buttons[i].x_start = ((p[0] & 0x3f) << 4) + (p[1] >> 4);
    pci->buttons[i].x_end   = ((p[1] & 0x3)  << 8) + p[2];
    pci->buttons[i].auto_action = p[3] >> 6;
    pci->buttons[i].y_start = ((p[3] & 0x3f) << 4) + (p[4] >> 4);
    pci->buttons[i].y_end   = ((p[4] & 0x3)  << 8) + p[5];
    pci->buttons[i].new_button.up = p[6];
    pci->buttons[i].new_button.down = p[7];
    pci->buttons[i].new_button.left = p[8];
    pci->buttons[i].new_button.right = p[9];
    readn((uint8_t*) &pci->buttons[i].cmd, p + 10, 8);
  }

  *ppci = pci;

  return 0;
}

int dvd_read_dsi(ka_error_t* pErrorBlock, dvd_dsi_t** pdsi, const uint8_t* p)
{
  dvd_dsi_t* dsi = (*pdsi == NULL) ? ka_mem_calloc(sizeof(*dsi)) : *pdsi;
  if (dsi == NULL)
  {
    ka_error_fill(pErrorBlock, ka_error_nomem);
    return -1;
  }

  dsi->scr = read32(p + 0x00);
  dsi->cur_lbn = read32(p + 0x04);
  dsi->vobu_last = read32(p + 0x08);
  dsi->refframes_last[0] = read32(p + 0x0C);
  dsi->refframes_last[1] = read32(p + 0x10);
  dsi->refframes_last[2] = read32(p + 0x14);
  dsi->vob_nr = read16(p + 0x18);
  dsi->cell_nr = p[0x1B];
  read_time(&dsi->cell_time, p + 0x1C);
  dsi->flags = read16(p + 0x20);
  dsi->ilvu_last = read32(p + 0x22);
  dsi->ilvu_next = read32(p + 0x26);
  dsi->ilvu_next_size = read16(p + 0x2A);
  dsi->vob_first_pts = read32(p + 0x2C);
  dsi->vob_last_pts = read32(p + 0x30);

  p += 0x34;
  for (int i = 0; i < 8; i++)
  {
    for (int j = 0; j < 2; j++, p += 4)
    {
      dsi->audio_gap_pts[i][j] = read32(p);
    }
  }

  for (int i = 0; i < 8; i++)
  {
    for (int j = 0; j < 2; j++, p += 4)
    {
      dsi->audio_gap_duration[i][j] = read32(p);
    }
  }

  for (int i = 0; i < 9; i++, p += 6)
  {
    dsi->angles[i].next_ilvu = read32(p);
    dsi->angles[i].ilvu_size = read16(p + 4);
  }

  dsi->vobu_next_video = read32(p);
  p += 4;

  for (int i = 0; i < 19; i++, p += 4)
  {
    dsi->vobu_fsearch[i] = read32(p);
  }

  dsi->vobu_next = read32(p);
  dsi->vobu_prev = read32(p + 4);
  p += 8;

  for (int i = 0; i < 19; i++, p += 4)
  {
    dsi->vobu_bsearch[i] = read32(p);
  }

  dsi->vobu_prev_video = read32(p);
  p += 4;

  for (int i = 0; i < 8; i++, p += 2)
  {
    dsi->offset_audio[i] = read16(p);
  }

  for (int i = 0; i < 21; i++, p += 4)
  {
    dsi->spu_vobu[i] = read32(p);
  }

  *pdsi = dsi;

  return 0;
}

int dvd_time_toRtc(const dvd_time_t* t)
{
  int time = (t->hour * 60) + t->min;
  time = (time * 60) + t->sec;
  time *= PTS_PER_SECOND;
  if (t->fps)
    time += (t->frameinfo * PTS_PER_SECOND) / t->fps;
  return time;
}
