/*
 * chap.c
 *
 * DVD chapter extractor for linux
 * written by Christian Vogelgsang <chris@lallafa.de>
 * under the GNU Public License V2
 *
 * using libdvdread 
 * source is heavily based on "title_info.c" of the libdvdread package.
 *
 * $Id: chaplin.c,v 1.10 2004/03/21 17:45:44 cnvogelg Exp $
 */

#include <assert.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <math.h>

/* libdvdread stuff */
#include <dvdread/dvd_reader.h>
#include <dvdread/ifo_types.h>
#include <dvdread/ifo_read.h>

/* ---------- myts - own time stamp and tools ------------------------------ */

typedef struct myts {
  unsigned short hour;
  unsigned short minute;
  unsigned short second;
  unsigned short frame;
  unsigned short frame_base;
} myts_t;

/* myts_to_secs - convert time to seconds */
double myts_to_secs(myts_t *t)
{
  return (t->hour * 3600. + t->minute * 60. + t->second) +
    ((double)t->frame / (double)t->frame_base);
}

/* mytes_to_frames - convert to frame number */
unsigned long myts_to_frames(myts_t *t,double frame_rate)
{
  /* use timestamp rate for conversion */
  if(frame_rate == 0.0) {
    unsigned long secs = 
      (unsigned long)t->hour * 3600
      + (unsigned long)t->minute * 60
      + (unsigned long)t->second;
    return secs * t->frame_base + t->frame;
  }
  /* use user specified frame rate */
  else
    return (unsigned int)ceil(myts_to_secs(t) * frame_rate);
}

/* myts_is_equal - compare two time stamps */
int myts_is_equal(myts_t *t1,myts_t *t2)
{
  assert(t1->frame_base==t2->frame_base);
  return ((t1->hour==t2->hour)&&
	  (t1->minute==t2->minute)&&
	  (t1->second==t2->second)&&
	  (t1->frame==t2->frame));
}

/* myts_add_to - add time stamp to sum */
void myts_add_to(myts_t *sum,myts_t *t)
{
  assert(sum->frame_base == t->frame_base);

  sum->frame += t->frame;
  if(sum->frame>=sum->frame_base) {
    sum->frame -= sum->frame_base;
    sum->second++;
  }
  sum->second += t->second;
  if(sum->second>=60) {
    sum->second -= 60;
    sum->minute++;
  }
  sum->minute += t->minute;
  if(sum->minute>=60) {
    sum->minute -= 60;
    sum->hour++;
  }
  sum->hour += t->hour;
}

/* myts_convert - convert from libdvdread dvd_time_t */
void myts_convert(dvd_time_t *time,myts_t *my)
{
  int base_code = time->frame_u>>6;
  assert((base_code==1)||(base_code==3));

  my->hour   = (time->hour>>4) * 10 + (time->hour&15);
  my->minute = (time->minute>>4) * 10 + (time->minute&15);
  my->second = (time->second>>4) * 10 + (time->second&15);
  my->frame  = ((time->frame_u>>4)&3) * 10 + (time->frame_u&15);
  my->frame_base = (base_code==1)?25:30;
}

/* myts_to_string - string output of time stamp */
const char *myts_to_string(myts_t *my)
{
  static char buf[12];
  
  sprintf(buf,"%02d:%02d:%02d.%02d",my->hour,my->minute,my->second,my->frame);
  return buf;
}

/* ---------- libdvdread stuff --------------------------------------------- */

typedef struct yuv {
  unsigned char y,u,v;
} yuv_t;

/* read_chapters - read all chapters and return the number of chapters
   found or 0 on error. also set the myts_t pointer to a valid array
   of per chapter times. */
int read_chapters(const char *dvd_path,int title,int verbose,
		  myts_t **ret_chap_time,myts_t *ret_total_time,
		  yuv_t *palette,int *palmode,int *widescreen)
{
  dvd_reader_t *dvd;
  ifo_handle_t *ifo_file;
  tt_srpt_t *tt_srpt;
  ifo_handle_t *vts_file;
  pgc_t *cur_pgc;

  int total_title;    /* total number of available titles */
  int title_set_nr;   /* title set of given title */
  int local_title_id; /* local title id for access in vts file */
  int total_chap;     /* number of chapters */
  myts_t *chap_time;  /* array of chapter time */
  myts_t sum_time;    /* sum chapter time to verify pgc time */

  /* access device */
  dvd = DVDOpen(dvd_path);
  if(!dvd) {
    fprintf(stderr, "Couldn't open DVD device/image: %s\n",dvd_path);
    return 0;
  }

  /* open title manager */
  ifo_file = ifoOpen(dvd,0);
  if(!ifo_file) {
    fprintf(stderr, "Can't open VMG info.\n" );
    DVDClose( dvd );
    return 0;
  }
  
  /* read total_title */
  tt_srpt = ifo_file->tt_srpt;
  total_title = tt_srpt->nr_of_srpts;
  
  /* check title number */
  if((title<1)||(title>total_title)) {
    fprintf(stderr,"Wrong title number %d given. Only %d titles\n",
	    title,total_title);
    DVDClose(dvd);
    return 0;
  }

  /* find global info about title */
  title_set_nr   = tt_srpt->title[title-1].title_set_nr;
  local_title_id = tt_srpt->title[title-1].vts_ttn - 1;
  total_chap     = tt_srpt->title[title-1].nr_of_ptts;
  
  if(verbose)
    printf("title %d:\n title_set_nr=%d, local_title_id=%d, total_chap=%d\n",
	   title,title_set_nr,local_title_id,total_chap);

  /* access title set file */
  vts_file = ifoOpen(dvd,title_set_nr);
  if(!vts_file) {
    fprintf(stderr,"Can't open info file for title set %d.\n",title_set_nr);
    DVDClose(dvd);
    return 0;
  }

  /* find program chain and check programs 
     all chapters should be in the same prog chain and
     should be numbered from 1 to <total_chap>
  */
  {
    vts_ptt_srpt_t *vts_ptt_srpt = vts_file->vts_ptt_srpt;
    int pgc_nr = vts_ptt_srpt->title[local_title_id].ptt[0].pgcn;
    int pg     = vts_ptt_srpt->title[local_title_id].ptt[0].pgn;
    int p;

    assert(pg==1);
    for(p=1;p<total_chap;p++) {
      int next_pg;
      int this_pgc;
      this_pgc = vts_ptt_srpt->title[local_title_id].ptt[p].pgcn;
      assert(pgc_nr==this_pgc);
      next_pg = vts_ptt_srpt->title[local_title_id].ptt[p].pgn;
      assert(pg+1==next_pg);
      pg = next_pg;
    }
    
    /* fetch program chain */
    cur_pgc = vts_file->vts_pgcit->pgci_srp[pgc_nr-1].pgc;
    assert(cur_pgc->nr_of_programs==total_chap);
  }

  /* ADDON: extract color info for subtitles */
  if(palette!=0) {
    int c;
    for(c=0;c<16;c++) {
      uint32_t yuv = cur_pgc->palette[c];
      palette[c].y = (yuv >> 16) & 0xff;
      palette[c].u = (yuv >> 8) & 0xff;
      palette[c].v = (yuv) & 0xff;
    }
  }

  /* ADDON: extract video attributes */
  video_attr_t *video_attr = &vts_file->vtsi_mat->vts_video_attr;
  int pal = video_attr->video_format;
  int ws  = (video_attr->display_aspect_ratio==3);
  if(verbose) {
    printf("  [video attributes: %s %s]\n",
	    pal?"PAL":"NTSC",
	    ws?"16:9":"4:3");
  }
  *widescreen = ws;
  *palmode    = pal;
  
  /* --- main cell loop --- */
  {
    pgc_program_map_t *chap_cell;
    cell_playback_t *cell_pb;
    int c;
    int chap;

    /* total cells in chain */
    int total_cell = cur_pgc->nr_of_cells;
    if(verbose)
      printf(" total_cell=%d\n",total_cell);

    /* get info */
    chap_cell = cur_pgc->program_map;
    cell_pb   = cur_pgc->cell_playback;

    /* alloc mem for chapter time */
    chap_time = (myts_t *)malloc(sizeof(myts_t)*total_cell);
    assert(chap_time!=0);

    /* reset time sum */
    sum_time.hour = sum_time.minute = sum_time.second = sum_time.frame = 0;

    /* loop through all cells */
    chap = -1;
    for(c=0;c<total_cell;c++) {
      myts_t myts;
      int cell_mode;
      char *mode;
      
      /* convert cell playback time */
      myts_convert(&cell_pb->playback_time,&myts);
      if(c==0)
	sum_time.frame_base = myts.frame_base;

      /* this cell is the begin of a new chapter! */
      if(chap_cell[chap+1]==c+1) {
	chap++;
	if(verbose)
	printf("---- chapter %02d ----\n",chap+1);
	
	/* reset chap time */
	chap_time[chap].hour = 0;
	chap_time[chap].minute = 0;
	chap_time[chap].second = 0;
	chap_time[chap].frame = 0;
	chap_time[chap].frame_base = myts.frame_base;
      }
      
      /* cell_mode: 0=normal, 1=first of angle, 2=in angle, 3=last of angle */
      cell_mode = cell_pb->block_mode;
      if((cell_mode==0)||(cell_mode==1)) {
	/* only account for normal or begin of angle cells */
	myts_add_to(&sum_time,&myts);
	myts_add_to(&chap_time[chap],&myts);
	mode = "counted";
      } else {
	mode = "skipped";
      }

      if(verbose)
	printf("%s cell %04d: mode=%d, %s %s %s %s %s\n",
	       mode,c+1,cell_mode,
	       myts_to_string(&myts),
	       (cell_pb->seamless_angle)?"seaml_angle":"-",
	       (cell_pb->seamless_play)?"seaml_play":"-",
	       (cell_pb->stc_discontinuity)?"stc_discnt.":"-",
	       (cell_pb->interleaved)?"intrlvd":"-");
      
      cell_pb++;
    }
  }

  /* compare time sum with total time of program chain */
  {
    /* extract total time */
    myts_convert(&cur_pgc->playback_time,ret_total_time);
    if(verbose) {
      printf("sum/total time: %s =",
	     myts_to_string(&sum_time));
      printf("= %s\n",
	     myts_to_string(ret_total_time));
    }
    /* make sure sum and total time are equal */
    assert(myts_is_equal(&sum_time,ret_total_time));
  }

  ifoClose(ifo_file);
  ifoClose(vts_file);
  DVDClose(dvd);

  /* all ok - return total number of chapters and set pointer */
  *ret_chap_time = chap_time;
  return total_chap;
}

/* ---------- part time calculations --------------------------------------- */

typedef struct part {
  myts_t  playtime;
  int     chap_begin,chap_len;
  unsigned long  frame_len,frame_off;
} part_t;

void find_parts(int total_chap,myts_t *chap_time,myts_t *total_time,
		int total_parts,part_t *parts,int verbose)
{
  double part_secs  = myts_to_secs(total_time) / (double)total_parts;
  double split_secs = part_secs;
  double last_secs;
  int c,p,l;

  myts_t sum = { 0,0,0,0 };
  sum.frame_base = total_time->frame_base;

  /* ----- split chapters into total_parts ----- */
  if(verbose)
    puts("finding parts:");
  parts[0].chap_begin = 0;
  p = 1; l = 0;
  for(c=0;c<total_chap;c++) {
    double secs;

    if(verbose)
      printf("  chapter=%d part=%d local chapter=%d\n",c,p,l);
    /* accumulate current chapter time */
    myts_add_to(&sum,&chap_time[c]);
    secs = myts_to_secs(&sum);
    
    /* need to split? (and can we?)  */
    if((secs>split_secs)&&(p<total_parts)) {
      /* judge split point */
      if((split_secs-last_secs)<(secs-split_secs)) {
	/* split before this chapter */
	parts[p].chap_begin = c;
	parts[p-1].chap_len = l;
	l=1;
      }
      else {
	/* split after this chapter */
	parts[p].chap_begin = c+1;
	parts[p-1].chap_len = l+1;
	l=0;
      }
      p++;
      split_secs += part_secs;
    } else {
      l++;
    }
    
    /* store last chapter time */
    last_secs = secs;
  }
  parts[p-1].chap_len = l;

  /* ----- now calc total time per part ----- */
  {
    int p,c;
    int sum=0;
    myts_t sumt = { 0,0,0,0 };
    sumt.frame_base = total_time->frame_base;

    for(p=0;p<total_parts;p++) {
      myts_t *ts = &parts[p].playtime;
      
      ts->hour = ts->minute = ts->second = ts->frame = 0;
      ts->frame_base = total_time->frame_base;
      
      for(c=0;c<parts[p].chap_len;c++) {
	int off = parts[p].chap_begin + c;
	myts_add_to(ts,&chap_time[off]);
	myts_add_to(&sumt,&chap_time[off]);
	sum++;
      }
    }
    assert(sum==total_chap);
    assert(myts_is_equal(&sumt,total_time));
  }
}

void calc_frames_per_part(int total_parts,part_t *parts,double frame_rate)
{
  int p;
  unsigned long offset = 0;
  for(p=0;p<total_parts;p++) {
    parts[p].frame_len = myts_to_frames(&parts[p].playtime,frame_rate);
    parts[p].frame_off = offset;
    offset += parts[p].frame_len;
  }
}

void calc_chap_part_offset(int total_chap,myts_t *chap_time,
			   int total_parts,part_t *parts,
			   myts_t *chap_part_offset)
{
  int p,c,coff=0;

  for(p=0;p<total_parts;p++) {
    myts_t offset = { 0,0,0,0 };
    offset.frame_base = chap_time[0].frame_base;

    for(c=0;c<parts[p].chap_len;c++) {
      chap_part_offset[coff] = offset;
      myts_add_to(&offset,&chap_time[coff]);
      coff++;
    }
  }
}

/* ---------- vcd imager xml stuff ----------------------------------------- */

/* structure for xml options */
typedef struct xmlopt {
  char *vcd_title;
  char *vcd_type;
  char *xml_name_pattern;
  char *video_name_pattern;
  char *menu_name_pattern;
} xmlopt_t;

/* convert string to iso 646 d-chars */
void convert_to_dchars(char *string)
{
  while(*string!='\0') {
    if(islower(*string)) {
      *string = *string + 'A' - 'a';
    } else if(!(isupper(*string)||isdigit(*string))) {
      *string = '_';
    }
    string++;
  }
}

/* parse xml options - return NULL on error */
xmlopt_t *parse_xml_options(const char *opt)
{
  const char *ptr,*next;
  int i=0;
  #define NUM 5
  int len[NUM];
  char *out[NUM] = { NULL,NULL,NULL,NULL,NULL };
  xmlopt_t *xo;

  /* calc len and duplicate */
  ptr = opt;
  for(i=0;i<NUM;i++) {
    next = strchr(ptr,',');
    if((i==NUM-1)||(next==NULL)) {
      len[i] = strlen(ptr);
      out[i] = strdup(ptr);
      break;
    } else {
      len[i] = next - ptr;
      out[i] = (char *)malloc(len[i]+1);
      strncpy(out[i],ptr,len[i]);
    }
    ptr = next + 1;
  }

  /* create options */
  xo = (xmlopt_t *)malloc(sizeof(xmlopt_t));
  if(xo==NULL)
    return NULL;

  /* setup */
  /* VCD title */
  if(out[0]!=NULL)
    xo->vcd_title = out[0];
  else
    xo->vcd_title = strdup(opt);

  /* VCD type */
  if(out[1]!=NULL) 
    xo->vcd_type = out[1];
  else
    xo->vcd_type = strdup("svcd");

  /* XML name pattern */
  if(out[2]!=NULL)
    xo->xml_name_pattern = out[2];
  else
    xo->xml_name_pattern = strdup("vcd%02d.xml");
  
  /* video name pattern */
  if(out[3]!=NULL)
    xo->video_name_pattern = out[3];
  else
    xo->video_name_pattern = strdup("part%02d.mpg");

  /* menu name pattern */
  if(out[4]!=NULL)
    xo->menu_name_pattern = out[4];
  else
    xo->menu_name_pattern = strdup("menu%02d-%d.mpg");

  /* fix naming */
  convert_to_dchars(xo->vcd_title);
  
  return xo;
}

/* free options */
void free_xml_options(xmlopt_t *xo)
{
  if(xo->vcd_title!=NULL)
    free(xo->vcd_title);
  if(xo->vcd_type!=NULL) 
    free(xo->vcd_type);
  if(xo->xml_name_pattern!=NULL)
    free(xo->xml_name_pattern);
  if(xo->video_name_pattern!=NULL)
    free(xo->video_name_pattern);

  free(xo);
}

/* output code for vcd imager */
int output_vcdxml_file(xmlopt_t *xo,int p,int total_parts,
		       part_t *part,myts_t *chap_part_offset,int menu_entries,
		       FILE *menu_fh)
{
  FILE *fh;
  int c,d;

  char *class,*version;
  char *xml_name   = NULL;
  char *video_name = NULL;
  char *menu_name  = NULL;
  int xml_len      = 0;
  int video_len    = 0;
  int menu_len     = 0;
  
  int menu_num = 0;
  int menu_last= 0;

  /* check type */
  if(strcmp(xo->vcd_type,"svcd")==0) {
    class   = "svcd";
    version = "1.0";
  } else if(strcmp(xo->vcd_type,"vcd")==0) {
    class   = "vcd";
    version = "2.0";
  } else {
    fprintf(stderr,"FATAL: unknown S/VCD type (used vcd or svcd)!\n");
    return 0;
  }

  /* prepare xml name */
  xml_len = strlen(xo->xml_name_pattern) + 2;
  xml_name = (char *)malloc(xml_len * sizeof(char));
  if(xml_name==NULL)
    return 0;
  snprintf(xml_name,xml_len,xo->xml_name_pattern,p);

  /* prepare video name */
  video_len = strlen(xo->video_name_pattern) + 2;
  video_name = (char *)malloc(video_len * sizeof(char));
  if(video_name==NULL)
    return 0;
  snprintf(video_name,video_len,xo->video_name_pattern,p);
  
  /* prepare menu name */
  menu_len = strlen(xo->menu_name_pattern) + 4;
  menu_name = (char *)malloc(menu_len * sizeof(char));
  if(menu_name==NULL)
    return 0;

  /* verbose output */
  printf("writing %s '%s_%d' to XML '%s' for video '%s'\n",
	 xo->vcd_type,xo->vcd_title,p,xml_name,video_name);

  /* menu setup */
  if(menu_entries>0) {
    menu_num = part->chap_len / menu_entries;
    menu_last= part->chap_len % menu_entries;
    if(menu_last>0) menu_num++;
    else menu_last = menu_entries;
  }

  /* open output file */
  fh = fopen(xml_name,"w");
  if(fh==NULL) {
    fprintf(stderr,"Error opening file '%s'\n",xml_name);
    return 0;
  }

  /* ----- XML file ----- */
  /* write header */  
  fprintf(fh,"<?xml version=\"1.0\"?>\n"
	  "<!DOCTYPE videocd PUBLIC \"-//GNU//DTD VideoCD//EN\""
	  " \"http://www.gnu.org/software/vcdimager/videocd.dtd\">\n"
	  "<videocd xmlns=\"http://www.gnu.org/software/vcdimager/1.0/\""
	  " class=\"%s\" version=\"%s\">\n",class,version);
  
  /* options - only for svcd */
  if(strcmp(xo->vcd_type,"svcd")==0)
    fprintf(fh,"\n<option name=\"update scan offsets\" value=\"true\"/>\n");
	
  /* write info */
  fprintf(fh,"\n<info>\n"
	  " <album-id>%s</album-id>\n <volume-count>%d</volume-count>\n"
	  " <volume-number>%d</volume-number>\n"
	  " <restriction>0</restriction>\n</info>\n",
	  xo->vcd_title,total_parts,p);

  /* write pvd */
  fprintf(fh,"\n<pvd>\n"
	  " <volume-id>%s_%d</volume-id>\n</pvd>\n",xo->vcd_title,p);

  /* ----- segment items ----- */
  /* add menus */
  if(menu_num>0) {
    int m;

    fprintf(fh,"\n<segment-items>\n");
    for(m=1;m<=menu_num;m++) {
      int entries = (m==menu_num)?menu_last:menu_entries;
      int offset  = (m-1) * menu_entries + 1;
      int begin   = offset + part->chap_begin;
      int end     = begin  + entries - 1;
      snprintf(menu_name,menu_len,xo->menu_name_pattern,p,m);
      
      printf("  added menu '%s' with %d entries (chapters %02d-%02d)\n",
	      menu_name,entries,begin,end);
      
      fprintf(fh," <segment-item src=\"%s\" id=\"menu%02d\" />\n",
	      menu_name,m);
	    
      /* write menu generation info */
      if(menu_fh!=NULL) {
	fprintf(menu_fh,"%s %d Part %d Menu %d\n",menu_name,entries,p,m);
	for(c=0;c<entries;c++) {
	  fprintf(menu_fh,"%02d 0 Chapter %d\n",begin+c,begin+c);
	}
      }
    }
    fprintf(fh,"</segment-items>\n");
  }

  /* ----- sequence items ----- */
  /* one video moview item */
  fprintf(fh,"\n<sequence-items>\n");
  fprintf(fh," <sequence-item src=\"%s\" id=\"movie\">\n",video_name);
  for(c=0;c<part->chap_len;c++) {
    fprintf(fh,"  <entry id=\"chapter%02d\">%.3f</entry>\n",
	    c+1,myts_to_secs(&chap_part_offset[part->chap_begin+c]));
  }
  fprintf(fh," </sequence-item>\n");  
  fprintf(fh,"</sequence-items>\n");
  
  /* ----- playback control ----- */
  fprintf(fh,"\n<pbc>\n");

  /* menu selections */
  if(menu_entries>0) {
    int m;
    int chap = 1;
  
    for(m=1;m<=menu_num;m++) {
      int num_entry;
    
      fprintf(fh,"\n<selection id=\"play-menu%02d\">\n"
	      " <bsn>1</bsn>\n",m);
      if(m>1)
	fprintf(fh," <prev ref=\"play-menu%02d\"/>\n",m-1);
      if(m<menu_num)
	fprintf(fh," <next ref=\"play-menu%02d\"/>\n",m+1);

      fprintf(fh,
	      " <default ref=\"play-chapter%02d\"/>\n"
	      " <timeout ref=\"play-chapter%02d\"/>\n"
	      " <wait>60</wait>\n"
	      " <loop jump-timing=\"immediate\">1</loop>\n"
	      " <play-item ref=\"menu%02d\"/>\n",chap,chap,m);
    
      /* jump table for buttons */
      num_entry = (m==menu_num)?menu_last:menu_entries;
      for(d=0;d<num_entry;d++) {
	fprintf(fh," <select ref=\"play-chapter%02d\"/>\n",chap+d);
      }
      
      fprintf(fh,"</selection>\n");
      chap += menu_entries;
    } 
  }

  /* chapter selections */
  for(c=0;c<part->chap_len;c++) {
    fprintf(fh,"\n<selection id=\"play-chapter%02d\">\n"
	    " <bsn>1</bsn>\n",c+1);
    if(c>0)
      fprintf(fh," <prev ref=\"play-chapter%02d\"/>\n",c);
    else if(menu_entries>0)
      fprintf(fh," <prev ref=\"play-menu01\"/>\n");
    if(c<(part->chap_len-1)) 
      fprintf(fh," <next ref=\"play-chapter%02d\"/>\n",c+2);
    else if(menu_entries<0)
      fprintf(fh," <next ref=\"play-menu01\">\n");
    
    fprintf(fh,
	    " <default ref=\"%s\"/>\n"
	    " <timeout ref=\"eol\"/>\n"
	    " <wait>1</wait>\n"
	    " <loop jump-timing=\"immediate\">1</loop>\n"
	    " <play-item ref=\"chapter%02d\"/>\n",
	    (menu_entries>0)?"play-menu01":"eol",
	    c+1);
    
    /* jump table for buttons */
    for(d=0;d<part->chap_len;d++) {
      fprintf(fh," <select ref=\"play-chapter%02d\"/>\n",d+1);
    }
    
    fprintf(fh,"</selection>\n");
  }

  /* endlist */
  if(p<total_parts) {
    fprintf(fh,"\n<endlist id=\"eol\">\n <next-volume>%d</next-volume>\n"
	    "</endlist>\n",p+1);
  } else {
    fprintf(fh,"\n<endlist id=\"eol\" rejected=\"true\"/>\n");
  }

  /* write footer */
  fprintf(fh,"\n</pbc>\n</videocd>\n");
  fclose(fh);
  free(xml_name);
  free(video_name);
  free(menu_name);
  return 1;
}

/* write color palette for subtitles */
int output_palette(const char *file,yuv_t palette[16])
{
  FILE *fh;
  int c;
  
  fh = fopen(file,"w");
  if(fh == NULL) {
    fprintf(stderr,"Error opening file '%s'\n",file);
    return 0;
  }
  
  printf("writing YUV palette to '%s'\n",file);
  for(c=0;c<16;c++) {
    fprintf(fh,"%3d %3d %3d\n",palette[c].y,palette[c].u,palette[c].v);
  }	
  
  fclose(fh);
  return 1;
}

/* ---------- main --------------------------------------------------------- */

/* usage - print usage information and exit program */
void usage()
{
  fprintf(stderr,
	  "chaplin - the DVD chapter tool for Linux\n"
	  " written by Christian Vogelgsang <chris@lallafa.de>\n"
	  " $Revision: 1.10 $ $Date: 2004/03/21 17:45:44 $\n\n"
	  "Usage: chaplin <options>\n\n"
	  " -d <dev>   input DVD device or image dir (default: /dev/dvd)\n"
	  " -t <num>   select title <num>\n"
	  " -p <num>   split output in <num> parts\n"
	  "\nOutput options:\n"
	  " -r         reset chapter numbering in each part\n"
	  " -z         start part numbering with zero not one\n"
	  " -c         show each chapter\n"
	  " -l         show chapter length (otherwise offset)\n"
	  " -f <fr>    calculate frames for given framerate\n"
	  "\nExtra options:\n"
	  " -x <opt>   output XML vcd description for vcdimager\n"
	  " -m <num>   enable XML menus with <num> entries per page\n"
	  " -g <file>  write chaplin menu file for chaplin-genmenu\n"
	  " -y <file>  write YUV color table of selected title\n"
	  " -v         be verbose\n"
	  " -h         show this help and program version\n"
	  "\nXML Options (-x argument):\n"
	  " <title>[,<vcd-type>]\n"
	  "        [,<xml_file_pattern>]\n"
	  "        [,<video_file_pattern>]\n"
	  "        [,<menu_file_pattern>]\n"
	  " e.g.: MYMOVIE,hqvcd,my%%02d.xml,movie%%02d.mpg,menu%%d-%%d.mpg\n"
	  );
  exit(1);
}

int main( int argc, char **argv )
{
  /* arguments */
  int title = 1;
  int total_parts = 1;
  char *dvd_path = "/dev/dvd";
  int verbose = 0;
  int arg;
  int reset_chap = 0;
  int num_start = 1;
  char *xml_opts = NULL;
  int xml_menu = 0;
  int show_chap = 0;
  int show_len = 0;
  double frame_rate = 0.0;
  char *yuv_file = NULL;
  char *menu_file = NULL;
  int palmode = 0;
  int widescreen = 0;

  /* resulting info */
  myts_t *chap_time;
  int total_chap;
  myts_t total_time;
  part_t *parts;
  myts_t *chap_part_offset;
  xmlopt_t *xo;
  yuv_t palette[16];

  /* check arguments */
  while((arg = getopt(argc,argv,"d:t:p:x:f:vhrzcly:m:g:"))!=-1) {
    switch(arg) {
      /* d - dvd_path */
    case 'd':
      dvd_path = optarg;
      break;
      /* t - title */
    case 't':
      title = atoi(optarg);
      break;
      /* p - parts */
    case 'p':
      total_parts = atoi(optarg);
      break;
      /* x - xml output title */
    case 'x':
      xml_opts = optarg;
      break;
      /* m - xml menu */
    case 'm':
      xml_menu = atoi(optarg);
      break;
      /* g - menu generation file */
    case 'g':
      menu_file = optarg;
      break;
      /* f - set frame rate for frame calculations */
    case 'f':
      frame_rate = atof(optarg);
      break;
      /* v - verbose */
    case 'v':
      verbose = 1;
      break;
      /* h - usage */
    case 'h':
      usage();
      break;
      /* r - reset chapter numbering */
    case 'r':
      reset_chap = 1;
      break;
      /* z - start numbering from zero */
    case 'z':
      num_start = 0;
      break;
      /* c - show chapters */
    case 'c':
      show_chap = 1;
      break;
      /* l - show length not offset */
    case 'l':
      show_len = 1;
      break;
      /* y - write yuv color table */
    case 'y':
      yuv_file = optarg;
      break;
      /* ??? */
    default:
      usage();
      break;
      }
  }
  /* no extra args! */
  if(optind!=argc)
    usage();

  /* check XML option */
  if(xml_opts!=NULL) {
    xo = parse_xml_options(xml_opts);
    if(xo==NULL) 
      return 1;
  } else 
    xo = NULL;

  /* check total_parts */
  if((total_parts<1)||(total_parts>10)) {
    fprintf(stderr,"Wrong number of parts specified (1-10).\n");
    return 1;
  }

  /* now read in chapters */
  total_chap = read_chapters(dvd_path,title,verbose,&chap_time,&total_time,
			    palette,&palmode,&widescreen);
  if(total_chap==0) {
    fprintf(stderr,"FATAL: no chapters found for title %d!\n",title);
    return 1;
  }
  if(total_chap<total_parts) {
    fprintf(stderr,"Too many parts(%d) for too few chapters(%d)!\n",
	    total_parts,total_chap);
    return 1;
  }

  /* split all chapters into <total_parts> parts with same size */
  parts = (part_t *)malloc(sizeof(part_t)*total_parts);
  assert(parts!=0);
  find_parts(total_chap,chap_time,&total_time,total_parts,parts,verbose);
  calc_frames_per_part(total_parts,parts,frame_rate);

  /* calc offset for each chapter to begin of part */
  chap_part_offset = (myts_t *)malloc(sizeof(myts_t)*total_chap);
  assert(chap_part_offset!=0);
  calc_chap_part_offset(total_chap,chap_time,total_parts,parts,
			chap_part_offset);

  /* ----- generate output ------------------------------------------------- */
  {
    int p,c,coff=0;
    part_t *x = parts;
    for(p=0;p<total_parts;p++) {
      /* part header */
      printf("part %02d: chapters %02d-%02d  frames @%06lu +%06lu"
	     "  playtime %s\n",
	     p+num_start,
	     x->chap_begin+1,x->chap_begin+x->chap_len,
	     x->frame_off,x->frame_len,
	     myts_to_string(&x->playtime)
	     );

      /* show chapters */
      if(show_chap) {
	/* chapter loop */
	for(c=0;c<x->chap_len;c++) {
	  /* determine time stamp to show */
	  myts_t *ts;
	  if(show_len) 
	    ts = &chap_time[coff];
	  else
	    ts = &chap_part_offset[coff];

	  printf("  chapter %02d  %s: %10.3f %06lu %s\n",
		 reset_chap?c+1:coff+1,show_len?"length":"begin",
		 myts_to_secs  (ts),
		 myts_to_frames(ts,frame_rate),
		 myts_to_string(ts));
	  coff++;
	}
      }
      x++;
    }
  }

  /* xml output */
  if(xml_opts!=NULL) {
    int p;
    part_t *x = parts;
    FILE *menu_fh = NULL;

    /* open menu generation info file */
    if((xml_menu>0)&&(menu_file!=NULL)) {
      menu_fh = fopen(menu_file,"w");
      if(menu_fh==NULL) {
	fprintf(stderr,"Error opening '%s'\n",menu_file);
	exit(1);
      }
      printf("writing menu info to '%s'\n",menu_file);
      /* write header of menu info */
      fprintf(menu_fh,"chaplin-menu %d\n",xml_menu);
      fprintf(menu_fh,"dvd \"%s\" title %d %s %s\n",
	      dvd_path,title,palmode?"PAL":"NTSC",widescreen?"16:9":"4:3");
    }

    /* now emit each part as XML file */
    for(p=0;p<total_parts;p++) {
      if(!output_vcdxml_file(xo,p+num_start,total_parts,
			     x,chap_part_offset,xml_menu,menu_fh))
	exit(1);
      x++;
    }
    free_xml_options(xo);
    
    if(menu_file!=NULL)
      fclose(menu_fh);
  }

  /* palette output */
  if(yuv_file!=NULL) {
    output_palette(yuv_file,palette);
  }

  /* cleanup */
  free(chap_part_offset);
  free(parts);
  free(chap_time);
  return 0;
}

