/*
 *  Copyright (C) 2005 Marco Pesenti Gritti
 *  Copyright (C) 2018 Germán Poo-Caamaño <gpoo@gnome.org>
 *
 *  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, 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 Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include "config.h"

#include <glib/gi18n.h>
#include <string.h>
#include <stdlib.h>

#include "pps-history.h"

enum {
	CHANGED,
        ACTIVATE_LINK,

	N_SIGNALS
};

enum {
        PROP_0,
        PROP_DOCUMENT_MODEL,
	NUM_PROPERTIES
};

#define PPS_HISTORY_MAX_LENGTH (32)

static guint signals[N_SIGNALS] = {0, };
static GParamSpec *props[NUM_PROPERTIES] = { NULL, };

typedef struct {
	GList           *list;
        GList           *current;

        PpsDocumentModel *model;

        guint            frozen;
} PpsHistoryPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (PpsHistory, pps_history, G_TYPE_OBJECT)

#define GET_PRIVATE(o) pps_history_get_instance_private (o);

static void pps_history_set_model        (PpsHistory       *history,
                                         PpsDocumentModel *model);
static gint pps_history_get_current_page (PpsHistory       *history);

static void
clear_list (GList *list)
{
        g_list_free_full (list, (GDestroyNotify) g_object_unref);
}

static void
pps_history_clear (PpsHistory *history)
{
	PpsHistoryPrivate *priv = GET_PRIVATE (history);

	g_clear_pointer (&priv->list, clear_list);

        priv->current = NULL;
}

static void
pps_history_prune (PpsHistory *history)
{
	PpsHistoryPrivate *priv = GET_PRIVATE (history);
        GList *l;
        guint i;

        g_assert (priv->current->next == NULL);

        for (i = 0, l = priv->current; i < PPS_HISTORY_MAX_LENGTH && l != NULL; i++, l = l->prev)
                /* empty */;

        if (l == NULL)
                return;

        /* Throw away all history up to @l */
        l = l->next;
        l->prev->next = NULL;
        l->prev = NULL;

        clear_list (priv->list);
        priv->list = l;

        g_assert (g_list_length (priv->list) == PPS_HISTORY_MAX_LENGTH);
}

static void
pps_history_finalize (GObject *object)
{
	PpsHistory *history = PPS_HISTORY (object);

        pps_history_clear (history);
        pps_history_set_model (history, NULL);

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

static void
pps_history_set_property (GObject      *object,
				     guint         prop_id,
				     const GValue *value,
				     GParamSpec   *pspec)
{
        PpsHistory *history = PPS_HISTORY (object);

        switch (prop_id) {
        case PROP_DOCUMENT_MODEL:
                pps_history_set_model (history, g_value_get_object (value));
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        }
}

static void
pps_history_class_init (PpsHistoryClass *class)
{
	GObjectClass *object_class = G_OBJECT_CLASS (class);

	object_class->finalize = pps_history_finalize;
	object_class->set_property = pps_history_set_property;

	signals[CHANGED] =
                g_signal_new ("changed",
                              G_OBJECT_CLASS_TYPE (object_class),
                              G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                              G_STRUCT_OFFSET (PpsHistoryClass, changed),
                              NULL, NULL,
                              g_cclosure_marshal_VOID__VOID,
                              G_TYPE_NONE, 0);

        signals[ACTIVATE_LINK] =
                g_signal_new ("activate-link",
                              G_OBJECT_CLASS_TYPE (object_class),
                              G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                              G_STRUCT_OFFSET (PpsHistoryClass, activate_link),
                              NULL, NULL,
                              g_cclosure_marshal_VOID__OBJECT,
                              G_TYPE_NONE, 1,
                              G_TYPE_OBJECT);

	props[PROP_DOCUMENT_MODEL] =
        	g_param_spec_object ("document-model",
				     "DocumentModel",
				     "The document model",
				     PPS_TYPE_DOCUMENT_MODEL,
				     G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties (object_class, NUM_PROPERTIES, props);
}

static void
pps_history_init (PpsHistory *history)
{
}

gboolean
pps_history_is_frozen (PpsHistory *history)
{
	PpsHistoryPrivate *priv = GET_PRIVATE (history);

        return priv->frozen > 0;
}

void
pps_history_add_link (PpsHistory *history,
                     PpsLink    *link)
{
        PpsHistoryPrivate *priv;

	g_return_if_fail (PPS_IS_HISTORY (history));
	g_return_if_fail (PPS_IS_LINK (link));

        if (pps_history_is_frozen (history))
                return;

	priv = GET_PRIVATE (history);

        if (priv->current) {
                /* Truncate forward history at @current */
                clear_list (priv->current->next);
                priv->current->next = NULL;
        }

        /* Push @link to the list */
        priv->current = g_list_append (NULL, g_object_ref (link));
        priv->list = g_list_concat (priv->list, priv->current);

        pps_history_prune (history);

	g_signal_emit (history, signals[CHANGED], 0);
}

static void
pps_history_activate_current_link (PpsHistory *history)
{
	PpsHistoryPrivate *priv = GET_PRIVATE (history);

        g_assert (priv->current);

        pps_history_freeze (history);
        g_signal_emit (history, signals[ACTIVATE_LINK], 0, priv->current->data);
        pps_history_thaw (history);

        g_signal_emit (history, signals[CHANGED], 0);
}

gboolean
pps_history_can_go_back (PpsHistory *history)
{
        PpsHistoryPrivate *priv;

        g_return_val_if_fail (PPS_IS_HISTORY (history), FALSE);

        priv = GET_PRIVATE (history);

        if (pps_history_is_frozen (history))
                return FALSE;

        if (abs (pps_document_model_get_page (priv->model) - pps_history_get_current_page (history)) > 1)
              return TRUE;

        return priv->current && priv->current->prev;
}

void
pps_history_go_back (PpsHistory *history)
{
        PpsHistoryPrivate *priv;

        g_return_if_fail (PPS_IS_HISTORY (history));

        if (!pps_history_can_go_back (history))
                return;

        priv = GET_PRIVATE (history);

        /* Move current back one step */
        priv->current = priv->current->prev;

        pps_history_activate_current_link (history);
}

gboolean
pps_history_can_go_forward (PpsHistory *history)
{
        PpsHistoryPrivate *priv;

        g_return_val_if_fail (PPS_IS_HISTORY (history), FALSE);

        if (pps_history_is_frozen (history))
                return FALSE;

        priv = GET_PRIVATE (history);
        return priv->current && priv->current->next;
}

void
pps_history_go_forward (PpsHistory *history)
{
        PpsHistoryPrivate *priv;

        g_return_if_fail (PPS_IS_HISTORY (history));

        if (!pps_history_can_go_forward (history))
                return;

        priv = GET_PRIVATE (history);

        /* Move current forward one step */
        priv->current = priv->current->next;

        pps_history_activate_current_link (history);
}

static gint
compare_link (PpsLink *a,
              PpsLink *b)
{
        PpsLinkAction *aa, *bb;

        if (a == b)
                return 0;

        aa = pps_link_get_action (a);
        bb = pps_link_get_action (b);

        return (aa && bb && pps_link_action_equal (aa, bb)) ? 0 : 1;
}

/*
 * pps_history_go_to_link:
 * @history: a #PpsHistory
 * @link: a #PpsLink
 *
 * Goes to the link, if it is in the history.
 *
 * Returns: %TRUE if the link was in the history and history isn't frozen; %FALSE otherwise
 */
gboolean
pps_history_go_to_link (PpsHistory *history,
                       PpsLink    *link)
{
        PpsHistoryPrivate *priv;
        GList *l;

        g_return_val_if_fail (PPS_IS_HISTORY (history), FALSE);
        g_return_val_if_fail (PPS_IS_LINK (link), FALSE);

        if (pps_history_is_frozen (history))
                return FALSE;

        priv = GET_PRIVATE (history);

        l = g_list_find_custom (priv->list, link, (GCompareFunc) compare_link);
        if (l == NULL)
                return FALSE;

        /* Set the link as current */
        priv->current = l;

        pps_history_activate_current_link (history);

        return TRUE;
}

/**
 * pps_history_get_back_list:
 * @history: a #PpsHistory
 *
 * Returns: (transfer container) (element-type PpsLink): the back history
 */
GList *
pps_history_get_back_list (PpsHistory *history)
{
        PpsHistoryPrivate *priv;
        GList *list, *l;

        g_return_val_if_fail (PPS_IS_HISTORY (history), NULL);

        priv = GET_PRIVATE (history);

        if (priv->current == NULL)
                return NULL;

        list = NULL;
        for (l = priv->current->prev; l != NULL; l = l->prev)
                list = g_list_prepend (list, l->data);

        return g_list_reverse (list);
}

/**
 * pps_history_get_forward_list:
 * @history: a #PpsHistory
 *
 * Returns: (transfer container) (element-type PpsLink): the forward history
 */
GList *
pps_history_get_forward_list (PpsHistory *history)
{
        PpsHistoryPrivate *priv;

        g_return_val_if_fail (PPS_IS_HISTORY (history), NULL);

        priv = GET_PRIVATE (history);

        return g_list_copy (priv->current->next);
}

void
pps_history_freeze (PpsHistory *history)
{
        PpsHistoryPrivate *priv;

        g_return_if_fail (PPS_IS_HISTORY (history));

        priv = GET_PRIVATE (history);

        priv->frozen++;
}

void
pps_history_thaw (PpsHistory *history)
{
        PpsHistoryPrivate *priv;

        g_return_if_fail (PPS_IS_HISTORY (history));

        priv = GET_PRIVATE (history);

        g_return_if_fail (priv->frozen > 0);

        priv->frozen--;
}

static gint
pps_history_get_current_page (PpsHistory *history)
{
        PpsHistoryPrivate *priv = GET_PRIVATE (history);
        PpsLink       *link;
        PpsDocument   *document;
        PpsLinkDest   *dest;
        PpsLinkAction *action;

        if (!priv->current)
                return -1;

        link = priv->current->data;
        action = pps_link_get_action (link);
        if (!action)
                return -1;

        dest = pps_link_action_get_dest (action);
        if (!dest)
                return -1;

        switch (pps_link_dest_get_dest_type (dest)) {
        case PPS_LINK_DEST_TYPE_NAMED:
                document = pps_document_model_get_document (priv->model);
                if (!PPS_IS_DOCUMENT_LINKS (document))
                        return -1;

                return pps_document_links_find_link_page (PPS_DOCUMENT_LINKS (document),
                                                         pps_link_dest_get_named_dest (dest));
        case PPS_LINK_DEST_TYPE_PAGE_LABEL: {
                gint page = -1;

                document = pps_document_model_get_document (priv->model);
                pps_document_find_page_by_label (document,
                                                pps_link_dest_get_page_label (dest),
                                                &page);

                return page;
        }
        default:
                return pps_link_dest_get_page (dest);
        }

        return -1;
}

void
pps_history_add_page (PpsHistory *history,
                     gint       page)
{
        PpsHistoryPrivate *priv = GET_PRIVATE (history);
        PpsDocument   *document;
        PpsLinkDest   *dest;
        PpsLinkAction *action;
        PpsLink       *link;
        gchar        *page_label;
        gchar        *title;

        if (pps_history_is_frozen (history))
                return;

        if (pps_history_get_current_page (history) == page)
                return;

        document = pps_document_model_get_document (priv->model);
        if (!document)
                return;

        page_label = pps_document_get_page_label (document, page);
        if (!page_label)
                return;

        title = g_strdup_printf (_("Page %s"), page_label);
        g_free (page_label);

        dest = pps_link_dest_new_page (page);
        action = pps_link_action_new_dest (dest);
        g_object_unref (dest);

        link = pps_link_new (title, action);
        g_object_unref (action);
        g_free (title);

        pps_history_add_link (history, link);
        g_object_unref (link);
}

static void
document_changed_cb (PpsDocumentModel *model,
                     GParamSpec      *pspec,
                     PpsHistory       *history)
{
        pps_history_clear (history);
        pps_history_add_page (history, pps_document_model_get_page (model));
}

static void
pps_history_set_model (PpsHistory       *history,
                      PpsDocumentModel *model)
{
        PpsHistoryPrivate *priv = GET_PRIVATE (history);

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

        if (priv->model) {
                g_object_remove_weak_pointer (G_OBJECT (priv->model),
                                              (gpointer)&priv->model);
        }

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

        g_object_add_weak_pointer (G_OBJECT (model),
                                   (gpointer)&priv->model);

        g_signal_connect (priv->model, "notify::document",
                          G_CALLBACK (document_changed_cb),
                          history);
}

PpsHistory *
pps_history_new (PpsDocumentModel *model)
{
        PpsHistory *history;

        g_return_val_if_fail (PPS_IS_DOCUMENT_MODEL (model), NULL);

        history = PPS_HISTORY (g_object_new (PPS_TYPE_HISTORY, NULL));
        pps_history_set_model (history, model);

        return history;
}
