#import "HTTPServer.h"
#import "HTTPConnection.h"
#import "HTTPLogging.h"

#import "GCDAsyncSocket.h"

#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif

#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
#pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion"
#pragma clang diagnostic ignored "-Wunused"

// Log levels: off, error, warn, info, verbose
// Other flags: trace
static const int httpLogLevel = HTTP_LOG_LEVEL_INFO; // | HTTP_LOG_FLAG_TRACE;

@interface HTTPServer (PrivateAPI)

- (void)unpublishBonjour;
- (void)publishBonjour;

+ (void)startBonjourThreadIfNeeded;
+ (void)performBonjourBlock:(dispatch_block_t)block;

@end

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@implementation HTTPServer

/**
 * Standard Constructor.
 * Instantiates an HTTP server, but does not start it.
 **/
- (id)init
{
  if ((self = [super init]))
  {
    HTTPLogTrace();
    
    // Setup underlying dispatch queues
    serverQueue = dispatch_queue_create("HTTPServer", NULL);
    connectionQueue = dispatch_queue_create("HTTPConnection", NULL);
    
    IsOnServerQueueKey = &IsOnServerQueueKey;
    IsOnConnectionQueueKey = &IsOnConnectionQueueKey;
    
    void *nonNullUnusedPointer = (__bridge void *)self; // Whatever, just not null
    
    dispatch_queue_set_specific(serverQueue, IsOnServerQueueKey, nonNullUnusedPointer, NULL);
    dispatch_queue_set_specific(connectionQueue, IsOnConnectionQueueKey, nonNullUnusedPointer, NULL);
    
    // Initialize underlying GCD based tcp socket
    asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:(id<GCDAsyncSocketDelegate>)self delegateQueue:serverQueue];

    // Use default connection class of HTTPConnection
    connectionClass = [HTTPConnection self];
    
    // By default bind on all available interfaces, en1, wifi etc
    interface = nil;
    
    // Use a default port of 0
    // This will allow the kernel to automatically pick an open port for us
    port = 0;
    
    // Configure default values for bonjour service
    
    // Bonjour domain. Use the local domain by default
    domain = @"local.";
    
    // If using an empty string ("") for the service name when registering,
    // the system will automatically use the "Computer Name".
    // Passing in an empty string will also handle name conflicts
    // by automatically appending a digit to the end of the name.
    name = @"";
    
    // Initialize arrays to hold all the HTTP connections
    connections = [[NSMutableArray alloc] init];
    
    connectionsLock = [[NSLock alloc] init];
    
    // Register for notifications of closed connections
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(connectionDidDie:)
                                                 name:HTTPConnectionDidDieNotification
                                               object:nil];
    
    isRunning = NO;
  }
  return self;
}

/**
 * Standard Deconstructor.
 * Stops the server, and clients, and releases any resources connected with this instance.
 **/
- (void)dealloc
{
  HTTPLogTrace();
  
  // Remove notification observer
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  
  // Stop the server if it's running
  [self stop];
  
  // Release all instance variables
  
#if !OS_OBJECT_USE_OBJC
  dispatch_release(serverQueue);
  dispatch_release(connectionQueue);
#endif
  
  [asyncSocket setDelegate:nil delegateQueue:NULL];
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Server Configuration
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * The document root is filesystem root for the webserver.
 * Thus requests for /index.html will be referencing the index.html file within the document root directory.
 * All file requests are relative to this document root.
 **/
- (NSString *)documentRoot
{
  __block NSString *result;
  
  dispatch_sync(serverQueue, ^{
    result = documentRoot;
  });
  
  return result;
}

- (void)setDocumentRoot:(NSString *)value
{
  HTTPLogTrace();
  
  // Document root used to be of type NSURL.
  // Add type checking for early warning to developers upgrading from older versions.
  
  if (value && ![value isKindOfClass:[NSString class]])
  {
    HTTPLogWarn(@"%@: %@ - Expecting NSString parameter, received %@ parameter",
                THIS_FILE, THIS_METHOD, NSStringFromClass([value class]));
    return;
  }
  
  NSString *valueCopy = [value copy];
  
  dispatch_async(serverQueue, ^{
    documentRoot = valueCopy;
  });
  
}

/**
 * The connection class is the class that will be used to handle connections.
 * That is, when a new connection is created, an instance of this class will be intialized.
 * The default connection class is HTTPConnection.
 * If you use a different connection class, it is assumed that the class extends HTTPConnection
 **/
- (Class)connectionClass
{
  __block Class result;
  
  dispatch_sync(serverQueue, ^{
    result = connectionClass;
  });
  
  return result;
}

- (void)setConnectionClass:(Class)value
{
  HTTPLogTrace();
  
  dispatch_async(serverQueue, ^{
    connectionClass = value;
  });
}

/**
 * What interface to bind the listening socket to.
 **/
- (NSString *)interface
{
  __block NSString *result;
  
  dispatch_sync(serverQueue, ^{
    result = interface;
  });
  
  return result;
}

- (void)setInterface:(NSString *)value
{
  NSString *valueCopy = [value copy];
  
  dispatch_async(serverQueue, ^{
    interface = valueCopy;
  });
  
}

/**
 * The port to listen for connections on.
 * By default this port is initially set to zero, which allows the kernel to pick an available port for us.
 * After the HTTP server has started, the port being used may be obtained by this method.
 **/
- (UInt16)port
{
  __block UInt16 result;
  
  dispatch_sync(serverQueue, ^{
    result = port;
  });
  
  return result;
}

- (UInt16)listeningPort
{
  __block UInt16 result;
  
  dispatch_sync(serverQueue, ^{
    if (isRunning)
      result = [asyncSocket localPort];
    else
      result = 0;
  });
  
  return result;
}

- (void)setPort:(UInt16)value
{
  HTTPLogTrace();
  
  dispatch_async(serverQueue, ^{
    port = value;
  });
}

/**
 * Domain on which to broadcast this service via Bonjour.
 * The default domain is @"local".
 **/
- (NSString *)domain
{
  __block NSString *result;
  
  dispatch_sync(serverQueue, ^{
    result = domain;
  });
  
  return result;
}

- (void)setDomain:(NSString *)value
{
  HTTPLogTrace();
  
  NSString *valueCopy = [value copy];
  
  dispatch_async(serverQueue, ^{
    domain = valueCopy;
  });
  
}

/**
 * The name to use for this service via Bonjour.
 * The default name is an empty string,
 * which should result in the published name being the host name of the computer.
 **/
- (NSString *)name
{
  __block NSString *result;
  
  dispatch_sync(serverQueue, ^{
    result = name;
  });
  
  return result;
}

- (NSString *)publishedName
{
  __block NSString *result;
  
  dispatch_sync(serverQueue, ^{
    
    if (netService == nil)
    {
      result = nil;
    }
    else
    {
      
      dispatch_block_t bonjourBlock = ^{
        result = [[netService name] copy];
      };
      
      [[self class] performBonjourBlock:bonjourBlock];
    }
  });
  
  return result;
}

- (void)setName:(NSString *)value
{
  NSString *valueCopy = [value copy];
  
  dispatch_async(serverQueue, ^{
    name = valueCopy;
  });
  
}

/**
 * The type of service to publish via Bonjour.
 * No type is set by default, and one must be set in order for the service to be published.
 **/
- (NSString *)type
{
  __block NSString *result;
  
  dispatch_sync(serverQueue, ^{
    result = type;
  });
  
  return result;
}

- (void)setType:(NSString *)value
{
  NSString *valueCopy = [value copy];
  
  dispatch_async(serverQueue, ^{
    type = valueCopy;
  });
  
}

/**
 * The extra data to use for this service via Bonjour.
 **/
- (NSDictionary *)TXTRecordDictionary
{
  __block NSDictionary *result;
  
  dispatch_sync(serverQueue, ^{
    result = txtRecordDictionary;
  });
  
  return result;
}

- (void)setTXTRecordDictionary:(NSDictionary *)value
{
  HTTPLogTrace();
  
  NSDictionary *valueCopy = [value copy];
  
  dispatch_async(serverQueue, ^{
    
    txtRecordDictionary = valueCopy;
    
    // Update the txtRecord of the netService if it has already been published
    if (netService)
    {
      NSNetService *theNetService = netService;
      NSData *txtRecordData = nil;
      if (txtRecordDictionary)
        txtRecordData = [NSNetService dataFromTXTRecordDictionary:txtRecordDictionary];
      
      dispatch_block_t bonjourBlock = ^{
        [theNetService setTXTRecordData:txtRecordData];
      };
      
      [[self class] performBonjourBlock:bonjourBlock];
    }
  });
  
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Server Control
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (BOOL)start:(NSError **)errPtr
{
  HTTPLogTrace();
  
  __block BOOL success = YES;
  __block NSError *err = nil;
  
  dispatch_sync(serverQueue, ^{ @autoreleasepool {
    
    success = [asyncSocket acceptOnInterface:interface port:port error:&err];
    if (success)
    {
      HTTPLogInfo(@"%@: Started HTTP server on port %hu", THIS_FILE, [asyncSocket localPort]);
      
      isRunning = YES;
      [self publishBonjour];
    }
    else
    {
      HTTPLogError(@"%@: Failed to start HTTP Server: %@", THIS_FILE, err);
    }
  }});
  
  if (errPtr)
    *errPtr = err;
  
  return success;
}

- (void)stop
{
  [self stop:NO];
}

- (void)stop:(BOOL)keepExistingConnections
{
  HTTPLogTrace();
  
  dispatch_sync(serverQueue, ^{ @autoreleasepool {
    
    // First stop publishing the service via bonjour
    [self unpublishBonjour];
    
    // Stop listening / accepting incoming connections
    [asyncSocket disconnect];
    isRunning = NO;
    
    if (!keepExistingConnections)
    {
      // Stop all HTTP connections the server owns
      [connectionsLock lock];
      for (HTTPConnection *connection in connections)
      {
        [connection stop];
      }
      [connections removeAllObjects];
      [connectionsLock unlock];
    }
  }});
}

- (BOOL)isRunning
{
  __block BOOL result;
  
  dispatch_sync(serverQueue, ^{
    result = isRunning;
  });
  
  return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Server Status
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Returns the number of http client connections that are currently connected to the server.
 **/
- (NSUInteger)numberOfHTTPConnections
{
  NSUInteger result = 0;
  
  [connectionsLock lock];
  result = [connections count];
  [connectionsLock unlock];
  
  return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Incoming Connections
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (HTTPConfig *)config
{
  // Override me if you want to provide a custom config to the new connection.
  // 
  // Generally this involves overriding the HTTPConfig class to include any custom settings,
  // and then having this method return an instance of 'MyHTTPConfig'.
  
  // Note: Think you can make the server faster by putting each connection on its own queue?
  // Then benchmark it before and after and discover for yourself the shocking truth!
  // 
  // Try the apache benchmark tool (already installed on your Mac):
  // $  ab -n 1000 -c 1 http://localhost:<port>/some_path.html
  
  return [[HTTPConfig alloc] initWithServer:self documentRoot:documentRoot queue:connectionQueue];
}

- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
  HTTPConnection *newConnection = (HTTPConnection *)[[connectionClass alloc] initWithAsyncSocket:newSocket
                                                                                   configuration:[self config]];
  [connectionsLock lock];
  [connections addObject:newConnection];
  [connectionsLock unlock];
  
  [newConnection start];
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Bonjour
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)publishBonjour
{
  HTTPLogTrace();
  
  NSAssert(dispatch_get_specific(IsOnServerQueueKey) != NULL, @"Must be on serverQueue");
  
  if (type)
  {
    netService = [[NSNetService alloc] initWithDomain:domain type:type name:name port:[asyncSocket localPort]];
    [netService setDelegate:self];
    
    NSNetService *theNetService = netService;
    NSData *txtRecordData = nil;
    if (txtRecordDictionary)
      txtRecordData = [NSNetService dataFromTXTRecordDictionary:txtRecordDictionary];
    
    dispatch_block_t bonjourBlock = ^{
      
      [theNetService removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
      [theNetService scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
      [theNetService publish];
      
      // Do not set the txtRecordDictionary prior to publishing!!!
      // This will cause the OS to crash!!!
      if (txtRecordData)
      {
        [theNetService setTXTRecordData:txtRecordData];
      }
    };
    
    [[self class] startBonjourThreadIfNeeded];
    [[self class] performBonjourBlock:bonjourBlock];
  }
}

- (void)unpublishBonjour
{
  HTTPLogTrace();
  
  NSAssert(dispatch_get_specific(IsOnServerQueueKey) != NULL, @"Must be on serverQueue");
  
  if (netService)
  {
    NSNetService *theNetService = netService;
    
    dispatch_block_t bonjourBlock = ^{
      
      [theNetService stop];
    };
    
    [[self class] performBonjourBlock:bonjourBlock];
    
    netService = nil;
  }
}

/**
 * Republishes the service via bonjour if the server is running.
 * If the service was not previously published, this method will publish it (if the server is running).
 **/
- (void)republishBonjour
{
  HTTPLogTrace();
  
  dispatch_async(serverQueue, ^{
    
    [self unpublishBonjour];
    [self publishBonjour];
  });
}

/**
 * Called when our bonjour service has been successfully published.
 * This method does nothing but output a log message telling us about the published service.
 **/
- (void)netServiceDidPublish:(NSNetService *)ns
{
  // Override me to do something here...
  // 
  // Note: This method is invoked on our bonjour thread.
  
  HTTPLogInfo(@"Bonjour Service Published: domain(%@) type(%@) name(%@)", [ns domain], [ns type], [ns name]);
}

/**
 * Called if our bonjour service failed to publish itself.
 * This method does nothing but output a log message telling us about the published service.
 **/
- (void)netService:(NSNetService *)ns didNotPublish:(NSDictionary *)errorDict
{
  // Override me to do something here...
  // 
  // Note: This method in invoked on our bonjour thread.
  
  HTTPLogWarn(@"Failed to Publish Service: domain(%@) type(%@) name(%@) - %@",
              [ns domain], [ns type], [ns name], errorDict);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Notifications
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * This method is automatically called when a notification of type HTTPConnectionDidDieNotification is posted.
 * It allows us to remove the connection from our array.
 **/
- (void)connectionDidDie:(NSNotification *)notification
{
  // Note: This method is called on the connection queue that posted the notification
  
  [connectionsLock lock];
  
  HTTPLogTrace();
  [connections removeObject:[notification object]];
  
  [connectionsLock unlock];
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Bonjour Thread
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * NSNetService is runloop based, so it requires a thread with a runloop.
 * This gives us two options:
 * 
 * - Use the main thread
 * - Setup our own dedicated thread
 * 
 * Since we have various blocks of code that need to synchronously access the netservice objects,
 * using the main thread becomes troublesome and a potential for deadlock.
 **/

static NSThread *bonjourThread;

+ (void)startBonjourThreadIfNeeded
{
  HTTPLogTrace();
  
  static dispatch_once_t predicate;
  dispatch_once(&predicate, ^{
    
    HTTPLogVerbose(@"%@: Starting bonjour thread...", THIS_FILE);
    
    bonjourThread = [[NSThread alloc] initWithTarget:self
                                            selector:@selector(bonjourThread)
                                              object:nil];
    [bonjourThread start];
  });
}

+ (void)bonjourThread
{
  @autoreleasepool {
    
    HTTPLogVerbose(@"%@: BonjourThread: Started", THIS_FILE);
    
    // We can't run the run loop unless it has an associated input source or a timer.
    // So we'll just create a timer that will never fire - unless the server runs for 10,000 years.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow]
                                     target:self
                                   selector:@selector(donothingatall:)
                                   userInfo:nil
                                    repeats:YES];
#pragma clang diagnostic pop
    
    [[NSRunLoop currentRunLoop] run];
    
    HTTPLogVerbose(@"%@: BonjourThread: Aborted", THIS_FILE);
    
  }
}

+ (void)executeBonjourBlock:(dispatch_block_t)block
{
  HTTPLogTrace();
  
  NSAssert([NSThread currentThread] == bonjourThread, @"Executed on incorrect thread");
  
  block();
}

+ (void)performBonjourBlock:(dispatch_block_t)block
{
  HTTPLogTrace();
  
  [self performSelector:@selector(executeBonjourBlock:)
               onThread:bonjourThread
             withObject:block
          waitUntilDone:YES];
}

@end
