/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; c-indent-level: 8 -*- */
/* this file is part of atril, a mate document viewer
 *
 *  Copyright (C) 2004 Red Hat, Inc
 *
 * Atril 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.
 *
 * Atril is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include <math.h>
#include <config.h>
#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>

#include "ev-selection.h"
#include "ev-page-cache.h"
#include "ev-view-accessible.h"
#include "ev-view-private.h"
#include "ev-page-accessible.h"

static void ev_view_accessible_action_iface_init    (AtkActionIface    *iface);
static void ev_view_accessible_document_iface_init  (AtkDocumentIface  *iface);

enum {
	ACTION_SCROLL_UP,
	ACTION_SCROLL_DOWN,
	LAST_ACTION
};

static const gchar *const ev_view_accessible_action_names[] =
{
	N_("Scroll Up"),
	N_("Scroll Down"),
	NULL
};

static const gchar *const ev_view_accessible_action_descriptions[] =
{
	N_("Scroll View Up"),
	N_("Scroll View Down"),
	NULL
};

struct _EvViewAccessiblePrivate {
	EvDocumentModel *model;

	/* AtkAction */
	gchar        *action_descriptions[LAST_ACTION];
	guint         action_idle_handler;
	GtkScrollType idle_scroll;

	gint previous_cursor_page;
	gint start_page;
	gint end_page;

	GPtrArray *children;
};

G_DEFINE_TYPE_WITH_CODE (EvViewAccessible, ev_view_accessible, GTK_TYPE_CONTAINER_ACCESSIBLE,
			 G_ADD_PRIVATE (EvViewAccessible)
			 G_IMPLEMENT_INTERFACE (ATK_TYPE_ACTION, ev_view_accessible_action_iface_init)
			 G_IMPLEMENT_INTERFACE (ATK_TYPE_DOCUMENT, ev_view_accessible_document_iface_init))

static gint
get_relevant_page (EvView *view)
{
	return ev_view_is_caret_navigation_enabled (view) ? view->cursor_page : view->current_page;
}

static void
clear_children (EvViewAccessible *self)
{
	gint i;
	AtkObject *child;

	if (self->priv->children == NULL)
		return;

	for (i = 0; i < self->priv->children->len; i++) {
		child = g_ptr_array_index (self->priv->children, i);
		atk_object_notify_state_change (child, ATK_STATE_DEFUNCT, TRUE);
	}

	g_clear_pointer (&self->priv->children, g_ptr_array_unref);
}

static void
ev_view_accessible_finalize (GObject *object)
{
	EvViewAccessiblePrivate *priv = EV_VIEW_ACCESSIBLE (object)->priv;
	int i;

	if (priv->model) {
		g_signal_handlers_disconnect_by_data (priv->model, object);
		g_object_unref (priv->model);
		priv->model = NULL;
	}
	if (priv->action_idle_handler)
		g_source_remove (priv->action_idle_handler);
	for (i = 0; i < LAST_ACTION; i++)
		g_free (priv->action_descriptions [i]);

	clear_children (EV_VIEW_ACCESSIBLE (object));

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

static void
ev_view_accessible_initialize (AtkObject *obj,
			       gpointer   data)
{
	if (ATK_OBJECT_CLASS (ev_view_accessible_parent_class)->initialize != NULL)
		ATK_OBJECT_CLASS (ev_view_accessible_parent_class)->initialize (obj, data);

	gtk_accessible_set_widget (GTK_ACCESSIBLE (obj), GTK_WIDGET (data));

	atk_object_set_name (obj, _("Document View"));
	atk_object_set_role (obj, ATK_ROLE_DOCUMENT_FRAME);
}

gint
ev_view_accessible_get_n_pages (EvViewAccessible *self)
{
	return self->priv->children == NULL ? 0 : self->priv->children->len;
}

static AtkObject *
ev_view_accessible_ref_child (AtkObject *obj,
			      gint       i)
{
	EvViewAccessible *self;
	EvView *view;

	g_return_val_if_fail (EV_IS_VIEW_ACCESSIBLE (obj), NULL);
	self = EV_VIEW_ACCESSIBLE (obj);
	g_return_val_if_fail (i >= 0 || i < ev_view_accessible_get_n_pages (self), NULL);

	view = EV_VIEW (gtk_accessible_get_widget (GTK_ACCESSIBLE (obj)));
	if (view == NULL)
		return NULL;

	/* If a given page is requested, we assume that the text would
	 * be requested soon, so we need to be sure that is cached.*/
	if (view->page_cache)
		ev_page_cache_ensure_page (view->page_cache, i);

	return g_object_ref (g_ptr_array_index (self->priv->children, i));
}

static gint
ev_view_accessible_get_n_children (AtkObject *obj)
{
	return ev_view_accessible_get_n_pages (EV_VIEW_ACCESSIBLE (obj));
}

static void
ev_view_accessible_class_init (EvViewAccessibleClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	AtkObjectClass *atk_class = ATK_OBJECT_CLASS (klass);

	object_class->finalize = ev_view_accessible_finalize;
	atk_class->initialize = ev_view_accessible_initialize;
	atk_class->get_n_children = ev_view_accessible_get_n_children;
	atk_class->ref_child = ev_view_accessible_ref_child;
}

static void
ev_view_accessible_init (EvViewAccessible *accessible)
{
	accessible->priv = ev_view_accessible_get_instance_private (accessible);
}

static gint
ev_view_accessible_get_page_count (AtkDocument *atk_document)
{
	g_return_val_if_fail (EV_IS_VIEW_ACCESSIBLE (atk_document), -1);

	return ev_view_accessible_get_n_pages (EV_VIEW_ACCESSIBLE (atk_document));
}

static gint
ev_view_accessible_get_current_page_number (AtkDocument *atk_document)
{
	GtkWidget *widget;

	g_return_val_if_fail (EV_IS_VIEW_ACCESSIBLE (atk_document), -1);

	widget = gtk_accessible_get_widget (GTK_ACCESSIBLE (atk_document));
	if (widget == NULL)
		return -1;

	/* +1 as user starts to count on 1, but evince starts on 0 */
	return get_relevant_page (EV_VIEW (widget)) + 1;
}

static void
ev_view_accessible_document_iface_init (AtkDocumentIface *iface)
{
	iface->get_current_page_number = ev_view_accessible_get_current_page_number;
	iface->get_page_count = ev_view_accessible_get_page_count;
}

static gboolean
ev_view_accessible_idle_do_action (gpointer data)
{
	EvViewAccessiblePrivate* priv = EV_VIEW_ACCESSIBLE (data)->priv;

	ev_view_scroll (EV_VIEW (gtk_accessible_get_widget (GTK_ACCESSIBLE (data))),
	                priv->idle_scroll,
	                FALSE);
	priv->action_idle_handler = 0;
	return FALSE;
}

static gboolean
ev_view_accessible_action_do_action (AtkAction *action,
				     gint       i)
{
	EvViewAccessiblePrivate* priv = EV_VIEW_ACCESSIBLE (action)->priv;

	if (gtk_accessible_get_widget (GTK_ACCESSIBLE (action)) == NULL)
		return FALSE;

	if (priv->action_idle_handler)
		return FALSE;

	switch (i) {
	case ACTION_SCROLL_UP:
		priv->idle_scroll = GTK_SCROLL_PAGE_BACKWARD;
		break;
	case ACTION_SCROLL_DOWN:
		priv->idle_scroll = GTK_SCROLL_PAGE_FORWARD;
		break;
	default:
		return FALSE;
	}
	priv->action_idle_handler = g_idle_add (ev_view_accessible_idle_do_action,
	                                        action);
	return TRUE;
}

static gint
ev_view_accessible_action_get_n_actions (AtkAction *action)
{
	return LAST_ACTION;
}

static const gchar *
ev_view_accessible_action_get_description (AtkAction *action,
					   gint       i)
{
	EvViewAccessiblePrivate* priv = EV_VIEW_ACCESSIBLE (action)->priv;

	if (i < 0 || i >= LAST_ACTION)
		return NULL;

	if (priv->action_descriptions[i])
		return priv->action_descriptions[i];
	else
		return ev_view_accessible_action_descriptions[i];
}

static const gchar *
ev_view_accessible_action_get_name (AtkAction *action,
				    gint       i)
{
	if (i < 0 || i >= LAST_ACTION)
		return NULL;

	return ev_view_accessible_action_names[i];
}

static gboolean
ev_view_accessible_action_set_description (AtkAction   *action,
					   gint         i,
					   const gchar *description)
{
	EvViewAccessiblePrivate* priv = EV_VIEW_ACCESSIBLE (action)->priv;
	gchar *old_description;

	if (i < 0 || i >= LAST_ACTION)
		return FALSE;

	old_description = priv->action_descriptions[i];
	priv->action_descriptions[i] = g_strdup (description);
	g_free (old_description);

	return TRUE;
}

static void
ev_view_accessible_action_iface_init (AtkActionIface * iface)
{
	iface->do_action = ev_view_accessible_action_do_action;
	iface->get_n_actions = ev_view_accessible_action_get_n_actions;
	iface->get_description = ev_view_accessible_action_get_description;
	iface->get_name = ev_view_accessible_action_get_name;
	iface->set_description = ev_view_accessible_action_set_description;
}

static void
ev_view_accessible_cursor_moved (EvView *view,
                                 gint page,
                                 gint offset,
                                 EvViewAccessible *accessible)
{
	EvViewAccessiblePrivate* priv = accessible->priv;
	EvPageAccessible *page_accessible = NULL;

	if (priv->previous_cursor_page != page) {
		AtkObject *previous_page = NULL;
		AtkObject *current_page = NULL;

		previous_page = g_ptr_array_index (priv->children,
						   priv->previous_cursor_page);
		atk_object_notify_state_change (previous_page, ATK_STATE_FOCUSED, FALSE);
		priv->previous_cursor_page = page;
		current_page = g_ptr_array_index (priv->children, page);
		atk_object_notify_state_change (current_page, ATK_STATE_FOCUSED, TRUE);

		/* +1 as user start to count on 1, but evince starts on 0 */
		g_signal_emit_by_name (accessible, "page-changed", page + 1);
	}

	page_accessible = g_ptr_array_index (priv->children, page);
	g_signal_emit_by_name (page_accessible, "text-caret-moved", offset);
}

static void
ev_view_accessible_selection_changed (EvView *view,
                                      EvViewAccessible *view_accessible)
{
	AtkObject *page_accessible;

	page_accessible = g_ptr_array_index (view_accessible->priv->children,
					     get_relevant_page (view));
	g_signal_emit_by_name (page_accessible, "text-selection-changed");
}

static void
page_changed_cb (EvDocumentModel  *model,
                 gint              old_page,
                 gint              new_page,
                 EvViewAccessible *accessible)
{
	EvView *view;

	view = EV_VIEW (gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible)));
	if (!ev_view_is_caret_navigation_enabled (view))
		g_signal_emit_by_name (accessible, "page-changed", new_page + 1);
}

static void
initialize_children (EvViewAccessible *self)
{
	gint i;
	EvPageAccessible *child;
	gint n_pages;
	EvDocument *ev_document;

	ev_document = ev_document_model_get_document (self->priv->model);
	n_pages = ev_document_get_n_pages (ev_document);

	self->priv->children = g_ptr_array_new_full (n_pages, (GDestroyNotify) g_object_unref);
	for (i = 0; i < n_pages; i++) {
		child = ev_page_accessible_new (self, i);
		g_ptr_array_add (self->priv->children, child);
	}
        /* When a document is reloaded, it may have less pages.
         * We need to update the end page accordingly to avoid
         * invalid access to self->priv->children
         * See https://bugzilla.gnome.org/show_bug.cgi?id=735744
         */
	if (self->priv->end_page >= n_pages)
		self->priv->end_page = n_pages - 1;
}

static void
document_changed_cb (EvDocumentModel  *model,
                     GParamSpec       *pspec,
                     EvViewAccessible *accessible)
{
	EvDocument *document = ev_document_model_get_document (model);

	clear_children (accessible);

	if (document == NULL)
		return;

	initialize_children (accessible);

	/* Inside this callback the document is already loaded. We
	 * don't have here an "just before" and "just after"
	 * signal. We emit both in a row, as usual ATs uses reload to
	 * know that current content has changed, and load-complete to
	 * know that the content is already available.
	 */
	g_signal_emit_by_name (accessible, "reload");
	g_signal_emit_by_name (accessible, "load-complete");
}

void
ev_view_accessible_set_model (EvViewAccessible *accessible,
                              EvDocumentModel  *model)
{
	EvViewAccessiblePrivate* priv = accessible->priv;

	if (priv->model == model)
		return;

	if (priv->model) {
		g_signal_handlers_disconnect_by_data (priv->model, accessible);
		g_object_unref (priv->model);
	}

	priv->model = g_object_ref (model);

	document_changed_cb (model, NULL, accessible);
	g_signal_connect (priv->model, "page-changed",
	                  G_CALLBACK (page_changed_cb),
	                  accessible);
	g_signal_connect (priv->model, "notify::document",
	                  G_CALLBACK (document_changed_cb),
	                  accessible);
}

static gboolean
ev_view_accessible_focus_changed (GtkWidget        *widget,
				  GdkEventFocus    *event,
				  EvViewAccessible *self)
{
	AtkObject *page_accessible;

	g_return_val_if_fail (EV_IS_VIEW (widget), FALSE);
	g_return_val_if_fail (EV_IS_VIEW_ACCESSIBLE (self), FALSE);

	if (self->priv->children == NULL || self->priv->children->len == 0)
		return FALSE;

	page_accessible = g_ptr_array_index (self->priv->children,
					     get_relevant_page (EV_VIEW (widget)));
	atk_object_notify_state_change (page_accessible,
					ATK_STATE_FOCUSED, event->in);

	return FALSE;
}

AtkObject *
ev_view_accessible_new (GtkWidget *widget)
{
	AtkObject *accessible;
	EvView    *view;

	g_return_val_if_fail (EV_IS_VIEW (widget), NULL);

	accessible = g_object_new (EV_TYPE_VIEW_ACCESSIBLE, NULL);
	atk_object_initialize (accessible, widget);

	g_signal_connect (widget, "cursor-moved",
	                  G_CALLBACK (ev_view_accessible_cursor_moved),
	                  accessible);
	g_signal_connect (widget, "selection-changed",
	                  G_CALLBACK (ev_view_accessible_selection_changed),
	                  accessible);
	g_signal_connect (widget, "focus-in-event",
			  G_CALLBACK (ev_view_accessible_focus_changed),
			  accessible);
	g_signal_connect (widget, "focus-out-event",
			  G_CALLBACK (ev_view_accessible_focus_changed),
			  accessible);

	view = EV_VIEW (widget);
	if (view->model)
		ev_view_accessible_set_model (EV_VIEW_ACCESSIBLE (accessible),
		                              view->model);

	return accessible;
}

gint
ev_view_accessible_get_relevant_page (EvViewAccessible *accessible)
{
	EvView *view;

	g_return_val_if_fail (EV_IS_VIEW_ACCESSIBLE (accessible), -1);

	view = EV_VIEW (gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible)));

	return get_relevant_page (view);
}

void
_transform_doc_rect_to_atk_rect (EvViewAccessible *accessible,
				 gint              page,
				 EvRectangle      *doc_rect,
				 EvRectangle      *atk_rect,
				 AtkCoordType      coord_type)
{
	EvView *view;
	GdkRectangle view_rect;
	GtkWidget *widget, *toplevel;
	gint x_widget, y_widget;

	view = EV_VIEW (gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible)));
	_ev_view_transform_doc_rect_to_view_rect (view, page, doc_rect, &view_rect);
	view_rect.x -= view->scroll_x;
	view_rect.y -= view->scroll_y;

	widget = GTK_WIDGET (view);
	toplevel = gtk_widget_get_toplevel (widget);
	gtk_widget_translate_coordinates (widget, toplevel, 0, 0, &x_widget, &y_widget);
	view_rect.x += x_widget;
	view_rect.y += y_widget;

	if (coord_type == ATK_XY_SCREEN) {
		gint x_window, y_window;
		gdk_window_get_origin (gtk_widget_get_window (toplevel), &x_window, &y_window);
		view_rect.x += x_window;
		view_rect.y += y_window;
	}

	atk_rect->x1 = view_rect.x;
	atk_rect->y1 = view_rect.y;
	atk_rect->x2 = view_rect.x + view_rect.width;
	atk_rect->y2 = view_rect.y + view_rect.height;
}

gboolean
ev_view_accessible_is_doc_rect_showing (EvViewAccessible *accessible,
					gint              page,
					EvRectangle      *doc_rect)
{
	EvView *view;
	GdkRectangle view_rect;
	GtkAllocation allocation;
	gint x, y;
	gboolean hidden;

	view = EV_VIEW (gtk_accessible_get_widget (GTK_ACCESSIBLE (accessible)));
	if (page < view->start_page || page > view->end_page)
		return FALSE;

	gtk_widget_get_allocation (GTK_WIDGET (view), &allocation);
	x = gtk_adjustment_get_value (view->hadjustment);
	y = gtk_adjustment_get_value (view->vadjustment);

	_ev_view_transform_doc_rect_to_view_rect (view, page, doc_rect, &view_rect);
	hidden = view_rect.x + view_rect.width < x || view_rect.x > x + allocation.width ||
		view_rect.y + view_rect.height < y || view_rect.y > y + allocation.height;

	return !hidden;
}

void
ev_view_accessible_set_page_range (EvViewAccessible *accessible,
				   gint start,
				   gint end)
{
	gint i;
	AtkObject *page;

	g_return_if_fail (EV_IS_VIEW_ACCESSIBLE (accessible));

	for (i = accessible->priv->start_page; i <= accessible->priv->end_page; i++) {
		if (i < start || i > end) {
			page = g_ptr_array_index (accessible->priv->children, i);
			atk_object_notify_state_change (page, ATK_STATE_SHOWING, FALSE);
		}
	}

	for (i = start; i <= end; i++) {
		if (i < accessible->priv->start_page || i > accessible->priv->end_page) {
			page = g_ptr_array_index (accessible->priv->children, i);
			atk_object_notify_state_change (page, ATK_STATE_SHOWING, TRUE);
		}
	}

	accessible->priv->start_page = start;
	accessible->priv->end_page = end;
}