//
//  BitSwarmClient.m
//  SFS2X
//
//  Original development by Infosfer Game Technologies Ltd. | http://www.infosfer.com.
//
//  Maintained and developed by A51 Integrated.
//  Copyright 2012 A51 Integrated | http://a51integrated.com. All rights reserved.
//

#import "BitSwarmClient.h"
#import "BitSwarmEvent.h"
#import "SmartFox2XClient.h"
#import "SFSException.h"
#import "Logger.h"
#import "IProtocolCodec.h"
#import "SystemController.h"
#import "ExtensionController.h"
#import "ClientDisconnectionReason.h"
#import "SendBuffer.h"
#import "ConnectionMode.h"
#import "BBClient.h"
#import "NSStream+QNetworkAdditions.h"

@implementation BitSwarmClient

@dynamic sfs;
@dynamic connected;
@dynamic ioHandler;
@dynamic compressionThreshold;
@dynamic reconnectionDelayMillis;
@dynamic sysController;
@dynamic extController;
@dynamic isReconnecting;
@dynamic connectionIp;
@dynamic connectionPort;
@dynamic reconnectionSeconds;
@dynamic connectionMode;
@dynamic useBlueBox;

@synthesize httpSocket = _bbClient;
@synthesize udpManager = _udpManager;
@synthesize pollSpeed = _pollSpeed;

@synthesize maxMessageSize;
@synthesize cryptoKey;

-(void)releaseSocketConnectionResources
{
    [_inStream close];
	[_inStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:runLoopMode];
	_inStream = nil;
	
	_lastSendBuffer = nil;
	
	[_outStream close];
	[_outStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:runLoopMode];
	_outStream = nil;
	
	_receiveBuffer = nil;
	_sendBuffers = nil;
	
	[_receiveLock tryLock];
	[_receiveLock unlock];
	[_sendLock tryLock];
	[_sendLock unlock];
	
	_receiveLock = nil;
	_sendLock = nil;
}


-(void)dealloc {

	[self releaseSocketConnectionResources];
    
}

-(id)initWithSmartFox:(SmartFox2XClient *)sfs {
	if ((self = [super init]))
    {
        _controllers = [NSMutableDictionary dictionary];
		_sfs = sfs;
		
		_delegate = sfs;
		
		_connected = NO;
		_log = [Logger getInstance];
		
		_compressionThreshold = 2000000;
		_reconnectionDelayMillis = 1000;
        
        _udpManager = [UDPManager alloc];
        
        maxMessageSize = 10000;
        runLoopMode = NSDefaultRunLoopMode;
        
	}
	return self;
}

-(void)addController:(NSInteger)id_ controller:(id <IController>)controller {
	
	if (controller == nil) {
		@throw [NSException exceptionWithName:@"ArgumentError" reason:@"Controller is null, it can't be added." userInfo:nil];
	}
	
	if ([_controllers objectForKey:[NSNumber numberWithInt:(int)id_]] != nil) {
		@throw [NSException exceptionWithName:@"ArgumentError" reason:[NSString stringWithFormat:@"A controller with id: %ld already exists! Controller can't be added", (long)id_] userInfo:nil];
	}
	
	[_controllers setObject:controller forKey:[NSNumber numberWithInt:(int)id_]];
}

-(void)initControllers {
	_sysController = [[SystemController alloc] initWithBitSwarm:self];
	_extController = [[ExtensionController alloc] initWithBitSwarmClient:self];
	
	[self addController:0 controller:_sysController];
	[self addController:1 controller:_extController];
}

-(SmartFox2XClient *)sfs {
	return _sfs;
}

-(BOOL)connected {
	return _connected;
}

-(BOOL)useBlueBox
{
	return _useBlueBox;
}

-(id <IoHandler>)ioHandler {
	return _ioHandler;
}

-(void)setIoHandler:(id <IoHandler>)value {
	
	if (_ioHandler != nil) {
		@throw [SFSException exceptionWithReason:@"IOHandler is already set!"];
	}
	
	_ioHandler = value;
}

-(NSInteger)compressionThreshold {
	return _compressionThreshold;
}

-(void)setCompressionThreshold:(NSInteger)value
{
	if (value > 100) {
		_compressionThreshold = value;
	}
	else {
		@throw [[NSException alloc] initWithName:@"ArgumentError" reason:@"Compression threshold cannot be < 100 bytes." userInfo:nil];
	}
}

-(NSInteger)reconnectionDelayMillis
{
	return _reconnectionDelayMillis;
}

-(void)setReconnectionDelayMillis:(NSInteger)value {
	_reconnectionDelayMillis = value;
}

-(void)initBitSwarm {
	if (!_controllersInited) {
		[self initControllers];
		_controllersInited = YES;
	}
}


-(void)destroy {
	if (_connected) {
		[self killConnection];
	}
}

-(SystemController *)sysController {
	return _sysController;
}

-(ExtensionController *)extController {
	return _extController;
}

-(BOOL)isReconnecting {
	return _attemptingReconnection;
}

-(void)setIsReconnecting:(BOOL)value {
	_attemptingReconnection = value;
}

-(id <IController>)getControllerById:(NSInteger)id_ {
	return [_controllers objectForKey:[NSNumber numberWithInt:(int)id_]];
}

-(NSString *)connectionIp {
	if (!_connected) {
		return @"Not Connected";
	}
	else {
		return _lastIpAddress;
	}
}

-(NSInteger)connectionPort {
	if (!_connected) {
		return -1;
	}
	else {
		return _lastTcpPort;
	}

}

-(NSString *)connectionMode
{
    return _connectionMode;
}


-(void)forceBlueBox:(BOOL)value
{
	if (!_connected)
    {
		_useBlueBox = value;
	}
	else
    {
		@throw [[NSException alloc] initWithName:@"IllegalOperationError" reason:@"You can't change the BlueBox mode while the connection is running" userInfo:nil];
	}
}


-(void)onSocketConnection
{
	_connected = YES;
	
	NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], @"success", [NSNumber numberWithBool:_attemptingReconnection], @"_isReconnection", nil];
	BitSwarmEvent *event = [[BitSwarmEvent alloc] initWithType:BitSwarmEvent_CONNECT params:params];
	
	[_delegate onSocketConnect:event];
}

-(void)onSocketIOError
{
	[self killConnection];
	
	NSDictionary* params = [NSDictionary dictionaryWithObjectsAndKeys:@"IOError", @"reason", nil];
	BitSwarmEvent* event = [[BitSwarmEvent alloc] initWithType:BitSwarmEvent_DISCONNECT params:params];
	
	[_delegate onSocketIOError:event];
}

/**
 This method should never get used
 Ported from AS3 because of the AS3 security behaviour, however
 Obj-C does not behave the same way


-(void) onSocketSecurityError:(BitSwarmEvent *)event
{
    // Reconnection failure
    if (_attemptingReconnection)
    {
        [self performSelector:@selector(reconnect:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:_lastIpAddress, @"ip", [NSNumber numberWithInt:(int)_lastTcpPort], @"port", nil]];
        return;
    }
    
	BitSwarmEvent *evt = [[[BitSwarmEvent alloc] initWithType:BitSwarmEvent_SECURITY_ERROR params:event.params] autorelease];
	
	[_delegate onSocketSecurityError:evt];
    
}
  */


-(void)onSocketClose:(BitSwarmEvent *)event
{
	
	BOOL isRegularDisconnection = !_attemptingReconnection && (_reconnectionSeconds == 0);
	BOOL isManualDisconnection = (event != nil) && [[event.params objectForKey:@"reason"] isEqual:ClientDisconnectionReason_MANUAL];
	
    if (isRegularDisconnection || isManualDisconnection)
    {
        [_udpManager reset];
        _firstReconnAttempt = [NSNumber numberWithInt: -1];
 
        if(_connected)
        {
            _connected = NO;
            [self executeDisconnection:event];
        }

		return;
	}
    else if(_attemptingReconnection)
    {
        _connected = NO;
        
        [self performSelector:@selector(reconnect:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:_lastIpAddress, @"ip", [NSNumber numberWithInt:(int)_lastTcpPort], @"port", nil]];
    }
    else
    {
        _connected = NO;
        _attemptingReconnection = YES;
        _firstReconnAttempt = [NSNumber numberWithDouble: [[NSDate date] timeIntervalSince1970]];
        _reconnCounter = 1;
        
        NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:ClientDisconnectionReason_UNKNOWN, @"reason", nil];
        BitSwarmEvent *evt = [[BitSwarmEvent alloc] initWithType:BitSwarmEvent_RECONNECTION_TRY params:params];
        [_delegate onSocketReconnectionTry:evt];
        
        
        [self performSelector:@selector(reconnect:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:_lastIpAddress, @"ip", [NSNumber numberWithInt:(int)_lastTcpPort], @"port", nil]];
    }
}

-(void)writeToSocket:(NSData *)data
{
	[_sendLock lock];
	
    if (_noDataSent == YES)
    {
		NSInteger len;
		_noDataSent = NO;
		len = [_outStream write:(const uint8_t *)[data bytes] maxLength:[data length]];
	
        if (_sfs.debug)
			[_log info:[NSString stringWithFormat:@"WRITE - Written directly to outStream len:%ld", (long)len]];
		
		if (len < [data length])
        {
			if (_sfs.debug)
            {
				[_log info:[NSString stringWithFormat:@"WRITE - Creating a new buffer for remaining data len:%ld", (long)[data length] - len]];
			}
            
			_lastSendBuffer = [SendBuffer dataWithNSData:[NSData dataWithBytes:(const uint8_t *)[data bytes] + len length:[data length] - len]];
			
            [_sendBuffers addObject:_lastSendBuffer];
		}
		
		[_sendLock unlock];
		return;
	}
	
	if (_lastSendBuffer)
    {
		NSInteger lastSendBufferLength;
		NSInteger newDataLength;
		
		lastSendBufferLength = [_lastSendBuffer length];
		newDataLength = [data length];
		
		if (lastSendBufferLength < 1024)
        {
			if (_sfs.debug)
				[_log info:@"WRITE - Have a buffer with enough space, appending data to it"];
			
			[_lastSendBuffer appendBytes:[data bytes] length:newDataLength];
			
			[_sendLock unlock];
            
			return;
		}
	}
    
	if (_sfs.debug)
		[_log info:@"WRITE - Creating a new buffer"];
	
	_lastSendBuffer = [SendBuffer dataWithNSData:data];
	[_sendBuffers addObject:_lastSendBuffer];
	
	[_sendLock unlock];
}

-(void)writeToUDPSocket:(NSData *)data
{
	[_sendLock lock];
    
	[_udpManager send:data]; 
	
	[_sendLock unlock];
}

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
	
	NSInteger len;
    
    switch(eventCode) {
		case NSStreamEventNone:
        {
			break;
		}
		case NSStreamEventOpenCompleted:
		{
			if (stream == _outStream) {
				[self onSocketConnection];
			}
			
			break;
        }
		case NSStreamEventHasSpaceAvailable:
		{
			[_sendLock lock];
            //NSLog(@"Inside OP_WRITE, buffers in queue: %lu", (unsigned long)_sendBuffers.count);
			
			if ([_sendBuffers count] == 0) {
				_noDataSent = YES;
                
                [_sendLock unlock];
				break;
			}
            
            /*
             * Consume all buffered data
             */
            [self consumeWriteBuffers];
            
            [_sendLock unlock];
			
			break;
        }
		case NSStreamEventErrorOccurred:
		{
            [self onSocketIOError];
			break;
        }
		case NSStreamEventEndEncountered:
		{
            [self onSocketClose:nil];
			break;
        }
		case NSStreamEventHasBytesAvailable:
        {
			uint8_t buf[1024];

			[_receiveLock lock];

			BOOL isErrorOccured = NO;

			while ([(NSInputStream *)stream hasBytesAvailable] == YES && isErrorOccured == NO)
            { 
				len = [(NSInputStream *)stream read:buf maxLength:1024];

				if (len == -1) {
					isErrorOccured = YES;
					continue;
				}

				@try {
					[_ioHandler onDataRead:[NSData dataWithBytes:buf length:len]];
				}
				@catch (SFSException *e) {
					[_log warn:@"Exception onDataRead!!!"];
				}
			}

			if (isErrorOccured == YES) {
				[self onSocketIOError];
                break;
			}
			
			[_receiveLock unlock];
            break;
        }
	}
}

/*
 
 Consumes all buffere data.
 If data is accumulated quickly there will be numerous buffers in the main buffer queue/list
 
 This aspect has been overlooked since version 1.6.1 -- It's now fixed via the method below,
 which takes care of checking that all buffers get a chance to be written to the socket.
 
*/

-(void) consumeWriteBuffers
{
    while (_sendBuffers.count > 0)
    {
        
        SendBuffer *sendBuffer = [_sendBuffers objectAtIndex:0];
        
        NSInteger sendBufferLength = [sendBuffer length];
        
        if (sendBufferLength == 0)
        {
            if (sendBuffer == _lastSendBuffer)
                _lastSendBuffer = nil;
            
            [_sendBuffers removeObjectAtIndex:0];
            
            _noDataSent = YES;
            
            continue;
        }
        
        // Calculate lenght of next chunk of data to send, with a limit of 1024
        long len = ((sendBufferLength - [sendBuffer sendPos] >= 1024) ? 1024 : (sendBufferLength - [sendBuffer sendPos]));
        
        if (len == 0)
        {
            if (sendBuffer == _lastSendBuffer)
                _lastSendBuffer = nil;
            
            [_sendBuffers removeObjectAtIndex:0];
            
            _noDataSent = YES;
            
            continue;
        }
        
        
        // ---> WRITE to Socket
        len = [_outStream write:((const uint8_t *)[sendBuffer mutableBytes] + [sendBuffer sendPos]) maxLength:len];
        
        if (len == -1)
        {
            [self onSocketIOError];
            continue;
        }
        
        if (_sfs.debug) {
            [_log info:[NSString stringWithFormat:@"WRITE - Written directly to outStream len:%ld", (long)len]];
        }
        
        [sendBuffer consumeData:len];
        
        if ([sendBuffer length] == 0)
        {
            if (sendBuffer == _lastSendBuffer)
                _lastSendBuffer = nil;
            
            [_sendBuffers removeObjectAtIndex:0];
        }
        
        _noDataSent = NO;
        
    }
}

-(void) stopReconnection
{
    _attemptingReconnection = NO;
    _firstReconnAttempt = [NSNumber numberWithDouble: -1 ];

    if (_connected)
        [self releaseSocketConnectionResources];
    
    [self executeDisconnection:nil];
}

-(UDPManager *)udpManager
{
    return _udpManager;
}


-(NSNumber *)nextUdpPacketId
{
    return [_udpManager nextUdpPacketId];
}


-(int)mySelfId
{
    return (int)[[_sfs mySelf] id];  
}

-(void)enableBBoxDebug:(BOOL)value
{
    _bbClient.isDebug = value;
}



-(void) executeDisconnection:(BitSwarmEvent *)evt
{
    /*
     * A BitSwarmEvent is passed if the disconnection was requested by the server
     * The event includes a reason for the disconnection (idle, kick, ban...)
     */
    if ([evt isMemberOfClass:[BitSwarmEvent class]])
    {
        [_delegate onSocketClose:evt];
    }
    else
    {
        NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:ClientDisconnectionReason_UNKNOWN, @"reason", nil];
        BitSwarmEvent * event = [[BitSwarmEvent alloc] initWithType:BitSwarmEvent_DISCONNECT params:params];
        
        [_delegate onSocketClose:event];
    }
    
    
}



-(void)reconnect:(NSDictionary *)params
{
    if (!_attemptingReconnection)
    {
        return;
    }

    NSInteger reconnectionSeconds = [_sfs getReconnectionSeconds];
    NSNumber *now =[NSNumber numberWithDouble: [[NSDate date] timeIntervalSince1970]];

    NSInteger timeLeft = ([_firstReconnAttempt integerValue] + reconnectionSeconds - [now integerValue]);

    if (timeLeft > 0)
    {

        [_log info:[NSString stringWithFormat:@"Reconnection attempt: %ld sec.",(long) timeLeft]];
        // Retry connection: pause 1 second and retry        
        [self performSelector:@selector(connect:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:_lastIpAddress, @"ip", [NSNumber numberWithInt:(int)_lastTcpPort], @"port", nil] afterDelay:_reconnectionDelayMillis / 1000.0];
    
		_reconnCounter++;
    }

    // We are out of time... connection failed :(
    else
    {
        [_log info:@"Reconnection attempts failed."];
        
        [self releaseSocketConnectionResources];
        
        BitSwarmEvent *evt = [[BitSwarmEvent alloc] initWithType:BitSwarmEvent_DISCONNECT params:[NSDictionary dictionaryWithObject:ClientDisconnectionReason_UNKNOWN forKey:@"reason"]];
		
		[_delegate onSocketClose:evt];
    }
    
}

-(void)connect:(NSString *)ip port:(int)port
{
	
	_lastIpAddress = ip;
	_lastTcpPort = port;
	
	_receiveLock = [[NSLock alloc] init];	
	_sendLock = [[NSLock alloc] init];
	
	_noDataSent = NO;
	_lastSendBuffer = nil;
	
	_receiveBuffer = [NSMutableData data];
	_sendBuffers = [NSMutableArray array];

    if (_useBlueBox)
    {
        _bbClient = [[BBClient alloc] initWithBitSwarm:self debug:YES];
        
        [_bbClient pollSpeed: _pollSpeed];
        
        _connectionMode = ConnectionMode_HTTP;
        
        [_bbClient connect:_lastIpAddress usingPort:_lastTcpPort];
    }
    else
    {
        NSInputStream *inputStream = _inStream;
        NSOutputStream *outputStream = _outStream;
        
        /*
         * New since October 2018, release 1.7.6
         * Due to NSStream.getStreamsToHostNamed being deprecated and removed in XCode 10
         * 
         * We use a new category added to NSStream to simulate a similar behavior
         * See here --> https://stackoverflow.com/questions/13534091/how-to-configure-cfstream-or-nsstream-for-ssl-handshake
         */
        
        [NSStream qNetworkAdditions_getStreamsToHostNamed:_lastIpAddress
                                                     port:_lastTcpPort
                                              inputStream:&inputStream
                                             outputStream:&outputStream];
        
        _inStream =  inputStream;
        _outStream = outputStream;
        
        
        [_inStream setDelegate:(id<NSStreamDelegate>)self];
        [_outStream setDelegate:(id<NSStreamDelegate>)self];
        
        /* This was until version 1.7.2
        [_inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [_outStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        */
        
        // New in version 1.7.3 // July 17th 2017
        [_inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:runLoopMode];
        [_outStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:runLoopMode];
        
        [_inStream open];
        [_outStream open];
        
        _connectionMode = ConnectionMode_SOCKET;
    }
}

-(void)connect:(NSDictionary *)params
{
	NSString *ip = [params objectForKey:@"ip"];
	int port = [(NSNumber *)[params objectForKey:@"port"] intValue];
	[self connect:ip port:port];
}

-(void)send:(id <IMessage>)message {
	[[_ioHandler codec] onPacketWrite:message];
}

-(id <IController>)getController:(NSInteger)id_ {
	return [_controllers objectForKey:[NSNumber numberWithInt:(int)id_]];
}

-(void)disconnect:(NSString *)reason
{
    if (_useBlueBox)
    {
        [_bbClient close];
        
        _useBlueBox = NO;
    }
    
    [self releaseSocketConnectionResources];
    
    BitSwarmEvent *evt = [[BitSwarmEvent alloc] initWithType:BitSwarmEvent_DISCONNECT params:[NSDictionary dictionaryWithObject:reason forKey:@"reason"]];
    
    [self onSocketClose:evt];
	
}



-(void)killConnection
{    
	[self releaseSocketConnectionResources];
	[self onSocketClose:nil];
}

-(NSInteger)reconnectionSeconds {
	return _reconnectionSeconds;
}

-(void)setReconnectionSeconds:(NSInteger)value {
	if (value < 0) {
		_reconnectionSeconds = 0;
	}
	else {
		_reconnectionSeconds = value;
	}

}

-(NSRunLoopMode) getRunLoopMode
{
    return runLoopMode;
}

-(void) setRunLoopMode:(NSRunLoopMode)mode
{
    runLoopMode = mode;
}


//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// BlueBox handlers
//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

-(void)onBBConnect:(BBEvent *)evt
{
    _connected = YES;

	BitSwarmEvent *event = [[BitSwarmEvent alloc] initWithType:BitSwarmEvent_CONNECT params:evt.params];
	[_delegate onSocketConnect:event];
    
}

-(void)onBBData:(BBEvent *)evt
{
    
    NSData * buffer = [evt.params objectForKey:@"data"];
    
    if(![[[NSString alloc] initWithData:buffer encoding:NSUTF8StringEncoding] isEqualToString:@"null"])
    {
        [_ioHandler onDataRead:buffer];
    }
}

-(void)onBBDisconnect:(BBEvent *)evt
{
    // Connection is off
    _connected = NO;
    
	BitSwarmEvent *event = [[BitSwarmEvent alloc] initWithType:BitSwarmEvent_DISCONNECT params:[NSDictionary dictionaryWithObject:ClientDisconnectionReason_UNKNOWN forKey:@"reason"]];
	[_delegate onSocketConnect:event];
}

-(void)onBBError:(BBEvent *)evt
{
    
    NSString * err = [evt.params objectForKey:@"message"];
    
	BitSwarmEvent *event = [[BitSwarmEvent alloc] initWithType:BitSwarmEvent_IO_ERROR params:[NSDictionary dictionaryWithObject:err forKey:@"message"]];
	[_delegate onSocketIOError:event];
    
}

@end
