#include "ka_demux.h"

#include <stdbool.h>
#include <string.h>

#include "config.h"
#include "ka_error.h"
#include "ka_log.h"
#include "ka_mem.h"
#include "ka_codecs.h"
#include "ka_acodec.h"
#include "ka_vars.h"
#include "ro_file.h"
#include "dvd/dvd_load_ifo.h"
#include "dvd/dvd_packets.h"
#include "input/ka_indvd.h"

typedef struct
{
  ka_demux_t  hdr;
  ka_input_t* pInput;
  ka_demux_t* mpegPS;

  dvd_desc_t* dvd_desc;
  dvd_vm_state_t currVmState; // Active VM
  dvd_vm_state_t decodeVmState; // VM for buffering (current or next cell)
  uint32_t decodeSection;

  dvd_pci_t* pciList;
  dvd_pci_t curPci;
  int actionButton;

  // Pos info
  dvd_ifo_desc_t* curIfo;
  const dvd_file_desc_t* curVobInfo;
  struct
  {
    // Absolute position in bytes on DVD
    uint64_t vtsStart;  // start of VTS menu VOB or 1st title VOB depending on domain
                        // all cell vobu addresses are relative to it
    uint64_t cellStart; // start of currently decoded cell
    uint64_t cellEnd;   // end of currently decoded cell (last byte + 1)
    // Info relative to program chain (which may be composed of non-contiguous cells)
    int64_t relToAbs;   // offset to add to convert position in pgc to absolute position
    uint64_t pgcSize;   // total nr of bytes in pgc
  } pos;
} data_t;

static int dvd_decodeVmIsDisplayVm(data_t* data)
{
  if ((data->currVmState.cellCount  != data->decodeVmState.cellCount)
  ||  (data->currVmState.pos.domain != data->decodeVmState.pos.domain)
  ||  (data->currVmState.pos.vtsNr  != data->decodeVmState.pos.vtsNr)
  ||  (data->currVmState.pos.pgc    != data->decodeVmState.pos.pgc)
  ||  (data->currVmState.pos.cellNr != data->decodeVmState.pos.cellNr))
    return 0;

  return 1;
}

static int64_t dvd_setDisplayCell(data_t* data, const dvd_vm_state_t* nextVmState, int64_t offset);

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

  while (data->pciList)
  {
    dvd_pci_t* next = data->pciList->next;
    ka_mem_free(data->pciList);
    data->pciList = next;
  }
  data->curPci.active = 0;

  if (data->mpegPS != NULL)
    data->mpegPS->vptr->FNreset(data->mpegPS, stack);
  memset(&data->curPci, 0, sizeof(data->curPci));
  dvd_vm_reset(&data->currVmState, data->dvd_desc, DVDVM_EXECUTE | ((config.debug & cfg_printnavstats) ? DVDVM_LOGN : 0));
  dvd_vm_setFirstPlay(&data->currVmState);

  // No decode VM yet
  data->decodeVmState.cellCount = 0;
  data->decodeVmState.events = 0;

  data->hdr.demuxSection = 0;
  data->hdr.demuxSectionPts = 0;
  data->decodeSection = data->mpegPS->demuxSection = data->hdr.demuxSection;

  dvd_setDisplayCell(data, &data->currVmState, 0ll);
  data->curPci.highlight.status = 0;
}

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

  while (data->pciList)
  {
    dvd_pci_t* next = data->pciList->next;
    ka_mem_free(data->pciList);
    data->pciList = next;
  }
  data->curPci.active = 0;

  int ret = data->mpegPS->vptr->FNclear(data->mpegPS, stack);

  data->hdr.demuxSection++;
  data->decodeSection = data->mpegPS->demuxSection = data->hdr.demuxSection;
  data->hdr.demuxSectionPts = data->decodeVmState.pos.pgc->cell_playbacks[data->decodeVmState.pos.cellNr - 1].pgc_rel_time;

  return ret;
}

/**
 * Parses DVD info, to build list of titles and chapters.
 *
 * @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_dvd_parse(ka_demux_t* pdemux, ka_stack_t* stack)
{
  data_t* data = (data_t*) pdemux;
  int i, t = 0;

  // VMG
  const dvd_vmg_ifo_t* vmg = data->dvd_desc->ifos[0].vmg;

  t = ka_stack_addTitle(stack, 0, (0 << 16) + 1, "dvd_menu:Menu");
  ka_stack_addChapter(stack, 0, t, (0 << 16) + DVD_MenuId_Root, "dvd_menu_root:Root");
  ka_stack_addChapter(stack, 0, t, (0 << 16) + DVD_MenuId_Title, "dvd_menu_title:Title");
  ka_stack_addChapter(stack, 0, t, (0 << 16) + DVD_MenuId_Subtitles, "dvd_menu_subtitles:Subtitles");
  ka_stack_addChapter(stack, 0, t, (0 << 16) + DVD_MenuId_Audio, "dvd_menu_audio:Audio");
  ka_stack_addChapter(stack, 0, t, (0 << 16) + DVD_MenuId_Angle, "dvd_menu_angle:Angle");
  ka_stack_addChapter(stack, 0, t, (0 << 16) + DVD_MenuId_Chapter, "dvd_menu_chapter:Chapter");

  const dvd_table_of_titles_t* tt = vmg->table_of_titles;

  for (i = 1; i <= tt->nr_titles; i++)
  {
    const dvd_title_t* title = &tt->titles[i - 1];

    t = ka_stack_addTitle(stack, 0, (i << 16) + 1, "titles_sm_nr:Title %d", i);
    if (t < 0)
      break;

    // Cf. "Le monde de Nemo" extra disc 0x158 chapters in a title !
    int step = 1 + (title->nr_chapters / KA_PROGRAM_MAX_CHAPTERS);

    for (int tc = 1; tc <= title->nr_chapters; tc += step)
    {
      if (ka_stack_addChapter(stack, 0, t, (i << 16) + tc, "chapters_sm_nr:Chapter %d", tc) < 0)
        break;
    }
  }

  return 0;
}

/**
 * Returns title, chapter current/max demuxer position.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  pbuffer Pointer to stack buffer.
 * @param  pos     Pointer to demuxer position to fill.
 */

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

  data->mpegPS->vptr->FNgetPos(data->mpegPS, pbuffer, pos);

  if (data->currVmState.pos.domain == DVD_DOMAIN_VTS_TITLE)
  {
    pos->titleNr = data->currVmState.chapter.titleNr;
    pos->chapterNr = data->currVmState.chapter.chapterNr - 1;
  }
  else
  {
    pos->titleNr = 0;
    pos->chapterNr = data->currVmState.menu.menuId - DVD_MenuId_Root;
  }
  pos->curPos -= data->pos.relToAbs;
  pos->endPos = data->pos.pgcSize;
}

/**
 * Selects the cell VOB and sectors associated to a cell.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  pos     Pos in currently played title/menu pgc, updated on exit.
 * @param  offset  Offset within cell.
 */
static void demux_dvd_selectVtsVob(data_t* data, const dvd_pos_t* pos, int64_t offset)
{
  data->curIfo = &data->dvd_desc->ifos[pos->vtsNr];

  // Will need update later
  data->pos.cellStart = ((uint64_t) pos->lbCellStart) * DVD_LB_SIZE;

  if (pos->domain == DVD_DOMAIN_VTS_TITLE)
  {
    data->pos.vtsStart = data->curIfo->vob_title_start;
    uint64_t pos = data->pos.vtsStart + offset + data->pos.cellStart;

    for(int vob = 1; vob <= data->curIfo->nr_vobs; vob++)
    {
      data->curVobInfo = &data->curIfo->vobs[vob];

      if (pos < data->curVobInfo->end)
        break;
    }
  }
  else
  {
    data->curVobInfo = &data->curIfo->vobs[0];
    data->pos.vtsStart = data->curIfo->vob_menu_start;
  }

  data->pos.cellStart += data->pos.vtsStart;
  data->pos.cellEnd   = data->pos.vtsStart + ((uint64_t) pos->lbCellEnd) * DVD_LB_SIZE;

  // Calculate a position and size for pgc
  const dvd_pgc_t* pgc = pos->pgc;
  const dvd_cell_playback_t* cell = &pgc->cell_playbacks[data->decodeVmState.pos.cellNr - 1];

  data->pos.relToAbs = data->pos.cellStart - ((uint64_t) cell->pgc_rel_lb_size) * DVD_LB_SIZE;
  data->pos.pgcSize = ((uint64_t) pgc->total_lb_size) * DVD_LB_SIZE;
}

static const char* domainStr[4] = {"FP  ", "VMG ", "VTSM", "VTST"};
static void log_pos(const char* label, const dvd_pos_t* pos)
{
  ka_log(ka_log_nav, "%s %s %d PGC %d CELLNR %d", label, domainStr[pos->domain], pos->vtsNr, pos->programChainNr, pos->cellNr);
}

/**
 * Set physical position in DVD (i.e select appropriate VOB file position within it).
 *
 * @param  data    Pointer to demuxer data.
 * @param  pos     Pos in currently played title/menu pgc.
 * @param  offset  Offset within cell.
 *
 * @returns Offset in input -1 if not possible to determine a position.
 */
static int64_t demux_dvd_updateDVDPos(data_t* data, const dvd_pos_t* pos, int64_t offset)
{
  demux_dvd_selectVtsVob(data, pos, offset);

  const dvd_file_desc_t* info = data->curVobInfo;

  // New cell ?
  if (offset == 0ll)
  {
    data->hdr.demuxSection++;
    data->decodeSection = data->mpegPS->demuxSection = data->hdr.demuxSection;
    data->hdr.demuxSectionPts = data->decodeVmState.pos.pgc->cell_playbacks[data->decodeVmState.pos.cellNr - 1].pgc_rel_time;
  }

  // We decode one cell at a time, but cell mey overlap 2 VTS_XX_Y.VOB files
  uint64_t end = data->pos.cellEnd;
  if (end > info->end)
    end = info->end;

  // Have we the title key already ?
  if (dvd_desc_getTitleKey(data->pInput, data->curIfo) < 0)
    return -1;

  if (ka_indvd_open(data->pInput, data->dvd_desc->path_video, info->name, info->start, end, data->curIfo->title_key))
    return -1;

  int64_t absPos = data->pos.cellStart + offset;
  if (config.debug & cfg_printnavstats)
    log_pos("DECODE ", &data->decodeVmState.pos);

  absPos = data->mpegPS->vptr->FNsetPos(data->mpegPS, absPos);

  if (absPos != -1ll)
    absPos -= data->pos.relToAbs;

  return absPos;
}

static void dvd_nextCell(data_t* data, dvd_vm_state_t* nextVmState, int decode)
{
  *nextVmState = data->currVmState;
  if (decode)
  {
    dvd_vm_setLogState(nextVmState, 0);
    dvd_vm_setLogPrefix(nextVmState, "          Next: ");
    dvd_vm_nextCell(nextVmState);
    dvd_vm_setLogState(nextVmState, (config.debug & cfg_printnavstats) ? DVDVM_LOGN : 0);
    dvd_vm_setLogPrefix(nextVmState, "");
    if (config.debug & cfg_printnavstats)
      log_pos("NEXT   ", &nextVmState->pos);
  }
  else
  {
    dvd_vm_nextCell(nextVmState);
  }
}

/**
 * Set physical position in DVD (i.e select appropriate VOB file position within it).
 *
 * @param  data         Pointer to demuxer data.
 * @param  nextVmState  New VM state to set positin from.
 * @param  offset       Offset within cell.
 *
 * @returns Offset in input -1 if not possible to determine a position.
 */
static int64_t dvd_setDisplayCell(data_t* data, const dvd_vm_state_t* nextVmState, int64_t offset)
{
  // Check if menu is still valid
  if ((data->currVmState.pos.domain != nextVmState->pos.domain)
  ||  (data->currVmState.pos.vtsNr  != nextVmState->pos.vtsNr)
  ||  (data->currVmState.pos.pgc    != nextVmState->pos.pgc)
  ||  (data->currVmState.pos.cellNr != nextVmState->pos.cellNr))
    data->curPci.highlight.status = 0;

  data->currVmState = *nextVmState;
  if (data->currVmState.events ^ DVDEV_SUBS_CHANGE)
  {
    data->mpegPS->vptr->FNsetAngle(data->mpegPS, data->currVmState.system.regs[DVDREG_ANGLE_NR]);
  }
  data->currVmState.events = 0;

  if (config.debug & cfg_printnavstats)
    log_pos("DISPLAY", &data->currVmState.pos);

  // Remove PCIs in front of the list belonging to previous cell

  dvd_pci_t** ppci = &data->pciList;

  while (*ppci)
  {
    dvd_pci_t* pci = *ppci;

    if ((data->currVmState.pos.lbCellStart <= pci->vobu.cur_lbn)
    &&  (pci->vobu.cur_lbn < data->currVmState.pos.lbCellEnd))
      break;

    // of no more use
    *ppci = pci->next;
    ka_mem_free(pci);
    continue;
  }

  // Check if decoding pos is still valid
  if (!dvd_decodeVmIsDisplayVm(data))
  {
    data->decodeVmState = data->currVmState;
    if (nextVmState->pos.pgc != NULL)
      ka_vars_set(data->hdr.vars, "DVD_PALETTE", nextVmState->pos.pgc->color_lookup);

    if (offset == -1ll)
      offset = 0ll;
  }

  if (offset >= 0)
    return demux_dvd_updateDVDPos(data, &data->decodeVmState.pos, offset);

  return 0ll;
}

/**
 * Returns position according to parsed input info.
 *
 * @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 determine a position.
 */
static int64_t demux_dvd_setPos(ka_demux_t* pdemux, int64_t pos)
{
  data_t* data = (data_t*) pdemux;
  dvd_vm_state_t nextVmState = data->currVmState;
  const dvd_cell_playback_t* cell;
  int i;

  // Only allow to change pos while playing a title
  if (nextVmState.pos.domain != DVD_DOMAIN_VTS_TITLE)
    return -1;
  nextVmState.events = 0;

  // Must determine corresponding cell
  const dvd_pgc_t* pgc = nextVmState.pos.pgc;
  int32_t lb = (int32_t)(uint64_t)(pos / DVD_LB_SIZE);

  if (lb >= pgc->total_lb_size)
    return -1;

  for (i = 1; i <= pgc->nr_cells; i++)
  {
    cell = &pgc->cell_playbacks[i - 1];

    // Beware, multi-angle blocks contain 1 cell per angle
    if (((cell->category & DVD_CELL_BLOCK_TYPE_MASK) == 0)
    ||  ((cell->category & DVD_CELL_TYPE_MASK) <= DVD_CELL_TYPE_FIRST_OF_ANGLE))
    {
      if (lb < cell->pgc_rel_lb_size)
        break;
    }
  }

  i--;
  cell = &pgc->cell_playbacks[i - 1];

  // Set current cell
  dvd_vm_setCellNr(&nextVmState, i);

  // Set pos relative to current cell, sector aligned
  pos = ((uint64_t) (uint32_t)(lb - cell->pgc_rel_lb_size)) * DVD_LB_SIZE;

  return dvd_setDisplayCell(data, &nextVmState, pos);
}

/**
 * Jumps to title/chapter.
 *
 * @param  state      VM state to update.
 * @param  id         Id of title/chapter.
 *
 * @returns 0 if OK or -1 if not possible to determine a position.
 */
static int dvd_setChapter(dvd_vm_state_t* state, uint32_t id)
{
  int titleNr = id >> 16;
  int chapterNr = id & 0xff;

  // Reset button
  state->system.regs[DVDREG_HIGHLIGHT_BUTTON_NR] = 1 << 10;

  if (titleNr == 0)
  {
    if (chapterNr == 0)
    {
      if (config.debug & cfg_printseekstats)
        ka_log(ka_log_seek, "first play");
      // First play
      if (-1 == dvd_vm_setFirstPlay(state))
        return -1;
    }
    else
    {
      if (config.debug & cfg_printseekstats)
        ka_log(ka_log_seek, "menu %d of vts %d", chapterNr, state->pos.vtsNr);
      // Menu
      if (-1 == dvd_vm_setMenu(state, state->pos.vtsNr, chapterNr))
        return -1;
    }
  }
  else
  {
    if (config.debug & cfg_printseekstats)
      ka_log(ka_log_seek, "title %d, chapter %d", titleNr, chapterNr);
    // Title/Chapter
    if (-1 == dvd_vm_setChapter(state, titleNr, chapterNr, 0))
      return -1; // no such tilte/chapter
  }

  return 0;
}

/**
 * Jumps to title/chapter.
 *
 * @param  pdemux     Pointer to demuxer data.
 * @param  id         Id of title/chapter.
 *
 * @returns 0 if OK or -1 if not possible to determine a position.
 */
static int demux_dvd_setChapter(ka_demux_t* pdemux, uint32_t id)
{
  data_t* data = (data_t*) pdemux;
  dvd_vm_state_t nextVmState = data->currVmState;

  nextVmState.events = 0;

  int ret = dvd_setChapter(&nextVmState, id);
  if (ret == -1)
    return -1;

  // Position is updated by the caller's clear that must follows
  // This makes it easier for FNnavigate
  dvd_setDisplayCell(data, &nextVmState, 0ll);

  return 0;
}

/**
 * Returns position of a given chapter.
 *
 * @param  pdemux     Pointer to demuxer data.
 * @param  id         Id of title/chapter.
 *
 * @returns position if OK or -1 if not possible to determine a position.
 */
static int64_t demux_dvd_getChapterPos(ka_demux_t* pdemux, uint32_t id)
{
  data_t* data = (data_t*) pdemux;
  dvd_vm_state_t nextVmState = data->currVmState;

  nextVmState.events = 0;

  // Not for menus
  if ((id >> 16) == 0)
    return -1;

  int ret = dvd_setChapter(&nextVmState, id);
  if (ret == -1)
    return -1;

  // Calculate a position and size for pgc
  const dvd_pgc_t* pgc = nextVmState.pos.pgc;
  const dvd_cell_playback_t* cell = &pgc->cell_playbacks[nextVmState.pos.cellNr - 1];

  return ((int64_t) cell->pgc_rel_lb_size) * DVD_LB_SIZE;
}

/**
 * Returns stream attributes.
 *
 * @param  pdemux     Pointer to demuxer data.
 */
static dvd_attributes_t* dvd_getCurrentAttributes(data_t* data)
{
  // locate current attributes
  dvd_attributes_t* attrs;

  switch(data->currVmState.pos.domain)
  {
    case DVD_DOMAIN_VTS_MENU:
      attrs = &data->dvd_desc->ifos[data->currVmState.pos.vtsNr].vts->menu_attributes;
    break;
    case DVD_DOMAIN_VTS_TITLE:
      attrs = &data->dvd_desc->ifos[data->currVmState.pos.vtsNr].vts->title_attributes;
    break;
    default:
      attrs = &data->dvd_desc->ifos[0].vmg->menu_attributes;
  }

  return attrs;
}

static const ka_block_id audioType[8] =
{ KA_ABLOCK_AC3
, KA_ABLOCK_UNKNOWN
, KA_ABLOCK_MPEG
, KA_ABLOCK_MPEG
, KA_ABLOCK_LPCM_DVD
, KA_ABLOCK_UNKNOWN
, KA_ABLOCK_DTS
, KA_ABLOCK_UNKNOWN
};

/**
 * Get audio stream information.
 *
 * @param  pdemux    Pointer to demuxer data.
 * @param  streamId  StreamId or -1 for active stream.
 * @param  pInfo     Pointer to info to fill in.
 */
static void demux_dvd_getAudioInfo(const ka_demux_t* pdemux, int32_t streamId, ka_demux_audio_info_t* pInfo)
{
  data_t* data = (data_t*) pdemux;
  uint32_t audio, id;

  // locate current attributes
  dvd_attributes_t* attrs = dvd_getCurrentAttributes(data);
  pInfo->nrStreams = attrs->nr_audio_streams;

  if (streamId < 0)
  {
    if (data->currVmState.pos.domain == DVD_DOMAIN_VTS_TITLE)
      audio = data->currVmState.system.regs[DVDREG_AUDIO_STREAM_NR];
    else
      audio = attrs->nr_audio_streams ? 0 : 15;
  }
  else if ((streamId == 0) || (streamId > attrs->nr_audio_streams))
    audio = 15;
  else
    audio = streamId - 1;

  // valid is 0-7, 15 is for no audio
  if (audio > 7)
    id = 0;
  else
    id = data->currVmState.pos.pgc->audio_stream[audio];

  // top bit marks audio as present
  if (id & 0x80)
  {
    ka_block_id type = audioType[attrs->audio_attributes[audio][0] >> 5];

    pInfo->streamId = 1 + audio;
    pInfo->kid = (type == KA_ABLOCK_UNKNOWN) ? 0 : (type | (id & 7));

    pInfo->channels = 1 + (attrs->audio_attributes[audio][1] & 0x07);
    if (attrs->audio_attributes[audio][0] & 0x0c)
    {
      pInfo->lang[0] = attrs->audio_attributes[audio][2];
      pInfo->lang[1] = attrs->audio_attributes[audio][3];
      pInfo->lang[2] = 0;
    }
    else
      pInfo->lang[0] = 0;
  }
  else
  {
    pInfo->streamId = 0;
    pInfo->kid = 0;

    pInfo->channels = 0;
    pInfo->lang[0] = 0;
  }
}

/**
 * Set current audio stream.
 *
 * @param  pdemux    Pointer to demuxer data.
 * @param  streamId  New audio stream.
 *
 * Returns true if changed.
 */
static int demux_dvd_selectAudio(ka_demux_t* pdemux, uint32_t streamId)
{
  data_t* data = (data_t*) pdemux;

  // Not while menus are displayed
  if (data->currVmState.pos.domain != DVD_DOMAIN_VTS_TITLE)
    return 0;

  dvd_attributes_t* attrs = &data->dvd_desc->ifos[data->currVmState.pos.vtsNr].vts->title_attributes;

  if (streamId > attrs->nr_audio_streams)
    return 0;

  int audio = (streamId && (streamId <= 8)) ? streamId - 1 : 15;

  if (data->currVmState.system.regs[DVDREG_AUDIO_STREAM_NR] != audio)
  {
    data->currVmState.system.regs[DVDREG_AUDIO_STREAM_NR] = audio;
    data->currVmState.events &= ~DVDEV_AUDIO_CHANGE;
    return 1;
  }

  return 0;
}

/**
 * Returns current subtitle stream info.
 *
 * @param  pdemux    Pointer to demuxer data.
 * @param  streamId  StreamId or -1 for forced stream, -2 for actived stream.
 * @param  pInfo     Pointer to current subtitle info to fill in.
 */
static void demux_dvd_getSubsInfo(const ka_demux_t* pdemux, int32_t streamId, ka_demux_subtitle_info_t* pInfo)
{
  data_t* data = (data_t*) pdemux;
  uint32_t subs;

  // locate current attributes
  dvd_attributes_t* attrs = dvd_getCurrentAttributes(data);
  pInfo->nrStreams = attrs->nr_subtitle_streams;

  if (streamId < -1) // current sub
    subs = data->currVmState.system.regs[DVDREG_SUBS_STREAM_NR];
  else if (streamId == -1) // forced sub
  {
    subs = data->currVmState.system.regs[DVDREG_SUBS_STREAM_NR];
    subs &= ~0x40;
    if (subs >= pInfo->nrStreams)
      subs = 0;
    subs |= 0x40;
  }
  else if ((streamId == 0) || (streamId > attrs->nr_subtitle_streams))
    subs = 62;
  else
    subs = 0x40 | (streamId - 1);

  // valid is 0-31, 62 none, 63 forced, bit 6 = display
  if (subs & 0x40)
  {
    subs &= 0x3f;
    if ((subs <= 31)
    &&  (data->currVmState.pos.pgc->subtitle_stream[subs][0] & 0x80))
    {
      pInfo->streamId = 1 + subs;
      // select stream id from normal or wide depending if video is 4:3 or 16:9
      pInfo->kid = data->currVmState.pos.pgc->subtitle_stream[subs][((attrs->video_attributes & 0xc00) == 0xc00) ? 1 : 0];
      pInfo->kid = KA_SBLOCK_DVD_SUBTITLE | (pInfo->kid & 0x1f);

      if (attrs->subtitle_attributes[subs][0] & 0x03)
      {
        pInfo->lang[0] = attrs->subtitle_attributes[subs][2];
        pInfo->lang[1] = attrs->subtitle_attributes[subs][3];
        pInfo->lang[2] = 0;
      }
      else
        pInfo->lang[0] = 0;
      return;
    }
  }

  pInfo->streamId = 0;
  pInfo->kid = 0;
  pInfo->lang[0] = 0;
}

/**
 * Set current subtitle stream.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  angle   New subtitle stream.
 *
 * Returns true if changed.
 */
static int demux_dvd_selectSubs(ka_demux_t* pdemux, uint32_t streamId)
{
  data_t* data = (data_t*) pdemux;

  // Not while menus are displayed
  if (data->currVmState.pos.domain != DVD_DOMAIN_VTS_TITLE)
    return 0;

  dvd_attributes_t* attrs = &data->dvd_desc->ifos[data->currVmState.pos.vtsNr].vts->title_attributes;
  if (streamId > attrs->nr_subtitle_streams)
    return 0;

  streamId = (streamId && (streamId <= 32)) ? 0x40 + streamId - 1 : 62;

  if (data->currVmState.system.regs[DVDREG_SUBS_STREAM_NR] != streamId)
  {
    data->currVmState.system.regs[DVDREG_SUBS_STREAM_NR] = streamId;
    data->currVmState.events &= ~DVDEV_SUBS_CHANGE;
    return 1;
  }

  return 0;
}

/**
 * Returns current/max angles.
 *
 * @param  pdemux     Pointer to demuxer data.
 * @param  pInfo      Pointer to angle info structure to fill.
 */
static void demux_dvd_getAngle(const ka_demux_t* pdemux, ka_demux_angle_info_t* pInfo)
{
  data_t* data = (data_t*) pdemux;

  data->mpegPS->vptr->FNgetAngle(data->mpegPS, pInfo);
  pInfo->angleId = data->currVmState.system.regs[DVDREG_ANGLE_NR];
}

/**
 * Set current angle.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  angle   New angle.
 *
 * Returns true if changed.
 */
static int demux_dvd_setAngle(ka_demux_t* pdemux, uint32_t angle)
{
  data_t* data = (data_t*) pdemux;

  if (data->mpegPS->vptr->FNsetAngle(data->mpegPS, angle))
  {
    ka_demux_angle_info_t info;
    data->mpegPS->vptr->FNgetAngle(data->mpegPS, &info);
    data->currVmState.system.regs[DVDREG_ANGLE_NR] = info.angleId;
    data->currVmState.events &= ~DVDEV_ANGLE_CHANGE;
    return 1;
  }

  return 0;
}

/**
 * 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_dvd_fillBuffer(ka_demux_t* pdemux, ka_buffer_t* buffer, int* pdone)
{
  data_t* data = (data_t*) pdemux;

  int read = data->mpegPS->vptr->FNfillBuffer(data->mpegPS, buffer, pdone);

  if (*pdone)
  {
    // VOB file change or cell change
    const dvd_file_desc_t* info = data->curVobInfo;
    uint64_t end = data->pos.cellEnd;
    if (end > info->end)
    {
      // Reached end of VOB file before end of cell, switch to new VOB
      demux_dvd_updateDVDPos(data, &data->decodeVmState.pos, info->end - data->pos.cellStart);
      *pdone = 0;
    }
  }

/*
  static int save = 1;

  if (save != *pdone)
  {
    save = *pdone;
    ka_logn("FILL BUFFER STATE %d\n", *pdone);
  }
*/

  return read;
}

/**
 * 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_dvd_push(ka_demux_t* pdemux, ka_stack_t* stack
                                       , const uint8_t* buf, const uint8_t* end)
{
  data_t* data = (data_t*) pdemux;

  const uint8_t* p = data->mpegPS->vptr->FNpush(data->mpegPS, stack, buf, end);

  // Process navigation packets
  ka_fifo_t* fifo = &stack->navigation;
  ka_block_t* block = fifo->fread;

  for (; block != fifo->fwrite; )
  {
    if ((block->typ & ~KA_BLOCK_STREAMMASK) == KA_NBLOCK_DVD_PCI)
    {
      dvd_pci_t** ppci = &data->pciList;

      while (*ppci)
        ppci = &((*ppci)->next);

      if (dvd_read_pci(pdemux->pErrorBlock, ppci, block->sod))
        return p;
    }

    // block was processed or skipped
    block->typ = 0;

    if (++block == fifo->fend) block = fifo->fstart;
  }
  fifo->fread = fifo->fwrite;

  return p;
}

/**
 * GetWaitTime.
 * Must be called after the cell was completely decoded.
 *
 * @param  pdemux  Pointer to demuxer data.
 *
 * Returns -1 if infinite still time, 0 if none, rtc duration otherwise.
 */
static int dvd_getWaitTime(const dvd_vm_state_t* state)
{
  const dvd_pos_t* pos = &state->pos;
  int val;
  val = pos->pgc->cell_playbacks[pos->cellNr - 1].still_time;
  if (pos->pgc->nr_cells == pos->cellNr)
    val += pos->pgc->still_time;
  // Not strictly correct
  if (pos->pgc->cell_playbacks[pos->cellNr - 1].category & DVD_CELL_VOBU_STILL_MODE)
    val = 0xFF;

  if (val >= 0xFF)
    val = -1;
  else
    val = 90000 * val;

  return val;
}

/**
 * Returns audio/video stream parameters.
 */
static const void* demux_dvd_params(const ka_demux_t* pdemux, ka_block_id kid)
{
  const data_t* data = (data_t*) pdemux;

  return data->mpegPS->vptr->FNparams(data->mpegPS, kid);
}

/**
 * Returns highlight button if fits current PCI conditions, 0 otherwise.
 */
static int get_highlight_button(const dvd_pci_t* cur, const dvd_vm_state_t* state, uint32_t rtc)
{
  int button = state->system.regs[DVDREG_HIGHLIGHT_BUTTON_NR] >> 10;

  if (((cur->highlight.status & 3) != 0)
  &&  button
  &&  (button <= cur->highlight.nr_buttons)
  &&  (cur->highlight.start_pts <= rtc)
  &&  ((cur->highlight.end_pts > rtc) || (dvd_getWaitTime(state) == -1)))
  {
    return button;
  }

  return 0;
}

/**
 * Updates highlight button.
 */
static void set_highlight_button(const dvd_pci_t* cur, dvd_vm_state_t* state, int button, const char* reason)
{
  // Don't touch if no menu
  if (!cur->highlight.nr_buttons)
    return;

  if (button)
  {
    if (button <= cur->highlight.nr_buttons)
    {
      state->system.regs[DVDREG_HIGHLIGHT_BUTTON_NR] = button << 10;

      if (config.debug & cfg_printnavstats)
        ka_log(ka_log_nav, "Highlight button set to %d, reason %s", button, reason);
    }
    else
    {
      if (config.debug & cfg_printnavstats)
        ka_log(ka_log_nav, "Highlight button set to %d, invalid, forced to 1", button);

      state->system.regs[DVDREG_HIGHLIGHT_BUTTON_NR] = 1 << 10;
    }
  }
  else
  {
    button = state->system.regs[DVDREG_HIGHLIGHT_BUTTON_NR] >> 10;

    if ((button == 0) || (button > cur->highlight.nr_buttons))
    {
      if (config.debug & cfg_printnavstats)
        ka_log(ka_log_nav, "Current Highlight button (%d) invalid, reset to 1", button);

      state->system.regs[DVDREG_HIGHLIGHT_BUTTON_NR] = 1 << 10;
    }
  }
}

/**
 * Update pci.
 *
 * @param  pdemux  Pointer to demuxer data.
 */
static int update_pci(data_t* data, uint32_t rtc)
{
  dvd_pci_t** ppci = &data->pciList;
  dvd_pci_t* cur = *ppci;

  while (*ppci)
  {
    dvd_pci_t* pci = *ppci;

    // Remove elapsed pcis but keep last one of current display cell
    // Note: last must be removed on display cell changes
    if (pci->next != NULL)
    {
      // Remove it pci elapse
      if ((data->currVmState.pos.lbCellStart <= pci->next->vobu.cur_lbn)
      &&  (pci->next->vobu.cur_lbn < data->currVmState.pos.lbCellEnd)
      &&  (rtc > pci->vobu.end_pts))
      {
        // of no more use
        *ppci = pci->next;
        ka_mem_free(pci);
        continue;
      }
    }

    cur = pci;
    break;
  }

  if (cur == NULL)
  {
//    if (config.debug & cfg_printnavstats)
//      ka_log(ka_log_nav, "No PCI");
    return 0;
  }

  if (cur->active)
    return 0;


  cur->active = 1;

  if (config.debug & cfg_printnavstats)
  {
    ka_log(ka_log_nav, "vobu start %010u end %010u send %010u", cur->vobu.start_pts, cur->vobu.end_pts, cur->vobu.seq_end_pts);
    ka_log(ka_log_nav, "lba %08x high start %010u end %010u", cur->vobu.cur_lbn, cur->highlight.start_pts, cur->highlight.end_pts);
    ka_logs( ka_log_nav, "PCI Off %d, Max %d, MaxSel %d, Force Sel %d Act %d, Status %d"
           , cur->highlight.offset_button
           , cur->highlight.nr_buttons
           , cur->highlight.nr_of_selectable_buttons
           , cur->highlight.force_select_button
           , cur->highlight.force_action_button
           , cur->highlight.status);
  }

  // Akways copy cf cmd updates for status 3
  data->curPci = *cur;

  if ((cur->highlight.status & 3) == 1)
  {
    if (config.debug & cfg_printnavstats)
      ka_logn(" -> Current\n");

    data->actionButton = 0;
    set_highlight_button(cur, &data->currVmState, cur->highlight.force_select_button, "New preset");

    return ka_nav_redraw_frame;
  }
  else
  {
    if (config.debug & cfg_printnavstats)
      ka_logn("\n");
  }

  return 0;
}

/**
 * GetWaitTime.
 * Must be called after the cell was completely displayed.
 *
 * @param  pdemux  Pointer to demuxer data.
 *
 * Returns -1 if infinite still time, 0 if none, rtc duration otherwise.
 */
static int demux_dvd_getWaitTime(ka_demux_t* pdemux, uint32_t rtc)
{
  data_t* data = (data_t*) pdemux;
  const dvd_pci_t* cur = &data->curPci;

  // cell displayed, ensure we play till end pts
  update_pci(data, rtc);

  int val = dvd_getWaitTime(&data->currVmState);
  if (val < 0) return val; // infinite

  if (cur->active)
  {
    int val2 = cur->vobu.start_pts
             + dvd_time_toRtc(&data->currVmState.pos.pgc->cell_playbacks[data->currVmState.pos.cellNr - 1].time)
             - dvd_time_toRtc(&cur->vobu.cell_time)
             - rtc;
    if ((val2 > 90000) && (val2 > val))
      return val2;
  }

  return val;
}

/**
 * SectionDone.
 * Must be called after the cell was completely displayed.
 *
 * @param  pdemux  Pointer to demuxer data.
 */
static ka_demux_section_state demux_dvd_sectionDone(ka_demux_t* pdemux, ka_demux_section_done done)
{
  data_t* data = (data_t*) pdemux;
  dvd_vm_state_t nextVmState;

  // Cell done, start a new one ?
  if (done == KA_DEMUX_SECTION_DONE_DECODE)
  {
    // Only if currently decoded one is displayed one
    if (!dvd_decodeVmIsDisplayVm(data))
      return KA_DEMUX_SECTION_STATE_WAIT;

    // Avoid recalculating what follows each time
    data->decodeVmState.cellCount = 0;

    // If we had waittime, rtc won't match timing of next section, treat as discontinuity
    if (dvd_getWaitTime(&data->currVmState) != 0)
    {
      return KA_DEMUX_SECTION_STATE_DISCONTINUITY;
    }

    dvd_nextCell(data, &nextVmState, 1);

    // Only if no discontinuity
    if (nextVmState.events & DVDEV_POS_CHANGE)
      return KA_DEMUX_SECTION_STATE_DISCONTINUITY;

    // Set next cell
    data->decodeVmState = nextVmState;
    if (nextVmState.pos.pgc != NULL)
      ka_vars_set(data->hdr.vars, "DVD_PALETTE", nextVmState.pos.pgc->color_lookup);
    demux_dvd_updateDVDPos(data, &data->decodeVmState.pos, 0ll);

    return KA_DEMUX_SECTION_STATE_NEXTSECTION;
  }
  else // Display next/new
  {
    dvd_nextCell(data, &nextVmState, 0);

    // If we had waittime, rtc won't match timing of next section
    if (dvd_getWaitTime(&data->currVmState) != 0)
      nextVmState.events |= DVDEV_POS_CHANGE;

    if (nextVmState.events & DVDEV_EXIT)
    {
      return KA_DEMUX_SECTION_STATE_EXIT;
    }

    if (nextVmState.events & DVDEV_POS_CHANGE)
    {
      // No decode VM yet
      data->decodeVmState.cellCount = 0;
      data->decodeVmState.events = 0;
      if (-1 == dvd_setDisplayCell(data, &nextVmState, 0ll))
        return KA_DEMUX_SECTION_STATE_ERROR;

      return KA_DEMUX_SECTION_STATE_DISCONTINUITY;
    }
    else
    {
      if (-1 == dvd_setDisplayCell(data, &nextVmState, -1ll))
        return KA_DEMUX_SECTION_STATE_ERROR;

      return KA_DEMUX_SECTION_STATE_NEXTSECTION;
    }
  }
}

/**
 * Navigate.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  rtc     Current rtc.
 * @param  key     Keyboard key.
 *
 * Returns 1 if navigation uses this key, 2 if causes discontinuity, 0 otherwise.
 */
static int demux_dvd_navigate(ka_demux_t* pdemux, uint32_t rtc, int key)
{
  data_t* data = (data_t*) pdemux;
  dvd_vm_state_t nextVmState;
  const dvd_vmg_ifo_t* vmg = data->dvd_desc->ifos[0].vmg;

  // Ensure menu highlight control is up to date (may alter button of currVmState)
  int ret = update_pci(data, rtc);

  // Reset events to avoid mixing up new events and preexisting ones
  nextVmState = data->currVmState;
  nextVmState.events = 0;

  if (nextVmState.pos.domain == DVD_DOMAIN_VTS_TITLE)
  {
    const dvd_title_t* t = vmg->table_of_titles->titles + (nextVmState.chapter.titleNr - 1);

    ret |= ka_nav_key_used;

    switch(key)
    {
      case 0x1ac: // ctrl left arrow, previous chapter
      {
        int titleNr, chapterNr;

        if (nextVmState.chapter.chapterNr > 1)
        {
          titleNr = nextVmState.chapter.titleNr;
          chapterNr = nextVmState.chapter.chapterNr - 1;
          dvd_setChapter(&nextVmState, (titleNr << 16) | chapterNr);
        }
        else if (nextVmState.chapter.titleNr > 1)
        {
          titleNr = nextVmState.chapter.titleNr - 1;
          t = vmg->table_of_titles->titles + (titleNr - 1);
          chapterNr = t->nr_chapters;
          dvd_setChapter(&nextVmState, (titleNr << 16) | chapterNr);
        }
      }
      break;
      case 0x1ad: // ctrl right arrow, next chapter
      {
        int titleNr, chapterNr;

        if (nextVmState.chapter.chapterNr < t->nr_chapters)
        {
          titleNr = nextVmState.chapter.titleNr;
          chapterNr = nextVmState.chapter.chapterNr + 1;
          dvd_setChapter(&nextVmState, (titleNr << 16) | chapterNr);
        }
        else if (nextVmState.chapter.titleNr < vmg->table_of_titles->nr_titles)
        {
          titleNr = nextVmState.chapter.titleNr + 1;
          chapterNr = 1;
          dvd_setChapter(&nextVmState, (titleNr << 16) | chapterNr);
        }
      }
      break;
      default:
        ret &= ~ka_nav_key_used;
    }
  }

  if ((ret & ka_nav_key_used) == 0)
  {
    switch(key)
    {
      case 0x1cd: // Ins
      {
        ret |= ka_nav_key_used;

        dvd_setChapter(&nextVmState, (0 << 16) | 2);
      }
      break;
      case 0x1e: // Home
      {
        ret |= ka_nav_key_used;

        if (-1 == dvd_setChapter(&nextVmState, (0 << 16) | 3))
          dvd_setChapter(&nextVmState, (0 << 16) | 2);
      }
      break;
      case 0x18c: // left arrow
      {
        const dvd_pci_t* cur = &data->curPci;
        int button = get_highlight_button(cur, &nextVmState, rtc);

        if (cur->highlight.nr_buttons)
          ret |= ka_nav_key_used;

        if (button)
        {
          button = cur->buttons[button - 1].new_button.left;

          if (button)
          {
            ret |= ka_nav_redraw_frame;

		    data->actionButton = 0;
            set_highlight_button(cur, &nextVmState, button, "Left");

            if (cur->buttons[button - 1].auto_action == 1)
            {
              data->actionButton = button;
              dvd_vm_action(&nextVmState, (uint8_t*) cur->buttons[button - 1].cmd);
            }
          }
        }
      }
      break;
      case 0x18d: // right arrow
      {
        const dvd_pci_t* cur = &data->curPci;
        int button = get_highlight_button(cur, &nextVmState, rtc);

        if (cur->highlight.nr_buttons)
          ret |= ka_nav_key_used;

        if (button)
        {
          button = cur->buttons[button - 1].new_button.right;

          if (button)
          {
            ret |= ka_nav_redraw_frame;

		    data->actionButton = 0;
            set_highlight_button(cur, &nextVmState, button, "Right");

            if (cur->buttons[button - 1].auto_action == 1)
            {
              data->actionButton = button;
              dvd_vm_action(&nextVmState, (uint8_t*) cur->buttons[button - 1].cmd);
            }
          }
        }
      }
      break;
      case 0x18e: // down arrow
      {
        const dvd_pci_t* cur = &data->curPci;
        int button = get_highlight_button(cur, &nextVmState, rtc);

        if (cur->highlight.nr_buttons)
          ret |= ka_nav_key_used;

        if (button)
        {
          button = cur->buttons[button - 1].new_button.down;

          if (button)
          {
            ret |= ka_nav_redraw_frame;

		    data->actionButton = 0;
            set_highlight_button(cur, &nextVmState, button, "Bottom");

            if (cur->buttons[button - 1].auto_action == 1)
            {
              data->actionButton = button;
              dvd_vm_action(&nextVmState, (uint8_t*) cur->buttons[button - 1].cmd);
            }
          }
        }
      }
      break;
      case 0x18f: // up arrow
      {
        const dvd_pci_t* cur = &data->curPci;
        int button = get_highlight_button(cur, &nextVmState, rtc);

        if (cur->highlight.nr_buttons)
          ret |= ka_nav_key_used;

        if (button)
        {
          button = cur->buttons[button - 1].new_button.up;

          if (button)
          {
            ret |= ka_nav_redraw_frame;

		    data->actionButton = 0;
            set_highlight_button(cur, &nextVmState, button, "Up");

            if (cur->buttons[button - 1].auto_action == 1)
            {
              data->actionButton = button;
              dvd_vm_action(&nextVmState, (uint8_t*) cur->buttons[button - 1].cmd);
            }
          }
        }
      }
      break;
      case 13: // Return
      {
        const dvd_pci_t* cur = &data->curPci;
        int button = get_highlight_button(cur, &nextVmState, rtc);

        if (cur->highlight.nr_buttons)
          ret |= ka_nav_key_used;

        if (button)
        {
          data->actionButton = button;
          dvd_vm_action(&nextVmState, (uint8_t*) cur->buttons[button - 1].cmd);
        }
      }
      break;
/*
      case 0:
      {
        const dvd_pci_t* cur = &data->curPci;
        int button = get_highlight_button(cur, &nextVmState, rtc);

        // Refresh menu
        ret &= ~ka_nav_key_used;
        if (button)
          ret |= ka_nav_redraw_frame;
      }
      break;
*/      default:
        ret &= ~ka_nav_key_used;
    }
  }

  if (nextVmState.events & (DVDEV_EXIT | DVDEV_CELL_CHANGE | DVDEV_POS_CHANGE))
  {
    dvd_setDisplayCell(data, &nextVmState, 0ll);

    if (nextVmState.events & (DVDEV_CELL_CHANGE | DVDEV_POS_CHANGE))
      ret |= ka_nav_discontinuity;
  }
  else
  {
    data->currVmState = nextVmState;
  }

  if (nextVmState.events & DVDEV_AUDIO_CHANGE)
    ret |= ka_nav_audio_change;
  if (nextVmState.events & DVDEV_SUBS_CHANGE)
    ret |= ka_nav_subtitle_change;
  if (nextVmState.events & DVDEV_ANGLE_CHANGE)
    ret |= ka_nav_angle_change;

  return ret;
}

/**
 * Update menu rect.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  rtc     Current rtc
 * @param  rect    Subtitle, updated on exit with final rect and colors (menus).
 * @param  step    Increasing step starting with 0, allows multiple draws from a single subtitle.
 *
 * Returns step + 1 if menu rect to display, 0 otherwise.
 */
static int demux_dvd_subtitleRect(ka_demux_t* pdemux, uint32_t rtc, ka_sub_rect* rect, int step)
{
  data_t* data = (data_t*) pdemux;
  const dvd_pci_t* cur = &data->curPci;
  ka_demux_subtitle_info_t info;

  // Ensure menu highlight control is up to date
  update_pci(data, rtc);

  // Check if subtitle corresponds to currently active subtitle stream
  demux_dvd_getSubsInfo(pdemux, rect->forceDisplay ? -1 : -2, &info);

  if (rect->typ != info.kid)
    return 0;

  // If forced and no buttons show the whole thing
  if (rect->forceDisplay
  &&  (cur->highlight.nr_buttons == 0))
    return ((step == 0) && (rect->nrColors > 0));

  // If no buttons show the whole thing
  if (!rect->forceDisplay
  ||  (cur->highlight.nr_buttons == 0))
    return ((step == 0) && (rect->nrColors > 0));

  int button = get_highlight_button(cur, &data->currVmState, rtc);
  if (!button)
    return 0;

  // Find normal group
  if (cur->highlight.groups.nr_groups > 1)
  {
    int group, offset;

    if (cur->highlight.groups.nr_groups == 2)
      offset = 18;
    else
      offset = 12;

    for (group = cur->highlight.groups.nr_groups - 1; group >= 0; group--)
    {
      if (cur->highlight.groups.type[group] & 1)
      {
        button += offset * group;
        break;
      }
    }

  }

  button--;

  // Steps are 0 Above btn, 1 Left of btn, 2 Right of btn, 3 Below btn, 4 btn
  if (step > 4)
    return 0;

  // We have to redraw the whole subtitle with its current colors
  // except the button which has its own colors, so we will decompose into up to 5 rectangles
  if (step == 0)
  {
    if ((cur->buttons[button].y_start <= rect->y0) || (rect->nrColors == 0))
      step = 1;
    else
    {
      // Below btn
      rect->y1 = cur->buttons[button].y_start;
      return step + 1;
    }
  }

  if (step == 1)
  {
    if ((cur->buttons[button].x_start <= rect->x0) || (rect->nrColors == 0))
      step = 2;
    else
    {
      // Same height as btn, left of it
      rect->data += rect->bpr * (cur->buttons[button].y_start - rect->y0);

      rect->x1 = cur->buttons[button].x_start;
      rect->y0 = cur->buttons[button].y_start;
      rect->y1 = 1 + cur->buttons[button].y_end;
      return step + 1;
    }
  }

  if (step == 2)
  {
    if ((cur->buttons[button].x_end + 1 >= rect->x1) || (rect->nrColors == 0))
      step = 3;
    else
    {
      // Same height as btn, right of it
      rect->data += cur->buttons[button].x_end + 1 - rect->x0;
      rect->data += rect->bpr * (cur->buttons[button].y_start - rect->y0);

      rect->x0 = 1 + cur->buttons[button].x_end;
      rect->y0 = cur->buttons[button].y_start;
      rect->y1 = 1 + cur->buttons[button].y_end;
      return step + 1;
    }
  }

  if (step == 3)
  {
    if ((cur->buttons[button].y_end + 1 >= rect->y1) || (rect->nrColors == 0))
      step = 4;
    else
    {
      // Above button
      rect->data += rect->bpr * (cur->buttons[button].y_end + 1 - rect->y0);

      rect->y0 = 1 + cur->buttons[button].y_end;
      return step + 1;
    }
  }

  {
    const dvd_color_t* palette = data->currVmState.pos.pgc->color_lookup;
    const dvd_pci_colors* butcol;
    dvd_color_t color;
    uint8_t alpha;

    int tbl = cur->buttons[button].color_table;

    // No table ? Invisible
    if (!tbl)
      return 0;

    tbl--;

    if (data->actionButton != (button + 1)) // button triggered
      butcol = &cur->highlight.selection[tbl];
    else
      butcol = &cur->highlight.action[tbl];

    // Invisible ?
    if ((butcol->alpha_background == 0)
    &&  (butcol->alpha_pattern == 0)
    &&  (butcol->alpha_emphasis1 == 0)
    &&  (butcol->alpha_emphasis2 == 0))
      return 0;

    color = palette[butcol->color_background];
    alpha = butcol->alpha_background;
    rect->palette[0].y = color.y;
    rect->palette[0].u = color.u;
    rect->palette[0].v = color.v;
    rect->palette[0].a = alpha + (alpha << 4);

    color = palette[butcol->color_pattern];
    alpha = butcol->alpha_pattern;
    rect->palette[1].y = color.y;
    rect->palette[1].u = color.u;
    rect->palette[1].v = color.v;
    rect->palette[1].a = alpha + (alpha << 4);

    color = palette[butcol->color_emphasis1];
    alpha = butcol->alpha_emphasis1;
    rect->palette[2].y = color.y;
    rect->palette[2].u = color.u;
    rect->palette[2].v = color.v;
    rect->palette[2].a = alpha + (alpha << 4);

    color = palette[butcol->color_emphasis2];
    alpha = butcol->alpha_emphasis2;
    rect->palette[3].y = color.y;
    rect->palette[3].u = color.u;
    rect->palette[3].v = color.v;
    rect->palette[3].a = alpha + (alpha << 4);

    rect->nrColors = 4;

    rect->data += cur->buttons[button].x_start - rect->x0;
    rect->data += rect->bpr * (cur->buttons[button].y_start - rect->y0);

    rect->x0 = cur->buttons[button].x_start;
    rect->x1 = 1 + cur->buttons[button].x_end;
    rect->y0 = cur->buttons[button].y_start;
    rect->y1 = 1 + cur->buttons[button].y_end;
  }

  return step + 1;
}

static inline void reduce(int* pwidth, int* pheight)
{
  int width = *pwidth;
  int height = *pheight;

  // find greatest common divisor
  while (width)
  {
    int tmp = width;
    width = height % tmp;
    height = tmp;
  }

  *pwidth /= height;
  *pheight /= height;
}

/**
 * Updates video info.
 *
 * @param  pdemux  Pointer to demuxer data.
 * @param  info    Pointer to video info.
 */
static void demux_dvd_updateVInfo(ka_demux_t* pdemux, ka_vinfo_t* info)
{
  data_t* data = (data_t*) pdemux;
  int width, height;
  int cwidth, cheight;
  uint16_t attrs = 0;

  switch (data->currVmState.pos.domain)
  {
    case DVD_DOMAIN_FIRST_PLAY:
    case DVD_DOMAIN_VMG_MENU:
      attrs = data->dvd_desc->ifos[0].vmg->menu_attributes.video_attributes;
    break;
    case DVD_DOMAIN_VTS_MENU:
      attrs = data->dvd_desc->ifos[data->currVmState.pos.vtsNr].vts->menu_attributes.video_attributes;
    break;
    case DVD_DOMAIN_VTS_TITLE:
      attrs = data->dvd_desc->ifos[data->currVmState.pos.vtsNr].vts->title_attributes.video_attributes;
    break;
  }

  if ((attrs & 0xc00) == 0xc00)
  {
    cwidth = 16;
    cheight = 9;
  }
  else
  {
    cwidth = 4;
    cheight = 3;
  }
  width = cwidth * info->picture.height;
  height = cheight * info->picture.width;
  reduce(&width, &height);

  info->aspect_ratio.width = width;
  info->aspect_ratio.height = height;
}

/**
 * Deletes a demuxer.
 *
 * @param  pdemux  Pointer to demuxer data.
 */
static void delete_demux_dvd(ka_demux_t** ppdemux)
{
  data_t* data = (data_t*) *ppdemux;
  *ppdemux = NULL;

  if (!data)
    return;

  if (data->mpegPS != NULL)
    data->mpegPS->vptr->FNdelete(&data->mpegPS);

  ka_delete_input(&data->pInput);
  dvd_desc_free(&data->dvd_desc);

  ka_mem_free(data);
}

static const kav_demux_t demux_DVD =
{ .name = "DVD"
, .FNdelete        = delete_demux_dvd
, .FNreset         = demux_dvd_reset
, .FNclear         = demux_dvd_clear
, .FNparse         = demux_dvd_parse
, .FNgetPos        = demux_dvd_getPos
, .FNsetPos        = demux_dvd_setPos
, .FNsetChapter    = demux_dvd_setChapter
, .FNgetChapterPos = demux_dvd_getChapterPos
, .FNgetAudioInfo  = demux_dvd_getAudioInfo
, .FNselectAudio   = demux_dvd_selectAudio
, .FNgetSubsInfo   = demux_dvd_getSubsInfo
, .FNselectSubs    = demux_dvd_selectSubs
, .FNgetAngle      = demux_dvd_getAngle
, .FNsetAngle      = demux_dvd_setAngle
, .FNfillBuffer    = demux_dvd_fillBuffer
, .FNpush          = demux_dvd_push
, .FNparams        = demux_dvd_params
, .FNgetWaitTime   = demux_dvd_getWaitTime
, .FNsectionDone   = demux_dvd_sectionDone
, .FNnavigate      = demux_dvd_navigate
, .FNsubtitleRect  = demux_dvd_subtitleRect
, .FNupdateVInfo   = demux_dvd_updateVInfo
};

/**
 * Checks for DVD folder/device.
 *
 * @param  ppdemux  Pointer where to fill in with address of DVD demuxer.
 * @param  param    Pameters (error block, filename, stack, work buffer).
 *
 * Returns
 *   ka_demux_select_found if DVD
 *   ka_demux_select_none  if not an DVD
 *   ka_demux_select_error if error occured
 */

ka_demux_select demux_selector_dvd(ka_demux_t** ppdemux, ka_demux_params_t* params)
{
  data_t* data = ka_mem_calloc(sizeof(*data));
  ka_demux_select ret = ka_demux_select_none;

  if (!data)
  {
    ka_error_fill(params->pErrorBlock, ka_error_nomem);
    return ka_demux_select_error;
  }

  *ppdemux = &data->hdr;
  data->hdr.vptr = &demux_DVD;
  data->hdr.vars = params->pVars;
  data->hdr.pErrorBlock = params->pErrorBlock;

  data->pInput = params->pInput = ka_new_indvd(data->hdr.pErrorBlock);
  if (!data->pInput)
  {
    ret = ka_demux_select_error;
    ka_error_fill(data->hdr.pErrorBlock, ka_error_nomem);
    goto fail;
  }

  {
    int rc = dvd_desc_load(data->pInput, params->pFilename, &data->dvd_desc, 1);
    if (rc)
    {
      ret = (rc < 0) ? ka_demux_select_error : ka_demux_select_none;
      goto fail;
    }
  }

  ret = demux_selector_mpeg_dvd(&data->mpegPS, params);
  if (ret != ka_demux_select_found)
    goto  fail;
  data->mpegPS->demuxSection = data->hdr.demuxSection;

  demux_dvd_reset(*ppdemux, params->pStack);

  if (!data->currVmState.pos.pgc)
  {
    ret = ka_demux_select_error;
    ka_error_fill(params->pErrorBlock, "error200:DVD without titles");
    goto  fail;
  }

  return ka_demux_select_found;

fail:
  delete_demux_dvd((ka_demux_t**) &data);
  *ppdemux = NULL;

  return ret;
}

