/*
 * xvdr_metronom.c:
 *
 * See the main source file 'xineliboutput.c' for copyright information and
 * how to reach the author.
 *
 * $Id$
 *
 */

#include "xvdr_metronom.h"

#include <stdlib.h>

#include <xine/xine_internal.h>
#include <xine/metronom.h>

#include "../tools/time_ms.h"

#define LOG_MODULENAME "[metronom ] "
#include "../logdefs.h"

#include "adjustable_scr.h"

typedef struct {
  xvdr_metronom_t m;

  /* master SCR for buffering control */
  struct adjustable_scr_s *scr;

  /* original metronom */
  metronom_t    *orig_metronom;
  xine_stream_t *stream;

  int     trickspeed;    /* current trick speed */
  int     still_mode;
  int64_t last_vo_pts;   /* last displayed video frame PTS */
  int     wired;         /* true if currently wired to stream */

  /* initial buffering in live mode */
  uint8_t  buffering;      /* buffering active */
  uint8_t  live_buffering; /* live buffering enabled */
  uint8_t  stream_start;
  int64_t  vid_pts;        /* last seen video pts */
  int64_t  aud_pts;        /* last seen audio pts */
  int64_t  disc_pts;       /* reported discontinuity pts */
  uint64_t buffering_start_time;
  uint64_t first_frame_seen_time;

  pthread_mutex_t mutex;
} xvdr_metronom_impl_t;

static int warnings = 0;

static int64_t absdiff(int64_t a, int64_t b) { int64_t diff = a-b; if (diff<0) diff = -diff; return diff; }
static int64_t min64(int64_t a, int64_t b) { return a < b ? a : b; }

static void check_buffering_done(xvdr_metronom_impl_t *this)
{
  /* both audio and video timestamps seen ? */
  if (this->vid_pts && this->aud_pts) {
    int64_t da = this->aud_pts - this->disc_pts;
    int64_t dv = this->vid_pts - this->disc_pts;
    int64_t d_min = min64(da, dv);
    LOGMSG("  stream A-V diff %d ms", (int)(this->vid_pts - this->aud_pts)/90);
    LOGMSG("  reported stream start at pts %"PRId64, this->disc_pts);
    LOGMSG("  output fifo end at: audio %"PRId64" video %"PRId64, this->aud_pts, this->vid_pts);
    LOGMSG("  dA %"PRId64" dV %"PRId64, da, dv);
    if (d_min < 0 && d_min > -10*90000) {
      LOGMSG("  *** output is late %"PRId64" ticks (%"PRId64" ms) ***", d_min, -d_min/90);
      this->scr->jump(this->scr, d_min);
    }
    this->buffering = 0;
    this->stream_start = 0;
    this->scr->set_buffering(this->scr, 0);
    return;
  }

  if (this->first_frame_seen_time) {
    int64_t ms_since_first_frame = elapsed(this->first_frame_seen_time);

    if (ms_since_first_frame > 1000) {

      this->stream_start = 0;

      /* abort buffering if no audio */
      if (this->vid_pts && !this->aud_pts) {
	LOGMSG("buffering stopped: NO AUDIO ? elapsed time %d ms", (int)ms_since_first_frame);
	this->buffering = 0;
	this->scr->set_buffering(this->scr, 0);
	return;
      }

      /* abort buffering if no video */
      if (!this->vid_pts && this->aud_pts) {
	LOGMSG("buffering stopped: NO VIDEO ? elapsed time %d ms", (int)ms_since_first_frame);
	this->buffering = 0;
	this->scr->set_buffering(this->scr, 0);
	return;
      }
    }
  }
}

static void got_video_frame(metronom_t *metronom, vo_frame_t *frame)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)metronom;
  int64_t          pts  = frame->pts;

#if 1 /* xine-lib master-slave metronom causes some problems ... */
  if (metronom->got_video_frame != got_video_frame) {
    if (!warnings++)
      LOGMSG("got_video_frame: invalid object");
    return;
  }
  warnings = 0;
#endif

  pthread_mutex_lock(&this->mutex);

  if (this->still_mode) {
    LOGVERBOSE("Still frame, type %d", frame->picture_coding_type);
    frame->pts       = 0;
  }

  if (this->trickspeed) {
    frame->pts       = 0;
    frame->duration *= 12; /* GOP */
  }

  /* initial buffering */
  if (this->buffering && !frame->bad_frame) {

    /* track video pts */
    if (pts) {
      if (this->vid_pts && (absdiff(this->vid_pts, pts) > 5*90000)) {
        LOGMSG("buffering: video jump resetted audio pts");
        this->aud_pts = 0;
      }
      if (this->vid_pts && this->aud_pts && (absdiff(this->vid_pts, this->aud_pts) > 5*90000)) {
        LOGMSG("buffering: A-V diff resetted audio pts");
        this->aud_pts = 0;
      }
      if (!this->vid_pts) {
        LOGMSG("got video pts, frame type %d (@%d ms)", frame->picture_coding_type, (int)elapsed(this->buffering_start_time));
	this->first_frame_seen_time = time_ms(); 
      }
      this->vid_pts = pts;
    }

    /* some logging */
    if (!pts) {
      LOGMSG("got video, pts 0, buffering, frame type %d, bad_frame %d", frame->picture_coding_type, frame->bad_frame);
    }
    if (pts && !frame->pts) {
      LOGMSG("*** ERROR: hiding video pts while buffering ***");
    }

    check_buffering_done(this);
  }

  pthread_mutex_unlock(&this->mutex);

  if (this->orig_metronom)
    this->orig_metronom->got_video_frame (this->orig_metronom, frame);

  frame->pts = pts;
}

static int64_t got_audio_samples(metronom_t *metronom, int64_t pts, int nsamples)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)metronom;

  pthread_mutex_lock(&this->mutex);

  /* initial buffering */
  if (this->buffering) {

    /* track audio pts */
    if (pts) {
      if (this->aud_pts && (this->aud_pts > pts || absdiff(pts, this->aud_pts) > 5*90000)) {
	LOGMSG("audio jump resetted video pts");
	this->vid_pts = 0;
      }
      if (this->aud_pts && this->vid_pts && (absdiff(this->vid_pts, this->aud_pts) > 5*90000)) {
	LOGMSG("buffering: A-V diff resetted video pts");
	this->vid_pts = 0;
      }
      if (!this->aud_pts) {
	LOGMSG("got audio pts (@%d ms)", (int)elapsed(this->buffering_start_time));
	this->first_frame_seen_time = time_ms();
      }
      this->aud_pts = pts;
    }

    /* some logging */
    if (!pts && !this->aud_pts) {
      LOGMSG("got audio, pts 0, buffering");
    }

    check_buffering_done(this);
  }

  pthread_mutex_unlock(&this->mutex);

  return this->orig_metronom->got_audio_samples (this->orig_metronom, pts, nsamples);
}

/*
 * dummy hooks
 */

static int64_t got_spu_packet(metronom_t *metronom, int64_t pts)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)metronom;
  return this->orig_metronom->got_spu_packet(this->orig_metronom, pts);
}

static void start_buffering(xvdr_metronom_impl_t *this, int64_t disc_off)
{
  if (this->live_buffering && this->stream_start && disc_off) {
    if (!this->buffering) {
      LOGMSG("live mode buffering started (@%d ms)", (int)elapsed(this->buffering_start_time));

      this->aud_pts  = 0;
      this->vid_pts  = 0;
      this->disc_pts = disc_off;

      this->first_frame_seen_time = 0;

      this->buffering = 1;
      this->scr->set_buffering(this->scr, 1);
    }
  } else {
    if (this->buffering) {
      LOGMSG("live mode buffering aborted (@%d ms)", (int)elapsed(this->buffering_start_time));
      this->buffering = 0;
      this->scr->set_buffering(this->scr, 0);
    }
  }
}

static void handle_audio_discontinuity(metronom_t *metronom, int type, int64_t disc_off)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)metronom;

  pthread_mutex_lock(&this->mutex);
  start_buffering(this, disc_off);
  pthread_mutex_unlock(&this->mutex);

  this->orig_metronom->handle_audio_discontinuity(this->orig_metronom, type, disc_off);
}

static void handle_video_discontinuity(metronom_t *metronom, int type, int64_t disc_off)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)metronom;

  pthread_mutex_lock(&this->mutex);
  start_buffering(this, disc_off);
  pthread_mutex_unlock(&this->mutex);

  this->orig_metronom->handle_video_discontinuity(this->orig_metronom, type, disc_off);
}

static void set_audio_rate(metronom_t *metronom, int64_t pts_per_smpls)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)metronom;
  this->orig_metronom->set_audio_rate(this->orig_metronom, pts_per_smpls);
}

static void set_option(metronom_t *metronom, int option, int64_t value)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)metronom;

  if (option == XVDR_METRONOM_LAST_VO_PTS) {
    if (value > 0) {
      pthread_mutex_lock(&this->mutex);
      this->last_vo_pts = value;
      pthread_mutex_unlock(&this->mutex);
    }
    return;
  }

  if (option == XVDR_METRONOM_LIVE_BUFFERING) {
    pthread_mutex_lock(&this->mutex);
    this->live_buffering = value;
    pthread_mutex_unlock(&this->mutex);
    return;
  }

  if (option == XVDR_METRONOM_STREAM_START) {
    pthread_mutex_lock(&this->mutex);
    this->stream_start = 1;
    this->buffering_start_time = time_ms();
    pthread_mutex_unlock(&this->mutex);
    return;
  }

  if (option == XVDR_METRONOM_TRICK_SPEED) {
    pthread_mutex_lock(&this->mutex);
    this->trickspeed = value;
    pthread_mutex_unlock(&this->mutex);
    return;
  }

  if (option == XVDR_METRONOM_STILL_MODE) {
    pthread_mutex_lock(&this->mutex);
    this->still_mode = value;
    pthread_mutex_unlock(&this->mutex);
    return;
  }

  this->orig_metronom->set_option(this->orig_metronom, option, value);
}

static int64_t get_option(metronom_t *metronom, int option)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)metronom;
  int64_t result;

  if (option == XVDR_METRONOM_LAST_VO_PTS) {
    pthread_mutex_lock(&this->mutex);
    result = this->last_vo_pts;
    pthread_mutex_unlock(&this->mutex);
    return result;
  }
  if (option == XVDR_METRONOM_TRICK_SPEED) {
    pthread_mutex_lock(&this->mutex);
    result = this->trickspeed;
    pthread_mutex_unlock(&this->mutex);
    return result;
  }
  if (option == XVDR_METRONOM_STILL_MODE) {
    pthread_mutex_lock(&this->mutex);
    result = this->still_mode;
    pthread_mutex_unlock(&this->mutex);
    return result;
  }
  if (option == XVDR_METRONOM_ID) {
    return XVDR_METRONOM_ID;
  }

  return this->orig_metronom->get_option(this->orig_metronom, option);
}

static void set_master(metronom_t *metronom, metronom_t *master)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)metronom;
  this->orig_metronom->set_master(this->orig_metronom, master);
}

static void metronom_exit(metronom_t *metronom)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)metronom;

  LOGMSG("xvdr_metronom: metronom_exit() called !");

  /* un-hook */
  this->m.unwire(&this->m);
  this->stream = NULL;

  if (this->orig_metronom) {
    metronom_t *orig_metronom = this->orig_metronom;
    this->orig_metronom = NULL;

    orig_metronom->exit(orig_metronom);
  }
}

/*
 * xvdr_metronom_t
 */

static void xvdr_metronom_wire(xvdr_metronom_t *this_gen)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)this_gen;

  if (!this->stream) {
    LOGMSG("xvdr_metronom_wire(): stream == NULL !");
    return;
  }
  if (!this->stream->metronom) {
    LOGMSG("xvdr_metronom_wire(): stream->metronom == NULL !");
    return;
  }

  if (!this->wired) {
    this->wired = 1;

    /* attach to stream */
    this->orig_metronom = this->stream->metronom;
    this->stream->metronom = &this->m.metronom;
  }
}

static void xvdr_metronom_unwire(xvdr_metronom_t *this_gen)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)this_gen;

  if (this->stream && this->wired) {
    this->wired = 0;

    /* detach from stream */
    this->stream->metronom = this->orig_metronom;
  }
}

static void xvdr_metronom_dispose(xvdr_metronom_t *this_gen)
{
  xvdr_metronom_impl_t *this = (xvdr_metronom_impl_t *)this_gen;

  xvdr_metronom_unwire(this_gen);

  pthread_mutex_destroy(&this->mutex);

  free(this);
}

/*
 * init
 */

xvdr_metronom_t *xvdr_metronom_init(xine_stream_t *stream)
{
  if (stream->metronom->get_option(stream->metronom, XVDR_METRONOM_ID) == XVDR_METRONOM_ID) {
    LOGMSG("xvdr_metronom_init(): stream already hooked !");
    return (xvdr_metronom_t*)stream->metronom;
  }

  xvdr_metronom_impl_t *this = calloc(1, sizeof(xvdr_metronom_impl_t));

  this->stream        = stream;
  this->orig_metronom = stream->metronom;

  this->m.wire         = xvdr_metronom_wire;
  this->m.unwire       = xvdr_metronom_unwire;
  this->m.dispose      = xvdr_metronom_dispose;

  this->m.metronom.set_audio_rate    = set_audio_rate;
  this->m.metronom.got_video_frame   = got_video_frame;
  this->m.metronom.got_audio_samples = got_audio_samples;
  this->m.metronom.got_spu_packet    = got_spu_packet;
  this->m.metronom.handle_audio_discontinuity = handle_audio_discontinuity;
  this->m.metronom.handle_video_discontinuity = handle_video_discontinuity;
  this->m.metronom.set_option = set_option;
  this->m.metronom.get_option = get_option;
  this->m.metronom.set_master = set_master;

  this->m.metronom.exit = metronom_exit;

  pthread_mutex_init(&this->mutex, NULL);

  xvdr_metronom_wire(&this->m);

  return &this->m;
}
