/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */

/* caja-bookmark.c - implementation of individual bookmarks.

   Copyright (C) 1999, 2000 Eazel, Inc.

   The Mate Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public License as
   published by the Free Software Foundation; either version 2 of the
   License, or (at your option) any later version.

   The Mate Library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public
   License along with the Mate Library; see the file COPYING.LIB.  If not,
   write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
   Boston, MA 02110-1301, USA.

   Authors: John Sullivan <sullivan@eazel.com>
*/

#include <config.h>
#include "caja-bookmark.h"

#include "caja-file.h"
#include <eel/eel-gdk-pixbuf-extensions.h>
#include <eel/eel-gtk-extensions.h>
#include <eel/eel-gtk-macros.h>
#include <eel/eel-vfs-extensions.h>
#include <gtk/gtk.h>
#include <gio/gio.h>
#include <string.h>
#include <libcaja-private/caja-file.h>
#include <libcaja-private/caja-icon-names.h>

enum
{
    APPEARANCE_CHANGED,
    CONTENTS_CHANGED,
    LAST_SIGNAL
};

#define ELLIPSISED_MENU_ITEM_MIN_CHARS  32

static guint signals[LAST_SIGNAL];

struct CajaBookmarkDetails
{
    char *name;
    gboolean has_custom_name;
    GFile *location;
    GIcon *icon;
    CajaFile *file;

    char *scroll_file;
};

static void	  caja_bookmark_connect_file	  (CajaBookmark	 *file);
static void	  caja_bookmark_disconnect_file	  (CajaBookmark	 *file);

G_DEFINE_TYPE (CajaBookmark, caja_bookmark, G_TYPE_OBJECT);

/* GObject methods.  */

static void
caja_bookmark_finalize (GObject *object)
{
    CajaBookmark *bookmark;

    g_assert (CAJA_IS_BOOKMARK (object));

    bookmark = CAJA_BOOKMARK (object);

    caja_bookmark_disconnect_file (bookmark);

    g_free (bookmark->details->name);
    g_object_unref (bookmark->details->location);
    if (bookmark->details->icon)
    {
        g_object_unref (bookmark->details->icon);
    }
    g_free (bookmark->details->scroll_file);
    g_free (bookmark->details);

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

/* Initialization.  */

static void
caja_bookmark_class_init (CajaBookmarkClass *class)
{
    G_OBJECT_CLASS (class)->finalize = caja_bookmark_finalize;

    signals[APPEARANCE_CHANGED] =
        g_signal_new ("appearance_changed",
                      G_TYPE_FROM_CLASS (class),
                      G_SIGNAL_RUN_LAST,
                      G_STRUCT_OFFSET (CajaBookmarkClass, appearance_changed),
                      NULL, NULL,
                      g_cclosure_marshal_VOID__VOID,
                      G_TYPE_NONE, 0);

    signals[CONTENTS_CHANGED] =
        g_signal_new ("contents_changed",
                      G_TYPE_FROM_CLASS (class),
                      G_SIGNAL_RUN_LAST,
                      G_STRUCT_OFFSET (CajaBookmarkClass, contents_changed),
                      NULL, NULL,
                      g_cclosure_marshal_VOID__VOID,
                      G_TYPE_NONE, 0);

}

static void
caja_bookmark_init (CajaBookmark *bookmark)
{
    bookmark->details = g_new0 (CajaBookmarkDetails, 1);
}

/**
 * caja_bookmark_compare_with:
 *
 * Check whether two bookmarks are considered identical.
 * @a: first CajaBookmark*.
 * @b: second CajaBookmark*.
 *
 * Return value: 0 if @a and @b have same name and uri, 1 otherwise
 * (GCompareFunc style)
 **/
int
caja_bookmark_compare_with (gconstpointer a, gconstpointer b)
{
    CajaBookmark *bookmark_a;
    CajaBookmark *bookmark_b;

    g_return_val_if_fail (CAJA_IS_BOOKMARK (a), 1);
    g_return_val_if_fail (CAJA_IS_BOOKMARK (b), 1);

    bookmark_a = CAJA_BOOKMARK (a);
    bookmark_b = CAJA_BOOKMARK (b);

    if (g_strcmp0 (bookmark_a->details->name,
                    bookmark_b->details->name) != 0)
    {
        return 1;
    }

    if (!g_file_equal (bookmark_a->details->location,
                       bookmark_b->details->location))
    {
        return 1;
    }

    return 0;
}

/**
 * caja_bookmark_compare_uris:
 *
 * Check whether the uris of two bookmarks are for the same location.
 * @a: first CajaBookmark*.
 * @b: second CajaBookmark*.
 *
 * Return value: 0 if @a and @b have matching uri, 1 otherwise
 * (GCompareFunc style)
 **/
int
caja_bookmark_compare_uris (gconstpointer a, gconstpointer b)
{
    CajaBookmark *bookmark_a;
    CajaBookmark *bookmark_b;

    g_return_val_if_fail (CAJA_IS_BOOKMARK (a), 1);
    g_return_val_if_fail (CAJA_IS_BOOKMARK (b), 1);

    bookmark_a = CAJA_BOOKMARK (a);
    bookmark_b = CAJA_BOOKMARK (b);

    return !g_file_equal (bookmark_a->details->location,
                          bookmark_b->details->location);
}

CajaBookmark *
caja_bookmark_copy (CajaBookmark *bookmark)
{
    g_return_val_if_fail (CAJA_IS_BOOKMARK (bookmark), NULL);

    return caja_bookmark_new (
               bookmark->details->location,
               bookmark->details->name,
               bookmark->details->has_custom_name,
               bookmark->details->icon);
}

char *
caja_bookmark_get_name (CajaBookmark *bookmark)
{
    g_return_val_if_fail(CAJA_IS_BOOKMARK (bookmark), NULL);

    return g_strdup (bookmark->details->name);
}


gboolean
caja_bookmark_get_has_custom_name (CajaBookmark *bookmark)
{
    g_return_val_if_fail(CAJA_IS_BOOKMARK (bookmark), FALSE);

    return (bookmark->details->has_custom_name);
}


GdkPixbuf *
caja_bookmark_get_pixbuf (CajaBookmark *bookmark,
                          GtkIconSize stock_size)
{
    GdkPixbuf *result;
    GIcon *icon;
    CajaIconInfo *info;
    int pixel_size;


    g_return_val_if_fail (CAJA_IS_BOOKMARK (bookmark), NULL);

    icon = caja_bookmark_get_icon (bookmark);
    if (icon == NULL)
    {
        return NULL;
    }

    pixel_size = caja_get_icon_size_for_stock_size (stock_size);
    info = caja_icon_info_lookup (icon, pixel_size);
    result = caja_icon_info_get_pixbuf_at_size (info, pixel_size);
    g_object_unref (info);

    g_object_unref (icon);

    return result;
}

GIcon *
caja_bookmark_get_icon (CajaBookmark *bookmark)
{
    g_return_val_if_fail (CAJA_IS_BOOKMARK (bookmark), NULL);

    /* Try to connect a file in case file exists now but didn't earlier. */
    caja_bookmark_connect_file (bookmark);

    if (bookmark->details->icon)
    {
        return g_object_ref (bookmark->details->icon);
    }
    return NULL;
}

GFile *
caja_bookmark_get_location (CajaBookmark *bookmark)
{
    g_return_val_if_fail(CAJA_IS_BOOKMARK (bookmark), NULL);

    /* Try to connect a file in case file exists now but didn't earlier.
     * This allows a bookmark to update its image properly in the case
     * where a new file appears with the same URI as a previously-deleted
     * file. Calling connect_file here means that attempts to activate the
     * bookmark will update its image if possible.
     */
    caja_bookmark_connect_file (bookmark);

    return g_object_ref (bookmark->details->location);
}

char *
caja_bookmark_get_uri (CajaBookmark *bookmark)
{
    GFile *file;
    char *uri;

    file = caja_bookmark_get_location (bookmark);
    uri = g_file_get_uri (file);
    g_object_unref (file);
    return uri;
}


/**
 * caja_bookmark_set_name:
 *
 * Change the user-displayed name of a bookmark.
 * @new_name: The new user-displayed name for this bookmark, mustn't be NULL.
 *
 * Returns: TRUE if the name changed else FALSE.
 **/
gboolean
caja_bookmark_set_name (CajaBookmark *bookmark, const char *new_name)
{
    g_return_val_if_fail (new_name != NULL, FALSE);
    g_return_val_if_fail (CAJA_IS_BOOKMARK (bookmark), FALSE);

    if (strcmp (new_name, bookmark->details->name) == 0)
    {
        return FALSE;
    }
    else if (!bookmark->details->has_custom_name)
    {
        bookmark->details->has_custom_name = TRUE;
    }

    g_free (bookmark->details->name);
    bookmark->details->name = g_strdup (new_name);

    g_signal_emit (bookmark, signals[APPEARANCE_CHANGED], 0);

    if (bookmark->details->has_custom_name)
    {
        g_signal_emit (bookmark, signals[CONTENTS_CHANGED], 0);
    }

    return TRUE;
}

static gboolean
caja_bookmark_icon_is_different (CajaBookmark *bookmark,
                                 GIcon *new_icon)
{
    g_assert (CAJA_IS_BOOKMARK (bookmark));
    g_assert (new_icon != NULL);

    if (bookmark->details->icon == NULL)
    {
        return TRUE;
    }

    return !g_icon_equal (bookmark->details->icon, new_icon) != 0;
}

/**
 * Update icon if there's a better one available.
 * Return TRUE if the icon changed.
 */
static gboolean
caja_bookmark_update_icon (CajaBookmark *bookmark)
{
    GIcon *new_icon;

    g_assert (CAJA_IS_BOOKMARK (bookmark));

    if (bookmark->details->file == NULL)
    {
        return FALSE;
    }

    if (!caja_file_is_local (bookmark->details->file))
    {
        /* never update icons for remote bookmarks */
        return FALSE;
    }

    if (!caja_file_is_not_yet_confirmed (bookmark->details->file) &&
            caja_file_check_if_ready (bookmark->details->file,
                                      CAJA_FILE_ATTRIBUTES_FOR_ICON))
    {
        new_icon = caja_file_get_gicon (bookmark->details->file, 0);
        if (caja_bookmark_icon_is_different (bookmark, new_icon))
        {
            if (bookmark->details->icon)
            {
                g_object_unref (bookmark->details->icon);
            }
            bookmark->details->icon = new_icon;
            return TRUE;
        }
        g_object_unref (new_icon);
    }

    return FALSE;
}

static void
bookmark_file_changed_callback (CajaFile *file, CajaBookmark *bookmark)
{
    GFile *location;
    gboolean should_emit_appearance_changed_signal;
    gboolean should_emit_contents_changed_signal;
    char *display_name;

    g_assert (CAJA_IS_FILE (file));
    g_assert (CAJA_IS_BOOKMARK (bookmark));
    g_assert (file == bookmark->details->file);

    should_emit_appearance_changed_signal = FALSE;
    should_emit_contents_changed_signal = FALSE;
    location = caja_file_get_location (file);

    if (!g_file_equal (bookmark->details->location, location) &&
            !caja_file_is_in_trash (file))
    {
        g_object_unref (bookmark->details->location);
        bookmark->details->location = location;
        should_emit_contents_changed_signal = TRUE;
    }
    else
    {
        g_object_unref (location);
    }

    if (caja_file_is_gone (file) ||
            caja_file_is_in_trash (file))
    {
        /* The file we were monitoring has been trashed, deleted,
         * or moved in a way that we didn't notice. We should make
         * a spanking new CajaFile object for this
         * location so if a new file appears in this place
         * we will notice. However, we can't immediately do so
         * because creating a new CajaFile directly as a result
         * of noticing a file goes away may trigger i/o on that file
         * again, noticeing it is gone, leading to a loop.
         * So, the new CajaFile is created when the bookmark
         * is used again. However, this is not really a problem, as
         * we don't want to change the icon or anything about the
         * bookmark just because its not there anymore.
         */
        caja_bookmark_disconnect_file (bookmark);
    }
    else if (caja_bookmark_update_icon (bookmark))
    {
        /* File hasn't gone away, but it has changed
         * in a way that affected its icon.
         */
        should_emit_appearance_changed_signal = TRUE;
    }

    if (!bookmark->details->has_custom_name)
    {
        display_name = caja_file_get_display_name (file);

        if (strcmp (bookmark->details->name, display_name) != 0)
        {
            g_free (bookmark->details->name);
            bookmark->details->name = display_name;
            should_emit_appearance_changed_signal = TRUE;
        }
        else
        {
            g_free (display_name);
        }
    }

    if (should_emit_appearance_changed_signal)
    {
        g_signal_emit (bookmark, signals[APPEARANCE_CHANGED], 0);
    }

    if (should_emit_contents_changed_signal)
    {
        g_signal_emit (bookmark, signals[CONTENTS_CHANGED], 0);
    }
}

/**
 * caja_bookmark_set_icon_to_default:
 *
 * Reset the icon to either the missing bookmark icon or the generic
 * bookmark icon, depending on whether the file still exists.
 */
static void
caja_bookmark_set_icon_to_default (CajaBookmark *bookmark)
{
    GIcon *icon, *emblemed_icon, *folder;
    GEmblem *emblem;

    if (bookmark->details->icon)
    {
        g_object_unref (bookmark->details->icon);
    }

    folder = g_themed_icon_new (CAJA_ICON_FOLDER);

    if (caja_bookmark_uri_known_not_to_exist (bookmark))
    {
        icon = g_themed_icon_new (GTK_STOCK_DIALOG_WARNING);
        emblem = g_emblem_new (icon);

        emblemed_icon = g_emblemed_icon_new (folder, emblem);

        g_object_unref (emblem);
        g_object_unref (icon);
        g_object_unref (folder);

        folder = emblemed_icon;
    }

    bookmark->details->icon = folder;
}

static void
caja_bookmark_disconnect_file (CajaBookmark *bookmark)
{
    g_assert (CAJA_IS_BOOKMARK (bookmark));

    if (bookmark->details->file != NULL)
    {
        g_signal_handlers_disconnect_by_func (bookmark->details->file,
                                              G_CALLBACK (bookmark_file_changed_callback),
                                              bookmark);
        caja_file_unref (bookmark->details->file);
        bookmark->details->file = NULL;
    }

    if (bookmark->details->icon != NULL)
    {
        g_object_unref (bookmark->details->icon);
        bookmark->details->icon = NULL;
    }
}

static void
caja_bookmark_connect_file (CajaBookmark *bookmark)
{
    char *display_name;

    g_assert (CAJA_IS_BOOKMARK (bookmark));

    if (bookmark->details->file != NULL)
    {
        return;
    }

    if (!caja_bookmark_uri_known_not_to_exist (bookmark))
    {
        bookmark->details->file = caja_file_get (bookmark->details->location);
        g_assert (!caja_file_is_gone (bookmark->details->file));

        g_signal_connect_object (bookmark->details->file, "changed",
                                 G_CALLBACK (bookmark_file_changed_callback), bookmark, 0);
    }

    /* Set icon based on available information; don't force network i/o
     * to get any currently unknown information.
     */
    if (!caja_bookmark_update_icon (bookmark))
    {
        if (bookmark->details->icon == NULL || bookmark->details->file == NULL)
        {
            caja_bookmark_set_icon_to_default (bookmark);
        }
    }

    if (!bookmark->details->has_custom_name &&
            bookmark->details->file &&
            caja_file_check_if_ready (bookmark->details->file, CAJA_FILE_ATTRIBUTE_INFO))
    {
        display_name = caja_file_get_display_name (bookmark->details->file);
        if (strcmp (bookmark->details->name, display_name) != 0)
        {
            g_free (bookmark->details->name);
            bookmark->details->name = display_name;
        }
        else
        {
            g_free (display_name);
        }
    }
}

CajaBookmark *
caja_bookmark_new (GFile *location, const char *name, gboolean has_custom_name,
                   GIcon *icon)
{
    CajaBookmark *new_bookmark;

    new_bookmark = CAJA_BOOKMARK (g_object_new (CAJA_TYPE_BOOKMARK, NULL));
    g_object_ref_sink (new_bookmark);

    new_bookmark->details->name = g_strdup (name);
    new_bookmark->details->location = g_object_ref (location);
    new_bookmark->details->has_custom_name = has_custom_name;
    if (icon)
    {
        new_bookmark->details->icon = g_object_ref (icon);
    }

    caja_bookmark_connect_file (new_bookmark);

    return new_bookmark;
}

static GtkWidget *
create_image_widget_for_bookmark (CajaBookmark *bookmark)
{
    GdkPixbuf *pixbuf;
    GtkWidget *widget;

    pixbuf = caja_bookmark_get_pixbuf (bookmark, GTK_ICON_SIZE_MENU);
    if (pixbuf == NULL)
    {
        return NULL;
    }

    widget = gtk_image_new_from_pixbuf (pixbuf);

    g_object_unref (pixbuf);
    return widget;
}

/**
 * caja_bookmark_menu_item_new:
 *
 * Return a menu item representing a bookmark.
 * @bookmark: The bookmark the menu item represents.
 * Return value: A newly-created bookmark, not yet shown.
 **/
GtkWidget *
caja_bookmark_menu_item_new (CajaBookmark *bookmark)
{
    GtkWidget *menu_item;
    GtkWidget *image_widget;
    GtkLabel *label;

    menu_item = gtk_image_menu_item_new_with_label (bookmark->details->name);
    label = GTK_LABEL (gtk_bin_get_child (GTK_BIN (menu_item)));
    gtk_label_set_use_underline (label, FALSE);
    gtk_label_set_ellipsize (label, PANGO_ELLIPSIZE_END);
    gtk_label_set_max_width_chars (label, ELLIPSISED_MENU_ITEM_MIN_CHARS);

    image_widget = create_image_widget_for_bookmark (bookmark);
    if (image_widget != NULL)
    {
        gtk_widget_show (image_widget);
        gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item),
                                       image_widget);
    }

    return menu_item;
}

gboolean
caja_bookmark_uri_known_not_to_exist (CajaBookmark *bookmark)
{
    char *path_name;
    gboolean exists;

    /* Convert to a path, returning FALSE if not local. */
    if (!g_file_is_native (bookmark->details->location))
    {
        return FALSE;
    }
    path_name = g_file_get_path (bookmark->details->location);

    /* Now check if the file exists (sync. call OK because it is local). */
    exists = g_file_test (path_name, G_FILE_TEST_EXISTS);
    g_free (path_name);

    return !exists;
}

void
caja_bookmark_set_scroll_pos (CajaBookmark      *bookmark,
                              const char            *uri)
{
    g_free (bookmark->details->scroll_file);
    bookmark->details->scroll_file = g_strdup (uri);
}

char *
caja_bookmark_get_scroll_pos (CajaBookmark      *bookmark)
{
    return g_strdup (bookmark->details->scroll_file);
}