/*
 * Copyright (C) 2004-2017 the xine-project
 *
 * This program 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.
 *
 * This program 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 St, Fifth Floor, Boston, MA  02110-1301
 * USA
 *
 * player object
 */

#include "globals.h"

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

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include <gtk/gtk.h>

#include <xine.h>

#include "globals.h"
#include "menu.h"
#include "ui.h"
#include "utils.h"
#include "preferences.h"
#include "playlist.h"
#include "gtkvideo.h"
#include "snapshot.h"
#include "engine.h"
#include "player.h"

#define GXINE_VO_ZOOM_MIN (XINE_VO_ZOOM_MIN < 0 ? 0 : XINE_VO_ZOOM_MIN)
#define GXINE_MAX_SPU_CHANNEL (31)

xine_stream_t *stream; /* global stream */
xine_audio_port_t *audio_port;
xine_video_port_t *video_port;

/*
 * async play machanism
 *
 * always start xine engine from a seperate thread so the gui stays responsive
 */

static int             goto_pos, goto_time, end_time;
static pthread_t       play_thread;
int                    play_thread_created = 0;
static char           *cur_mrl = NULL, *cur_title = NULL;

static gboolean demuxer_for_logo = TRUE;

static int play_exec_error_suppress = 0;

void play_exec_error_suppress_next (void)
{
  ++play_exec_error_suppress;
}

static void play_exec_error (void)
{
  if (play_exec_error_suppress)
  {
#ifdef XINE_PARAM_GAPLESS_SWITCH
    xine_set_param (stream, XINE_PARAM_GAPLESS_SWITCH, 0);
#endif
    --play_exec_error_suppress;
    return;
  }

  int error;
  switch (error = xine_get_error (stream))
  {
  case XINE_ERROR_NO_INPUT_PLUGIN:
    display_error (FROM_XINE_COMBINABLE, _("The xine engine failed to start."),
		   _("No input plugin was found.\n"
		     "Maybe the file does not exist or cannot be accessed, "
		     "or there is an error in the URL."));
    break;

  case XINE_ERROR_NO_DEMUX_PLUGIN:
    if (playlist_showing_logo ())
    {
      g_idle_add ((GSourceFunc)infobar_show_metadata, infobars);
      if (demuxer_for_logo)
      {
	demuxer_for_logo = FALSE;
	display_error (FROM_XINE,
		       _("Whoops. You seem to have a broken xine-lib."),
		       _("No demuxer found - stream format not recognised."));
      }
    }
    else
      display_error (FROM_XINE, _("The xine engine failed to start."),
		     _("No demuxer found - stream format not recognised."));
    break;

  case XINE_ERROR_DEMUX_FAILED:
    display_error (FROM_XINE, _("The xine engine failed to start."),
		   _("Demuxing failed.\n"
		     "Maybe the stream is corrupt or of an unexpected type."));
    break;

  case XINE_ERROR_MALFORMED_MRL:
    display_error (FROM_XINE, _("The xine engine failed to start."),
		   _("The MRL is malformed."));
    break;

  case XINE_ERROR_INPUT_FAILED:
    display_error (FROM_XINE_COMBINABLE, _("The xine engine failed to start."),
		   _("The stream could not be opened."));
    break;


  default:
    display_error (FROM_XINE, _("The xine engine failed to start."),
		   _("Unknown error (code %d)"), error);
  }

#ifdef XINE_PARAM_GAPLESS_SWITCH
  xine_set_param (stream, XINE_PARAM_GAPLESS_SWITCH, 0);
#endif

  if (!playlist_showing_logo ())
    g_idle_add ((GSourceFunc)playlist_logo, NULL);
}

static void my_gdk_threads_leave(void *p)
{
  gdk_threads_leave();
}

static void my_pthread_mutex_unlock(void *p)
{
  pthread_mutex_unlock(p);
}

static __attribute__ ((noreturn)) void *play_exec (void *data)
{
  xine_cfg_entry_t entry;
  const char *hash;
  gchar *mrl = NULL;

  pthread_setcancelstate (PTHREAD_CANCEL_ENABLE, NULL);
  pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

  /*pthread_mutex_lock (&engine_lock);*/
  pthread_mutex_lock (&widgets_update_lock);
  pthread_cleanup_push (my_pthread_mutex_unlock, &widgets_update_lock);
  gdk_threads_enter();
  pthread_cleanup_push (my_gdk_threads_leave, NULL);

  gxineinfo_clear (infobars);
  gxineinfo_update_line (infobars, 0, "%s", _("opening..."));
  gxineinfo_update_line (infobars, 1, "%s", cur_mrl);

  if ((!xine_config_lookup_entry (xine, "gui.subtitle_autoload", &entry)
       || entry.num_value)
      && !strncmp (cur_mrl, "file:", 5)
      && ((hash = strchr (cur_mrl, '#')) == NULL
	  || (strncmp (hash, "#subtitle:", 10)
	      && !strstr (hash, ";subtitle:"))))
  {
    char *name = cur_mrl + 5;
    if (name)
    {
      unsigned int length = (strchr (name, '#') ? : name + strlen (name)) - name;
      char file[length + 5]; /* allow for ".ext" + NUL */

      unsigned int i = UINT_MAX, ext = 0;

      /* URL decode (can't simply use g_filename_from_uri()...) */
      while (++i < length)
      {
	if (name[i] == '%')
	{
	  char c[3] = { name[i+1], name[i+2], 0 };
	  i += 2;
	  file[ext] = (char) strtol (c, NULL, 16);
	}
	else
	  file[ext] = name[i];
	++ext;
      }

      /* find last '.' in leaf */
      i = ext;
      if (ext)
	while (--ext && file[ext] != '.' && file[ext] != '/')
	  /**/;
      if (file[ext] == '/') /* if no '.' */
	ext = i;

      name = strrchr (name, '.');

      static const char subs[][4] = { "sub", "srt", "asc", "smi", "ssa", "txt" };
      for (i = 0; i < G_N_ELEMENTS (subs); ++i)
	if (!name || strcmp (name + 1, subs[i]))
	{
	  struct stat st;
	  sprintf (file + ext, ".%s", subs[i]);
	  if (stat (file, &st) > -1 && S_ISREG (st.st_mode))
	    break;
	}

      if (i < G_N_ELEMENTS (subs))
      {
        gchar *tmp = g_filename_to_uri (file, NULL, NULL);
	mrl = g_strconcat (cur_mrl, hash ? ";subtitle:" : "#subtitle:", tmp + 7, NULL);
        g_free (tmp);
      }
    }
  }

  pthread_cleanup_pop (TRUE);
  int ret = xine_open (stream, mrl ? : cur_mrl);

  if (ret)
  {
    gdk_threads_enter ();
    pthread_cleanup_push (my_gdk_threads_leave, NULL);
    ui_set_status (playlist_showing_logo () ? UI_STOP : UI_PLAY);
    infobar_show_metadata (infobars);
    pthread_cleanup_pop (TRUE);

//    xine_play (stream, 0, 0);
    xine_play (stream, goto_pos, goto_time);
    gtk_video_set_end_time ((GtkVideo *)gtv, end_time);
    goto_pos = 0;
    goto_time = 0;
    end_time = 0;

    /* correct for possible stream changes */
    gdk_threads_enter ();
    pthread_cleanup_push (my_gdk_threads_leave, NULL);
    ui_set_status (playlist_showing_logo () ? UI_STOP : UI_PLAY);
    infobar_show_metadata (infobars);
    playlist_check_set_title ();
    pthread_cleanup_pop (TRUE);
  }
  else
  {
    gdk_threads_enter ();
    pthread_cleanup_push (my_gdk_threads_leave, NULL);
    play_exec_error ();
    pthread_cleanup_pop (TRUE);
  }

  g_free (mrl);
  pthread_cleanup_pop (TRUE);
  /*pthread_mutex_unlock (&engine_lock);*/
  play_thread_created = 0;
  pthread_exit (NULL);
}

void player_launch (const char *title, const char *mrl,
		    int pos, int pos_time, int duration)
{
  int err;
  pthread_attr_t attr;

  goto_pos  = pos < 0 ? 0 : pos;
  goto_time = pos_time < 0 ? 0 : pos_time;
  end_time = duration <= 0 ? 0: pos_time + duration;
  free (cur_title);
  free (cur_mrl);
  cur_title = title && !playlist_showing_logo () ? strdup (title) : NULL;
  cur_mrl = strdup (mrl);

  gtk_video_unblank_screen ((GtkVideo *)gtv);

  if (!demuxer_for_logo && playlist_showing_logo ())
    return;

  /* start separate thread, so xine_open will not block ui */
  pthread_attr_init (&attr);
  pthread_attr_setstacksize (&attr, 256 * 1024); /* probably overkill */
  if ((err = pthread_create (&play_thread, &attr, play_exec, NULL)) != 0)
  {
    display_error_modal (FROM_GXINE, _("Player failure"),
			 _("Can't create new thread: %s\n"), strerror(err));
    abort();
  }
  play_thread_created = 1;

  if (!playlist_showing_logo ())
    pthread_detach (play_thread);

  pthread_attr_destroy (&attr);
}

void player_wait (void)
{
  if (play_thread_created) {
    play_thread_created = 0;
    pthread_join (play_thread, NULL);
  }
}

void player_stop (void)
{
  if (play_thread_created) {
    play_thread_created = 0;
    pthread_cancel (play_thread);
  }
  gtk_video_in_spu_button ((GtkVideo *)gtv, 0);
  xine_stop (stream);
}

static void set_mute (int mute)
{
  ui_set_status (mute ? UI_AUDIO_MUTE : UI_AUDIO_UNMUTE);
  xine_set_param (stream, XINE_PARAM_AUDIO_MUTE, mute);
}

static inline int calc_speed (int i)
{
  return i ? (1 << (i - 1)) : 0;
}

static void set_speed (int speed)
{
  xine_set_param (stream, XINE_PARAM_SPEED, speed);
  if (speed == XINE_SPEED_NORMAL)
    ui_set_status (UI_PLAY);
  else if (speed > XINE_SPEED_NORMAL)
    ui_set_status (UI_FAST_FORWARD);
  else if (speed)
    ui_set_status (UI_PLAY_SLOW);
  else
    ui_set_status (UI_PAUSE);
}

static int listen_speed (void *se, se_t *se_t, se_o_t *o, se_prop_t *prop,
			 se_prop_read_t r)
{
  set_speed (calc_speed (r.i));
  return FALSE;
}

static void get_speed (const struct ui_property_s *prop, se_prop_read_t *value)
{
  int speed = xine_get_param (stream, XINE_PARAM_SPEED);
  int bit = 0;
  if (speed)
    while (++bit, speed >>= 1);
  value->i = bit;
}

static void set_zoom (int zoom)
{
  if (zoom < GXINE_VO_ZOOM_MIN)
    zoom = GXINE_VO_ZOOM_MIN;
  else if (zoom > XINE_VO_ZOOM_MAX)
    zoom = XINE_VO_ZOOM_MAX;
  xine_set_param (stream, XINE_PARAM_VO_ZOOM_X, zoom);
  xine_set_param (stream, XINE_PARAM_VO_ZOOM_Y, zoom);
}

static void set_subtitles (int sub)
{
  if (sub >= -2 && sub <= GXINE_MAX_SPU_CHANNEL)
  {
    GSList *menuitem = action_items.subtitles;
    int count = g_slist_length (menuitem);
    /* last three menu items (in definition order, not menu display order):
     * None (-2), Auto (-1), Other (>= 16, hidden)
     */
    if (count > sub + 3)
    {
      if (sub < 0)
	count = 1; /* for None, Auto */
      while (menuitem && --count > sub)
	menuitem = menuitem->next;
      if (menuitem)
      {
	gtk_action_activate (menuitem->data);
	return;
      }
    }
    /* not in menu */
    gtk_action_activate (action_items.subtitles->data);
    xine_set_param (stream, XINE_PARAM_SPU_CHANNEL, sub);
  }
}

static void set_aspect (int aspect)
{
  GSList *menuitem = action_items.aspect;
  int count = g_slist_length (menuitem);
  if (aspect >= XINE_VO_ASPECT_NUM_RATIOS)
    aspect = XINE_VO_ASPECT_AUTO;
  else if (aspect < 0)
    aspect = XINE_VO_ASPECT_NUM_RATIOS - 1;
  while (--count > aspect && menuitem->next)
    menuitem = menuitem->next;
  if (menuitem)
    gtk_action_activate (menuitem->data);
}

static void update_postproc_chain (const char *pref, const char *value)
{
  xine_cfg_entry_t entry;
  if (xine_config_lookup_entry (xine, pref, &entry)) {
    entry.str_value = (char *)value;
    preferences_update_entry (&entry);
  }
}

static JSBool set_postproc_chain (const char *func, const char *pref,
				  JSContext *cx, uintN argc, jsval *vp)
{
  jsval *argv = JS_ARGV (cx, vp);
  se_log_fncall (func);
  se_argc_check (1, func);
  se_arg_is_string (0, func);
  char *value = SE_JS_ENCODE_STRING (cx, JS_ValueToString (cx, argv[0]));
  update_postproc_chain (pref, value);
  SE_JS_FREE_ENCODED_STRING(cx, value);
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}


static JSBool set_vo_postproc_chain (JSContext *cx, uintN argc, jsval *vp)
{
  return set_postproc_chain ("vo_postproc.set_chain", "gui.post_plugins.video",
			     cx, argc, vp);
}

static JSBool set_deint_postproc_chain (JSContext *cx, uintN argc, jsval *vp)
{
  return set_postproc_chain ("vo_deinterlace.set_chain", "gui.post_plugins.deinterlace",
			     cx, argc, vp);
}

static JSBool set_ao_postproc_chain (JSContext *cx, uintN argc, jsval *vp)
{
  return set_postproc_chain ("ao_postproc.set_chain", "gui.post_plugins.audio",
			     cx, argc, vp);
}

static int listen_postproc_chain (void *pref, se_t *se, se_o_t *obj,
				  se_prop_t *prop, se_prop_read_t value)
{
  update_postproc_chain (pref, value.s);
  return 0;
}

static JSBool get_time (JSContext *cx, uintN argc, jsval *vp)
{
  int   pos, pos_time, len;
  se_log_fncall ("get_time");
  xine_get_pos_length (stream, &pos, &pos_time, &len);
  JS_SET_RVAL (cx, vp, INT_TO_JSVAL (pos_time));
  return JS_TRUE;
}

static JSBool get_pos (JSContext *cx, uintN argc, jsval *vp)
{
  int pos, pos_time, len;
  se_log_fncall ("get_pos");
  xine_get_pos_length (stream, &pos, &pos_time, &len);

  jsval rval = JS_RVAL (cx, vp);
  SE_JS_NEW_NUMBER_VAL (cx, pos / 655.35, rval);
  return JS_TRUE;
}

static JSBool has_time (JSContext *cx, uintN argc, jsval *vp)
{
  int pos, pos_time, len;
  se_log_fncall ("has_time");
  xine_get_pos_length (stream, &pos, &pos_time, &len);
  JS_SET_RVAL (cx, vp, BOOLEAN_TO_JSVAL (len != 0));
  return JS_TRUE;
}

#ifdef WITH_OBSOLETE

static JSBool js_get_speed (JSContext *cx, uintN argc, jsval *vp)
{
  se_prop_read_t speed;
  se_log_fncall_obsolete ("get_speed");
  get_speed (NULL, &speed);
  JS_SET_RVAL (cx, vp, INT_TO_JSVAL (speed.i));
  return JS_TRUE;
}

static JSBool js_get_volume (JSContext *cx, uintN argc, jsval *vp)
{
  /* se_t *se = (se_t *) JS_GetContextPrivate(cx); */
  int   volume;
  se_log_fncall_obsolete ("get_volume");
  volume = xine_get_param (stream, XINE_PARAM_AUDIO_VOLUME);
  JS_SET_RVAL (cx, vp, INT_TO_JSVAL (volume));
  return JS_TRUE;
}

static JSBool js_get_mute (JSContext *cx, uintN argc, jsval *vp)
{
  /* se_t *se = (se_t *) JS_GetContextPrivate(cx); */
  int mute;
  se_log_fncall_obsolete ("get_mute");
  mute = xine_get_param (stream, XINE_PARAM_AUDIO_MUTE);
  JS_SET_RVAL (cx, vp, BOOLEAN_TO_JSVAL (mute));
  return JS_TRUE;
}

static JSBool js_get_zoom (JSContext *cx, uintN argc, jsval *vp)
{
  /* se_t *se = (se_t *) JS_GetContextPrivate(cx); */
  int   zoom;
  se_log_fncall_obsolete ("get_zoom");
  zoom = xine_get_param (stream, XINE_PARAM_VO_ZOOM_X);
  JS_SET_RVAL (cx, vp, INT_TO_JSVAL (zoom));
  return JS_TRUE;
}

#endif /* WITH_OBSOLETE */

static JSBool controls_pause (JSContext *cx, uintN argc, jsval *vp)
{
  int32 pause_state=-1;
  jsval *argv = JS_ARGV (cx, vp);

  se_log_fncall_checkinit ("pause");
  se_argc_check_max (1, "pause");

  if (argc == 1)
  {
    se_arg_is_int_or_bool (0, "pause");
    JS_ValueToInt32 (cx, argv[0], &pause_state);
  }

  switch (pause_state)
  {
  case 0: /* pause off */
    set_speed (XINE_SPEED_NORMAL);
    break;
  case 1: /* pause on */
    set_speed (XINE_SPEED_PAUSE);
    break;
  default: /* toggle */
    if (xine_get_param (stream, XINE_PARAM_SPEED) != XINE_SPEED_PAUSE)
      set_speed (XINE_SPEED_PAUSE);
    else
      set_speed (XINE_SPEED_NORMAL);
  }

  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool controls_stop (JSContext *cx, uintN argc, jsval *vp)
{
  se_log_fncall_checkinit ("stop");
  player_stop ();
  playlist_logo (NULL);
  ui_set_control_adjustment (Control_SEEKER, 0);
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool controls_eject (JSContext *cx, uintN argc, jsval *vp)
{
  se_log_fncall_checkinit ("eject");
  if (xine_get_status (stream) == XINE_STATUS_PLAY)
    player_stop ();
  gdk_threads_leave (); /* need to do this to allow play_exec() to proceed */
  playlist_logo (cx); /* doesn't matter so long as it's not NULL */
  gdk_threads_enter ();
  ui_set_control_adjustment (Control_SEEKER, 0);
  gtk_video_in_spu_button ((GtkVideo *)gtv, 0);
  int ret = xine_eject (stream);
  JS_SET_RVAL (cx, vp, BOOLEAN_TO_JSVAL (ret));
  return JS_TRUE;
}

#ifdef WITH_OBSOLETE

static JSBool js_set_speed (JSContext *cx, uintN argc, jsval *vp)
{
  int32 speed;
  jsval *argv = JS_ARGV (cx, vp);

  se_log_fncall_obsolete ("set_speed");

  se_argc_check (1, "set_speed");
  se_arg_is_int (0, "set_speed");

  JS_ValueToInt32 (cx, argv[0], &speed);

  if (speed<0)
    speed = 0;
  set_speed (calc_speed (speed));

  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool js_set_volume (JSContext *cx, uintN argc, jsval *vp)
{
  int32 volume;
  jsval *argv = JS_ARGV (cx, vp);

  se_log_fncall_obsolete ("set_volume");
  se_argc_check (1, "set_volume");
  se_arg_is_int (0, "set_volume");

  JS_ValueToInt32 (cx, argv[0], &volume);

  if (volume<0)
    volume = 0;
  ui_set_control_adjustment (Control_VOLUME, volume);
  ui_xine_set_param_from_adjustment (Control_VOLUME);

  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool js_set_mute (JSContext *cx, uintN argc, jsval *vp)
{
  JSBool mute;
  jsval *argv = JS_ARGV (cx, vp);

  se_log_fncall_obsolete ("set_mute");
  se_argc_check_max (1, "set_mute");

  if (argc == 1)
  {
    se_arg_is_int_or_bool (0, "set_mute");
    JS_ValueToBoolean (cx, argv[0], &mute);
  }
  else
    mute = !xine_get_param (stream, XINE_PARAM_AUDIO_MUTE);

  set_mute (mute);

  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool js_set_zoom (JSContext *cx, uintN argc, jsval *vp)
{
  int32 zoom;
  jsval *argv = JS_ARGV (cx, vp);

  se_log_fncall_obsolete ("set_zoom");
  se_argc_check (1, "set_zoom");
  se_arg_is_int (0, "set_zoom");

  JS_ValueToInt32 (cx, argv[0], &zoom);
  set_zoom (zoom);

  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

#endif /* WITH_OBSOLETE */

static int zoomcb, zoomcb_id = 0;

static gboolean video_rescale_cb (gpointer unused)
{
  gtk_video_rescale ((GtkVideo *) gtv, zoomcb);
  zoomcb_id = 0;
  return FALSE;
}

static gboolean set_video_size_cb (gpointer unused)
{
  gdk_threads_enter ();
  /* find & activate an action item which corresponds to the requested scale */
  GtkAction *action = (zoomcb == -1) ? NULL : find_video_size_action (zoomcb);
  if (action)
  {
    gtk_action_activate (action);
    zoomcb_id = 0;
  }
  else
  {
    scale_changed_cb (NULL, zoomcb, NULL);
    zoomcb_id = g_idle_add ((GSourceFunc) video_rescale_cb, NULL);
  }
  gdk_threads_leave ();
  return FALSE;
}

static JSBool js_set_video_size (JSContext *cx, uintN argc, jsval *vp)
{
  jsval *argv = JS_ARGV (cx, vp);
  se_log_fncall ("set_video_size");
  se_argc_check (1, "set_video_size");
  se_arg_is_int (0, "set_video_size");

  int32 zoom;
  JS_ValueToInt32 (cx, argv[0], &zoom);

  if (zoom < GXINE_VO_ZOOM_MIN && zoom != -1)
    zoom = GXINE_VO_ZOOM_MIN;
  else if (zoom > XINE_VO_ZOOM_MAX)
    zoom = XINE_VO_ZOOM_MAX;

  zoomcb = zoom;
  if (zoomcb_id)
    g_source_remove (zoomcb_id);
  zoomcb_id = g_idle_add ((GSourceFunc) set_video_size_cb, NULL);

  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

#ifdef WITH_DEPRECATED

static JSBool js_set_fullscreen (JSContext *cx, uintN argc, jsval *vp)
{
  int32 fs;
  jsval *argv = JS_ARGV (cx, vp);

  se_log_fncall_deprecated ("set_fullscreen");
  se_argc_check_max (1, "set_fullscreen");

  if (argc == 1)
  {
    se_arg_is_int_or_bool (0, "set_fullscreen");
    JS_ValueToInt32 (cx, argv[0], &fs);
  }
  else
    fs = !gtk_video_is_fullscreen ((GtkVideo *)gtv);

  if (gtk_toggle_action_get_active (action_items.fullscreen) != fs)
    gtk_action_activate (GTK_ACTION (action_items.fullscreen));

  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

#ifdef WITH_OBSOLETE

static JSBool js_set_deinterlace (JSContext *cx, uintN argc, jsval *vp)
{
  int32 di;
  jsval *argv = JS_ARGV (cx, vp);

  se_log_fncall_obsolete ("set_deinterlace");
  se_argc_check_max (1, "set_deinterlace");

  if (argc == 1)
  {
    se_arg_is_int_or_bool (0, "set_deinterlace");
    JS_ValueToInt32 (cx, argv[0], &di);
  }
  else
    di = !gtk_video_get_use_post_plugins_deinterlace ((GtkVideo *)gtv);

  set_deinterlace (di);
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool js_set_postproc_video (JSContext *cx, uintN argc, jsval *vp)
{
  se_t *se = (se_t *) JS_GetContextPrivate(cx);
  int32 di;
  jsval *argv = JS_ARGV (cx, vp);

  se_log_fncall_obsolete ("set_postproc_video");
  se_argc_check_max (1, "set_postproc_video");

  if (argc == 1)
  {
    if (JSVAL_IS_INT (argv[0]))
      JS_ValueToInt32 (cx, argv[0], &di);
    else if (JSVAL_IS_STRING (argv[0]))
      return set_vo_postproc_chain (cx, argc, vp);
    else
    {
      se->print_cb (se->print_cb_data,
		   _("error: %s() argument %d is neither int nor string\n"),
		   "set_postproc_video", 1);
      JS_SET_RVAL (cx, vp, JSVAL_VOID);
      return JS_TRUE;
    }
  } else
    di = !gtk_video_get_use_post_plugins_video ((GtkVideo *)gtv);

  set_vo_postproc (di);
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool js_set_postproc_audio (JSContext *cx, uintN argc, jsval *vp)
{
  se_t *se = (se_t *) JS_GetContextPrivate(cx);
  int32 di;
  jsval *argv = JS_ARGV (cx, vp);

  se_log_fncall_obsolete ("set_postproc_audio");
  se_argc_check_max (1, "set_postproc_audio");

  if (argc == 1)
  {
    if (JSVAL_IS_INT (argv[0]))
      JS_ValueToInt32 (cx, argv[0], &di);
    else if (JSVAL_IS_STRING (argv[0]))
      return set_ao_postproc_chain (cx, argc, vp);
    else
    {
      se->print_cb (se->print_cb_data,
		   _("error: %s() argument %d is neither int nor string\n"),
		   "set_postproc_audio", 1);
      JS_SET_RVAL (cx, vp, JSVAL_VOID);
      return JS_TRUE;
    }
  } else
    di = !gtk_video_get_use_post_plugins_audio ((GtkVideo *)gtv);

  set_ao_postproc (di);
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool js_set_auto_resize (JSContext *cx, uintN argc, jsval *vp)
{
  int32 ar;
  jsval *argv = JS_ARGV (cx, vp);

  se_log_fncall_obsolete ("set_auto_resize");
  se_argc_check_max (1, "set_auto_resize");

  if (argc == 1)
  {
    se_arg_is_int_or_bool (0, "set_auto_resize");
    JS_ValueToInt32 (cx, argv[0], &ar);
  }
  else
    ar = !gtk_video_get_auto_resize ((GtkVideo *)gtv);

  set_auto_resize (ar);
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool js_set_auto_rescale (JSContext *cx, uintN argc, jsval *vp)
{
  int32 ar;
  jsval *argv = JS_ARGV (cx, vp);

  se_log_fncall_obsolete ("set_auto_rescale");
  se_argc_check_max (1, "set_auto_rescale");

  if (argc == 1)
  {
    se_arg_is_int_or_bool (0, "set_auto_rescale");
    JS_ValueToInt32 (cx, argv[0], &ar);
  }
  else
    ar = !gtk_video_get_auto_rescale ((GtkVideo *)gtv);

  set_auto_rescale (ar);
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool js_set_aspect (JSContext *cx, uintN argc, jsval *vp)
{
  int32 aspect;
  jsval *argv = JS_ARGV (cx, vp);

  se_log_fncall_obsolete ("set_aspect");
  se_argc_check_max (1, "set_aspect");

  if (argc==1)
  {
    se_arg_is_int (0, "set_aspect");
    JS_ValueToInt32 (cx, argv[0], &aspect);
  }
  else
    aspect = xine_get_param (stream, XINE_PARAM_VO_ASPECT_RATIO) + 1;

  set_aspect (aspect);
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

#endif /* WITH_OBSOLETE */
#endif /* WITH_DEPRECATED */

static JSBool js_snapshot (JSContext *cx, uintN argc, jsval *vp)
{
  JSString *str;
  char     *fname = NULL;
  int32	    scale = -1, blend = -1;
  jsval    *argv = JS_ARGV (cx, vp);

  se_log_fncall_checkinit ("snapshot");
  se_argc_check_max (3, "snapshot");

  switch (argc) /* note fall-through */
  {
  case 3:
    se_arg_is_int (2, "snapshot");
    JS_ValueToInt32 (cx, argv[2], &blend);
  case 2:
    se_arg_is_int (1, "snapshot");
    JS_ValueToInt32 (cx, argv[1], &scale);
  case 1:
    se_arg_is_string (0, "snapshot");
    str = JS_ValueToString (cx, argv[0]);
    fname = SE_JS_ENCODE_STRING (cx, str);
  }

  make_snapshot (fname, scale, blend);

  SE_JS_FREE_ENCODED_STRING (cx, fname);
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}


static JSBool controls_play (JSContext *cx, uintN argc, jsval *vp)
{
  jsval *argv = JS_ARGV (cx, vp);
  se_log_fncall_checkinit ("play");
  se_argc_check_max (3, "play");

  char *mrl = NULL;
  int32 pos = -1, pos_time = -1;

  switch (argc)
  {
  case 0:
    break;

  case 1:
    {
      JSString *str;
      se_arg_is_string (0, "play");

      str = JS_ValueToString (cx, argv[0]);

      mrl = SE_JS_ENCODE_STRING (cx, str);
      se_log ("playing '%s' from start\n", mrl);
    }
    break;

  case 2:
    {
      se_arg_is_number (0, "play");
      se_arg_is_int (1, "play");

      jsdouble posv;
      JS_ValueToNumber (cx, argv[0], &posv);
      pos = (posv >= 0 && posv < 100) ? posv * 655.35 : -1;

      JS_ValueToInt32 (cx, argv[1], &pos_time);
      mrl = NULL;
      se_log ("playing from %d, %d\n", pos, pos_time);
    }
    break;

  case 3:
    {
      se_arg_is_string (0, "play");
      se_arg_is_number (1, "play");
      se_arg_is_int (2, "play");

      JSString *str = JS_ValueToString (cx, argv[0]);

      jsdouble posv;
      JS_ValueToNumber (cx, argv[1], &posv);
      pos = (posv >= 0 && posv < 100) ? posv * 655.35 : -1;

      JS_ValueToInt32 (cx, argv[2], &pos_time);
      mrl = SE_JS_ENCODE_STRING (cx, str);
      se_log ("playing '%s' from %d, %d\n", mrl, pos, pos_time);
    }
    break;
  }

  if (mrl)
  {
    playlist_play_from (playlist_add_mrl (mrl, -1), pos, pos_time);
    SE_JS_FREE_ENCODED_STRING (cx, mrl);
  }
  else
  {
    play_item_t *item = playlist_get_current_item ();

    if (item)
    {
      switch (playlist_showing_logo () ? XINE_STATUS_IDLE : xine_get_status (stream))
      {
      case XINE_STATUS_STOP:
      case XINE_STATUS_IDLE:
	playlist_play_from (playlist_get_list_pos (), pos,
			   pos_time < 0 ? item->start_time : pos_time);
	break;
      default:
	if (pos >= 0 || pos_time >= 0)
	{
	  gtk_video_unblank_screen ((GtkVideo *)gtv);
	  xine_play (stream, pos < 0 ? 0 : pos, pos_time < 0 ? 0 : pos_time);
	}
	break;
      }
      play_item_dispose (item);
    } else {
      logprintf (_("script_engine: error: no valid play item available\n"));
    }
  }
  set_speed (XINE_SPEED_NORMAL);

  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

/* Crude hack to allow combined play/pause function.
 * (Useful on some keyboards with CD/DVD control keys.)
 */
static JSBool controls_play_pause (JSContext *cx, uintN argc, jsval *vp)
{
  jsval *argv = JS_ARGV (cx, vp);
  se_log_fncall_checkinit ("play_pause");
  se_argc_check_max (1, "play_pause");

  if (argc == 1)
    se_arg_is_int_or_bool (0, "play_pause");

  switch (xine_get_status (stream))
  {
  case XINE_STATUS_STOP:
  case XINE_STATUS_IDLE:
    return controls_play (cx, argc, vp);
  default:
    return controls_pause (cx, argc, vp);
  }
}

static JSBool js_is_live_stream (JSContext *cx, uintN argc, jsval *vp)
{
  JS_SET_RVAL(cx, vp, JSVAL_FALSE);

  se_log_fncall ("is_live_stream");
  se_argc_check_max (0, "is_live_stream");

  if (player_live_stream ())
    JS_SET_RVAL (cx, vp, JSVAL_TRUE);

  return JS_TRUE;
}

int player_live_stream (void)
{
  gint pos_stream, pos_time, length_time;

  return (xine_get_status (stream) == XINE_STATUS_PLAY) &&
	 (xine_get_pos_length (stream, &pos_stream, &pos_time, &length_time)) &&
	 length_time == 0 &&
	 !xine_get_stream_info (stream, XINE_STREAM_INFO_SEEKABLE);
}

const char *player_get_cur_title (void)
{
  return cur_title;
}

const char *player_get_cur_mrl (void)
{
  return cur_mrl;
}


static xine_audio_port_t *load_audio_out_driver (const char *audio_driver_id)
{
  xine_audio_port_t      *audio_port;

  const char * const * choices = xine_list_audio_output_plugins (xine);
  /* try to init audio with stored information */
  int i = xine_config_register_enum (xine,
                                     "audio.driver", 0, (char **)choices,
				     _("audio driver to use"),
				     NULL, 10, GXINE_TRUE, NULL);
  if (!audio_driver_id)
    audio_driver_id = choices[i];

  if (!strcmp (audio_driver_id, "null"))
    return NULL;

  if (strcmp (audio_driver_id, "auto"))
  {
    audio_port = xine_open_audio_driver (xine, audio_driver_id, NULL);
    if (audio_port)
      return audio_port;

    g_printerr (_("audio driver %s failed\n"), audio_driver_id);
  }

  /* autoprobe */
  audio_port = xine_open_audio_driver (xine, NULL, NULL);
  if (audio_port)
    return audio_port;

  g_printerr (_("audio driver %s failed\n"), "auto");
  return xine_open_audio_driver (xine, "none", NULL);
}

/* Properties */

static void add_chain_funcs (const char *prop, const char *config,
			     JSBool (*set_chain) (JSContext *, uintN, jsval *))
{
  se_o_t *obj = se_find_object (gse, NULL, prop);
  if (set_chain)
    se_defun (gse, obj, "set_chain", set_chain, 0, JSFUN_FAST_NATIVE, SE_GROUP_HIDDEN, NULL, NULL);
  se_prop_create_xine_id (gse, obj, "chain", config);
  se_prop_add_listener (gse, obj, "chain",
			listen_postproc_chain, (void *)config);
}

void player_init (const char *audio_driver_id)
{
/*
  se_o_t            *controls_obj;
 */

  cur_mrl = NULL;

  /* load drivers, create xine stream */

  audio_port = load_audio_out_driver (audio_driver_id);
  if (!audio_port)
  {
    display_error_modal (FROM_GXINE, _("Fatal error"),
			 _("No audio output driver could be loaded."));
    exit (2);
  }

  video_port = xine_open_video_driver (xine, "none", 0, NULL);

  stream = xine_stream_new (xine, audio_port, video_port);
#ifdef XINE_PARAM_EARLY_FINISHED_EVENT
  xine_set_param (stream, XINE_PARAM_EARLY_FINISHED_EVENT, 1);
#endif

  /*
   * set up script engine objects and functions
   */

/*
  controls_obj = se_create_object (gse, NULL, "controls", NULL);

  se_defun (gse, controls_obj, "play", controls_play, 0, 0,
	    SE_GROUP_HIDDEN, NULL, NULL);
 */

  {
    static const ui_property_t int_props[] = {
      /* Text is help for the commands. Format is:
       *   To the left of the semicolon, a comma-separated list of
       *     VARIABLE[=TYPE] or METHOD([PARAMETER TYPE,...])
       *   To the right, descriptive text
       */
      { "av_speed", N_("v=int, min, max; playback speed"),
       UI_CUSTOM (get_speed, set_speed),
       XINE_SPEED_PAUSE,
       XINE_SPEED_FAST_4,
       FALSE,
       { listen_speed } },
      { "vo_aspect", N_("v=int; aspect ratio"), UI_XINE_PARAM (XINE_PARAM_VO_ASPECT_RATIO, set_aspect), 0, XINE_VO_ASPECT_NUM_RATIOS - 1, TRUE },
      { "vo_subtitle", N_("v=int, min, max; subtitle channel"), UI_XINE_PARAM (XINE_PARAM_SPU_CHANNEL, set_subtitles), -2, GXINE_MAX_SPU_CHANNEL },
      { "vo_zoom", N_("v=int, min, max; video zoom (within window)"), UI_XINE_PARAM (XINE_PARAM_VO_ZOOM_X, set_zoom), XINE_VO_ZOOM_MIN, XINE_VO_ZOOM_MAX },
      { NULL }
    };
    static const ui_property_t bool_props[] = {
      { "ao_mute", N_("v=bool, toggle(): audio mute"), UI_XINE_PARAM (XINE_PARAM_AUDIO_MUTE, set_mute) },
      { "vo_auto_resize", N_("v=bool, toggle(): auto-resize video on frame shape change"), UI_GTK_VIDEO (auto_resize, gtk_video_get_auto_resize) },
      { "vo_auto_rescale", N_("v=bool, toggle(): double size of low-resolution video"), UI_GTK_VIDEO (auto_rescale, gtk_video_get_auto_rescale) },
      { "vo_fullscreen", N_("v=bool, toggle(): full-screen mode"), UI_GTK_VIDEO (fullscreen, gtk_video_is_fullscreen) },
      { "vo_deinterlace", N_("v=bool, toggle(): enable deinterlacing"), UI_GTK_VIDEO (deinterlace, gtk_video_get_use_post_plugins_deinterlace) },
      { "ao_postproc", N_("v=bool, toggle(): audio post-processing; chain=string: plugin chain"), UI_GTK_VIDEO (ao_postproc, gtk_video_get_use_post_plugins_audio) },
      { "vo_postproc", N_("v=bool, toggle(): video post-processing; chain=string: plugin chain"), UI_GTK_VIDEO (vo_postproc, gtk_video_get_use_post_plugins_video) },
      { "wm_unblank", N_("v=bool, toggle(): unblanking in windowed mode"), UI_GTK_VIDEO (wm_unblank, gtk_video_get_windowed_unblank) },
      { NULL }
    };
    static const ui_property_t zoom_subprops[] = {
      { "x", N_("v=int, min, max; video zoom (within window)"), UI_XINE_PARAM (XINE_PARAM_VO_ZOOM_X, NULL), XINE_VO_ZOOM_MIN, XINE_VO_ZOOM_MAX },
      { "y", N_("v=int, min, max; video zoom (within window)"), UI_XINE_PARAM (XINE_PARAM_VO_ZOOM_Y, NULL), XINE_VO_ZOOM_MIN, XINE_VO_ZOOM_MAX },
      { NULL }
    };
    static const ui_property_t fs_subprops[] = {
      { "sticky", NULL, UI_BOOL (fs_sticky, fs_always_sticky, NULL) },
	/* ↑ deprecated; it's really just gui.fullscreen_mode.always_sticky */
      { "is_sticky", N_("v=bool, toggle(); forced stickiness in full-screen mode"), UI_BOOL (fs_sticky, fs_is_sticky, window_fs_stickiness_update) },
      { NULL }
    };

    ui_create_properties (int_props, NULL, SE_TYPE_INT);
    ui_create_properties (bool_props, NULL, SE_TYPE_BOOL);
    ui_create_properties (zoom_subprops, se_find_object (gse, NULL, "vo_zoom"),
			  SE_TYPE_INT);
    ui_create_properties (fs_subprops, se_find_object (gse, NULL, "vo_fullscreen"),
			  SE_TYPE_BOOL);
  }

  /* a few extras - chain definition should be done via the GUI */
  add_chain_funcs ("ao_postproc", "gui.post_plugins.audio", set_ao_postproc_chain);
  add_chain_funcs ("vo_postproc", "gui.post_plugins.video", set_vo_postproc_chain);
  add_chain_funcs ("vo_deinterlace", "gui.post_plugins.deinterlace", set_deint_postproc_chain);

  /*
   * hook up global xine functions for convenience
   */

  {
    static const se_f_def_t defs[] = {
      { "play", controls_play, 0, JSFUN_FAST_NATIVE,
	/* help text describes function parameters */
	SE_GROUP_ENGINE, N_("[mrl] [, pos, time]"),
	N_("time in milliseconds") },
      { "get_time", get_time, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_ENGINE, NULL, N_("stream time index") },
      { "get_pos", get_pos, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_ENGINE, NULL, N_("stream position (%)") },
      { "has_time", has_time, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_ENGINE, NULL, N_("whether the stream has a time index") },
      { "pause", controls_pause, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_ENGINE, N_("[bool]"), NULL },
      { "stop", controls_stop, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_ENGINE, NULL, NULL },
      { "eject", controls_eject, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_ENGINE, NULL, NULL },
      { "play_pause", controls_play_pause, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_ENGINE, N_("[bool]"), NULL },
      { "snapshot", js_snapshot, 0, JSFUN_FAST_NATIVE,
	/* these correspond to the options in the snapshot-save dialogue box */
	SE_GROUP_ENGINE, N_("[file name [, scale, blend]]"), NULL },

      { "set_video_size", js_set_video_size, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_PROPERTIES, N_("int"), NULL },

#ifdef WITH_DEPRECATED
#ifdef WITH_OBSOLETE
      { "set_speed", js_set_speed, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
      { "get_speed", js_get_speed, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
      { "set_volume", js_set_volume, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
      { "get_volume", js_get_volume, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
      { "set_mute", js_set_mute, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
      { "get_mute", js_get_mute, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
      { "set_zoom", js_set_zoom, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
      { "get_zoom", js_get_zoom, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
#endif /* WITH_OBSOLETE */
      { "set_fullscreen", js_set_fullscreen, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
#ifdef WITH_OBSOLETE
      { "set_aspect", js_set_aspect, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
      { "set_deinterlace", js_set_deinterlace, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
      { "set_postproc_video", js_set_postproc_video, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
      { "set_postproc_audio", js_set_postproc_audio, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
      { "set_auto_resize", js_set_auto_resize, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
      { "set_auto_rescale", js_set_auto_rescale, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_HIDDEN, NULL, NULL },
#endif /* WITH_OBSOLETE */
#endif /* WITH_DEPRECATED */

      { "is_live_stream", js_is_live_stream, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_PROPERTIES, NULL, NULL },
      { NULL }
    };
    se_defuns (gse, NULL, defs);
  }

  {
    static const char *const window_size_labels[] = {
      N_("50%"), N_("75%"), N_("100%"), N_("150%"), N_("200%"),
      NULL
    };
    xine_config_register_enum
      (xine, "gui.window_size", 0, (char **)window_size_labels,
       _("The main window's default size"),
       NULL, 0, NULL, NULL);
    xine_config_register_bool
      (xine, "gui.windowed_mode.magnify_lowres_video", CONFIG_VERSION_ATLEAST (0, 6, 0),
       _("Double the size of small video streams"),
       _("This affects video streams whose dimensions are at most ⅓ "
	 "of those of the display."),
       0, GXINE_TRUE, NULL);
    config_update_default ("gui.windowed_mode.magnify_lowres_video", 1);
    xine_config_register_bool
      (xine, "gui.windowed_mode.auto_resize", 1,
       _("Automatic video window resizing"),
       _("Whether the window is automatically resized on a stream format change."),
       0, NULL, NULL);
    xine_config_register_bool
      (xine, "gui.subtitle_autoload", 1,
       _("Subtitle autoloading"),
       _("Automatically load subtitles if they exist."),
       0, GXINE_TRUE, NULL);
  }
}
