#include "dvd_desc.h"
#include "dvd_vm.h"
#include "config.h"
#include "ka_mem.h"
#include "ka_log.h"

/**
 * Logs IFO/VOB name and sector info.
 */
static void FileStats(const dvd_file_desc_t* fi)
{
  ka_log(ka_log_simple, " File %s, %08llx-%08llx, size %08llx"
     , fi->name
     , fi->start / DVD_LB_SIZE
     , fi->end / DVD_LB_SIZE
     , fi->size / DVD_LB_SIZE);
}

/**
 * Logs DVD's IFOs and VOBs name and sector info.
 */
void dvd_desc_stats(dvd_desc_t* dvd)
{
  if ((config.debug & cfg_printnavstats) == 0)
    return;

  for(int i = 0; i < dvd->nr_ifos; i++)
  {
    dvd_ifo_desc_t* idesc = &dvd->ifos[i];

    if (!idesc->ifo.size && !idesc->bup.size)
      break;

    if (idesc->ifo.size)
      FileStats(&idesc->ifo);

    if (idesc->vobs[0].size)
      FileStats(&idesc->vobs[0]);

    for(int j = 1; j <= DVD_VOBS_MAX; j++)
    {
      if (!idesc->vobs[j].size)
        break;

      FileStats(&idesc->vobs[j]);
    }

    if (idesc->bup.size)
      FileStats(&idesc->bup);
  }
}

static void logPgc(dvd_vm_state_t* vm, const dvd_pgc_t* pgc, int id, const char* prefix)
{
  vm->pos.pgc = pgc;

  if (!pgc->nr_cells)
  {
    ka_logn("%sPGC %02d empty\n", prefix, id);
  }
  else
  {
    int p = 0;
    int nextp = pgc->program_cellnrs[p] - 1;
    for (int c = 0; c < pgc->nr_cells; c++)
    {
      ka_logn("%sPGC %02d CELL NR %02d ", prefix, id, c + 1);
      if (c >= nextp)
      {
        p += 1;
        ka_logn("PG %02d -> ", p);
        nextp = (p < pgc->nr_programs) ? pgc->program_cellnrs[p] - 1 : pgc->nr_cells;
      }
      else
        ka_logn("      -> ");

      ka_logn( "VOB ID %02d, CELL ID %02d, T %02d:%02d:%02d.%02d/%02d LB %08x-%08x, STILL %02x CMD %02x CAT % 4x D %d\n"
             , pgc->cells[c].vob_id
             , pgc->cells[c].cell_id
             , pgc->cell_playbacks[c].time.hour
             , pgc->cell_playbacks[c].time.min
             , pgc->cell_playbacks[c].time.sec
             , pgc->cell_playbacks[c].time.frameinfo
             , pgc->cell_playbacks[c].time.fps
             , pgc->cell_playbacks[c].lb_first_vobu_start
             , pgc->cell_playbacks[c].lb_last_vobu_last
             , pgc->cell_playbacks[c].still_time
             , pgc->cell_playbacks[c].cell_command_nr
             , pgc->cell_playbacks[c].category
             , pgc->cell_playbacks[c].pgc_rel_time / 90000
             );
    }

    ka_logn("%s Playback mode  : %02x\n", prefix, pgc->playback_mode);
    ka_logn("%s Still time     : %d\n", prefix, pgc->still_time);
    ka_logn("%s Audio tracks   :", prefix);
    for (int i = 0; i < 8; i++)
    {
      if (pgc->audio_stream[i] & 0x80)
        ka_logn(" %d -> %d", i, pgc->audio_stream[i] & 0x7f);
    }
    ka_logn("\n");

    ka_logn("%s Subtitle tracks:", prefix);
    for (int i = 0; i < 32; i++)
    {
      if (pgc->subtitle_stream[i][0] & 0x80)
      {
        ka_logn( " %d -> [N: %d, W: %d, L: %d, P: %d]", i
               , pgc->subtitle_stream[i][0] & 0x7f
               , pgc->subtitle_stream[i][1] & 0x7f
               , pgc->subtitle_stream[i][2] & 0x7f
               , pgc->subtitle_stream[i][3] & 0x7f
               );
      }
    }
    ka_logn("\n");
  }

  dvd_vm_setLogPrefix(vm, prefix);

  if (pgc->commands != NULL)
  {
    const uint8_t* pc = (uint8_t*) pgc->commands->commands;

    if (pgc->commands->nr_pre_cmds > 0)
    {
      ka_logn("%s Pre Cmds:\n", prefix);
      dvd_vm_playCmds(vm, pgc->commands->nr_pre_cmds, pc);
      pc += 8 * pgc->commands->nr_pre_cmds;
    }

    if (pgc->commands->nr_post_cmds > 0)
    {
      ka_logn("%s Post Cmds:\n", prefix);
      dvd_vm_playCmds(vm, pgc->commands->nr_post_cmds, pc);
      pc += 8 * pgc->commands->nr_post_cmds;
    }

    if (pgc->commands->nr_cell_cmds > 0)
    {
      ka_logn("%s Cells Cmds:\n", prefix);
      dvd_vm_playCmds(vm, pgc->commands->nr_cell_cmds, pc);
    }
  }

  ka_logn( "%s Chain: Prev %02d, Next %02d, GoUp %02d\n"
         , prefix, pgc->prev_pgcn, pgc->next_pgcn, pgc->goup_pgcn);
}

static void logAttributes(const char* text, const dvd_attributes_t* attrs)
{
  static const char* atype[8] = {"AC3", "???", "MP3", "MP3", "LPCM", "???", "DTS", "???"};

  ka_logn(" %s:\n", text);
  ka_logn("  Video %s letterboxed: %s\n"
         , ((attrs->video_attributes & 0xc00) == 0xc00) ? "16:9" : "4:3"
         , (attrs->video_attributes & 0x04) ? "Cropped" : "Full Screen"
         );
  for(int i = 0; i < attrs->nr_audio_streams; i++)
  {
    ka_logn("  Audio %d: %s %d channels"
           , i
           , atype[attrs->audio_attributes[i][0] >> 5]
           , 1 + (attrs->audio_attributes[i][1] & 0x07)
           );
    if (attrs->audio_attributes[i][0] & 0x0c)
      ka_logn(" lang %c%c", attrs->audio_attributes[i][2]
                          , attrs->audio_attributes[i][3]
                          );
    ka_logn("\n");
  }

  for(int i = 0; i < attrs->nr_subtitle_streams; i++)
  {
    ka_logn("  Subtitle %d:", i);
    if (attrs->subtitle_attributes[i][0] & 0x03)
      ka_logn(" lang %c%c", attrs->subtitle_attributes[i][2]
                          , attrs->subtitle_attributes[i][3]
                          );
    ka_logn("\n");
  }
  ka_logn("\n");
}

int dvd_desc_validate(ka_error_t* pErrorBlock, const dvd_desc_t* dvd)
{
  const dvd_vmg_ifo_t* vmg = dvd->ifos[0].vmg;

  // Check all VTS
  for (int i = 1; i < dvd->nr_ifos; i++)
  {
    const dvd_vts_ifo_t* vts = dvd->ifos[i].vts;

    // Check, table of title parts
    if (config.debug & cfg_printnavstats)
    {
      int t = -1;
      int max = -1;
      int index;

      for (int p = 0; p < vts->title_parts->nr_parts; p++)
      {
        if (p >= max)
        {
          t++;
          index = vts->title_parts->parts_indexes[t];
          if (index != p)
          {
            ka_error_fill( pErrorBlock
                         , "err:Expected title %d offset to be %d in table of title parts of VTS %d"
                         , t + 1, p, i);
            return -1;
          }
          max = (t >= vts->title_parts->nr_titles)
              ? vts->title_parts->nr_parts
              : vts->title_parts->parts_indexes[t + 1];
        }
      }
    }

    for (int j = 0; j < vts->title_parts->nr_parts; j++)
    {
      const dvd_part_t* part = &vts->title_parts->parts[j];

      if (part->program_chain_nr > vts->title_pgcs->nr_pgc_lus)
      {
        ka_error_fill( pErrorBlock
                     , "err:Incorrect PROGRAM CHAIN NR %d at pos %d in table of title parts of VTS %d, max is %d"
                     , part->program_chain_nr
                     , j
                     , i
                     , vts->title_pgcs->nr_pgc_lus
                     );
        return -1;
      }

      const dvd_pgc_t* pgc = vts->title_pgcs->pgc_lus[part->program_chain_nr - 1].pgc;
      if (part->program_nr > pgc->nr_programs)
      {
        ka_error_fill( pErrorBlock
                     , "err:Incorrect PROGRAM CHAIN NR %d PROGRAM NR %d at pos %d in table of title parts of VTS %d, max is %d"
                     , part->program_chain_nr
                     , part->program_nr
                     , j
                     , i
                     , pgc->nr_programs
                     );
        //return -1;
      }
    }
  }

  // Check VMG, table of titles
  {
    const dvd_table_of_titles_t* tt = vmg->table_of_titles;

    for (int i = 0; i < tt->nr_titles; i++)
    {
      const dvd_title_t* t = tt->titles + i;

      if (t->vts_nr >= dvd->nr_ifos)
      {
        ka_error_fill( pErrorBlock
                     , "err:Incorrect VTS NR %d in table of titles, max is %d"
                     , t->vts_nr
                     , dvd->nr_ifos - 1
                     );
        return -1;
      }

      const dvd_vts_ifo_t* vts = dvd->ifos[t->vts_nr].vts;

      if (t->vts_title_nr > vts->title_parts->nr_titles)
      {
        ka_error_fill( pErrorBlock
                     , "err:Incorrect VTS TITLE NR %d in table of titles, max is %d"
                     , t->vts_title_nr
                     , vts->title_parts->nr_titles
                     );
        return -1;
      }

      int index = vts->title_parts->parts_indexes[t->vts_title_nr - 1];
      int max = (t->vts_title_nr == vts->title_parts->nr_titles)
              ? vts->title_parts->nr_parts
              : vts->title_parts->parts_indexes[t->vts_title_nr];
      if (index + t->nr_chapters > max)
      {
        ka_error_fill( pErrorBlock
                     , "err:Incorrect NR CHAPTERS %d in table of titles, max is %d"
                     , t->nr_chapters
                     , max
                     );
      }
    }
  }

  //
  // Log DVD structure
  //

  dvd_vm_state_t vm;

  // Define a VM in test mode for logging pgc commands
  dvd_vm_reset(&vm, dvd, DVDVM_LOGN);

  if (config.debug & cfg_printnavstats)
  {
    // Log Title/chapters structure
    const dvd_table_of_titles_t* tt = vmg->table_of_titles;

    ka_log(ka_log_simple, "DVD with %d ifos and  %d titles:", dvd->nr_ifos, tt->nr_titles);

    for (int i = 0; i < tt->nr_titles; i++)
    {
      const dvd_title_t* t = tt->titles + i;
      const dvd_vts_ifo_t* vts = dvd->ifos[t->vts_nr].vts;
      int index = vts->title_parts->parts_indexes[t->vts_title_nr - 1];

      ka_logn( " %02d: Vts %02d Vts Title %03d Chapters %02d: PGC, PG"
             , i + 1, t->vts_nr, t->vts_title_nr, t->nr_chapters
             );

      for (int c = 0; c < t->nr_chapters; c++)
      {
        ka_logn( " : %02d, %02d"
               , vts->title_parts->parts[index + c].program_chain_nr
               , vts->title_parts->parts[index + c].program_nr
               );
      }
      ka_logn("\n");
    }

    ka_logn("\nMain:\n");
    logAttributes("Menus attributes", &vmg->menu_attributes);

    if (vmg->first_play_pgc)
    {
      vm.pos.domain = DVD_DOMAIN_FIRST_PLAY;
      vm.pos.vtsNr = 0;
      ka_logn(" First play:\n");
      logPgc(&vm, vmg->first_play_pgc, 1, "  ");
    }

    if (vmg->menu_languages)
    {
      vm.pos.domain = DVD_DOMAIN_VMG_MENU;
      vm.pos.vtsNr = 0;
      for (int l = 0; l < vmg->menu_languages->nr_languages; l++)
      {
        const dvd_language_t* language = &vmg->menu_languages->languages[l];
        const dvd_pgc_lu_table_t* lut = language->lut;

        ka_logn(" Menu: Lang %s\n", language->iso639code);

        for (int c = 0; c < lut->nr_pgc_lus; c++)
        {
          const dvd_pgc_t* pgc = lut->pgc_lus[c].pgc;

          if (lut->pgc_lus[c].category)
            ka_logn( "  Type %02x\n", lut->pgc_lus[c].category);

          logPgc(&vm, pgc, c + 1, "   ");
        }
      }
    }
    ka_logn("\n");

    // Log programs info
    for (int i = 1; i < dvd->nr_ifos; i++)
    {
      vm.pos.vtsNr = i;
      const dvd_vts_ifo_t* vts = dvd->ifos[i].vts;
      ka_logn("VTS %02d:\n", i);
      logAttributes("Menus attributes", &vts->menu_attributes);
      logAttributes("Title attributes", &vts->title_attributes);

      if (vts->menu_languages)
      {
        vm.pos.domain = DVD_DOMAIN_VTS_MENU;
        for (int l = 0; l < vts->menu_languages->nr_languages; l++)
        {
          const dvd_language_t* language = &vts->menu_languages->languages[l];
          const dvd_pgc_lu_table_t* lut = language->lut;

          ka_logn(" Menu: Lang %s\n", language->iso639code);

          for (int c = 0; c < lut->nr_pgc_lus; c++)
          {
            const dvd_pgc_t* pgc = lut->pgc_lus[c].pgc;

            if (lut->pgc_lus[c].category)
              ka_logn( "  Type %02x\n", lut->pgc_lus[c].category);

            logPgc(&vm, pgc, c + 1, "   ");
          }
        }
      }

      vm.pos.domain = DVD_DOMAIN_VTS_TITLE;
      ka_logn("\n Titles:\n");
      for (int c = 0; c < vts->title_pgcs->nr_pgc_lus; c++)
      {
        const dvd_pgc_t* pgc = vts->title_pgcs->pgc_lus[c].pgc;

        logPgc(&vm, pgc, c + 1, "   ");
      }
      ka_logn("\n");
    }
  }

  return 0;
}

const dvd_pgc_lu_table_t* dvd_desc_extractMenuLut(const dvd_desc_t* dvd, int ifoNr, const char* lang)
{
  const dvd_vmg_ifo_t* vmg = dvd->ifos[0].vmg;
  const dvd_table_of_menu_languages_t* l;

  if (ifoNr == 0)
    l = vmg->menu_languages;
  else
  {
    const dvd_vts_ifo_t* vts = dvd->ifos[ifoNr].vts;
    l = vts->menu_languages;
  }

  if ((l == NULL) || !l->nr_languages)
    return NULL;

  const dvd_pgc_lu_table_t* lut = NULL;

  // Find language
  for (int i = 0; i < l->nr_languages; i++)
  {
    if (!stricmp(l->languages[i].iso639code, lang))
      lut = l->languages[i].lut;
  }

  // Not found, use first one
  if (lut == NULL)
    lut = l->languages[0].lut;

  return lut;
}

const dvd_pgc_lu_table_t* dvd_desc_extractMenu(dvd_pos_range_t* p, const dvd_desc_t* dvd, int ifoNr, const char* lang, int menuNr)
{
  const dvd_pgc_lu_table_t* lut = dvd_desc_extractMenuLut(dvd, ifoNr, lang);

  // Presence of pgc will allow to very exitence of menu
  p->lastPos.vtsNr      = p->firstPos.vtsNr      = ifoNr;
  p->lastPos.vtsTitleNr = p->firstPos.vtsTitleNr = 0;
  p->lastPos.pgc        = p->firstPos.pgc        = NULL;

  if (lut == NULL)
    return lut;
  // Scan table to find the starting & ending program chains of menu
  for (int i = 0; i < lut->nr_pgc_lus; i++)
  {
    const dvd_pgc_lu_t* lu = &lut->pgc_lus[i];

    if (lu->pgc && lu->pgc->nr_programs)
    {
      const dvd_pgc_t* pgc = lu->pgc;

      // Start of menu
      if (lu->category & DVD_PGC_LU_ISENTRY)
      {
        // If we already found start of our menu, the menu end with previous pgc
        if (p->firstPos.pgc != NULL)
          return lut;

        int id = lu->category & 0xf;
        if (id == menuNr)
        {
          // Matches menu number, define start/end of menu as start and end of program chain
          p->firstPos.pgc            = pgc;
          p->firstPos.programChainNr = i + 1;
          p->firstPos.programNr      = 1;
          p->firstPos.cellNr         = pgc->program_cellnrs[0];
          if (!p->firstPos.cellNr) p->firstPos.cellNr = pgc->nr_cells;
          p->firstPos.lbCellStart    = pgc->cell_playbacks[p->firstPos.cellNr - 1].lb_first_vobu_start;
          p->firstPos.lbCellEnd      = pgc->cell_playbacks[p->firstPos.cellNr - 1].lb_last_vobu_last + 1;
          p->lastPos.pgc            = pgc;
          p->lastPos.programChainNr = i + 1;
          p->lastPos.programNr      = pgc->nr_programs;
          p->lastPos.cellNr         = pgc->nr_cells;
          p->lastPos.lbCellStart    = pgc->cell_playbacks[p->lastPos.cellNr - 1].lb_first_vobu_start;
          p->lastPos.lbCellEnd      = pgc->cell_playbacks[p->lastPos.cellNr - 1].lb_last_vobu_last + 1;
        }
      }
      else
      {
        if (p->firstPos.pgc != NULL)
        {
          // We are within our menu, extend length of menu to end of thei program chain
          p->lastPos.pgc            = pgc;
          p->lastPos.programChainNr = i + 1;
          p->lastPos.programNr      = pgc->nr_programs;
          p->lastPos.cellNr         = pgc->nr_cells;
          p->lastPos.lbCellStart    = pgc->cell_playbacks[p->lastPos.cellNr - 1].lb_first_vobu_start;
          p->lastPos.lbCellEnd      = pgc->cell_playbacks[p->lastPos.cellNr - 1].lb_last_vobu_last + 1;
        }
      }
    }
  }
  return lut;
}

void dvd_desc_extractChapter(dvd_pos_range_t* p, const dvd_desc_t* dvd, int titleNr, int chapterNr)
{
  const dvd_vmg_ifo_t* vmg = dvd->ifos[0].vmg;

  // Presence of pgc will allow to very exitence of chapter
  p->lastPos.vtsNr      = p->firstPos.vtsNr      = 0;
  p->lastPos.vtsTitleNr = p->firstPos.vtsTitleNr = 0;
  p->lastPos.pgc        = p->firstPos.pgc        = NULL;

  // Check title is within range
  if (!titleNr || (titleNr > vmg->table_of_titles->nr_titles))
    return;

  // Title is mapped to a title part of a VTS/IFO
  const dvd_title_t* t = vmg->table_of_titles->titles + (titleNr - 1);
  const dvd_vts_ifo_t* vts = dvd->ifos[t->vts_nr].vts;

  // Trying to access an invalid chapter
  if ((chapterNr < 1) || (chapterNr > t->nr_chapters))
    return;

  p->lastPos.vtsNr      = p->firstPos.vtsNr      = t->vts_nr;
  p->lastPos.vtsTitleNr = p->firstPos.vtsTitleNr = t->vts_title_nr;

  // Title parts, chapter part is just at an offset from title part
  int index = vts->title_parts->parts_indexes[t->vts_title_nr - 1] + chapterNr - 1;

  // Title part provides program chain nr and program nr.
  // Program can cover more than one cell of the progrm chain.
  p->firstPos.programChainNr = vts->title_parts->parts[index].program_chain_nr;
  p->firstPos.programNr      = vts->title_parts->parts[index].program_nr;
  p->firstPos.pgc            = vts->title_pgcs->pgc_lus[p->firstPos.programChainNr - 1].pgc;
  const dvd_pgc_t* pgc = p->firstPos.pgc;
  p->firstPos.cellNr         = pgc->program_cellnrs[p->firstPos.programNr - 1];
  if (!p->firstPos.cellNr) p->firstPos.cellNr = pgc->nr_cells;
  p->firstPos.lbCellStart    = pgc->cell_playbacks[p->firstPos.cellNr - 1].lb_first_vobu_start;
  p->firstPos.lbCellEnd      = pgc->cell_playbacks[p->firstPos.cellNr - 1].lb_last_vobu_last + 1;
  p->lastPos.programChainNr = p->firstPos.programChainNr;
  p->lastPos.programNr      = p->firstPos.programNr;
  p->lastPos.pgc            = pgc;
  p->lastPos.cellNr         = (p->lastPos.programNr == pgc->nr_programs)
                             ? pgc->nr_cells
                             : pgc->program_cellnrs[p->lastPos.programNr]
                               ? (pgc->program_cellnrs[p->lastPos.programNr] - 1)
                               : pgc->nr_cells;
  p->lastPos.lbCellStart    = pgc->cell_playbacks[p->lastPos.cellNr - 1].lb_first_vobu_start;
  p->lastPos.lbCellEnd      = pgc->cell_playbacks[p->lastPos.cellNr - 1].lb_last_vobu_last + 1;
}

void dvd_desc_free(dvd_desc_t** pdvd)
{
  dvd_desc_t* dvd = *pdvd;
  *pdvd = NULL;

  if (dvd)
  {
    if(dvd->path_video) ka_mem_free(dvd->path_video);
    if(dvd->volume) ka_mem_free(dvd->volume);

    ka_mem_free(dvd);
  }
}
