/* SOGoContactSourceFolder.m - this file is part of SOGo
 *
 * Copyright (C) 2006-2015 Inverse inc.
 *
 * This file 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, or (at your option)
 * any later version.
 *
 * This file 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; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSString.h>
#import <Foundation/NSURL.h>
#import <Foundation/NSValue.h>

#import <NGObjWeb/NSException+HTTP.h>
#import <NGObjWeb/WOApplication.h>
#import <NGObjWeb/WOContext+SoObjects.h>
#import <NGObjWeb/WORequest.h>
#import <NGObjWeb/WOResponse.h>
#import <NGObjWeb/SoObject.h>
#import <NGObjWeb/SoSelectorInvocation.h>
#import <NGObjWeb/SoUser.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSString+misc.h>
#import <DOM/DOMElement.h>
#import <DOM/DOMProtocols.h>
#import <EOControl/EOSortOrdering.h>
#import <SaxObjC/XMLNamespaces.h>

#import <SOGo/DOMNode+SOGo.h>
#import <SOGo/NSArray+Utilities.h>
#import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/NSObject+DAV.h>
#import <SOGo/SOGoPermissions.h>
#import <SOGo/SOGoSource.h>
#import <SOGo/SOGoUserSettings.h>
#import <SOGo/WORequest+SOGo.h>
#import <SOGo/WOResponse+SOGo.h>

#import "SOGoContactFolders.h"
#import "SOGoContactGCSFolder.h"
#import "SOGoContactLDIFEntry.h"
#import "SOGoContactSourceFolder.h"

@class WOContext;

@implementation SOGoContactSourceFolder

+ (id) folderWithName: (NSString *) aName
       andDisplayName: (NSString *) aDisplayName
	  inContainer: (id) aContainer
{
  id folder;

  folder = [[self alloc] initWithName: aName
                         andDisplayName: aDisplayName
                         inContainer: aContainer];
  [folder autorelease];

  return folder;
}

- (id) init
{
  if ((self = [super init]))
    {
      childRecords = [NSMutableDictionary new];
      source = nil;
    }

  return self;
}

- (id) initWithName: (NSString *) newName
     andDisplayName: (NSString *) newDisplayName
	inContainer: (id) newContainer
{
  if ((self = [self initWithName: newName
		    inContainer: newContainer]))
    {
      if (![newDisplayName length])
        newDisplayName = newName;
      ASSIGN (displayName, newDisplayName);
    }

  return self;
}

- (void) dealloc
{
  [childRecords release];
  [source release];
  [super dealloc];
}

- (void) setSource: (id <SOGoSource>) newSource
{
  ASSIGN (source, newSource);
}

- (id <SOGoSource>) source
{
  return source;
}

- (void) setIsPersonalSource: (BOOL) isPersonal
{
  isPersonalSource = isPersonal;
}

- (BOOL) isPersonalSource
{
  return isPersonalSource;
}

- (NSString *) groupDavResourceType
{
  return @"vcard-collection";
}

- (NSArray *) davResourceType
{
  NSMutableArray *resourceType;
  NSArray *type;

  resourceType = [NSMutableArray arrayWithArray: [super davResourceType]];
  type = [NSArray arrayWithObjects: @"addressbook", XMLNS_CARDDAV, nil];
  [resourceType addObject: type];
  type = [NSArray arrayWithObjects: @"directory", XMLNS_CARDDAV, nil];
  [resourceType addObject: type];

  return resourceType;
}

- (id) lookupName: (NSString *) objectName
        inContext: (WOContext *) lookupContext
          acquire: (BOOL) acquire
{
  NSDictionary *ldifEntry;
  SOGoContactLDIFEntry *obj;
  NSString *url;
  BOOL isNew = NO;
  NSArray *baseClasses;

  /* first check attributes directly bound to the application */
  obj = [super lookupName: objectName inContext: lookupContext acquire: NO];

  if (!obj)
    {
      ldifEntry = [childRecords objectForKey: objectName];
      if (!ldifEntry)
        {
          ldifEntry = [source lookupContactEntry: objectName];
	  if (ldifEntry)
	    [childRecords setObject: ldifEntry forKey: objectName];
          else if ([self isValidContentName: objectName])
            {
              url = [[[lookupContext request] uri] urlWithoutParameters];
              if ([url hasSuffix: @"AsContact"])
                {
                  baseClasses = [NSArray arrayWithObjects: @"inetorgperson",
                                         @"mozillaabpersonalpha", nil];
                  ldifEntry = [NSMutableDictionary
                                dictionaryWithObject: baseClasses
                                forKey: @"objectclass"];
                  isNew = YES;
                }
            }
        }
      if (ldifEntry)
        {
          obj = [SOGoContactLDIFEntry contactEntryWithName: objectName
                                             withLDIFEntry: ldifEntry
                                               inContainer: self];
          if (isNew)
            [obj setIsNew: YES];
        }
      else
        obj = [NSException exceptionWithHTTPStatus: 404];
    }

  return obj;
}

- (NSArray *) toOneRelationshipKeys
{
  NSString *userDomain;

  userDomain = [[context activeUser] domain];
  return [source allEntryIDsVisibleFromDomain: userDomain];
}

- (NSException *) saveLDIFEntry: (SOGoContactLDIFEntry *) ldifEntry
{
  return (([ldifEntry isNew])
          ? [source addContactEntry: [ldifEntry ldifRecord]
                    withID: [ldifEntry nameInContainer]]
          : [source updateContactEntry: [ldifEntry ldifRecord]]);
}

- (NSException *) deleteLDIFEntry: (SOGoContactLDIFEntry *) ldifEntry
{
  return [source removeContactEntryWithID: [ldifEntry nameInContainer]];
}

/**
 * Normalize keys of dictionary representing a contact.
 * @param oldRecord a dictionary with pairs from the source folder (LDAP or SQL)
 * @see [SOGoContactGCSFolder _fixupContactRecord]
 */
- (NSDictionary *) _flattenedRecord: (NSDictionary *) oldRecord
{
  NSMutableDictionary *newRecord;
  id data;
  NSObject <SOGoSource> *recordSource;

  newRecord = [NSMutableDictionary dictionaryWithCapacity: 8];
  [newRecord setObject: [oldRecord objectForKey: @"c_uid"]
                forKey: @"c_uid"];

  // c_name => id
  [newRecord setObject: [oldRecord objectForKey: @"c_name"]
                forKey: @"c_name"];
  [newRecord setObject: [oldRecord objectForKey: @"c_name"]
                forKey: @"id"];

  // displayname || c_cn => fn
  data = [oldRecord objectForKey: @"displayname"];
  if (!data)
    data = [oldRecord objectForKey: @"c_cn"];
  if (data)
    [newRecord setObject: data forKey: @"fn"];
  else
    data = @"";
  [newRecord setObject: data forKey: @"c_cn"];

  // mail => emails[]
  data = [oldRecord objectForKey: @"c_emails"];
  if (data)
    {
      if ([data isKindOfClass: [NSArray class]])
        {
          if ([data count] > 0)
            {
              NSEnumerator *emails;
              NSMutableArray *recordEmails;
              NSString *email;
              emails = [(NSArray *)data objectEnumerator];
              recordEmails = [NSMutableArray arrayWithCapacity: [data count]];
              while ((email = [emails nextObject]))
                {
                  [recordEmails addObject: [NSDictionary dictionaryWithObject: email forKey: @"value"]];
                }
              [newRecord setObject: recordEmails forKey: @"emails"];
            }
        }
      else if (data)
        {
          NSDictionary *email;
          email = [NSDictionary dictionaryWithObjectsAndKeys: @"pref", @"type", data, @"value", nil];
          [newRecord setObject: [NSArray arrayWithObject: email] forKey: @"emails"];
        }
      else
        data = @"";
    }
  else
    data = @"";
  [newRecord setObject: data forKey: @"c_mail"];

  data = [oldRecord objectForKey: @"nsaimid"];
  if (![data length])
    data = [oldRecord objectForKey: @"nscpaimscreenname"];
  if (![data length])
    data = @"";
  [newRecord setObject: data forKey: @"c_screenname"];

  // o => org
  data = [oldRecord objectForKey: @"o"];
  if (data)
    [newRecord setObject: data forKey: @"org"];
  else
    data = @"";
  [newRecord setObject: data forKey: @"c_o"];

  // telephonenumber || cellphone || homephone => phones[]
  data = [oldRecord objectForKey: @"telephonenumber"];
  if (![data length])
    data = [oldRecord objectForKey: @"cellphone"];
  if (![data length])
    data = [oldRecord objectForKey: @"homephone"];
  if (data)
    {
      NSDictionary *phonenumber;
      phonenumber = [NSDictionary dictionaryWithObjectsAndKeys: @"pref", @"type", data, @"value", nil];
      [newRecord setObject: [NSArray arrayWithObject: phonenumber] forKey: @"phones"];
    }
  else
    data = @"";
  [newRecord setObject: data forKey: @"c_telephonenumber"];

  // Custom attribute for group-lookups. See LDAPSource.m where
  // it's set.
  data = [oldRecord objectForKey: @"isGroup"];
  if (data)
    {
      [newRecord setObject: data forKey: @"isGroup"];
      [newRecord setObject: @"vlist" forKey: @"c_component"];
    }
#warning TODO: create a custom icon for resources
  else
    {
      [newRecord setObject: @"vcard" forKey: @"c_component"];
    }
  
  // c_info => note
  data = [oldRecord objectForKey: @"c_info"];
  if ([data length] > 0)
    {
      [newRecord setObject: data forKey: @"note"];
      [newRecord setObject: data forKey: @"contactInfo"];
    }

  recordSource = [oldRecord objectForKey: @"source"];
  if ([recordSource conformsToProtocol: @protocol (SOGoDNSource)] &&
      [[(NSObject <SOGoDNSource>*) recordSource MSExchangeHostname] length])
    [newRecord setObject: [NSNumber numberWithInt: 1] forKey: @"isMSExchange"];
  
  return newRecord;
}

- (NSArray *) _flattenedRecords: (NSArray *) records
{
  NSMutableArray *newRecords;
  NSEnumerator *oldRecords;
  NSDictionary *oldRecord;

  newRecords = [NSMutableArray arrayWithCapacity: [records count]];

  oldRecords = [records objectEnumerator];
  while ((oldRecord = [oldRecords nextObject]))
    [newRecords addObject: [self _flattenedRecord: oldRecord]];

  return newRecords;
}

/* This method returns the entry corresponding to the name passed as
   parameter. */
- (NSDictionary *) lookupContactWithName: (NSString *) aName
{
  NSDictionary *record;

  if (aName && [aName length] > 0)
    record = [self _flattenedRecord: [source lookupContactEntry: aName]];
  else
    record = nil;
  
  return record;
}

- (NSArray *) lookupContactsWithFilter: (NSString *) filter
                            onCriteria: (NSString *) criteria
                                sortBy: (NSString *) sortKey
                              ordering: (NSComparisonResult) sortOrdering
                              inDomain: (NSString *) domain
{
  NSArray *records, *result;
  EOSortOrdering *ordering;

  result = nil;

  if (([filter length] > 0 && [criteria isEqualToString: @"name_or_address"])
      || ![source listRequiresDot])
    {
      records = [source fetchContactsMatching: filter
                                     inDomain: domain];
      [childRecords setObjects: records
                       forKeys: [records objectsForKey: @"c_name"
                                        notFoundMarker: nil]];
      records = [self _flattenedRecords: records];
      ordering
        = [EOSortOrdering sortOrderingWithKey: sortKey
                          selector: ((sortOrdering == NSOrderedDescending)
                                     ? EOCompareCaseInsensitiveDescending
                                     : EOCompareCaseInsensitiveAscending)];
      result
        = [records sortedArrayUsingKeyOrderArray:
                     [NSArray arrayWithObject: ordering]];
    }

  return result;
}

- (NSString *) _deduceObjectNameFromURL: (NSString *) url
                            fromBaseURL: (NSString *) baseURL
{
  NSRange urlRange;
  NSString *name;

  urlRange = [url rangeOfString: baseURL];
  if (urlRange.location != NSNotFound)
    {
      name = [url substringFromIndex: NSMaxRange (urlRange)];
      if ([name hasPrefix: @"/"])
        name = [name substringFromIndex: 1];
    }
  else
    name = nil;

  return name;
}

/* TODO: multiget reorg */
- (NSString *) _nodeTagForProperty: (NSString *) property
{
  NSString *namespace, *nodeName, *nsRep;
  NSRange nsEnd;

  nsEnd = [property rangeOfString: @"}"];
  namespace
    = [property substringFromRange: NSMakeRange (1, nsEnd.location - 1)];
  nodeName = [property substringFromIndex: nsEnd.location + 1];
  if ([namespace isEqualToString: XMLNS_CARDDAV])
    nsRep = @"C";
  else
    nsRep = @"D";

  return [NSString stringWithFormat: @"%@:%@", nsRep, nodeName];
}

- (NSString *) _nodeTag: (NSString *) property
{
  static NSMutableDictionary *tags = nil;
  NSString *nodeTag;

  if (!tags)
    tags = [NSMutableDictionary new];
  nodeTag = [tags objectForKey: property];
  if (!nodeTag)
    {
      nodeTag = [self _nodeTagForProperty: property];
      [tags setObject: nodeTag forKey: property];
    }

  return nodeTag;
}

- (NSString **) _properties: (NSString **) properties
                      count: (unsigned int) propertiesCount
                   ofObject: (NSDictionary *) object
{
  SOGoContactLDIFEntry *ldifEntry;
  NSString **currentProperty;
  NSString **values, **currentValue;
  SEL methodSel;

//   NSLog (@"_properties:ofObject:: %@", [NSDate date]);

  values = NSZoneMalloc (NULL,
                         (propertiesCount + 1) * sizeof (NSString *));
  *(values + propertiesCount) = nil;

  ldifEntry = [SOGoContactLDIFEntry
                contactEntryWithName: [object objectForKey: @"c_name"]
                       withLDIFEntry: object
                         inContainer: self];
  currentProperty = properties;
  currentValue = values;
  while (*currentProperty)
    {
      methodSel = SOGoSelectorForPropertyGetter (*currentProperty);
      if (methodSel && [ldifEntry respondsToSelector: methodSel])
        *currentValue = [[ldifEntry performSelector: methodSel]
                          stringByEscapingXMLString];
      currentProperty++;
      currentValue++;
    }

//    NSLog (@"/_properties:ofObject:: %@", [NSDate date]);

  return values;
}

- (NSArray *) _propstats: (NSString **) properties
                   count: (unsigned int) propertiesCount
		ofObject: (NSDictionary *) object
{
  NSMutableArray *propstats, *properties200, *properties404, *propDict;
  NSString **property, **values, **currentValue;
  NSString *propertyValue, *nodeTag;

//   NSLog (@"_propstats:ofObject:: %@", [NSDate date]);

  propstats = [NSMutableArray array];

  properties200 = [NSMutableArray array];
  properties404 = [NSMutableArray array];

  values = [self _properties: properties count: propertiesCount
                    ofObject: object];
  currentValue = values;

  property = properties;
  while (*property)
    {
      nodeTag = [self _nodeTag: *property];
      if (*currentValue)
	{
	  propertyValue = [NSString stringWithFormat: @"<%@>%@</%@>",
				    nodeTag, *currentValue, nodeTag];
	  propDict = properties200;
	}
      else
	{
	  propertyValue = [NSString stringWithFormat: @"<%@/>", nodeTag];
	  propDict = properties404;
	}
      [propDict addObject: propertyValue];
      property++;
      currentValue++;
    }
  free (values);

  if ([properties200 count])
    [propstats addObject: [NSDictionary dictionaryWithObjectsAndKeys:
					  properties200, @"properties",
					@"HTTP/1.1 200 OK", @"status",
					nil]];
  if ([properties404 count])
    [propstats addObject: [NSDictionary dictionaryWithObjectsAndKeys:
					  properties404, @"properties",
					@"HTTP/1.1 404 Not Found", @"status",
					nil]];
//    NSLog (@"/_propstats:ofObject:: %@", [NSDate date]);

  return propstats;
}

- (void) _appendPropstat: (NSDictionary *) propstat
                toBuffer: (NSMutableString *) r
{
  NSArray *properties;
  unsigned int count, max;

  [r appendString: @"<D:propstat><D:prop>"];
  properties = [propstat objectForKey: @"properties"];
  max = [properties count];
  for (count = 0; count < max; count++)
    [r appendString: [properties objectAtIndex: count]];
  [r appendString: @"</D:prop><D:status>"];
  [r appendString: [propstat objectForKey: @"status"]];
  [r appendString: @"</D:status></D:propstat>"];
}

- (void) appendObject: (NSDictionary *) object
	   properties: (NSString **) properties
                count: (unsigned int) propertiesCount
          withBaseURL: (NSString *) baseURL
	     toBuffer: (NSMutableString *) r
{
  NSArray *propstats;
  unsigned int count, max;

  [r appendFormat: @"<D:response><D:href>"];
  [r appendString: baseURL];
  [r appendString: [[object objectForKey: @"c_name"] stringByEscapingURL]];
  [r appendString: @"</D:href>"];

  propstats = [self _propstats: properties count: propertiesCount
                      ofObject: object];
  max = [propstats count];
  for (count = 0; count < max; count++)
    [self _appendPropstat: [propstats objectAtIndex: count]
                 toBuffer: r];

  [r appendString: @"</D:response>"];
}

- (void) appendMissingObjectRef: (NSString *) href
		       toBuffer: (NSMutableString *) r
{
  [r appendString: @"<D:response><D:href>"];
  [r appendString: href];
  [r appendString: @"</D:href><D:status>HTTP/1.1 404 Not Found</D:status></D:response>"];
}

- (void) _appendComponentProperties: (NSArray *) properties
                       matchingURLs: (id <DOMNodeList>) refs
                         toResponse: (WOResponse *) response
{
  NSObject <DOMElement> *element;
  NSString *url, *baseURL, *cname;
  NSString **propertiesArray;
  NSMutableString *buffer;
  NSDictionary *object;

  unsigned int count, max, propertiesCount;
  
  baseURL = [self davURLAsString];
#warning review this when fixing http://www.scalableogo.org/bugs/view.php?id=276
  if (![baseURL hasSuffix: @"/"])
    baseURL = [NSString stringWithFormat: @"%@/", baseURL];

  propertiesArray = [properties asPointersOfObjects];
  propertiesCount = [properties count];

  max = [refs length];
  buffer = [NSMutableString stringWithCapacity: max*512];

  for (count = 0; count < max; count++)
    {
      element = [refs objectAtIndex: count];
      url = [[[element firstChild] nodeValue] stringByUnescapingURL];
      cname = [self _deduceObjectNameFromURL: url fromBaseURL: baseURL];
      object = [source lookupContactEntry: cname];
      if (object)
        [self appendObject: object
                properties: propertiesArray
                     count: propertiesCount
               withBaseURL: baseURL
                  toBuffer: buffer];
      else
        [self appendMissingObjectRef: url
                            toBuffer: buffer];
    }
  [response appendContentString: buffer];
//   NSLog (@"/adding properties with url");

  NSZoneFree (NULL, propertiesArray);
}

- (WOResponse *) performMultigetInContext: (WOContext *) queryContext
                              inNamespace: (NSString *) namespace
{
  WOResponse *r;
  id <DOMDocument> document;
  id <DOMElement> documentElement, propElement;

  r = [context response];
  [r prepareDAVResponse];
  [r appendContentString:
       [NSString stringWithFormat: @"<D:multistatus xmlns:D=\"DAV:\""
                         @" xmlns:C=\"%@\">", namespace]];
  document = [[queryContext request] contentAsDOMDocument];
  documentElement = [document documentElement];
  propElement = [(NGDOMNodeWithChildren *) documentElement
                    firstElementWithTag: @"prop"
                            inNamespace: @"DAV:"];
  [self _appendComponentProperties: [(NGDOMNodeWithChildren *) propElement flatPropertyNameOfSubElements]
                      matchingURLs: [documentElement getElementsByTagName: @"href"]
                        toResponse: r];
  [r appendContentString:@"</D:multistatus>"];

  return r;
}

- (id) davAddressbookMultiget: (id) queryContext
{
  return [self performMultigetInContext: queryContext
                            inNamespace: XMLNS_CARDDAV];
}

- (NSString *) davDisplayName
{
  return displayName;
}

- (BOOL) isFolderish
{
  return YES;
}

/* folder type */

- (NSString *) folderType
{
  return @"Contact";
}

/* sorting */
- (NSComparisonResult) compare: (id) otherFolder
{
  NSComparisonResult comparison;
  BOOL otherIsPersonal;

  otherIsPersonal = ([otherFolder isKindOfClass: [SOGoContactGCSFolder class]]
                     || ([otherFolder isKindOfClass: isa] && [otherFolder isPersonalSource]));

  if (isPersonalSource)
    {
      if (otherIsPersonal && ![nameInContainer isEqualToString: @"personal"])
        {
          if ([[otherFolder nameInContainer] isEqualToString: @"personal"])
            comparison = NSOrderedDescending;
          else
            comparison
              = [[self displayName]
                  localizedCaseInsensitiveCompare: [otherFolder displayName]];
        }
      else
        comparison = NSOrderedAscending;
    }
  else
    {
      if (otherIsPersonal)
        comparison = NSOrderedDescending;
      else
        comparison
          = [[self displayName]
              localizedCaseInsensitiveCompare: [otherFolder displayName]];
    }

  return comparison;
}

/* common methods */

- (NSException *) delete
{
  NSException *error;

  if (isPersonalSource)
    {
      error = [(SOGoContactFolders *) container
                  removeLDAPAddressBook: nameInContainer];
      if (!error && [[context request] handledByDefaultHandler])
        [self sendFolderAdvisoryTemplate: @"Removal"];
    }
  else
    error = [NSException exceptionWithHTTPStatus: 501 /* not implemented */
                                          reason: @"delete not available on system sources"];

  return error;
}

- (void) renameTo: (NSString *) newName
{
  NSException *error;

  if (isPersonalSource)
    {
      if (![[source displayName] isEqualToString: newName])
        {
          error = [(SOGoContactFolders *) container
                      renameLDAPAddressBook: nameInContainer
                            withDisplayName: newName];
          if (!error)
            [self setDisplayName: newName];
        }
    }
  /* If public source then method is ignored, maybe we should return an
     NSException instead... */
}

/* acls */
- (NSString *) ownerInContext: (WOContext *) noContext
{
  NSString *sourceOwner;

  if (isPersonalSource)
    sourceOwner = [[source modifiers] objectAtIndex: 0];
  else
    sourceOwner = @"nobody";

  return sourceOwner;
}

- (NSArray *) subscriptionRoles
{
  return [NSArray arrayWithObject: SoRole_Authenticated];
}

- (NSArray *) aclsForUser: (NSString *) uid
{
  NSArray *acls, *modifiers;
  static NSArray *modifierRoles = nil;

  if (!modifierRoles)
    modifierRoles = [[NSArray alloc] initWithObjects: @"Owner",
                                     @"ObjectViewer",
                                     @"ObjectEditor", @"ObjectCreator",
                                     @"ObjectEraser", nil];

  modifiers = [source modifiers];
  if ([modifiers containsObject: uid])
    acls = [modifierRoles copy];
  else
    acls = [NSArray new];

  [acls autorelease];

  return acls;
}

@end
