/*
 * Copyright (C) 2001-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
 *
 * Functions to stup and deal with a preferences dialog interfacing with
 * the xine config system.
 *
 * Richard Wareham <richwareham@users.sourceforge.net> -- March 2002
 * Darren Salt <dsalt@users.sourceforge.net> -- December 2004
 */

#include "globals.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include <glib.h>
#include <gdk/gdk.h>
#include <gdk/gdkkeysyms.h>
#if GTK_CHECK_VERSION(3, 0, 0)
#include <gdk/gdkkeysyms-compat.h>
#endif
#include <gtk/gtk.h>

#include "mediamarks.h"
#include "preferences.h"
#include "ui.h"
#include "utils.h"

#include <jsapi.h>

#if defined(WITH_GUDEV) || defined(WITH_HAL)
# define GXINE_DEVICE_INFO
#endif

typedef struct {
  void *notebook; /* NULL - get from parent */
  GtkWidget *todefault, *revert, *editable, *label, *separator;
  int exp;
} pref_item_t;
#define PREF_ITEM_T(NODE) ((pref_item_t *)(NODE)->data)

typedef struct {
  GtkWidget *notebook; /* contains child pages, not this page */
  GtkWidget *page, *table;
  char *prefix, *label;
} pref_page_t;
#define PREF_PAGE_T(NODE) ((pref_page_t *)(NODE)->data)

typedef union {
  GtkWidget *notebook;
  pref_item_t item;
  pref_page_t page;
} pref_t;
#define PREF_T(NODE) ((pref_t *)(NODE)->data)

static GtkWidget  *prefs_dialog;
static GNode	  *prefs = NULL;
static GData	  *prefs_map = NULL;
static int         is_visible;
static int         update_lock = 0;


static int select_show_prefs_internal (GNode *page, int exp)
{
  int state = 0;
  pref_t *item = page->data;
  pref_t *parent = page->parent ? page->parent->data : NULL;
  GNode *child;

  if (parent && parent->notebook == GINT_TO_POINTER (1))
    return 0; /* called too early */

  child = g_node_first_child (page);

  if (PREF_T(child)->notebook)
  {
    /* recursively process subpages (but first we hide them all) */
    GtkNotebook *notebook = GTK_NOTEBOOK(item->page.notebook);
    while (gtk_notebook_get_n_pages (notebook))
      gtk_notebook_remove_page (notebook, 0);
    for (; child; child = g_node_next_sibling (child))
      state |= select_show_prefs_internal (child, exp);
  }
  else
  {
    /* hide/show widgets on this page */
    pref_item_t *last = NULL;
    for (; child; child = g_node_next_sibling (child))
    {
      pref_item_t *pref = child->data;
      void (*func)(GtkWidget *);
      if (pref->exp <= exp)
      {
	func = gtk_widget_show;
	state |= 1;
	last = pref;
      }
      else
      {
	func = gtk_widget_hide;
	state |= 2;
      }
      if (pref->exp <= exp)
	last = pref;
      func (pref->todefault);
      func (pref->revert);
      func (pref->editable);
      func (pref->label);
      func (pref->separator);
    }
    if (last)
      gtk_widget_hide (last->separator);
  }

  /* re-add the page if it has visible widgets */
  if (parent && state != 2)
    gtk_notebook_append_page (GTK_NOTEBOOK(parent->notebook), item->page.page,
			      gtk_label_new (item->page.label));

  return state;
}

static gboolean select_show_pref_widgets (void *data)
{
  static const int experience_values[] = { 0, 10, 20, 30 };
  int exp = *(int *)data;

  if (exp >= 0 && exp < (int) G_N_ELEMENTS (experience_values))
    exp = experience_values[exp];
  else
    exp = 0;

  ++update_lock;
  gdk_threads_enter ();
  select_show_prefs_internal (prefs, exp);
  gdk_threads_leave ();
  --update_lock;
  return FALSE;
}

static gboolean entry_cb (GtkEntry *editable, GdkEventFocus *even,
			  gpointer user_data)
{
  xine_cfg_entry_t      entry;
  gchar                *key = (gchar *) user_data;

  if (update_lock)
    return FALSE;

  logprintf ("preferences: entry cb for key %s\n", key);

  if (!xine_config_lookup_entry (xine, key, &entry))
    return FALSE;

  key = gtk_editable_get_chars (GTK_EDITABLE(editable), 0, -1);
  if (!strcmp (entry.str_value, key))
    return FALSE;

  logprintf ("preferences: updating entry\n");
  entry.str_value = key;
  xine_config_update_entry (xine, &entry);

  return FALSE;
}

static gboolean entry_keypress_cb (GtkEntry *widget, GdkEventKey *event,
				   gpointer data)
{
  switch (event->keyval)
  {
  case GDK_Return:
  case GDK_KP_Enter:
    entry_cb (widget, NULL, data);
    return TRUE;

  case GDK_minus:
    if ((event->state & GXINE_MODIFIER_MASK) != GDK_CONTROL_MASK)
      break;
  case GDK_Undo:
    {
      const char *key = g_object_get_data (G_OBJECT(widget), "cfg");
      xine_cfg_entry_t entry;
      if (key && xine_config_lookup_entry (xine, key, &entry))
	gtk_entry_set_text (widget, entry.str_value);
    }
    return TRUE;
  }

  return FALSE;
}

static void check_box_cb (GtkToggleButton *togglebutton, gpointer user_data)
{
  xine_cfg_entry_t      entry;
  gchar                *key = (gchar *) user_data;
  int			state;

  if (update_lock)
    return;

  logprintf ("preferences: check box cb for key %s\n", key);

  if (!xine_config_lookup_entry (xine, key, &entry))
    return;

  state = gtk_toggle_button_get_active (togglebutton);
  if (entry.num_value == state)
    return;

  logprintf ("preferences: updating entry\n");
  entry.num_value = state;
  xine_config_update_entry (xine, &entry);
}

static void range_cb (GtkAdjustment *adj, const gchar *key)
{
  xine_cfg_entry_t      entry;

  if (update_lock)
    return;

  logprintf ("preferences: range cb for key %s\n", key);

  double value = gtk_adjustment_get_value (adj);
  if (!xine_config_lookup_entry (xine, key, &entry) ||
      entry.num_value == value)
    return;

  logprintf ("preferences: updating entry to %lf\n", value);
  entry.num_value = value;
  xine_config_update_entry (xine, &entry);
}

static void spin_cb (GtkSpinButton *widget, const gchar *key)
{
  xine_cfg_entry_t entry;
  int value;

  if (update_lock)
    return;

  logprintf ("preferences: spin cb for key %s\n", key);

  value = gtk_spin_button_get_value_as_int (widget);
  if (!xine_config_lookup_entry (xine, key, &entry) ||
      entry.num_value == value)
    return;

  logprintf ("preferences: updating entry to %d\n", value);
  entry.num_value = value;
  xine_config_update_entry (xine, &entry);
}

static void enum_cb (GtkWidget* widget, gpointer data)
{
  xine_cfg_entry_t      entry;
  gchar                *key = (gchar *) data;
  static int            pos;

  if (update_lock)
    return;

  logprintf ("preferences: enum cb for key %s\n", key);

  if (!xine_config_lookup_entry (xine, key, &entry))
    return;

  pos = gtk_combo_box_get_active (GTK_COMBO_BOX(widget));

  if (entry.num_value != pos)
  {
    entry.num_value = pos;
    logprintf ("preferences: updating entry to %d\n", pos);
    xine_config_update_entry (xine, &entry);
    if (!strcmp (key, "gui.experience_level"))
      g_idle_add ((GSourceFunc) select_show_pref_widgets, &pos);
  }
}

#ifdef WITH_GUDEV
#define G_UDEV_API_IS_SUBJECT_TO_CHANGE
#include <gudev/gudev.h>

static GUdevClient *gudev = NULL;

static gboolean check_gudev (void)
{
  static const gchar *const subsystems[] = { NULL }; /* at least DVB & block */
  if (!gudev)
    gudev = g_udev_client_new (subsystems);
  return !!gudev;
}

static char *
get_gudev_property (const char *name, const char *property, const char *sysfs)
{
  GUdevDevice *dev = g_udev_client_query_by_device_file (gudev, name);
  const gchar *prop;
  char *ret;

  if (!dev)
    return NULL;

  prop = property ? g_udev_device_get_property (dev, property) : NULL;
  if (prop && prop[0])
  {
    /* g_strcompress doesn't handle "\xNN" */
    char *ptr = ret = malloc (strlen (prop) + 1);
    char c;
    while ((c = *prop++))
    {
      if (c == '\\')
      {
	char tmp[] = { '0', prop[0], prop[1], prop[2], 0 };
	prop += 3;
	c = (char) strtol (tmp, NULL, 0);
      }
      *ptr++ = c;
    }
    *ptr = 0;
  }
  else
  {
    prop = sysfs ? g_udev_device_get_sysfs_attr (dev, sysfs) : NULL;
    ret = (prop && prop[0]) ? strdup (prop) : NULL;
  }

  g_object_unref ((GObject *)dev);

  return ret;
}

static char *
get_hal_device_info (const char *dev)
{
  if (!check_gudev () || !dev)
    return NULL;

  int ldev = strlen (dev);
  char buf[FILENAME_MAX + ldev + 1];
  strcpy (buf, dev); /* safe! */
  if (buf[0] == '/')
  {
    ldev = strrchr (buf, '/') - buf + 1;
    int tmp = readlink (dev, buf + ldev, FILENAME_MAX - 1);
    if (tmp <= 0)
      strcpy (buf, dev); /* safe! */
    else if (buf[ldev] == '/')
    {
      memmove (buf, buf + ldev, tmp);
      buf[tmp] = 0;
    }
    else
    {
      char *p;
      buf[ldev + tmp] = 0;
      while ((p = strstr (buf, "//")))
	memmove (p, p + 1, strlen (p));
      while ((p = strstr (buf, "/./")))
	memmove (p, p + 2, strlen (p) - 1);
      while ((p = strstr (buf, "/../")))
      {
	char *q = p;
	while (--q >= buf && *q != '/')
	  /**/;
	memmove (q, p + 3, strlen (p) - 2);
      }
    }
  }

  return get_gudev_property (buf, "ID_MODEL_ENC", "name");
}
#elif defined(WITH_HAL)
#include <unistd.h>
#include <hal/libhal.h>

static LibHalContext *hal = NULL;

static gboolean check_hal (void)
{
  if (hal)
    return TRUE;
  DBusConnection *dbus = dbus_bus_get (DBUS_BUS_SYSTEM, NULL);
  if (!dbus)
    return FALSE;
  hal = libhal_ctx_new ();
  if (!hal)
    return FALSE;
  libhal_ctx_set_dbus_connection (hal, dbus);
  return TRUE;
}

static char *
get_hal_property (const char *dev, const char *dev_tag, const char *prop_tag)
{
  int count;
  char **devs = libhal_manager_find_device_string_match
		  (hal, dev_tag, dev, &count, NULL);
  if (!devs)
    return NULL;
  if (!devs[0])
  {
    libhal_free_string_array (devs);
    return NULL;
  }

  char *prop = libhal_device_get_property_string (hal, devs[0], prop_tag, NULL);
  char *ret = (prop && prop[0]) ? strdup (prop) : NULL;
  libhal_free_string (prop);
  libhal_free_string_array (devs);
  return ret;
}

static char *
get_hal_device_info (const char *dev)
{
  if (!check_hal () || !dev)
    return NULL;

  int ldev = strlen (dev);
  char buf[FILENAME_MAX + ldev + 1];
  strcpy (buf, dev); /* safe! */
  if (buf[0] == '/')
  {
    ldev = strrchr (buf, '/') - buf + 1;
    int tmp = readlink (dev, buf + ldev, FILENAME_MAX - 1);
    if (tmp <= 0)
      strcpy (buf, dev); /* safe! */
    else if (buf[ldev] == '/')
    {
      memmove (buf, buf + ldev, tmp);
      buf[tmp] = 0;
    }
    else
    {
      char *p;
      buf[ldev + tmp] = 0;
      while ((p = strstr (buf, "//")))
	memmove (p, p + 1, strlen (p));
      while ((p = strstr (buf, "/./")))
	memmove (p, p + 2, strlen (p) - 1);
      while ((p = strstr (buf, "/../")))
      {
	char *q = p;
	while (--q >= buf && *q != '/')
	  /**/;
	memmove (q, p + 3, strlen (p) - 2);
      }
    }
  }

#ifdef HAL_DEVICE_FILE_PREFIX
  char *const buf_unprefixed =
    strncmp (buf, HAL_DEVICE_FILE_PREFIX, sizeof (HAL_DEVICE_FILE_PREFIX) - 1)
    ? "" : buf + sizeof (HAL_DEVICE_FILE_PREFIX) - 2;
#else
  char *const buf_unprefixed = buf;
#endif

  return get_hal_property (buf, "block.device", "storage.model")
	 ? : get_hal_property (buf, "block.device", "info.product")
#ifdef HAL_DEVICE_FILE_PROPERTY
	 ? : get_hal_property (buf_unprefixed, HAL_DEVICE_FILE_PROPERTY, "info.product")
#else
	 /* FIXME - BSD etc. */
	 ? : NULL
#endif
	 ;
}
#endif /* WITH_HAL */

#ifdef GXINE_DEVICE_INFO
static void
display_hal_device_info (GtkWidget *widget, const char *dev)
{
  GtkLabel *info = g_object_get_data (G_OBJECT (widget), "extra");
  if (info)
  {
    char *hal = get_hal_device_info (dev);
    char *content = g_markup_printf_escaped ("<small>%s</small>", hal ? : "");
    gtk_label_set_markup (info, content);
    free (content);
    free (hal);
  }
}
#endif /* WITH_GUDEV || WITH_HAL */

static void file_cb (GtkWidget *widget, int response, gpointer data)
{
  xine_cfg_entry_t entry;
  gchar *key = g_object_get_data (data, "cfg");

  if (update_lock || response != GTK_RESPONSE_ACCEPT)
    return;

  logprintf ("preferences: file cb for key %s\n", key);

  if (!xine_config_lookup_entry (xine, key, &entry))
    return;

  key = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (widget));
  if (key && strcmp (entry.str_value, key))
  {
    logprintf ("preferences: updating entry\n");
    entry.str_value = key;
    xine_config_update_entry (xine, &entry);
#ifdef GXINE_DEVICE_INFO
    if (entry.num_value == 2 /* XINE_CONFIG_STRING_IS_DEVICE_NAME */)
      display_hal_device_info (data, key);
#endif
  }
  free (key);
}

static void file_activate_cb (GtkWidget *widget, gpointer data)
{
  file_cb (widget, GTK_RESPONSE_ACCEPT, data);
}

#ifdef GXINE_DEVICE_INFO
static void file_preview_cb (GtkFileChooser *fc, gpointer data)
{
  GtkLabel *info = GTK_LABEL (gtk_file_chooser_get_preview_widget (fc));
  char *file = gtk_file_chooser_get_preview_filename (fc);
  if (!file)
    file = gtk_file_chooser_get_filename (fc);
  char *content = get_hal_device_info (file);
  gtk_label_set_text (info, content ? : "");
  free (content);
  g_free (file);
}
#endif

static void file_map_cb (GtkFileChooser *fcb, gpointer fc)
{
  if (!g_object_get_data (fc, "gxine"))
  {
    g_signal_connect_swapped (fcb, "current-folder-changed",
			      G_CALLBACK(file_activate_cb), fc);
    g_object_set_data (fc, "gxine", fc);
  }
#ifdef GXINE_DEVICE_INFO
  file_preview_cb (fcb, fc);
#endif
}

static void
default_item_cb (GtkWidget *w, gpointer key)
{
  xine_cfg_entry_t entry;

  if (!xine_config_lookup_entry (xine, key, &entry))
    return;

  if (entry.type == XINE_CONFIG_TYPE_STRING)
    entry.str_value = entry.str_default;
  else
    entry.num_value = entry.num_default;
  preferences_update_entry (&entry);
}

static inline GtkWidget *
create_item_default (const xine_cfg_entry_t * entry)
{
  char *tip;
  gboolean alloc = FALSE;

  switch (entry->type)
  {
  case XINE_CONFIG_TYPE_ENUM:
    tip = entry->enum_values[entry->num_default];
    break;

  case XINE_CONFIG_TYPE_RANGE:
  case XINE_CONFIG_TYPE_NUM:
    alloc = TRUE;
    tip = g_strdup_printf ("%d", entry->num_default);
    break;

  case XINE_CONFIG_TYPE_BOOL:
    tip = entry->num_default ? "✓" : "✗";
    break;

  case XINE_CONFIG_TYPE_STRING:
    tip = entry->str_default;
    break;

  default:
    return NULL;
  }

  GtkWidget *widget = ui_button_new_stock (GTK_STOCK_CLEAR);
  gtk_widget_set_tooltip_text (widget, tip);
  if (alloc)
    g_free (tip);

  g_signal_connect ((GObject *) widget, "clicked",
		    G_CALLBACK (default_item_cb), (gpointer) entry->key);
  return widget;
}

static void
revert_item_cb (GtkWidget *w, gpointer key)
{
  xine_cfg_entry_t entry;

  if (!xine_config_lookup_entry (xine, key, &entry))
    return;

  if (entry.type == XINE_CONFIG_TYPE_STRING)
    entry.str_value = g_object_get_data ((GObject *) w, "revert");
  else
    entry.num_value = (int) (intptr_t) g_object_get_data ((GObject *) w, "revert");
  preferences_update_entry (&entry);
}

static inline GtkWidget *
create_item_revert (const xine_cfg_entry_t *entry)
{
  intptr_t value;
  char *tip;
  gboolean alloc = FALSE;
  char *tmpstr = NULL;

  switch (entry->type)
  {
  case XINE_CONFIG_TYPE_ENUM:
    tip = entry->enum_values[entry->num_value];
    value = entry->num_value;
    break;

  case XINE_CONFIG_TYPE_RANGE:
  case XINE_CONFIG_TYPE_NUM:
    alloc = TRUE;
    tip = g_strdup_printf ("%d", entry->num_value);
    value = entry->num_value;
    break;

  case XINE_CONFIG_TYPE_BOOL:
    tip = entry->num_value ? "✓" : "✗";
    value = entry->num_value;
    break;

  case XINE_CONFIG_TYPE_STRING:
    tip = entry->str_value;
    tmpstr = strdup (entry->str_value);
    value = (intptr_t) tmpstr;
    break;

  default:
    return NULL;
  }

  GtkWidget *widget = ui_button_new_stock (GTK_STOCK_REVERT_TO_SAVED);
  g_object_set_data ((GObject *) widget, "revert", (void *) value);
  g_signal_connect ((GObject *) widget, "clicked",
		    G_CALLBACK (revert_item_cb), (gpointer) entry->key);
  gtk_widget_set_tooltip_text (widget, tip);
  if (alloc)
    g_free (tip);
  free(tmpstr);

  return widget;
}

static GtkWidget *create_item_editable (const xine_cfg_entry_t *entry)
{
  GtkWidget *widget;

  switch (entry->type)
  {
  case XINE_CONFIG_TYPE_ENUM:
    {
      int i;
      widget = gtk_combo_box_text_new ();
      for (i = 0; entry->enum_values[i]; ++i)
      {
	const char *label = gettext (entry->enum_values[i]);
	if (label == entry->enum_values[i])
	  label = dgettext (LIB_PACKAGE, label);
        gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT(widget), label);
      }
      gtk_combo_box_set_active (GTK_COMBO_BOX(widget), entry->num_value);
      g_signal_connect (G_OBJECT(widget), "changed",
			G_CALLBACK(enum_cb), strdup(entry->key));
    }
    break;

  case XINE_CONFIG_TYPE_STRING:
    if (entry->num_value)
    {
      GtkWidget *fc = gtk_file_chooser_dialog_new (entry->key, GTK_WINDOW (prefs_dialog),
					    entry->num_value == 3 /* XINE_CONFIG_STRING_IS_DIRECTORY_NAME */
					      ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER
					      : GTK_FILE_CHOOSER_ACTION_OPEN,
					    GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
					    GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
					    NULL);
      widget = gtk_file_chooser_button_new_with_dialog (fc);
      gtk_file_chooser_set_local_only ((GtkFileChooser *) fc, TRUE);
      struct stat st;
      if (stat (entry->str_value, &st))
      {
	g_printerr (_("warning: configuration item %s points to a non-existent location %s\n"),
		  entry->key, entry->str_value);
	xine_log (xine, 0, _("warning: configuration item %s points to a non-existent location %s\n"),
		  entry->key, entry->str_value);
      }
      else
	gtk_file_chooser_set_filename ((GtkFileChooser *) fc, entry->str_value);
#ifdef GXINE_DEVICE_INFO
      gtk_file_chooser_set_use_preview_label ((GtkFileChooser *) fc, FALSE);
      GtkWidget *preview = g_object_new (GTK_TYPE_LABEL, "angle", 90.0,
					 "selectable", FALSE, NULL);
      gtk_file_chooser_set_preview_widget ((GtkFileChooser *) fc, preview);
#endif
      g_object_connect (G_OBJECT(fc),
	"signal::response", G_CALLBACK(file_cb), widget,
	"signal::file-activated", G_CALLBACK(file_activate_cb), widget,
#ifdef GXINE_DEVICE_INFO
	"signal::update-preview", G_CALLBACK(file_preview_cb), widget,
	"signal::map", G_CALLBACK(file_preview_cb), fc,
#endif
	NULL);
      g_signal_connect_after (G_OBJECT(widget), "map", G_CALLBACK(file_map_cb), fc);
    }
    else
    {
      widget = gtk_entry_new();
      g_object_connect (G_OBJECT(widget),
	"signal::key-press-event", G_CALLBACK(entry_keypress_cb), (gchar *)entry->key,
	"signal::focus-out-event", G_CALLBACK(entry_cb), (gchar *)entry->key,
	NULL);
      gtk_entry_set_text (GTK_ENTRY(widget), entry->str_value);
    }
    g_object_set_data (G_OBJECT(widget), "cfg", (gpointer)entry->key);

    break;

  case XINE_CONFIG_TYPE_RANGE: /* slider */
    {
      GtkAdjustment *adj = GTK_ADJUSTMENT (gtk_adjustment_new (entry->num_value, entry->range_min,
                                                               entry->range_max, 1.0, 10.0, 0.0));
      widget = ui_hscale_new (adj, GTK_POS_TOP, 0);
      gtk_widget_set_size_request (widget, 128, -1);
      g_signal_connect (adj, "value-changed",
			G_CALLBACK (range_cb), (gchar *)entry->key);
    }
    break;

  case XINE_CONFIG_TYPE_NUM:
    {
      GtkAdjustment *adj = GTK_ADJUSTMENT (gtk_adjustment_new (entry->num_value, INT_MIN, INT_MAX,
                                                               1, 10, 0));
      widget = ui_spin_button_new (adj);
      g_signal_connect (G_OBJECT(widget), "value-changed",
			G_CALLBACK(spin_cb), (gchar *)entry->key);
    }
    break;

  case XINE_CONFIG_TYPE_BOOL:
    widget = gtk_check_button_new();
    gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(widget),
				  entry->num_value == 1);
    g_signal_connect (G_OBJECT(widget), "toggled",
		      G_CALLBACK(check_box_cb), (gchar *)entry->key);
    /* gtk_misc_set_alignment (GTK_MISC(widget), 1.0, 1.0); */
    break;

  default:
    widget = gtk_label_new ("?");
    g_print (_("preferences: unknown type for entry ‘%s’\n"), entry->key);
  }

  g_datalist_set_data (&prefs_map, entry->key, widget);
  gtk_widget_set_tooltip_text (widget, entry->help);
  return widget;
}

static GtkWidget *create_item_label (const xine_cfg_entry_t *entry)
{
  GtkWidget *label;
  char *labeltext;
  const char *labelkey = strrchr (entry->key, '.') + 1;

  if (entry->callback)
    labeltext = g_markup_printf_escaped ("<b>%s</b>\n%s",
					 labelkey, entry->description);
  else
    labeltext = g_markup_printf_escaped ("<b>• <i>%s</i></b>\n%s",
					 labelkey, entry->description);

  label = ui_label_new_with_markup (labeltext);
  free (labeltext);

  gtk_label_set_line_wrap (GTK_LABEL(label), TRUE);
  gtk_label_set_line_wrap_mode (GTK_LABEL(label), PANGO_WRAP_WORD);
  gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_LEFT);
#if GTK_CHECK_VERSION(3, 14, 0)
  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
  gtk_label_set_yalign (GTK_LABEL (label), 0.5);
#else
  gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5);
#endif

  return label;
}

static GtkWidget *create_item_extra (const xine_cfg_entry_t *entry)
{
  switch (entry->type)
  {
  case XINE_CONFIG_TYPE_STRING:
#ifdef GXINE_DEVICE_INFO
    if (entry->num_value == 2 /* XINE_CONFIG_STRING_IS_DEVICE_NAME */)
      return gtk_label_new (NULL);
#endif
    break;
  }
  return NULL;
}

static GNode *create_item (const xine_cfg_entry_t *entry)
{
  pref_item_t *item = malloc (sizeof (pref_item_t));
  item->notebook = NULL;
  item->todefault = create_item_default (entry);
  item->revert = create_item_revert (entry);
  item->editable = create_item_editable (entry);
  item->label = create_item_label (entry);
  GtkWidget *extra = create_item_extra (entry);
  if (extra)
    g_object_set_data (G_OBJECT (item->editable), "extra", extra);
#if GTK_CHECK_VERSION(3, 2, 0)
  item->separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
#else
  item->separator = gtk_hseparator_new ();
#endif
  item->exp = entry->exp_level;
  return g_node_new (item);
}

static GNode *create_page (const char *key, size_t length)
{
  pref_page_t *page = malloc (sizeof (pref_page_t));
  page->notebook = GINT_TO_POINTER (1); /* dummy value - filled in later */
  page->page = page->table = NULL;
  page->prefix = g_strdup_printf ("%.*s", (int) length, key);
  page->label = strrchr (page->prefix, '.');
  if (page->label)
    ++page->label;
  else
    page->label = page->prefix;
  return g_node_new (page);
}

struct page_find_s {
  const char *key;
  size_t length;
  GNode *match;
};

static gboolean find_page_sub (GNode *node, struct page_find_s *find)
{
  const pref_page_t *pref = node->data;
  if (!pref->notebook)
    return FALSE;
  if (strncmp (pref->prefix, find->key, find->length) ||
      pref->prefix[find->length])
    return FALSE;
  find->match = node;
  return TRUE;
}

static GNode *find_page (const char *key, size_t length)
{
  struct page_find_s find = { key, length, NULL };
  g_node_traverse (prefs, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
		   (GNodeTraverseFunc) find_page_sub, &find);
  return find.match;
}

static gboolean unmix_mixed_page (GNode *node, gboolean *done)
{
  int types = 0;
  GNode *child, *new_page;
  char *prefix;
  pref_page_t *page = node->data;

  /* not a page? return */
  if (!page->notebook)
    return FALSE;

  /* check if this page has mixed content */
  for (child = g_node_first_child (node); child;
       child = g_node_next_sibling (child))
    types |= (PREF_T(child)->notebook ? 1 : 2);

  switch (types)
  {
  case 0:
    /* hmm, odd... found a blank page - delete it */
    logprintf ("gxine: prefs: eek, found a blank page '%s'\n", page->prefix);
    gtk_widget_destroy (page->notebook);
    free (page->prefix);
    free (page);
    g_node_destroy (node);
    *done = FALSE;
    return TRUE;

  case 3:
    /* found a mixed-mode page (this is normal) */
    prefix = g_strdup_printf ("%s. ", page->prefix);
    new_page = g_node_prepend (node, create_page (prefix, strlen (prefix)));
    g_free (prefix);

    /* move the new subpage's sibling item nodes into it */
    child = g_node_first_child (node);
    while (child)
    {
      GNode *next = g_node_next_sibling (child);
      pref_t *pref = child->data;
      if (!pref->notebook)
      {
	g_node_unlink (child);
	g_node_append (new_page, child);
      }
      child = next;
    }

    *done = FALSE;
    return TRUE;
  }

  return FALSE;
}

static gboolean focus_item_cb (GtkWidget *widget, GtkDirectionType *dir,
			       GNode *node)
{
  if (!node->parent)
    return FALSE;

  const pref_t *pref = node->data;
  const pref_t *parent = node->parent->data;
  GtkWidget *viewport = gtk_widget_get_parent (parent->page.table);
  GtkAdjustment *adj = gtk_viewport_get_vadjustment ((GtkViewport *)viewport);

  GtkAllocation allocation_edit = { 0 };
  GtkAllocation allocation_label = { 0 };
  GtkAllocation allocation_viewport = { 0 };
  gtk_widget_get_allocation (pref->item.editable, &allocation_edit);
  gtk_widget_get_allocation (pref->item.label,    &allocation_label);
  gtk_widget_get_allocation (viewport,            &allocation_viewport);

  int wt = allocation_edit.y;
  int wb = wt + allocation_edit.height;
  int lt = allocation_label.y;
  int lb = lt + allocation_label.height;
  int top = wt < lt ? wt : lt;
  int bottom = wb > lb ? wb : lb;
  double value = gtk_adjustment_get_value (adj);
  if (top < value)
    gtk_adjustment_set_value (adj, top);
  else if (bottom + 4 > value + allocation_viewport.height)
    gtk_adjustment_set_value (adj, bottom - allocation_viewport.height + 4);

  return FALSE;
}

static gboolean put_content (GNode *node, gpointer data)
{
  pref_t *pref = node->data;
  pref_t *parent = node->parent ? node->parent->data : NULL;

  if (pref->notebook)
  {
    /* we have a page */
    pref_t *child = g_node_first_child (node)->data;
    GtkWidget *widget;

    if (child->notebook)
    {
      /* child pages present - create a notebook */
      widget = pref->page.notebook = pref->page.page = gtk_notebook_new ();
      gtk_notebook_set_scrollable (GTK_NOTEBOOK(widget), TRUE);
    }
    else
    {
      /* child pages not present - create a scrollable table */
      widget = pref->page.table = gtk_table_new (1, 1, FALSE);
      pref->page.page = gtk_scrolled_window_new (NULL, NULL);
      gtk_scrolled_window_set_policy
	(GTK_SCROLLED_WINDOW(pref->page.page),
			     GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
      gtk_scrolled_window_add_with_viewport
	(GTK_SCROLLED_WINDOW(pref->page.page), widget);
    }

    if (parent)
    {
      /* add this page to its parent's notebook */
      g_object_ref (pref->page.page); /* hiding is done by removing */
      gtk_notebook_append_page (GTK_NOTEBOOK(parent->notebook), pref->page.page,
				gtk_label_new (pref->page.label));
    }
    else
      gtk_widget_set_size_request (pref->page.page, 600, 350);
  }
  else if (parent)
  {
    /* we have a config item */
    int r = g_node_child_position (node->parent, node) * 2;
    GtkTable *table = GTK_TABLE(parent->page.table);
    gtk_table_attach (table, pref->item.todefault, 0, 1, r, r + 1,
		      GTK_FILL, GTK_SHRINK, 2, 5);
    gtk_table_attach (table, pref->item.revert, 1, 2, r, r + 1,
		      GTK_FILL, GTK_SHRINK, 2, 5);
    GtkWidget *extra = g_object_get_data (G_OBJECT (pref->item.editable), "extra");
    if (extra)
    {
      GtkBox *box = GTK_BOX (gtk_vbox_new (2, FALSE));
      gtk_table_attach (table, (GtkWidget *) box, 2, 3, r, r + 1,
			GTK_EXPAND | GTK_FILL, GTK_SHRINK, 2, 5);
      gtk_box_pack_start (box, pref->item.editable, TRUE, TRUE, 0);
      gtk_box_pack_start (box, extra, TRUE, TRUE, 0);
#ifdef GXINE_DEVICE_INFO
      xine_cfg_entry_t entry;
      const char *key = g_object_get_data (G_OBJECT (pref->item.editable), "cfg");
      if (xine_config_lookup_entry (xine, key, &entry))
	display_hal_device_info (pref->item.editable, entry.str_value);
#endif
    }
    else
      gtk_table_attach (table, pref->item.editable, 2, 3, r, r + 1,
			GTK_EXPAND | GTK_FILL, GTK_SHRINK, 2, 5);
    gtk_table_attach (table, pref->item.label, 3, 6, r, r + 1,
		      GTK_EXPAND | GTK_FILL, GTK_SHRINK, 5, 5);
    gtk_table_attach (table, pref->item.separator, 0, 6, r + 1, r + 2,
		      GTK_EXPAND | GTK_FILL, GTK_SHRINK | GTK_FILL, 5, 5);
    g_signal_connect (pref->item.todefault, "focus",
		      G_CALLBACK (focus_item_cb), node);
    g_signal_connect (pref->item.revert, "focus",
		      G_CALLBACK (focus_item_cb), node);
    g_signal_connect (pref->item.editable, "focus",
		      G_CALLBACK (focus_item_cb), node);
  }

  return FALSE;
}

static GtkWidget *make_prefs_window (void)
{
  xine_cfg_entry_t entry;
  gboolean done;

  if (!xine_config_get_first_entry (xine, &entry))
    return NULL;

  prefs = create_page ("", 0); /* root page */

  do
  {
    const char *point;
    GNode *section;

    if (!entry.description ||
	!(point= strrchr (entry.key, '.')) || point == entry.key)
      continue;

    /* ensure that a page (window+table) is present for this config item */
    section = find_page (entry.key, point - entry.key);
    if (!section)
    {
      section = prefs;
      point = entry.key - 1;
      while ((point = strchr (point + 1, '.')))
      {
	GNode *self = find_page (entry.key, point - entry.key);
	if (!self)
	{
	  self = create_page (entry.key, point - entry.key);
	  g_node_append (section, self);
	}
	section = self;
      }
    }

    /* add the config item (and create its widgets) */
    g_node_append (section, create_item (&entry));
  } while (xine_config_get_next_entry (xine, &entry));

  /* we now have a full tree*/

  do
  {
    done = TRUE;
    g_node_traverse (prefs, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
		     (GNodeTraverseFunc) unmix_mixed_page, &done);
  } while (!done);

  g_node_traverse (prefs, G_PRE_ORDER, G_TRAVERSE_ALL, -1, put_content, NULL);

  return PREF_PAGE_T(prefs)->page;
}

static void response_cb (GtkDialog *dbox, int response, gpointer data)
{
  gchar *fname;
  switch (response)
  {
  case GTK_RESPONSE_ACCEPT:
    fname = get_config_filename (FILE_CONFIG);
    xine_config_save (xine, fname);
    g_free (fname);
    break;
  default:
    is_visible = 0;
    gtk_widget_hide (prefs_dialog);
  }
}

static JSBool js_preferences_show (JSContext *cx, uintN argc, jsval *vp)
{
  /* se_t *se = (se_t *) JS_GetContextPrivate(cx); */
  se_log_fncall_checkinit ("preferences_show");
  preferences_show ();
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static void preferences_init_dbox (void)
{
  prefs_dialog = gtk_dialog_new_with_buttons (_("Preferences"), NULL, 0,
				GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
				GTK_STOCK_CLOSE, GTK_RESPONSE_DELETE_EVENT,
				NULL);
  gtk_window_set_default_size (GTK_WINDOW (prefs_dialog), 500, 150);
  hide_on_delete (prefs_dialog, &is_visible);
  g_signal_connect (G_OBJECT(prefs_dialog), "response",
		    G_CALLBACK(response_cb), NULL);

  /* Make new tabbed box (notebook) */
  GtkWidget *prefs_notebook = make_prefs_window ();
  gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (prefs_dialog))),
                      prefs_notebook, TRUE, TRUE, 0);

  GtkWidget *w = ui_label_new_with_markup
		   (_("Items marked “<b>• <i>like this</i></b>” require "
		      "gxine to be restarted to take effect."));
  gtk_box_pack_end (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (prefs_dialog))),
                    w, FALSE, FALSE, 2);
}

static gpointer preferences_first_show (gpointer unused)
{
  static int value = 0;
  xine_cfg_entry_t entry;

  gtk_widget_show_all (gtk_dialog_get_content_area (GTK_DIALOG (prefs_dialog)));
  if (xine_config_lookup_entry (xine, "gui.experience_level", &entry))
    value = entry.num_value;

  select_show_pref_widgets (&value);
  return NULL;
}

void preferences_show (void)
{
  if (is_visible)
  {
    is_visible = FALSE;
    gtk_widget_hide (prefs_dialog);
  }
  else
  {
    is_visible = TRUE;
    static GOnce once = G_ONCE_INIT;
    g_once (&once, preferences_first_show, NULL);
    window_present (prefs_dialog, NULL);
  }
}

void preferences_init (void)
{
  is_visible = FALSE;
  /* script engine functions */
  se_defun (gse, NULL, "preferences_show", js_preferences_show, 0, JSFUN_FAST_NATIVE,
	    SE_GROUP_DIALOGUE, NULL, NULL);

  preferences_init_dbox ();
}

void preferences_update_entry (const xine_cfg_entry_t *entry)
{
  gpointer widget = g_datalist_get_data (&prefs_map, entry->key);

  if (widget)
    switch (entry->type)
    {
    case XINE_CONFIG_TYPE_ENUM:
      gtk_combo_box_set_active (widget, entry->num_value);
      break;

    case XINE_CONFIG_TYPE_STRING:
      if (entry->num_value)
      {
	gtk_file_chooser_set_filename (widget, entry->str_value);
#ifdef GXINE_DEVICE_INFO
	display_hal_device_info (widget, entry->str_value);
#endif
      }
      else
	gtk_entry_set_text (widget, entry->str_value);
      break;

    case XINE_CONFIG_TYPE_RANGE: /* slider */
      gtk_range_set_value (widget, entry->num_value);
      break;

    case XINE_CONFIG_TYPE_NUM:
      gtk_spin_button_set_value (widget, entry->num_value);
      break;

    case XINE_CONFIG_TYPE_BOOL:
      gtk_toggle_button_set_active (widget, entry->num_value == 1);
      break;

    default:;
    }

  xine_config_update_entry (xine, entry);
}
