/*
 * Copyright (C) 2007 Intel
 * Copyright (C) 2008 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as 
 * published by the Free Software Foundation.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 * Authored by Neil Jagdish Patel <njp@o-hand.com>
 *             Neil Jagdish Patel <neil.patel@canonical.com>
 *
 */

#include <glib.h>

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

#define GMENU_I_KNOW_THIS_IS_UNSTABLE 1
#include <gmenu-tree.h>

#include <libwnck/window.h>

#include <clutter/clutter.h>
#include <libgnome/gnome-desktop-item.h>

#include "launcher-config.h"
#include "launcher-defines.h"
#include "launcher-menu.h"

G_DEFINE_TYPE (LauncherMenu, launcher_menu, G_TYPE_OBJECT)

#define LAUNCHER_MENU_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE(obj, \
        LAUNCHER_TYPE_MENU, LauncherMenuPrivate))

struct _LauncherMenuPrivate
{
  GList      *categories;
  GList      *menu_apps;
  GMenuTree *app_tree;
  GMenuTree *sys_tree;

  guint      tag;

  gboolean   refresh;
  GList     *old_cats;
  GList     *old_apps;
};

enum 
{
  MENU_CHANGED,

  LAST_SIGNAL
};

static guint _menu_signals[LAST_SIGNAL] = { 0 };

/* Forwards */
static void tree_changed (GMenuTree *tree, LauncherMenu *menu);

/* Utility functions */

/* From matchbox-desktop */
static char *
strip_extension (const char *file)
{
        char *stripped, *p;

        stripped = g_strdup (file);

        p = strrchr (stripped, '.');
        if (p &&
            (!strcmp (p, ".png") ||
             !strcmp (p, ".svg") ||
             !strcmp (p, ".xpm")))
	        *p = 0;

        return stripped;
}

/* Gets the pixbuf from a desktop file's icon name. Based on the same function
 * from matchbox-desktop
 */
static GdkPixbuf *
get_icon (const gchar *name, guint size)
{
  static GtkIconTheme *theme = NULL;
  GdkPixbuf *pixbuf = NULL;
  GError *error = NULL;
  gchar *stripped = NULL;

  gint width, height;

  if (theme == NULL)
    theme = gtk_icon_theme_get_default ();

  if (name == NULL)
  {
    pixbuf = gtk_icon_theme_load_icon (theme, "application-x-executable",
                                       size, 0, NULL);
    return pixbuf;
  }

  if (g_path_is_absolute (name))
  {
    if (g_file_test (name, G_FILE_TEST_EXISTS))
    {
      pixbuf = gdk_pixbuf_new_from_file_at_scale (name, size, size, 
                                                  TRUE, &error);
      if (error)
      {
        /*g_warning ("Error loading icon: %s\n", error->message);*/
        g_error_free (error);
        error = NULL;
     }
      return pixbuf;
    } 
  }

  stripped = strip_extension (name);
  
  pixbuf = gtk_icon_theme_load_icon (theme,
                                     stripped,
                                     size,
                                     0, &error);
  if (error)
  {   
    /*g_warning ("Error loading icon: %s\n", error->message);*/
    g_error_free (error);
    error = NULL;
  }
  
  /* Always try and send back something */
  if (pixbuf == NULL)
    pixbuf = gtk_icon_theme_load_icon (theme, "application-x-executable",
                                       size, 0, NULL);
  
  width = gdk_pixbuf_get_width (pixbuf);
  height = gdk_pixbuf_get_height (pixbuf);

  if (width != size || height != size)
  {
    GdkPixbuf *temp = pixbuf;
    pixbuf = gdk_pixbuf_scale_simple (temp, 
                                      size,
                                      size,
                                      GDK_INTERP_HYPER);
    g_object_unref (temp);
  }

  g_free (stripped);

 return pixbuf;
}

/* Public functions */
GList*
launcher_menu_get_categories (LauncherMenu *menu)
{
  g_return_val_if_fail (LAUNCHER_IS_MENU (menu), NULL);
  return menu->priv->categories;
}

GList*
launcher_menu_get_applications (LauncherMenu *menu)
{
  g_return_val_if_fail (LAUNCHER_IS_MENU (menu), NULL);
  return menu->priv->menu_apps;
}

const gchar *
launcher_menu_category_get_name (LauncherMenuCategory *category)
{
  g_return_val_if_fail (category, NULL);

  return category->name;
}

const gchar *
launcher_menu_category_get_comment (LauncherMenuCategory *category)
{
  g_return_val_if_fail (category, NULL);

  return category->comment;
}

GdkPixbuf*
launcher_menu_category_get_icon (LauncherMenuCategory *category)
{
  g_return_val_if_fail (category, NULL);

  return launcher_menu_category_get_pixbuf (category, CAT_ICON_SIZE);
}

const gchar *
launcher_menu_category_get_icon_name (LauncherMenuCategory *category)
{
  g_return_val_if_fail (category, NULL);

  return category->icon;
}

GList*
launcher_menu_category_get_applications (LauncherMenuCategory *category)
{
  g_return_val_if_fail (category, NULL);

  return category->applications;

}

GdkPixbuf *
launcher_menu_category_get_pixbuf (LauncherMenuCategory *category,
                                   guint                 size)
{
  const gchar *name;

  g_return_val_if_fail (category, NULL);

  name = category->icon;

  if (!GDK_IS_PIXBUF (category->pixbuf))
    category->pixbuf = get_icon (name, CAT_ICON_SIZE);

  return category->pixbuf;
}
  
const gchar *
launcher_menu_application_get_name (LauncherMenuApplication *application)
{
  g_return_val_if_fail (application, NULL);

  return application->name;
}


const gchar *
launcher_menu_application_get_comment (LauncherMenuApplication *application)
{
  g_return_val_if_fail (application, NULL);

  return application->comment;
}

GdkPixbuf*
launcher_menu_application_get_icon (LauncherMenuApplication *application)
{
  g_return_val_if_fail (application, NULL);

  if (application->pixbuf)
    return application->pixbuf;

  return launcher_menu_application_get_pixbuf (application, APP_ICON_SIZE);
}

LauncherMenuCategory*
launcher_menu_application_get_category (LauncherMenuApplication *application)
{
  g_return_val_if_fail (application, NULL);

  return application->category;
}

GdkPixbuf *
launcher_menu_application_get_pixbuf (LauncherMenuApplication *application,
                                      guint                    size)
{
  const gchar *name;

  g_return_val_if_fail (application, NULL);

  name = application->icon;

  if (!GDK_IS_PIXBUF (application->pixbuf))
    application->pixbuf = get_icon (name, APP_ICON_SIZE);

  return application->pixbuf;
}

ClutterActor *
launcher_menu_application_get_actor (LauncherMenuApplication *application)
{
  g_return_val_if_fail (application, NULL);

  /*FIXME: */
  //application->actor = launcher_item_app_new (application);
 
  return application->actor;
}

const gchar*
launcher_menu_application_get_exec (LauncherMenuApplication *application)
{
  g_return_val_if_fail (application, NULL);

  return application->exec;
}

const gchar *
launcher_menu_application_get_desktop_filename (LauncherMenuApplication *application)
{
  g_return_val_if_fail (application, NULL);

  return application->path;
}

gboolean
launcher_menu_application_is_new (LauncherMenuApplication *application)
{
  g_return_val_if_fail (application, FALSE);

  return application->new;
}

void
launcher_menu_application_set_new (LauncherMenuApplication *application, 
                                   gboolean                 is_new)
{
  g_return_if_fail (application);
  application->new = is_new;
}

gint
launcher_menu_application_get_pid (LauncherMenuApplication *application)
{
  g_return_val_if_fail (application, -1);

  return application->pid;
}

void
launcher_menu_application_set_pid (LauncherMenuApplication *application,  
                                   gint                     pid)
{
  g_return_if_fail (application);

  application->pid = pid;
}

WnckWindow *
launcher_menu_application_get_window (LauncherMenuApplication *application)
{
  g_return_val_if_fail (application, NULL);

  return application->window;
}

void
launcher_menu_application_set_window (LauncherMenuApplication *application,
                                      WnckWindow              *window)
{
  g_return_if_fail (application);

  application->window = window;
}

/* Private */

static LauncherMenuCategory*
make_category (LauncherMenu *menu, GMenuTreeDirectory *dir)
{
  LauncherMenuPrivate *priv = menu->priv;
  LauncherMenuCategory *category = NULL;

  /* if we are refreshing, try searching for the category */
  if (priv->refresh)
  {
    GList *l;
    const gchar *name = gmenu_tree_directory_get_name (dir);
    for (l = priv->old_cats; l; l = l->next)
    {
      LauncherMenuCategory *cat = l->data;
      if (name && cat->name && strcmp (name, cat->name) == 0)
      {
        category = cat;
        category->cat_ref = dir;
        break;
      }
    }
    if (category)
    {
      g_list_free (category->applications);
      category->applications = NULL;
      priv->categories = g_list_append (priv->categories, (gpointer)category);

      priv->old_cats = g_list_remove (priv->old_cats, category);
      return category;
    }
  }

  category = g_slice_new0 (LauncherMenuCategory);
  category->name = g_strdup (gmenu_tree_directory_get_name (dir));
  category->comment = g_strdup (gmenu_tree_directory_get_comment (dir));
  category->icon = g_strdup (gmenu_tree_directory_get_icon (dir));
  category->applications = NULL;
  category->cat_ref = dir;

  priv->categories = g_list_append (priv->categories, (gpointer)category);

  return category;
} 

/*
 * Each application has a pointer to its category, and gets added to the 
 * categories application list and the main applicaton list 
 */
static void
make_application (LauncherMenu *menu, 
                  GMenuTreeEntry *entry, 
                  LauncherMenuCategory *category)
{
  LauncherMenuPrivate *priv = menu->priv;
  LauncherMenuApplication *app = NULL;

  /* if we are refreshing, try searching for the category */
  if (priv->refresh)
  {
    GList *a;
    const gchar *path = gmenu_tree_entry_get_desktop_file_path (entry);

    LauncherMenuApplication *application;
    for (a = priv->old_apps; a; a = a->next)
    {
      application = a->data;
      if (path && application->path && strcmp (path, application->path) == 0)
      {
        app = application;
        break;
      }
    }
    if (app)
    {
      app->category = category;
      category->applications = g_list_append (category->applications, app);
      priv->menu_apps = g_list_append (priv->menu_apps, app);

      priv->old_apps = g_list_remove (priv->old_apps, app);
      return;
    }    
  }

  app = g_slice_new0 (LauncherMenuApplication);
  app->name = g_strdup (gmenu_tree_entry_get_name (entry));
  app->comment = g_strdup (gmenu_tree_entry_get_comment (entry));
  app->icon = g_strdup (gmenu_tree_entry_get_icon (entry));
  app->exec = g_strdup (gmenu_tree_entry_get_exec (entry));
  app->path = g_strdup (gmenu_tree_entry_get_desktop_file_path (entry));
  app->category = category;
  app->pid = -1;
  app->window = NULL;
  app->new = priv->refresh;
  app->can_fav = TRUE;

  category->applications = g_list_append (category->applications,
                                          (gpointer)app);
  priv->menu_apps = g_list_append (priv->menu_apps, (gpointer)app);
} 

/* 
 * Traverse through the root tree, treating each directory as a category, and
 * each entry as an application. We only want 1st tier categories, so we pass
 * a 'category' variable to the function, which, if present, blocks the
 * 2nd tier directory from becoming a new category, and uses it's parent as the
 * category.
 */
static gint
compare_apps (LauncherMenuApplication *a1, LauncherMenuApplication *a2)
{
  if (!a1)
    return 1;
  if (!a2)
    return -1;

  return g_strcmp0 (a1->name, a2->name);
}

static void
load_menu_from_directory (LauncherMenu *menu, 
                          GMenuTreeDirectory *dir,
                          LauncherMenuCategory *category)
{
  GSList *list, *l;
  
  if (dir == NULL)
    return;

  list = gmenu_tree_directory_get_contents (dir);
  for (l = list; l; l = l->next)
  {
    GMenuTreeItem *item = (GMenuTreeItem*)l->data;

    switch (gmenu_tree_item_get_type (item))
    {
      case GMENU_TREE_ITEM_DIRECTORY:
        
        if (!category)
        {
          load_menu_from_directory (menu, 
                                   GMENU_TREE_DIRECTORY (item),
                                   make_category (menu,
                                                  GMENU_TREE_DIRECTORY (item)));

        }
        else
          load_menu_from_directory (menu, 
                                    GMENU_TREE_DIRECTORY (item), category);
        break;
      case GMENU_TREE_ITEM_ENTRY:
        if (category)
        {
          make_application (menu, GMENU_TREE_ENTRY (item), category);
        }
        else
        {
          GList *c;
          for (c = menu->priv->categories; c; c = c->next)
          {
            LauncherMenuCategory *cat = c->data;
            const gchar *dir_desktop = NULL;

            if (!cat->cat_ref)
              continue;

            dir_desktop = gmenu_tree_directory_get_desktop_file_path (
                                GMENU_TREE_DIRECTORY (cat->cat_ref));
            
            if (dir_desktop && g_strstr_len (dir_desktop, -1, "Utility"))
            {
              make_application (menu, GMENU_TREE_ENTRY (item), cat);
              cat->applications = g_list_sort (cat->applications,
                                               (GCompareFunc)compare_apps);
              break;
            }
          }
        }
        break;

      case GMENU_TREE_ITEM_SEPARATOR:
       ;
                
      default:
        break;
    }
    gmenu_tree_item_unref (item);
  }

  g_slist_free (list);
}

static GMenuTree *
load_menu_from_tree (LauncherMenu *menu, const gchar *name)
{
  LauncherMenuPrivate *priv;
  GMenuTreeDirectory *root = NULL;
  GMenuTree *tree = NULL;

  g_return_val_if_fail (LAUNCHER_IS_MENU (menu), NULL);
  priv = menu->priv;
 
  tree = gmenu_tree_lookup (name, GMENU_TREE_FLAGS_NONE);
  if (!tree)
  {
    g_warning ("Unable to find %s", name);
    return NULL;
  }
  root = gmenu_tree_get_root_directory (tree);
  load_menu_from_directory (menu, root, NULL); 
  gmenu_tree_item_unref (root);

  gmenu_tree_add_monitor (tree, (GMenuTreeChangedFunc)tree_changed, menu);

  return tree;
}

static gboolean
_poor_mans_garbage_collection (GList *pixbufs)
{
  GList *p;

  for (p = pixbufs; p; p = p->next)
  {
    while (G_IS_OBJECT (p->data))
    {
      g_debug ("PMGC: Swept one up");
      g_object_unref (p->data);
    }
  }
  g_list_free (pixbufs);

  return FALSE;
}

static void
free_menu (LauncherMenu *menu)
{
  LauncherMenuPrivate *priv;
  GList *l;
  GList *pixbufs = NULL;
  static gint i = 0;

  g_return_if_fail (LAUNCHER_IS_MENU (menu));
  priv = menu->priv;

  for (l = priv->old_apps; l; l = l->next)
  {
    LauncherMenuApplication *app = l->data;
    
    g_free (app->name);
    g_free (app->comment);
    g_free (app->icon);
    g_free (app->exec);
    g_free (app->path);
    
    if (G_IS_OBJECT (app->pixbuf))
    {
      if (G_OBJECT (app->pixbuf)->ref_count >1)
        g_object_unref (app->pixbuf);
      else
        pixbufs = g_list_append (pixbufs, app->pixbuf);
    }
    g_slice_free (LauncherMenuApplication, app);
    app = NULL;
  }
  g_list_free (priv->old_apps);
  priv->old_apps = NULL;

  for (l = priv->old_cats; l; l = l->next)
  {
    LauncherMenuCategory *cat = l->data;
    
    if (cat->icon && strcmp (cat->icon, FAVCATNAME)==0)
    {
      priv->categories = g_list_insert (priv->categories, cat, 0);


        g_list_free (cat->applications);
        cat->applications = NULL;

      continue;
    }

    g_free (cat->name);
    g_free (cat->comment);
    g_free (cat->icon);
    g_list_free (cat->applications);

    if (G_IS_OBJECT (cat->pixbuf))
    {
      if (G_OBJECT (cat->pixbuf)->ref_count >1)
        g_object_unref (cat->pixbuf);
      else
        pixbufs = g_list_append (pixbufs, cat->pixbuf);
    }
    g_slice_free (LauncherMenuCategory, cat);
    cat = NULL;
  }
  g_list_free (priv->old_cats);
  priv->old_cats = NULL;

  g_timeout_add (1000, (GSourceFunc)_poor_mans_garbage_collection, pixbufs);

  i++;
}

static void
sort_categories (LauncherMenu *menu)
{
  LauncherMenuPrivate *priv;
  GList *l, *fav = NULL;

  g_return_if_fail (LAUNCHER_IS_MENU (menu));
  priv = menu->priv;

  for (l = priv->categories; l; l= l->next)
  {
    LauncherMenuCategory *cat = l->data;
    if (cat->icon && strstr (cat->icon, FAVCATNAME))
    {
      fav = l;
      break;
    }
  }
  if (fav)
  {
    LauncherMenuCategory *favcat = fav->data;
    priv->categories = g_list_delete_link (priv->categories, fav);
    priv->categories = g_list_prepend (priv->categories, favcat);

    return;
  }
  else
  {
    LauncherMenuCategory *category;
    GnomeDesktopItem *item = gnome_desktop_item_new_from_file (
                            DATADIR"/desktop-directories/Favorites.directory",
                                         GNOME_DESKTOP_ITEM_LOAD_ONLY_IF_EXISTS,
                                           NULL);
    if (item)
    {
        category = g_slice_new0 (LauncherMenuCategory);
        category->name = g_strdup (gnome_desktop_item_get_localestring (item,
                          GNOME_DESKTOP_ITEM_NAME));
        category->comment = g_strdup (gnome_desktop_item_get_localestring (item,
                                    GNOME_DESKTOP_ITEM_COMMENT));
        category->icon = g_strdup (gnome_desktop_item_get_localestring (item, 
                                                        GNOME_DESKTOP_ITEM_ICON));
        category->applications = NULL;
        category->cat_ref = NULL;
        priv->categories = g_list_append (priv->categories, (gpointer)category);
        gnome_desktop_item_unref (item);  
    }
    else
    {
        return;
    }

  }

  sort_categories (menu);
}

static gboolean
list_changed (LauncherMenu *menu)
{
  LauncherMenuPrivate *priv;
  GMenuTreeDirectory *root;
  GList *l, *fav = NULL;
  
  g_return_val_if_fail (LAUNCHER_IS_MENU (menu), FALSE);
  priv = menu->priv;

  g_debug ("Menu changed");

  priv->old_cats = priv->categories;
  priv->old_apps = priv->menu_apps;
  priv->categories = NULL;
  priv->menu_apps = NULL;

  priv->refresh = TRUE;

  root = gmenu_tree_get_root_directory (priv->app_tree);
  load_menu_from_directory (menu, root, NULL);
  gmenu_tree_item_unref (root);

  root = gmenu_tree_get_root_directory (priv->sys_tree);
  load_menu_from_directory (menu, root, NULL);
  gmenu_tree_item_unref (root);

  priv->refresh = FALSE;

  free_menu (menu);

  for (l = priv->categories; l; l= l->next)
  {
    LauncherMenuCategory *cat = l->data;
    if (cat->icon && strstr (cat->icon, FAVCATNAME))
    {
      fav = l;
      break;
    }
  }
  if (fav)
  {
    LauncherMenuCategory *favcat = fav->data;
    priv->categories = g_list_delete_link (priv->categories, fav);
    priv->categories = g_list_prepend (priv->categories, favcat);
  }

  g_signal_emit (menu, _menu_signals[MENU_CHANGED], 0);
  menu->priv->tag = 0;  
  
  return FALSE;
}

/*
 * FIXME: The tree changing should not be destructive. Find a way to make it
 * work without re-creating the entire menu again.
 */
static void
tree_changed (GMenuTree *tree, LauncherMenu *menu)
{
  LauncherMenuPrivate *priv;
    
  g_return_if_fail (LAUNCHER_IS_MENU (menu));
  priv = menu->priv;
  
  if (priv->tag)
  {
    return;
  }
  priv->tag = g_timeout_add (200, (GSourceFunc)list_changed, menu);
}

static void
on_theme_changed (GtkIconTheme *theme, LauncherMenu *menu)
{
  LauncherMenuPrivate *priv;
  
  g_return_if_fail (LAUNCHER_IS_MENU (menu));
  priv = menu->priv;
  
  if (priv->tag)
  {
    return;
  }
  priv->tag = g_timeout_add (500, (GSourceFunc)list_changed, menu);
}


/* GObject functions */
static void
launcher_menu_dispose (GObject *object)
{
  free_menu (LAUNCHER_MENU (object));

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

static void
launcher_menu_finalize (GObject *menu)
{
  LauncherMenuPrivate *priv;
  
  g_return_if_fail (LAUNCHER_IS_MENU (menu));
  priv = LAUNCHER_MENU (menu)->priv;

  G_OBJECT_CLASS (launcher_menu_parent_class)->finalize (menu);
}


static void
launcher_menu_class_init (LauncherMenuClass *klass)
{
  GObjectClass *obj_class = G_OBJECT_CLASS (klass);

  obj_class->finalize = launcher_menu_finalize;
  obj_class->dispose = launcher_menu_dispose;

   _menu_signals[MENU_CHANGED] = 
      g_signal_new ("menu-changed",
                  G_OBJECT_CLASS_TYPE (obj_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (LauncherMenuClass, menu_changed),
                  NULL, NULL,
                  g_cclosure_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);

 
  g_type_class_add_private (obj_class, sizeof (LauncherMenuPrivate)); 
}

static void
launcher_menu_init (LauncherMenu *menu)
{
  LauncherMenuPrivate *priv;
  GtkIconTheme *theme;
    
  priv = menu->priv = LAUNCHER_MENU_GET_PRIVATE (menu);

  priv->categories = NULL;
  priv->menu_apps = NULL;
  priv->tag = 0;
  priv->refresh = FALSE;

  priv->app_tree = load_menu_from_tree (menu, "applications.menu");
  priv->sys_tree = load_menu_from_tree (menu, "settings.menu");

  sort_categories (menu);

  /* React to theme-changed signals */
  theme = gtk_icon_theme_get_default ();
  g_signal_connect (theme, "changed",
                    G_CALLBACK (on_theme_changed), menu);
}

LauncherMenu*
launcher_menu_get_default (void)
{
  static LauncherMenu *menu = NULL;
  
  if (menu == NULL)
    menu = g_object_new (LAUNCHER_TYPE_MENU, 
                         NULL);

  return menu;
}
