#import <libxml/parser.h>

#import "DAVResponse.h"
#import "HTTPLogging.h"

// WebDAV specifications: http://webdav.org/specs/rfc4918.html

typedef enum {
  kDAVProperty_ResourceType = (1 << 0),
  kDAVProperty_CreationDate = (1 << 1),
  kDAVProperty_LastModified = (1 << 2),
  kDAVProperty_ContentLength = (1 << 3),
  kDAVAllProperties = kDAVProperty_ResourceType | kDAVProperty_CreationDate | kDAVProperty_LastModified | kDAVProperty_ContentLength
} DAVProperties;

#define kXMLParseOptions (XML_PARSE_NONET | XML_PARSE_RECOVER | XML_PARSE_NOBLANKS | XML_PARSE_COMPACT | XML_PARSE_NOWARNING | XML_PARSE_NOERROR)

static const int httpLogLevel = HTTP_LOG_LEVEL_WARN;

@implementation DAVResponse

static void _AddPropertyResponse(NSString* itemPath, NSString* resourcePath, DAVProperties properties, NSMutableString* xmlString) {
  CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)resourcePath, NULL,
                                                                    CFSTR("<&>?+"), kCFStringEncodingUTF8);
  if (escapedPath) {
    NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:itemPath error:NULL];
    BOOL isDirectory = [[attributes fileType] isEqualToString:NSFileTypeDirectory];
    [xmlString appendString:@"<D:response>"];
      [xmlString appendFormat:@"<D:href>%@</D:href>", escapedPath];
      [xmlString appendString:@"<D:propstat>"];
        [xmlString appendString:@"<D:prop>"];
        
          if (properties & kDAVProperty_ResourceType) {
            if (isDirectory) {
              [xmlString appendString:@"<D:resourcetype><D:collection/></D:resourcetype>"];
            } else {
              [xmlString appendString:@"<D:resourcetype/>"];
            }
          }
          
          if ((properties & kDAVProperty_CreationDate) && [attributes objectForKey:NSFileCreationDate]) {
            NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
            formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
            formatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"];
            formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'+00:00'";
            [xmlString appendFormat:@"<D:creationdate>%@</D:creationdate>", [formatter stringFromDate:[attributes fileCreationDate]]];
          }
          
          if ((properties & kDAVProperty_LastModified) && [attributes objectForKey:NSFileModificationDate]) {
            NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
            formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
            formatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"];
            formatter.dateFormat = @"EEE', 'd' 'MMM' 'yyyy' 'HH:mm:ss' GMT'";
            [xmlString appendFormat:@"<D:getlastmodified>%@</D:getlastmodified>", [formatter stringFromDate:[attributes fileModificationDate]]];
          }
          
          if ((properties & kDAVProperty_ContentLength) && !isDirectory && [attributes objectForKey:NSFileSize]) {
            [xmlString appendFormat:@"<D:getcontentlength>%qu</D:getcontentlength>", [attributes fileSize]];
          }
        
        [xmlString appendString:@"</D:prop>"];
        [xmlString appendString:@"<D:status>HTTP/1.1 200 OK</D:status>"];
      [xmlString appendString:@"</D:propstat>"];
    [xmlString appendString:@"</D:response>\n"];
    CFRelease(escapedPath);
  }
}

static xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name) {
  while (child) {
    if ((child->type == XML_ELEMENT_NODE) && !xmlStrcmp(child->name, name)) {
      return child;
    }
    child = child->next;
  }
  return NULL;
}

- (id) initWithMethod:(NSString*)method headers:(NSDictionary*)headers bodyData:(NSData*)body resourcePath:(NSString*)resourcePath rootPath:(NSString*)rootPath {
  if ((self = [super init])) {
    _status = 200;
    _headers = [[NSMutableDictionary alloc] init];
    
    // 10.1 DAV Header
    if ([method isEqualToString:@"OPTIONS"]) {
      if ([[headers objectForKey:@"User-Agent"] hasPrefix:@"WebDAVFS/"]) {  // Mac OS X WebDAV support
        [_headers setObject:@"1, 2" forKey:@"DAV"];
      } else {
        [_headers setObject:@"1" forKey:@"DAV"];
      }
    }
    
    // 9.1 PROPFIND Method
    if ([method isEqualToString:@"PROPFIND"]) {
      NSInteger depth;
      NSString* depthHeader = [headers objectForKey:@"Depth"];
      if ([depthHeader isEqualToString:@"0"]) {
        depth = 0;
      } else if ([depthHeader isEqualToString:@"1"]) {
        depth = 1;
      } else {
        HTTPLogError(@"Unsupported DAV depth \"%@\"", depthHeader);
        return nil;
      }
      
      DAVProperties properties = 0;
      xmlDocPtr document = xmlReadMemory(body.bytes, (int)body.length, NULL, NULL, kXMLParseOptions);
      if (document) {
        xmlNodePtr node = _XMLChildWithName(document->children, (const xmlChar*)"propfind");
        if (node) {
          node = _XMLChildWithName(node->children, (const xmlChar*)"prop");
        }
        if (node) {
          node = node->children;
          while (node) {
            if (!xmlStrcmp(node->name, (const xmlChar*)"resourcetype")) {
              properties |= kDAVProperty_ResourceType;
            } else if (!xmlStrcmp(node->name, (const xmlChar*)"creationdate")) {
              properties |= kDAVProperty_CreationDate;
            } else if (!xmlStrcmp(node->name, (const xmlChar*)"getlastmodified")) {
              properties |= kDAVProperty_LastModified;
            } else if (!xmlStrcmp(node->name, (const xmlChar*)"getcontentlength")) {
              properties |= kDAVProperty_ContentLength;
            } else {
              HTTPLogWarn(@"Unknown DAV property requested \"%s\"", node->name);
            }
            node = node->next;
          }
        } else {
          HTTPLogWarn(@"HTTP Server: Invalid DAV properties\n%@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]);
        }
        xmlFreeDoc(document);
      }
      if (!properties) {
        properties = kDAVAllProperties;
      }
      
      NSString* basePath = [rootPath stringByAppendingPathComponent:resourcePath];
      if (![basePath hasPrefix:rootPath] || ![[NSFileManager defaultManager] fileExistsAtPath:basePath]) {
        return nil;
      }
      
      NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
      [xmlString appendString:@"<D:multistatus xmlns:D=\"DAV:\">\n"];
      if (![resourcePath hasPrefix:@"/"]) {
        resourcePath = [@"/" stringByAppendingString:resourcePath];
      }
      _AddPropertyResponse(basePath, resourcePath, properties, xmlString);
      if (depth == 1) {
        if (![resourcePath hasSuffix:@"/"]) {
          resourcePath = [resourcePath stringByAppendingString:@"/"];
        }
        NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:basePath];
        NSString* path;
        while ((path = [enumerator nextObject])) {
          _AddPropertyResponse([basePath stringByAppendingPathComponent:path], [resourcePath stringByAppendingString:path], properties, xmlString);
          [enumerator skipDescendents];
        }
      }
      [xmlString appendString:@"</D:multistatus>"];
      
      [_headers setObject:@"application/xml; charset=\"utf-8\"" forKey:@"Content-Type"];
      _data = [xmlString dataUsingEncoding:NSUTF8StringEncoding];
      _status = 207;
    }
    
    // 9.3 MKCOL Method
    if ([method isEqualToString:@"MKCOL"]) {
      NSString* path = [rootPath stringByAppendingPathComponent:resourcePath];
      if (![path hasPrefix:rootPath]) {
        return nil;
      }
      
      if (![[NSFileManager defaultManager] fileExistsAtPath:[path stringByDeletingLastPathComponent]]) {
        HTTPLogError(@"Missing intermediate collection(s) at \"%@\"", path);
        _status = 409;
      } else if (![[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:NO attributes:nil error:NULL]) {
        HTTPLogError(@"Failed creating collection at \"%@\"", path);
        _status = 405;
      }
    }
    
    // 9.8 COPY Method
    // 9.9 MOVE Method
    if ([method isEqualToString:@"MOVE"] || [method isEqualToString:@"COPY"]) {
      if ([method isEqualToString:@"COPY"] && ![[headers objectForKey:@"Depth"] isEqualToString:@"infinity"]) {
        HTTPLogError(@"Unsupported DAV depth \"%@\"", [headers objectForKey:@"Depth"]);
        return nil;
      }
      
      NSString* sourcePath = [rootPath stringByAppendingPathComponent:resourcePath];
      if (![sourcePath hasPrefix:rootPath] || ![[NSFileManager defaultManager] fileExistsAtPath:sourcePath]) {
        return nil;
      }
      
      NSString* destination = [headers objectForKey:@"Destination"];
      NSRange range = [destination rangeOfString:[headers objectForKey:@"Host"]];
      if (range.location == NSNotFound) {
        return nil;
      }
      NSString* destinationPath = [rootPath stringByAppendingPathComponent:
        [[destination substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
      if (![destinationPath hasPrefix:rootPath] || [[NSFileManager defaultManager] fileExistsAtPath:destinationPath]) {
        return nil;
      }
      
      BOOL isDirectory;
      if (![[NSFileManager defaultManager] fileExistsAtPath:[destinationPath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
        HTTPLogError(@"Invalid destination path \"%@\"", destinationPath);
        _status = 409;
      } else {
        BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:destinationPath];
        if (existing && [[headers objectForKey:@"Overwrite"] isEqualToString:@"F"]) {
          HTTPLogError(@"Pre-existing destination path \"%@\"", destinationPath);
          _status = 412;
        } else {
          if ([method isEqualToString:@"COPY"]) {
            if ([[NSFileManager defaultManager] copyItemAtPath:sourcePath toPath:destinationPath error:NULL]) {
              _status = existing ? 204 : 201;
            } else {
              HTTPLogError(@"Failed copying \"%@\" to \"%@\"", sourcePath, destinationPath);
              _status = 403;
            }
          } else {
            if ([[NSFileManager defaultManager] moveItemAtPath:sourcePath toPath:destinationPath error:NULL]) {
              _status = existing ? 204 : 201;
            } else {
              HTTPLogError(@"Failed moving \"%@\" to \"%@\"", sourcePath, destinationPath);
              _status = 403;
            }
          }
        }
      }
    }
    
    // 9.10 LOCK Method - TODO: Actually lock the resource
    if ([method isEqualToString:@"LOCK"]) {
      NSString* path = [rootPath stringByAppendingPathComponent:resourcePath];
      if (![path hasPrefix:rootPath]) {
        return nil;
      }
      
      NSString* depth = [headers objectForKey:@"Depth"];
      NSString* scope = nil;
      NSString* type = nil;
      NSString* owner = nil;
      NSString* token = nil;
      xmlDocPtr document = xmlReadMemory(body.bytes, (int)body.length, NULL, NULL, kXMLParseOptions);
      if (document) {
        xmlNodePtr node = _XMLChildWithName(document->children, (const xmlChar*)"lockinfo");
        if (node) {
          xmlNodePtr scopeNode = _XMLChildWithName(node->children, (const xmlChar*)"lockscope");
          if (scopeNode && scopeNode->children && scopeNode->children->name) {
            scope = [NSString stringWithUTF8String:(const char*)scopeNode->children->name];
          }
          xmlNodePtr typeNode = _XMLChildWithName(node->children, (const xmlChar*)"locktype");
          if (typeNode && typeNode->children && typeNode->children->name) {
            type = [NSString stringWithUTF8String:(const char*)typeNode->children->name];
          }
          xmlNodePtr ownerNode = _XMLChildWithName(node->children, (const xmlChar*)"owner");
          if (ownerNode) {
            ownerNode = _XMLChildWithName(ownerNode->children, (const xmlChar*)"href");
            if (ownerNode && ownerNode->children && ownerNode->children->content) {
              owner = [NSString stringWithUTF8String:(const char*)ownerNode->children->content];
            }
          }
        } else {
          HTTPLogWarn(@"HTTP Server: Invalid DAV properties\n%@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]);
        }
        xmlFreeDoc(document);
      } else {
		  // No body, see if they're trying to refresh an existing lock.  If so, then just fake up the scope, type and depth so we fall
		  // into the lock create case.
		  NSString* lockToken;
		  if ((lockToken = [headers objectForKey:@"If"]) != nil) {
			  scope = @"exclusive";
			  type = @"write";
			  depth = @"0";
			  token = [lockToken stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"(<>)"]];
		  }
	  }
      if ([scope isEqualToString:@"exclusive"] && [type isEqualToString:@"write"] && [depth isEqualToString:@"0"] &&
        ([[NSFileManager defaultManager] fileExistsAtPath:path] || [[NSData data] writeToFile:path atomically:YES])) {
        NSString* timeout = [headers objectForKey:@"Timeout"];
		if (!token) {
          CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
          NSString *uuidStr = (__bridge_transfer NSString *)CFUUIDCreateString(kCFAllocatorDefault, uuid);
          token = [NSString stringWithFormat:@"urn:uuid:%@", uuidStr];
          CFRelease(uuid);
		}
        
        NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
        [xmlString appendString:@"<D:prop xmlns:D=\"DAV:\">\n"];
        [xmlString appendString:@"<D:lockdiscovery>\n<D:activelock>\n"];
        [xmlString appendFormat:@"<D:locktype><D:%@/></D:locktype>\n", type];
        [xmlString appendFormat:@"<D:lockscope><D:%@/></D:lockscope>\n", scope];
        [xmlString appendFormat:@"<D:depth>%@</D:depth>\n", depth];
        if (owner) {
          [xmlString appendFormat:@"<D:owner><D:href>%@</D:href></D:owner>\n", owner];
        }
        if (timeout) {
          [xmlString appendFormat:@"<D:timeout>%@</D:timeout>\n", timeout];
        }
        [xmlString appendFormat:@"<D:locktoken><D:href>%@</D:href></D:locktoken>\n", token];
		NSString* lockroot = [@"http://" stringByAppendingString:[[headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:resourcePath]]];
        [xmlString appendFormat:@"<D:lockroot><D:href>%@</D:href></D:lockroot>\n", lockroot];
        [xmlString appendString:@"</D:activelock>\n</D:lockdiscovery>\n"];
        [xmlString appendString:@"</D:prop>"];
        
        [_headers setObject:@"application/xml; charset=\"utf-8\"" forKey:@"Content-Type"];
        _data = [xmlString dataUsingEncoding:NSUTF8StringEncoding];
        _status = 200;
        HTTPLogVerbose(@"Pretending to lock \"%@\"", resourcePath);
      } else {
        HTTPLogError(@"Locking request \"%@/%@/%@\" for \"%@\" is not allowed", scope, type, depth, resourcePath);
        _status = 403;
      }
    }
    
    // 9.11 UNLOCK Method - TODO: Actually unlock the resource
    if ([method isEqualToString:@"UNLOCK"]) {
      NSString* path = [rootPath stringByAppendingPathComponent:resourcePath];
      if (![path hasPrefix:rootPath] || ![[NSFileManager defaultManager] fileExistsAtPath:path]) {
        return nil;
      }
      
      NSString* token = [headers objectForKey:@"Lock-Token"];
      _status = token ? 204 : 400;
      HTTPLogVerbose(@"Pretending to unlock \"%@\"", resourcePath);
    }
    
  }
  return self;
}


- (UInt64) contentLength {
  return _data ? _data.length : 0;
}

- (UInt64) offset {
  return _offset;
}

- (void) setOffset:(UInt64)offset {
  _offset = offset;
}

- (NSData*) readDataOfLength:(NSUInteger)lengthParameter {
  if (_data) {
    NSUInteger remaining = _data.length - (NSUInteger)_offset;
    NSUInteger length = lengthParameter < remaining ? lengthParameter : remaining;
    void* bytes = (void*)(_data.bytes + _offset);
    _offset += length;
    return [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:NO];
  }
  return nil;
}

- (BOOL) isDone {
  return _data ? _offset == _data.length : YES;
}

- (NSInteger) status {
  return _status;
}

- (NSDictionary*) httpHeaders {
  return _headers;
}

@end
