/*
 * Copyright (C) 2000-2020 the xine project
 * Copyright (C) 2009-2011 Petri Hintukainen <phintuka@users.sourceforge.net>
 *
 * This file is part of xine, a free video player.
 *
 * xine is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * xine is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110, USA
 *
 * Input plugin for BluRay discs / images
 *
 * Requires libbluray 0.2.1 or later:
 *   http://www.videolan.org/developers/libbluray.html
 *   git://git.videolan.org/libbluray.git
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>

/* libbluray */
#include <libbluray/bluray.h>
#include <libbluray/bluray-version.h>
#include <libbluray/keys.h>
#include <libbluray/overlay.h>
#include <libbluray/meta_data.h>

/* xine */

#define LOG_MODULE "input_bluray"
#define LOG_VERBOSE

/*#define LOG*/

#define LOGMSG(x...)  xine_log (this->stream->xine, XINE_LOG_MSG, "input_bluray: " x);

#include <xine/xine_internal.h>
#include <xine/xineutils.h>
#include <xine/input_plugin.h>

#include "media_helper.h"
#include "input_helper.h"

/* */

#ifndef MIN
# define MIN(a,b) ((a)<(b)?(a):(b))
#endif
#ifndef MAX
# define MAX(a,b) ((a)>(b)?(a):(b))
#endif

#define ALIGNED_UNIT_SIZE 6144
#define PKT_SIZE          192
#define TICKS_IN_MS       45

#define MIN_TITLE_LENGTH  180

#define BLURAY_MNT_PATH "/mnt/bluray"
#if defined(__sun)
#define BLURAY_PATH "/vol/dev/aliases/cdrom0"
#elif defined(__OpenBSD__)
#define BLURAY_PATH "/dev/rcd0c"
#else
#define BLURAY_PATH "/dev/dvd"
#endif

/* */

typedef struct {

  input_class_t   input_class;

  xine_t         *xine;

  xine_mrl_t    **xine_playlist;
  int             xine_playlist_size;

  /* config */
  const char     *mountpoint;
  const char     *device;
  const char     *language;
  const char     *country;
  int             region;
  int             parental;
  int             skip_mode;
} bluray_input_class_t;

#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 2, 4)

typedef struct {
  BD_ARGB_BUFFER   buf;
  pthread_mutex_t  buf_lock;
} XINE_BD_ARGB_BUFFER;

static void osd_buf_lock(BD_ARGB_BUFFER *buf_gen)
{
  XINE_BD_ARGB_BUFFER *buf = (XINE_BD_ARGB_BUFFER*)buf_gen;
  pthread_mutex_lock(&buf->buf_lock);
}

static void osd_buf_unlock(BD_ARGB_BUFFER *buf_gen)
{
  XINE_BD_ARGB_BUFFER *buf = (XINE_BD_ARGB_BUFFER*)buf_gen;
  pthread_mutex_unlock(&buf->buf_lock);
}

static void osd_buf_init(XINE_BD_ARGB_BUFFER *buf)
{
  buf->buf.lock   = osd_buf_lock;
  buf->buf.unlock = osd_buf_unlock;
  pthread_mutex_init(&buf->buf_lock, NULL);
}

static void osd_buf_destroy(XINE_BD_ARGB_BUFFER *buf)
{
  if (buf->buf.lock) {
    buf->buf.lock   = NULL;
    buf->buf.unlock = NULL;
    pthread_mutex_destroy(&buf->buf_lock);
  }
}

#endif /* BLURAY_VERSION >= 0.2.4 */

typedef struct {
  input_plugin_t        input_plugin;

  bluray_input_class_t *class;

  xine_stream_t        *stream;
  xine_event_queue_t   *event_queue;
  xine_osd_t           *osd[2];
#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 2, 4)
  XINE_BD_ARGB_BUFFER   osd_buf;
#endif

  char                 *mrl;
  char                 *disc_root;
  char                 *disc_name;

  BLURAY               *bdh;

  const BLURAY_DISC_INFO *disc_info;
  const META_DL          *meta_dl; /* disc library meta data */

  int                num_title_idx;     /* number of relevant playlists */
  int                current_title_idx;
  int                num_titles;        /* navigation mode, number of titles in disc index */
  int                current_title;     /* navigation mode, title from disc index */
  BLURAY_TITLE_INFO *title_info;
  pthread_mutex_t    title_info_mutex;  /* lock this when accessing title_info outside of input/demux thread */
  unsigned int       current_clip;
  time_t             still_end_time;
  int                pg_stream;

  uint8_t            nav_mode : 1;
  uint8_t            error : 1;
  uint8_t            menu_open : 1;
  uint8_t            stream_flushed : 1;
  uint8_t            stream_reset_done : 1;
  uint8_t            demux_action_req : 1;
  uint8_t            end_of_title : 1;
  uint8_t            pg_enable : 1;
  uint8_t            has_video : 1;
  int                mouse_inside_button;
} bluray_input_plugin_t;

static void queue_black_frame(bluray_input_plugin_t *this)
{
  vo_frame_t *img    = NULL;

  if (!_x_lock_port_rewiring(this->class->xine, 0)) {
    return;
  }

  img = this->stream->video_out->get_frame(this->stream->video_out,
                                           1920, 1080, 16.0/9.0,
                                           XINE_IMGFMT_YV12, VO_BOTH_FIELDS);

  if (img) {
    if (img->format == XINE_IMGFMT_YV12 && img->base[0] && img->base[1] && img->base[2]) {
      memset(img->base[0], 0x00, img->pitches[0] * img->height);
      memset(img->base[1], 0x80, img->pitches[1] * img->height / 2);
      memset(img->base[2], 0x80, img->pitches[2] * img->height / 2);
      img->duration  = 0;
      img->pts       = 0;
      img->bad_frame = 0;
      img->draw(img, this->stream);

      this->has_video = 1;
    }
    img->free(img);
  }

  _x_unlock_port_rewiring(this->class->xine);
}

/*
 * overlay
 */

#define PALETTE_INDEX_BACKGROUND 0xff

static void send_num_buttons(bluray_input_plugin_t *this, int n)
{
  xine_event_t   event;
  xine_ui_data_t data;

  event.type = XINE_EVENT_UI_NUM_BUTTONS;
  event.data = &data;
  event.data_length = sizeof(data);
  data.num_buttons = n;

  xine_event_send(this->stream, &event);
}

static void clear_overlay(xine_osd_t *osd)
{
  /* palette entry 0xff is background --> can't use xine_osd_clear(). */
  memset(osd->osd.area, PALETTE_INDEX_BACKGROUND, osd->osd.width * osd->osd.height);
  osd->osd.x1 = osd->osd.width;
  osd->osd.y1 = osd->osd.height;
  osd->osd.x2 = 0;
  osd->osd.y2 = 0;
  osd->osd.area_touched = 0;
}

static xine_osd_t *get_overlay(bluray_input_plugin_t *this, int plane)
{
  if (!this->pg_enable) {
    _x_select_spu_channel(this->stream, -1);
  }
  this->stream->video_out->enable_ovl(this->stream->video_out, 1);
  return this->osd[plane];
}

static void close_overlay(bluray_input_plugin_t *this, int plane)
{
  if (plane < 0) {
    close_overlay(this, 0);
    close_overlay(this, 1);
    return;
  }

  lprintf("close_overlay(#%d)\n", plane);

  if (plane < 2 && this->osd[plane]) {

#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 2, 4)
    osd_buf_lock(&this->osd_buf.buf);
#endif

    xine_osd_free(this->osd[plane]);
    this->osd[plane] = NULL;

#if BLURAY_VERSION < BLURAY_VERSION_CODE(0, 2, 2)
    if (plane == 1) {
      send_num_buttons(this, 0);
      this->menu_open = 0;
    }
#endif

#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 2, 4)
    free(this->osd_buf.buf.buf[plane]);
    this->osd_buf.buf.buf[plane] = NULL;

    osd_buf_unlock(&this->osd_buf.buf);
#endif
  }
}

static void open_overlay(bluray_input_plugin_t *this, int plane, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
  lprintf("open_overlay(#%d,%d,%d)\n", plane, w, h);

  if (this->osd[plane]) {
    close_overlay(this, plane);
  }

  this->osd[plane] = xine_osd_new(this->stream, x, y, w, h);
  xine_osd_set_extent(this->osd[plane], w, h);
  clear_overlay(this->osd[plane]);
}

static void draw_bitmap(xine_osd_t *osd, const BD_OVERLAY * const ov)
{
  size_t i;

  /* convert and set palette */
  if (ov->palette) {
    uint32_t color[256];
    uint8_t  trans[256];
    for(i = 0; i < 256; i++) {
      trans[i] = ov->palette[i].T;
      color[i] = (ov->palette[i].Y << 16) | (ov->palette[i].Cr << 8) | ov->palette[i].Cb;
    }

    xine_osd_set_palette(osd, color, trans);
  }

#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 3, 0)
  if (ov->palette_update_flag)
    return;
#endif

  /* uncompress and draw bitmap */
  if (ov->img && ov->w > 0 && ov->h > 0) {
    const BD_PG_RLE_ELEM *rlep = ov->img;
    size_t pixels = (size_t)ov->w * ov->h;
    uint8_t *img = malloc(pixels);

    if (img) {
      for (i = 0; i < pixels; i += rlep->len, rlep++) {
        memset(img + i, rlep->color, rlep->len);
      }

      xine_osd_draw_bitmap(osd, img, ov->x, ov->y, ov->w, ov->h, NULL);

      free(img);
    }
  }
}

static void overlay_proc(void *this_gen, const BD_OVERLAY * const ov)
{
  bluray_input_plugin_t *this = (bluray_input_plugin_t *) this_gen;
  xine_osd_t *osd;
  int64_t vpts = 0;

  if (!this) {
    return;
  }

  if (!ov) {
    /* hide OSD */
    close_overlay(this, -1);
    return;
  }

  if (ov->plane > 1) {
    return;
  }

  switch (ov->cmd) {
    case BD_OVERLAY_INIT:
      open_overlay(this, ov->plane, ov->x, ov->y, ov->w, ov->h);
      return;
    case BD_OVERLAY_CLOSE:
      close_overlay(this, ov->plane);
      return;
  }

  osd = get_overlay(this, ov->plane);
  if (!osd) {
    LOGMSG("overlay_proc(): overlay not open (cmd=%d)\n", ov->cmd);
    return;
  }

  if (ov->pts > 0) {
    vpts = ov->pts + this->stream->metronom->get_option(this->stream->metronom, METRONOM_VPTS_OFFSET);
  }

  switch (ov->cmd) {
    case BD_OVERLAY_DRAW:
      draw_bitmap(osd, ov);
      return;

    case BD_OVERLAY_WIPE:
      xine_osd_draw_rect(osd, ov->x, ov->y, ov->x + ov->w - 1, ov->y + ov->h - 1, PALETTE_INDEX_BACKGROUND, 1);
      return;

    case BD_OVERLAY_CLEAR:
      clear_overlay(osd);
      return;

#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 3, 0)
    case BD_OVERLAY_HIDE:
      osd->osd.area_touched = 0; /* will be hiden at next commit time */
      break;
#endif

    case BD_OVERLAY_FLUSH:
      if (!osd->osd.area_touched) {
        xine_osd_hide(osd, vpts);
      } else {
        xine_osd_show(osd, vpts);
      }
#if BLURAY_VERSION < BLURAY_VERSION_CODE(0, 2, 2)
      if (ov->plane == 1) {
        this->menu_open = !!osd->osd.area_touched;
        send_num_buttons(this, !!osd->osd.area_touched);
      }
#endif
      return;

    default:
      lprintf("unknown overlay command %d\n", ov->cmd);
      return;
  }
}

#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 2, 4)

static void open_argb_overlay(bluray_input_plugin_t *this, int plane, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
  lprintf("open_argb_overlay(#%d,%d,%d)\n", plane, w, h);

  open_overlay(this, plane, x, y, w, h);

  if (xine_osd_get_capabilities(this->osd[plane]) & XINE_OSD_CAP_ARGB_LAYER) {
    this->osd_buf.buf.width = w;
    this->osd_buf.buf.height = h;
    this->osd_buf.buf.buf[plane] = calloc(sizeof(uint32_t), (size_t)w * h);
  } else {
    LOGMSG("open_argb_overlay() failed: video driver does not support ARGB overlays.\n");
  }
}

static xine_osd_t *get_argb_overlay(bluray_input_plugin_t *this, int plane)
{
  if (!this->osd_buf.buf.buf[plane]) {
    return NULL;
  }
  return get_overlay(this, plane);
}

static void argb_overlay_proc(void *this_gen, const BD_ARGB_OVERLAY * const ov)
{
  bluray_input_plugin_t *this = (bluray_input_plugin_t *) this_gen;
  xine_osd_t *osd;
  int64_t vpts = 0;

  if (!this) {
    return;
  }

  if (!ov) {
    /* hide OSD */
    close_overlay(this, -1);
    return;
  }

  if (ov->pts > 0) {
    vpts = ov->pts + this->stream->metronom->get_option(this->stream->metronom, METRONOM_VPTS_OFFSET);
  }

  switch (ov->cmd) {
    case BD_ARGB_OVERLAY_INIT:
      open_argb_overlay(this, ov->plane, 0, 0, ov->w, ov->h);
      return;

    case BD_ARGB_OVERLAY_CLOSE:
      close_overlay(this, ov->plane);
      return;
  }

  osd = get_argb_overlay(this, ov->plane);
  if (!osd) {
    LOGMSG("argb_overlay_proc(): ARGB overlay not open (cmd=%d)\n", ov->cmd);
    return;
  }

  switch (ov->cmd) {
    case BD_ARGB_OVERLAY_FLUSH:

      osd_buf_lock(&this->osd_buf.buf);

      xine_osd_set_argb_buffer(osd, this->osd_buf.buf.buf[ov->plane],
                               this->osd_buf.buf.dirty[ov->plane].x0,
                               this->osd_buf.buf.dirty[ov->plane].y0,
                               this->osd_buf.buf.dirty[ov->plane].x1 - this->osd_buf.buf.dirty[ov->plane].x0 + 1,
                               this->osd_buf.buf.dirty[ov->plane].y1 - this->osd_buf.buf.dirty[ov->plane].y0 + 1);

      xine_osd_show(osd, vpts);

      osd_buf_unlock(&this->osd_buf.buf);
      return;
  case BD_ARGB_OVERLAY_DRAW:
    /* nothing to do, DRI in use */
    break;

   default:
      lprintf("unknown ARGB overlay command %d\n", ov->cmd);
      return;
  }
}

#endif /* BLURAY_VERSION >= 0.2.4 */

/*
 * stream info
 */

static void update_stream_info(bluray_input_plugin_t *this)
{
  if (this->title_info) {
    /* set stream info */
    _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_ANGLE_COUNT,    this->title_info->angle_count);
    _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_ANGLE_NUMBER,   bd_get_current_angle(this->bdh));
    _x_stream_info_set(this->stream, XINE_STREAM_INFO_HAS_CHAPTERS,       this->title_info->chapter_count > 0);
    _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_CHAPTER_COUNT,  this->title_info->chapter_count);
    _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_CHAPTER_NUMBER, bd_get_current_chapter(this->bdh) + 1);
  }
}

static void update_title_name(bluray_input_plugin_t *this)
{
  char           title_name[64] = "";
  xine_ui_data_t udata;
  xine_event_t   uevent = {
    .type        = XINE_EVENT_UI_SET_TITLE,
    .stream      = this->stream,
    .data        = &udata,
    .data_length = sizeof(udata)
  };

  /* check disc library metadata */
  if (this->meta_dl) {
    unsigned i;
    for (i = 0; i < this->meta_dl->toc_count; i++)
      if (this->meta_dl->toc_entries[i].title_number == (unsigned)this->current_title)
        if (this->meta_dl->toc_entries[i].title_name)
          if (strlen(this->meta_dl->toc_entries[i].title_name) > 2) {
            strlcpy(title_name, this->meta_dl->toc_entries[i].title_name, sizeof(title_name));
          }
  }

  /* title name */
  if (title_name[0]) {
  } else if (this->current_title == BLURAY_TITLE_TOP_MENU) {
    strcpy(title_name, "Top Menu");
  } else if (this->current_title == BLURAY_TITLE_FIRST_PLAY) {
    strcpy(title_name, "First Play");
  } else if (this->nav_mode) {
    snprintf(title_name, sizeof(title_name), "Title %d/%d",
             this->current_title, this->num_titles);
  } else {
    snprintf(title_name, sizeof(title_name), "Title %d/%d",
             this->current_title_idx + 1, this->num_title_idx);
  }

  /* disc name */
  if (this->disc_name && this->disc_name[0]) {
    udata.str_len = snprintf(udata.str, sizeof(udata.str), "%s, %s",
                             this->disc_name, title_name);
  } else {
    udata.str_len = snprintf(udata.str, sizeof(udata.str), "%s",
                             title_name);
  }

  _x_meta_info_set(this->stream, XINE_META_INFO_TITLE, udata.str);

  xine_event_send(this->stream, &uevent);
}

static void update_title_info(bluray_input_plugin_t *this, int playlist_id)
{
  /* update title_info */

  pthread_mutex_lock(&this->title_info_mutex);

  if (this->title_info)
    bd_free_title_info(this->title_info);

  if (playlist_id < 0)
    this->title_info = bd_get_title_info(this->bdh, this->current_title_idx, 0);
  else
    this->title_info = bd_get_playlist_info(this->bdh, playlist_id, 0);

  pthread_mutex_unlock(&this->title_info_mutex);

  if (!this->title_info) {
    LOGMSG("bd_get_title_info(%d) failed\n", this->current_title_idx);
    return;
  }

  /* calculate and set stream rate */

  uint64_t rate = bd_get_title_size(this->bdh) * UINT64_C(8) // bits
                  * INT64_C(90000)
                  / (uint64_t)(this->title_info->duration);
  _x_stream_info_set(this->stream, XINE_STREAM_INFO_BITRATE, rate);

  /* set stream info */

  if (this->nav_mode) {
    _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_TITLE_COUNT,  this->num_titles);
    _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_TITLE_NUMBER, this->current_title);
  } else {
    _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_TITLE_COUNT,  this->num_title_idx);
    _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_TITLE_NUMBER, this->current_title_idx + 1);
  }

  update_stream_info(this);

  /* set title name */

  update_title_name(this);
}

/*
 * libbluray event handling
 */

static void fifos_wait(bluray_input_plugin_t *this)
{
  if (!this->stream)
    return;

  if (this->stream->video_fifo) {
    buf_element_t *buf = this->stream->video_fifo->buffer_pool_alloc(this->stream->video_fifo);
    if (buf) {
      buf->type = BUF_CONTROL_FLUSH_DECODER;
      this->stream->video_fifo->put(this->stream->video_fifo, buf);
    }
  }

  time_t start = time(NULL);

  while (1) {
    int vb = -1, ab = -1, vf = -1, af = -1;
    _x_query_buffer_usage(this->stream, &vb, &ab, &vf, &af);

    if (vb <= 0 && ab <= 0 && vf <= 0 && af <= 0)
      break;

    xine_usec_sleep(5000);

    if (time(NULL) > start + 10) {
      LOGMSG("fifos_wait timeout");
      break;
    }
  }
}


static void stream_flush(bluray_input_plugin_t *this)
{
  if (!this || this->stream_flushed || !this->stream)
    return;

  lprintf("Stream flush\n");

  this->stream_flushed = 1;

  xine_event_t event = {
    .type        = XINE_EVENT_END_OF_CLIP,
    .stream      = this->stream,
    .data        = NULL,
    .data_length = 0,
  };
  xine_event_send (this->stream, &event);

  this->demux_action_req = 1;
}

static void stream_reset(bluray_input_plugin_t *this)
{
  if (!this || this->stream_reset_done || !this->stream)
    return;

  lprintf("Stream reset\n");

  xine_event_t event = {
    .type        = XINE_EVENT_PIDS_CHANGE,
    .stream      = this->stream,
    .data        = NULL,
    .data_length = 0,
  };

  if (!this->end_of_title) {
    _x_demux_flush_engine(this->stream);
  }

  xine_event_send (this->stream, &event);

  this->demux_action_req = 1;
  this->stream_reset_done = 1;
}

static void wait_secs(bluray_input_plugin_t *this, unsigned seconds)
{
  stream_flush(this);

  if (this->still_end_time) {
    if (time(NULL) >= this->still_end_time) {
      lprintf("pause end\n");
      this->still_end_time = 0;
      bd_read_skip_still(this->bdh);
      stream_reset(this);
      return;
    }
  }

  else if (seconds) {
    if (seconds > 300) {
      seconds = 300;
    }

    lprintf("still image, pause for %d seconds\n", seconds);
    this->still_end_time = time(NULL) + seconds;
  }

  xine_usec_sleep(40*1000);
}

static void update_spu_channel(bluray_input_plugin_t *this, int channel)
{
  if (this->stream->video_fifo) {
    buf_element_t *buf = this->stream->video_fifo->buffer_pool_alloc(this->stream->video_fifo);
    buf->type = BUF_CONTROL_SPU_CHANNEL;
    buf->decoder_info[0] = channel;
    buf->decoder_info[1] = channel;
    buf->decoder_info[2] = channel;

    this->stream->video_fifo->put(this->stream->video_fifo, buf);
  }
}

static void update_audio_channel(bluray_input_plugin_t *this, int channel)
{
  if (this->stream->audio_fifo) {
    buf_element_t *buf = this->stream->audio_fifo->buffer_pool_alloc(this->stream->audio_fifo);
    buf->type = BUF_CONTROL_AUDIO_CHANNEL;
    buf->decoder_info[0] = channel;

    this->stream->audio_fifo->put(this->stream->audio_fifo, buf);
  }
}

static void handle_libbluray_event(bluray_input_plugin_t *this, BD_EVENT ev)
{
    switch ((bd_event_e)ev.event) {

      case BD_EVENT_NONE:
        break;

      case BD_EVENT_ERROR:
        lprintf("BD_EVENT_ERROR\n");
        _x_message (this->stream, XINE_MSG_GENERAL_WARNING,
                    "Error playing BluRay disc", NULL);
        this->error = 1;
        return;

      case BD_EVENT_READ_ERROR:
        LOGMSG("BD_EVENT_READ_ERROR\n");
        return;

      case BD_EVENT_ENCRYPTED:
        lprintf("BD_EVENT_ENCRYPTED\n");
        _x_message (this->stream, XINE_MSG_ENCRYPTED_SOURCE,
                    "Media stream scrambled/encrypted", NULL);
        this->error = 1;
        return;

      /* sound effects */
#if BLURAY_VERSION >= 202
      case BD_EVENT_SOUND_EFFECT:
        lprintf("BD_EVENT_SOUND_EFFECT %d\n", ev.param);
        break;
#endif

      /* playback control */

      case BD_EVENT_SEEK:
        lprintf("BD_EVENT_SEEK\n");
        this->still_end_time = 0;
        stream_reset(this);
        break;

      case BD_EVENT_STILL_TIME:
        wait_secs(this, ev.param);
        break;

      case BD_EVENT_STILL:
        lprintf("BD_EVENT_STILL %d\n", ev.param);
        unsigned int paused = _x_get_fine_speed(this->stream) == XINE_SPEED_PAUSE;
        if (paused != ev.param) {
          _x_set_fine_speed(this->stream, ev.param ? XINE_SPEED_PAUSE : XINE_SPEED_NORMAL);
        }
        break;

#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 3, 0)
      case BD_EVENT_IDLE:
        xine_usec_sleep(10000);
        break;
#endif

      /* playback position */

      case BD_EVENT_ANGLE:
        lprintf("BD_EVENT_ANGLE_NUMBER %d\n", ev.param);
        _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_ANGLE_NUMBER, ev.param);
        break;

      case BD_EVENT_END_OF_TITLE:
        lprintf("BD_EVENT_END_OF_TITLE\n");
        stream_flush(this);
        fifos_wait(this);
        this->end_of_title = 1;
        break;

      case BD_EVENT_TITLE:
        if (this->nav_mode) {
          lprintf("BD_EVENT_TITLE %d\n", ev.param);
          this->current_title = ev.param;
        }
        break;

      case BD_EVENT_PLAYLIST:
        lprintf("BD_EVENT_PLAYLIST %d\n", ev.param);
        if (!this->nav_mode) {
          this->current_title_idx = bd_get_current_title(this->bdh);
        }
        this->current_clip = 0;
        update_title_info(this, ev.param);
        stream_reset(this);
        this->end_of_title = 0;
        break;

      case BD_EVENT_PLAYITEM:
        lprintf("BD_EVENT_PLAYITEM %d\n", ev.param);
        this->current_clip = ev.param;
        this->still_end_time = 0;
        break;

      case BD_EVENT_CHAPTER:
        lprintf("BD_EVENT_CHAPTER %d\n", ev.param);
        _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_CHAPTER_NUMBER, ev.param);
        break;

      /* stream selection */

      case BD_EVENT_AUDIO_STREAM:
        lprintf("BD_EVENT_AUDIO_STREAM %d\n", ev.param);
        if (ev.param < 32) {
          update_audio_channel(this, ev.param - 1);
        } else {
          update_audio_channel(this, 0);
        }
        break;

      case BD_EVENT_PG_TEXTST:
        lprintf("BD_EVENT_PG_TEXTST %s\n", ev.param ? "ON" : "OFF");
        this->pg_enable = !!ev.param;
        update_spu_channel(this, this->pg_enable ? this->pg_stream : -1);
        break;

      case BD_EVENT_PG_TEXTST_STREAM:
        lprintf("BD_EVENT_PG_TEXTST_STREAM %d\n", ev.param);
        if (ev.param < 64) {
          this->pg_stream = ev.param - 1;
        } else {
          this->pg_stream = -1;
        }
        if (this->pg_enable) {
          update_spu_channel(this, this->pg_stream);
        }
        break;

#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 2, 2)
      case BD_EVENT_POPUP:
        lprintf("BD_EVENT_POPUP %d\n", ev.param);
        break;

      case BD_EVENT_MENU:
        this->menu_open = !!ev.param;
        send_num_buttons(this, ev.param);
        break;
#endif

      default:
        lprintf("unhandled libbluray event %d [param %d]\n", ev.event, ev.param);
        break;
    }
}

static void handle_libbluray_events(bluray_input_plugin_t *this)
{
  BD_EVENT ev;
  while (bd_get_event(this->bdh, &ev)) {
    handle_libbluray_event(this, ev);
    if (this->error || ev.event == BD_EVENT_NONE || ev.event == BD_EVENT_ERROR)
      break;
  }
}

/*
 * xine event handling
 */

static int open_title (bluray_input_plugin_t *this, int title_idx)
{
  if (bd_select_title(this->bdh, title_idx) <= 0) {
    LOGMSG("bd_select_title(%d) failed\n", title_idx);
    return 0;
  }

  this->current_title_idx = title_idx;

  update_title_info(this, -1);

#ifdef LOG
  int ms = this->title_info->duration / INT64_C(90);
  lprintf("Opened title %d. Length %"PRId64" bytes / %02d:%02d:%02d.%03d\n",
          this->current_title_idx, bd_get_title_size(this->bdh),
          ms / 3600000, (ms % 3600000 / 60000), (ms % 60000) / 1000, ms % 1000);
#endif

  return 1;
}

static void send_mouse_enter_leave_event(bluray_input_plugin_t *this, int direction)
{
  if (direction != this->mouse_inside_button) {
    xine_event_t        event;
    xine_spu_button_t   spu_event;

    spu_event.direction = direction;
    spu_event.button    = 1;

    event.type        = XINE_EVENT_SPU_BUTTON;
    event.stream      = this->stream;
    event.data        = &spu_event;
    event.data_length = sizeof(spu_event);
    xine_event_send(this->stream, &event);

    this->mouse_inside_button = direction;
  }
}

static void handle_events(bluray_input_plugin_t *this)
{
  xine_event_t *event;

  if (!this->event_queue)
    return;

  while (NULL != (event = xine_event_get(this->event_queue))) {

    if (!this->bdh || !this->title_info) {
      xine_event_free(event);
      return;
    }

    int64_t pts = xine_get_current_vpts(this->stream) -
      this->stream->metronom->get_option(this->stream->metronom, METRONOM_VPTS_OFFSET);

    if (this->menu_open) {
      switch (event->type) {
        case XINE_EVENT_INPUT_LEFT:  bd_user_input(this->bdh, pts, BD_VK_LEFT);  break;
        case XINE_EVENT_INPUT_RIGHT: bd_user_input(this->bdh, pts, BD_VK_RIGHT); break;
      }
    } else {
      switch (event->type) {

        case XINE_EVENT_INPUT_LEFT:
          lprintf("XINE_EVENT_INPUT_LEFT: previous title\n");
          if (!this->nav_mode) {
            open_title(this, MAX(0, this->current_title_idx - 1));
          } else {
            bd_play_title(this->bdh, MAX(1, this->current_title - 1));
          }
          stream_reset(this);
          break;

        case XINE_EVENT_INPUT_RIGHT:
          lprintf("XINE_EVENT_INPUT_RIGHT: next title\n");
          if (!this->nav_mode) {
            open_title(this, MIN(this->num_title_idx - 1, this->current_title_idx + 1));
          } else {
            bd_play_title(this->bdh, MIN(this->num_titles, this->current_title + 1));
          }
          stream_reset(this);
          break;
      }
    }

    switch (event->type) {

      case XINE_EVENT_INPUT_MOUSE_BUTTON: {
        xine_input_data_t *input = event->data;
        lprintf("mouse click: button %d at (%d,%d)\n", input->button, input->x, input->y);
        if (input->button == 1) {
          bd_mouse_select(this->bdh, pts, input->x, input->y);
          bd_user_input(this->bdh, pts, BD_VK_MOUSE_ACTIVATE);
          send_mouse_enter_leave_event(this, 0);
        }
        break;
      }

      case XINE_EVENT_INPUT_MOUSE_MOVE: {
        xine_input_data_t *input = event->data;
        if (bd_mouse_select(this->bdh, pts, input->x, input->y) > 0) {
          send_mouse_enter_leave_event(this, 1);
        } else {
          send_mouse_enter_leave_event(this, 0);
        }
        break;
      }

      case XINE_EVENT_INPUT_MENU1:
        if (!this->disc_info->top_menu_supported) {
          _x_message (this->stream, XINE_MSG_GENERAL_WARNING,
                      "Can't open Top Menu",
                      "Top Menu title not supported", NULL);
        }
        bd_menu_call(this->bdh, pts);
        break;

      case XINE_EVENT_INPUT_MENU2:     bd_user_input(this->bdh, pts, BD_VK_POPUP); break;
      case XINE_EVENT_INPUT_UP:        bd_user_input(this->bdh, pts, BD_VK_UP);    break;
      case XINE_EVENT_INPUT_DOWN:      bd_user_input(this->bdh, pts, BD_VK_DOWN);  break;
      case XINE_EVENT_INPUT_SELECT:    bd_user_input(this->bdh, pts, BD_VK_ENTER); break;
      case XINE_EVENT_INPUT_NUMBER_0:  bd_user_input(this->bdh, pts, BD_VK_0); break;
      case XINE_EVENT_INPUT_NUMBER_1:  bd_user_input(this->bdh, pts, BD_VK_1); break;
      case XINE_EVENT_INPUT_NUMBER_2:  bd_user_input(this->bdh, pts, BD_VK_2); break;
      case XINE_EVENT_INPUT_NUMBER_3:  bd_user_input(this->bdh, pts, BD_VK_3); break;
      case XINE_EVENT_INPUT_NUMBER_4:  bd_user_input(this->bdh, pts, BD_VK_4); break;
      case XINE_EVENT_INPUT_NUMBER_5:  bd_user_input(this->bdh, pts, BD_VK_5); break;
      case XINE_EVENT_INPUT_NUMBER_6:  bd_user_input(this->bdh, pts, BD_VK_6); break;
      case XINE_EVENT_INPUT_NUMBER_7:  bd_user_input(this->bdh, pts, BD_VK_7); break;
      case XINE_EVENT_INPUT_NUMBER_8:  bd_user_input(this->bdh, pts, BD_VK_8); break;
      case XINE_EVENT_INPUT_NUMBER_9:  bd_user_input(this->bdh, pts, BD_VK_9); break;

      case XINE_EVENT_INPUT_NEXT: {
        switch (this->class->skip_mode) {
          case 0: /* skip by chapter */
            bd_seek_chapter(this->bdh, bd_get_current_chapter(this->bdh) + 1);
            update_stream_info(this);
            break;
          case 1: /* skip by title */
            if (!this->nav_mode) {
              open_title(this, MIN(this->num_title_idx - 1, this->current_title_idx + 1));
            } else {
              bd_play_title(this->bdh, MIN(this->num_titles, this->current_title + 1));
            }
            break;
        }
        stream_reset(this);
        break;
      }

      case XINE_EVENT_INPUT_PREVIOUS: {
        switch (this->class->skip_mode) {
          case 0: /* skip by chapter */
            bd_seek_chapter(this->bdh, MAX(0, ((int)bd_get_current_chapter(this->bdh)) - 1));
            update_stream_info(this);
            break;
          case 1: /* skip by title */
            if (!this->nav_mode) {
              open_title(this, MAX(0, this->current_title_idx - 1));
            } else {
              bd_play_title(this->bdh, MAX(1, this->current_title - 1));
            }
            break;
        }
        stream_reset(this);
        break;
      }

      case XINE_EVENT_INPUT_ANGLE_NEXT: {
        unsigned curr_angle = bd_get_current_angle(this->bdh);
        unsigned angle      = MIN(8, curr_angle + 1);
        lprintf("XINE_EVENT_INPUT_ANGLE_NEXT: set angle %d --> %d\n", curr_angle, angle);
        bd_seamless_angle_change(this->bdh, angle);
        _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_ANGLE_NUMBER, bd_get_current_angle(this->bdh));
        break;
      }

      case XINE_EVENT_INPUT_ANGLE_PREVIOUS: {
        unsigned curr_angle = bd_get_current_angle(this->bdh);
        unsigned angle      = curr_angle ? curr_angle - 1 : 0;
        lprintf("XINE_EVENT_INPUT_ANGLE_PREVIOUS: set angle %d --> %d\n", curr_angle, angle);
        bd_seamless_angle_change(this->bdh, angle);
        _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_ANGLE_NUMBER, bd_get_current_angle(this->bdh));
        break;
      }
    }

    xine_event_free(event);
  }
}

/*
 * xine plugin interface
 */

static uint32_t bluray_plugin_get_capabilities (input_plugin_t *this_gen)
{
  (void)this_gen;
  return INPUT_CAP_SEEKABLE  |
         INPUT_CAP_TIME_SEEKABLE |
         INPUT_CAP_BLOCK     |
         INPUT_CAP_AUDIOLANG |
         INPUT_CAP_SPULANG   |
         INPUT_CAP_CHAPTERS;
}

#define CHECK_READ_INTERRUPT               \
  do {                                     \
    if (this->demux_action_req) {          \
      this->demux_action_req = 0;          \
      errno = EAGAIN;                      \
      return -1;                           \
    }                                      \
    if (_x_action_pending(this->stream)) { \
      errno = EINTR;                       \
      return -1;                           \
    }                                      \
  } while (0)

static off_t bluray_plugin_read (input_plugin_t *this_gen, void *buf, off_t len)
{
  bluray_input_plugin_t *this = (bluray_input_plugin_t *) this_gen;
  off_t result;

  if (!this || !this->bdh || len < 0 || this->error)
    return -1;

  if (!this->has_video) {
    queue_black_frame(this);
  }

  handle_events(this);
  CHECK_READ_INTERRUPT;

  if (this->nav_mode) {
    do {
      BD_EVENT ev;
      result = bd_read_ext (this->bdh, (unsigned char *)buf, len, &ev);
      handle_libbluray_event(this, ev);
      CHECK_READ_INTERRUPT;

      if (result == 0) {
        handle_events(this);
        CHECK_READ_INTERRUPT;
#if 0
        if (ev.event == BD_EVENT_NONE) {
          if (_x_action_pending(this->stream)) {
            break;
          }
        }
#endif
      }
    } while (!this->error && result == 0);

  } else {
    result = bd_read (this->bdh, (unsigned char *)buf, len);
    handle_libbluray_events(this);
  }

  if (result < 0) {
    LOGMSG("bd_read() failed: %s (%d of %d)\n", strerror(errno), (int)result, (int)len);
  }

  if (result > 0) {
    this->stream_flushed = 0;
    this->stream_reset_done = 0;
  }

  return result;
}

static buf_element_t *bluray_plugin_read_block (input_plugin_t *this_gen, fifo_buffer_t *fifo, off_t todo)
{
  buf_element_t *buf;

  if (todo > ALIGNED_UNIT_SIZE)
    todo = ALIGNED_UNIT_SIZE;

  buf = fifo->buffer_pool_size_alloc (fifo, todo);
  if (todo > (off_t)buf->max_size)
    todo = buf->max_size;

  if (todo > 0) {
    bluray_input_plugin_t *this = (bluray_input_plugin_t *) this_gen;

    buf->size = bluray_plugin_read(this_gen, (char*)buf->mem, todo);
    buf->type = BUF_DEMUX_BLOCK;

    if (buf->size > 0) {
      buf->extra_info->total_time = this->title_info->duration / 90;
      return buf;
    }
  }

  buf->free_buffer (buf);
  return NULL;
}

static off_t bluray_plugin_seek (input_plugin_t *this_gen, off_t offset, int origin)
{
  bluray_input_plugin_t *this = (bluray_input_plugin_t *) this_gen;

  if (!this || !this->bdh)
    return -1;
  if (this->still_end_time)
    return offset;

  /* convert relative seeks to absolute */

  if (origin == SEEK_CUR) {
    offset = bd_tell(this->bdh) + offset;
  }
  else if (origin == SEEK_END) {
    if (offset < (off_t)bd_get_title_size(this->bdh))
      offset = bd_get_title_size(this->bdh) - offset;
    else
      offset = 0;
  }

  lprintf("bluray_plugin_seek() seeking to %lld\n", (long long)offset);

  return bd_seek (this->bdh, offset);
}

static off_t bluray_plugin_seek_time (input_plugin_t *this_gen, int time_offset, int origin)
{
  bluray_input_plugin_t *this = (bluray_input_plugin_t *) this_gen;

  if (!this || !this->bdh)
    return -1;

  if (this->still_end_time)
    return bd_tell(this->bdh);

  /* convert relative seeks to absolute */

  if (origin == SEEK_CUR) {
    time_offset += this_gen->get_current_time(this_gen);
  }
  else if (origin == SEEK_END) {

    pthread_mutex_lock(&this->title_info_mutex);

    if (!this->title_info) {
      pthread_mutex_unlock(&this->title_info_mutex);
      return -1;
    }

    int duration = this->title_info->duration / 90;
    if (time_offset < duration)
      time_offset = duration - time_offset;
    else
      time_offset = 0;

    pthread_mutex_unlock(&this->title_info_mutex);
  }

  lprintf("bluray_plugin_seek_time() seeking to %d.%03ds\n", time_offset / 1000, time_offset % 1000);

  return bd_seek_time(this->bdh, time_offset * INT64_C(90));
}

static off_t bluray_plugin_get_current_pos (input_plugin_t *this_gen)
{
  bluray_input_plugin_t *this = (bluray_input_plugin_t *) this_gen;

  return this->bdh ? bd_tell(this->bdh) : 0;
}

static int bluray_plugin_get_current_time (input_plugin_t *this_gen)
{
  bluray_input_plugin_t *this = (bluray_input_plugin_t *) this_gen;

  return this->bdh ? (int)(bd_tell_time(this->bdh) / UINT64_C(90)) : -1;
}

static off_t bluray_plugin_get_length (input_plugin_t *this_gen)
{
  bluray_input_plugin_t *this = (bluray_input_plugin_t *) this_gen;

  return this->bdh ? (off_t)bd_get_title_size(this->bdh) : (off_t)-1;
}

static uint32_t bluray_plugin_get_blocksize (input_plugin_t *this_gen)
{
  (void)this_gen;
  return ALIGNED_UNIT_SIZE;
}

static const char* bluray_plugin_get_mrl (input_plugin_t *this_gen)
{
  bluray_input_plugin_t *this = (bluray_input_plugin_t *) this_gen;

  return this->mrl;
}

static int get_audio_lang (bluray_input_plugin_t *this, void *data)
{
  /*
   * audio track language:
   * - channel number can be mpeg-ts PID (0x1100 ... 0x11ff)
   */

  unsigned int current_clip = this->current_clip; /* can change any time */

  if (this->title_info && current_clip < this->title_info->clip_count) {
    BLURAY_CLIP_INFO *clip    = &this->title_info->clips[current_clip];
    int channel;

    memcpy(&channel, data, sizeof(channel));

    if (channel >= 0 && channel < clip->audio_stream_count) {
      memcpy(data, clip->audio_streams[channel].lang, 4);
      return INPUT_OPTIONAL_SUCCESS;
    }

    /* search by pid */
    int i;
    for (i = 0; i < clip->audio_stream_count; i++) {
      if (channel == clip->audio_streams[i].pid) {
        memcpy(data, clip->audio_streams[i].lang, 4);
        return INPUT_OPTIONAL_SUCCESS;
      }
    }
  }

  return INPUT_OPTIONAL_UNSUPPORTED;
}

static int get_spu_lang (bluray_input_plugin_t *this, void *data)
{
  /*
   * SPU track language:
   * - channel number can be mpeg-ts PID (0x1200 ... 0x12ff)
   */

  unsigned int current_clip = this->current_clip; /* can change any time */

  if (this->title_info && current_clip < this->title_info->clip_count) {
    BLURAY_CLIP_INFO *clip    = &this->title_info->clips[current_clip];
    int channel;

    memcpy(&channel, data, sizeof(channel));

    if (channel >= 0 && channel < clip->pg_stream_count) {
      memcpy(data, clip->pg_streams[channel].lang, 4);
      return INPUT_OPTIONAL_SUCCESS;
    }

    /* search by pid */
    int i;
    for (i = 0; i < clip->pg_stream_count; i++) {
      if (channel == clip->pg_streams[i].pid) {
        memcpy(data, clip->pg_streams[i].lang, 4);
        return INPUT_OPTIONAL_SUCCESS;
      }
    }
  }

  return INPUT_OPTIONAL_UNSUPPORTED;
}

static int get_optional_data_impl (bluray_input_plugin_t *this, void *data, int data_type)
{
  int r;

  switch (data_type) {

    case INPUT_OPTIONAL_DATA_DEMUXER:
      if (data)
        *(const char **)data = "mpeg-ts";
      return INPUT_OPTIONAL_SUCCESS;

    case INPUT_OPTIONAL_DATA_AUDIOLANG:
      r = get_audio_lang(this, data);
      return r;

    case INPUT_OPTIONAL_DATA_SPULANG:
      r = get_spu_lang(this, data);
      return r;

    case INPUT_OPTIONAL_DATA_DURATION:
      if (data && this->title_info) {
        uint32_t duration = (uint32_t)(this->title_info->duration / UINT64_C(90));
        memcpy (data, &duration, sizeof (duration));
        return INPUT_OPTIONAL_SUCCESS;
      }
      return INPUT_OPTIONAL_UNSUPPORTED;

    default:
      return INPUT_OPTIONAL_UNSUPPORTED;
    }

  return INPUT_OPTIONAL_UNSUPPORTED;
}

static int bluray_plugin_get_optional_data (input_plugin_t *this_gen, void *data, int data_type)
{
  bluray_input_plugin_t *this = (bluray_input_plugin_t *) this_gen;
  int r = INPUT_OPTIONAL_UNSUPPORTED;

  if (this && this->stream && data) {
    pthread_mutex_lock(&this->title_info_mutex);
    r = get_optional_data_impl(this, data, data_type);
    pthread_mutex_unlock(&this->title_info_mutex);
  }

  return r;
}

static void bluray_plugin_dispose (input_plugin_t *this_gen)
{
  bluray_input_plugin_t *this = (bluray_input_plugin_t *) this_gen;

  if (this->bdh) {
#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 2, 4)
    bd_register_argb_overlay_proc(this->bdh, NULL, NULL, NULL);
#endif
    bd_register_overlay_proc(this->bdh, NULL, NULL);
  }

  close_overlay(this, -1);

  if (this->event_queue)
    xine_event_dispose_queue(this->event_queue);

  pthread_mutex_lock(&this->title_info_mutex);
  if (this->title_info)
    bd_free_title_info(this->title_info);
  this->title_info = NULL;
  pthread_mutex_unlock(&this->title_info_mutex);

  pthread_mutex_destroy(&this->title_info_mutex);

  if (this->bdh)
    bd_close(this->bdh);

#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 2, 4)
  osd_buf_destroy(&this->osd_buf);
#endif

  _x_freep (&this->mrl);
  _x_freep (&this->disc_root);
  _x_freep (&this->disc_name);

  free (this);
}

static int parse_mrl(const char *mrl_in, char **path, int *title, int *chapter)
{
  int skip = 0;

  if (!strncasecmp(mrl_in, "bluray:", 7))
    skip = 7;
  else if (!strncasecmp(mrl_in, "bd:", 3))
    skip = 3;
  else
    return -1;

  char *mrl = strdup(mrl_in + skip);
  if (!mrl)
    return 0;

  /* title[.chapter] given ? parse and drop it */
  if (title && mrl[0] && mrl[strlen(mrl)-1] != '/') {
    char *end = strrchr(mrl, '/');
    int tail_len = 0;
    if (end && end[1]) {
      if (sscanf(end, "/%d.%d%n", title, chapter, &tail_len) < 1)
        *title = -1;
      else if (end[tail_len])
        *title = -1;
      else
        *end = 0;
    }
    lprintf(" -> title %d, chapter %d, mrl \'%s\'\n", *title, *chapter, mrl);
  }

  if ((mrl[0] == 0) || !strcmp(mrl, "/") || !strcmp(mrl, "//") || !strcmp(mrl, "///")) {

    /* default device */
    *path = NULL;

  } else if (*mrl == '/') {

    /* strip extra slashes */
    char *start = mrl;
    while (start[0] == '/' && start[1] == '/')
      start++;

    *path = strdup(start);

    _x_mrl_unescape(*path);

    lprintf("non-defaut mount point \'%s\'\n", *path);

  } else {
    lprintf("invalid mrl \'%s\'\n", mrl_in);
    free(mrl);
    return 0;
  }

  free(mrl);

  return 1;
}

static int get_disc_info(bluray_input_plugin_t *this)
{
  const BLURAY_DISC_INFO *disc_info;

  disc_info = bd_get_disc_info(this->bdh);

  if (!disc_info) {
    LOGMSG("bd_get_disc_info() failed\n");
    return -1;
  }

  if (!disc_info->bluray_detected) {
    LOGMSG("bd_get_disc_info(): BluRay not detected\n");
    this->nav_mode = 0;
    return 0;
  }

  if (disc_info->aacs_detected && !disc_info->aacs_handled) {
    if (!disc_info->libaacs_detected)
      _x_message (this->stream, XINE_MSG_ENCRYPTED_SOURCE,
                  "Media stream scrambled/encrypted with AACS",
                  "libaacs not installed", NULL);
    else
      _x_message (this->stream, XINE_MSG_ENCRYPTED_SOURCE,
                  "Media stream scrambled/encrypted with AACS", NULL);
    return -1;
  }

  if (disc_info->bdplus_detected && !disc_info->bdplus_handled) {
    if (!disc_info->libbdplus_detected)
      _x_message (this->stream, XINE_MSG_ENCRYPTED_SOURCE,
                  "Media scrambled/encrypted with BD+",
                  "libbdplus not installed.", NULL);
    else
      _x_message (this->stream, XINE_MSG_ENCRYPTED_SOURCE,
                  "Media stream scrambled/encrypted with BD+", NULL);
    return -1;
  }

  if (this->nav_mode && !disc_info->first_play_supported) {
    _x_message (this->stream, XINE_MSG_GENERAL_WARNING,
                "Can't play disc using menus",
                "First Play title not supported", NULL);
    this->nav_mode = 0;
  }

  if (this->nav_mode && disc_info->num_unsupported_titles > 0) {
    _x_message (this->stream, XINE_MSG_GENERAL_WARNING,
                "Unsupported titles found",
                "Some titles can't be played in navigation mode", NULL);
  }

  if (this->nav_mode && disc_info->num_bdj_titles > 0) {
    if (!(this->stream->video_out->get_capabilities(this->stream->video_out) & VO_CAP_ARGB_LAYER_OVERLAY)) {
      _x_message (this->stream, XINE_MSG_GENERAL_WARNING,
                  "BD-J titles found. Current video driver does not support ARGB graphics.",
                  "Try another video driver (ex. --video opengl2) or play this disc without menus.",
                  NULL);
    }
  }

  this->num_titles = disc_info->num_hdmv_titles + disc_info->num_bdj_titles;
  this->disc_info  = disc_info;

  return 1;
}

static char *get_disc_name(const char *path)
{
  const char *name_start;
  char       *file_name  = NULL;
  int         len;

  name_start = path + strlen(path) - 1;
  /* skip trailing '/' */
  while (name_start > path && name_start[0] == '/')
    name_start--;
  /* find prev '/' */
  while (name_start > path && name_start[-1] != '/')
    name_start--;

  file_name = strdup(name_start);
  len = strlen(file_name);

  /* trim trailing '/' */
  while (len > 0 && file_name[len - 1] ==  '/')
    file_name[--len] = 0;

  /* trim trailing ".iso" */
  if (len > 3 && !strcasecmp(file_name + len - 4, ".iso"))
    file_name[len - 4] = 0;

  /* '_' --> ' ' */
  for (len = 0; file_name[len]; ++len)
    if (file_name[len] == '_')
      file_name[len] = ' ';

  lprintf("disc name: %s\n", file_name);
  return file_name;
}

static int is_iso_image(const char *mrl)
{
  if (mrl) {
    const char *pos = strrchr(mrl, '.');
    return pos && !strcasecmp(pos + 1, "iso");
  }
  return 0;
}

static int bluray_plugin_open (input_plugin_t *this_gen)
{
  bluray_input_plugin_t *this    = (bluray_input_plugin_t *) this_gen;
  int                    title   = -1;
  int                    chapter = 0;
  int major, minor, micro;

  lprintf("bluray_plugin_open '%s'\n",this->mrl);

  /* validate and parse mrl */
  if (!parse_mrl(this->mrl, &this->disc_root, &title, &chapter))
    return -1;

  if (!strncasecmp(this->mrl, "bd:", 3))
    this->nav_mode = 1;

  if (!this->disc_root)
    this->disc_root = strdup(this->class->mountpoint);

  bd_get_version(&major, &minor, &micro);
  if (BLURAY_VERSION_CODE(major, minor, micro) < BLURAY_VERSION_CODE(0, 8, 0)) {
    if (is_iso_image(this->disc_root)) {
      _x_message (this->stream, XINE_MSG_GENERAL_WARNING,
                  "Can't play BluRay .iso image. Update libbluray.",
                  "", NULL);
      return -1;
    }
  }

  /* open libbluray */

  if (! (this->bdh = bd_open (this->disc_root, NULL))) {
    LOGMSG("bd_open(\'%s\') failed: %s\n", this->disc_root, strerror(errno));
    return -1;
  }
  lprintf("bd_open(\'%s\') OK\n", this->disc_root);

  if (get_disc_info(this) < 0) {
    return -1;
  }

  /* load title list */

  if (!this->nav_mode) {
    this->num_title_idx = bd_get_titles(this->bdh, TITLES_RELEVANT, MIN_TITLE_LENGTH);
    LOGMSG("%d titles\n", this->num_title_idx);

    if (this->num_title_idx < 1)
      return -1;

    /* if title was not in mrl, guess the main title */
#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 5, 0)
    if (title < 0) {
      title = bd_get_main_title(this->bdh);
      LOGMSG("main title: %d\n", title);
    }
#endif
    if (title < 0) {
      uint64_t duration = 0;
      int i, playlist = 99999;
      for (i = 0; i < this->num_title_idx; i++) {
        BLURAY_TITLE_INFO *info = bd_get_title_info(this->bdh, i, 0);
        if (info->duration > duration) {
          title    = i;
          duration = info->duration;
          playlist = info->playlist;
        }
        bd_free_title_info(info);
      }
      LOGMSG("main title: %d (%05d.mpls)\n", title, playlist);
    }

  } else {
    LOGMSG("%d titles\n", this->num_titles);
  }

  /* update player settings */

  bd_set_player_setting    (this->bdh, BLURAY_PLAYER_SETTING_REGION_CODE,  this->class->region);
  bd_set_player_setting    (this->bdh, BLURAY_PLAYER_SETTING_PARENTAL,     this->class->parental);
  bd_set_player_setting_str(this->bdh, BLURAY_PLAYER_SETTING_AUDIO_LANG,   this->class->language);
  bd_set_player_setting_str(this->bdh, BLURAY_PLAYER_SETTING_PG_LANG,      this->class->language);
  bd_set_player_setting_str(this->bdh, BLURAY_PLAYER_SETTING_MENU_LANG,    this->class->language);
  bd_set_player_setting_str(this->bdh, BLURAY_PLAYER_SETTING_COUNTRY_CODE, this->class->country);

  /* init event queue */
  bd_get_event(this->bdh, NULL);

  /* get disc name */

  this->meta_dl = bd_get_meta(this->bdh);

  if (this->meta_dl && this->meta_dl->di_name && strlen(this->meta_dl->di_name) > 1) {
    this->disc_name = strdup(this->meta_dl->di_name);
  }
  else if (strcmp(this->disc_root, this->class->mountpoint)) {
    this->disc_name = get_disc_name(this->disc_root);
  }

  /* register overlay (graphics) handler */

#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 2, 4)
  if (this->stream->video_out->get_capabilities(this->stream->video_out) & VO_CAP_ARGB_LAYER_OVERLAY) {
    osd_buf_init(&this->osd_buf);
    bd_register_argb_overlay_proc(this->bdh, this, argb_overlay_proc, &this->osd_buf.buf);
  }
#endif

  bd_register_overlay_proc(this->bdh, this, overlay_proc);

  /* open */
  this->current_title = -1;
  this->current_title_idx = -1;

  if (this->nav_mode) {
    if (bd_play(this->bdh) <= 0) {
      LOGMSG("bd_play() failed\n");
      return -1;
    }

  } else {
    if (open_title(this, title) <= 0 &&
        open_title(this, 0) <= 0)
      return -1;
  }

  /* jump to chapter */

  if (chapter > 0) {
    chapter = MAX(0, MIN((int)this->title_info->chapter_count, chapter) - 1);
    bd_seek_chapter(this->bdh, chapter);
    _x_stream_info_set(this->stream, XINE_STREAM_INFO_DVD_CHAPTER_NUMBER, chapter + 1);
  }

  return 1;
}

static input_plugin_t *bluray_class_get_instance (input_class_t *cls_gen, xine_stream_t *stream,
                                                  const char *mrl)
{
  bluray_input_plugin_t *this;

  if (strncasecmp(mrl, "bluray:", 7) && strncasecmp(mrl, "bd:", 3))
    return NULL;

  this = calloc(1, sizeof (bluray_input_plugin_t));
  if (!this)
    return NULL;

  this->stream = stream;
  this->class  = (bluray_input_class_t*)cls_gen;
  this->mrl    = strdup(mrl);
  if (!this->mrl) {
    free(this);
    return NULL;
  }

  this->input_plugin.open               = bluray_plugin_open;
  this->input_plugin.get_capabilities   = bluray_plugin_get_capabilities;
  this->input_plugin.read               = bluray_plugin_read;
  this->input_plugin.read_block         = bluray_plugin_read_block;
  this->input_plugin.seek               = bluray_plugin_seek;
  this->input_plugin.seek_time          = bluray_plugin_seek_time;
  this->input_plugin.get_current_pos    = bluray_plugin_get_current_pos;
  this->input_plugin.get_current_time   = bluray_plugin_get_current_time;
  this->input_plugin.get_length         = bluray_plugin_get_length;
  this->input_plugin.get_blocksize      = bluray_plugin_get_blocksize;
  this->input_plugin.get_mrl            = bluray_plugin_get_mrl;
  this->input_plugin.get_optional_data  = bluray_plugin_get_optional_data;
  this->input_plugin.dispose            = bluray_plugin_dispose;
  this->input_plugin.input_class        = cls_gen;

  this->event_queue = xine_event_new_queue (this->stream);

  pthread_mutex_init(&this->title_info_mutex, NULL);

  this->pg_stream = -1;

  return &this->input_plugin;
}

/*
 * plugin class
 */

static void mountpoint_change_cb(void *data, xine_cfg_entry_t *cfg)
{
  bluray_input_class_t *class = (bluray_input_class_t *) data;

  class->mountpoint = cfg->str_value;
}

static void device_change_cb(void *data, xine_cfg_entry_t *cfg)
{
  bluray_input_class_t *class = (bluray_input_class_t *) data;

  class->device = cfg->str_value;
}

static void language_change_cb(void *data, xine_cfg_entry_t *cfg)
{
  bluray_input_class_t *class = (bluray_input_class_t *) data;

  class->language = cfg->str_value;
}

static void country_change_cb(void *data, xine_cfg_entry_t *cfg)
{
  bluray_input_class_t *class = (bluray_input_class_t *) data;

  class->country = cfg->str_value;
}

static void region_change_cb(void *data, xine_cfg_entry_t *cfg)
{
  bluray_input_class_t *class = (bluray_input_class_t *) data;

  class->region = cfg->num_value;
}

static void parental_change_cb(void *data, xine_cfg_entry_t *cfg)
{
  bluray_input_class_t *class = (bluray_input_class_t *) data;

  class->parental = cfg->num_value;
}

static void skip_mode_change_cb (void *data, xine_cfg_entry_t *cfg) {
  bluray_input_class_t *class = (bluray_input_class_t *) data;
  class->skip_mode = cfg->num_value;
}

static const char * const *bluray_class_get_autoplay_list (input_class_t *this_gen, int *num_files)
{
  static const char * const autoplay_list[] = { "bluray:/", NULL };

  (void)this_gen;
  *num_files = 1;

  return autoplay_list;
}

static xine_mrl_t **bluray_class_get_dir(input_class_t *this_gen, const char *filename, int *nFiles)
{
  bluray_input_class_t *this = (bluray_input_class_t*) this_gen;
  char *path = NULL;
  int title = -1, chapter = -1, i, num_pl;
  BLURAY *bdh;

  lprintf("bluray_class_get_dir(%s)\n", filename);

  _x_input_free_mrls(&this->xine_playlist);
  *nFiles = 0;

  if (filename)
    parse_mrl(filename, &path, &title, &chapter);

  bdh = bd_open(path ? path : this->mountpoint, NULL);
  if (!bdh)
    goto out;

  num_pl = bd_get_titles(bdh, TITLES_RELEVANT, MIN_TITLE_LENGTH);
  if (num_pl < 1)
    goto out;

  this->xine_playlist = _x_input_alloc_mrls(num_pl);
  if (!this->xine_playlist)
    goto out;

  for (i = 0; i < num_pl; i++) {
    this->xine_playlist[i]->origin = _x_asprintf("bluray:/%s", path ? path : "");
    this->xine_playlist[i]->mrl    = _x_asprintf("bluray:/%s/%d", path ? path : "", i);
    this->xine_playlist[i]->type   = mrl_dvd;
  }

  *nFiles = num_pl;

 out:
  if (bdh)
    bd_close(bdh);
  free(path);
  return this->xine_playlist;
}

static int bluray_class_eject_media (input_class_t *this_gen)
{
  bluray_input_class_t *this = (bluray_input_class_t*) this_gen;

  return media_eject_media (this->xine, this->device);
}

static void bluray_class_dispose (input_class_t *this_gen)
{
  bluray_input_class_t *this   = (bluray_input_class_t *) this_gen;
  config_values_t      *config = this->xine->config;

  _x_input_free_mrls(&this->xine_playlist);

  config->unregister_callbacks (config, NULL, NULL, this, sizeof (*this));

  free (this);
}

static void *bluray_init_plugin (xine_t *xine, const void *data)
{
  static const char * const skip_modes[] = {"skip chapter", "skip title", NULL};

  config_values_t      *config = xine->config;
  bluray_input_class_t *this   = (bluray_input_class_t *) calloc(1, sizeof (bluray_input_class_t));
  if (!this)
    return NULL;

  (void)data;
  this->xine = xine;

  this->input_class.get_instance       = bluray_class_get_instance;
  this->input_class.get_dir            = bluray_class_get_dir;
  this->input_class.get_autoplay_list  = bluray_class_get_autoplay_list;
  this->input_class.dispose            = bluray_class_dispose;
  this->input_class.eject_media        = bluray_class_eject_media;

  this->input_class.identifier     = "bluray";
  this->input_class.description    = _("BluRay input plugin");

  this->mountpoint = config->register_filename (config, "media.bluray.mountpoint",
    BLURAY_MNT_PATH, XINE_CONFIG_STRING_IS_DIRECTORY_NAME,
    _("BluRay mount point"),
    _("Default mount location for BluRay discs."),
    0, mountpoint_change_cb, (void *) this);

  this->device = config->register_filename (config, "media.bluray.device",
    BLURAY_PATH, XINE_CONFIG_STRING_IS_DEVICE_NAME,
    _("device used for BluRay playback"),
    _("The path to the device which you intend to use for playing BluRay discs."),
    0, device_change_cb, (void *) this);

  /* Player settings */
  this->language = config->register_string (config, "media.bluray.language",
    "eng",
    _("default language for BluRay playback"),
    _("xine tries to use this language as a default for BluRay playback. "
      "As far as the BluRay supports it, menus and audio tracks will be presented in this language.\n"
      "The value must be a three characterISO639-2 language code."),
    0, language_change_cb, this);

  this->country = config->register_string (config, "media.bluray.country",
    "en",
    _("BluRay player country code"),
    _("The value must be a two character ISO3166-1 country code."),
    0, country_change_cb, this);

  this->region = config->register_num (config, "media.bluray.region",
    7,
    _("BluRay player region code (1=A, 2=B, 4=C)"),
    _("This only needs to be changed if your BluRay jumps to a screen "
      "complaining about a wrong region code. It has nothing to do with "
      "the region code set in BluRay drives, this is purely software."),
    0, region_change_cb, this);

  this->parental = config->register_num (config, "media.bluray.parental",
    99,
    _("parental control age limit (1-99)"),
    _("Prevents playback of BluRay titles where parental control age limit "
      "is higher than this limit"),
    0, parental_change_cb, this);

  this->skip_mode = config->register_enum (config, "media.bluray.skip_behaviour",
    0,
    (char **)skip_modes,
    _("unit for the skip action"),
    _("You can configure the behaviour when issuing a skip command (using the skip "
      "buttons for example)."),
    20, skip_mode_change_cb, this);

  return this;
}

static const char * const *bd_class_get_autoplay_list (input_class_t *this_gen, int *num_files)
{
  static const char * const autoplay_list[] = { "bd:/", NULL };

  (void)this_gen;
  *num_files = 1;

  return autoplay_list;
}

static void *bd_init_plugin (xine_t *xine, const void *data)
{
  bluray_input_class_t *this = bluray_init_plugin(xine, data);

  if (this) {
    this->input_class.identifier  = "bluray";
    this->input_class.description = _("BluRay input plugin (using menus)");

    this->input_class.get_dir            = NULL;
    this->input_class.get_autoplay_list  = bd_class_get_autoplay_list;
  }

  return this;
}

/*
 * exported plugin catalog entry
 */

const plugin_info_t xine_plugin_info[] EXPORTED = {
  /* type, API, "name", version, special_info, init_function */
  { PLUGIN_INPUT, 18, "BLURAY", XINE_VERSION_CODE, NULL, bluray_init_plugin },
  { PLUGIN_INPUT, 18, "BD",     XINE_VERSION_CODE, NULL, bd_init_plugin },
  { PLUGIN_NONE, 0, NULL, 0, NULL, NULL }
};
