/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */

#include "nsAbOSXCard.h"
#include "nsAbOSXDirectory.h"
#include "nsAbOSXUtils.h"
#include "nsAutoPtr.h"
#include "nsIAbManager.h"
#include "nsObjCExceptions.h"
#include "nsServiceManagerUtils.h"

#include <AddressBook/AddressBook.h>

NS_IMPL_ISUPPORTS_INHERITED(nsAbOSXCard,
                             nsAbCardProperty,
                             nsIAbOSXCard)

#ifdef DEBUG
static ABPropertyType
GetPropertType(ABRecord *aCard, NSString *aProperty)
{
  ABPropertyType propertyType = kABErrorInProperty;
  if ([aCard isKindOfClass:[ABPerson class]])
    propertyType = [ABPerson typeOfProperty:aProperty];
  else if ([aCard isKindOfClass:[ABGroup class]])
    propertyType = [ABGroup typeOfProperty:aProperty];
  return propertyType;
}
#endif

static void
SetStringProperty(nsAbOSXCard *aCard, const nsString &aValue,
                  const char *aMemberName, bool aNotify,
                  nsIAbManager *aAbManager)
{
  nsString oldValue;
  nsresult rv = aCard->GetPropertyAsAString(aMemberName, oldValue);
  if (NS_FAILED(rv))
    oldValue.Truncate();

  if (!aNotify) {
    aCard->SetPropertyAsAString(aMemberName, aValue);
  }
  else if (!oldValue.Equals(aValue)) {
    aCard->SetPropertyAsAString(aMemberName, aValue);

    nsISupports *supports = NS_ISUPPORTS_CAST(nsAbCardProperty*, aCard);

    aAbManager->NotifyItemPropertyChanged(supports, aMemberName,
                                          oldValue.get(), aValue.get());
  }
}

static void
SetStringProperty(nsAbOSXCard *aCard, NSString *aValue, const char *aMemberName,
                  bool aNotify, nsIAbManager *aAbManager)
{
  nsAutoString value;
  if (aValue)
    AppendToString(aValue, value);

  SetStringProperty(aCard, value, aMemberName, aNotify, aAbManager);
}

static void
MapStringProperty(nsAbOSXCard *aCard, ABRecord *aOSXCard, NSString *aProperty,
                  const char *aMemberName, bool aNotify,
                  nsIAbManager *aAbManager)
{
  NS_ASSERTION(aProperty, "This is bad! You asked for an unresolved symbol.");
  NS_ASSERTION(GetPropertType(aOSXCard, aProperty) == kABStringProperty,
               "Wrong type!");
  
  SetStringProperty(aCard, [aOSXCard valueForProperty:aProperty], aMemberName,
                    aNotify, aAbManager);
}

static ABMutableMultiValue*
GetMultiValue(ABRecord *aCard, NSString *aProperty)
{
  NS_ASSERTION(aProperty, "This is bad! You asked for an unresolved symbol.");
  NS_ASSERTION(GetPropertType(aCard, aProperty) & kABMultiValueMask,
               "Wrong type!");
  
  return [aCard valueForProperty:aProperty];
}

static void
MapDate(nsAbOSXCard *aCard, NSDate *aDate, const char *aYearPropName,
        const char *aMonthPropName, const char *aDayPropName, bool aNotify,
        nsIAbManager *aAbManager)
{
  // XXX Should we pass a format and timezone?
  NSCalendarDate *date = [aDate dateWithCalendarFormat:nil timeZone:nil];
  
  nsAutoString value;
  value.AppendInt(static_cast<int32_t>([date yearOfCommonEra]));
  SetStringProperty(aCard, value, aYearPropName, aNotify, aAbManager);
  value.Truncate();
  value.AppendInt(static_cast<int32_t>([date monthOfYear]));
  SetStringProperty(aCard, value, aMonthPropName, aNotify, aAbManager);
  value.Truncate();
  value.AppendInt(static_cast<int32_t>([date dayOfMonth]));
  SetStringProperty(aCard, value, aDayPropName, aNotify, aAbManager);
}

static bool
MapMultiValue(nsAbOSXCard *aCard, ABRecord *aOSXCard,
              const nsAbOSXPropertyMap &aMap, bool aNotify,
              nsIAbManager *aAbManager)
{
  ABMultiValue *value = GetMultiValue(aOSXCard, aMap.mOSXProperty);
  if (value) {
    unsigned int j;
    unsigned int count = [value count];
    for (j = 0; j < count; ++j) {
      if ([[value labelAtIndex:j] isEqualToString:aMap.mOSXLabel]) {
        NSString *stringValue = (aMap.mOSXKey)
          ? [[value valueAtIndex:j] objectForKey:aMap.mOSXKey]
          : [value valueAtIndex:j];
        
        SetStringProperty(aCard, stringValue, aMap.mPropertyName, aNotify,
                          aAbManager);
        
        return true;
      }
    }
  }
  // String wasn't found, set value of card to empty if it was set previously
  SetStringProperty(aCard, EmptyString(), aMap.mPropertyName, aNotify,
                    aAbManager);
  
  return false;
}

// Maps Address Book's instant messenger name to the corresponding nsIAbCard field name.
static const char*
InstantMessengerFieldName(NSString* aInstantMessengerName)
{
  if ([aInstantMessengerName isEqualToString:@"AIMInstant"]) {
    return "_AimScreenName";
  }
  if ([aInstantMessengerName isEqualToString:@"GoogleTalkInstant"]) {
    return "_GoogleTalk";
  }
  if ([aInstantMessengerName isEqualToString:@"ICQInstant"]) {
    return "_ICQ";
  }
  if ([aInstantMessengerName isEqualToString:@"JabberInstant"]) {
    return "_JabberId";
  }
  if ([aInstantMessengerName isEqualToString:@"MSNInstant"]) {
    return "_MSN";
  }
  if ([aInstantMessengerName isEqualToString:@"QQInstant"]) {
    return "_QQ";
  }
  if ([aInstantMessengerName isEqualToString:@"SkypeInstant"]) {
    return "_Skype";
  }
  if ([aInstantMessengerName isEqualToString:@"YahooInstant"]) {
    return "_Yahoo";
  }

  // Fall back to AIM for everything else.
  // We don't have nsIAbCard fields for FacebookInstant and GaduGaduInstant.
  return "_AimScreenName";
}

nsresult
nsAbOSXCard::Init(const char *aUri)
{
  if (strncmp(aUri, NS_ABOSXCARD_URI_PREFIX,
              sizeof(NS_ABOSXCARD_URI_PREFIX) - 1) != 0)
    return NS_ERROR_FAILURE;

  mURI = aUri;

  SetLocalId(nsDependentCString(aUri));

  return Update(false);
}

nsresult
nsAbOSXCard::GetURI(nsACString &aURI)
{
  if (mURI.IsEmpty())
    return NS_ERROR_NOT_INITIALIZED;

  aURI = mURI;
  return NS_OK;
}

nsresult
nsAbOSXCard::Update(bool aNotify)
{
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;

  ABAddressBook *addressBook = [ABAddressBook sharedAddressBook];

  const char *uid = &((mURI.get())[16]);
  ABRecord *card = [addressBook recordForUniqueId:[NSString stringWithUTF8String:uid]];
  NS_ENSURE_TRUE(card, NS_ERROR_FAILURE);

  nsCOMPtr<nsIAbManager> abManager;
  nsresult rv;
  if (aNotify) {
    abManager = do_GetService(NS_ABMANAGER_CONTRACTID, &rv);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  if ([card isKindOfClass:[ABGroup class]]) {
    m_IsMailList = true;
    m_MailListURI.AssignLiteral(NS_ABOSXDIRECTORY_URI_PREFIX);
    m_MailListURI.Append(uid);
    MapStringProperty(this, card, kABGroupNameProperty, "DisplayName", aNotify,
                      abManager);
    MapStringProperty(this, card, kABGroupNameProperty, "LastName", aNotify,
                      abManager);

    return NS_OK;
  }
  
  bool foundHome = false, foundWork = false;
  
  uint32_t i;
  for (i = 0; i < nsAbOSXUtils::kPropertyMapSize; ++i) {
    const nsAbOSXPropertyMap &propertyMap = nsAbOSXUtils::kPropertyMap[i];
    if (!propertyMap.mOSXProperty)
      continue;
    
    if (propertyMap.mOSXLabel) {
      if (MapMultiValue(this, card, propertyMap, aNotify,
                        abManager) && propertyMap.mOSXProperty == kABAddressProperty) {
        if (propertyMap.mOSXLabel == kABAddressHomeLabel) 
          foundHome = true;
        else
          foundWork = true;
      }
    }
    else {
      MapStringProperty(this, card, propertyMap.mOSXProperty,
                        propertyMap.mPropertyName, aNotify, abManager);
    }
  }
  
  int flags = 0;
  if (kABPersonFlags)
    flags = [[card valueForProperty:kABPersonFlags] intValue];
  
#define SET_STRING(_value, _name, _notify, _session) \
  SetStringProperty(this, _value, #_name, _notify, _session)
    
    // If kABShowAsCompany is set we use the company name as display name.
    if (kABPersonFlags && (flags & kABShowAsCompany)) {
      nsString company;
      nsresult rv = GetPropertyAsAString(kCompanyProperty, company);
      if (NS_FAILED(rv))
        company.Truncate();
      SET_STRING(company, DisplayName, aNotify, abManager);
    }
  else {
    // Use the order used in the OS X address book to set DisplayName.
    int order = kABPersonFlags && (flags & kABNameOrderingMask);
    if (kABPersonFlags && (order == kABDefaultNameOrdering)) {
      order = [addressBook defaultNameOrdering];
    }
    
    nsAutoString displayName, tempName;
    if (kABPersonFlags && (order == kABFirstNameFirst)) {
      GetFirstName(tempName);
      displayName.Append(tempName);

      GetLastName(tempName);

      // Only append a space if the last name and the first name are not empty
      if (!tempName.IsEmpty() && !displayName.IsEmpty())
        displayName.Append(' ');

      displayName.Append(tempName);
    }
    else {
      GetLastName(tempName);
      displayName.Append(tempName);

      GetFirstName(tempName);

      // Only append a space if the last name and the first name are not empty
      if (!tempName.IsEmpty() && !displayName.IsEmpty())
        displayName.Append(' ');

      displayName.Append(tempName);
    }
    SET_STRING(displayName, DisplayName, aNotify, abManager);
  }
  
  ABMultiValue *value = GetMultiValue(card, kABEmailProperty);
  if (value) {
    unsigned int count = [value count];
    if (count > 0) {
      unsigned int j = [value indexForIdentifier:[value primaryIdentifier]];
      
      if (j < count)
        SET_STRING([value valueAtIndex:j], PrimaryEmail, aNotify,
                   abManager);
      
      // If j is 0 (first in the list) we want the second in the list
      // (index 1), if j is anything else we want the first in the list
      // (index 0).
      j = (j == 0);
      if (j < count)
        SET_STRING([value valueAtIndex:j], SecondEmail, aNotify,
                   abManager);
    }
  }
  
  // We map the first home address we can find and the first work address
  // we can find. If we find none, we map the primary address to the home
  // address.
  if (!foundHome && !foundWork) {
    value = GetMultiValue(card, kABAddressProperty);
    if (value) {
      unsigned int count = [value count];
      unsigned int j = [value indexForIdentifier:[value primaryIdentifier]];
      
      if (j < count) {
        NSDictionary *address = [value valueAtIndex:j];
        if (address) {
          SET_STRING([address objectForKey:kABAddressStreetKey],
                     HomeAddress, aNotify, abManager);
          SET_STRING([address objectForKey:kABAddressCityKey],
                     HomeCity, aNotify, abManager);
          SET_STRING([address objectForKey:kABAddressStateKey],
                     HomeState, aNotify, abManager);
          SET_STRING([address objectForKey:kABAddressZIPKey],
                     HomeZipCode, aNotify, abManager);
          SET_STRING([address objectForKey:kABAddressCountryKey],
                     HomeCountry, aNotify, abManager);
        }
      }
    }
  }
  // This was kABAIMInstantProperty previously, but it was deprecated in OS X 10.7.
  #ifndef kABInstantMessageProperty
  #define kABInstantMessageProperty kABAIMInstantProperty
  #endif
  value = GetMultiValue(card, kABInstantMessageProperty);
  if (value) {
    unsigned int count = [value count];
    for (size_t i = 0; i < count; i++) {
      id imValue = [value valueAtIndex:i];
      // Depending on the macOS version, imValue can be an NSString or an NSDictionary.
      if ([imValue isKindOfClass:[NSString class]]) {
        if (i == [value indexForIdentifier:[value primaryIdentifier]]) {
          SET_STRING(imValue, _AimScreenName, aNotify, abManager);
        }
      } else if ([imValue isKindOfClass:[NSDictionary class]]) {
        NSString* instantMessageService = [imValue objectForKey:@"InstantMessageService"];
        const char* fieldName = InstantMessengerFieldName(instantMessageService);
        NSString* userName = [imValue objectForKey:@"InstantMessageUsername"];
        SetStringProperty(this, userName, fieldName, aNotify, abManager);
      }
    }
  }
  
#define MAP_DATE(_date, _name, _notify, _session) \
  MapDate(this, _date, #_name"Year", #_name"Month", #_name"Day", _notify, \
  _session)
    
    NSDate *date = [card valueForProperty:kABBirthdayProperty];
  if (date)
    MAP_DATE(date, Birth, aNotify, abManager);
  
  if (kABOtherDatesProperty) {
    value = GetMultiValue(card, kABOtherDatesProperty);
    if (value) {
      unsigned int j, count = [value count];
      for (j = 0; j < count; ++j) {
        if ([[value labelAtIndex:j] isEqualToString:kABAnniversaryLabel]) {
          date = [value valueAtIndex:j];
          if (date) {
            MAP_DATE(date, Anniversary, aNotify, abManager);
            
            break;
          }
        }
      }
    }
  }
#undef MAP_DATE
#undef SET_STRING
  
  date = [card valueForProperty:kABModificationDateProperty];
  if (date) 
    SetPropertyAsUint32("LastModifiedDate",
                        uint32_t([date timeIntervalSince1970]));
    // XXX No way to notify about this?

  return NS_OK;

  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
}
