#include "dvd_load_ifo.h"

#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include "config.h"
#include "ka_log.h"

/**
 * Updates programNr and cell lb limits to reflect new valid cellNr.
 * Updates system registers to ensure they remain correct.
 */
static void dvd_vm_updateCellInfo(dvd_vm_state_t* vm, dvd_pos_t* jump)
{
  const dvd_pgc_t* pgc = jump->pgc;

  if (pgc == NULL)
  {
    if (vm->workMode & DVDVM_LOGN)
      ka_logn("%sCell info: no pgc\n", vm->logPrefix);
    return;
  }

  // Default for next cell
  jump->nextCellNr = jump->cellNr + 1;

  // Fill in cell limits
  if (jump->cellNr <= pgc->nr_cells)
  {
    // Take into account multi-angle block
    const dvd_cell_playback_t* cp = &pgc->cell_playbacks[jump->cellNr - 1];
    const dvd_cell_playback_t* rcp = cp;

    if ((cp->category & DVD_CELL_BLOCK_TYPE_MASK) == DVD_CELL_BLOCK_TYPE_ANGLE)
    {
      int first = jump->cellNr;
      int last;

      // Locate first cell of block
      while ((first > 1)
             && ((cp->category & DVD_CELL_BLOCK_TYPE_MASK) == DVD_CELL_BLOCK_TYPE_ANGLE)
             && ((cp->category & DVD_CELL_TYPE_MASK) != DVD_CELL_TYPE_FIRST_OF_ANGLE))
      {
        first--;
        cp--;
      }

      // Locate last cell of block
      last = jump->cellNr;
      cp = rcp;
      while ((last <= pgc->nr_cells)
             && ((cp->category & DVD_CELL_BLOCK_TYPE_MASK) == DVD_CELL_BLOCK_TYPE_ANGLE)
             && ((cp->category & DVD_CELL_TYPE_MASK) != DVD_CELL_TYPE_LAST_OF_ANGLE))
      {
        last++;
        cp++;
      }

      // If block limits found, jump to corresponding angle cell
      if ((first > 0)
      &&  (last <= pgc->nr_cells)
      &&  (first + vm->system.regs[DVDREG_ANGLE_NR] - 1 <= last))
      {
        jump->cellNr = first + vm->system.regs[DVDREG_ANGLE_NR] - 1;
        // Next cell if after the block
        jump->nextCellNr = last + 1;
      }

      cp = &pgc->cell_playbacks[jump->cellNr - 1];
    }

    jump->lbCellStart = cp->lb_first_vobu_start;
    jump->lbCellEnd   = cp->lb_last_vobu_last + 1;
  }
  else
  {
    jump->lbCellStart = jump->lbCellEnd = 0;
  }

  if (jump->nextCellNr <= pgc->nr_cells)
  {
    // Take into account multi-angle block
    const dvd_cell_playback_t* cp = &pgc->cell_playbacks[jump->nextCellNr - 1];

    if ((cp->category & DVD_CELL_BLOCK_TYPE_MASK) == DVD_CELL_BLOCK_TYPE_ANGLE)
    {
      int first = jump->cellNr;
      int last;

      // Locate last cell of block
      last = jump->cellNr;
      while ((last <= pgc->nr_cells)
             && ((cp->category & DVD_CELL_BLOCK_TYPE_MASK) == DVD_CELL_BLOCK_TYPE_ANGLE)
             && ((cp->category & DVD_CELL_TYPE_MASK) != DVD_CELL_TYPE_LAST_OF_ANGLE))
      {
        last++;
        cp++;
      }

      // If block limits found, jump to corresponding angle cell
      if ((first > 0)
      &&  (last <= pgc->nr_cells)
      &&  (first + vm->system.regs[DVDREG_ANGLE_NR] - 1 <= last))
      {
        jump->nextCellNr += vm->system.regs[DVDREG_ANGLE_NR] - 1;
      }
    }
  }

  // Recalculate program number from cell number
  jump->programNr = 0;

  while (   (jump->programNr < pgc->nr_programs)
         && (jump->cellNr >= pgc->program_cellnrs[jump->programNr]))
    jump->programNr++;

  switch (jump->domain)
  {
    case DVD_DOMAIN_VTS_TITLE:
    {
      // Determine title and chapter numbers from current program.
      // Assumes 1 Chapter is 1 program.
      const dvd_vmg_ifo_t* vmg = vm->vmg;
      const dvd_vts_ifo_t* vts = vm->dvdDesc->ifos[jump->vtsNr].vts;

      for (int i = 1; i <= vmg->table_of_titles->nr_titles; i++)
      {
        dvd_title_t* t = &vmg->table_of_titles->titles[i - 1];

        if (t->vts_nr == jump->vtsNr)
        {
          const dvd_part_t* pmin = vts->title_parts->parts + vts->title_parts->parts_indexes[t->vts_title_nr - 1];
          const dvd_part_t* pmax = pmin + t->nr_chapters;

          for (const dvd_part_t* p = pmin; p < pmax; p++)
          {
            if ((p->program_chain_nr == jump->programChainNr)
            &&  (p->program_nr == jump->programNr))
            {
              vm->chapter.titleNr = i;
              vm->chapter.vtsTitleNr = t->vts_title_nr;
              vm->chapter.chapterNr = 1 + (p - pmin);
              break;
            }
          }
        }
      }

      // Update impacted system registers
      vm->system.regs[DVDREG_TITLE_NR] = vm->chapter.titleNr;
      vm->system.regs[DVDREG_VTS_TITLE_NR] = vm->chapter.vtsTitleNr;
      vm->system.regs[DVDREG_CHAPTER_NR] = vm->chapter.chapterNr;
      vm->system.regs[DVDREG_PROGRAM_CHAIN_NR] = jump->programChainNr;

      if (vm->workMode & DVDVM_LOGN)
      {
        if (jump->programNr)
          ka_logn( "%sCell title %d, chapter %d: vts %d, pgcn %d, pgn %d, cell %d, lb %08x -> %08x\n"
                 , vm->logPrefix, vm->chapter.titleNr, vm->chapter.chapterNr, jump->vtsNr
                 , jump->programChainNr, jump->programNr, jump->cellNr, jump->lbCellStart, jump->lbCellEnd);
        else
          ka_logn( "%sCell title %d, chapter %d: vts %d, pgcn %d (no cells)\n"
                 , vm->logPrefix, vm->chapter.titleNr, vm->chapter.chapterNr, jump->vtsNr, jump->programChainNr);
      }
    }
    break;
    case DVD_DOMAIN_FIRST_PLAY:
    {
      if (vm->workMode & DVDVM_LOGN)
      {
        if (jump->programNr)
          ka_logn( "%sCell first play: pgcn %d, pgn %d, cell %d, lb %08x -> %08x\n"
                 , vm->logPrefix, jump->programChainNr, jump->programNr, jump->cellNr, jump->lbCellStart, jump->lbCellEnd);
        else
          ka_logn( "%sCell first play: pgcn %d (no cells)\n", vm->logPrefix, jump->programChainNr);
      }
    }
    break;
    default:
    {
      vm->menu.menuId = jump->vtsChapterNr;

      if (vm->workMode & DVDVM_LOGN)
      {
        if (jump->programNr)
          ka_logn( "%sCell menu %d of vts %d: pgcn %d, pgn %d, cell %d, lb %08x -> %08x\n"
                 , vm->logPrefix, vm->menu.menuId, jump->vtsNr
                 , jump->programChainNr, jump->programNr, jump->cellNr, jump->lbCellStart, jump->lbCellEnd);
        else
          ka_logn( "%sCell menu %d of vts %d: pgcn %d (no cells)\n"
                 , vm->logPrefix, vm->menu.menuId, jump->vtsNr, jump->programChainNr);
      }
    }
  }

  vm->pos = *jump;
}

static void dvd_logjump(const char* prefix, dvd_pos_t* jump)
{
  switch (jump->domain)
  {
    case DVD_DOMAIN_VTS_TITLE:
    {
      ka_log(ka_log_error | ka_log_nav, "%s vts %d, pgcn %d, pgn %d, cell %d"
             , prefix, jump->vtsNr, jump->programChainNr, jump->programNr, jump->cellNr);
    }
    break;
    case DVD_DOMAIN_FIRST_PLAY:
    {
      ka_log(ka_log_error | ka_log_nav, "%s first play pgcn %d, pgn %d, cell %d"
             , prefix, jump->programChainNr, jump->programNr, jump->cellNr);
    }
    break;
    case DVD_DOMAIN_VMG_MENU:
    {
      ka_log(ka_log_error | ka_log_nav, "%s menu %d of vmg, pgcn %d, pgn %d, cell %d"
             , prefix, jump->vtsChapterNr, jump->programChainNr, jump->programNr, jump->cellNr);
    }
    default:
    {
      ka_log(ka_log_error | ka_log_nav, "%s menu %d of vts %d, pgcn %d, pgn %d, cell %d"
             , prefix, jump->vtsChapterNr, jump->vtsNr, jump->programChainNr, jump->programNr, jump->cellNr);
    }
  }
}

/**
 * Prepare jump to first play.
 */
static void dvd_vm_setJumpFirstPlay(const dvd_vm_state_t* vm, dvd_pos_t* jump)
{
  jump->domain = DVD_DOMAIN_FIRST_PLAY;
  jump->lut = NULL;

  jump->vtsNr = 0;
  jump->vtsTitleNr = 0;
  jump->vtsChapterNr = 0;
  jump->programChainNr = 1;
  jump->programNr = 1;

  const dvd_pgc_t* pgc = vm->vmg->first_play_pgc;
  jump->pgc = pgc;
  if ((pgc != NULL) && (pgc->nr_cells > 0))
  {
    jump->cellNr = pgc->program_cellnrs[0];
    if (!jump->cellNr) jump->cellNr = jump->pgc->nr_cells + 1;
  }
  else
    jump->cellNr = 1;
}

/**
 * Prepare jump to VTS part.
 * If vtsTitleNr > 0 partNr is a chapterNr else it is a partNr from the VTS title parts table.
 *
 * Return 0 if OK, -1 if Invalid.
 */
static int dvd_vm_setJumpVtsTitlePart(dvd_vm_state_t* vm, dvd_pos_t* jump, int vtsNr, int vtsTitleNr, int partNr)
{
  jump->domain = DVD_DOMAIN_VTS_TITLE;
  jump->lut = NULL;
  jump->vtsNr = vtsNr;

  if (vtsNr && (vtsNr <= vm->dvdDesc->nr_ifos))
  {
    const dvd_vts_ifo_t* vts = vm->dvdDesc->ifos[vtsNr].vts;
    // Chapter mode ?
    if (vtsTitleNr)
    {
      jump->vtsTitleNr = vtsTitleNr;
      jump->vtsChapterNr = partNr;
      partNr += vts->title_parts->parts_indexes[vtsTitleNr - 1];
    }

    if (partNr && (partNr <= vts->title_parts->nr_parts))
    {
      const dvd_part_t* part = &vts->title_parts->parts[partNr - 1];

      jump->programChainNr = part->program_chain_nr;
      jump->programNr = part->program_nr;

      jump->lut = vts->title_pgcs;

      const dvd_pgc_t* pgc = jump->lut->pgc_lus[jump->programChainNr - 1].pgc;
      jump->pgc = pgc;
      jump->cellNr = pgc->program_cellnrs[part->program_nr - 1];
      if (!jump->cellNr) jump->cellNr = jump->pgc->nr_cells + 1;

      return 0;
    }
  }

  ka_log(ka_log_error | ka_log_nav, "Jump failed, VTS %d, VTSTitle %d part %d", vtsNr, vtsTitleNr, partNr);
  return -1;
}

/**
 * Prepare jump to Title/Chapter.
 *
 * Return 0 if OK, -1 if Invalid.
 */
static int dvd_vm_setJumpTitle(dvd_vm_state_t* vm, dvd_pos_t* jump, int titleNr, int chapterNr)
{
  if (titleNr && (titleNr <= vm->vmg->table_of_titles->nr_titles))
  {
    const dvd_title_t* t = vm->vmg->table_of_titles->titles + (titleNr - 1);

    return dvd_vm_setJumpVtsTitlePart(vm, jump, t->vts_nr, t->vts_title_nr, chapterNr);
  }

  ka_log(ka_log_error | ka_log_nav, "Jump failed, Title %d Chapter %d", titleNr, chapterNr);
  return -1;
}

/**
 * Prepare jump to Menu.
 *
 * Return 0 if OK, -1 if Invalid.
 */
static int dvd_vm_setJumpMenu(dvd_vm_state_t* vm, dvd_pos_t* jump, int vtsNr, int menuId, int pgcNr)
{
  const dvd_pgc_lu_table_t* lut = dvd_desc_extractMenuLut(vm->dvdDesc, vtsNr, vm->lang);

  if (lut != NULL)
  {
    if (pgcNr == 0)
    {
      // Find pgc corresponding to menu
      for (int i = 0; i < lut->nr_pgc_lus; i++)
      {
        const dvd_pgc_lu_t* lu = &lut->pgc_lus[i];

        // Start of menu
        if (lu->category & DVD_PGC_LU_ISENTRY)
        {
          int id = lu->category & 0xf;

          if (id == menuId)
          {
            // Matches menu number
            pgcNr = i + 1;
            break;
          }
        }
      }
    }

    if (pgcNr && (pgcNr <= lut->nr_pgc_lus))
    {
      if (vtsNr == 0)
        jump->domain = DVD_DOMAIN_VMG_MENU;
      else
        jump->domain = DVD_DOMAIN_VTS_MENU;
      jump->lut = lut;
      jump->vtsNr = vtsNr;
      jump->vtsTitleNr = 0;
      jump->vtsChapterNr = menuId;
      jump->programChainNr = pgcNr;
      jump->programNr = 1;

      const dvd_pgc_t* pgc = lut->pgc_lus[pgcNr - 1].pgc;
      jump->pgc = pgc;
      if ((pgc != NULL) && (pgc->nr_cells > 0))
      {
        jump->cellNr = pgc->program_cellnrs[0];
        if (!jump->cellNr) jump->cellNr = jump->pgc->nr_cells + 1;
      }
      else
        jump->cellNr = 1;

      return 0;
    }
    else
    {
      ka_log(ka_log_error | ka_log_nav, "Jump failed, menu of VTS %d, menu %d pgcNr %d", vtsNr, menuId, pgcNr);
    }
  }
  else
  {
    ka_log(ka_log_error | ka_log_nav, "Jump failed, menu of VTS %d, menu %d has no lut", vtsNr, menuId);
  }

  return -1;
}

/**
 * Reinitialise VM.
 */
void dvd_vm_reset(dvd_vm_state_t* vm, const dvd_desc_t* dvdDesc, int flags)
{
  memset(vm, 0, sizeof(vm));
  // Avoid logging till we finish reset.
  vm->workMode = 0;
  vm->dvdDesc = dvdDesc;
  vm->vmg = dvdDesc->ifos[0].vmg;
  vm->logPrefix = "";
  if (config.language[0])
  {
    vm->lang[0] = tolower(config.language[0]);
    vm->lang[1] = tolower(config.language[1]);
  }
  else
  {
    vm->lang[0] = 0xff;
    vm->lang[1] = 0xff;
  }

  vm->system.regs[DVDREG_PREF_MENU_LANG     ] = (vm->lang[0] << 8) | vm->lang[1];
  vm->system.regs[DVDREG_AUDIO_STREAM_NR    ] = 15;
  vm->system.regs[DVDREG_SUBS_STREAM_NR     ] = 62; // none
  vm->system.regs[DVDREG_ANGLE_NR           ] = 1;
  vm->system.regs[DVDREG_TITLE_NR           ] = 1;
  vm->system.regs[DVDREG_VTS_TITLE_NR       ] = 1;
  vm->system.regs[DVDREG_PROGRAM_CHAIN_NR   ] = 1;
  vm->system.regs[DVDREG_CHAPTER_NR         ] = 1;
  vm->system.regs[DVDREG_HIGHLIGHT_BUTTON_NR] = 1 << 10;
  vm->system.regs[DVDREG_PGC_FRO_NAV_TIMER  ] = 1;
  vm->system.regs[DVDREG_PARENTAL_COUNTRY   ] = 0xffff;
  vm->system.regs[DVDREG_PARENTAL_LEVEL     ] = 15;
  vm->system.regs[DVDREG_VIDEO_MODES        ] = (1 << 10) | (0 << 8);
  vm->system.regs[DVDREG_AUDIO_CAPABILITIES ] = 0xf3f3; // not SDDS nor DTS
  vm->system.regs[DVDREG_PREF_AUDIO_LANG    ] = vm->system.regs[DVDREG_PREF_MENU_LANG];
  vm->system.regs[DVDREG_PREF_SUBS_LANG     ] = vm->system.regs[DVDREG_PREF_MENU_LANG];
  vm->system.regs[DVDREG_PLAYER_REGIONS_MASK] = 0xffff;

  // Set position to first chapter of first program
  dvd_vm_setJumpTitle(vm, &vm->jump, 1, 1);
  dvd_vm_updateCellInfo(vm, &vm->jump);
  // Set real workking mode
  vm->workMode = flags;
  vm->events = 0;
}

/**
 * Turn logging on/off.
 */
void dvd_vm_setLogState(dvd_vm_state_t* vm, int on)
{
  if (on)
    vm->workMode |= DVDVM_LOGN;
  else
    vm->workMode &= ~DVDVM_LOGN;
}

/**
 * Set prefix to insert before any command log, used when building the description of the DVD.
 */
void dvd_vm_setLogPrefix(dvd_vm_state_t* vm, const char* logPrefix)
{
  vm->logPrefix = logPrefix;
}

/**
 * Execute the current program chain's pre-commands.
 */
static void dvd_vm_playProgramChainPreCmds(dvd_vm_state_t* vm, const dvd_pgc_t* pgc)
{
  if (pgc == NULL)
    return;

  // Play pre commands which may trigger a jump
  if ((pgc->commands != NULL)
  &&  (pgc->commands->nr_pre_cmds > 0))
  {
    const uint8_t* pc = (uint8_t*) pgc->commands->commands;

    dvd_vm_playCmds(vm, pgc->commands->nr_pre_cmds, pc);
  }
}

/**
 * Execute the current program chain's post-commands.
 */
static void dvd_vm_playProgramChainPostCmds(dvd_vm_state_t* vm, const dvd_pgc_t* pgc)
{
  if (pgc == NULL)
    return;

  // Play pre commands which may trigger a jump
  if ((pgc->commands != NULL)
  &&  (pgc->commands->nr_post_cmds > 0))
  {
    const uint8_t* pc = (uint8_t*) pgc->commands->commands;
    pc += 8 * pgc->commands->nr_pre_cmds;

    dvd_vm_playCmds(vm, pgc->commands->nr_post_cmds, pc);
  }
}

/**
 * Execute the current cell's (post-)commands.
 */
static void dvd_vm_playCellPostCmd(dvd_vm_state_t* vm, const dvd_pgc_t* pgc, uint32_t cmdNr)
{
  if (pgc == NULL)
    return;

  // Play cell command which may trigger a jump
  if ((pgc->commands != NULL)
  &&  (cmdNr > 0)
  &&  (pgc->commands->nr_cell_cmds >= cmdNr))
  {
    const uint8_t* pc = (uint8_t*) pgc->commands->commands;
    pc += 8 * (pgc->commands->nr_pre_cmds + pgc->commands->nr_post_cmds + cmdNr - 1);

    if (vm->workMode & DVDVM_LOGN)
      ka_logn("%sCell Cmd:\n", vm->logPrefix);
    dvd_vm_playCmds(vm, 1, pc);
  }
}

/**
 * Handle jump commands, and process pre/post program chain commands when required.
 *
 * Pre program chain commands may trigger an unwanted jump when the user selects a title/chapter
 * hence the playPreCmds parameter way be set to false to avoid the jump.
 */
static void dvd_vm_jump(dvd_vm_state_t* vm, int playPreCmds)
{
  dvd_pos_t* jump = &vm->jump;
  dvd_pos_t savePos = vm->pos;
  int steps = 0;

  if (vm->events & DVDEV_POS_CHANGE)
    ka_log(ka_log_error | ka_log_nav, "JUMP but already POS_CHANGE");
  if (vm->events & DVDEV_EXIT)
  {
    ka_log(ka_log_error | ka_log_nav, "DVD Exit");
    return;
  }

  // Loop while following a jumps triggers another jump
  while (vm->events & DVDEV_JUMP)
  {
    // Reset jump flags to see if a new one will be triggered
    vm->events &= ~DVDEV_JUMP;

    steps++;

    // Exit if it seems we are in an infinite loop
    if ((steps > 1000) || (vm->events & DVDEV_EXIT))
    {
      vm->events |= DVDEV_EXIT;
      ka_log(ka_log_error | ka_log_nav, "DVD, infinite loop in jump");
      return;
    }

    if ((vm->events & DVDEV_JUMP_IN_PGC) == 0)
    {
      // New program chain, we must execute the Pre-commands

      // Jump lut & pgc must be recalculated
      jump->lut = NULL;
      jump->pgc = NULL;

      switch (jump->domain)
      {
        case DVD_DOMAIN_FIRST_PLAY:
        {
          jump->pgc = vm->vmg->first_play_pgc;
        }
        break;
        case DVD_DOMAIN_VMG_MENU:
        {
          jump->lut = dvd_desc_extractMenuLut(vm->dvdDesc, 0, vm->lang);
        }
        break;
        case DVD_DOMAIN_VTS_MENU:
        {
          jump->lut = dvd_desc_extractMenuLut(vm->dvdDesc, jump->vtsNr, vm->lang);
        }
        break;
        case DVD_DOMAIN_VTS_TITLE:
        {
           if (jump->vtsNr && (jump->vtsNr <= vm->dvdDesc->nr_ifos))
             jump->lut = vm->dvdDesc->ifos[jump->vtsNr].vts->title_pgcs;
        }
        break;
      }

      // Except for first-play case, pgc is derived from the lut
      if (jump->lut != NULL)
      {
        if (jump->programChainNr && (jump->programChainNr <= jump->lut->nr_pgc_lus))
            jump->pgc = jump->lut->pgc_lus[jump->programChainNr -1].pgc;
      }

      // Check we have a program chain
      if (jump->pgc == NULL)
      {
        vm->events |= DVDEV_EXIT;
        ka_log(ka_log_error | ka_log_nav, "DVD jump with no target PGC");
        return;
      }

      // Update title/chapter/program/cell info
      // This is a must for Pre commands to work correctly
      dvd_vm_updateCellInfo(vm, &vm->jump);

      dvd_pos_t save = *jump;

      // Run pre-commands
      if ((vm->events & DVDEV_RESUME) == 0)
        dvd_vm_playProgramChainPreCmds(vm, jump->pgc);
      else
        vm->events &= ~DVDEV_RESUME;

      if (vm->events & DVDEV_JUMP)
      {
        if (!playPreCmds)
        {
          // Don't take the jump
          *jump = save;
          vm->events &= ~(DVDEV_JUMP | DVDEV_EXIT);
        }
        // If jump to same pgc, treat it as local
        // to avoid coming here again and triggering an infinite loop
        else if ((save.domain == jump->domain)
             &&  (save.vtsNr == jump->vtsNr)
             &&  (save.programChainNr == jump->programChainNr))
        {
          vm->events &= ~DVDEV_JUMP;
        }
      }
      // "Don't take the jump" only on the first jump
      playPreCmds = 1;

      // If no new jump, enter pgc
      if ((vm->events & DVDEV_JUMP) == 0)
      {
        if (jump->pgc->playback_mode)
        {
          int count = 1 + (jump->pgc->playback_mode & 0x7f);
          uint32_t i, i0 = count;

          vm->workMode |= DVDVM_SHUFFLE_MODE;
          vm->shuffleCount = count;

          if (jump->pgc->playback_mode & 0x80)
          {
            uint32_t j, k;

            // Shuffle complete set of cells
            while (i0 >= jump->pgc->nr_cells)
            {
              i0 -= jump->pgc->nr_cells;

              for (i = 0; i < jump->pgc->nr_cells; i++)
              {
                vm->shuffleList[i0 + i] = i + 1;
              }

              for (i = 0; i < jump->pgc->nr_cells; i++)
              {
                j = rand() % jump->pgc->nr_cells;
                k = vm->shuffleList[i0 + j];
                vm->shuffleList[i0 + j] = vm->shuffleList[i0 + i];
                vm->shuffleList[i0 + i] = k;
              }
            }

            // Use random selection for remaining cells
          }

          if (i0 > 0)
          {
            // Random
            for (i = 0; i < i0; i++)
            {
               vm->shuffleList[i] = 1 + (rand() % jump->pgc->nr_cells);
            }
          }
        }
        else
        {
          vm->workMode &= ~DVDVM_SHUFFLE_MODE;
          vm->shuffleCount = 0;
        }

        vm->events |= DVDEV_JUMP_IN_PGC;
      }
    }

    // Shuffle list active, play next cell in list
    if (vm->workMode & DVDVM_SHUFFLE_MODE)
    {
      // This will cause SCR discontinuity
      vm->events |= DVDEV_POS_CHANGE;

      if (vm->shuffleCount)
      {
        vm->shuffleCount--;
        jump->cellNr = vm->shuffleList[vm->shuffleCount];
      }
      else
      {
        // Shuffle list completed, exit cell
        vm->workMode &= ~DVDVM_SHUFFLE_MODE;
        jump->cellNr = jump->pgc->nr_cells + 1;
      }
    }

    // Move within pgc
    if (vm->events & DVDEV_JUMP_IN_PGC)
    {
      // Jump within chain
      vm->events &= ~DVDEV_JUMP_IN_PGC;

      if (jump->cellNr > jump->pgc->nr_cells)
      {
        // We reached the end of the program chain, play the post commands
        dvd_vm_playProgramChainPostCmds(vm, jump->pgc);

        if (!(vm->events & DVDEV_JUMP))
        {
          // Post commands did not trigger a jump, so we must define one
          if (jump->pgc->next_pgcn)
          {
            // The current program chain refers to a new program chain to jump to
            jump->programChainNr = jump->pgc->next_pgcn;
            jump->programNr = 1;
            jump->cellNr = 1;
            vm->events |= DVDEV_JUMP;
          }
          else
          {
            dvd_logjump("DVD, no defined next PGC:", jump);
            // No defined next PGC, resume play if we are in menu ?
            if (vm->pos.domain != DVD_DOMAIN_VTS_TITLE)
            {
              ka_log(ka_log_error | ka_log_nav, "Just resume title ?");
              *jump = vm->resume;
              vm->events |= DVDEV_JUMP | DVDEV_RESUME;
            }
            // No defined next PGC, jumping to a chapter directly may break the expected sequence
            else if (jump->programChainNr < jump->lut->nr_pgc_lus)
            {
              ka_log(ka_log_error | ka_log_nav, "Just try next one ?");
              jump->programChainNr++;
              jump->programNr = 1;
              jump->cellNr = 1;
              vm->events |= DVDEV_JUMP;
            }
            else if (jump->programChainNr > 1)
            {
              ka_log(ka_log_error | ka_log_nav, "Just from last one to first one ?");
              jump->programChainNr = 1;
              jump->programNr = 1;
              jump->cellNr = 1;
              vm->events |= DVDEV_JUMP;
            }
            else
            {
              // Try root menu
              if (dvd_vm_setJumpMenu(vm, jump, 0, DVD_MenuId_Root, 0))
                dvd_vm_setJumpFirstPlay(vm, jump);
              ka_log(ka_log_error | ka_log_nav, "Back to root");
              vm->events |= DVDEV_JUMP;
            }
          }
        }
      }
      else
      {
        // Update title/chapter/program/cell info
        dvd_vm_updateCellInfo(vm, &vm->jump);
      }
    }
  }

  vm->events |= DVDEV_CELL_CHANGE;
  vm->cellCount++;

  // Check if we jumped and need to signal player to complete playing previous cell
  // before doing a fresh start from this cell.

  if ((savePos.domain != vm->pos.domain)
  ||  (savePos.vtsNr != vm->pos.vtsNr)
  ||  (savePos.pgc != vm->pos.pgc)
  ||  (savePos.nextCellNr != vm->pos.cellNr)) // Caching
  {
    if ((vm->pos.lbCellEnd != 0)
    &&  ((vm->events & DVDEV_POS_CHANGE) == 0))
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn("%sCell discontinuity\n", vm->logPrefix);
      vm->events |= DVDEV_POS_CHANGE;
    }
  }

  if (vm->pos.domain == DVD_DOMAIN_VTS_TITLE)
  {
    // Check if audio is still valid
    dvd_attributes_t* attrs = &vm->dvdDesc->ifos[vm->pos.vtsNr].vts->title_attributes;
    int val = vm->system.regs[DVDREG_AUDIO_STREAM_NR];

    if ((val == 15)
    ||  ((val < 8) && ((val >= attrs->nr_audio_streams) || ((vm->pos.pgc->audio_stream[val] & 0x80) == 0))))
    {
      // Undefined, select another stream by language
      for (int i = 0; i < attrs->nr_audio_streams; i++)
      {
        int lang = (attrs->audio_attributes[i][2] << 8) | attrs->audio_attributes[i][3];
        if ((attrs->audio_attributes[i][0] & 0x0c)
        &&  (lang == vm->system.regs[DVDREG_PREF_AUDIO_LANG])
        &&  (vm->pos.pgc->audio_stream[i] & 0x80))
        {
          ka_log(ka_log_error, "Audio is undefined, switching to channel %d", i + 1);
          vm->system.regs[DVDREG_AUDIO_STREAM_NR] = i;
          vm->events |= DVDEV_AUDIO_CHANGE;
          break;
        }
      }

      // Still undefined? select any defined stream
      if ((vm->events & DVDEV_AUDIO_CHANGE) == 0)
      {
        for (int i = 0; i < attrs->nr_audio_streams; i++)
        {
          if (vm->pos.pgc->audio_stream[i] & 0x80)
          {
            ka_log(ka_log_error, "Audio is undefined, switching to channel %d", i + 1);
            vm->system.regs[DVDREG_AUDIO_STREAM_NR] = i;
            vm->events |= DVDEV_AUDIO_CHANGE;
            break;
          }
        }
      }
    }

    // Check if subtitle is still valid
    val = vm->system.regs[DVDREG_SUBS_STREAM_NR];

    if (val == 62)
    {
      // Undefined, select another stream by language
      for (int i = 0; i < attrs->nr_subtitle_streams; i++)
      {
        int lang = (attrs->subtitle_attributes[i][2] << 8) | attrs->subtitle_attributes[i][3];
        if ((attrs->subtitle_attributes[i][0] & 0x03)
        &&  (lang == vm->system.regs[DVDREG_PREF_SUBS_LANG])
        &&  (vm->pos.pgc->subtitle_stream[i][0] & 0x80))
        {
          ka_log(ka_log_error, "Subtitle is undefined, switching to channel %d", i + 1);
          vm->system.regs[DVDREG_SUBS_STREAM_NR] = i; // Not visible
          vm->events |= DVDEV_SUBS_CHANGE;
          break;
        }
      }

      // Still undefined? select any defined stream
      if ((vm->events & DVDEV_SUBS_CHANGE) == 0)
      {
        for (int i = 0; i < attrs->nr_subtitle_streams; i++)
        {
          if (vm->pos.pgc->subtitle_stream[i][0] & 0x80)
          {
            ka_log(ka_log_error, "Subtitle is undefined, switching to channel %d", i + 1);
            vm->system.regs[DVDREG_SUBS_STREAM_NR] = i; // Not visible
            vm->events |= DVDEV_SUBS_CHANGE;
            break;
          }
        }
      }
    }

    // Update resume pos
    vm->resume = vm->pos;
  }

  // Don't throw DVDEV_POS_CHANGE on very first cell
  if ((vm->workMode & DVDVM_HAS_CELL) == 0)
  {
    vm->events &= ~DVDEV_POS_CHANGE;
    vm->workMode |= DVDVM_HAS_CELL;
  }
}

/**
 * Decide next cell to play.
 */
void dvd_vm_nextCell(dvd_vm_state_t* vm)
{
  const dvd_pgc_t* pgc = vm->pos.pgc;

  if (pgc != NULL)
  {
    vm->events &= ~(DVDEV_JUMP | DVDEV_JUMP_IN_PGC | DVDEV_EXIT);

    // Play eventual cell command
    if (vm->pos.cellNr <= pgc->nr_cells)
    {
      dvd_vm_playCellPostCmd(vm, pgc, pgc->cell_playbacks[vm->pos.cellNr - 1].cell_command_nr);
    }

    if (!(vm->events & DVDEV_JUMP))
    {
      // No jump, move to next cell (except in shuffle mode)
      vm->jump = vm->pos;
      if ((vm->workMode & DVDVM_SHUFFLE_MODE) == 0)
        vm->jump.cellNr = vm->pos.nextCellNr;
      vm->events |= DVDEV_JUMP | DVDEV_JUMP_IN_PGC;
    }

    dvd_vm_jump(vm, 1);
  }
  else
  {
    ka_log(ka_log_error | ka_log_nav, "DVD, cannot look for next cell if no program chain");
    vm->events |= DVDEV_EXIT;
  }
}

/**
 * Decide menu action.
 */
void dvd_vm_action(dvd_vm_state_t* vm, uint8_t* cmd)
{
  if (vm->workMode & DVDVM_LOGN)
    ka_logn("%sBtn Cmd: ", vm->logPrefix);
  dvd_vm_playCmds(vm, 1, cmd);

  if (vm->events & DVDEV_JUMP)
    dvd_vm_jump(vm, 1);
}

/**
 * Switch to first play.
 *
 * Returns -1 if no first play, 0 otherwise.
 */
int dvd_vm_setFirstPlay(dvd_vm_state_t* vm)
{
  if (vm->vmg->first_play_pgc == NULL)
    return dvd_vm_setChapter(vm, 1, 1, 1);

  dvd_vm_setJumpFirstPlay(vm, &vm->jump);

  vm->events |= DVDEV_JUMP;
  dvd_vm_jump(vm, 1);

  if (vm->events & DVDEV_EXIT)
  {
    vm->events &= ~DVDEV_EXIT;
    return -1;
  }

  vm->events |= DVDEV_POS_CHANGE;

  return 0;
}

/**
 * Switch to menu.
 *
 * Returns -1 if no such menu, 0 otherwise.
 */
int dvd_vm_setMenu(dvd_vm_state_t* vm, int vtsNr, int menuId)
{
  if (dvd_vm_setJumpMenu(vm, &vm->jump, (menuId == DVD_MenuId_Root) ? 0 : vtsNr, menuId, 0))
    return -1;

  vm->events |= DVDEV_JUMP;
  dvd_vm_jump(vm, 1);

  if (vm->events & DVDEV_EXIT)
  {
    vm->events &= ~DVDEV_EXIT;
    return -1;
  }

  vm->events |= DVDEV_POS_CHANGE;

  return 0;
}

/**
 * Switch to title/chapter.
 *
 * Returns -1 if no such title/chapter, 0 otherwise.
 */
int dvd_vm_setChapter(dvd_vm_state_t* vm, int titleNr, int chapterNr, int playPreCmds)
{
  if (dvd_vm_setJumpTitle(vm, &vm->jump, titleNr, chapterNr))
    return -1;

  vm->events |= DVDEV_JUMP;
  dvd_vm_jump(vm, playPreCmds);

  if (vm->events & DVDEV_EXIT)
  {
    vm->events &= ~DVDEV_EXIT;
    return -1;
  }

  vm->events |= DVDEV_POS_CHANGE;

  return 0;
}

/**
 * Move to given cell.
 *
 * Returns -1 if no such cell in current pgc, 0 otherwise.
 */
int dvd_vm_setCellNr(dvd_vm_state_t* vm, int val)
{
  dvd_pos_t* jump = &vm->jump;

  if (val && (val <= jump->pgc->nr_cells))
  {
    vm->events |= DVDEV_JUMP | DVDEV_JUMP_IN_PGC;
    vm->cellCount++; // do it even if dvd_vm_jump also does it
    *jump = vm->pos;
    jump->cellNr = val;
    dvd_vm_jump(vm, 1);

    return 0;
  }

  return -1;
}


static const char* set_op[16] =
{ NULL
, "="
, "<->"
, "+="
, "-="
, "*="
, "/="
, "%="
, "Rnd"
, "&="
, "|="
, "^="
, NULL
, NULL
, NULL
, NULL
};

#define GPRM_MAX 0xFFFF
static uint16_t saturate(int32_t val)
{
  if (val < 0) val = 0;
  if (val > GPRM_MAX) val = GPRM_MAX;
  return (uint16_t) val;
}

/**
 * Decodes a possible Set command.
 * Destination register is defined at offset a.
 * Operation type is defined in bottom nibble of offset 0.
 * Argument is defined at offset b (register) or [b-1, b] (constant).
 *
 * Returns 1 has Set command, 0 no Set command, -1 invalid Set command.
 */
static int decode_set(dvd_vm_state_t* vm, const uint8_t* cmd, int a, int b)
{
  int rd, rs, vald, vals;
  int op = cmd[0] & 0xf;

  if (a == 1) // top nibble is used by compare type
    rd = cmd[1] & 0xf;
  else
    rd = cmd[a];

  if (set_op[op] == NULL)
    return 0;

  if (rd < 0x10)
  {
    vald = vm->general.regs[rd];
  }
  else
  {
    if (vm->workMode & DVDVM_LOGN)
      ka_logn(" Invalid %s: destination must be general register", set_op[op]);
    return -1;
  }

  if (cmd[0] & 0x10)
  {
    // source is constant
    vals = (cmd[b - 1] << 8) | cmd[b];
    rs = 0x10;

    if (vm->workMode & DVDVM_LOGN)
      ka_logn( " G[%02d] %s %d", rd, set_op[op], vals);
  }
  else
  {
    // source is register
    rs = cmd[b];

    if (rs < 0x10)
      vals = vm->general.regs[rs];
    else if ((rs >= 0x80) && (rs < 0x98))
      vals = vm->system.regs[rs & 0x7f];
    else
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Invalid %s: incorrect source register", set_op[op]);
      return -1;
    }

    if (vm->workMode & DVDVM_LOGN)
      ka_logn( " G[%02d] %s %s[%02d]", rd, set_op[op], (rs & 0x80) ? "S" : "G", rs & 0x7f);
  }

  switch(cmd[0] & 0xf)
  {
    case 1: // Mov
    {
      if (vm->workMode & DVDVM_EXECUTE)
        vm->general.regs[rd] = vals;
    }
    break;
    case 2: // Swap
    {
      if (rs >=  0x10)
      {
        if (vm->workMode & DVDVM_LOGN)
          ka_logn(" Invalid: restricted to general registers");
        return -1;
      }

      if (vm->workMode & DVDVM_EXECUTE)
      {
        vm->general.regs[rd] = vals;
        vm->general.regs[rs] = vald;
      }
    }
    break;
    case 3: // Add
    {
      if (rs <= 0x10)
      {
        if (vm->workMode & DVDVM_EXECUTE)
          vm->general.regs[rd] = saturate(((int32_t) vald) + vals);
      }
      else
      {
        if (vm->workMode & DVDVM_LOGN)
          ka_logn(" Invalid: source cannot be a systen register");
        return -1;
      }
    }
    break;
    case 4: // Sub
    {
      if (rs <= 0x10)
      {
        if (vm->workMode & DVDVM_EXECUTE)
        {
          vm->general.regs[rd] = saturate(((int32_t) vald) - vals);
        }
      }
      else
      {
        if (vm->workMode & DVDVM_LOGN)
          ka_logn(" Invalid: source cannot be a systen register");
        return -1;
      }
    }
    break;
    case 5: // Mul
    {
      if (rs <= 0x10)
      {
        if (vm->workMode & DVDVM_EXECUTE)
        {
          vm->general.regs[rd] = saturate(((int32_t) vald) * vals);
        }
      }
      else
      {
        if (vm->workMode & DVDVM_LOGN)
          ka_logn(" Invalid: source cannot be a systen register");
        return -1;
      }
    }
    break;
    case 6: // Div
    {
      if (rs <= 0x10)
      {
        if (vm->workMode & DVDVM_EXECUTE)
          if (vals)
            vm->general.regs[rd] = vald / vals;
      }
      else
      {
        if (vm->workMode & DVDVM_LOGN)
          ka_logn(" Invalid: source cannot be a systen register");
        return -1;
      }
    }
    break;
    case 7: // Mod
    {
      if (rs <= 0x10)
      {
        if (vm->workMode & DVDVM_EXECUTE)
          if (vals)
            vm->general.regs[rd] = vald % vals;
      }
      else
      {
        if (vm->workMode & DVDVM_LOGN)
          ka_logn(" Invalid: source cannot be a systen register");
        return -1;
      }
    }
    break;
    case 8: // Rnd
    {
      if (rs <= 0x10)
      {
        if (vm->workMode & DVDVM_EXECUTE)
          vm->general.regs[rd] = (vals) ? 1 + (rand() % vals) : 1;
      }
      else
      {
        if (vm->workMode & DVDVM_LOGN)
          ka_logn(" Invalid: source cannot be a systen register");
        return -1;
      }
    }
    break;
    case 9: // And
    {
      if (vm->workMode & DVDVM_EXECUTE)
        vm->general.regs[rd] = vald & vals;
    }
    break;
    case 10: // Or
    {
      if (vm->workMode & DVDVM_EXECUTE)
        vm->general.regs[rd] = vald | vals;
    }
    break;
    case 11: // Xor
    {
      if (vm->workMode & DVDVM_EXECUTE)
        vm->general.regs[rd] = vald ^ vals;
    }
    break;
    default:
      return 0;
  }

  if ((vm->workMode & DVDVM_LOGN) && (vm->workMode & DVDVM_EXECUTE))
    ka_logn(" = %d", vm->general.regs[rd]);
  return 1;
}

/**
 * Decodes a link command from offset 7 and button highlight from offset 6.
 *
 * Returns 1 has link command, 0 no link command, -1 invalid link command.
 */
static int decode_hlink(dvd_vm_state_t* vm, const uint8_t* cmd)
{
  // Most of the cases
  uint16_t hh = (cmd[6] >> 2);
  dvd_pos_t* jump = &vm->jump;

  switch(cmd[7])
  {
    case 0x00: // LinkNoLink
    {
      // Just highlight a button
    }
    break;
    case 0x01: // LinkCellTop
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Restart Cell");

      // restart current cell
      vm->events |= DVDEV_JUMP | DVDEV_JUMP_IN_PGC;
      *jump = vm->pos;
    }
    break;
    case 0x02: // LinkNextCell
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Next Cell");

      // next cell
      vm->events |= DVDEV_JUMP | DVDEV_JUMP_IN_PGC;
      *jump = vm->pos;

      jump->cellNr = vm->pos.nextCellNr;
    }
    break;
    case 0x03: // LinkPrevCell
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Previous Cell");

      // previous cell
      vm->events |= DVDEV_JUMP | DVDEV_JUMP_IN_PGC;
      *jump = vm->pos;

      // don't go back to previous program chain ?
      if (jump->cellNr > 1)
      {
        // If multi-angle block, move before first cell of block
        const dvd_cell_playback_t* cp = &jump->pgc->cell_playbacks[jump->cellNr - 1];
        int first = jump->cellNr;

        while ((first > 1)
             && ((cp->category & DVD_CELL_BLOCK_TYPE_MASK) == DVD_CELL_BLOCK_TYPE_ANGLE)
             && ((cp->category & DVD_CELL_TYPE_MASK) != DVD_CELL_TYPE_FIRST_OF_ANGLE))
        {
          first--;
          cp--;
        }
       if (first > 1)
          jump->cellNr = first - 1;
      }
    }
    break;
    case 0x05: // LinkPGTop
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Restart program");

      // restart program if valid
      vm->events |= DVDEV_JUMP | DVDEV_JUMP_IN_PGC;
      *jump = vm->pos;

      if (jump->programNr && (jump->programNr <= jump->pgc->nr_programs))
      {
        jump->cellNr = jump->pgc->program_cellnrs[jump->programNr - 1];
        if (!jump->cellNr) jump->cellNr = jump->pgc->nr_cells + 1;
      }
      else
        jump->cellNr = jump->pgc->nr_cells + 1;
    }
    break;
    case 0x06: // LinkNextPG
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Next program");

      // next program, if not valid set cellNr past end of PGC
      vm->events |= DVDEV_JUMP | DVDEV_JUMP_IN_PGC;
      *jump = vm->pos;

      if (jump->programNr < jump->pgc->nr_programs)
      {
        jump->programNr++;
        jump->cellNr = jump->pgc->program_cellnrs[jump->programNr - 1];
        if (!jump->cellNr) jump->cellNr = jump->pgc->nr_cells + 1;
      }
      else
        jump->cellNr = jump->pgc->nr_cells + 1;
    }
    break;
    case 0x07: // LinkPrevPG
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Previous program");

      // previous program, if invalid set cellNr to 1
      vm->events |= DVDEV_JUMP | DVDEV_JUMP_IN_PGC;
      *jump = vm->pos;

      if (jump->programNr > 1)
      {
        jump->programNr--;
        jump->cellNr = jump->pgc->program_cellnrs[jump->programNr - 1];
        if (!jump->cellNr) jump->cellNr = jump->pgc->nr_cells + 1;
      }
      else
      {
        jump->cellNr = 1;
      }
    }
    break;
    case 0x09: // LinkPGCTop
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Restart program chain");

      // restart program chain, if invalid set cellNr past PGC limit
      vm->events |= DVDEV_JUMP | DVDEV_JUMP_IN_PGC;
      *jump = vm->pos;

      if (1 <= jump->pgc->nr_programs)
      {
        jump->programNr = 1;
        jump->cellNr = jump->pgc->program_cellnrs[jump->programNr - 1];
        if (!jump->cellNr) jump->cellNr = jump->pgc->nr_cells + 1;
      }
      else
        jump->cellNr = jump->pgc->nr_cells + 1;
    }
    break;
    case 0x0A: // LinkNextPGC
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Next program chain");

      // next program chain
      vm->events |= DVDEV_JUMP;
      *jump = vm->pos;

      if (jump->pgc->next_pgcn)
      {
        jump->programChainNr = jump->pgc->next_pgcn;
        jump->programNr = 1;
        jump->cellNr = 1;
      }
      else
      {
        if (vm->workMode & DVDVM_LOGN)
          ka_logn(" (Invalid)");

        vm->events |= DVDEV_EXIT;
      }
    }
    break;
    case 0x0B: // LinkPrevPGC
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Previous program chain");

      // previous program chain
      vm->events |= DVDEV_JUMP;
      *jump = vm->pos;

      if (jump->pgc->prev_pgcn)
      {
        jump->programChainNr = jump->pgc->prev_pgcn;
        jump->programNr = 1;
        jump->cellNr = 1;
      }
      else
      {
        if (vm->workMode & DVDVM_LOGN)
          ka_logn(" (Invalid)");

        vm->events |= DVDEV_EXIT;
      }
    }
    break;
    case 0x0C: // LinkGoupPGC
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" GoUp program chain");

      // goup program chain
      vm->events |= DVDEV_JUMP;
      *jump = vm->pos;

      if (jump->pgc->goup_pgcn)
      {
        jump->programChainNr = jump->pgc->goup_pgcn;
        jump->programNr = 1;
        jump->cellNr = 1;
      }
      else
      {
        if (vm->workMode & DVDVM_LOGN)
          ka_logn(" (Invalid)");

        vm->events |= DVDEV_EXIT;
      }
    }
    break;
    case 0x0D: // LinkTailPGC
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" End program chain");

      // Move past end of program chain
      vm->events |= DVDEV_JUMP | DVDEV_JUMP_IN_PGC;
      *jump = vm->pos;

      jump->cellNr = jump->pgc->nr_cells + 1;
    }
    break;
    case 0x10: // Resume
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Resume");

      // Resume
      vm->events |= DVDEV_JUMP | DVDEV_RESUME;
      *jump = vm->resume;

      if (jump->pgc == NULL)
      {
        ka_log(ka_log_error | ka_log_nav, "Resume but no PGC");
        vm->events |= DVDEV_EXIT;
      }
    }
    break;
    default:
      return 0;
  }

  if (hh)
  {
    if (vm->workMode & DVDVM_LOGN)
      ka_logn(" Highlight button %d", hh);

    if (vm->workMode & DVDVM_EXECUTE)
      vm->system.regs[DVDREG_HIGHLIGHT_BUTTON_NR] = hh << 10;
  }

  return 1;
}

/**
 * Depending on bottom nubble of offset 1:
 *  - Decodes a link command from offset 7 and button highlight from offset 6.
 *  - Decodes a link command woth param from offset 6 and/or 7.
 *
 * Returns 1 has link command, 0 no link command, -1 invalid link command.
 */
static int decode_link(dvd_vm_state_t* vm, const uint8_t* cmd)
{
  dvd_pos_t* jump = &vm->jump;
  uint16_t hh, val;

  // Most of the cases
  hh = (cmd[6] >> 2);

  switch(cmd[1] & 0x0f)
  {
    case 1:
    {
      return decode_hlink(vm, cmd);
    }
    break;
    case 4: // LinkPGCN
    {
      hh = 0;
      val = (cmd[6] << 8) | cmd[7];

      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Goto program chain nr %d", val);

      vm->events |= DVDEV_JUMP;
      *jump = vm->pos;

      jump->programChainNr = val;
      jump->programNr = 1;
      jump->cellNr = 1;
    }
    break;
    case 5: // LinkPTTN
    {
      val= ((cmd[6] & 3) << 8) | cmd[7];

      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Goto title part nr %d", val);

      vm->events |= DVDEV_JUMP;

      if (dvd_vm_setJumpVtsTitlePart(vm, jump, vm->pos.vtsNr, vm->pos.vtsTitleNr, val))
      {
        if (vm->workMode & DVDVM_LOGN)
          ka_logn(" (Invalid)");

        vm->events |= DVDEV_EXIT;
      }
    }
    break;
    case 6: // LinkPGN
    {
      val= cmd[7];

      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Goto program nr %d", val);

      vm->events |= DVDEV_JUMP | DVDEV_JUMP_IN_PGC;
      *jump = vm->pos;

      if (val && (val <= jump->pgc->nr_programs))
      {
        jump->programNr = val;
        jump->cellNr = jump->pgc->program_cellnrs[jump->programNr - 1];
        if (!jump->cellNr) jump->cellNr = jump->pgc->nr_cells + 1;
      }
      else
        jump->cellNr = jump->pgc->nr_cells + 1;
    }
    break;
    case 7: // LinkCN
    {
      val= cmd[7];

      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Goto cell nr %d", val);

      vm->events |= DVDEV_JUMP | DVDEV_JUMP_IN_PGC;
      *jump = vm->pos;

      if (val && (val <= jump->pgc->nr_cells))
        jump->cellNr = val;
      else
        jump->cellNr = jump->pgc->nr_cells + 1;
    }
    break;
    default:
      return 0;
  }

  if (hh)
  {
    if (vm->workMode & DVDVM_LOGN)
      ka_logn(" Highlight button %d", hh);

    if (vm->workMode & DVDVM_EXECUTE)
      vm->system.regs[DVDREG_HIGHLIGHT_BUTTON_NR] = hh << 10;
  }

  return 1;
}

static const char* cmp_op[8] =
{ NULL
, "&"
, "=="
, "!="
, ">="
, ">"
, "<="
, "<"
};

/**
 * Decodes a compare.
 * Register to check is defined at offset a.
 * Compare type is in top nibble of offset 1.
 * Argument is defined at offset b (register) or [b-1, b] (constant).
 *
 * Returns 1 if success or no compare, 0 compare fails, -1 if invalid compare.
 */
static int compare(dvd_vm_state_t* vm, const uint8_t* cmd, int a, int b)
{
  int op = (cmd[1] & 0x70) >> 4;
  int ok = 1;
  int r1, val1, r2 = 0, val2;
  const char* type1;
  const char* type2 = "";

  if (!op) return 1;

  if (a == 1) // top nibble is used by compare type
    r1 = cmd[a] & 0xf;
  else
    r1 = cmd[a];

  if (r1 < 0x10)
  {
    val1 = vm->general.regs[r1];
    type1 = "G";
  }
  else if ((r1 >= 0x80) && (r1 < 0x98))
  {
    r1 -= 0x80;
    val1 = vm->system.regs[r1];
    type1 = "S";
  }
  else
  {
    if (vm->workMode & DVDVM_LOGN)
      ka_logn(" Invalid register %d", r1);
    return -1;
  }

  if (cmd[1] & 0x80)
  {
    // compare register value against constant value
    val2 = (cmd[b - 1] << 8) | cmd[b];
  }
  else
  {
    r2 = cmd[b];

    if (r2 < 0x10)
    {
      val2 = vm->general.regs[r2];
      type2 = "G";
    }
    else if ((r2 >= 0x80) && (r2 < 0x98))
    {
      r2 -= 0x80;
      val2 = vm->system.regs[r2];
      type2 = "S";
    }
    else
    {
      if (vm->workMode & DVDVM_LOGN)
        ka_logn(" Invalid register %d", r2);
      return -1;
    }
  }

  if (vm->workMode & DVDVM_EXECUTE)
  {
    switch (op)
    {
      case 1: ok = ((val1 & val2) != 0); break;
      case 2: ok = (val1 == val2); break;
      case 3: ok = (val1 != val2); break;
      case 4: ok = (val1 >= val2); break;
      case 5: ok = (val1 >  val2); break;
      case 6: ok = (val1 <= val2); break;
      case 7: ok = (val1 <  val2); break;
    }
  }

  if (vm->workMode & DVDVM_LOGN)
  {
    if (cmd[1] & 0x80)
    {
      if (vm->workMode & DVDVM_EXECUTE)
        ka_logn(" if (%s[%02d] %s %d) -> %d", type1, r1, cmp_op[op], val2, ok);
      else
        ka_logn(" if (%s[%02d] %s %d)", type1, r1, cmp_op[op], val2);
    }
    else
    {
      if (vm->workMode & DVDVM_EXECUTE)
        ka_logn( " if (%s[%02d] %s %s[%02d]) -> %d"
               , type1, r1, cmp_op[op], type2, r2, ok);
      else
        ka_logn( " if (%s[%02d] %s %s[%02d])"
               , type1, r1, cmp_op[op], type2, r2);
    }
  }

  return ok;
}

/**
 * In simulation mode, just decode and logs the commands.
 * In execution mode
 * - decode and execute the commands
 * - in case of a jump to another position, record it in vm->jump,
 *   set DVDEV_JUMP(_IN_PGC) and skip the rest of the commands.
 */
void dvd_vm_playCmds(dvd_vm_state_t* vm, int nrCommands, const uint8_t* cmdList)
{
  const uint8_t* cmd = cmdList;
  const uint8_t* end = cmdList + (8 * nrCommands);
  int steps = 0; // to check if we are in an infinite loop

  vm->events &= ~(DVDEV_JUMP | DVDEV_JUMP_IN_PGC | DVDEV_EXIT);

  for (; (cmd < end) && (steps < 1000); cmd += 8)
  {
    int type = (cmd[0] & 0xf0) >> 4;
    int ok;

    // We triggered a jump, ignore the rest of the commands
    if (vm->workMode & DVDVM_EXECUTE)
    {
      if (vm->events & (DVDEV_JUMP | DVDEV_EXIT))
        break;
    }

    steps++;

    if (vm->workMode & DVDVM_LOGN)
    {
      if (cmd != cmdList)
        ka_logn("\n");
      ka_logn( "%s %02x%02x%02x%02x%02x%02x%02x%02x", vm->logPrefix
             , cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5], cmd[6], cmd[7]);
    }

    // Perform command
    switch (type)
    {
// Type 0
      case 0:
      {
        ok = compare(vm, cmd, 3, 5);
        if (!ok) continue;

        switch(cmd[1] & 0x0f)
        {
          case 0: // Nop
          {
            if ((cmd[0] == 0)
            &&  (cmd[1] == 0)
            &&  (cmd[2] == 0)
            &&  (cmd[3] == 0)
            &&  (cmd[4] == 0)
            &&  (cmd[5] == 0)
            &&  (cmd[6] == 0)
            &&  (cmd[7] == 0))
            {
              if (vm->workMode & DVDVM_LOGN)
                ka_logn(" NOP");

              continue;
            }
          }
          break;
          case 1: // Goto command [1 - nr_cmds]
          {
            if ((cmd[2] == 0) && (cmd[6] == 0))
            {
              int val = cmd[7];

              if (vm->workMode & DVDVM_LOGN)
                ka_logn(" Goto cmd %d", val);

              if (!val || (val > nrCommands))
              {
                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" (Invalid)");
                if (!val) continue;
              }

              if (vm->workMode & DVDVM_EXECUTE)
              {
                cmd = cmdList + 8 * (val - 2); // -2 cf for loop
              }

              continue;
            }
          }
          break;
          case 2: // Break: exit command list
          {
            if ((cmd[2] == 0) && (cmd[6] == 0) && (cmd[7] == 0))
            {
              if (vm->workMode & DVDVM_LOGN)
                ka_logn(" Break: exit command list");

              if (vm->workMode & DVDVM_EXECUTE)
              {
                cmd = end;
              }

              continue;
            }
          }
          break;
          case 3: // SetTmpPML
          {
            if ((cmd[2] == 0) && ((cmd[6] & 0xf0) == 0))
            {
              int level = cmd[6] & 0x7f;
              int dst = cmd[7];

              if (vm->workMode & DVDVM_LOGN)
                ka_logn(" Set parental level %d, if valid goto cmd %d", level, dst);

              if (!dst || (dst > nrCommands))
              {
                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" (Invalid)");
                if (!dst) continue;
              }

              if (((level > 0) && (level <= 8)) || (level == 15))
              {
                if (vm->workMode & DVDVM_EXECUTE)
                {
                  vm->system.regs[DVDREG_PARENTAL_LEVEL] = level;
                  cmd = cmdList +  8 * (dst - 2); // -2 cf for loop
                }
              }

              continue;
            }
          }
          break;
        }
      }
      break;
// Type 2
      case 2:
      {
        ok = compare(vm, cmd, 3, 5);
        if (!ok) continue;

        if (cmd[2] == 0)
        {
          if (decode_link(vm, cmd))
            continue;
        }
      }
      break;
// Type 3
      case 3:
      {
        // only direct compare is possible, no space for constant
        if ((cmd[1] & 0x80) == 0)
        {
          ok = compare(vm, cmd, 6, 7);
          if (!ok) continue;

          switch(cmd[1] & 0x0f)
          {
            case 1: // Exit
            {
              if ((cmd[2] == 0) && (cmd[3] == 0) && (cmd[4] == 0) && (cmd[5] == 0))
              {
                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" Exit");

                vm->events |= DVDEV_JUMP | DVDEV_EXIT;

                continue;
              }
            }
            break;
            case 2: // JumpTT
            {
              if ((cmd[2] == 0) && (cmd[3] == 0) && (cmd[4] == 0))
              {
                int titleNr = cmd[5];

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" Jump to title %d", titleNr);

                vm->events |= DVDEV_JUMP;

                if (dvd_vm_setJumpTitle(vm, &vm->jump, titleNr, 1))
                {
                  if (vm->workMode & DVDVM_LOGN)
                    ka_logn(" (Invalid)");

                  vm->events |= DVDEV_EXIT;
                }

                continue;
              }
            }
            break;
            case 3: // JumpVTS_TT
            {
              if ((cmd[2] == 0) && (cmd[3] == 0) && (cmd[4] == 0))
              {
                int vtsTitleNr = cmd[5];

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" Jump to VTS title %d", vtsTitleNr);

                vm->events |= DVDEV_JUMP;

                if (dvd_vm_setJumpVtsTitlePart(vm, &vm->jump, vm->pos.vtsNr, vtsTitleNr, 1))
                {
                  if (vm->workMode & DVDVM_LOGN)
                    ka_logn(" (Invalid)");

                  vm->events |= DVDEV_EXIT;
                }

                continue;
              }
            }
            break;
            case 5: // JumpVTS_PTT
            {
              if (cmd[4] == 0)
              {
                int vtsTitleNr = cmd[5];
                int chapterNr = (cmd[2] & 0xf) << 8 | cmd[3];

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" Jump to VTS title %d, chapter %d", vtsTitleNr, chapterNr);

                vm->events |= DVDEV_JUMP;

                if (dvd_vm_setJumpVtsTitlePart(vm, &vm->jump, vm->pos.vtsNr, vtsTitleNr, chapterNr))
                {
                  if (vm->workMode & DVDVM_LOGN)
                    ka_logn(" (Invalid)");

                  vm->events |= DVDEV_EXIT;
                }

                continue;
              }
            }
            break;
            case 6:
            {
              switch(cmd[5] >> 6)
              {
                case 0: // JumpSS_FP
                {
                  if ((cmd[2] == 0) && (cmd[3] == 0) && (cmd[4] == 0) && ((cmd[5] & 0x3f) == 0))
                  {
                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" Jump to First Play");

                    vm->events |= DVDEV_JUMP;

                    dvd_vm_setJumpFirstPlay(vm, &vm->jump);

                    continue;
                  }
                }
                break;
                case 1: // JumpSS_VMGM
                {
                  if ((cmd[2] == 0) && (cmd[3] == 0) && (cmd[4] == 0))
                  {
                    int menuId = cmd[5] & 0xf;

                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" Jump to VMG Menu %d", menuId);

                    vm->events |= DVDEV_JUMP;

                    if (dvd_vm_setJumpMenu(vm, &vm->jump, 0, menuId, 0))
                    {
                      if (vm->workMode & DVDVM_LOGN)
                        ka_logn(" (Invalid)");

                      vm->events |= DVDEV_EXIT;
                    }

                    continue;
                  }
                }
                break;
                case 2: // JumpSS_VTSM
                {
                  if (cmd[2] == 0)
                  {
                    int vtsNr = cmd[4];
                    int vtsTitleNr = cmd[3];
                    int menuId = cmd[5] & 0xf;

                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn( " Jump to Menu %d of VTS %d, set VTS Title %d"
                             , menuId, vtsNr, vtsTitleNr);

                    vm->events |= DVDEV_JUMP;

                    if (dvd_vm_setJumpMenu(vm, &vm->jump, vtsNr ? vtsNr : vm->pos.vtsNr, menuId, 0))
                    {
                      if (vm->workMode & DVDVM_LOGN)
                        ka_logn(" (Invalid)");

                      vm->events |= DVDEV_EXIT;
                    }

                    dvd_vm_setJumpVtsTitlePart(vm, &vm->resume, vtsNr, vtsTitleNr, 1);

                    continue;
                  }
                }
                break;
                case 3: // JumpSS_VMGM_PGCN
                {
                  if ((cmd[4] == 0) && ((cmd[5] & 0xf) == 0))
                  {
                    int pgcNr = (cmd[2] << 8) | cmd[3];

                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" Jump to VMG Menu PGC %d", pgcNr);

                    vm->events |= DVDEV_JUMP;

                    if (dvd_vm_setJumpMenu(vm, &vm->jump, 0, DVD_MenuId_Root, pgcNr))
                    {
                      if (vm->workMode & DVDVM_LOGN)
                        ka_logn(" (Invalid)");

                      vm->events |= DVDEV_EXIT;
                    }

                    continue;
                  }
                }
                break;
              }
            }
            break;
            case 8:
            {
              switch(cmd[5] >> 6)
              {
                case 0: // CallSS_FP
                {
                  if ((cmd[2] == 0) && (cmd[3] == 0) && ((cmd[5] & 0x3f) == 0))
                  {
                    int cellNr = cmd[4];

                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" Call First Play, resume cell = %d", cellNr);

                    vm->events |= DVDEV_JUMP;

                    vm->resume = vm->pos;
                    if (cellNr)
                      vm->resume.cellNr = cellNr;

                    dvd_vm_setJumpFirstPlay(vm, &vm->jump);

                    continue;
                  }
                }
                break;
                case 1: // CallSS_VMGM
                {
                  if ((cmd[2] == 0) && (cmd[3] == 0))
                  {
                    int menuId = cmd[5] & 0xf;
                    int cellNr = cmd[4];

                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" Call VMG Menu %d, resume cell = %d", menuId, cellNr);

                    vm->events |= DVDEV_JUMP;

                    vm->resume = vm->pos;
                    if (cellNr)
                      vm->resume.cellNr = cellNr;

                    if (dvd_vm_setJumpMenu(vm, &vm->jump, 0, menuId, 0))
                    {
                      if (vm->workMode & DVDVM_LOGN)
                        ka_logn(" (Invalid)");

                      vm->events |= DVDEV_EXIT;
                    }

                    continue;
                  }
                }
                break;
                case 2: // CallSS_VTSM
                {
                  if ((cmd[2] == 0) && (cmd[3] == 0))
                  {
                    int menuId = cmd[5] & 0xf;
                    int cellNr = cmd[4];

                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" Call VTS Menu %d, resume cell = %d", menuId, cellNr);

                    vm->events |= DVDEV_JUMP;

                    vm->resume = vm->pos;
                    if (cellNr)
                      vm->resume.cellNr = cellNr;

                    if (dvd_vm_setJumpMenu(vm, &vm->jump, vm->pos.vtsNr, menuId, 0))
                    {
                      if (vm->workMode & DVDVM_LOGN)
                        ka_logn(" (Invalid)");

                      vm->events |= DVDEV_EXIT;
                    }

                    continue;
                  }
                }
                break;
                case 3: // CallSS_VMGM_PGCN
                {
                  if ((cmd[5] & 0x3f) == 0)
                  {
                    int pgcNr = (cmd[2] << 8) | cmd[3];
                    int cellNr = cmd[4];

                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" Call VMG Menu PGC %d, resume cell = %d", pgcNr, cellNr);

                    vm->events |= DVDEV_JUMP;

                    vm->resume = vm->pos;
                    if (cellNr)
                      vm->resume.cellNr = cellNr;

                    if (dvd_vm_setJumpMenu(vm, &vm->jump, 0, DVD_MenuId_Root, pgcNr))
                    {
                      if (vm->workMode & DVDVM_LOGN)
                        ka_logn(" (Invalid)");

                      vm->events |= DVDEV_EXIT;
                    }

                    continue;
                  }
                }
                break;
              }
            }
            break;
          }
        }
      }
      break;
// Type 4, 5
      case 4:
      case 5:
      {
        // only direct compare is possible
        if ((cmd[1] & 0x80) == 0)
        {
          ok = compare(vm, cmd, 6, 7);
          if (!ok) continue;

          switch(cmd[0] & 0xf)
          {
            case 1: // SetSTN (set audio nr, subtitle nr, angle nr)
            {
              int val;

              if (vm->workMode & DVDVM_LOGN)
                ka_logn(" SetSTN");

              val = cmd[3];
              if (val & 0x80)
              {
                val = val & 0x7f;

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" audio");

                if (cmd[0] & 0x10)
                {
                  // constant
                  if (vm->workMode & DVDVM_LOGN)
                    ka_logn(" = %d", val);
                }
                else
                {
                  // register
                  if (val < 0x10)
                  {
                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" = G[%02d]", val);

                      val = vm->general.regs[val];
                  }
                  else
                  {
                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" Invalid general register");
                    continue;
                  }
                }

                if (vm->workMode & DVDVM_EXECUTE)
                {
                  if (vm->system.regs[DVDREG_AUDIO_STREAM_NR] != val)
                  {
                    vm->system.regs[DVDREG_AUDIO_STREAM_NR] = val;
                    vm->events |= DVDEV_AUDIO_CHANGE;
                  }
                }
              }

              val = cmd[4];
              if (val & 0x80)
              {
                val = val & 0x7f;

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" substitle");

                if (cmd[0] & 0x10)
                {
                  // constant
                  if (vm->workMode & DVDVM_LOGN)
                    ka_logn(" = %d", val);
                }
                else
                {
                  // register
                  if (val < 0x10)
                  {
                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" = G[%02d]", val);
                    val = vm->general.regs[val];
                  }
                  else
                  {
                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" Invalid general register");
                    continue;
                  }
                }

                if (vm->workMode & DVDVM_EXECUTE)
                {
                  if (vm->system.regs[DVDREG_SUBS_STREAM_NR] != val)
                  {
                    vm->system.regs[DVDREG_SUBS_STREAM_NR] = val;
                    vm->events |= DVDEV_SUBS_CHANGE;
                  }
                }
              }

              val = cmd[5];
              if (val & 0x80)
              {
                val = val & 0x7f;

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" angle");

                if (cmd[0] & 0x10)
                {
                  // constant
                  if (vm->workMode & DVDVM_LOGN)
                    ka_logn(" = %d", val);
                }
                else
                {
                  // register
                  if (val < 0x10)
                  {
                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" = G[%02d]", val);
                    val = vm->general.regs[val];
                  }
                  else
                  {
                    if (vm->workMode & DVDVM_LOGN)
                      ka_logn(" Invalid general register");
                    continue;
                  }
                }

                if (vm->workMode & DVDVM_EXECUTE)
                {
                  if (val && (vm->system.regs[DVDREG_ANGLE_NR] != val))
                  {
                    vm->system.regs[DVDREG_ANGLE_NR] = val;
                    vm->events |= DVDEV_ANGLE_CHANGE;
                  }
                }
              }
            }
            break;
            case 2: // SetNVTMR, set navigation timer and destination pgc
            {
              int timer, pgcnr;

              pgcnr = (cmd[4] << 8) | cmd[5];

              if (cmd[0] & 0x10)
              {
                // constant
                timer = (cmd[2] << 8) | cmd[3];

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" Set navigation timer %d -> pgc %d", timer, pgcnr);
              }
              else
              {
                // register
                timer = cmd[3] & 0xf;

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" Set navigation timer G[%02d] -> pgc %d", timer, pgcnr);

                timer = vm->general.regs[timer];
              }

              if (vm->workMode & DVDVM_EXECUTE)
              {
                vm->system.regs[DVDREG_NAVIGATION_TIMER] = timer;
                vm->system.regs[DVDREG_PGC_FRO_NAV_TIMER] = pgcnr;
                vm->system.modified = 1;
              }
            }
            break;
            case 3: // SetGPRMMD, set genral register value and mode
            {
              int val, reg;

              reg = cmd[5];

              if (cmd[0] & 0x10)
              {
                // constant
                val = (cmd[2] << 8) | cmd[3];

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn( " G[%02d] = %d%s", reg & 0xf, val, (reg & 0x80) ? " as counter" : "");
              }
              else
              {
                // register
                val = cmd[3] & 0xf;

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn( " G[%02d] = G[%02d]%s", reg & 0xf, val, (reg & 0x80) ? " as counter" : "");

                val = vm->general.regs[val];
              }

              if (vm->workMode & DVDVM_EXECUTE)
              {
                vm->general.regs[reg & 0xf] = val;
                if (reg & 0x80)
                  vm->general.counterMode |= (1 << (reg & 0xf));
                else
                  vm->general.counterMode &= ~(1 << (reg & 0xf));
                vm->general.modified = 1;
              }
            }
            break;
            case 4: // SetAMXMD, set karaoke mixing mode
            {
              int val;

              if (cmd[0] & 0x10)
              {
                // constant
                val = (cmd[4] << 8) | cmd[5];

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" Set karaoke mixing mode to %d", val);
              }
              else
              {
                // register
                val = cmd[5] & 0xf;

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" Set karaoke mixing mode to G%02d]", val);

                val = vm->general.regs[val];
              }

              if (vm->workMode & DVDVM_EXECUTE)
              {
                vm->system.regs[DVDREG_KARAOKE_MIXING_MODE] = val;
                vm->system.modified = 1;
              }
            }
            break;
            case 6: // SetHL_BTNN
            {
              int val;

              if (cmd[0] & 0x10)
              {
                // constant
                val = (cmd[4] << 8) | cmd[5];

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" Highlight button %d", val >> 10);
              }
              else
              {
                // register
                val = cmd[5] & 0xf;

                if (vm->workMode & DVDVM_LOGN)
                  ka_logn(" Highlight button to G[%02d]", val);

                val = vm->general.regs[val];
              }

              if (vm->workMode & DVDVM_EXECUTE)
              {
                vm->system.regs[DVDREG_HIGHLIGHT_BUTTON_NR] = val;
                vm->system.modified = 1;
              }
            }
            break;
            default:
              if (vm->workMode & DVDVM_LOGN)
                ka_logn(" Invalid OP");
              continue;
          }
          // only when no condition
          if ((cmd[1] & 0x70) == 0)
          {
            decode_link(vm, cmd);
          }

          continue;
        }
      }
      break;
// Type 6, 7
      case 6:
      case 7:
      {
        // link is only present in absence of conditions
        if ((cmd[1] & 0x70) == 0)
        {
          decode_set(vm, cmd, 3, 5);
          decode_link(vm, cmd);
        }
        else
        {
          ok = compare(vm, cmd, 2, 7);
          // set if condition is met
          if (ok)
            decode_set(vm, cmd, 3, 5);
        }

        continue;
      }
      break;
// Type 8, 9
      case 8:
      case 9:
      {
        // set always performed
        decode_set(vm, cmd, 1, 3);

        ok = compare(vm, cmd, 1, 5);

        // link if condition is met
        if (ok)
        {
          decode_hlink(vm, cmd);
        }

        continue;
      }
      break;
// Type A
      case 0xa:
      {
        ok = compare(vm, cmd, 3, 5);

        // set and link if condition is met
        if (!ok) continue;

        decode_set(vm, cmd, 1, 2);
        decode_hlink(vm, cmd);

        continue;
      }
      break;
// Type B
      case 0xb:
      {
        // only direct compare is possible, no space for constant
        if ((cmd[1] & 0x80) == 0)
        {
          ok = compare(vm, cmd, 4, 5);

          // set and link if condition is met
          if (!ok) continue;

          decode_set(vm, cmd, 1, 3);
          decode_hlink(vm, cmd);

          continue;
        }
      }
      break;
// Type C
      case 0xc:
      {
        ok = compare(vm, cmd, 3, 5);

        // set if condition is met
        if (ok)
          decode_set(vm, cmd, 1, 2);
        // link is always taken
        decode_hlink(vm, cmd);

        continue;
      }
      break;
// Type D
      case 0xd:
      {
        // only direct compare is possible, no space for constant
        if ((cmd[1] & 0x80) == 0)
        {
          ok = compare(vm, cmd, 4, 5);

          // set if condition is met
          if (ok)
            decode_set(vm, cmd, 1, 3);
          // link is always taken
          decode_hlink(vm, cmd);

          continue;
        }
      }
      break;
    }

    if (vm->workMode & DVDVM_LOGN)
      ka_logn(" Invalid OP");
  }

  if (vm->workMode & DVDVM_LOGN)
    ka_logn("\n");
}
