/*
 * Copyright (C) 2003-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
 *
 * init xine engine, set up script engine
 */

#include "globals.h"
#include "version.h"

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

#include <glib.h>

#include "engine.h"
#include "ui.h"
#include "utils.h"

se_t *gse; /* global script engine */
xine_t *xine; /* global xine instance */
GRecMutex engine_lock;

gboolean initialised = FALSE;

int __lock_depth = -1;

static GtkWidget *se_startup_window, *se_startup_textarea;
static GtkTextBuffer *se_startup_buf;
static GtkTextTag *se_startup_tag;
static gint se_startup_insert;

static char *se_startup_cmds = NULL;
static gboolean modified = FALSE;

GAsyncQueue *js_queue;
static gboolean queue_available = FALSE;

static JSBool
show_about (JSContext *cx, uintN argc, jsval *vp)
{
  static const gchar *const authors[] = {
    "Darren Salt <dsalt@users.sourceforge.net>",
    "Guenter Bartsch <guenter@users.sourceforge.net>",
    "Petri Hintukainen <phintuka@users.sourceforge.net>",
    NULL
  };
  static const gchar *const documenters[] = {
    "Darren Salt <dsalt@users.sourceforge.net>",
    "Craig Sanders <cas@taz.net.au>",
    "Philipp Matthias Hahn <pmhahn@titan.lahn.de>",
    NULL
  };
  static GtkWidget *about = NULL;

  se_log_fncall_checkinit ("show_about");

  if (!about)
  {
    GtkWidget *label;
    GtkBox *vbox;
    /* translators' names go here */
    const gchar *translator = _("TRANSLATOR");
    gchar *licence =
	_("This program is free software; you can redistribute it and/or "
	  "modify it under the terms of the GNU General Public Licence as "
	  "published by the Free Software Foundation; either version 2 of the "
	  "Licence, or (at your option) any later version.\n"
	  "\n"
	  "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 Licence for more details.\n"
	  "\n"
	  "You should have received a copy of the GNU General Public Licence "
	  "along with this program; if not, write to the Free Software "
	  "Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, "
	  "MA  02110-1301  USA");

    about = gtk_about_dialog_new ();
    g_object_set (G_OBJECT (about),
		  "name", "gxine",
		  "version", VERSION,
		  "copyright", get_copyright_notice (),
		  "comments", _("A GTK media player front end for xine-lib"),
		  "license", licence,
		  "wrap-license", TRUE,
		  "website", "http://www.xine-project.org/",
		  "authors", authors,
		  "documenters", documenters,
		  "translator-credits",
		  strcmp (translator, "TRANSLATOR") ? translator : NULL,
		  "logo-icon-name", GXINE_LOGO, NULL);

    gchar *version_label =
      g_strdup_printf (_("<small><i>%s\nUsing xine-lib %s, built with %s</i></small>"),
		       VENDOR_PKG_VERSION CSET_ID,
		       xine_get_version_string (), XINE_VERSION);
    label = ui_label_new_with_markup (version_label);
    gtk_label_set_justify ((GtkLabel *) label, GTK_JUSTIFY_CENTER);
    g_free (version_label);
    vbox = GTK_BOX (gtk_dialog_get_content_area(GTK_DIALOG (about)));
    gtk_box_pack_end (vbox, label, FALSE, FALSE, 2);
    g_object_connect (about,
	"signal::delete-event", G_CALLBACK (gtk_widget_hide_on_delete), NULL,
	"signal::response", G_CALLBACK (gtk_widget_hide), NULL,
	NULL);
  }

  window_show (about, NULL);
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static void __attribute__ ((format (printf, 2, 3)))
print_cb (void *user_data, const char *str, ...)
{
  if (verbosity)
  {
    va_list ap;
    /* JS diagnostic output */
    g_print (_("engine: < "));
    va_start (ap, str);
    vprintf (str, ap);
    va_end (ap);
    puts ("");
  }
}

int engine_exec_obj (const char *cmd, se_o_t *obj, se_print_cb_t cb,
		     void *cb_data, se_error_cb_t ecb, const char *src)
{
  struct {
    JSInt32 i;
    JSFloat64 d;
    JSBool b;
  } num;
  char *str;

#ifdef LOG
  /* this is probably harmless */
  if (!queue_available)
    g_warning ("JS execution during startup: %s: %s", src, cmd);
#endif

  if (!g_rec_mutex_trylock (&engine_lock))
  {
    g_critical (_("JS execution attempted while lock already held.\n"
		  "Try to reproduce this in a debugger (set a breakpoint on g_log).\n"
		  "Get a backtrace of all gxine threads and report it!"));
    engine_queue_push (cmd, NULL, cb, cb_data, ecb, src);
    return 1;
  }

  if (verbosity)
    g_print (_("engine: > executing ‘%s’...\n"), cmd);

  if (!cb)
  {
    cb = print_cb;
    cb_data = NULL;
  }

  se_eval_ext (gse, cmd, obj, cb, cb_data, ecb, src);

  if ((str = se_result_str (gse)))
  {
    /* JS output. Could end up in a terminal or the JS console. */
    char *free_str = str;
    str = g_strdup_printf (_("result: %s"), str);
    cb (cb_data, "%s", str);
    free (free_str);
    g_free (str);
  }
  else if (se_result_double (gse, &num.d))
  {
    char str[32];
    snprintf (str, sizeof (str), _("result: %lf"), num.d);
    cb (cb_data, "%s", str);
  }
  else if (se_result_int (gse, &num.i))
  {
    char str[32];
    snprintf (str, sizeof (str), _("result: %d"), num.i);
    cb (cb_data, "%s", str);
  }
  else if (se_result_bool (gse, &num.b))
  {
    char str[32];
    snprintf (str, sizeof (str), _("result: %s"),
	      num.b == JS_TRUE ? _("true") : _("false"));
    cb (cb_data, "%s", str);
  }
  else if (JSVAL_IS_NULL (gse->rval))
  {
    cb (cb_data, _("null"));
  }
  else
  {
    /* return 0 iff the result type wasn't recognised */
    g_rec_mutex_unlock (&engine_lock);
    return 0;
  }

  g_rec_mutex_unlock (&engine_lock);
  return 1;
}

int engine_exec (const char *cmd, se_print_cb_t cb, void *cb_data,
		 const char *src)
{
  return engine_exec_obj (cmd, NULL, cb, cb_data, se_error_cb, src);
}

int v_engine_exec(const char *command, se_print_cb_t cb, void *cb_data,
		  const char *src, ...)
{
  char *buf;
  va_list va;
  int ret;

  va_start (va, src);
  buf = g_strdup_vprintf (command, va);
  va_end (va);
  ret = engine_exec (buf, cb, cb_data, src);
  free (buf);
  return ret;
}

int v_engine_exec_obj (const char *command, se_o_t *obj, se_print_cb_t cb,
		       void *cb_data, se_error_cb_t ecb, const char *src, ...)
{
  char *buf;
  va_list va;
  int ret;

  va_start (va, src);
  buf = g_strdup_vprintf (command, va);
  va_end (va);
  ret = engine_exec_obj (buf, obj, cb, cb_data, ecb, src);
  free (buf);
  return ret;
}

typedef struct {
  char *cmd;
  se_o_t *obj;
  se_print_cb_t cb;
  void *cb_data;
  se_error_cb_t ecb;
  /*const*/ char *src;
} exec_t;

static void js_queue_cb (void)
{
  /* js_lock must be recursive since, if the engine is busy, this function
   * WILL be re-entered from the same thread with the lock already held.
   */
  static GRecMutex js_lock;
  exec_t *js;

  g_rec_mutex_lock (&js_lock);
  while ((js = g_async_queue_try_pop (js_queue)))
  {
    /* This function is entered with the GDK lock held. This is bad, from
     * gxine's POV since it needs to sync on widgets_update_lock (play_exec
     * holds onto it for a while), and play_exec claims that *then* the GDK
     * lock, which it needs to release from time to time. So we have to
     * release the GDK lock for a short time.
     * This _really_ needs a better solution...
     */
    gdk_threads_leave ();
    pthread_mutex_lock (&widgets_update_lock);
    pthread_mutex_unlock (&widgets_update_lock); /* JS 'play()' needs it */
    gdk_threads_enter ();
    engine_exec_obj (js->cmd, js->obj, js->cb, js->cb_data, js->ecb, js->src);
    free (js->cmd);
    free (js->src);
    free (js);
  }
  g_rec_mutex_unlock (&js_lock);
}

#if !GTK_CHECK_VERSION(3, 0, 0)
static gboolean client_event_cb (GtkWidget *widget, GdkEventClient *event,
                                 gpointer data)
{
  js_queue_cb();
  return TRUE;
}
#endif

#if GTK_CHECK_VERSION(3, 0, 0)
static gboolean js_queue_cb_0 (gpointer data)
{
  js_queue_cb();
  return FALSE;
}
#endif

void engine_queue_push (const char *command, se_o_t *obj, se_print_cb_t cb,
                        void *cb_data, se_error_cb_t ecb, const char *src)
{
  exec_t *data = malloc (sizeof (exec_t));
  *data = (exec_t){ g_strdup (command), obj, cb, cb_data, ecb, g_strdup (src) };
  g_async_queue_push (js_queue, data);

#if !GTK_CHECK_VERSION(3, 0, 0)
  GdkEvent event;
  event.client.type = GDK_CLIENT_EVENT;
  event.client.window = gtk_widget_get_window (app);
  event.client.send_event = TRUE;
  event.client.message_type = GDK_NONE;
  event.client.data_format = 8;
  strcpy (event.client.data.b, "Javascript");
  gdk_event_put (&event);
#else
  gdk_threads_add_idle (js_queue_cb_0, NULL);
#endif
}

static char *
load_user_startup_script (void)
{
  char *file = get_config_filename (FILE_STARTUP);
  char *text = read_entire_file (file, NULL);
  if (!text)
  {
    text = strdup ("\n");
    if (errno != ENOENT)
      display_error (FROM_GXINE, _("Couldn't load startup script"), "%s: %s",
		     file, strerror (errno));
  }
  free (file);

  g_strstrip (text); /* strips \n added by read_entire_file() */
  return strcat (text, "\n"); /* ... so re-add it */
}

static void
reset_startup_buffer (const char *text)
{
  GtkTextIter start, end;
  gtk_text_buffer_get_iter_at_offset (se_startup_buf, &start,
				      se_startup_insert);
  gtk_text_buffer_get_end_iter (se_startup_buf, &end);
  gtk_text_buffer_delete (se_startup_buf, &start, &end);
  gtk_text_buffer_get_iter_at_offset (se_startup_buf, &start,
				      se_startup_insert);
  gtk_text_buffer_insert (se_startup_buf, &start, text, -1);
  gtk_text_buffer_set_modified (se_startup_buf, FALSE);
}

static void
se_startup_tag_cb (GtkWidget *widget, GtkStyle *oldstyle, gpointer data)
{
  if (se_startup_tag) {
    GtkStyle *style = gtk_widget_get_style(widget);
    g_object_set (se_startup_tag, "foreground-gdk",
		  &style->text[GTK_STATE_INSENSITIVE], NULL);
  }
}

static void
se_startup_insert_text_cb (GtkTextBuffer *widget, const GtkTextIter *iter,
			   const gchar *text, gint length, gpointer data)
{
  if (gtk_text_iter_get_offset (iter) < se_startup_insert)
    g_signal_stop_emission_by_name (widget, "insert-text");
}

static void
se_startup_response_cb (GtkWidget *widget, int response, gpointer data)
{
  GtkTextIter start, end;

  switch (response)
  {
  case 1:
    {
      char *file = get_config_filename (FILE_STARTUP);
      char *tmp = read_entire_file (file, NULL);
      if (!tmp && errno != ENOENT)
	display_error (FROM_GXINE, _("Couldn't load startup script"), "%s: %s",
		       file, strerror (errno));
      else
	modified = FALSE;
      free (file);
      reset_startup_buffer (tmp ? : "");
      free (tmp);
    }
    gtk_widget_grab_focus (se_startup_textarea);
    break;
  case GTK_RESPONSE_REJECT:
    reset_startup_buffer (se_startup_cmds);
    gtk_widget_grab_focus (se_startup_textarea);
    break;
  case GTK_RESPONSE_OK:
    free (se_startup_cmds);
    gtk_text_buffer_get_iter_at_offset (se_startup_buf, &start,
					se_startup_insert);
    gtk_text_buffer_get_end_iter (se_startup_buf, &end);
    se_startup_cmds = gtk_text_buffer_get_text (se_startup_buf, &start, &end,
						FALSE);
    g_strstrip (se_startup_cmds);
    modified |= gtk_text_buffer_get_modified (se_startup_buf);
  default:
    gtk_widget_hide (se_startup_window);
  }
}

static JSBool
show_startup (JSContext *cx, uintN argc, jsval *vp)
{
  if (!gtk_widget_get_visible (se_startup_window))
    reset_startup_buffer (se_startup_cmds);
  window_show (se_startup_window, NULL);
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

void engine_init (void)
{
  char *cfgfilename;
  GtkWidget *w;

  /*
   * init the global xine engine
   */

  xine = xine_new ();

  cfgfilename = get_config_filename (FILE_CONFIG);
  have_config = !access (cfgfilename, R_OK); /* upgrade or install? */
  xine_config_load (xine, cfgfilename);
  free (cfgfilename);
  xine_engine_set_param (xine, XINE_ENGINE_PARAM_VERBOSITY, verbosity);

  xine_init (xine);
  ui_preferences_register (xine);

  /* init the javascript engine */

  gse = se_new ();

  static const se_f_def_t defs[] = {
    { "about_show", show_about, 0, JSFUN_FAST_NATIVE, SE_GROUP_DIALOGUE, NULL, NULL },
    { "startup_cmds_show", show_startup, 0, JSFUN_FAST_NATIVE, SE_GROUP_DIALOGUE, NULL, NULL },
    { NULL }
  };
  se_defuns (gse, gse->g, defs);

  /*
   * create the startup commands editor window
   */

  se_startup_window = gtk_dialog_new_with_buttons (_("Startup script"), NULL, 0,
				GTK_STOCK_REVERT_TO_SAVED, 1,
				GTK_STOCK_UNDO, GTK_RESPONSE_REJECT,
				GTK_STOCK_CANCEL, GTK_RESPONSE_DELETE_EVENT,
				GTK_STOCK_OK, GTK_RESPONSE_OK,
				NULL);
  gtk_window_set_default_size (GTK_WINDOW (se_startup_window), 400, 250);
  gtk_dialog_set_default_response (GTK_DIALOG (se_startup_window),
				   GTK_RESPONSE_OK);
  ui_add_undo_response (se_startup_window, NULL);
  g_object_connect (se_startup_window,
	"signal::delete-event", G_CALLBACK (gtk_widget_hide_on_delete), NULL,
	"signal::response", G_CALLBACK (se_startup_response_cb), NULL,
	NULL);
  w = gtk_scrolled_window_new (NULL, NULL);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (w),
				  GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
  se_startup_buf = gtk_text_buffer_new (NULL);
  se_startup_textarea = gtk_text_view_new_with_buffer (se_startup_buf);
  gtk_widget_set_name (se_startup_textarea, "gxine_js_edit");
  gtk_container_add (GTK_CONTAINER (w), se_startup_textarea);
  gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (se_startup_window))),
                      w, TRUE, TRUE, 0);
  g_signal_connect (se_startup_textarea, "style-set",
		    G_CALLBACK (se_startup_tag_cb), NULL);
  g_signal_connect (se_startup_buf, "insert-text",
		    G_CALLBACK (se_startup_insert_text_cb), NULL);

  js_queue = g_async_queue_new ();
  g_async_queue_ref (js_queue);
}

void
engine_startup (void)
{
  /* note: order of actions is significant */
  queue_available = TRUE;
#if !GTK_CHECK_VERSION(3, 0, 0)
  g_signal_connect (G_OBJECT (app), "client-event",
                    G_CALLBACK (client_event_cb), NULL);
#endif
  js_queue_cb ();
}

void
engine_startup_script (void)
{
  gchar *file, *name = g_build_filename (confdir, "startup", NULL);
  GtkTextIter pos;

  file = read_entire_file (name, NULL);
  if (!file && errno != ENOENT)
    display_error (FROM_GXINE, _("Couldn't load startup script"), "%s: %s",
		   name, strerror (errno));
  else if (file)
  {
    GtkStyle *style = gtk_widget_get_style (se_startup_textarea);
    se_eval_ext (gse, file, NULL, NULL, NULL, se_error_cb,
		 _("Startup script (system)"));
    se_startup_tag = gtk_text_buffer_create_tag (se_startup_buf, "global",
		"editable", FALSE, "foreground-gdk",
                &style->text[GTK_STATE_INSENSITIVE],
		"scale", 0.9, NULL);
    gtk_text_buffer_get_start_iter (se_startup_buf, &pos);
    gtk_text_buffer_insert_with_tags (se_startup_buf, &pos, file, -1,
				      se_startup_tag, NULL);
  }
  free (name);
  free (file);
  gtk_text_buffer_get_end_iter (se_startup_buf, &pos);
  se_startup_insert = gtk_text_buffer_get_char_count (se_startup_buf);

  se_startup_cmds = load_user_startup_script ();
  se_eval (gse, se_startup_cmds, NULL, NULL, NULL, _("Startup script"));
}

void
save_startup_script (void)
{
  /* only save if the buffer is modified */
  if (!modified)
    return;

  char *fname = get_config_filename (FILE_STARTUP);
  FILE *f = open_write (fname, _("Failed to save startup script"));

  if (f)
  {
    fputs (se_startup_cmds, f);
    close_write (fname, f, _("Failed to save startup script"));
  }

  free (fname);
}

#ifdef LOCK_DEBUG
#include <assert.h>

static __thread int m_depth = 0, g_depth = 0;
static pthread_mutex_t guard = PTHREAD_MUTEX_INITIALIZER;

#include <sys/syscall.h>
#include <unistd.h>
#define gettid() (int) syscall (__NR_gettid)

int gxine_mutex_lock (pthread_mutex_t *mutex, const char *fn, int line)
{
  (pthread_mutex_lock) (&guard);
  int l = m_depth++ + g_depth;
  (pthread_mutex_unlock) (&guard);
  g_printerr ("%*s   lock %5d: %s %d\n", l + 3, "pth", gettid (), fn, line);
  return (pthread_mutex_lock) (mutex);
}

int gxine_mutex_unlock (pthread_mutex_t *mutex, const char *fn, int line)
{
  (pthread_mutex_lock) (&guard);
  int l = --m_depth + g_depth;
  assert (m_depth >= 0);
  (pthread_mutex_unlock) (&guard);
  g_printerr ("%*s unlock %5d: %s %d\n", l + 3, "pth", gettid (), fn, line);
  return (pthread_mutex_unlock) (mutex);
}

void gxine_threads_enter (const char *fn, int line)
{
  (pthread_mutex_lock) (&guard);
  int l = g_depth++ + m_depth;
  (pthread_mutex_unlock) (&guard);
  g_printerr ("%*s   lock %5d: %s %d\n", l + 3, "gdk", gettid (), fn, line);
  (gdk_threads_enter) ();
}

void gxine_threads_leave (const char *fn, int line)
{
  (pthread_mutex_lock) (&guard);
  int l = --g_depth + m_depth;
  /* assert (l >= 0); ** -1 unavoidably occurs in one instance */
  (pthread_mutex_unlock) (&guard);
  g_printerr ("%*s unlock %5d: %s %d\n", l + 3, "gdk", gettid (), fn, line);
  (gdk_threads_leave) ();
}
#endif
