/* -*- Mode: C; c-file-style: "gnu"; tab-width: 8 -*- */
/* GTK - The GIMP Toolkit
 * gtkbookmarksmanager.c: Utilities to manage and monitor ~/.gtk-bookmarks
 * Copyright (C) 2003, Red Hat, Inc.
 * Copyright (C) 2007-2008 Carlos Garnacho
 * Copyright (C) 2011 Suse
 *
 * 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 Lesser General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Federico Mena Quintero <federico@gnome.org>
 */

#include "config.h"

#include <string.h>

#include <glib/gi18n-lib.h>

#include "gtkbookmarksmanagerprivate.h"
#include "gtkfilechooser.h" /* for the GError types */

static void
_gtk_bookmark_free (gpointer data)
{
  GtkBookmark *bookmark = data;

  g_object_unref (bookmark->file);
  g_free (bookmark->label);
  g_slice_free (GtkBookmark, bookmark);
}

static void
set_error_bookmark_doesnt_exist (GFile *file, GError **error)
{
  char *uri = g_file_get_uri (file);

  g_set_error (error,
               GTK_FILE_CHOOSER_ERROR,
               GTK_FILE_CHOOSER_ERROR_NONEXISTENT,
               _("%s does not exist in the bookmarks list"),
               uri);

  g_free (uri);
}

static GFile *
get_legacy_bookmarks_file (void)
{
  GFile *file;
  char *filename;

  filename = g_build_filename (g_get_home_dir (), ".gtk-bookmarks", NULL);
  file = g_file_new_for_path (filename);
  g_free (filename);

  return file;
}

static GFile *
get_bookmarks_file (void)
{
  GFile *file;
  char *filename;

  /* Use gtk-3.0's bookmarks file as the format didn't change.
   * Add the 3.0 file format to get_legacy_bookmarks_file() when
   * the format does change.
   */
  filename = g_build_filename (g_get_user_config_dir (), "gtk-3.0", "bookmarks", NULL);
  file = g_file_new_for_path (filename);
  g_free (filename);

  return file;
}

static GSList *
parse_bookmarks (const char *contents)
{
  char **lines, *space;
  GSList *bookmarks = NULL;
  int i;

  lines = g_strsplit (contents, "\n", -1);

  for (i = 0; lines[i]; i++)
    {
      GtkBookmark *bookmark;

      if (!*lines[i])
	continue;

      if (!g_utf8_validate (lines[i], -1, NULL))
	continue;

      bookmark = g_slice_new0 (GtkBookmark);

      if ((space = strchr (lines[i], ' ')) != NULL)
	{
	  space[0] = '\0';
	  bookmark->label = g_strdup (space + 1);
	}

      bookmark->file = g_file_new_for_uri (lines[i]);
      bookmarks = g_slist_prepend (bookmarks, bookmark);
    }

  bookmarks = g_slist_reverse (bookmarks);
  g_strfreev (lines);

  return bookmarks;
}

static GSList *
read_bookmarks (GFile *file)
{
  char *contents;
  GSList *bookmarks = NULL;

  if (!g_file_load_contents (file, NULL, &contents,
			     NULL, NULL, NULL))
    return NULL;

  bookmarks = parse_bookmarks (contents);

  g_free (contents);

  return bookmarks;
}

static void
notify_changed (GtkBookmarksManager *manager)
{
  if (manager->changed_func)
    manager->changed_func (manager->changed_func_data);
}

static void
read_bookmarks_finish (GObject      *source,
                       GAsyncResult *result,
                       gpointer      data)
{
  GFile *file = G_FILE (source);
  GtkBookmarksManager *manager = data;
  char *contents = NULL;
  GError *error = NULL;

  if (!g_file_load_contents_finish (file, result, &contents, NULL, NULL, &error)) 
    {
      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        g_warning ("Failed to load '%s': %s", g_file_peek_path (file), error->message);
      g_error_free (error);
      return;
    }

  g_slist_free_full (manager->bookmarks, _gtk_bookmark_free);
  manager->bookmarks = parse_bookmarks (contents);

  g_free (contents);

  notify_changed (manager);
}

static void
save_bookmarks (GFile  *bookmarks_file,
		GSList *bookmarks)
{
  GError *error = NULL;
  GString *contents;
  GSList *l;
  GFile *parent = NULL;

  contents = g_string_new ("");

  for (l = bookmarks; l; l = l->next)
    {
      GtkBookmark *bookmark = l->data;
      char *uri;

      uri = g_file_get_uri (bookmark->file);
      if (!uri)
	continue;

      g_string_append (contents, uri);

      if (bookmark->label && g_utf8_validate (bookmark->label, -1, NULL))
	g_string_append_printf (contents, " %s", bookmark->label);

      g_string_append_c (contents, '\n');
      g_free (uri);
    }

  parent = g_file_get_parent (bookmarks_file);
  if (!g_file_make_directory_with_parents (parent, NULL, &error))
    {
       if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
	 g_clear_error (&error);
       else
	 goto out;
    }
  if (!g_file_replace_contents (bookmarks_file,
				contents->str,
				contents->len,
				NULL, FALSE, 0, NULL,
				NULL, &error))
    goto out;

 out:
  if (error)
    {
      g_critical ("%s", error->message);
      g_error_free (error);
    }
  g_clear_object (&parent);
  g_string_free (contents, TRUE);
}

static void
bookmarks_file_changed (GFileMonitor      *monitor,
			GFile             *file,
			GFile             *other_file,
			GFileMonitorEvent  event,
			gpointer           data)
{
  GtkBookmarksManager *manager = data;

  switch (event)
    {
    case G_FILE_MONITOR_EVENT_CHANGED:
    case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
    case G_FILE_MONITOR_EVENT_CREATED:
      g_file_load_contents_async (file, manager->cancellable, read_bookmarks_finish, manager);
      break;

    case G_FILE_MONITOR_EVENT_DELETED:
    case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
    case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
    case G_FILE_MONITOR_EVENT_UNMOUNTED:
    case G_FILE_MONITOR_EVENT_MOVED:
    case G_FILE_MONITOR_EVENT_RENAMED:
    case G_FILE_MONITOR_EVENT_MOVED_IN:
    case G_FILE_MONITOR_EVENT_MOVED_OUT:
    default:
      /* ignore at the moment */
      break;
    }
}

GtkBookmarksManager *
_gtk_bookmarks_manager_new (GtkBookmarksChangedFunc changed_func, gpointer changed_func_data)
{
  GtkBookmarksManager *manager;
  GFile *bookmarks_file;
  GError *error;

  manager = g_new0 (GtkBookmarksManager, 1);

  manager->changed_func = changed_func;
  manager->changed_func_data = changed_func_data;

  manager->cancellable = g_cancellable_new ();

  bookmarks_file = get_bookmarks_file ();
  if (!g_file_query_exists (bookmarks_file, NULL))
    {
      GFile *legacy_bookmarks_file;

      /* Read the legacy one and write it to the new one */
      legacy_bookmarks_file = get_legacy_bookmarks_file ();
      manager->bookmarks = read_bookmarks (legacy_bookmarks_file);
      if (manager->bookmarks)
	save_bookmarks (bookmarks_file, manager->bookmarks);

      g_object_unref (legacy_bookmarks_file);
    }
  else
    g_file_load_contents_async (bookmarks_file, manager->cancellable, read_bookmarks_finish, manager);

  error = NULL;
  manager->bookmarks_monitor = g_file_monitor_file (bookmarks_file,
						    G_FILE_MONITOR_NONE,
						    NULL, &error);
  if (error)
    {
      g_warning ("%s", error->message);
      g_error_free (error);
    }
  else
    manager->bookmarks_monitor_changed_id = g_signal_connect (manager->bookmarks_monitor, "changed",
							      G_CALLBACK (bookmarks_file_changed), manager);


  g_object_unref (bookmarks_file);

  return manager;
}

void
_gtk_bookmarks_manager_free (GtkBookmarksManager *manager)
{
  g_return_if_fail (manager != NULL);

  g_cancellable_cancel (manager->cancellable);
  g_object_unref (manager->cancellable);

  if (manager->bookmarks_monitor)
    {
      g_file_monitor_cancel (manager->bookmarks_monitor);
      g_signal_handler_disconnect (manager->bookmarks_monitor, manager->bookmarks_monitor_changed_id);
      manager->bookmarks_monitor_changed_id = 0;
      g_object_unref (manager->bookmarks_monitor);
    }

  g_slist_free_full (manager->bookmarks, _gtk_bookmark_free);

  g_free (manager);
}

GSList *
_gtk_bookmarks_manager_list_bookmarks (GtkBookmarksManager *manager)
{
  GSList *bookmarks, *files = NULL;

  g_return_val_if_fail (manager != NULL, NULL);

  bookmarks = manager->bookmarks;

  while (bookmarks)
    {
      GtkBookmark *bookmark;

      bookmark = bookmarks->data;
      bookmarks = bookmarks->next;

      files = g_slist_prepend (files, g_object_ref (bookmark->file));
    }

  return g_slist_reverse (files);
}

static GSList *
find_bookmark_link_for_file (GSList *bookmarks, GFile *file, int *position_ret)
{
  int pos;

  pos = 0;
  for (; bookmarks; bookmarks = bookmarks->next)
    {
      GtkBookmark *bookmark = bookmarks->data;

      if (g_file_equal (file, bookmark->file))
	{
	  if (position_ret)
	    *position_ret = pos;

	  return bookmarks;
	}

      pos++;
    }

  if (position_ret)
    *position_ret = -1;

  return NULL;
}

gboolean
_gtk_bookmarks_manager_has_bookmark (GtkBookmarksManager *manager,
                                     GFile               *file)
{
  GSList *link;

  link = find_bookmark_link_for_file (manager->bookmarks, file, NULL);
  return (link != NULL);
}

gboolean
_gtk_bookmarks_manager_insert_bookmark (GtkBookmarksManager *manager,
					GFile               *file,
					int                  position,
					GError             **error)
{
  GSList *link;
  GtkBookmark *bookmark;
  GFile *bookmarks_file;

  g_return_val_if_fail (manager != NULL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  link = find_bookmark_link_for_file (manager->bookmarks, file, NULL);

  if (link)
    {
      char *uri;
      bookmark = link->data;
      uri = g_file_get_uri (bookmark->file);

      g_set_error (error,
		   GTK_FILE_CHOOSER_ERROR,
		   GTK_FILE_CHOOSER_ERROR_ALREADY_EXISTS,
		   _("%s already exists in the bookmarks list"),
		   uri);

      g_free (uri);

      return FALSE;
    }

  bookmark = g_slice_new0 (GtkBookmark);
  bookmark->file = g_object_ref (file);

  manager->bookmarks = g_slist_insert (manager->bookmarks, bookmark, position);

  bookmarks_file = get_bookmarks_file ();
  save_bookmarks (bookmarks_file, manager->bookmarks);
  g_object_unref (bookmarks_file);

  notify_changed (manager);

  return TRUE;
}

gboolean
_gtk_bookmarks_manager_remove_bookmark (GtkBookmarksManager *manager,
					GFile               *file,
					GError             **error)
{
  GSList *link;
  GFile *bookmarks_file;

  g_return_val_if_fail (manager != NULL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  if (!manager->bookmarks)
    return FALSE;

  link = find_bookmark_link_for_file (manager->bookmarks, file, NULL);
  if (link)
    {
      GtkBookmark *bookmark = link->data;

      manager->bookmarks = g_slist_remove_link (manager->bookmarks, link);
      _gtk_bookmark_free (bookmark);
      g_slist_free_1 (link);
    }
  else
    {
      set_error_bookmark_doesnt_exist (file, error);
      return FALSE;
    }

  bookmarks_file = get_bookmarks_file ();
  save_bookmarks (bookmarks_file, manager->bookmarks);
  g_object_unref (bookmarks_file);

  notify_changed (manager);

  return TRUE;
}

gboolean
_gtk_bookmarks_manager_reorder_bookmark (GtkBookmarksManager *manager,
					 GFile               *file,
					 int                  new_position,
					 GError             **error)
{
  GSList *link;
  GFile *bookmarks_file;
  int old_position;

  g_return_val_if_fail (manager != NULL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
  g_return_val_if_fail (new_position >= 0, FALSE);

  if (!manager->bookmarks)
    return FALSE;

  link = find_bookmark_link_for_file (manager->bookmarks, file, &old_position);
  if (new_position == old_position)
    return TRUE;

  if (link)
    {
      GtkBookmark *bookmark = link->data; 

      manager->bookmarks = g_slist_remove_link (manager->bookmarks, link);
      g_slist_free_1 (link);

      if (new_position > old_position)
	new_position--;

      manager->bookmarks = g_slist_insert (manager->bookmarks, bookmark, new_position);
    }
  else
    {
      set_error_bookmark_doesnt_exist (file, error);
      return FALSE;
    }

  bookmarks_file = get_bookmarks_file ();
  save_bookmarks (bookmarks_file, manager->bookmarks);
  g_object_unref (bookmarks_file);

  notify_changed (manager);

  return TRUE;
}

char *
_gtk_bookmarks_manager_get_bookmark_label (GtkBookmarksManager *manager,
					   GFile               *file)
{
  GSList *bookmarks;
  char *label = NULL;

  g_return_val_if_fail (manager != NULL, NULL);
  g_return_val_if_fail (file != NULL, NULL);

  bookmarks = manager->bookmarks;

  while (bookmarks)
    {
      GtkBookmark *bookmark;

      bookmark = bookmarks->data;
      bookmarks = bookmarks->next;

      if (g_file_equal (file, bookmark->file))
	{
	  label = g_strdup (bookmark->label);
	  break;
	}
    }

  return label;
}

gboolean
_gtk_bookmarks_manager_set_bookmark_label (GtkBookmarksManager *manager,
					   GFile               *file,
					   const char          *label,
					   GError             **error)
{
  GFile *bookmarks_file;
  GSList *link;

  g_return_val_if_fail (manager != NULL, FALSE);
  g_return_val_if_fail (file != NULL, FALSE);

  link = find_bookmark_link_for_file (manager->bookmarks, file, NULL);
  if (link)
    {
      GtkBookmark *bookmark = link->data;

      g_free (bookmark->label);
      bookmark->label = g_strdup (label);
    }
  else
    {
      set_error_bookmark_doesnt_exist (file, error);
      return FALSE;
    }

  bookmarks_file = get_bookmarks_file ();
  save_bookmarks (bookmarks_file, manager->bookmarks);
  g_object_unref (bookmarks_file);

  notify_changed (manager);

  return TRUE;
}

static gboolean
_gtk_bookmarks_manager_get_xdg_type (GtkBookmarksManager *manager,
                                     GFile               *file,
                                     GUserDirectory      *directory)
{
  GSList *link;
  gboolean match;
  GFile *location;
  const char *path;
  GUserDirectory dir;
  GtkBookmark *bookmark;

  link = find_bookmark_link_for_file (manager->bookmarks, file, NULL);
  if (!link)
    return FALSE;

  match = FALSE;
  bookmark = link->data;

  for (dir = 0; dir < G_USER_N_DIRECTORIES; dir++)
    {
      path = g_get_user_special_dir (dir);
      if (!path)
        continue;

      location = g_file_new_for_path (path);
      match = g_file_equal (location, bookmark->file);
      g_object_unref (location);

      if (match)
        break;
    }

  if (match && directory != NULL)
    *directory = dir;

  return match;
}

gboolean
_gtk_bookmarks_manager_get_is_builtin (GtkBookmarksManager *manager,
                                       GFile               *file)
{
  GUserDirectory xdg_type;

  /* if this is not an XDG dir, it's never builtin */
  if (!_gtk_bookmarks_manager_get_xdg_type (manager, file, &xdg_type))
    return FALSE;

  /* exclude XDG locations we don't display by default */
  return _gtk_bookmarks_manager_get_is_xdg_dir_builtin (xdg_type);
}

gboolean
_gtk_bookmarks_manager_get_is_xdg_dir_builtin (GUserDirectory xdg_type)
{
  return (xdg_type != G_USER_DIRECTORY_DESKTOP) &&
    (xdg_type != G_USER_DIRECTORY_TEMPLATES) &&
    (xdg_type != G_USER_DIRECTORY_PUBLIC_SHARE);
}
