#import "RoutingHTTPServer.h"
#import "RoutingConnection.h"
#import "Route.h"

#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Widiomatic-parentheses"

@implementation RoutingHTTPServer {
  NSMutableDictionary *routes;
  NSMutableDictionary *defaultHeaders;
  NSMutableDictionary *mimeTypes;
  dispatch_queue_t routeQueue;
}

@synthesize defaultHeaders;

- (id)init {
  if (self = [super init]) {
    connectionClass = [RoutingConnection self];
    routes = [[NSMutableDictionary alloc] init];
    defaultHeaders = [[NSMutableDictionary alloc] init];
    [self setupMIMETypes];
  }
  return self;
}

#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE
- (void)dealloc {
  if (routeQueue)
    dispatch_release(routeQueue);
}
#endif

- (void)setDefaultHeaders:(NSDictionary *)headers {
  if (headers) {
    defaultHeaders = [headers mutableCopy];
  } else {
    defaultHeaders = [[NSMutableDictionary alloc] init];
  }
}

- (void)setDefaultHeader:(NSString *)field value:(NSString *)value {
  [defaultHeaders setObject:value forKey:field];
}

- (dispatch_queue_t)routeQueue {
  return routeQueue;
}

- (void)setRouteQueue:(dispatch_queue_t)queue {
#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE
  if (queue)
    dispatch_retain(queue);
  
  if (routeQueue)
    dispatch_release(routeQueue);
#endif
  
  routeQueue = queue;
}

- (NSDictionary *)mimeTypes {
  return mimeTypes;
}

- (void)setMIMETypes:(NSDictionary *)types {
  NSMutableDictionary *newTypes;
  if (types) {
    newTypes = [types mutableCopy];
  } else {
    newTypes = [[NSMutableDictionary alloc] init];
  }
  
  mimeTypes = newTypes;
}

- (void)setMIMEType:(NSString *)theType forExtension:(NSString *)ext {
  [mimeTypes setObject:theType forKey:ext];
}

- (NSString *)mimeTypeForPath:(NSString *)path {
  NSString *ext = [[path pathExtension] lowercaseString];
  if (!ext || [ext length] < 1)
    return nil;
  
  return [mimeTypes objectForKey:ext];
}

- (void)get:(NSString *)path withBlock:(RequestHandler)block {
  [self handleMethod:@"GET" withPath:path block:block];
}

- (void)post:(NSString *)path withBlock:(RequestHandler)block {
  [self handleMethod:@"POST" withPath:path block:block];
}

- (void)put:(NSString *)path withBlock:(RequestHandler)block {
  [self handleMethod:@"PUT" withPath:path block:block];
}

- (void)delete:(NSString *)path withBlock:(RequestHandler)block {
  [self handleMethod:@"DELETE" withPath:path block:block];
}

- (void)handleMethod:(NSString *)method
            withPath:(NSString *)path
               block:(RequestHandler)block {
  Route *route = [self routeWithPath:path];
  route.handler = block;
  
  [self addRoute:route forMethod:method];
}

- (void)handleMethod:(NSString *)method
            withPath:(NSString *)path
              target:(id)target
            selector:(SEL)selector {
  Route *route = [self routeWithPath:path];
  route.target = target;
  route.selector = selector;
  
  [self addRoute:route forMethod:method];
}

- (void)addRoute:(Route *)route forMethod:(NSString *)method {
  method = [method uppercaseString];
  NSMutableArray *methodRoutes = [routes objectForKey:method];
  if (methodRoutes == nil) {
    methodRoutes = [NSMutableArray array];
    [routes setObject:methodRoutes forKey:method];
  }
  
  [methodRoutes addObject:route];
  
  // Define a HEAD route for all GET routes
  if ([method isEqualToString:@"GET"]) {
    [self addRoute:route forMethod:@"HEAD"];
  }
}

- (Route *)routeWithPath:(NSString *)path {
  Route *route = [[Route alloc] init];
  NSMutableArray *keys = [NSMutableArray array];
  
  if ([path length] > 2 && [path characterAtIndex:0] == '{') {
    // This is a custom regular expression, just remove the {}
    path = [path substringWithRange:NSMakeRange(1, [path length] - 2)];
  } else {
    NSRegularExpression *regex = nil;
    
    // Escape regex characters
    regex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]" options:(NSRegularExpressionOptions)0 error:nil];
    path = [regex stringByReplacingMatchesInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length) withTemplate:@"\\\\$0"];
    
    // Parse any :parameters and * in the path
    regex = [NSRegularExpression regularExpressionWithPattern:@"(:(\\w+)|\\*)"
                                                      options:(NSRegularExpressionOptions)0
                                                        error:nil];
    NSMutableString *regexPath = [NSMutableString stringWithString:path];
    __block NSInteger diff = 0;
    [regex enumerateMatchesInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length)
                         usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
      NSRange replacementRange = NSMakeRange(diff + result.range.location, result.range.length);
      NSString *replacementString;
      
      NSString *capturedString = [path substringWithRange:result.range];
      if ([capturedString isEqualToString:@"*"]) {
        [keys addObject:@"wildcards"];
        replacementString = @"(.*?)";
      } else {
        NSString *keyString = [path substringWithRange:[result rangeAtIndex:2]];
        [keys addObject:keyString];
        replacementString = @"([^/]+)";
      }
      
      [regexPath replaceCharactersInRange:replacementRange withString:replacementString];
      diff += replacementString.length - result.range.length;
    }];
    
    path = [NSString stringWithFormat:@"^%@$", regexPath];
  }
  
  route.regex = [NSRegularExpression regularExpressionWithPattern:path options:NSRegularExpressionCaseInsensitive error:nil];
  if ([keys count] > 0) {
    route.keys = keys;
  }
  
  return route;
}

- (BOOL)supportsMethod:(NSString *)method {
  return ([routes objectForKey:method] != nil);
}

- (void)handleRoute:(Route *)route
        withRequest:(RouteRequest *)request
           response:(RouteResponse *)response {
  if (route.handler) {
    route.handler(request, response);
  } else {
    id target = route.target;
    SEL selector = route.selector;
    NSMethodSignature *signature = [target methodSignatureForSelector:selector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    [invocation setSelector:selector];
    [invocation setArgument:&request atIndex:2];
    [invocation setArgument:&response atIndex:3];
    [invocation invokeWithTarget:target];
  }
}

- (RouteResponse *)routeMethod:(NSString *)method
                      withPath:(NSString *)path
                    parameters:(NSDictionary *)params
                       request:(HTTPMessage *)httpMessage
                    connection:(HTTPConnection *)connection {
  NSMutableArray *methodRoutes = [routes objectForKey:method];
  if (methodRoutes == nil)
    return nil;
  
  for (Route *route in methodRoutes) {
    NSTextCheckingResult *result = [route.regex firstMatchInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length)];
    if (!result)
      continue;
    
    // The first range is all of the text matched by the regex.
    NSUInteger captureCount = [result numberOfRanges];
    
    if (route.keys) {
      // Add the route's parameters to the parameter dictionary, accounting for
      // the first range containing the matched text.
      if (captureCount == [route.keys count] + 1) {
        NSMutableDictionary *newParams = [params mutableCopy];
        NSUInteger index = 1;
        BOOL firstWildcard = YES;
        for (NSString *key in route.keys) {
          NSString *capture = [path substringWithRange:[result rangeAtIndex:index]];
          if ([key isEqualToString:@"wildcards"]) {
            NSMutableArray *wildcards = [newParams objectForKey:key];
            if (firstWildcard) {
              // Create a new array and replace any existing object with the same key
              wildcards = [NSMutableArray array];
              [newParams setObject:wildcards forKey:key];
              firstWildcard = NO;
            }
            [wildcards addObject:capture];
          } else {
            [newParams setObject:capture forKey:key];
          }
          index++;
        }
        params = newParams;
      }
    } else if (captureCount > 1) {
      // For custom regular expressions place the anonymous captures in the captures parameter
      NSMutableDictionary *newParams = [params mutableCopy];
      NSMutableArray *captures = [NSMutableArray array];
      for (NSUInteger i = 1; i < captureCount; i++) {
        [captures addObject:[path substringWithRange:[result rangeAtIndex:i]]];
      }
      [newParams setObject:captures forKey:@"captures"];
      params = newParams;
    }
    
    RouteRequest *request = [[RouteRequest alloc] initWithHTTPMessage:httpMessage parameters:params];
    RouteResponse *response = [[RouteResponse alloc] initWithConnection:connection];
    if (!routeQueue) {
      [self handleRoute:route withRequest:request response:response];
    } else {
      // Process the route on the specified queue
      dispatch_sync(routeQueue, ^{
        @autoreleasepool {
          [self handleRoute:route withRequest:request response:response];
        }
      });
    }
    return response;
  }
  
  return nil;
}

- (void)setupMIMETypes {
  mimeTypes = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
               @"application/x-javascript",   @"js",
               @"image/gif",                  @"gif",
               @"image/jpeg",                 @"jpg",
               @"image/jpeg",                 @"jpeg",
               @"image/png",                  @"png",
               @"image/svg+xml",              @"svg",
               @"image/tiff",                 @"tif",
               @"image/tiff",                 @"tiff",
               @"image/x-icon",               @"ico",
               @"image/x-ms-bmp",             @"bmp",
               @"text/css",                   @"css",
               @"text/html",                  @"html",
               @"text/html",                  @"htm",
               @"text/plain",                 @"txt",
               @"text/xml",                   @"xml",
               nil];
}

@end
