"Matches:" instead of "Match of:"
[fremantle-contacts:contacts-merger.git] / lib / merger.c
1 /*
2  * Copyright (C) 2010 Collabora Ltd.
3  *   @author Marco Barisione <marco.barisione@collabora.co.uk>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18  */
19
20 #include "config.h"
21
22 #include <string.h>
23 #include <libebook/e-book-util.h>
24 #include "debug.h"
25 #include "merger.h"
26
27
28 /* MatchToken */
29
30 typedef struct
31 {
32     gchar *string;
33     OssoABookContact *contact;
34     gchar *description;
35     gboolean partial;
36 } MatchToken;
37
38 static MatchToken *
39 match_token_new (const gchar      *string,
40                  OssoABookContact *contact,
41                  const gchar      *description,
42                  gboolean          partial)
43 {
44     MatchToken *token;
45
46     g_return_val_if_fail (string, NULL);
47     g_return_val_if_fail (contact, NULL);
48     g_return_val_if_fail (description, NULL);
49
50     token = g_new0 (MatchToken, 1);
51     token->string = g_strdup (string);
52     token->contact = g_object_ref (contact);
53     token->description = g_strdup (description);
54     token->partial = partial;
55
56     return token;
57 }
58
59 static void
60 match_token_free (MatchToken *token)
61 {
62     if (!token)
63         return;
64
65     g_free (token->string);
66     g_object_unref (token->contact);
67     g_free (token->description);
68 }
69
70
71 /* MatchTokens table */
72
73 static void
74 free_value_queue (GQueue *queue)
75 {
76     if (!queue)
77         return;
78
79     g_queue_foreach (queue, (GFunc) match_token_free, NULL);
80     g_queue_free (queue);
81 }
82
83 static GHashTable *
84 match_tokens_table_new (void)
85 {
86     return g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
87             (GDestroyNotify) free_value_queue);
88 }
89
90 static void
91 match_tokens_table_add (GHashTable       *table,
92                         const gchar      *string,
93                         OssoABookContact *contact,
94                         const gchar      *description,
95                         gboolean          partial)
96 {
97     gchar *simplified_string;
98     MatchToken *token;
99     GQueue *queue;
100
101     g_return_if_fail (table);
102     g_return_if_fail (string);
103     g_return_if_fail (contact);
104     g_return_if_fail (OSSO_ABOOK_IS_CONTACT (contact));
105     g_return_if_fail (description);
106
107     simplified_string = string_simplify (string);
108
109     queue = g_hash_table_lookup (table, simplified_string);
110     if (!queue) {
111         queue = g_queue_new ();
112         /* Leave ownership of the simplified string */
113         g_hash_table_insert (table, simplified_string, queue);
114     } else {
115         g_free (simplified_string);
116     }
117
118     token = match_token_new (string, contact, description, partial);
119     g_queue_push_head (queue, token);
120 }
121
122
123 /* Token generation */
124
125 typedef struct
126 {
127     GHashTable *full_ids; /* Emails and IM names */
128     GHashTable *partial_ids; /* Local part of emails and IM names */
129     GHashTable *phones; /* Phone numbers */
130     GHashTable *full_names; /* First + last names and last + first names */
131     GHashTable *nicknames; /* Unparsed nicknames */
132
133     GHashTable *im_field_quarks; /* quark of vcard fields -> TRUE */
134 } SuggestionsData;
135
136 static gboolean
137 inster_generic_name (SuggestionsData    *data,
138                      OssoABookContact   *contact,
139                      const EContactName *name,
140                      const gchar        *description)
141 {
142     gchar *tmp;
143
144     if (IS_EMPTY (name->given) || IS_EMPTY (name->family))
145         return FALSE;
146
147     tmp = g_strdup_printf ("%s %s", name->given, name->family);
148     DEBUG ("      Add token for full name %s", tmp);
149     match_tokens_table_add (data->full_names, tmp, contact,
150             description, FALSE);
151     g_free (tmp);
152
153     tmp = g_strdup_printf ("%s %s", name->family, name->given);
154     DEBUG ("      Add token for reversed full name %s", tmp);
155     match_tokens_table_add (data->full_names, tmp, contact,
156             description, FALSE);
157     g_free (tmp);
158
159     if (!IS_EMPTY (name->additional)) {
160         EContactName *name_no_middle;
161
162         /* Bah, consts... */
163         name_no_middle = e_contact_name_copy ((EContactName*) name);
164         name_no_middle->additional[0] = '\0';
165         g_free (name_no_middle->given);
166         name_no_middle->given = g_strdup_printf ("%s %s", name->given,
167                 name->additional);
168
169         DEBUG ("      Retrying with the middle name %s too",
170             name->additional);
171         inster_generic_name (data, contact, name_no_middle, description);
172     }
173
174     return TRUE;
175 }
176
177 static void
178 insert_full_name (SuggestionsData    *data,
179                   OssoABookContact   *contact,
180                   const EContactName *name)
181 {
182     DEBUG ("    Add tokens for name:", name->given, name->family);
183
184     inster_generic_name (data, contact, name, _("name"));
185 }
186
187 static void
188 insert_nickname (SuggestionsData    *data,
189                  OssoABookContact   *contact,
190                  const gchar        *nickname)
191 {
192     EContactName *name;
193
194     DEBUG ("    Add tokens for nickname %s:", nickname);
195
196     name = e_contact_name_from_string (nickname);
197
198     if (!inster_generic_name (data, contact, name, _("nickname"))) {
199         /* If the name doesn't parse as a full name then it still makes sense
200          * to try to still match it with other nicknames. The chance of false
201          * positives seems lower than the one for considering first and last
202          * names by themselves */
203         DEBUG ("      Add token for unparsed nickname %s", nickname);
204         match_tokens_table_add (data->nicknames, nickname, contact,
205                 _("nickname"), FALSE);
206     }
207
208     e_contact_name_free (name);
209 }
210
211 static void
212 insert_id (SuggestionsData  *data,
213            OssoABookContact *contact,
214            const gchar      *id,
215            const gchar      *description)
216 {
217     gchar *modified_value;
218     gchar *tmp;
219     EContactName *ename;
220
221     DEBUG ("    Add token for ID %s", id);
222     match_tokens_table_add (data->full_ids, id, contact, description, FALSE);
223
224     /* If you have the same local part of your EMAIL or IM name across
225      * multiple services there are good chances you are the same person */
226     modified_value = g_strdup (id);
227     tmp = strpbrk (modified_value, "@%");
228     if (tmp)
229         *tmp = '\0';
230     /* FIXME: maybe we should skip admin, info, jobs and webmaster? */
231     DEBUG ("    Add token for partial ID %s", modified_value);
232     match_tokens_table_add (data->partial_ids, modified_value, contact,
233             description, TRUE);
234
235     /* Try to use the local part of the ID as a name */
236     g_strdelimit (modified_value, "-_.?/+", ' ');
237     DEBUG ("    Add tokens for parsed ID %s:", modified_value);
238     ename = e_contact_name_from_string (modified_value);
239     inster_generic_name (data, contact, ename, description);
240
241     e_contact_name_free (ename);
242     g_free (modified_value);
243 }
244
245 static gchar *
246 normalize_phone (const gchar *phone)
247 {
248     gchar *normalized;
249     gint offset;
250
251     g_return_val_if_fail (phone != NULL, FALSE);
252
253     /* Strip spaces, dashes, etc. */
254     normalized = e_normalize_phone_number (phone);
255
256     /* Strip DTMF chars */
257     offset = strcspn (normalized, OSSO_ABOOK_DTMF_CHARS);
258     normalized[offset] = '\0';
259
260     /* Only consider the last 7 digits of the number */
261     if (offset > 7) {
262         /* strcpy and similar functions don't handle overlapping
263          * areas */
264         gint offset_src = offset - 7;
265         gint offset_dest = 0;
266         while (normalized[offset_dest] != '\0')
267             normalized[offset_dest++] = normalized[offset_src++];
268         normalized[offset_dest] = '\0';
269     }
270
271     return normalized;
272 }
273
274 static void
275 insert_phone (SuggestionsData  *data,
276               OssoABookContact *contact,
277               const gchar      *phone)
278 {
279     gchar *normalized;
280
281     normalized = normalize_phone (phone);
282     DEBUG ("    Add token for phone %s (%s)", phone, normalized);
283     match_tokens_table_add (data->phones, normalized, contact,
284             _("phone"), FALSE);
285     g_free (normalized);
286 }
287
288 static void
289 insert_attr (SuggestionsData  *data,
290              OssoABookContact *original_contact,
291              OssoABookContact *flat_contact,
292              EVCardAttribute  *attr)
293 {
294     const gchar *attr_name;
295     GList *values;
296     GQuark name_quark;
297
298     attr_name = e_vcard_attribute_get_name (attr);
299     values = e_vcard_attribute_get_values (attr);
300     if (!values || IS_EMPTY (values->data))
301         return;
302
303     name_quark = g_quark_from_string (attr_name);
304
305     if (name_quark == OSSO_ABOOK_QUARK_VCA_EMAIL) {
306         insert_id (data, original_contact, values->data, _("email"));
307     } else if (name_quark == OSSO_ABOOK_QUARK_VCA_TEL) {
308         insert_phone (data, original_contact, values->data);
309     } else if (name_quark == OSSO_ABOOK_QUARK_VCA_FN) {
310         const EContactName *name;
311         /* There's no way to convert the GList in a EContactName... */
312         name = e_contact_get_const (E_CONTACT (flat_contact), E_CONTACT_NAME);
313         insert_full_name (data, original_contact, name);
314     } else if (name_quark == OSSO_ABOOK_QUARK_VCA_NICKNAME) {
315         insert_nickname (data, original_contact, values->data);
316     } else if (g_hash_table_lookup (data->im_field_quarks,
317                     GUINT_TO_POINTER (name_quark))) {
318         insert_id (data, original_contact, values->data, _("IM name"));
319     }
320 }
321
322
323 /* ContactPair */
324
325 typedef struct {
326     OssoABookContact *contact1;
327     OssoABookContact *contact2;
328 } ContactPair;
329
330 static ContactPair *
331 contact_pair_new (OssoABookContact *contact1,
332                   OssoABookContact *contact2)
333 {
334     ContactPair *pair;
335
336     g_return_val_if_fail (contact1, NULL);
337     g_return_val_if_fail (contact2, NULL);
338     g_return_val_if_fail (contact1 != contact2, NULL);
339
340     pair = g_new0 (ContactPair, 1);
341
342     /* Keep pair->contact1 < pair->contact2 to make contact_pair_equal work */
343     pair->contact1 = MIN (contact1, contact2);
344     pair->contact2 = MAX (contact1, contact2);
345
346     g_object_ref (pair->contact1);
347     g_object_ref (pair->contact2);
348
349     return pair;
350 }
351
352 static void
353 contact_pair_free (ContactPair *pair)
354 {
355     if (!pair)
356         return;
357
358     g_object_unref (pair->contact1);
359     g_object_unref (pair->contact2);
360     g_free (pair);
361 }
362
363 static guint
364 contact_pair_hash (gconstpointer p)
365 {
366     const ContactPair *pair = p;
367
368     g_return_val_if_fail (pair, 0);
369
370     return GPOINTER_TO_UINT (pair->contact1) +
371            GPOINTER_TO_UINT (pair->contact2);
372 }
373
374 static gboolean
375 contact_pair_equal (gconstpointer p1,
376                     gconstpointer p2)
377 {
378     const ContactPair *pair1 = p1;
379     const ContactPair *pair2 = p2;
380
381     if (pair1 == NULL && pair2 == NULL)
382         return TRUE;
383     else if (pair1 == NULL || pair2 == NULL)
384         return FALSE;
385     else
386         /* This works as we always have contact1 < contact2 */
387         return pair1->contact1 == pair2->contact1 &&
388                pair1->contact2 == pair2->contact2;
389 }
390
391
392 /* MatchInfo */
393
394 typedef struct {
395     gint score;
396     gboolean partial;
397 } MatchData;
398
399 typedef struct {
400     gint total_score;
401     GHashTable *descriptions; /* gchar* -> MatchData* */
402 }  MatchInfo;
403
404 static MatchInfo *
405 match_info_new (void)
406 {
407     MatchInfo *info;
408
409     info = g_new0 (MatchInfo, 1);
410     info->descriptions = g_hash_table_new_full (g_str_hash, g_str_equal,
411             g_free, g_free);
412
413     return info;
414 }
415
416 static void
417 match_info_free (MatchInfo *info)
418 {
419     if (!info)
420         return;
421
422     g_hash_table_unref (info->descriptions);
423     g_free (info);
424 }
425
426 static void
427 match_info_add_description (MatchInfo   *info,
428                             const gchar *description,
429                             gint         score,
430                             gboolean     partial)
431 {
432     MatchData *data;
433
434     data = g_hash_table_lookup (info->descriptions, description);
435     if (!data) {
436         data = g_new0 (MatchData, 1);
437         data->partial = TRUE;
438         g_hash_table_insert (info->descriptions, g_strdup (description), data);
439     }
440
441     data->score += score;
442     /* If "description" matches at least something in a full way then it's
443      * not reported as a partial match */
444     data->partial &= partial;
445 }
446
447 static void
448 match_info_add (MatchInfo   *info,
449                 const gchar *description1,
450                 const gchar *description2,
451                 gint         score,
452                 gboolean     partial)
453 {
454     g_return_if_fail (info);
455
456     info->total_score += score;
457     match_info_add_description (info, description1, score, partial);
458     match_info_add_description (info, description2, score, partial);
459 }
460
461
462 /* Matches hash table */
463
464 static GHashTable *
465 match_table_new (void)
466 {
467     return g_hash_table_new_full (contact_pair_hash, contact_pair_equal,
468             (GDestroyNotify) contact_pair_free,
469             (GDestroyNotify) match_info_free);
470 }
471
472 static void
473 match_table_add (GHashTable       *table,
474                  OssoABookContact *contact1,
475                  OssoABookContact *contact2,
476                  const gchar      *description1,
477                  const gchar      *description2,
478                  gint              score,
479                  gboolean          partial)
480 {
481     ContactPair *pair;
482     MatchInfo *info;
483
484     if (contact1 == contact2)
485         return;
486
487     pair = contact_pair_new (contact1, contact2);
488
489     info = g_hash_table_lookup (table, pair);
490     if (!info) {
491         info = match_info_new ();
492         g_hash_table_insert (table, pair, info); /* Leave ownership */
493     } else {
494         contact_pair_free (pair);
495     }
496
497     match_info_add (info, description1, description2, score, partial);
498 }
499
500
501 /* Match */
502
503 struct _Match {
504     guint ref_count;
505     ContactPair *pair;
506     MatchInfo *info;
507     gchar *description;
508 };
509
510 static Match *
511 match_new (ContactPair *pair,
512            MatchInfo   *info)
513 {
514     Match *match;
515
516     g_return_val_if_fail (pair, NULL);
517     g_return_val_if_fail (info, NULL);
518
519     match = g_new0 (Match, 1);
520     match->ref_count = 1;
521     match->pair = pair;
522     match->info = info;
523
524     return match;
525 }
526
527 Match *
528 match_ref (Match *match)
529 {
530     g_return_val_if_fail (match, NULL);
531
532     match->ref_count++;
533
534     return match;
535 }
536
537 void
538 match_unref (Match *match)
539 {
540     g_return_if_fail (match);
541     g_return_if_fail (match->ref_count > 0);
542
543     match->ref_count--;
544
545     if (match->ref_count == 0) {
546         contact_pair_free (match->pair);
547         match_info_free (match->info);
548         g_free (match->description);
549         g_free (match);
550     }
551 }
552
553 GType
554 match_get_type (void)
555 {
556     static GType our_type = 0;
557
558     if (G_UNLIKELY (our_type == 0))
559         our_type = g_boxed_type_register_static ("Match",
560                 (GBoxedCopyFunc) match_ref, (GBoxedFreeFunc) match_unref);
561
562     return our_type;
563 }
564
565 static gint
566 match_compare (gconstpointer a,
567                gconstpointer b)
568 {
569     const Match *ma = a;
570     const Match *mb = b;
571
572     g_return_val_if_fail (ma, -1);
573     g_return_val_if_fail (mb, 1);
574
575     return mb->info->total_score - ma->info->total_score;
576 }
577
578 typedef struct {
579     gchar *description;
580     MatchData *data;
581 } DescriptionMatchDataPair;
582
583 static gint
584 description_match_data_compare (gconstpointer a,
585                                 gconstpointer b)
586 {
587     const DescriptionMatchDataPair *pa = a;
588     const DescriptionMatchDataPair *pb = b;
589
590     g_return_val_if_fail (pa, 1);
591     g_return_val_if_fail (pb, -1);
592
593     return pb->data->score - pa->data->score;
594 }
595
596 void
597 match_get_contacts (Match             *match,
598                     OssoABookContact **c1,
599                     OssoABookContact **c2)
600 {
601     g_return_if_fail (match);
602
603     if (c1)
604         *c1 = match->pair->contact1;
605
606     if (c2)
607         *c2 = match->pair->contact2;
608 }
609
610 const gchar *
611 match_get_description (Match *match)
612 {
613     g_return_val_if_fail (match, NULL);
614
615     if (!match->description) {
616         GHashTableIter iter;
617         gpointer key, value;
618         GList *descriptions_list = NULL;
619         DescriptionMatchDataPair *pair;
620         GString *s;
621         GList *l;
622
623         g_hash_table_iter_init (&iter, match->info->descriptions);
624         while (g_hash_table_iter_next (&iter, &key, &value)) {
625             pair = g_new0 (DescriptionMatchDataPair, 1);
626             pair->description = key;
627             pair->data = value;
628             descriptions_list = g_list_prepend (descriptions_list, pair);
629         }
630
631         descriptions_list = g_list_sort (descriptions_list,
632                 description_match_data_compare);
633
634         s = g_string_new (_("Matches: "));
635
636         for (l = descriptions_list; l; l = l->next) {
637             pair = l->data;
638
639             if (!debug_is_enabled ())
640                 g_string_append_printf (s, "%s%s", pair->description,
641                         pair->data->partial ? " (partial)" : "");
642             else
643                 g_string_append_printf (s, "%s (%s%d)", pair->description,
644                         pair->data->partial ? "partial, " : "",
645                         pair->data->score);
646
647             if (l->next)
648                 g_string_append (s, ", ");
649
650             g_free (pair);
651         }
652
653         match->description = g_string_free (s, FALSE);
654
655         g_list_free (descriptions_list);
656     }
657
658     return match->description;
659 }
660
661 gint
662 match_get_score (Match *match)
663 {
664     return match->info->total_score;
665 }
666
667
668 /* Match list */
669
670 static GList *
671 match_list_from_table_steal (GHashTable *table)
672 {
673     GHashTableIter iter;
674     gpointer key, value;
675     Match *match;
676     GList *matches = NULL;
677
678     g_return_val_if_fail (table, NULL);
679
680     g_hash_table_iter_init (&iter, table);
681     while (g_hash_table_iter_next (&iter, &key, &value)) {
682         match = match_new (key, value);
683         matches = g_list_prepend (matches, match);
684     }
685
686     matches = g_list_sort (matches, match_compare);
687
688     g_hash_table_steal_all (table);
689
690     return matches;
691 }
692
693 void
694 match_list_free (GList *matches)
695 {
696     g_list_foreach (matches, (GFunc) match_unref, NULL);
697     g_list_free (matches);
698 }
699
700 GList *
701 match_list_copy (GList *matches)
702 {
703     GList *copy;
704
705     copy = g_list_copy (matches);
706     g_list_foreach (copy, (GFunc) match_ref, NULL);
707
708     return copy;
709 }
710
711 GType
712 match_list_get_type (void)
713 {
714     static GType our_type = 0;
715
716     if (G_UNLIKELY (our_type == 0))
717         our_type = g_boxed_type_register_static ("MatchList",
718                 (GBoxedCopyFunc) match_list_copy,
719                 (GBoxedFreeFunc) match_list_free);
720
721     return our_type;
722 }
723
724
725 /* Suggestions */
726
727 /* Matched both the full ID and partial one and also the ones coming from
728  * parsing the ID as a name */
729 #define SCORE_IDS          30
730 #define SCORE_PARTIAL_IDS  10
731 #define SCORE_PHONES       15
732 /* Matches twice because of the reverse combination */
733 #define SCORE_FULL_NAME    10
734 #define SCORE_NICKNAMES    10
735
736 static void
737 generate_same_kind_suggestions (GHashTable *matches,
738                                 GHashTable *tokens_table,
739                                 gint        score)
740 {
741     GHashTableIter iter;
742     gpointer value;
743     GQueue *queue;
744     GList *l1, *l2;
745     MatchToken *t1, *t2;
746
747     g_return_if_fail (matches);
748     g_return_if_fail (tokens_table);
749
750     g_hash_table_iter_init (&iter, tokens_table);
751     while (g_hash_table_iter_next (&iter, NULL, &value)) {
752         queue = value;
753
754         for (l1 = queue->head; l1; l1 = l1->next) {
755             t1 = l1->data;
756             for (l2 = l1->next; l2; l2 = l2->next) {
757                 t2 = l2->data;
758                 DEBUG ("    %s (uid: %s) matches %s (uid: %s)\n"
759                     "      as %s (%s) matches %s (%s)",
760                     osso_abook_contact_get_display_name (t1->contact),
761                     osso_abook_contact_get_uid (t1->contact),
762                     osso_abook_contact_get_display_name (t2->contact),
763                     osso_abook_contact_get_uid (t2->contact),
764                     t1->string, t1->description,
765                     t2->string, t2->description);
766                 match_table_add (matches,
767                     t1->contact, t2->contact,
768                     t1->description, t2->description,
769                     score, t1->partial || t2->partial);
770             }
771         }
772     }
773 }
774
775
776 /* Merge suggestions generation */
777
778 GList * /* List of Match* */
779 generate_merge_suggestions (GList *contacts,
780                             GList *im_fields)
781 {
782     SuggestionsData *data;
783     GHashTable *match_table;
784     GList *matches;
785     GList *l;
786
787     data = g_new0 (SuggestionsData, 1);
788     data->full_ids = match_tokens_table_new ();
789     data->partial_ids = match_tokens_table_new ();
790     data->phones = match_tokens_table_new ();
791     data->full_names = match_tokens_table_new ();
792     data->nicknames = match_tokens_table_new ();
793     data->im_field_quarks = g_hash_table_new (g_direct_hash, g_direct_equal);
794     for (l = im_fields; l; l = l->next) {
795         /* Skip phone numbers are they are handled separately */
796         if (strcmp (l->data, "TEL") != 0)
797             g_hash_table_insert (data->im_field_quarks,
798                     GUINT_TO_POINTER (g_quark_from_string (l->data)),
799                     GINT_TO_POINTER (TRUE));
800     }
801
802     DEBUG ("Analysing contacts:");
803
804     for (; contacts; contacts = contacts->next) {
805         OssoABookContact *original_contact = contacts->data;
806         OssoABookContact *flat_contact;
807         GList *attrs;
808
809         DEBUG ("  Analysing %s (uid: %s):",
810             osso_abook_contact_get_display_name (original_contact),
811             osso_abook_contact_get_uid (original_contact));
812
813         /* We care about the flattened contact as it's also easier to analyse */
814         flat_contact = osso_abook_contact_merge_roster_info (original_contact);
815
816         attrs = e_vcard_get_attributes (E_VCARD (flat_contact));
817         for (; attrs; attrs = attrs->next) {
818             insert_attr (data, original_contact, flat_contact, attrs->data);
819         }
820
821         g_object_unref (flat_contact);
822     }
823
824     DEBUG ("Generating all the matches:");
825     match_table = match_table_new ();
826     DEBUG ("  Full IDs:");
827     generate_same_kind_suggestions (match_table, data->full_ids, SCORE_IDS);
828     DEBUG ("  Partial IDs:");
829     generate_same_kind_suggestions (match_table, data->partial_ids,
830             SCORE_PARTIAL_IDS);
831     DEBUG ("  Phone numbers:");
832     generate_same_kind_suggestions (match_table, data->phones, SCORE_PHONES);
833     DEBUG ("  Full names:");
834     generate_same_kind_suggestions (match_table, data->full_names,
835             SCORE_FULL_NAME);
836     DEBUG ("  Nicknames:");
837     generate_same_kind_suggestions (match_table, data->nicknames,
838             SCORE_NICKNAMES);
839
840     DEBUG ("Matches are:");
841     matches = match_list_from_table_steal (match_table);
842
843     g_hash_table_unref (match_table);
844     g_hash_table_unref (data->im_field_quarks);
845     g_hash_table_unref (data->nicknames);
846     g_hash_table_unref (data->full_names);
847     g_hash_table_unref (data->phones);
848     g_hash_table_unref (data->partial_ids);
849     g_hash_table_unref (data->full_ids);
850     g_free (data);
851
852     return matches;
853 }
854
855
856 /* Generate list of contacts to merge */
857
858 static GHashTable *
859 contacts_set_new ()
860 {
861     return g_hash_table_new (g_direct_hash, g_direct_equal);
862 }
863
864 static void
865 contacts_set_add (GHashTable       *set,
866                   OssoABookContact *contact)
867 {
868     g_hash_table_insert (set, contact, GUINT_TO_POINTER (TRUE));
869 }
870
871 static void
872 contacts_set_extend (GHashTable *dest,
873                      GHashTable *src)
874 {
875     GHashTableIter iter;
876     gpointer key;
877
878     g_hash_table_iter_init (&iter, src);
879     while (g_hash_table_iter_next (&iter, &key, NULL))
880         contacts_set_add (dest, key);
881 }
882
883 static void
884 merge_table_set (GHashTable       *table,
885                  OssoABookContact *contact,
886                  GHashTable       *set)
887 {
888     g_hash_table_insert (table, contact, g_hash_table_ref (set));
889 }
890
891 static void
892 merge_table_replace (GHashTable *table,
893                      GHashTable *old,
894                      GHashTable *new)
895 {
896     GList *keys;
897
898     keys = g_hash_table_get_keys (table);
899     while (keys) {
900         OssoABookContact *c = keys->data;
901
902         if (g_hash_table_lookup (table, c) == old)
903             merge_table_set (table, c, new);
904
905         keys = g_list_delete_link (keys, keys);
906     }
907 }
908
909 static GList * /* of GList of OssoABookContact* */
910 generate_merge_lists (GList *matches)
911 {
912     GHashTable *table;
913     GList *l;
914     OssoABookContact *c1;
915     OssoABookContact *c2;
916     GHashTable *set_c1;
917     GHashTable *set_c2;
918     GHashTableIter iter;
919     gpointer value;
920     GList *all_merges = NULL;
921
922     /* We have a list of pairs of contacts, with the same contact possibly
923      * in multiple pairs, and we have to transform this is a list of lists
924      * of contacts to merge.
925      * If, for example, we have (A, B), (C, D), (E, F) and (A, F) we need to
926      * get (A, B, E, F) and (C, D). */
927
928     /* OssoABookContact -> Set<OssoABookContact>
929      * Each contact points to a set of contacts. The set contains the
930      * contact itself plus the other contacts that should be merged with
931      * it */
932     table = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
933                 (GDestroyNotify) g_hash_table_unref);
934
935     for (l = matches; l; l = l->next) {
936         match_get_contacts (l->data, &c1, &c2);
937
938         set_c1 = g_hash_table_lookup (table, c1);
939         set_c2 = g_hash_table_lookup (table, c2);
940
941         if (set_c1 == NULL && set_c2 == NULL) {
942             /* Neither of the contacts is in the table yet, create a
943              * set for both of them */
944             set_c1 = contacts_set_new ();
945             contacts_set_add (set_c1, c1);
946             contacts_set_add (set_c1, c2);
947             merge_table_set (table, c1, set_c1);
948             merge_table_set (table, c2, set_c1);
949             g_hash_table_unref (set_c1);
950         } else if (set_c1 != NULL && set_c2 != NULL) {
951             /* Both of the contacts are already in the table, we
952              * copy all the contacts from the second set to the
953              * first one. Then we replace the second set in the
954              * table with the first one (that now contains the
955              * union of the two sets) */
956             contacts_set_extend (set_c1, set_c2);
957             merge_table_replace (table, set_c2, set_c1);
958         } else if (set_c1 != NULL) {
959             /* We already have a set, so we just add the other
960              * contact to it */
961             contacts_set_add (set_c1, c2);
962             merge_table_set (table, c2, set_c1);
963         } else {
964             /* Ditto */
965             contacts_set_add (set_c2, c1);
966             merge_table_set (table, c1, set_c2);
967         }
968     }
969
970     /* Now we get the list of merges from the table */
971     g_hash_table_iter_init (&iter, table);
972     while (g_hash_table_iter_next (&iter, NULL, &value)) {
973         GHashTable *set = value;
974         GList *contacts;
975
976         contacts = g_hash_table_get_keys (set);
977
978         /* This set was already added */
979         if (!contacts)
980             continue;
981
982         g_list_foreach (contacts, (GFunc) g_object_ref, NULL);
983         all_merges = g_list_prepend (all_merges, contacts);
984
985         /* There are multiple contacts referring to the same set.
986          * We empty the set so that the next time we find it we can
987          * skip it. */
988         g_hash_table_remove_all (set);
989     }
990
991     g_hash_table_unref (table);
992
993     return all_merges;
994 }
995
996
997 /* Merge contacts */
998
999 /* We cannot use more for now or the dialogs for merge resolution
1000  * could come up while other ones are visible :( */
1001 #define MAX_PENDING 1
1002
1003 typedef struct
1004 {
1005     gint pending_count;
1006     gint merged_count;
1007     gint failed_count;
1008     GList *contacts_to_merge;
1009     GList *failed;
1010     GtkWindow *parent;
1011     MergeFinishedCb cb;
1012     gpointer user_data;
1013 } MergeData;
1014
1015 typedef struct
1016 {
1017     MergeData *merge_data;
1018     GList *contacts;
1019 } MergeCbData;
1020
1021 static void merge_continue (MergeData *data);
1022
1023 static void
1024 free_contacts_list (GList *contacts)
1025 {
1026     g_list_foreach (contacts, (GFunc) g_object_unref, NULL);
1027     g_list_free (contacts);
1028 }
1029
1030 static gboolean
1031 merge_continue_from_idle_cb (gpointer user_data)
1032 {
1033     MergeData *data = user_data;
1034
1035     merge_continue (data);
1036
1037     return FALSE;
1038 }
1039
1040 static void
1041 contacts_merged_cb (const gchar *uid,
1042                     gpointer     user_data)
1043 {
1044     MergeCbData *merge_cb_data = user_data;
1045     MergeData *data = merge_cb_data->merge_data;
1046
1047     if (uid) {
1048         DEBUG ("Merge of %d contacts succeeded, new UID: %s",
1049             g_list_length (merge_cb_data->contacts), uid);
1050         data->merged_count += g_list_length (merge_cb_data->contacts);
1051         free_contacts_list (merge_cb_data->contacts);
1052     } else {
1053         DEBUG ("Merge of %d contacts failed",
1054             g_list_length (merge_cb_data->contacts));
1055         data->failed_count += g_list_length (merge_cb_data->contacts);
1056         data->failed = g_list_prepend (data->failed, merge_cb_data->contacts);
1057     }
1058
1059     g_slice_free (MergeCbData, merge_cb_data);
1060
1061     data->pending_count--;
1062
1063     /* The merge function can show a dialog with gtk_dialog_run()
1064      * and this can make us lose some D-Bus method returns, see
1065      * https://bugs.freedesktop.org/show_bug.cgi?id=14581 */
1066     gdk_threads_add_idle_full (G_PRIORITY_HIGH_IDLE,
1067             merge_continue_from_idle_cb, data, NULL);
1068 }
1069
1070 static void
1071 merge_continue (MergeData *data)
1072 {
1073     if (data->pending_count >= MAX_PENDING)
1074         return;
1075
1076     if (data->pending_count == 0 && data->contacts_to_merge == NULL) {
1077         /* We are done */
1078         DEBUG ("Done merging %d contacts, %d failed",
1079             data->merged_count, data->failed_count);
1080
1081         if (data->cb)
1082             data->cb (data->merged_count, data->failed_count,
1083                     data->failed, data->user_data);
1084
1085         g_list_foreach (data->failed, (GFunc) free_contacts_list, NULL);
1086         g_list_free (data->failed);
1087         g_slice_free (MergeData, data);
1088         return;
1089     }
1090
1091     while (data->pending_count < MAX_PENDING && data->contacts_to_merge) {
1092         GList *contacts = data->contacts_to_merge->data;
1093         MergeCbData *merge_cb_data;
1094
1095         DEBUG ("Scheduling merge of %d contacts", g_list_length (contacts));
1096
1097         merge_cb_data = g_slice_new0 (MergeCbData);
1098         merge_cb_data->merge_data = data;
1099         merge_cb_data->contacts = contacts;
1100
1101         data->pending_count++;
1102         data->contacts_to_merge = g_list_delete_link (
1103                 data->contacts_to_merge, data->contacts_to_merge);
1104
1105         osso_abook_merge_contacts_and_save (contacts, data->parent,
1106                 contacts_merged_cb, merge_cb_data);
1107     }
1108 }
1109
1110 void
1111 merge_contacts (GList           *matches,
1112                 GtkWindow       *parent,
1113                 MergeFinishedCb  cb,
1114                 gpointer         user_data)
1115 {
1116     GList *contacts_to_merge;
1117     GList *l;
1118     GList *contacts;
1119     GList *m;
1120     MergeData *data;
1121
1122     g_return_if_fail (matches);
1123
1124     contacts_to_merge = generate_merge_lists (matches);
1125
1126     if (debug_is_enabled ()) {
1127         for (l = contacts_to_merge; l; l = l->next) {
1128             contacts = l->data;
1129             DEBUG ("Going to merge:");
1130             for (m = contacts; m; m = m->next) {
1131                 DEBUG ("  %s (%s)",
1132                         osso_abook_contact_get_display_name (m->data),
1133                         osso_abook_contact_get_uid (m->data));
1134             }
1135         }
1136     }
1137
1138     data = g_slice_new0 (MergeData);
1139     data->contacts_to_merge = contacts_to_merge;
1140     data->parent = parent;
1141     data->cb = cb;
1142     data->user_data = user_data;
1143
1144     merge_continue (data);
1145 }