//
//  SmartFox2XClient.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 "SmartFox2XClient.h"
#import "SFSGlobalUserManager2.h"
#import "SFSRoomManager.h"
#import "SFSBuddyManager.h"
#import "BitSwarmClient.h"
#import "BitSwarmEvent.h"
#import "SFSIOHandler.h"
#import "ConfigLoader.h"
#import "ConfigData.h"
#import "HandshakeRequest.h"
#import "ManualDisconnectionRequest.h"
#import "ClientDisconnectionReason.h"
#import "SFSErrorCodes.h"
#import "ConnectionMode.h"
#import "SFSCryptoInitializer.h"

@interface SmartFox2XClient()

-(void)verifyConfigData:(ConfigData *)cfg;

@end


@implementation SmartFox2XClient

@synthesize mySelf          = _mySelf;
@synthesize buddyManager    = _buddyManager;
@synthesize userManager     = _userManager;
@synthesize roomManager     = _roomManager;
@synthesize lastJoinedRoom  = _lastJoinedRoom;
@synthesize config          = _config;
@synthesize sessionToken    = _sessionToken;
@synthesize debug           = _debug;
@synthesize bitSwarm        = _bitSwarm;
@synthesize currentZone     = _currentZone;
@synthesize isJoining       = _isJoining;
@synthesize delegate        = _delegate;
@synthesize logger          = _log;
@synthesize useBlueBox      = _useBlueBox;
@synthesize lagMonitor      = _lagMonitor;

@dynamic joinedRooms;
@dynamic isConnected;
@dynamic connectionMode;
@dynamic compressionThreshold;
@dynamic currentIp;
@dynamic currentPort;
@dynamic roomList;
@dynamic version;

int const DEFAULT_HTTP_PORT = 8080;
int const DEFAULT_HTTPS_PORT = 8443;

NSString *const CLIENT_TYPE_SEPARATOR = @":";


-(void)reset
{	
	self.userManager = [[SFSGlobalUserManager2 alloc] initWithSfs:self];
	self.roomManager = [[SFSRoomManager alloc] initWithSfs:self];
	self.buddyManager = [[SFSBuddyManager alloc] initWithSfs:self];
	
    //self.useBlueBox = NO;
	_isJoining = NO;
    
    // Remove previous lag monitor, if any
    if (_lagMonitor)
        [_lagMonitor destroy];
	
	self.currentZone = nil;
	self.lastJoinedRoom = nil;
	self.sessionToken = nil;
	self.mySelf = nil;
    
    _clientDetails = nil;
}

-(void)initialize
{	
	if (_inited) {
		return;
	}
	
	self.bitSwarm = [[BitSwarmClient alloc] initWithSmartFox:self];
	_bitSwarm.ioHandler = [[SFSIOHandler alloc] initWithBitSwarmClient:_bitSwarm];
	[_bitSwarm initBitSwarm];
	
	_inited = YES;
    
	if (_debug)
    {
		[_log info:[NSString stringWithFormat:@"SFS2X API version: %@", [self version]]];
	}

	[self reset];
}

-(id)initSmartFoxWithDebugMode:(BOOL)debug delegate:(id <ISFSEvents>)delegate
{
	if ((self = [super init]))
    {
        // Release version number
        
        _majVersion = 1;
		_minVersion = 7;
		_subVersion = 16;
		
		_delegate = delegate;
		
		_log = [Logger getInstance];
		_log.enableEventDispatching = YES;
		_log.delegate = delegate;
		_debug = debug;
		
		[self initialize];
	}
	return self;
}

+(id)smartFoxWithDebugMode:(BOOL)debug delegate:(id <ISFSEvents>)delegate
{
	SmartFox2XClient *client = [[SmartFox2XClient alloc] initSmartFoxWithDebugMode:debug delegate:delegate];
	return client;
}

-(NSMutableArray *)joinedRooms
{
	return [_roomManager getJoinedRooms];
}


-(void)send:(id <IRequest>)request
{	
	if (!self.isConnected)
    {
		[_log warn:[NSString stringWithFormat:@"Not connected: can't send any request: %@", request]];
        return;
	}
	
	@try
    {
		if ([request isKindOfClass:[JoinRoomRequest class]])
        {
			if (_isJoining)
            {
				return;
			}
			else
            {
				_isJoining = YES;
			}
		}
		
		[request validate:self];
		[request execute:self];
		
		[_bitSwarm send:[request getMessage]];
	}
	@catch (SFSValidationException * e)
    {
		NSMutableString *err = [NSMutableString string];
		for (NSString *item in e.errors)
        {
			[err appendFormat:@"\t%@\n", item];
		}
		
		[_log warn:err];
	}
}


-(NSInteger)compressionThreshold
{
	return _bitSwarm.compressionThreshold;
}

-(void)killConnection
{
	[_bitSwarm killConnection];
}


-(void) enableLagMonitor:(BOOL)enabled;
{
    [self enableLagMonitorWithInterval:enabled interval:4 queueSize:10];
}

-(void) enableLagMonitorWithInterval:(BOOL)enabled interval:(NSInteger)interval queueSize:(NSInteger)qSize;
{
    if (!_mySelf)
    {
        [_log warn:@"Lag Monitoring requires that you are logged in a Zone!"];
        return;
    }
    
    if (enabled)
    {
        _lagMonitor = [LagMonitor initWithInterval:interval qSize:qSize smartfox:self];
        [_lagMonitor start];
    }
    else
        [_lagMonitor stop];
}

-(NSString*) httpUploadURI
{
    if (_config == nil || _mySelf == nil)
        return nil;
    
    return [NSString stringWithFormat:@"http://%@:%ld/BlueBox/SFS2XFileUpload?sessHashId=%@", _config.host, (long)_config.httpPort, _sessionToken];
}

-(NSString *)currentIp
{
	return _bitSwarm.connectionIp;
}

-(NSInteger)currentPort
{
	return _bitSwarm.connectionPort;
}

-(void)sendHandshakeRequest:(BOOL)isReconnection
{
	if (_debug)
    {
		[_log info:[NSString stringWithFormat:@"sending handshake request: isReconnection : %@", (isReconnection ? @"YES" : @"NO")]];
	}
	
	id <IRequest> req = [[HandshakeRequest alloc] initWithApiVersion:[self version] reconnectionToken:(isReconnection) ? _sessionToken : nil clientDetails: _clientDetails];
	
	[self send:req];
}

-(void)handleConnectionProblem:(BitSwarmEvent *)evt
{
    // Socket failed, attempt using the BBox
    
    [_log warn:@"Cannot initialize Socket connection to SFS2X."];
    
    if (_bitSwarm.connectionMode == ConnectionMode_SOCKET && _useBlueBox)
    {
        [_bitSwarm forceBlueBox:YES];
        _bitSwarm.pollSpeed = _config.blueBoxPollingRate;
        
        int bbPort = (_config != nil) ? (int) _config.httpsPort : DEFAULT_HTTPS_PORT;
        
        [_bitSwarm connect:_lastIpAddress port:bbPort];
        
        if ([_delegate respondsToSelector:@selector(onConectionAttempHTTP:)])
        {
            [_delegate onConectionAttempHTTP:[[SFSEvent alloc] initWithType:SFSEvent_CONNECTION_ATTEMPT_HTTP params:[NSDictionary dictionary]]];
        }
    }
    
    // Connection failed
    else
    {        
        NSMutableDictionary *params = [NSMutableDictionary dictionary];
        [params setValue:[NSNumber numberWithBool:NO] forKey:@"success"];
        [params setValue:[evt.params objectForKey:@"message"] forKey:@"errorMessage"];
        
        if ([_delegate respondsToSelector:@selector(onConnection:)])
        {
            [_delegate onConnection:[[SFSEvent alloc] initWithType:SFSEvent_CONNECTION params:params]];
        }
        
        _isConnecting = NO;
    }
}

-(void)initUDP:(NSString *)udpHost :(NSInteger)udpPort
{
    if (!self.isConnected)
    {
        [_log info:@"Cannot initialize UDP protocol until the client is connected to SFS2X."];
        return;
    }
    
    // Attempt to use external config (if exist) for missing params
    if (_config != nil)
    {
        if (udpHost == nil) { udpHost = _config.udpHost; }
        if (udpPort == -1) { udpPort = _config.udpPort; }
    }
    
    if ([udpHost length] == 0)
    {
        @throw [NSException exceptionWithName:@"ArgumentError" reason:@"Invalid Host/Ip Address supplied" userInfo:nil];
    }
    
    if (udpPort < 1 || udpPort > 65535)
    {
        @throw [NSException exceptionWithName:@"ArgumentError" reason:@"Invalid UDP port supplied" userInfo:nil];
    }
    
    (void)[[_bitSwarm udpManager] initWithBitSwarm:_bitSwarm host:udpHost port:udpPort];
}

-(void)initCrypto
{
    SFSCryptoInitializer* init = [[SFSCryptoInitializer alloc] initWithSFS:self];
    [init execute];
}

-(BitSwarmClient *)getSocketEngine
{
	return _bitSwarm;
}


-(BOOL)isConnected
{
	BOOL value = NO;
	
	if (_bitSwarm != nil)
		value = _bitSwarm.connected;
	
	return value;
}

-(NSString *)connectionMode
{	
	return _bitSwarm.connectionMode;
}


-(NSArray *)roomList
{
	return [_roomManager getRoomList];
}

-(NSString *)version
{
	return [NSString stringWithFormat:@"%ld.%ld.%ld",(long) _majVersion, (long)_minVersion, (long)_subVersion];
}

#pragma mark -
#pragma mark IBitSwarmEvents protocol

-(void)onSocketConnect:(BitSwarmEvent *)evt
{	
	if ([(NSNumber *)[evt.params objectForKey:@"success"] boolValue])
    {
		[self sendHandshakeRequest:[(NSNumber *)[evt.params objectForKey:@"_isReconnection"] boolValue]];
	}
	else
    {
        [_log warn:@"A connection attempt failed (1)"];
		[self handleConnectionProblem:evt];
	}
}

-(void)onSocketReconnectionTry:(BitSwarmEvent *)evt
{
	if ([_delegate respondsToSelector:@selector(onConnectionRetry:)])
    {
		[_delegate onConnectionRetry:[[SFSEvent alloc] initWithType:SFSEvent_CONNECTION_RETRY params:[NSDictionary dictionary]]];
	}
}

-(void)onSocketIOError:(BitSwarmEvent *)evt
{
	if (_isConnecting)
    {
        [_log warn:@"A connection attempt failed (2)"];
		[self handleConnectionProblem:evt];
	}
}

-(void)onSocketClose:(BitSwarmEvent *)evt
{
	[self reset];
	if ([_delegate respondsToSelector:@selector(onConnectionLost:)])
    {
		//[_delegate onConnectionLost:(SFSEvent *)evt];
        
        [_delegate onConnectionLost:[[SFSEvent alloc] initWithType:SFSEvent_CONNECTION_LOST params:evt.params]];
	}
}
/*
-(void)onSocketSecurityError:(BitSwarmEvent *)evt
{
	if (_isConnecting)
    {
		[self handleConnectionProblem:evt];
	}
}
 */

#pragma mark -
#pragma mark UDP

-(void)onUDPSocketConnect:(SFSEvent *)evt
{
    [_log info:@"UDP init!"];
    if ([_delegate respondsToSelector:@selector(onUDPSocketConnect:)])
    {
		[_delegate onUDPSocketConnect:evt];
	}
}


#pragma mark -
#pragma mark IConfigLoader

-(void)onConfigLoadSuccess:(SFSEvent *)event
{
	ConfigData *cfgData = [event.params objectForKey:@"cfg"];
    
    [self verifyConfigData:cfgData];
	
    self.useBlueBox = cfgData.useBlueBox;
	
	_config = cfgData;
	
	SFSEvent *sfsEvent = [[SFSEvent alloc] initWithType:SFSEvent_CONFIG_LOAD_SUCCESS params:event.params];
	
	if ([_delegate respondsToSelector:@selector(onConfigLoadSuccess:)])
    {
		[_delegate onConfigLoadSuccess:sfsEvent];
	}
	if (_autoConnectOnConfig)
    {
		[self connect:_config.host port: (int)_config.port];
	}
}

-(void)onConfigLoadFailure:(SFSEvent *)event
{
	SFSEvent *sfsEvent = [[SFSEvent alloc] initWithType:SFSEvent_CONFIG_LOAD_FAILURE params:nil];
	
	if ([_delegate respondsToSelector:@selector(onConfigLoadFailure:)])
    {
		[_delegate onConfigLoadFailure:sfsEvent];
	}
}

#pragma mark -

-(void)connectWithConfig:(ConfigData *)cfg
{
    
    [self verifyConfigData:cfg];
    
    self.useBlueBox = cfg.useBlueBox;
	
	_config = cfg;
    
    _lastIpAddress = cfg.host;
	_isConnecting = YES;
	
	[_bitSwarm connect:cfg.host port: (int) cfg.port];
}

-(void)connect:(NSString *)host port:(int)port
{
	if (self.isConnected)
    {
		[_log warn:@"Already connected!"];
		return;
	}
	
	if (_isConnecting)
    {
		[_log warn:@"A connection attempt is already running"];
		return;
	}
	
	if (_config != nil)
    {
		if (host == nil)
        {
			host = _config.host;
		}
		
		if (port == -1)
        {
			port = (int) _config.port;
		}
	}
	
	if (host == nil || [host length] == 0)
    {
		@throw [NSException exceptionWithName:@"ArgumentError" reason:@"Invalid connection host/address" userInfo:nil];
	}
	
	if (port < 0 || port > 65535)
    {
		@throw [NSException exceptionWithName:@"ArgumentError" reason:@"Invalid connection port" userInfo:nil];
	}
	
    _lastIpAddress = host;
	_isConnecting = YES;
	
	[_bitSwarm connect:host port:port];
}

-(void)connect
{
	[self connect:nil port:-1];
}

-(void)loadConfig:(NSString *)filePath connectOnSuccess:(BOOL)connectOnSuccess
{
	ConfigLoader *configLoader = [[ConfigLoader alloc] init];
	
	_autoConnectOnConfig = connectOnSuccess;
	[configLoader loadConfig:filePath delegate:self];
}

-(void)loadConfig
{
	[self loadConfig:@"sfs-config.xml" connectOnSuccess:YES];
}

-(void)verifyConfigData:(ConfigData *)cfg
{
	if (cfg.host == nil || [cfg.host length] == 0)
    {
		@throw [NSException exceptionWithName:@"ArgumentError" reason:@"Invalid Host/IpAddress in config" userInfo:nil];
	}
	
	if (cfg.port < 0 || cfg.port > 65535)
    {
		@throw [NSException exceptionWithName:@"ArgumentError" reason:@"Invalid TCP port in config" userInfo:nil];
	}
	
	if (cfg.zone == nil || [cfg.zone length] == 0)
    {
		@throw [NSException exceptionWithName:@"ArgumentError" reason:@"Invalid Zone name in config" userInfo:nil];
	}
}

-(void)setReconnectionSeconds:(NSInteger)seconds
{
	_bitSwarm.reconnectionSeconds = seconds;
}
-(NSInteger)getReconnectionSeconds
{
	return _bitSwarm.reconnectionSeconds;
}

-(void)handleHandshake:(SFSEvent *)evt
{
	id <IMessage> msg = [evt.params objectForKey:@"message"];
	id <ISFSObject> obj = [msg content];
	
	if ([obj isNull:BaseRequest_KEY_ERROR_CODE])
    {
		self.sessionToken = [obj getUtfString:HandshakeRequest_KEY_SESSION_TOKEN];
		_bitSwarm.compressionThreshold = [obj getInt:HandshakeRequest_KEY_COMPRESSION_THRESHOLD];
        _bitSwarm.maxMessageSize = [obj getInt:HandshakeRequest_KEY_MAX_MESSAGE_SIZE];//obj.getInt(HandshakeRequest.KEY_MAX_MESSAGE_SIZE)
		
		if (_bitSwarm.isReconnecting)
        {
			_bitSwarm.isReconnecting = NO;
			
			if ([_delegate respondsToSelector:@selector(onConnectionResume:)])
            {
				[_delegate onConnectionResume:[[SFSEvent alloc] initWithType:SFSEvent_CONNECTION_RESUME params:[NSDictionary dictionary]]];
			}
		}
		else
        {
			_isConnecting = NO;
			
			if ([_delegate respondsToSelector:@selector(onConnection:)])
            {
				NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], @"success", nil];
				[_delegate onConnection:[[SFSEvent alloc] initWithType:SFSEvent_CONNECTION params:params]];
			}
		}
	}
	else
    {
        NSNumber* errCode = [NSNumber numberWithShort: [obj getShort:BaseRequest_KEY_ERROR_CODE]];
		NSString *errorMsg = [SFSErrorCodes getErrorMessage: errCode params:[obj getUtfStringArray:BaseRequest_KEY_ERROR_PARAMS]];
		
		NSMutableDictionary *params = [NSMutableDictionary dictionary];
		[params setValue:[NSNumber numberWithBool:NO] forKey:@"success"];
		[params setValue:errorMsg forKey:@"errorMessage"];
		
		if ([_delegate respondsToSelector:@selector(onConnection:)])
        {
			[_delegate onConnection:[[SFSEvent alloc] initWithType:SFSEvent_CONNECTION params:params]];
		}
	}

}

-(void)handleLogout
{
	self.userManager = [[SFSGlobalUserManager2 alloc] initWithSfs:self];
	self.roomManager = [[SFSRoomManager alloc] initWithSfs:self];
	self.isJoining = NO;
	self.lastJoinedRoom = nil;
	self.currentZone = nil;
	self.mySelf = nil;
    
    // Stop LagMonitor if it's running
    if (_lagMonitor && [_lagMonitor isRunning])
        [_lagMonitor stop];
    
    // Clear BuddyManager
    [_buddyManager clearAll];
    [(SFSBuddyManager *) _buddyManager setInited:NO];
}

-(void)handleLogin:(SFSEvent *)evt
{
	self.currentZone = [evt.params objectForKey:@"zone"];
}

-(void)handleClientDisconnection:(NSString *)reason
{
	_bitSwarm.reconnectionSeconds = 0;
	[_bitSwarm disconnect:reason];
	
	[self reset];
}

-(void)handleReconnectionFailure
{
    [self setReconnectionSeconds:0];
    [_bitSwarm stopReconnection];
}

-(id <Room>)getRoomById:(NSInteger)roomId
{
	return [_roomManager getRoomById:roomId];
}

-(id <Room>)getRoomByName:(NSString *)roomName
{
	return [_roomManager getRoomByName:roomName];
}

-(NSArray *)getRoomListFromGroup:(NSString *)groupId
{
	return [_roomManager getRoomListFromGroup:groupId];
}

-(void)disconnect
{	
	if (self.isConnected)
    {
		if (_bitSwarm.reconnectionSeconds > 0)
        {
			[self send:[[ManualDisconnectionRequest alloc] init]];
		}
		
		[_bitSwarm performSelector:@selector(disconnect:) withObject:ClientDisconnectionReason_MANUAL afterDelay:0.1];
	}
	else
    {
		[_log info:@"You are not connected..."];
	}
}


-(void)setClientDetails:(NSString *)platformId :(NSString *)version
{
    if (self.isConnected)
    {
        [_log warn:@"SetClientDetails must be called before the connection is started"];
        return;
    }
        
    _clientDetails = [[NSMutableString alloc] initWithString: (![[NSNull null] isEqual:platformId]) ? [platformId stringByReplacingOccurrencesOfString:@" " withString: CLIENT_TYPE_SEPARATOR] : @""];
    [_clientDetails appendString:[CLIENT_TYPE_SEPARATOR mutableCopy]];
    [_clientDetails appendString: (![[NSNull null] isEqual:version]) ? [[version stringByReplacingOccurrencesOfString:@" " withString:CLIENT_TYPE_SEPARATOR] mutableCopy] : @""];
}

// New in 1.7.3 -- August 2017
-(NSRunLoopMode) getRunLoopMode
{
    return [[self getSocketEngine] getRunLoopMode];
}

-(void) setRunLoopMode:(NSRunLoopMode)mode
{
    [[self getSocketEngine] setRunLoopMode:mode];
}

@end
