/*
 * Copyright (C) 2011 Alexander Larsson <alexl@redhat.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 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 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/>.
 */

using Gtk;
using Gee;
using Folks;

public class Contacts.TypeSet : Object  {
  const string X_GOOGLE_LABEL = "x-google-label";
  const int MAX_TYPES = 3;
  private struct InitData {
    unowned string display_name_u;
    unowned string types[3]; //MAX_TYPES
  }

  private class TypeDescriptor : Object {
    public string display_name; // Translated
    public GLib.List<InitData*> init_data;
    public TreeIter iter; // Set if in_store
    public bool in_store;
  }

  // Dummy TypeDescriptor to mark the "Custom..." store entry
  private static TypeDescriptor custom_dummy = new TypeDescriptor ();
  // Dummy TypeDescriptor to mark the "Other..." store entry
  private static TypeDescriptor other_dummy = new TypeDescriptor ();

  // Map from translated display name to TypeDescriptor for all "standard" types
  private HashTable<unowned string, TypeDescriptor> display_name_hash;
  // Map from all type strings in to list of InitData with the data in it
  private HashTable<unowned string, GLib.List<InitData*> > vcard_lookup_hash;
  // Map from display name to TreeIter for all custom types
  private HashTable<string, TreeIter?> custom_hash;

  public Gtk.ListStore store;
  private TreeIter other_iter;

  private TypeSet () {
    display_name_hash = new HashTable<unowned string, TypeDescriptor> (str_hash, str_equal);
    vcard_lookup_hash = new HashTable<unowned string, GLib.List<InitData*> > (str_hash, str_equal);
    custom_hash = new HashTable<string, TreeIter? > (str_hash, str_equal);

    store = new Gtk.ListStore (2,
                               // Display name or null for separator
                               typeof(string?),
                               // TypeDescriptor for standard types, null for custom
                               typeof (TypeDescriptor));
  }

  private void add_descriptor_to_store (TypeDescriptor descriptor, bool is_custom) {
    if (descriptor.in_store)
      return;

    descriptor.in_store = true;
    if (is_custom)
      this.store.insert_before (out descriptor.iter, null);
    else
      this.store.append (out descriptor.iter);

    store.set (descriptor.iter, 0, descriptor.display_name, 1, descriptor);
  }

  private void add_init_data (InitData *init_data) {
    unowned string dn = dgettext (Config.GETTEXT_PACKAGE, init_data.display_name_u);
    TypeDescriptor descriptor = display_name_hash.lookup (dn);
    if (descriptor == null) {
      descriptor = new TypeDescriptor ();
      descriptor.display_name = dn;
      display_name_hash.insert (dn, descriptor);
    }

    descriptor.init_data.append (init_data);

    for (int j = 0; j < MAX_TYPES && init_data.types[j] != null; j++) {
      unowned string type = init_data.types[j];
      unowned GLib.List<InitData*> l = this.vcard_lookup_hash.lookup (type);
      if (l != null) {
        l.append (init_data);
      } else {
        GLib.List<InitData*> l2 = null;
        l2.append (init_data);
        this.vcard_lookup_hash.insert (type, (owned) l2);
      }
    }
  }

  private void add_init_data_done (string[] standard_untranslated) {
    foreach (var untranslated in standard_untranslated) {
      var descriptor = display_name_hash.lookup (dgettext (Config.GETTEXT_PACKAGE, untranslated));
      if (descriptor != null)
        add_descriptor_to_store (descriptor, false);
      else
        error ("Internal error: Can't find display name %s in TypeSet data", untranslated);
    }

    store.append (out other_iter);
    /* Refers to the type of the detail, could be Home, Work or Other for email, and the same
     * for phone numbers, addresses, etc. */
    store.set (other_iter, 0, _("Other"), 1, other_dummy);
  }

  public void add_custom_label (string label, out TreeIter iter) {
    // If we add a custom name equal to one of the standard ones, reuse that one
    var descriptor = display_name_hash.lookup (label);
    if (descriptor != null) {
      add_descriptor_to_store (descriptor, true);
      iter = descriptor.iter;
      return;
    }

    if (label == _("Other")) {
      iter = other_iter;
      return;
    }

    unowned TreeIter? iterp = custom_hash.lookup (label);
    if (iterp != null) {
      iter = iterp;
      return;
    }

    store.insert_before (out iter, null);
    store.set (iter, 0, label, 1, null);
    custom_hash.insert (label, iter);
  }

  private unowned TypeDescriptor? lookup_descriptor_by_string (string str) {
    unowned GLib.List<InitData *>? l = vcard_lookup_hash.lookup (str);
    foreach (unowned InitData *d in l) {
      if (d.types[1] == null) {
        unowned string dn = dgettext (Config.GETTEXT_PACKAGE, d.display_name_u);
        return display_name_hash.lookup (dn);
      }
    }

    return null;
  }


  private unowned TypeDescriptor? lookup_descriptor (AbstractFieldDetails detail) {
    var i = detail.get_parameter_values ("type");
    if (i == null || i.is_empty)
      return null;

    var list = new Gee.ArrayList<string> ();
    foreach (var s in detail.get_parameter_values ("type"))
      list.add (s.up ());

    // Make sure all items in the InitData is in the specified type, there might
    // be more, but we ignore them (so a HOME,FOO,PREF,BLAH contact still matches
    // the standard HOME one, but not HOME,FAX
    unowned GLib.List<InitData *>? l = vcard_lookup_hash.lookup (list[0]);
    foreach (unowned InitData *d in l) {
      bool all_found = true;
      for (int j = 0; j < MAX_TYPES && d.types[j] != null; j++) {
        if (!list.contains (d.types[j])) {
          all_found = false;
          break;
        }
      }
      if (all_found) {
        unowned string dn = dgettext (Config.GETTEXT_PACKAGE, d.display_name_u);
        return display_name_hash.lookup (dn);
      }
    }

    return null;
  }

  // Looks up (and creates if necessary) the type in the store
  public void lookup_type (AbstractFieldDetails detail, out TreeIter iter) {
    if (detail.parameters.contains (X_GOOGLE_LABEL)) {
      var label = Utils.get_first<string> (detail.parameters.get (X_GOOGLE_LABEL));
      add_custom_label (label, out iter);
      return;
    }

    unowned TypeDescriptor? d = lookup_descriptor (detail);
    if (d != null) {
      add_descriptor_to_store (d, true);
      iter = d.iter;
    } else {
      iter = other_iter;
    }
  }

  public void lookup_type_by_string (string type, out TreeIter iter) {
    unowned TypeDescriptor? d = lookup_descriptor_by_string (type);
    iter = (d != null)? d.iter : this.other_iter;
  }

  public string format_type (AbstractFieldDetails detail) {
    if (detail.parameters.contains (X_GOOGLE_LABEL))
      return Utils.get_first<string> (detail.parameters[X_GOOGLE_LABEL]);

    unowned TypeDescriptor? d = lookup_descriptor (detail);
    return (d != null)? d.display_name : _("Other");
  }

  public void update_details (AbstractFieldDetails details, TreeIter iter) {
    var old_parameters = details.parameters;
    details.parameters = new HashMultiMap<string, string> ();
    bool has_pref = false;
    foreach (var val in old_parameters["type"]) {
      if (val.ascii_casecmp ("PREF") == 0) {
        has_pref = true;
        break;
      }
    }
    foreach (var param in old_parameters.get_keys()) {
      if (param != "type" && param != X_GOOGLE_LABEL)
        foreach (var val in old_parameters[param])
          details.parameters[param] = val;
    }

    TypeDescriptor descriptor;
    string display_name;
    store.get (iter, 0, out display_name, 1, out descriptor);

    assert (display_name != null); // Not separator
    assert (descriptor != custom_dummy); // Not custom...

    if (descriptor == null) { // A custom label
      details.parameters["type"] = "OTHER";
      details.parameters[X_GOOGLE_LABEL] = display_name;
    } else {
      if (descriptor == other_dummy) {
        details.parameters["type"] = "OTHER";
      } else {
        InitData *init_data = descriptor.init_data.data;
        for (int j = 0; j < MAX_TYPES && init_data.types[j] != null; j++)
          details.parameters["type"] = init_data.types[j];
      }
    }

    if (has_pref)
      details.parameters["type"] = "PREF";
  }

  public bool is_custom (TreeIter iter) {
    TypeDescriptor descriptor;
    store.get (iter, 1, out descriptor);
    return descriptor == custom_dummy;
  }

  private static TypeSet _general;
  private const InitData[] general_data = {
    // List most specific first, always in upper case
    { N_("Home"), { "HOME" } },
    { N_("Work"), { "WORK" } }
  };
  public static TypeSet general {
    get {
      string[] standard = {
        "Work", "Home"
      };

      if (_general == null) {
        _general = new TypeSet ();
        for (int i = 0; i < general_data.length; i++)
          _general.add_init_data (&general_data[i]);
        _general.add_init_data_done (standard);
      }

      return _general;
    }
  }

  private static TypeSet _email;
  private const InitData[] email_data = {
    // List most specific first, always in upper case
    { N_("Personal"),    { "PERSONAL" } },
    { N_("Home"),        { "HOME" } },
    { N_("Work"),        { "WORK" } }
  };
  public static TypeSet email {
    get {
      string[] standard = {
        "Personal", "Home", "Work"
      };

      if (_email == null) {
        _email = new TypeSet ();
        for (int i = 0; i < email_data.length; i++)
          _email.add_init_data (&email_data[i]);
        _email.add_init_data_done (standard);
      }

      return _email;
    }
  }

  private static TypeSet _phone;
  public static TypeSet phone {
    get {
      const InitData[] data = {
        // List most specific first, always in upper case
        { N_("Assistant"),  { "X-EVOLUTION-ASSISTANT" } },
        { N_("Work"),       { "WORK", "VOICE" } },
        { N_("Work Fax"),   { "WORK", "FAX" } },
        { N_("Callback"),   { "X-EVOLUTION-CALLBACK" } },
        { N_("Car"),        { "CAR" } },
        { N_("Company"),    { "X-EVOLUTION-COMPANY" } },
        { N_("Home"),       { "HOME", "VOICE" } },
        { N_("Home Fax"),   { "HOME", "FAX" } },
        { N_("ISDN"),       { "ISDN" } },
        { N_("Mobile"),     { "CELL" } },
        { N_("Other"),      { "VOICE" } },
        { N_("Fax"),        { "FAX" } },
        { N_("Pager"),      { "PAGER" } },
        { N_("Radio"),      { "X-EVOLUTION-RADIO" } },
        { N_("Telex"),      { "X-EVOLUTION-TELEX" } },
        /* To translators: TTY is Teletypewriter */
        { N_("TTY"),        { "X-EVOLUTION-TTYTDD" } }
      };

      // Make sure these strings are the same as the above
      string[] standard = {
        "Mobile", "Work", "Home"
      };

      if (_phone == null) {
        _phone = new TypeSet ();
        for (int i = 0; i < data.length; i++)
          _phone.add_init_data (&data[i]);
        for (int i = 0; i < general_data.length; i++)
          _phone.add_init_data (&general_data[i]);
        _phone.add_init_data_done (standard);
      }

      return _phone;
    }
  }
}
