/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#import <Foundation/Foundation.h>
#import "TSocketServer.h"
#import "TNSFileHandleTransport.h"
#import "TProtocol.h"
#import "TTransportError.h"

#import <sys/socket.h>
#include <netinet/in.h>
#include <sys/un.h>


NSString *const TSocketServerClientConnectionFinished = @"TSocketServerClientConnectionFinished";
NSString *const TSocketServerProcessorKey = @"TSocketServerProcessor";
NSString *const TSockerServerTransportKey = @"TSockerServerTransport";


@interface TSocketServer ()

@property(strong, nonatomic) id<TProtocolFactory> inputProtocolFactory;
@property(strong, nonatomic) id<TProtocolFactory> outputProtocolFactory;
@property(strong, nonatomic) id<TProcessorFactory> processorFactory;
@property(strong, nonatomic) NSFileHandle *socketFileHandle;
@property(strong, nonatomic) dispatch_queue_t processingQueue;
@property(strong, nonatomic) NSString *domainSocketPath;

@end


@implementation TSocketServer

-(instancetype) initWithSocket:(CFSocketRef)socket
             protocolFactory:(id <TProtocolFactory>)protocolFactory
            processorFactory:(id <TProcessorFactory>)processorFactory;
{
  self = [super init];

  _inputProtocolFactory = protocolFactory;
  _outputProtocolFactory = protocolFactory;
  _processorFactory = processorFactory;

  dispatch_queue_attr_t processingQueueAttr =
    dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_BACKGROUND, 0);

  _processingQueue = dispatch_queue_create("TSocketServer.processing", processingQueueAttr);

  // create a socket.
  int fd = CFSocketGetNative(socket);

  // wrap it in a file handle so we can get messages from it
  _socketFileHandle = [[NSFileHandle alloc] initWithFileDescriptor:fd
                                                    closeOnDealloc:YES];

  // throw away our socket
  CFSocketInvalidate(socket);
  CFRelease(socket);

  // register for notifications of accepted incoming connections
  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(connectionAccepted:)
                                               name:NSFileHandleConnectionAcceptedNotification
                                             object:_socketFileHandle];

  // tell socket to listen
  [_socketFileHandle acceptConnectionInBackgroundAndNotify];

  return self;
}

- (id) initWithPort: (int) port
    protocolFactory: (id <TProtocolFactory>) protocolFactory
   processorFactory: (id <TProcessorFactory>) processorFactory
{
  CFSocketRef socket = [[self class] createSocketWithPort:port];
  if (socket == NULL) {
    return nil;
  }

  if (self = [self initWithSocket:socket protocolFactory:protocolFactory processorFactory:processorFactory]) {
    NSLog(@"TSocketServer: Listening on TCP port %d", port);
  }
  return self;
}


+(CFSocketRef) createSocketWithPort:(int)port
{
  CFSocketRef socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, 0, NULL, NULL);
  if (socket) {
    CFSocketSetSocketFlags(socket, CFSocketGetSocketFlags(socket) & ~kCFSocketCloseOnInvalidate);
    int fd = CFSocketGetNative(socket);
    int yes = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void *)&yes, sizeof(yes));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_len = sizeof(addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    NSData *address = [NSData dataWithBytes:&addr length:sizeof(addr)];
    if (CFSocketSetAddress(socket, (__bridge CFDataRef)address) != kCFSocketSuccess) {
      CFSocketInvalidate(socket);
      CFRelease(socket);
      NSLog(@"TSocketServer: Could not bind to address");
      return NULL;
    }

    return socket;
  }
  else {
    NSLog(@"TSocketServer: No server socket");
    return NULL;
  }
}

- (id) initWithPath: (NSString *) path
    protocolFactory: (id <TProtocolFactory>) protocolFactory
   processorFactory: (id <TProcessorFactory>) processorFactory
{
  _domainSocketPath = path;
  CFSocketRef socket = [[self class] createSocketWithPath:path];
  if (socket == NULL) {
    return nil;
  }

  if (self = [self initWithSocket:socket protocolFactory:protocolFactory processorFactory:processorFactory]) {
    NSLog(@"TSocketServer: Listening on path %@", path);
  }
  return self;
}

+ (CFSocketRef) createSocketWithPath: (NSString *) path
{
  CFSocketRef socket = CFSocketCreate(kCFAllocatorDefault, PF_LOCAL, SOCK_STREAM, IPPROTO_IP, 0, NULL, NULL);
  if (socket) {
    CFSocketSetSocketFlags(socket,  CFSocketGetSocketFlags(socket) & ~kCFSocketCloseOnInvalidate);
    int fd = CFSocketGetNative(socket);
    int yes = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void *)&yes, sizeof(yes));

    size_t nullTerminatedPathLength = path.length + 1;
    struct sockaddr_un addr;
    if (nullTerminatedPathLength> sizeof(addr.sun_path)) {
      NSLog(@"TSocketServer: Unable to create socket at path %@. Path is too long.", path);
      return NULL;
    }

    addr.sun_family = AF_LOCAL;
    memcpy(addr.sun_path, path.UTF8String, nullTerminatedPathLength);
    addr.sun_len = SUN_LEN(&addr);

    NSData *address = [NSData dataWithBytes:&addr length:sizeof(addr)];
    if (CFSocketSetAddress(socket, (__bridge CFDataRef)address) != kCFSocketSuccess) {
      CFSocketInvalidate(socket);
      CFRelease(socket);
      NSLog(@"TSocketServer: Could not bind to address");
      return NULL;
    }

    return socket;
  } else {
    NSLog(@"TSocketServer: No server socket");
    return NULL;
  }
}

-(void) dealloc
{
  [[NSNotificationCenter defaultCenter] removeObserver:self];

  if (_domainSocketPath != nil) {
    unlink(_domainSocketPath.UTF8String);
  }
}


-(void) connectionAccepted:(NSNotification *)notification
{
  NSFileHandle *socket = [notification.userInfo objectForKey:NSFileHandleNotificationFileHandleItem];

  // Now that we have a client connected, handle request on queue
  dispatch_async(_processingQueue, ^{

    [self handleClientConnection:socket];

  });

  // Continue accepting connections
  [_socketFileHandle acceptConnectionInBackgroundAndNotify];
}


-(void) handleClientConnection:(NSFileHandle *)clientSocket
{
  @autoreleasepool {

    TNSFileHandleTransport *transport = [[TNSFileHandleTransport alloc] initWithFileHandle:clientSocket];
    id<TProcessor> processor = [_processorFactory processorForTransport:transport];

    id <TProtocol> inProtocol = [_inputProtocolFactory newProtocolOnTransport:transport];
    id <TProtocol> outProtocol = [_outputProtocolFactory newProtocolOnTransport:transport];

    NSError *error;
    if (![processor processOnInputProtocol:inProtocol outputProtocol:outProtocol error:&error]) {
      // Handle error
      NSLog(@"Error processing request: %@", error);
    }

    dispatch_async(dispatch_get_main_queue(), ^{

      [NSNotificationCenter.defaultCenter postNotificationName:TSocketServerClientConnectionFinished
                                                        object:self
                                                      userInfo:@{TSocketServerProcessorKey: processor,
                                                                 TSockerServerTransportKey: transport}];
    });

  }
}

@end
