/*
 * Copyright (C) 2010 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 Mikkel Kamstrup Erlandsen <mikkel.kamstrup@canonical.com>
 *
 */

/*
 * This file contains C bindings written in C++ to hook up with the
 * Xapian indexes generated by Ubuntu Software Center.
 *
 * It also hooks up with libgnome-menu and creates an attached in-memory
 * Xapian index of the menu contents.
 */

#include <xapian.h>
#include <iostream>
#include <gmenu-tree.h>
#include <glib.h>
#include <unity.h>
#include <string.h>
#include <vector>

using namespace std;

#define SOFTWARE_CENTER_INDEX "/var/cache/software-center/xapian"
#define QUERY_PARSER_EXACTSEARCH_FLAGS Xapian::QueryParser::FLAG_BOOLEAN|Xapian::QueryParser::FLAG_PHRASE|Xapian::QueryParser::FLAG_LOVEHATE
#define QUERY_PARSER_PREFIXSEARCH_FLAGS QUERY_PARSER_EXACTSEARCH_FLAGS|Xapian::QueryParser::FLAG_WILDCARD|Xapian::QueryParser::FLAG_PARTIAL
#define QUERY_PARSER_FILTER_FLAGS Xapian::QueryParser::FLAG_BOOLEAN|Xapian::QueryParser::FLAG_LOVEHATE

// values used in the database
#define XAPIAN_VALUE_APPNAME 170
#define XAPIAN_VALUE_PKGNAME 171
#define XAPIAN_VALUE_ICON 172
#define XAPIAN_VALUE_GETTEXT_DOMAIN 173
#define XAPIAN_VALUE_ARCHIVE_SECTION 174
#define XAPIAN_VALUE_ARCHIVE_ARCH 175
#define XAPIAN_VALUE_POPCON 176
#define XAPIAN_VALUE_SUMMARY 177
#define XAPIAN_VALUE_ARCHIVE_CHANNEL 178
#define XAPIAN_VALUE_DESKTOP_FILE 179
#define XAPIAN_VALUE_PRICE 180
#define XAPIAN_VALUE_PURCHASED_DATE 184
#define XAPIAN_VALUE_SCREENSHOT_URLS 185
#define XAPIAN_VALUE_DESCRIPTION 188
#define XAPIAN_VALUE_VERSION_INFO 198
#define XAPIAN_VALUE_EXENAME 294
#define XAPIAN_VALUE_CURRENCY 201

/* this isn't a Software Center slot, but we use it to mark master
 * scopes in the scope index. */
#define XAPIAN_VALUE_MASTERSCOPE 1000

#include "unity-package-search.h"
#include "columbus.hh"

struct _UnityPackageSearcher
{
  Xapian::Database    *db;
  Xapian::KeyMaker    *sorter;
  Xapian::Enquire     *enquire;
  Xapian::QueryParser *query_parser;
  GRand               *random;
  Columbus::Matcher   *matcher;
  vector<string>      col_mapping;
  bool                db_merged;
};

static void buildMatcher(UnityPackageSearcher *searcher) {
    Columbus::Matcher *m  = searcher->matcher;
    Xapian::Database *db = searcher->db;
    Columbus::Corpus c;
    Columbus::Word appnameField("appname");
    Columbus::Word summaryField("summary");
    Columbus::Word pkgnameField("pkgname");
    Columbus::Word exenameField("exename");

    for(Xapian::PostingIterator post = db->postlist_begin(""); post != db->postlist_end(""); post++) {
        Xapian::Document xdoc = db->get_document(*post);
        DocumentID id;
        if(searcher->db_merged) {
            searcher->col_mapping.push_back(xdoc.get_value(XAPIAN_VALUE_APPNAME));
            id = searcher->col_mapping.size()-1;
        } else {
            id = xdoc.get_docid();
        }
        Columbus::Document cdoc(id);
        std::string val;

        val = xdoc.get_value(XAPIAN_VALUE_APPNAME);
        if(!val.empty()) {
            cdoc.addText(appnameField, val.c_str());
        }
        /* It turned out that fuzzy matching non-visible content
         * is confusing to users, so let's not index these at all.
         * In the future we might want to explore better ways
         * of exposing this info.
        val = xdoc.get_value(XAPIAN_VALUE_SUMMARY);
        if(!val.empty()) {
            cdoc.addText(summaryField, val.c_str());
        }
        val = xdoc.get_value(XAPIAN_VALUE_PKGNAME);
        if(!val.empty()) {
            cdoc.addText(pkgnameField, val.c_str());
        }
        */
        val = xdoc.get_value(XAPIAN_VALUE_EXENAME);
        if(!val.empty()) {
            cdoc.addText(exenameField, val.c_str());
        }
        c.addDocument(cdoc);
    }
    m->index(c);
    m->getErrorValues().addStandardErrors();
    // Substring mode resulted in too many false positives,
    // so disable it for the time being.
    //m->getErrorValues().setSubstringMode();
    m->getIndexWeights().setWeight(summaryField, 0.5);
}


extern "C"
{
  extern gchar* unity_applications_lens_utils_preprocess_string (const gchar* input);
}

/* A Xapian::Sorter that respects the collation rules for the current locale */
class LocaleKeyMaker : public Xapian::KeyMaker
{
  private:
  Xapian::valueno value;

  public:

    LocaleKeyMaker (Xapian::valueno value)
    {
      this->value = value;
    }

    virtual ~LocaleKeyMaker() { }

    virtual std::string operator() (const Xapian::Document &doc) const
    {
      string val = doc.get_value (value);
      gchar* col_key_c = g_utf8_collate_key (val.c_str (), -1);
      string col_key_mm = col_key_c;
      g_free (col_key_c);
      return col_key_mm;
    }
};

/* Do generic searcher setup */
static void
init_searcher (UnityPackageSearcher *searcher)
{
  // Activate Xapian CJK support
  setenv("XAPIAN_CJK_NGRAM", "1", 1);

  Xapian::Database db = *searcher->db;

  // Start an enquire session
  Xapian::Enquire *enquire = new Xapian::Enquire (db);
  enquire->set_sort_by_value (XAPIAN_VALUE_APPNAME, FALSE);
  searcher->enquire = enquire;

  // Make sure we respect sorting rules for the current locale
  searcher->sorter = new LocaleKeyMaker (XAPIAN_VALUE_APPNAME);
  enquire->set_sort_by_key (searcher->sorter, FALSE);

  // Create query parser
  Xapian::QueryParser *query_parser = new Xapian::QueryParser ();
  query_parser->add_prefix ("section", "AE");
  query_parser->add_prefix ("type", "AT");
  query_parser->add_prefix ("category", "AC");
  query_parser->add_prefix ("name", "AA");
  query_parser->add_prefix ("pkgname", "AP");
  query_parser->add_prefix ("exec", "XX");
  query_parser->add_prefix ("keyword", "KW");
  query_parser->add_prefix ("pkg_wildcard", "XP");
  query_parser->add_prefix ("pkg_wildcard", "XPM");
  query_parser->add_prefix ("pkg_wildcard", "AP");
  query_parser->add_prefix ("pkg_wildcard", "APM");
  query_parser->set_default_op (Xapian::Query::OP_AND);
  query_parser->set_database (db);
  searcher->query_parser = query_parser;

  // Init random number generator from 32 lowest bits of system time
  searcher->random = g_rand_new_with_seed (g_get_monotonic_time ());
}

/* Recursively traverse a menu tree and add it to the index */
static void
index_menu_item (Xapian::WritableDatabase *db,
                 Xapian::TermGenerator    *indexer,
                 GMenuTreeDirectory       *item)
{
  GMenuTreeEntry       *entry;
  GMenuTreeDirectory *dir;
  GMenuTreeIter *iter;
  GDesktopAppInfo *info;
  GAppInfo *app_info;
  GMenuTreeItemType item_type;
  UnityAppInfoManager  *appman;
  gchar               **cats, **keywords;
  gchar                *dum1, *dum2, *dum3;
  gint                  i, len;

  g_return_if_fail (db != NULL);
  g_return_if_fail (indexer != NULL);
  g_return_if_fail (item != NULL);

  iter = gmenu_tree_directory_iter (item);
  while ((item_type = gmenu_tree_iter_next (iter)) != GMENU_TREE_ITEM_INVALID)
  {
    Xapian::Document      doc;

    switch (item_type)
    {
    case GMENU_TREE_ITEM_DIRECTORY:
      /* Recurse into directory */
      dir = gmenu_tree_iter_get_directory (iter);
      index_menu_item (db, indexer, dir);
      break;
    case GMENU_TREE_ITEM_ENTRY:
      /* Add this entry to the index */
      entry = gmenu_tree_iter_get_entry (iter);
      info = gmenu_tree_entry_get_app_info (entry);
      app_info = G_APP_INFO (info);

      /* Store relevant values */
      if (g_app_info_get_display_name (app_info))
        doc.add_value (XAPIAN_VALUE_APPNAME, g_app_info_get_display_name (app_info));

      if (g_app_info_get_description (app_info))
        doc.add_value (XAPIAN_VALUE_DESCRIPTION, g_app_info_get_description (app_info));

      if (g_app_info_get_icon (app_info))
      {
        gchar *icon_str = g_icon_to_string (g_app_info_get_icon (app_info));
        if (icon_str != NULL)
        {
          doc.add_value (XAPIAN_VALUE_ICON, icon_str);
          g_free (icon_str);
        }
      }
      if (gmenu_tree_entry_get_desktop_file_id (entry))
      {
        doc.add_value (XAPIAN_VALUE_DESKTOP_FILE, gmenu_tree_entry_get_desktop_file_id (entry));
      }

      /* Index full text data */
      indexer->set_document(doc);
      if (g_app_info_get_display_name (app_info))
      {
        dum1 = unity_applications_lens_utils_preprocess_string (g_app_info_get_display_name (app_info));
        indexer->index_text(dum1, 5);
        g_free (dum1);
      }
      if (g_app_info_get_name (app_info))
      {
        dum1 = unity_applications_lens_utils_preprocess_string (g_app_info_get_name (app_info));
        indexer->index_text(dum1, 5);
        g_free (dum1);
      }
      if (g_app_info_get_description (app_info))
      {
        dum1 = unity_applications_lens_utils_preprocess_string (g_app_info_get_description (app_info));
        indexer->index_text (dum1, 0);
        g_free (dum1);
      }

      /* Index the XDG categories */
      appman = unity_app_info_manager_get_default ();
      cats = unity_app_info_manager_get_categories (appman, // const return
                                                    gmenu_tree_entry_get_desktop_file_id (entry),
                                                    &len);

      /* Note: Wine apps and app launchers created with Alacarte commonly
       * don't have any category metadata, so they'll only show up under
       * All Applications */
      for (i = 0; i < len; i++)
      {
        dum1 = g_ascii_strdown (cats[i], -1);
        dum2 = g_strconcat ("AC", dum1, NULL);
        doc.add_term (dum2);
        g_free (dum1);
        g_free (dum2);
      }

      /* Index Keywords*/
      keywords = unity_app_info_manager_get_keywords (appman, // const return
                                                      gmenu_tree_entry_get_desktop_file_id (entry),
                                                      &len);
      for (i = 0; i < len; i++)
      {
        dum1 = unity_applications_lens_utils_preprocess_string (keywords[i]);
        indexer->index_text (dum1, 0);
        indexer->index_text (dum1, 0, "KW");
        g_free (dum1);
      }

      g_object_unref (appman);

      /* Always assume Type=Application for items in a menu... */
      doc.add_term ("ATapplication");

      /* Index application names */
      dum1 = (gchar *) g_app_info_get_display_name (app_info);
      dum2 = g_strconcat ("AA", dum1, NULL);
      doc.add_term (dum2);
      g_free (dum2);

      dum1 = (gchar *) g_app_info_get_name (app_info);
      dum2 = g_strconcat ("AA", dum1, NULL);
      doc.add_term (dum2);
      g_free (dum2);

      /* Index executable name, change - in _ for exec */
      dum1 = g_strdup (g_app_info_get_commandline (app_info)); // alloc
      if (dum1)
      {
        dum2 = strstr (dum1, " "); // const
        dum2 == NULL ? : *dum2 = '\0'; // const
        dum2 = g_path_get_basename (dum1); // alloc
        doc.add_value(XAPIAN_VALUE_EXENAME, dum2);
        indexer->index_text (dum2, 2);
        g_free (dum1);
        dum1 = g_strdelimit (dum2, "-", '_'); // in place switch, dum1 own mem
        dum2 = g_strconcat ("AA", dum1, NULL); // alloc
        doc.add_term (dum2);
        g_free (dum2);
        dum2 = g_strconcat ("XX", dum1, NULL); // alloc
        doc.add_term (dum2);
        g_free (dum2);
        g_free (dum1);
      }

      db->add_document(doc);
      break;
    case GMENU_TREE_ITEM_SEPARATOR:
    case GMENU_TREE_ITEM_HEADER:
    case GMENU_TREE_ITEM_ALIAS:
      break;
    default:
      g_warning ("Unexpected GMenuTreeItemType %u", item_type);
      break;
    }
  }

  // Add the document to the database.
}

/* Create a searcher that searches in a menu tree. The menu tree
 * will be indexed into an in-memory Xapian index */
UnityPackageSearcher*
unity_package_searcher_new_for_menu (GMenuTree *menu)
{
  UnityPackageSearcher *searcher;
  Xapian::WritableDatabase *db;

  searcher = new UnityPackageSearcher;
  db = new Xapian::WritableDatabase ();
  searcher->db = db;
  searcher->db->add_database (Xapian::InMemory::open ());

  init_searcher (searcher);

  /* Index the menu recursively */
  searcher->db_merged = false;
  Xapian::TermGenerator *indexer = new Xapian::TermGenerator ();
  index_menu_item (db, indexer, gmenu_tree_get_root_directory (menu));
  delete indexer;
  db->flush ();

  searcher->matcher = new Columbus::Matcher();
  buildMatcher(searcher);

  return searcher;
}

/* Recursively traverse the set of scopes */
static void
index_scope (Xapian::WritableDatabase                *db,
             Xapian::TermGenerator                   *indexer,
             UnityProtocolScopeRegistryScopeMetadata *info)
{
  Xapian::Document doc;
  char *dum1;

  if (info->name != NULL)
    doc.add_value (XAPIAN_VALUE_APPNAME, info->name);
  if (info->description != NULL)
    doc.add_value (XAPIAN_VALUE_DESCRIPTION, info->description);
  if (info->icon != NULL)
    doc.add_value (XAPIAN_VALUE_ICON, info->icon);
  if (info->id != NULL)
    doc.add_value (XAPIAN_VALUE_DESKTOP_FILE, info->id);
  if (info->is_master)
      doc.add_value (XAPIAN_VALUE_MASTERSCOPE, "true");

  indexer->set_document (doc);
  if (info->name != NULL)
  {
    dum1 = unity_applications_lens_utils_preprocess_string (info->name);
    indexer->index_text (dum1, 5);
    g_free (dum1);
  }
  if (info->description != NULL)
  {
    dum1 = unity_applications_lens_utils_preprocess_string (info->description);
    indexer->index_text (dum1, 5);
    g_free (dum1);
  }
  for (GSList *l = info->keywords; l != NULL; l = l->next)
  {
    dum1 = unity_applications_lens_utils_preprocess_string ((char *)l->data);
    indexer->index_text (dum1, 0);
    indexer->index_text (dum1, 0, "KW");
    g_free (dum1);
  }

  /* Always assume Type=Scope for scopes... */
  doc.add_term ("ATscope");

  /* Index application name */
  dum1 = g_strconcat ("AA", info->name, NULL);
  doc.add_term (dum1);
  g_free (dum1);

  db->add_document (doc);
}

UnityPackageSearcher*
unity_package_searcher_new_for_scopes (UnityProtocolScopeRegistry *scope_registry)
{
  UnityPackageSearcher *searcher;
  Xapian::WritableDatabase *db;

  searcher = new UnityPackageSearcher;
  db = new Xapian::WritableDatabase ();
  searcher->db = db;
  searcher->db->add_database (Xapian::InMemory::open ());

  init_searcher (searcher);

  /* Index the menu recursively */
  searcher->db_merged = false;
  Xapian::TermGenerator *indexer = new Xapian::TermGenerator ();

  GSList *scopes = unity_protocol_scope_registry_get_scopes (scope_registry);
  for (GSList *l = scopes; l != NULL; l = l->next)
  {
    UnityProtocolScopeRegistryScopeRegistryNode *node = (UnityProtocolScopeRegistryScopeRegistryNode *)l->data;
    index_scope (db, indexer, node->scope_info);

    // Also index any sub-scopes
    for (GSList *sl = node->sub_scopes; sl != NULL; sl = sl->next)
    {
      UnityProtocolScopeRegistryScopeMetadata *info = (UnityProtocolScopeRegistryScopeMetadata *)sl->data;
      index_scope (db, indexer, info);
    }
  }
  delete indexer;
  db->flush ();

  searcher->matcher = new Columbus::Matcher();
  buildMatcher(searcher);

  return searcher;
}

/* Create a new searcher that searches into the Xapian index
 * provided by the Software Center */
UnityPackageSearcher*
unity_package_searcher_new ()
{
  UnityPackageSearcher *searcher;
  gchar *agent = NULL;

  searcher = new UnityPackageSearcher;

  // Xapian initialization
  try
  {
    searcher->db = new Xapian::Database (SOFTWARE_CENTER_INDEX);
  }
  catch(const Xapian::Error &error)
  {
    cerr << "Error loading package indexes: "  << error.get_msg() << endl;
    return NULL;
  }

  // add software-center-agent.db
  try
  {
    agent = g_strdup_printf("%s/software-center/software-center-agent.db",
                            g_get_user_cache_dir());
    if (g_file_test(agent, G_FILE_TEST_IS_DIR))
      searcher->db->add_database (Xapian::Database (agent));
    g_free(agent);
  }
  catch(const Xapian::Error &error)
  {
    cerr << "Error loading agent indexes: "  << error.get_msg() << endl;
  }

  init_searcher (searcher);
  searcher->db_merged = true;
  searcher->matcher = new Columbus::Matcher();
  buildMatcher(searcher);

  return searcher;
}

void
unity_package_searcher_free (UnityPackageSearcher *searcher)
{
  g_return_if_fail (searcher != NULL);

  delete searcher->db;
  delete searcher->sorter;
  delete searcher->enquire;
  delete searcher->query_parser;
  delete searcher->matcher;
  g_rand_free (searcher->random);
  delete searcher;
}

static UnityPackageInfo*
_pkginfo_from_document (Xapian::Document     doc) throw (Xapian::Error)

{
  UnityPackageInfo *pkginfo = g_slice_new0 (UnityPackageInfo);

  string pkgname = doc.get_value (XAPIAN_VALUE_PKGNAME);
  pkginfo->package_name = g_strdup (pkgname.c_str ());

  string appname = doc.get_value (XAPIAN_VALUE_APPNAME);
  pkginfo->application_name = g_strdup (appname.c_str ());

  string description = doc.get_value (XAPIAN_VALUE_DESCRIPTION);
  pkginfo->description = g_strdup (description.c_str ());

  string desktop_file = doc.get_value (XAPIAN_VALUE_DESKTOP_FILE);
  pkginfo->desktop_file = g_strdup (desktop_file.c_str ());

  string icon = doc.get_value (XAPIAN_VALUE_ICON);
  pkginfo->icon = g_strdup (icon.c_str ());

  // FIXME: we may want to format it according to locales at some point
  string price = doc.get_value (XAPIAN_VALUE_CURRENCY);
  if (!price.empty ())
  {
    price += " " + doc.get_value (XAPIAN_VALUE_PRICE);
  }
  pkginfo->price = g_strdup (price.c_str ());

  const string purchase_date = doc.get_value (XAPIAN_VALUE_PURCHASED_DATE);
  pkginfo->needs_purchase = purchase_date.empty ();

  string is_master = doc.get_value (XAPIAN_VALUE_MASTERSCOPE);
  pkginfo->is_master_scope = (is_master == "true");

  return pkginfo;
}

UnityPackageInfo* unity_package_package_info_new ()
{
  UnityPackageInfo *pkginfo = g_slice_new0 (UnityPackageInfo);
  pkginfo->package_name = g_strdup ("");
  pkginfo->application_name = g_strdup ("");
  pkginfo->desktop_file = g_strdup ("");
  pkginfo->price = g_strdup ("");
  pkginfo->icon = g_strdup ("");
  return pkginfo;
}

void unity_package_package_info_free (gpointer pkg)
{
  g_return_if_fail (pkg != NULL);
  UnityPackageInfo *pkginfo = (UnityPackageInfo*) pkg;

  g_free (pkginfo->package_name);
  g_free (pkginfo->application_name);
  g_free (pkginfo->desktop_file);
  g_free (pkginfo->price);
  g_free (pkginfo->icon);

  g_slice_free (UnityPackageInfo, pkginfo);
}

Xapian::Document get_doc_from_col_match(UnityPackageSearcher *searcher, DocumentID id) {
    if(searcher->db_merged) {
        string name = searcher->col_mapping[id];
        string query = "AA\"";
        query += name;
        query += "\"";
        Xapian::QueryParser p;
        Xapian::Query q;
        Xapian::Enquire e(*searcher->db);
        Xapian::MSet matches;
        p.set_database(*searcher->db);
        q = p.parse_query(query);
        e.set_query(q);
        matches = e.get_mset(0, 1);
        return matches.begin().get_document();
    } else {
        return searcher->db->get_document(id);
    }
}

static UnityPackageSearchResult*
xapian_search (UnityPackageSearcher  *searcher,
               const gchar           *search_string,
               guint                  max_hits,
               UnityPackageSearchType search_type,
               UnityPackageSort       sort)
{
  UnityPackageSearchResult* result;
  string _search_string (search_string);
  Xapian::Query query;

  try
  {
    switch (search_type)
    {
    case UNITY_PACKAGE_SEARCHTYPE_PREFIX:
        query = searcher->query_parser->parse_query (_search_string, QUERY_PARSER_PREFIXSEARCH_FLAGS);
        break;
    case UNITY_PACKAGE_SEARCHTYPE_EXACT:
        query = searcher->query_parser->parse_query (_search_string, QUERY_PARSER_EXACTSEARCH_FLAGS);
        break;
    default:
        g_critical ("Unknown search type '%i'. Fallback to a prefix search", search_type);
        query = searcher->query_parser->parse_query (_search_string, QUERY_PARSER_PREFIXSEARCH_FLAGS);
        break;
    }
  }
  catch (Xapian::Error &e)
  {
    g_warning ("Error parsing query '%s': %s", search_string, e.get_msg().c_str());
    return g_slice_new0 (UnityPackageSearchResult);
  }

  switch (sort)
  {
    case UNITY_PACKAGE_SORT_BY_NAME:
      searcher->enquire->set_sort_by_key (searcher->sorter, FALSE);
      break;
    case UNITY_PACKAGE_SORT_BY_RELEVANCY:
      searcher->enquire->set_sort_by_relevance ();
      break;
    default:
      g_critical ("Unknown sort type '%i'", sort);
      searcher->enquire->set_sort_by_relevance ();
      break;
  }

  result = g_slice_new0 (UnityPackageSearchResult);
  try
  {
    // Perform search
    searcher->enquire->set_collapse_key(XAPIAN_VALUE_DESKTOP_FILE);
    max_hits = (max_hits != 0 ? max_hits : searcher->db->get_doccount ());
    searcher->enquire->set_query(query);
    Xapian::MSet matches = searcher->enquire->get_mset(0, max_hits);

    // Retrieve the results, note that we build the result->results
    // list in reverse order and then reverse it before we return it
    result->num_hits = matches.get_matches_estimated ();
    result->fuzzy_search = false;

    for (Xapian::MSetIterator i = matches.begin(); i != matches.end(); ++i)
      {
        try
          {
            Xapian::Document doc = i.get_document();
            UnityPackageInfo *pkginfo = _pkginfo_from_document (doc);
            pkginfo->relevancy = i.get_percent();
            result->results = g_slist_prepend (result->results, pkginfo);
          }
        catch (Xapian::Error &e)
          {
            g_warning ("Unable to read document from result set: %s",
                       e.get_msg().c_str());
          }
      }

    result->results = g_slist_reverse (result->results);
  }
  catch(const Xapian::Error &e)
  {
    g_warning ("Error running query '%s': %s", search_string, e.get_msg().c_str());
  }

  return result;
}

static UnityPackageSearchResult*
libcolumbus_search(UnityPackageSearcher *searcher, const char *str) {
    Columbus::MatchResults results;
    UnityPackageSearchResult* result;

    result = g_slice_new0 (UnityPackageSearchResult);
    results = searcher->matcher->match(str);
    for (size_t i = 0; i < results.size(); ++i)
      {
        try
          {
            Xapian::Document doc = get_doc_from_col_match(searcher, results.getDocumentID(i));
            UnityPackageInfo *pkginfo = _pkginfo_from_document (doc);
            pkginfo->relevancy = results.getRelevancy(i);
            result->results = g_slist_prepend (result->results, pkginfo);
          }
        catch (Xapian::Error &e)
          {
            g_warning ("Unable to read document from result set: %s",
                       e.get_msg().c_str());
          }
      }

    result->results = g_slist_reverse (result->results);
    result->fuzzy_search = true;
    return result;
}

UnityPackageSearchResult*
unity_package_searcher_search (UnityPackageSearcher  *searcher,
                               const gchar           *search_string,
                               guint                  max_hits,
                               UnityPackageSearchType search_type,
                               UnityPackageSort       sort)
{
  UnityPackageSearchResult *xap_result;
  g_return_val_if_fail (searcher != NULL, NULL);
  g_return_val_if_fail (search_string != NULL, NULL);

  bool is_filtered = strstr(search_string, "category:") != NULL
    || strstr(search_string, "pkg_wildcard:") != NULL;
  const char *col_query_str = strstr(search_string, "AND");
  xap_result = xapian_search(searcher, search_string, max_hits, search_type, sort);
  // If Xapian does not find anything, try fuzzy matching.
  if (g_slist_length(xap_result->results) == 0 && (!is_filtered && col_query_str)) {
      g_slice_free(UnityPackageSearchResult, xap_result);
      col_query_str += 3;
      UnityPackageSearchResult *res = libcolumbus_search(searcher, col_query_str);
      return res;
  }
  return xap_result;
}


/**
 * Get applications matching given xapian filter query and additionally filter results out
 * using AppFilterCallback, until n_apps matching apps are found.
 * Results are filtered out if AppFilterCallback returns FALSE.
 */
UnityPackageSearchResult* unity_package_searcher_get_apps (UnityPackageSearcher *searcher,
                                                           const gchar          *filter_query,
                                                           guint                 n_apps,
                                                           AppFilterCallback    cb,
                                                           gpointer data)
{
  UnityPackageSearchResult *result;
  GHashTable               *unique;
  guint num_matches = 0;

  g_return_val_if_fail (searcher != NULL, NULL);

  unique = g_hash_table_new (g_str_hash, g_str_equal);

  result = g_slice_new0 (UnityPackageSearchResult);

  g_debug ("FILTER %s", filter_query);

  Xapian::Query query;
  try
  {
    query = searcher->query_parser->parse_query (filter_query, QUERY_PARSER_FILTER_FLAGS);
    searcher->enquire->set_sort_by_relevance ();
    searcher->enquire->set_query(query);

    Xapian::MSet matches = searcher->enquire->get_mset(0, searcher->db->get_doccount ());
    Xapian::MSetIterator iter = matches.begin();
    while (num_matches < n_apps && iter != matches.end())
    {
      Xapian::Document doc = iter.get_document ();
      UnityPackageInfo *pkginfo = _pkginfo_from_document (doc);

      if (g_hash_table_lookup_extended (unique, pkginfo->package_name, NULL, NULL) || cb(pkginfo, data) == FALSE)
      {
        unity_package_package_info_free (pkginfo);
      }
      else
      {
        g_hash_table_insert (unique, pkginfo->package_name, NULL);
        result->results = g_slist_prepend (result->results, pkginfo);
        num_matches++;
      }
      ++iter;
    }
  }
  catch (Xapian::Error e)
  {
    g_debug ("Error getting apps for query '%s': %s", filter_query, e.get_msg().c_str());
    return g_slice_new0 (UnityPackageSearchResult);
  }

  g_hash_table_unref (unique);

  result->num_hits = num_matches;
  result->fuzzy_search = false;
  return result;
}

UnityPackageSearchResult*
unity_package_searcher_get_random_apps (UnityPackageSearcher *searcher,
                                        const gchar          *filter_query,
                                        guint                 n_apps)
{
  UnityPackageSearchResult *result;
  GHashTable               *unique;
  guint                     i, n_unique, lastdocid, docid;

  g_return_val_if_fail (searcher != NULL, NULL);

  result = g_slice_new0 (UnityPackageSearchResult);
  result->num_hits = n_apps;
  result->fuzzy_search = false;
  lastdocid = searcher->db->get_lastdocid ();

  /* Since we really just pick random apps we may end up with dupes */
  unique = g_hash_table_new (g_str_hash, g_str_equal);

  /* When looking for random apps we check up to twice the number of
   * requested apps in order to try and avoid dupes. This is a sloppy
   * check, but works well enough in practice */
  if (filter_query == NULL)
  {
    g_debug ("RANDOM");
    for (i = 0, n_unique = 0; i < n_apps*2, n_unique < n_apps; i++)
    {
      Xapian::Document doc;
      try
      {
        doc = searcher->db->get_document (
          g_rand_int_range (searcher->random, 1, lastdocid));
        UnityPackageInfo *pkginfo = _pkginfo_from_document (doc);
        if (g_hash_table_lookup_extended (unique, pkginfo->package_name, NULL, NULL))
        {
          unity_package_package_info_free (pkginfo);
        }
        else
        {
          g_hash_table_insert (unique, pkginfo->package_name, NULL);
          result->results = g_slist_prepend (result->results, pkginfo);
          n_unique++;
        }
      }
      catch (Xapian::Error &e)
      {
        g_warning ("Error getting random apps: %s", e.get_msg().c_str());
        continue;
      }
    }
  }
  else
  {
    g_debug ("FILTER %s", filter_query);
    Xapian::Query query;
    try
    {
      query = searcher->query_parser->parse_query (filter_query, QUERY_PARSER_FILTER_FLAGS);
      searcher->enquire->set_sort_by_relevance ();
      searcher->enquire->set_query(query);
      Xapian::MSet matches = searcher->enquire->get_mset(0, searcher->db->get_doccount ());
      for (i = 0, n_unique = 0; i < n_apps*4, n_unique < n_apps; i++)
      {
        docid = g_rand_int_range (searcher->random, 0, matches.size ());
        Xapian::MSetIterator iter = matches[docid];
        Xapian::Document doc = iter.get_document ();
        UnityPackageInfo *pkginfo = _pkginfo_from_document (doc);
        if (g_hash_table_lookup_extended (unique, pkginfo->package_name, NULL, NULL))
        {
          unity_package_package_info_free (pkginfo);
        }
        else
        {
          g_hash_table_insert (unique, pkginfo->package_name, NULL);
          result->results = g_slist_prepend (result->results, pkginfo);
          n_unique++;
        }
      }
    }
    catch (Xapian::Error &e)
    {
      g_debug ("Error getting random apps for query '%s': %s", filter_query, e.get_msg().c_str());
      return g_slice_new0 (UnityPackageSearchResult);
    }
  }

  g_hash_table_unref (unique);

  result->results = g_slist_reverse (result->results);
  return result;
}

/**
 * Returns all apps that match exactly app+pkg names.
 * names must be a flat list of appname1, pkgname1, appname2, pkgname2... strings (must have even number of elements).
 */
UnityPackageSearchResult*
unity_package_searcher_get_by_exact_names (UnityPackageSearcher *searcher, GSList *names)
{
  g_return_val_if_fail (searcher != NULL, NULL);

  vector<Xapian::Query> query_list;
  GSList *iter = names;

  while (iter)
  {
    const string appname ((char *)iter->data);
    iter = g_slist_next (iter);
    if (iter == NULL)
    {
      g_warning ("List should have even list of arguments");
      return NULL;
    }

    const string pkgname = ((char *)iter->data);
    Xapian::Query query1 ("AA" + appname);
    Xapian::Query query2 ("AP" + pkgname);
    Xapian::Query query (Xapian::Query::OP_AND, query1, query2);
    query_list.push_back (query);

    iter = g_slist_next (iter);
  }

  guint num_matches = 0;
  UnityPackageSearchResult *result;

  result = g_slice_new0 (UnityPackageSearchResult);

  try
  {
    Xapian::Query top_query = Xapian::Query (Xapian::Query::OP_OR, query_list.begin (), query_list.end ());
    searcher->enquire->set_sort_by_relevance ();

    //this is crucial; we want all the requested packages, some commercial stuff don't have desktop files, we don't want to collapse them
    searcher->enquire->set_collapse_key(Xapian::BAD_VALUENO);
    searcher->enquire->set_query (top_query);

    Xapian::MSet matches = searcher->enquire->get_mset(0, searcher->db->get_doccount ());
    Xapian::MSetIterator iter = matches.begin();
    while (iter != matches.end ())
    {
      Xapian::Document doc = iter.get_document ();
      UnityPackageInfo *pkginfo = _pkginfo_from_document (doc);
      result->results = g_slist_prepend (result->results, pkginfo);
      num_matches++;
      ++iter;
    }
  }
  catch (Xapian::Error e)
  {
    g_debug ("Error getting apps: %s", e.get_msg().c_str());
    return g_slice_new0 (UnityPackageSearchResult);
  }

  result->num_hits = num_matches;
  result->fuzzy_search = false;
  return result;
}

UnityPackageInfo *
unity_package_searcher_get_by_desktop_file (UnityPackageSearcher *searcher, const gchar *desktop_file)
{
  g_return_val_if_fail (searcher != NULL, NULL);

  UnityPackageInfo *pkginfo = NULL;

  Xapian::PostingIterator it = searcher->db->postlist_begin ("");
  Xapian::PostingIterator end_it = searcher->db->postlist_end ("");

  const string query = desktop_file;

  while (it != end_it)
  {
    Xapian::Document doc = searcher->db->get_document (*it);
    string value = doc.get_value (XAPIAN_VALUE_DESKTOP_FILE);

    bool matches = false;
    size_t sep = value.find (':');
    if (sep != string::npos)
    {
      matches = value.compare (sep+1, value.size() - sep, query) == 0;
    }
    else
    {
      matches = value == query;
    }
    if (matches)
    {
      pkginfo = _pkginfo_from_document (doc);
      return pkginfo;
    }
    ++it;
  }

  return pkginfo;
}

UnityPackageSearchResult* unity_package_search_result_new ()
{
  UnityPackageSearchResult* result = g_slice_new0 (UnityPackageSearchResult);
  return result;
}

void
unity_package_search_result_free (UnityPackageSearchResult *result)
{
  g_return_if_fail (result != NULL);

  g_slist_free_full (result->results, unity_package_package_info_free);
  g_slice_free (UnityPackageSearchResult, result);
}
