/* gdict-sidebar.c - sidebar widget
 *
 * Copyright (C) 2006  Emmanuele Bassi <ebassi@gmail.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * Based on the equivalent widget from Evince
 * 	by Jonathan Blandford,
 * 	Copyright (C) 2004  Red Hat, Inc.
 */

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>

#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>
#include <glib/gi18n.h>

#include "gdict-sidebar.h"

typedef struct
{
  guint index;

  gchar *id;
  gchar *name;

  GtkWidget *child;
  GtkWidget *menu_item;
} SidebarPage;

#define GDICT_SIDEBAR_GET_PRIVATE(obj)	(G_TYPE_INSTANCE_GET_PRIVATE ((obj), GDICT_TYPE_SIDEBAR, GdictSidebarPrivate))

struct _GdictSidebarPrivate
{
  GHashTable *pages_by_id;
  GSList *pages;

  GtkWidget *hbox;
  GtkWidget *notebook;
  GtkWidget *menu;
  GtkWidget *close_button;
  GtkWidget *label;
  GtkWidget *select_button;
};

enum
{
  PAGE_CHANGED,
  CLOSED,

  LAST_SIGNAL
};

static guint sidebar_signals[LAST_SIGNAL] = { 0 };
static GQuark sidebar_page_id_quark = 0;

G_DEFINE_TYPE (GdictSidebar, gdict_sidebar, GTK_TYPE_BOX);

SidebarPage *
sidebar_page_new (const gchar *id,
		  const gchar *name,
		  GtkWidget   *widget)
{
  SidebarPage *page;

  page = g_slice_new (SidebarPage);
  
  page->id = g_strdup (id);
  page->name = g_strdup (name);
  page->child = widget;
  page->index = -1;
  page->menu_item = NULL;

  return page;
}

void
sidebar_page_free (SidebarPage *page)
{
  if (G_LIKELY (page))
    {
      g_free (page->name);
      g_free (page->id);
      
      g_slice_free (SidebarPage, page);
    }
}

static void
gdict_sidebar_finalize (GObject *object)
{
  GdictSidebar *sidebar = GDICT_SIDEBAR (object);
  GdictSidebarPrivate *priv = sidebar->priv;

  if (priv->pages_by_id)
    g_hash_table_destroy (priv->pages_by_id);

  if (priv->pages)
    {
      g_slist_foreach (priv->pages, (GFunc) sidebar_page_free, NULL);
      g_slist_free (priv->pages);
    }

  G_OBJECT_CLASS (gdict_sidebar_parent_class)->finalize (object);
}

static void
gdict_sidebar_dispose (GObject *object)
{
  GdictSidebar *sidebar = GDICT_SIDEBAR (object);

  if (sidebar->priv->menu)
    {
      gtk_menu_detach (GTK_MENU (sidebar->priv->menu));
      sidebar->priv->menu = NULL;
    }

  G_OBJECT_CLASS (gdict_sidebar_parent_class)->dispose (object);
}

#if !GTK_CHECK_VERSION (3, 22, 0)
/* We only use this with older versions of GTK+ */
static void
gdict_sidebar_menu_position_function (GtkMenu  *menu,
				      gint     *x,
				      gint     *y,
				      gboolean *push_in,
				      gpointer  user_data)
{
  GtkWidget *widget;
  GtkAllocation allocation;

  g_assert (GTK_IS_BUTTON (user_data));

  widget = GTK_WIDGET (user_data);

  gdk_window_get_origin (gtk_widget_get_window (widget), x, y);

  gtk_widget_get_allocation (widget, &allocation);
  *x += allocation.x;
  *y += allocation.y + allocation.height;

  *push_in = FALSE;
}
#endif /* !GTK_CHECK_VERSION (3, 22, 0) */

static gboolean
gdict_sidebar_select_button_press_cb (GtkWidget      *widget,
				      GdkEventButton *event,
				      gpointer        user_data)
{
  GdictSidebar *sidebar = GDICT_SIDEBAR (user_data);
  GtkAllocation allocation;

  if (event->button == 1)
    {
      GtkRequisition req;
      gint width;

      gtk_widget_get_allocation (widget, &allocation);
      width = allocation.width;
      gtk_widget_set_size_request (sidebar->priv->menu, -1, -1);
      gtk_widget_get_preferred_size (sidebar->priv->menu, NULL, &req);
      gtk_widget_set_size_request (sidebar->priv->menu,
		      		   MAX (width, req.width), -1);
      gtk_widget_grab_focus (widget);

      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), TRUE);

#if GTK_CHECK_VERSION (3, 22, 0)
      gtk_menu_popup_at_widget (GTK_MENU (sidebar->priv->menu),
                                widget,
                                GDK_GRAVITY_SOUTH_WEST,
                                GDK_GRAVITY_NORTH_WEST,
                                (GdkEvent *) event);
#else
      gtk_menu_popup (GTK_MENU (sidebar->priv->menu),
		      NULL, NULL,
		      gdict_sidebar_menu_position_function, widget,
		      event->button, event->time);
#endif

      return TRUE;
    }

  return FALSE;
}

static gboolean
gdict_sidebar_select_key_press_cb (GtkWidget   *widget,
				   GdkEventKey *event,
				   gpointer     user_data)
{
  GdictSidebar *sidebar = GDICT_SIDEBAR (user_data);

  if (event->keyval == GDK_KEY_space ||
      event->keyval == GDK_KEY_KP_Space ||
      event->keyval == GDK_KEY_Return ||
      event->keyval == GDK_KEY_KP_Enter)
    {
      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), TRUE);

#if GTK_CHECK_VERSION (3, 22, 0)
      gtk_menu_popup_at_widget (GTK_MENU (sidebar->priv->menu),
                                widget,
                                GDK_GRAVITY_SOUTH_WEST,
                                GDK_GRAVITY_NORTH_WEST,
                                (GdkEvent *) event);
#else
      gtk_menu_popup (GTK_MENU (sidebar->priv->menu),
		      NULL, NULL,
		      gdict_sidebar_menu_position_function, widget,
		      1, event->time);
#endif

      return TRUE;
    }

  return FALSE;
}

static void
gdict_sidebar_close_clicked_cb (GtkWidget *widget,
				gpointer   user_data)
{
  GdictSidebar *sidebar = GDICT_SIDEBAR (user_data);

  g_signal_emit (sidebar, sidebar_signals[CLOSED], 0);
}

static void
gdict_sidebar_menu_deactivate_cb (GtkWidget *widget,
				  gpointer   user_data)
{
  GdictSidebar *sidebar = GDICT_SIDEBAR (user_data);
  GdictSidebarPrivate *priv = sidebar->priv;
  GtkToggleButton *select_button = GTK_TOGGLE_BUTTON (priv->select_button);

  gtk_toggle_button_set_active (select_button, FALSE);
}

static void
gdict_sidebar_menu_detach_cb (GtkWidget *widget,
			      GtkMenu   *menu)
{
  GdictSidebar *sidebar = GDICT_SIDEBAR (widget);

  sidebar->priv->menu = NULL;
}

static void
gdict_sidebar_menu_item_activate (GtkWidget *widget,
				  gpointer   user_data)
{
  GdictSidebar *sidebar = GDICT_SIDEBAR (user_data);
  GdictSidebarPrivate *priv = sidebar->priv;
  GtkWidget *menu_item;
  const gchar *id;
  SidebarPage *page;
  gint current_index;

  menu_item = gtk_menu_get_active (GTK_MENU (priv->menu));
  id = g_object_get_qdata (G_OBJECT (menu_item), sidebar_page_id_quark);
  g_assert (id != NULL);
  
  page = g_hash_table_lookup (priv->pages_by_id, id);
  g_assert (page != NULL);

  current_index = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook));
  if (current_index == page->index)
    return;

  gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook),
		  		 page->index);
  gtk_label_set_text (GTK_LABEL (priv->label), page->name);

  g_signal_emit (sidebar, sidebar_signals[PAGE_CHANGED], 0);
}

static void
gdict_sidebar_class_init (GdictSidebarClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  g_type_class_add_private (gobject_class, sizeof (GdictSidebarPrivate));
  
  sidebar_page_id_quark = g_quark_from_static_string ("gdict-sidebar-page-id");

  gobject_class->finalize = gdict_sidebar_finalize;
  gobject_class->dispose = gdict_sidebar_dispose;

  sidebar_signals[PAGE_CHANGED] =
    g_signal_new ("page-changed",
		  G_TYPE_FROM_CLASS (gobject_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (GdictSidebarClass, page_changed),
		  NULL, NULL,
		  g_cclosure_marshal_VOID__VOID,
		  G_TYPE_NONE, 0);
  sidebar_signals[CLOSED] =
    g_signal_new ("closed",
		  G_TYPE_FROM_CLASS (gobject_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (GdictSidebarClass, closed),
		  NULL, NULL,
		  g_cclosure_marshal_VOID__VOID,
		  G_TYPE_NONE, 0);
}

static void
gdict_sidebar_init (GdictSidebar *sidebar)
{
  GdictSidebarPrivate *priv;
  GtkWidget *hbox;
  GtkWidget *select_hbox;
  GtkWidget *select_button;
  GtkWidget *close_button;
  GtkWidget *arrow;

  sidebar->priv = priv = GDICT_SIDEBAR_GET_PRIVATE (sidebar);

  gtk_orientable_set_orientation (GTK_ORIENTABLE (sidebar),
                                  GTK_ORIENTATION_VERTICAL);

  /* we store all the pages inside the list, but we keep
   * a pointer inside the hash table for faster look up
   * times; what's inside the table will be destroyed with
   * the list, so there's no need to supply the destroy
   * functions for keys and values.
   */
  priv->pages = NULL;
  priv->pages_by_id = g_hash_table_new (g_str_hash, g_str_equal);

  gtk_widget_set_vexpand (GTK_WIDGET (sidebar), TRUE);

  /* top option menu */
  hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
  gtk_box_pack_start (GTK_BOX (sidebar), hbox, FALSE, FALSE, 0);
  gtk_widget_show (hbox);
  priv->hbox = hbox;

  select_button = gtk_toggle_button_new ();
  gtk_button_set_relief (GTK_BUTTON (select_button), GTK_RELIEF_NONE);
  g_signal_connect (select_button, "button-press-event",
		    G_CALLBACK (gdict_sidebar_select_button_press_cb),
		    sidebar);
  g_signal_connect (select_button, "key-press-event",
		    G_CALLBACK (gdict_sidebar_select_key_press_cb),
		    sidebar);
  priv->select_button = select_button;

  select_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
  
  priv->label = g_object_new (GTK_TYPE_LABEL,
                              "xalign", 0.0,
                              "yalign", 0.5,
                              NULL);
  gtk_box_pack_start (GTK_BOX (select_hbox), priv->label, FALSE, FALSE, 0);
  gtk_widget_show (priv->label);

  arrow = gtk_image_new_from_icon_name ("go-down-symbolic", GTK_ICON_SIZE_BUTTON);
  gtk_box_pack_end (GTK_BOX (select_hbox), arrow, FALSE, FALSE, 0);
  gtk_widget_show (arrow);

  gtk_container_add (GTK_CONTAINER (select_button), select_hbox);
  gtk_widget_show (select_hbox);

  gtk_box_pack_start (GTK_BOX (hbox), select_button, TRUE, TRUE, 0);
  gtk_widget_show (select_button);

  close_button = gtk_button_new ();
  gtk_button_set_relief (GTK_BUTTON (close_button), GTK_RELIEF_NONE);
  gtk_button_set_image (GTK_BUTTON (close_button),
		        gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_SMALL_TOOLBAR));
  g_signal_connect (close_button, "clicked",
		    G_CALLBACK (gdict_sidebar_close_clicked_cb),
		    sidebar);
  gtk_box_pack_end (GTK_BOX (hbox), close_button, FALSE, FALSE, 0);
  gtk_widget_show (close_button);
  priv->close_button = close_button;

  sidebar->priv->menu = gtk_menu_new ();
  g_signal_connect (sidebar->priv->menu, "deactivate",
		    G_CALLBACK (gdict_sidebar_menu_deactivate_cb),
		    sidebar);
  gtk_menu_attach_to_widget (GTK_MENU (sidebar->priv->menu),
		  	     GTK_WIDGET (sidebar),
			     gdict_sidebar_menu_detach_cb);
  gtk_widget_show (sidebar->priv->menu);

  sidebar->priv->notebook = gtk_notebook_new ();
  gtk_notebook_set_show_border (GTK_NOTEBOOK (sidebar->priv->notebook), FALSE);
  gtk_notebook_set_show_tabs (GTK_NOTEBOOK (sidebar->priv->notebook), FALSE);
  gtk_box_pack_start (GTK_BOX (sidebar), sidebar->priv->notebook, TRUE, TRUE, 6);
  gtk_widget_show (sidebar->priv->notebook);
}

/*
 * Public API
 */

GtkWidget *
gdict_sidebar_new (void)
{
  return g_object_new (GDICT_TYPE_SIDEBAR, NULL);
}

void
gdict_sidebar_add_page (GdictSidebar *sidebar,
			const gchar  *page_id,
			const gchar  *page_name,
			GtkWidget    *page_widget)
{
  GdictSidebarPrivate *priv;
  SidebarPage *page;
  GtkWidget *menu_item;
  
  g_return_if_fail (GDICT_IS_SIDEBAR (sidebar));
  g_return_if_fail (page_id != NULL);
  g_return_if_fail (page_name != NULL);
  g_return_if_fail (GTK_IS_WIDGET (page_widget));

  priv = sidebar->priv;
  
  if (g_hash_table_lookup (priv->pages_by_id, page_id))
    {
      g_warning ("Attempting to add a page to the sidebar with "
		 "id `%s', but there already is a page with the "
		 "same id.  Aborting...",
		 page_id);
      return;
    }

  /* add the page inside the page list */
  page = sidebar_page_new (page_id, page_name, page_widget);
  
  priv->pages = g_slist_append (priv->pages, page);
  g_hash_table_insert (priv->pages_by_id, page->id, page);

  page->index = gtk_notebook_append_page (GTK_NOTEBOOK (priv->notebook),
		  			  page_widget,
					  NULL);

  /* add the menu item for the page */
  menu_item = gtk_menu_item_new_with_label (page_name);
  g_object_set_qdata_full (G_OBJECT (menu_item),
			   sidebar_page_id_quark,
                           g_strdup (page_id),
			   (GDestroyNotify) g_free);
  g_signal_connect (menu_item, "activate",
		    G_CALLBACK (gdict_sidebar_menu_item_activate),
		    sidebar);
  gtk_menu_shell_append (GTK_MENU_SHELL (priv->menu), menu_item);
  gtk_widget_show (menu_item);
  page->menu_item = menu_item;

  if (gtk_widget_get_realized (priv->menu))
    gtk_menu_shell_select_item (GTK_MENU_SHELL (priv->menu), menu_item);
  gtk_label_set_text (GTK_LABEL (priv->label), page_name);
  gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), page->index);
}

void
gdict_sidebar_remove_page (GdictSidebar *sidebar,
			   const gchar  *page_id)
{
  GdictSidebarPrivate *priv;
  SidebarPage *page;
  GList *children, *l;
  
  g_return_if_fail (GDICT_IS_SIDEBAR (sidebar));
  g_return_if_fail (page_id != NULL);

  priv = sidebar->priv;
  
  if ((page = g_hash_table_lookup (priv->pages_by_id, page_id)) == NULL)
    {
      g_warning ("Attempting to remove a page from the sidebar with "
		 "id `%s', but there is no page with this id. Aborting...",
		 page_id);
      return;
    }

  children = gtk_container_get_children (GTK_CONTAINER (priv->menu));
  for (l = children; l != NULL; l = l->next)
    {
      GtkWidget *menu_item = l->data;

      if (menu_item == page->menu_item)
        {
          gtk_container_remove (GTK_CONTAINER (priv->menu), menu_item);
	  break;
	}
    }
  g_list_free (children);

  gtk_notebook_remove_page (GTK_NOTEBOOK (priv->notebook), page->index);

  g_hash_table_remove (priv->pages_by_id, page->id);
  priv->pages = g_slist_remove (priv->pages, page);

  sidebar_page_free (page);

  /* select the first page, if present */
  page = priv->pages->data;
  if (page)
    {
      if (gtk_widget_get_realized (priv->menu))
        gtk_menu_shell_select_item (GTK_MENU_SHELL (priv->menu),
                                    page->menu_item);
      gtk_label_set_text (GTK_LABEL (priv->label), page->name);
      gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), page->index);
    }
  else
    gtk_widget_hide (GTK_WIDGET (sidebar));
}

void
gdict_sidebar_view_page (GdictSidebar *sidebar,
			 const gchar  *page_id)
{
  GdictSidebarPrivate *priv;
  SidebarPage *page;
  
  g_return_if_fail (GDICT_IS_SIDEBAR (sidebar));
  g_return_if_fail (page_id != NULL);

  priv = sidebar->priv;
  page = g_hash_table_lookup (priv->pages_by_id, page_id);
  if (!page)
    return;

  gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), page->index);
  gtk_label_set_text (GTK_LABEL (priv->label), page->name);
  if (gtk_widget_get_realized (priv->menu))
    gtk_menu_shell_select_item (GTK_MENU_SHELL (priv->menu), page->menu_item);
}

const gchar *
gdict_sidebar_current_page (GdictSidebar *sidebar)
{
  GdictSidebarPrivate *priv;
  gint index;
  SidebarPage *page;
  
  g_return_val_if_fail (GDICT_IS_SIDEBAR (sidebar), NULL);

  priv = sidebar->priv;
  
  index = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook));
  page = g_slist_nth_data (priv->pages, index);
  if (page == NULL)
    return NULL;

  return page->id;
}

gchar **
gdict_sidebar_list_pages (GdictSidebar *sidebar,
                          gsize        *length)
{
  GdictSidebarPrivate *priv;
  gchar **retval;
  gint i;
  GSList *l;

  g_return_val_if_fail (GDICT_IS_SIDEBAR (sidebar), NULL);

  priv = sidebar->priv;

  retval = g_new (gchar*, g_slist_length (priv->pages) + 1);
  for (l = priv->pages, i = 0; l; l = l->next, i++)
    retval[i++] = g_strdup (l->data);

  retval[i] = NULL;

  if (length)
    *length = i;

  return retval;
}
