/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const kNonVcardFields =
        ["NickNameContainer", "SecondaryEmailContainer", "ScreenNameContainer",
         "customFields", "preferDisplayName"];

const kPhoneticFields =
        ["PhoneticLastName", "PhoneticLabel1", "PhoneticSpacer1",
         "PhoneticFirstName", "PhoneticLabel2", "PhoneticSpacer2"];

// Item is |[dialogField, cardProperty]|.
const kVcardFields =
        [ // Contact > Name
         ["FirstName", "FirstName"],
         ["LastName", "LastName"],
         ["DisplayName", "DisplayName"],
         ["NickName", "NickName"],
          // Contact > Internet
         ["PrimaryEmail", "PrimaryEmail"],
         ["SecondEmail", "SecondEmail"],
          // Contact > Phones
         ["WorkPhone", "WorkPhone"],
         ["HomePhone", "HomePhone"],
         ["FaxNumber", "FaxNumber"],
         ["PagerNumber", "PagerNumber"],
         ["CellularNumber", "CellularNumber"],
          // Address > Home
         ["HomeAddress", "HomeAddress"],
         ["HomeAddress2", "HomeAddress2"],
         ["HomeCity", "HomeCity"],
         ["HomeState", "HomeState"],
         ["HomeZipCode", "HomeZipCode"],
         ["HomeCountry", "HomeCountry"],
         ["WebPage2", "WebPage2"],
          // Address > Work
         ["JobTitle", "JobTitle"],
         ["Department", "Department"],
         ["Company", "Company"],
         ["WorkAddress", "WorkAddress"],
         ["WorkAddress2", "WorkAddress2"],
         ["WorkCity", "WorkCity"],
         ["WorkState", "WorkState"],
         ["WorkZipCode", "WorkZipCode"],
         ["WorkCountry", "WorkCountry"],
         ["WebPage1", "WebPage1"],
          // Other > (custom)
         ["Custom1", "Custom1"],
         ["Custom2", "Custom2"],
         ["Custom3", "Custom3"],
         ["Custom4", "Custom4"],
          // Other > Notes
         ["Notes", "Notes"],
          // Chat
         ["Yahoo", "_Yahoo"],
         ["Skype", "_Skype"],
         ["QQ", "_QQ"],
         ["MSN", "_MSN"],
         ["ICQ", "_ICQ"],
         ["XMPP", "_JabberId"],
         ["IRC", "_IRC"]
        ];

var gEditCard;
var gOnSaveListeners = [];
var gOnLoadListeners = [];
var gOkCallback = null;
var gHideABPicker = false;
var gPhotoHandlers = {};
// If any new photos were added to the card, this stores the name of the original
// and any temporary new filenames used to store photos of the card.
// 'null' is a valid value when there was no photo (e.g. the generic photo).
var gOldPhotos = [];
// If a new photo was added, the name is stored here.
var gNewPhoto = null;

function OnLoadNewCard()
{
  InitEditCard();

  gEditCard.card =
    (("arguments" in window) && (window.arguments.length > 0) &&
     (window.arguments[0] instanceof Ci.nsIAbCard))
    ? window.arguments[0]
    : Cc["@mozilla.org/addressbook/cardproperty;1"]
        .createInstance(Ci.nsIAbCard);
  gEditCard.titleProperty = "newContactTitle";
  gEditCard.selectedAB = "";

  if ("arguments" in window && window.arguments[0])
  {
    gEditCard.selectedAB = kPersonalAddressbookURI;

    if ("selectedAB" in window.arguments[0] &&
        (window.arguments[0].selectedAB != kAllDirectoryRoot + "?")) {
      // check if selected ab is a mailing list
      var abURI = window.arguments[0].selectedAB;

      var directory = GetDirectoryFromURI(abURI);
      if (directory.isMailList) {
        var parentURI = GetParentDirectoryFromMailingListURI(abURI);
        if (parentURI)
          gEditCard.selectedAB = parentURI;
      }
      else if (!directory.readOnly)
        gEditCard.selectedAB = window.arguments[0].selectedAB;
    }

    // we may have been given properties to pre-initialize the window with....
    // we'll fill these in here...
    if ("primaryEmail" in window.arguments[0])
      gEditCard.card.primaryEmail = window.arguments[0].primaryEmail;
    if ("displayName" in window.arguments[0]) {
      gEditCard.card.displayName = window.arguments[0].displayName;
      // if we've got a display name, don't generate
      // a display name (and stomp on the existing display name)
      // when the user types a first or last name
      if (gEditCard.card.displayName)
        gEditCard.generateDisplayName = false;
    }
    if ("okCallback" in window.arguments[0])
      gOkCallback = window.arguments[0].okCallback;

    if ("escapedVCardStr" in window.arguments[0]) {
      // hide non vcard values
      HideNonVcardFields();
      gEditCard.card = Cc["@mozilla.org/addressbook/msgvcardservice;1"]
                         .getService(Ci.nsIMsgVCardService)
                         .escapedVCardToAbCard(window.arguments[0].escapedVCardStr);
    }

    if ("titleProperty" in window.arguments[0])
      gEditCard.titleProperty = window.arguments[0].titleProperty;

    if ("hideABPicker" in window.arguments[0])
      gHideABPicker = window.arguments[0].hideABPicker;
  }

  // set popup with address book names
  var abPopup = document.getElementById('abPopup');
  abPopup.value = gEditCard.selectedAB || kPersonalAddressbookURI;

  if (gHideABPicker && abPopup) {
    abPopup.hidden = true;
    document.getElementById("abPopupLabel").hidden = true;
  }

  SetCardDialogTitle(gEditCard.card.displayName);

  GetCardValues(gEditCard.card, document);

  // FIX ME - looks like we need to focus on both the text field and the tab widget
  // probably need to do the same in the addressing widget

  // focus on first or last name based on the pref
  var focus = document.getElementById(gEditCard.displayLastNameFirst
                                      ? "LastName" : "FirstName");
  if (focus) {
    // XXX Using the setTimeout hack until bug 103197 is fixed
    setTimeout( function(firstTextBox) { firstTextBox.focus(); }, 0, focus );
  }
}

/**
 * Get the source directory containing the card we are editing.
 */
function getContainingDirectory() {
  let directory = GetDirectoryFromURI(gEditCard.abURI);
  // If the source directory is "All Address Books", find the parent
  // address book of the card being edited and reflect the changes in it.
  if (directory.URI == kAllDirectoryRoot + "?") {
    let dirId =
      gEditCard.card.directoryId
                    .substring(0, gEditCard.card.directoryId.indexOf("&"));
    directory = MailServices.ab.getDirectoryFromId(dirId);
  }
  return directory;
}

function EditCardOKButton()
{
  if (!CheckCardRequiredDataPresence(document))
    return false;  // don't close window

  // See if this card is in any mailing list
  // if so then we need to update the addresslists of those mailing lists
  let directory = getContainingDirectory();

  // if the directory is a mailing list we need to search all the mailing lists
  // in the parent directory if the card exists.
  if (directory.isMailList) {
    var parentURI = GetParentDirectoryFromMailingListURI(gEditCard.abURI);
    directory = GetDirectoryFromURI(parentURI);
  }

  var listDirectoriesCount = directory.addressLists.length;
  var foundDirectories = [];

  // create a list of mailing lists and the index where the card is at.
  for (let i = 0; i < listDirectoriesCount; i++)
  {
    var subdirectory = directory.addressLists.queryElementAt(i, Ci.nsIAbDirectory);
    if (subdirectory.isMailList)
    {
      // See if any card in this list is the one we edited.
      // Must compare card contents using .equals() instead of .indexOf()
      // because gEditCard is not really a member of the .addressLists array.
      let listCardsCount = subdirectory.addressLists.length;
      for (let index = 0; index < listCardsCount; index++)
      {
        let card = subdirectory.addressLists.queryElementAt(index, Ci.nsIAbCard);
        if (card.equals(gEditCard.card))
          foundDirectories.push({directory:subdirectory, cardIndex:index});
      }
    }
  }

  CheckAndSetCardValues(gEditCard.card, document, false);

  directory.modifyCard(gEditCard.card);

  while (foundDirectories.length)
  {
    // Update the addressLists item for this card
    let foundItem = foundDirectories.pop();
    foundItem.directory.addressLists.replaceElementAt(gEditCard.card, foundItem.cardIndex);
  }

  NotifySaveListeners(directory);

  // callback to allow caller to update
  if (gOkCallback)
    gOkCallback();

  return true;  // close the window
}

function EditCardCancelButton()
{
  // If a new photo was created, remove it now as it won't be used.
  purgeOldPhotos(false);
}

function OnLoadEditCard()
{
  InitEditCard();

  gEditCard.titleProperty = "editContactTitle";

  if (window.arguments && window.arguments[0])
  {
    if ( window.arguments[0].card )
      gEditCard.card = window.arguments[0].card;
    if ( window.arguments[0].okCallback )
      gOkCallback = window.arguments[0].okCallback;
    if ( window.arguments[0].abURI )
      gEditCard.abURI = window.arguments[0].abURI;
  }

  // set global state variables
  // if first or last name entered, disable generateDisplayName
  if (gEditCard.generateDisplayName &&
      (gEditCard.card.firstName.length +
       gEditCard.card.lastName.length +
       gEditCard.card.displayName.length > 0))
  {
    gEditCard.generateDisplayName = false;
  }

  GetCardValues(gEditCard.card, document);

  SetCardDialogTitle(gEditCard.card.displayName);

  // check if selectedAB is a writeable
  // if not disable all the fields
  if ("arguments" in window && window.arguments[0])
  {
    if ("abURI" in window.arguments[0]) {
      var abURI = window.arguments[0].abURI;
      var directory = GetDirectoryFromURI(abURI);

      if (directory.readOnly)
      {
        // Set all the editable vcard fields to read only
        for (var i = kVcardFields.length; i-- > 0; )
          document.getElementById(kVcardFields[i][0]).readOnly = true;

        // the birthday fields
        document.getElementById("Birthday").readOnly = true;
        document.getElementById("BirthYear").readOnly = true;
        document.getElementById("Age").readOnly = true;

        // the photo field and buttons
        document.getElementById("PhotoType").disabled        = true;
        document.getElementById("GenericPhotoList").disabled = true;
        document.getElementById("PhotoURI").disabled         = true;
        document.getElementById("PhotoURI").placeholder      = "";
        document.getElementById("BrowsePhoto").disabled      = true;
        document.getElementById("UpdatePhoto").disabled      = true;

        // And the phonetic fields
        document.getElementById(kPhoneticFields[0]).readOnly = true;
        document.getElementById(kPhoneticFields[3]).readOnly = true;

        // Also disable the mail format popup.
        document.getElementById("PreferMailFormatPopup").disabled = true;

        // And the "prefer display name" checkbox.
        document.getElementById("preferDisplayName").disabled = true;

        document.documentElement.buttons = "accept";
        document.documentElement.removeAttribute("ondialogaccept");
      }
    }
  }
}

/* Registers functions that are called when loading the card
 * values into the contact editor dialog.  This is useful if
 * extensions have added extra fields to the nsIAbCard, and
 * need to display them in the contact editor.
 */
function RegisterLoadListener(aFunc)
{
  gOnLoadListeners.push(aFunc);
}

function UnregisterLoadListener(aFunc)
{
  var fIndex = gOnLoadListeners.indexOf(aFunc);
  if (fIndex != -1)
    gOnLoadListeners.splice(fIndex, 1);
}

// Notifies load listeners that an nsIAbCard is being loaded.
function NotifyLoadListeners(aCard, aDoc)
{
  if (!gOnLoadListeners.length)
    return;

  for (let listener of gOnLoadListeners)
    listener(aCard, aDoc);
}

/* Registers functions that are called when saving the card
 * values.  This is useful if extensions have added extra
 * fields to the user interface, and need to set those values
 * in their nsIAbCard.
 */
function RegisterSaveListener(aFunc)
{
  gOnSaveListeners.push(aFunc);
}

function UnregisterSaveListener(aFunc)
{
  var fIndex = gOnSaveListeners.indexOf(aFunc);
  if (fIndex != -1)
    gOnSaveListeners.splice(fIndex, 1);
}

// Notifies save listeners that an nsIAbCard is being saved.
function NotifySaveListeners(directory)
{
  if (!gOnSaveListeners.length)
    return;

  for (let listener of gOnSaveListeners)
    listener(gEditCard.card, document);

  // the save listeners might have tweaked the card
  // in which case we need to commit it.
  directory.modifyCard(gEditCard.card);
}

function InitPhoneticFields()
{
  var showPhoneticFields =
    Services.prefs.getComplexValue("mail.addr_book.show_phonetic_fields",
      Ci.nsIPrefLocalizedString).data;

  // show phonetic fields if indicated by the pref
  if (showPhoneticFields == "true")
  {
    for (var i = kPhoneticFields.length; i-- > 0; )
      document.getElementById(kPhoneticFields[i]).hidden = false;
  }
}

function InitEditCard()
{
  InitPhoneticFields();

  InitCommonJS();
  // Create gEditCard object that contains global variables for the current js
  //   file.
  gEditCard = new Object();

  // get specific prefs that gEditCard will need
  try {
    var displayLastNameFirst =
      Services.prefs.getComplexValue("mail.addr_book.displayName.lastnamefirst",
        Ci.nsIPrefLocalizedString).data;
    gEditCard.displayLastNameFirst = (displayLastNameFirst == "true");
    gEditCard.generateDisplayName =
      Services.prefs.getBoolPref("mail.addr_book.displayName.autoGeneration");
  }
  catch (ex) {
    dump("ex: failed to get pref" + ex + "\n");
  }
}

function NewCardOKButton()
{
  if (gOkCallback)
  {
    if (!CheckAndSetCardValues(gEditCard.card, document, true))
      return false;  // don't close window

    gOkCallback(gEditCard.card.translateTo("vcard"));
    return true;  // close the window
  }

  var popup = document.getElementById('abPopup');
  if ( popup )
  {
    var uri = popup.value;

    // FIX ME - hack to avoid crashing if no ab selected because of blank option bug from template
    // should be able to just remove this if we are not seeing blank lines in the ab popup
    if ( !uri )
      return false;  // don't close window
    // -----

    if (gEditCard.card)
    {
      if (!CheckAndSetCardValues(gEditCard.card, document, true))
        return false;  // don't close window

      // replace gEditCard.card with the card we added
      // so that save listeners can get / set attributes on
      // the card that got created.
      var directory = GetDirectoryFromURI(uri);
      gEditCard.card = directory.addCard(gEditCard.card);
      NotifySaveListeners(directory);
    }
  }

  return true;  // close the window
}

function NewCardCancelButton()
{
  // If a new photo was created, remove it now as it won't be used.
  purgeOldPhotos(false);
}

// Move the data from the cardproperty to the dialog
function GetCardValues(cardproperty, doc)
{
  if (!cardproperty)
    return;

  // Pass the nsIAbCard and the Document through the listeners
  // to give extensions a chance to populate custom fields.
  NotifyLoadListeners(cardproperty, doc);

  for (var i = kVcardFields.length; i-- > 0; ) {
    doc.getElementById(kVcardFields[i][0]).value =
      cardproperty.getProperty(kVcardFields[i][1], "");
  }

  var birthday = doc.getElementById("Birthday");
  modifyDatepicker(birthday);

  // Get the year first, so that the following month/day
  // calculations can take leap years into account.
  var year = cardproperty.getProperty("BirthYear", null);
  var birthYear = doc.getElementById("BirthYear");
  // set the year in the datepicker to the stored year
  // if the year isn't present, default to 2000 (a leap year)
  birthday.year = saneBirthYear(year);
  birthYear.value = year;

  // get the month of the year (1 - 12)
  var month = cardproperty.getProperty("BirthMonth", null);
  if (month > 0 && month < 13)
    birthday.month = month - 1;
  else
    birthday.monthField.value = null;

  // get the date of the month (1 - 31)
  var date = cardproperty.getProperty("BirthDay", null);
  if (date > 0 && date < 32)
    birthday.date = date;
  else
    birthday.dateField.value = null;

  // get the current age
  calculateAge(null, birthYear);
  // when the birth year changes, update the datepicker's year to the new value
  // or to kDefaultYear if the value is null
  birthYear.onchange = calculateAge;
  birthday.onchange = calculateAge;
  var age = doc.getElementById("Age");
  age.onchange = calculateYear;

  var popup = document.getElementById("PreferMailFormatPopup");
  if (popup)
    popup.value = cardproperty.getProperty("PreferMailFormat", "");

  var preferDisplayNameEl = document.getElementById("preferDisplayName");
  if (preferDisplayNameEl)
    // getProperty may return a "1" or "0" string, we want a boolean
    preferDisplayNameEl.checked = cardproperty.getProperty("PreferDisplayName", true) != false;

  // get phonetic fields if exist
  try {
    doc.getElementById("PhoneticFirstName").value = cardproperty.getProperty("PhoneticFirstName", "");
    doc.getElementById("PhoneticLastName").value = cardproperty.getProperty("PhoneticLastName", "");
  }
  catch (ex) {}

  // Select the type if there is a valid value stored for that type, otherwise
  // select the generic photo
  loadPhoto(cardproperty);

  updateChatName();
}

// when the ab card dialog is being loaded to show a vCard,
// hide the fields which aren't supported
// by vCard so the user does not try to edit them.
function HideNonVcardFields()
{
  document.getElementById("homeTabButton").hidden = true;
  document.getElementById("photoTabButton").hidden = true;
  var i;
  for (i = kNonVcardFields.length; i-- > 0; )
    document.getElementById(kNonVcardFields[i]).collapsed = true;
  for (i = kPhoneticFields.length; i-- > 0; )
    document.getElementById(kPhoneticFields[i]).collapsed = true;
}

// Move the data from the dialog to the cardproperty to be stored in the database
// @Returns false - Some required data are missing (card values were not set);
//          true - Card values were set, or there is no card to set values on.
function CheckAndSetCardValues(cardproperty, doc, check)
{
  // If requested, check the required data presence.
  if (check && !CheckCardRequiredDataPresence(document))
    return false;

  if (!cardproperty)
    return true;

  for (var i = kVcardFields.length; i-- > 0; )
    cardproperty.setProperty(kVcardFields[i][1],
      doc.getElementById(kVcardFields[i][0]).value);

  // get the birthday information from the dialog
  var birthdayElem = doc.getElementById("Birthday");
  var birthMonth = birthdayElem.monthField.value;
  var birthDay = birthdayElem.dateField.value;
  var birthYear = doc.getElementById("BirthYear").value;

  // set the birth day, month, and year properties
  cardproperty.setProperty("BirthDay", birthDay);
  cardproperty.setProperty("BirthMonth", birthMonth);
  cardproperty.setProperty("BirthYear", birthYear);

  var popup = document.getElementById("PreferMailFormatPopup");
  if (popup)
    cardproperty.setProperty("PreferMailFormat", popup.value);

  var preferDisplayNameEl = document.getElementById("preferDisplayName");
  if (preferDisplayNameEl)
    cardproperty.setProperty("PreferDisplayName", preferDisplayNameEl.checked);

  // set phonetic fields if exist
  try {
    cardproperty.setProperty("PhoneticFirstName", doc.getElementById("PhoneticFirstName").value);
    cardproperty.setProperty("PhoneticLastName", doc.getElementById("PhoneticLastName").value);
  }
  catch (ex) {}

  let photoType = doc.getElementById("PhotoType").value;
  if (gPhotoHandlers[photoType]) {
    if (!gPhotoHandlers[photoType].onSave(cardproperty, doc)) {
      photoType = "generic";
      onSwitchPhotoType("generic");
      gPhotoHandlers[photoType].onSave(cardproperty, doc);
    }
  }
  cardproperty.setProperty("PhotoType", photoType);
  purgeOldPhotos(true);

  // Remove obsolete chat names.
  try {
    cardproperty.setProperty("_GoogleTalk", "");
  }
  catch (ex) {}
  try {
    cardproperty.setProperty("_AimScreenName", "");
  }
  catch (ex) {}

  return true;
}

function CleanUpWebPage(webPage)
{
  // no :// yet so we should add something
  if ( webPage.length && webPage.search("://") == -1 )
  {
    // check for missing / on http://
    if ( webPage.substr(0, 6) == "http:/" )
      return( "http://" + webPage.substr(6) );
    else
      return( "http://" + webPage );
  }
  else
    return(webPage);
}

// @Returns false - Some required data are missing;
//          true - All required data are present.
function CheckCardRequiredDataPresence(doc)
{
  // Bug 314995 - We require at least one of the following fields to be
  // filled in: email address, first name, last name, display name,
  //            organization (company name).
  var primaryEmail = doc.getElementById("PrimaryEmail");
  if (primaryEmail.textLength == 0 &&
      doc.getElementById("FirstName").textLength == 0 &&
      doc.getElementById("LastName").textLength == 0 &&
      doc.getElementById("DisplayName").textLength == 0 &&
      doc.getElementById("Company").textLength == 0)
  {
    Services.prompt.alert(window,
      gAddressBookBundle.getString("cardRequiredDataMissingTitle"),
      gAddressBookBundle.getString("cardRequiredDataMissingMessage"));

    return false;
  }

  // Simple checks that the primary email should be of the form |user@host|.
  // Note: if the length of the primary email is 0 then we skip the check
  // as some other field must have something as per the check above.
  if (primaryEmail.textLength != 0 && !/.@./.test(primaryEmail.value))
  {
    Services.prompt.alert(window,
      gAddressBookBundle.getString("incorrectEmailAddressFormatTitle"),
      gAddressBookBundle.getString("incorrectEmailAddressFormatMessage"));

    // Focus the dialog field, to help the user.
    document.getElementById("abTabs").selectedIndex = 0;
    primaryEmail.focus();

    return false;
  }

  return true;
}

function GenerateDisplayName()
{
  if (!gEditCard.generateDisplayName)
    return;

  var displayName;

  var firstNameValue = document.getElementById("FirstName").value;
  var lastNameValue = document.getElementById("LastName").value;
  if (lastNameValue && firstNameValue) {
    displayName = (gEditCard.displayLastNameFirst)
      ? gAddressBookBundle.getFormattedString("lastFirstFormat", [lastNameValue, firstNameValue])
      : gAddressBookBundle.getFormattedString("firstLastFormat", [firstNameValue, lastNameValue]);
  }
  else {
    // one (or both) of these is empty, so this works.
    displayName = firstNameValue + lastNameValue;
  }

  document.getElementById("DisplayName").value = displayName;

  SetCardDialogTitle(displayName);
}

function DisplayNameChanged()
{
  // turn off generateDisplayName if the user changes the display name
  gEditCard.generateDisplayName = false;

  SetCardDialogTitle(document.getElementById("DisplayName").value);
}

function SetCardDialogTitle(displayName)
{
  document.title = displayName
    ? gAddressBookBundle.getFormattedString(gEditCard.titleProperty + "WithDisplayName", [displayName])
    : gAddressBookBundle.getString(gEditCard.titleProperty);
}

/**
 * Calculates the duration of time between an event and now and updates the year
 * of whichever element did not call this function.
 * @param aEvent   The event calling this method.
 * @param aElement Optional, but required if this function is not called from an
 *                 element's event listener. The element that would call this.
 */
function calculateAge(aEvent, aElement) {
  var datepicker, yearElem, ageElem;
  if (aEvent)
    aElement = this;
  if (aElement.id == "BirthYear" || aElement.id == "Birthday") {
    datepicker = document.getElementById("Birthday");
    yearElem = document.getElementById("BirthYear");
    ageElem = document.getElementById("Age");
  }
  if (!datepicker || !yearElem || !ageElem)
    return;

  // if the datepicker was updated, update the year element
  if (aElement == datepicker && !(datepicker.year == kDefaultYear && !yearElem.value))
    yearElem.value = datepicker.year;
  var year = yearElem.value;
  // if the year element's value is invalid set the year and age elements to null
  if (isNaN(year) || year < kMinYear || year > kMaxYear) {
    yearElem.value = null;
    ageElem.value = null;
    datepicker.year = kDefaultYear;
    return;
  }
  else if (aElement == yearElem)
    datepicker.year = year;
  // calculate the length of time between the event and now
  try {
    var event = new Date(datepicker.year, datepicker.month, datepicker.date);
    // if the year is only 2 digits, then the year won't be set correctly
    // using setFullYear fixes this issue
    event.setFullYear(datepicker.year);
    // get the difference between today and the event
    var age = new Date(new Date() - event);
    // get the number of years of the difference and subtract 1970 (epoch)
    ageElem.value = age.getFullYear() - 1970;
  }
  catch(e) {
    datepicker.year = kDefaultYear;
    // if there was an error (like invalid year) set the year and age to null
    yearElem.value = null;
    ageElem.value = null;
  }
}

/**
 * Calculates the year an event ocurred based on the number of years, months,
 * and days since the event and updates the relevant element.
 * @param aEvent   The event calling this method.
 * @param aElement Optional, but required if this function is not called from an
 *                 element's event listener. The element that would call this.
 */
function calculateYear(aEvent, aElement) {
  var yearElem, datepicker;
  if (aEvent)
    aElement = this;
  if (aElement.id == "Age") {
    datepicker = document.getElementById("Birthday");
    yearElem = document.getElementById("BirthYear");
  }
  if (!datepicker || !yearElem)
    return;

  // if the age is null, remove the year from the year element, and set the
  // datepicker to the default year
  if (!aElement.value) {
    datepicker.year = kDefaultYear;
    yearElem.value = null;
    return;
  }
  var today = new Date();
  try {
    var date = new Date(aElement.value, datepicker.month, datepicker.date);
    date.setFullYear(aElement.value);
    // get the difference between today and the age (the year is offset by 1970)
    var difference = new Date(today - date);
    datepicker.year = yearElem.value = difference.getFullYear() - 1970;
  }
  // the above code may throw an invalid year exception.  If that happens, set
  // the year to kDefaultYear and set the year element's value to 0
  catch (e) {
    datepicker.year = kDefaultYear;
    // if there was an error (like invalid year) set the year and age to null
    yearElem.value = null;
    let ageElem = document.getElementById("Age");
    if (ageElem)
      ageElem.value = null;
  }
}

/**
 * Modifies a datepicker in the following ways:
 *  - Removes the scroll arrows
 *  - Hides the year
 *  - Allows the day and month to be blank
 * NOTE:
 * The datepicker's date, month, year, and dateValue properties are not always
 * what appear physically to the user in the datepicker fields.
 * If any field is blank, the corresponding property is either the previous
 * value if there was one since the card was opened or the relevant portion of
 * the current date.
 *
 * To get the displayed values, get the value of the individual field, such as
 * datepicker.yyyyField.value where yyyy is "year", "month", or "date" for the
 * year, month, and day, respectively.
 * If the value is null, then the field is blank and vice versa.
 * @param aDatepicker The datepicker to modify.
 */
function modifyDatepicker(aDatepicker) {
  // collapse the year field and separator
  aDatepicker.yearField.parentNode.collapsed = true;
  if (aDatepicker.yearField == aDatepicker._fieldThree ||
      aDatepicker.yearField == aDatepicker._fieldTwo)
    aDatepicker._separatorSecond.collapsed = true;
  else
    aDatepicker._separatorFirst.collapsed = true;
  // collapse the spinner element
  document.getAnonymousElementByAttribute(aDatepicker, "anonid", "buttons")
          .collapsed = true;
  // this modified constrain value function ignores values less than the minimum
  // to let the value be blank (null)
  // from: mozilla/toolkit/content/widgets/datetimepicker.xml#759
  aDatepicker._constrainValue = function newConstrainValue(aField, aValue, aNoWrap) {
    // if the value is less than one, make the field's value null
    if (aValue < 1) {
      aField.value = null;
      return null;
    }
    if (aNoWrap && aField == this.monthField)
      aValue--;
    // make sure the date is valid for the given month
    if (aField == this.dateField) {
      var currentMonth = this.month;
      var dt = new Date(this.year, currentMonth, aValue);
      return dt.getMonth() != currentMonth ? 1 : aValue;
    }
    var min = (aField == this.monthField) ? 0 : 1;
    var max = (aField == this.monthField) ? 11 : kMaxYear;
    // make sure the value isn't too high
    if (aValue > max)
      return aNoWrap ? max : min;
    return aValue;
  }
  // sets the specified field to the given value, but allows blank fields
  // from: mozilla/toolkit/content/widgets/datetimepicker.xml#698
  aDatepicker._setFieldValue = function setValue(aField, aValue) {
    if (aField == this.yearField && aValue >= kMinYear && aValue <= kMaxYear) {
      var oldDate = this._dateValue;
      this._dateValue.setFullYear(aValue);
      if (oldDate != this._dateValue) {
        this._dateValue.setDate(0);
        this._updateUI(this.dateField, this.date);
      }
    }
    // update the month if the value isn't null
    else if (aField == this.monthField && aValue != null) {
      var oldDate = this.date;
      this._dateValue.setMonth(aValue);
      if (oldDate != this.date)
        this._dateValue.setDate(0);
      this._updateUI(this.dateField, this.date);
      var date = this._dateValue.getDate();
      this.dateField.value = date < 10 && this.dateLeadingZero ? "0" + date : date;
      var month = this._dateValue.getMonth() + 1;
      this.monthField.value = month < 10 && this.monthLeadingZero ? "0" + month : month;
    }
    // update the date if the value isn't null
    else if (aField == this.dateField && aValue != null) {
      this._dateValue.setDate(aValue);
      this._updateUI(this.dateField, this.date);
      var date = this._dateValue.getDate();
      this.dateField.value = date < 10 && this.dateLeadingZero ? "0" + date : date;
      var month = this._dateValue.getMonth() + 1;
      this.monthField.value = month < 10 && this.monthLeadingZero ? "0" + month : month;
    }
    this.setAttribute("value", this.value);

    if (this.attachedControl)
      this.attachedControl._setValueNoSync(this._dateValue);
    // if the aField's value is null or 0, set both field's values to null
    if (!aField.value && aField != this.yearField) {
      this.dateField.value = null;
      this.monthField.value = null;
    }
    // make the field's value null if aValue is null and the field's value isn't
    if (aValue == null && aField.value != null)
      aField.value = null;
  }
}

var chatNameFieldIds =
  ["Yahoo", "Skype", "QQ", "MSN", "ICQ", "XMPP", "IRC"];

/**
 * Show the 'Chat' tab and focus the first field that has a value, or
 * the first field if none of them has a value.
 */
function showChat()
{
  document.getElementById('abTabPanels').parentNode.selectedTab =
    document.getElementById('chatTabButton');
  for (let id of chatNameFieldIds) {
    let elt = document.getElementById(id);
    if (elt.value) {
      elt.focus();
      return;
    }
  }
  document.getElementById(chatNameFieldIds[0]).focus();
}

/**
 * Fill in the value of the ChatName readonly field with the first
 * value of the fields in the Chat tab.
 */
function updateChatName()
{
  let value = "";
  for (let id of chatNameFieldIds) {
    let val = document.getElementById(id).value;
    if (val) {
      value = val;
      break;
    }
  }
  document.getElementById("ChatName").value = value;
}

/**
 * Extract the photo information from an nsIAbCard, and populate
 * the appropriate input fields in the contact editor.  If the
 * nsIAbCard returns an unrecognized PhotoType, the generic
 * display photo is switched to.
 *
 * @param aCard The nsIAbCard to extract the information from.
 *
 */
function loadPhoto(aCard) {
  var type = aCard.getProperty("PhotoType", "");

  if (!gPhotoHandlers[type] || !gPhotoHandlers[type].onLoad(aCard, document)) {
    type = "generic";
    gPhotoHandlers[type].onLoad(aCard, document);
  }

  document.getElementById("PhotoType").value = type;
  gPhotoHandlers[type].onShow(aCard, document, "photo");
}

/**
 * Event handler for when the user switches the type of
 * photo for the nsIAbCard being edited. Tries to initiate a
 * photo download.
 *
 * @param aPhotoType {string}  The type to switch to
 * @param aEvent {Event}       The event object if used as an event handler
 */
function onSwitchPhotoType(aPhotoType, aEvent) {
  if (!gEditCard)
    return;

  // Stop event propagation to the radiogroup command event in case that the
  // child button is pressed. Otherwise, the download is started twice in a row.
  if (aEvent) {
    aEvent.stopPropagation();
  }

  if (aPhotoType) {
    if (aPhotoType != document.getElementById("PhotoType").value) {
      document.getElementById("PhotoType").value = aPhotoType;
    }
  } else {
    aPhotoType = document.getElementById("PhotoType").value;
  }

  if (gPhotoHandlers[aPhotoType]) {
    if (!gPhotoHandlers[aPhotoType].onRead(gEditCard.card, document)) {
      onSwitchPhotoType("generic");
    }
  }
}

/**
 * Removes the photo file at the given path, if present.
 *
 * @param aName The name of the photo to remove from the Photos directory.
 *              'null' value is allowed and means to remove no file.
 *
 * @return {boolean} True if the file was deleted, false otherwise.
 */
function removePhoto(aName) {
  if (!aName)
    return false;
  // Get the directory with all the photos
  var file = getPhotosDir();
  // Get the photo (throws an exception for invalid names)
  try {
    file.append(aName);
    file.remove(false);
    return true;
  }
  catch (e) {}
  return false;
}

/**
 * Remove previous and temporary photo files from the Photos directory.
 *
 * @param aSaved {boolean}  Whether the new card is going to be saved/committed.
 */
function purgeOldPhotos(aSaved = true) {
  // If photo was changed, the array contains at least one member, the original photo.
  while (gOldPhotos.length > 0) {
    let photoName = gOldPhotos.pop();
    if (!aSaved && (gOldPhotos.length == 0)) {
      // If the saving was cancelled, we want to keep the original photo of the card.
      break;
    }
    removePhoto(photoName);
  }

  if (aSaved) {
    // The new photo should stay so we clear the reference to it.
    gNewPhoto = null;
  } else {
    // Changes to card not saved, we don't need the new photo.
    // It may be null when there was no change of it.
    removePhoto(gNewPhoto);
  }
}

/**
 * Opens a file picker with image filters to look for a contact photo.
 * If the user selects a file and clicks OK then the PhotoURI textbox is set
 * with a file URI pointing to that file and updatePhoto is called.
 *
 * @param aEvent {Event} The event object if used as an event handler.
 */
function browsePhoto(aEvent) {
  // Stop event propagation to the radiogroup command event in case that the
  // child button is pressed. Otherwise, the download is started twice in a row.
  if (aEvent)
    aEvent.stopPropagation();

  let fp = Cc["@mozilla.org/filepicker;1"]
             .createInstance(Ci.nsIFilePicker);
  fp.init(window, gAddressBookBundle.getString("browsePhoto"),
          Ci.nsIFilePicker.modeOpen);

  // Open the directory of the currently chosen photo (if any)
  let currentPhotoFile = document.getElementById("PhotoFile").file
  if (currentPhotoFile) {
    fp.displayDirectory = currentPhotoFile.parent;
  }

  // Add All Files & Image Files filters and select the latter
  fp.appendFilters(Ci.nsIFilePicker.filterImages);
  fp.appendFilters(Ci.nsIFilePicker.filterAll);

  fp.open(rv => {
    if (rv != Ci.nsIFilePicker.returnOK) {
      return;
    }
    document.getElementById("PhotoFile").file = fp.file;
    onSwitchPhotoType("file");
  });
}

/**
 * Handlers to add drag and drop support.
 */
function checkDropPhoto(aEvent) {
  // Just allow anything to be dropped. Different types of data are handled
  // in doDropPhoto() below.
  aEvent.preventDefault();
}

function doDropPhoto(aEvent) {
  aEvent.preventDefault();

  let photoType = "";

  // Check if a file has been dropped.
  let file = aEvent.dataTransfer.mozGetDataAt("application/x-moz-file", 0);
  if (file instanceof Ci.nsIFile) {
    photoType = "file";
    document.getElementById("PhotoFile").file = file;
  } else {
    // Check if a URL has been dropped.
    let link = aEvent.dataTransfer.getData("URL");
    if (link) {
      photoType = "web";
      document.getElementById("PhotoURI").value = link;
    } else {
      // Check if dropped text is a URL.
      link = aEvent.dataTransfer.getData("text/plain");
      if (/^(ftps?|https?):\/\//i.test(link)) {
        photoType = "web";
        document.getElementById("PhotoURI").value = link;
      }
    }
  }

  onSwitchPhotoType(photoType);
}

/**
 * Self-contained object to manage the user interface used for downloading
 * and storing contact photo images.
 */
var gPhotoDownloadUI = (function() {
  // UI DOM elements
  let elProgressbar;
  let elProgressLabel;
  let elPhotoType;
  let elProgressContainer;

  window.addEventListener("load", function load(event) {
    if (!elProgressbar)
      elProgressbar = document.getElementById("PhotoDownloadProgress");
    if (!elProgressLabel)
      elProgressLabel = document.getElementById("PhotoStatus");
    if (!elPhotoType)
      elPhotoType = document.getElementById("PhotoType");
    if (!elProgressContainer)
      elProgressContainer = document.getElementById("ProgressContainer");
  }, false);

  function onStart() {
    elProgressContainer.setAttribute("class", "expanded");
    elProgressLabel.value = "";
    elProgressbar.hidden = false;
    elProgressbar.value = 3; // Start with a tiny visible progress
  }

  function onSuccess() {
    elProgressLabel.value = "";
    elProgressContainer.setAttribute("class", "");
  }

  function onError(state) {
    let msg;
    switch (state) {
      case gImageDownloader.ERROR_INVALID_URI:
        msg = gAddressBookBundle.getString("errorInvalidUri");
        break;
      case gImageDownloader.ERROR_UNAVAILABLE:
        msg = gAddressBookBundle.getString("errorNotAvailable");
        break;
      case gImageDownloader.ERROR_INVALID_IMG:
        msg = gAddressBookBundle.getString("errorInvalidImage");
        break;
      case gImageDownloader.ERROR_SAVE:
        msg = gAddressBookBundle.getString("errorSaveOperation");
        break;
    }
    if (msg) {
      elProgressLabel.value = msg;
      elProgressbar.hidden = true;
      onSwitchPhotoType("generic");
    }
  }

  function onProgress(state, percent) {
    elProgressbar.value = percent;
    elProgressLabel.value = gAddressBookBundle.getString("stateImageSave");
  }

  return {
    onStart: onStart,
    onSuccess: onSuccess,
    onError: onError,
    onProgress: onProgress
  }
})();

/* A photo handler defines the behaviour of the contact editor
 * for a particular photo type. Each photo handler must implement
 * the following interface:
 *
 * onLoad: function(aCard, aDocument):
 *   Called when the editor wants to populate the contact editor
 *   input fields with information about aCard's photo.  Note that
 *   this does NOT make aCard's photo appear in the contact editor -
 *   this is left to the onShow function.  Returns true on success.
 *   If the function returns false, the generic photo handler onLoad
 *   function will be called.
 *
 * onShow: function(aCard, aDocument, aTargetID):
 *   Called when the editor wants to show this photo type.
 *   The onShow method should take the input fields in the document,
 *   and render the requested photo in the IMG tag with id
 *   aTargetID.  Note that onShow does NOT save the photo for aCard -
 *   this job is left to the onSave function.  Returns true on success.
 *   If the function returns false, the generic photo handler onShow
 *   function will be called.
 *
 * onRead: function(aCard, aDocument)
 *   Called when the editor wants to read the user supplied new photo.
 *   The onRead method is responsible for analyzing the photo of this
 *   type requested by the user, and storing it, as well as the
 *   other fields required by onLoad/onShow to retrieve and display
 *   the photo again. Returns true on success.  If the function
 *   returns false, the generic photo handler onRead function will
 *   be called.
 *
 * onSave: function(aCard, aDocument)
 *   Called when the editor wants to save this photo type to the card.
 *   Returns true on success.
 */

var gGenericPhotoHandler = {
  onLoad: function(aCard, aDocument) {
    return true;
  },

  onShow: function(aCard, aDocument, aTargetID) {
    // XXX TODO: this ignores any other value from the generic photos
    // menulist than "default".
    aDocument.getElementById(aTargetID)
             .setAttribute("src", defaultPhotoURI);
    return true;
  },

  onRead: function(aCard, aDocument) {
    gPhotoDownloadUI.onSuccess();

    newPhotoAdded("", aCard);

    gGenericPhotoHandler.onShow(aCard, aDocument, "photo");
    return true;
  },

  onSave: function(aCard, aDocument) {
    // XXX TODO: this ignores any other value from the generic photos
    // menulist than "default".

    // Update contact
    aCard.setProperty("PhotoName", "");
    aCard.setProperty("PhotoURI", "");
    return true;
  }
};

var gFilePhotoHandler = {

  onLoad: function(aCard, aDocument) {
    let photoURI = aCard.getProperty("PhotoURI", "");
    let file;
    try {
      // The original file may not exist anymore, but we still display it.
      file = Services.io.newURI(photoURI)
                        .QueryInterface(Ci.nsIFileURL)
                        .file;
    } catch (e) {}

    if (!file)
      return false;

    aDocument.getElementById("PhotoFile").file = file;
    return true;
  },

  onShow: function(aCard, aDocument, aTargetID) {
    let photoName = gNewPhoto || aCard.getProperty("PhotoName", null);
    let photoURI = getPhotoURI(photoName);
    aDocument.getElementById(aTargetID).setAttribute("src", photoURI);
    return true;
  },

  onRead: function(aCard, aDocument) {
    let file = aDocument.getElementById("PhotoFile").file;
    if (!file)
      return false;

    // If the local file has been removed/renamed, keep the current photo as is.
    if (!file.exists() || !file.isFile())
      return false;

    let photoURI = Services.io.newFileURI(file).spec;

    gPhotoDownloadUI.onStart();

    let cbSuccess = function(newPhotoName) {
      gPhotoDownloadUI.onSuccess();

      newPhotoAdded(newPhotoName, aCard);
      aDocument.getElementById("PhotoFile").setAttribute("PhotoURI", photoURI);

      gFilePhotoHandler.onShow(aCard, aDocument, "photo");
    };

    gImageDownloader.savePhoto(photoURI, cbSuccess,
                               gPhotoDownloadUI.onError,
                               gPhotoDownloadUI.onProgress);
    return true;
  },

  onSave: function(aCard, aDocument) {
    // Update contact
    if (gNewPhoto) {
      // The file may not be valid unless the photo has changed.
      let photoURI = aDocument.getElementById("PhotoFile").getAttribute("PhotoURI");
      aCard.setProperty("PhotoName", gNewPhoto);
      aCard.setProperty("PhotoURI", photoURI);
    }
    return true;
  }
};

var gWebPhotoHandler = {
  onLoad: function(aCard, aDocument) {
    let photoURI = aCard.getProperty("PhotoURI", null);

    if (!photoURI)
      return false;

    aDocument.getElementById("PhotoURI").value = photoURI;
    return true;
  },

  onShow: function(aCard, aDocument, aTargetID) {
    let photoName = gNewPhoto || aCard.getProperty("PhotoName", null);
    if (!photoName)
      return false;

    let photoURI = getPhotoURI(photoName);

    aDocument.getElementById(aTargetID).setAttribute("src", photoURI);
    return true;
  },

  onRead: function(aCard, aDocument) {
    let photoURI = aDocument.getElementById("PhotoURI").value;
    if (!photoURI)
      return false;

    gPhotoDownloadUI.onStart();

    let cbSuccess = function(newPhotoName) {
      gPhotoDownloadUI.onSuccess();

      newPhotoAdded(newPhotoName, aCard);

      gWebPhotoHandler.onShow(aCard, aDocument, "photo");

    }

    gImageDownloader.savePhoto(photoURI, cbSuccess,
                               gPhotoDownloadUI.onError,
                               gPhotoDownloadUI.onProgress);
    return true;
  },

  onSave: function(aCard, aDocument) {
    // Update contact
    if (gNewPhoto) {
      let photoURI = aDocument.getElementById("PhotoURI").value;
      aCard.setProperty("PhotoName", gNewPhoto);
      aCard.setProperty("PhotoURI", photoURI);
    }
    return true;
  }
};

function newPhotoAdded(aPhotoName, aCard) {
  // If we had the photo saved locally, shedule it for removal if card is saved.
  gOldPhotos.push(gNewPhoto !== null ? gNewPhoto : aCard.getProperty("PhotoName", null));
  gNewPhoto = aPhotoName;
}

/* In order for other photo handlers to be recognized for
 * a particular type, they must be registered through this
 * function.
 * @param aType the type of photo to handle
 * @param aPhotoHandler the photo handler to register
 */
function registerPhotoHandler(aType, aPhotoHandler)
{
  gPhotoHandlers[aType] = aPhotoHandler;
}

registerPhotoHandler("generic", gGenericPhotoHandler);
registerPhotoHandler("web", gWebPhotoHandler);
registerPhotoHandler("file", gFilePhotoHandler);
