//
//  UDPManager.m
//  SFS2X
//
//  Created by Wayne Helman on 12-01-10.
//  Copyright 2012 A51 Integrated | http://a51integrated.com. All rights reserved.
//

#include <netdb.h>

#import "UDPManager.h"
#import "Logger.h"
#import "BitSwarmClient.h"
#import "BitSwarmEvent.h"
#import "SmartFox2XClient.h"
#import "PacketHeader.h"
#import "DefaultObjectDumpFormatter.h"
#import "DefaultSFSDataSerializer.h"
#import "SFSIOHandler.h"
#import "UDPController.h"
#import "SFSException.h"


NSInteger const MAX_RETRY = 3; // Retry max 3 times
NSInteger const RESPONSE_TIMEOUT = 3; // Wait response for max 3 seconds


@implementation UDPManager

@synthesize bitSwarm    = _bitSwarm;
@synthesize sfs         = _sfs;

@synthesize udpc        = _udpc;
@synthesize startingThread  = _startingThread;

@synthesize hostName    = _hostName;
@synthesize port        = _port;
@synthesize inited      = _initSuccess;


#pragma mark * Utilities

static NSString * DisplayErrorFromError(NSError *error)
{
    NSString *      result;
    NSNumber *      failureNum;
    int             failure;
    const char *    failureStr;
    
    result = nil;
    
    if ( [[error domain] isEqual:(NSString *)kCFErrorDomainCFNetwork] && ([error code] == kCFHostErrorUnknown) )
    {
        failureNum = [[error userInfo] objectForKey:(id)kCFGetAddrInfoFailureKey];
        if ( [failureNum isKindOfClass:[NSNumber class]] )
        {
            failure = [failureNum intValue];
            if (failure != 0) {
                failureStr = gai_strerror(failure);
                if (failureStr != NULL) {
                    result = [NSString stringWithUTF8String:failureStr];
                }
            }
        }
    }
    
    if (result == nil) {
        result = [error localizedFailureReason];
    }
    if (result == nil) {
        result = [error localizedDescription];
    }
    if (result == nil) {
        result = [error description];
    }

    return result;
}

#pragma mark * Main

-(void)dealloc
{    

    [_udpc stop];
    
    [_startingThread invalidate];
    
    
}


-(id)initWithBitSwarm:(BitSwarmClient *)bitSwarm host:(NSString *)host port:(NSInteger)port
{    
	if ((self = [super init]))
    {
        _log = [Logger getInstance];
        
		self.bitSwarm = bitSwarm;
		self.sfs = bitSwarm.sfs;
        
		if (!(self = [self initDefaults])) return nil;
        [self initializeWithHost:host port:port];
	}
	return self;
}

-(id)initDefaults
{    
    _locked = NO;
    _initSuccess = NO;
    _packetId = 0;
    _currentAttempt = 0;
    
    self.inited = NO;
    
    return self;
}


-(void)reset
{
    [self.udpc stop];
    self.udpc = nil;
    [_startingThread invalidate];
    (void)[self initDefaults];
}


-(void)dispatchInitEvent:(BOOL)isInit
{
    NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:isInit], @"success", nil];
    SFSEvent *event = [[SFSEvent alloc] initWithType:SFSEvent_UDP_INIT params:params];
    
    if ([[self.sfs delegate] respondsToSelector:@selector(onUDPSocketConnect:)])
    {
        [[self.sfs delegate] onUDPSocketConnect:event];
    }
}


- (void)udpc:(UDPController *)udpc didReceiveData:(NSData *)data fromAddress:(NSData *)addr
{
    if(!self.inited)
    {
        self.inited = YES;
        [_startingThread invalidate];
        _locked = false;
        [self dispatchInitEvent:YES]; 
    }
    else
    {
        @try
        {
            [[_bitSwarm ioHandler] onDataRead:data];
        }
        @catch (SFSException *e)
        {
            [_log warn:@"Exception onDataRead!!!"];
        }
        
    }
}

- (void)udpc:(UDPController *)udpc didReceiveError:(NSError *)error
{
    if(!self.inited)
    {
        NSLog(@"UDP failed before init with error: %@", DisplayErrorFromError(error));
    }
    else
    {
        NSLog(@"UDP failed with error: %@", DisplayErrorFromError(error));
    }
}

// NOT USED
- (void)udpc:(UDPController *)udpc didSendData:(NSData *)data toAddress:(NSData *)addr
{
    NSLog(@"didSendData to UDP socket");
}

- (void)udpc:(UDPController *)udpc didFailToSendData:(NSData *)data toAddress:(NSData *)addr error:(NSError *)error
{
    NSLog(@"ERROR: %@", DisplayErrorFromError(error));
}

- (void)udpc:(UDPController *)udpc didStartWithAddress:(NSData *)address;
{    
    [self sendInitializationRequest];
}

- (void)udpc:(UDPController *)udpc didStopWithError:(NSError *)error
{
    NSLog(@"UDP failed with error: %@", DisplayErrorFromError(error));
    
    self.udpc = nil;
}


-(void)initializeWithHost:(NSString *)udpHost port:(NSInteger)updPort
{
    self.hostName = udpHost;
    self.port = updPort;
    
    if (self.inited)
    {
        [_log warn:@"UDP Channel already initialized!"];
        return;
    }
    
    if (!_locked)
    {
        _locked = true;
                
        self.udpc = [[UDPController alloc] init];
        self.udpc.delegate = self;
        
        self.startingThread = [NSTimer scheduledTimerWithTimeInterval:RESPONSE_TIMEOUT target:self selector:@selector(sendInitializationRequest) userInfo:nil repeats:YES];
        
        [self.udpc startConnectionToHostName:udpHost port:updPort];
    }
    else
    {
        [_log warn:@"UPD initialization is already in progress!"];
    }
    
}


-(void)sendInitializationRequest
{
    if(_currentAttempt == MAX_RETRY)
    {
        _locked = false;
        _currentAttempt = 0;
        [_startingThread invalidate];
        
        [self dispatchInitEvent:NO];
        
        return;
    }
    else
    {
        _currentAttempt++;
    }
    
    SFSObject *message = [SFSObject newInstance];
    [message putByte:@"c" value:1];	
    [message putByte:@"h" value:1];
    [message putLong:@"i" value: [self nextUdpPacketId]];
    [message putInt:@"u" value: [[_sfs mySelf] id]];
    
	NSMutableData *writeBuffer = [NSMutableData data];
	
	NSData *binData = [message toBinary];
    
	int8_t encodedHeader = 0x80;
    [writeBuffer appendBytes:&encodedHeader length:1];
    
	if ([binData length] > 65535)
    {
		int len = (int)[binData length];
		len = CFSwapInt32(len);
		[writeBuffer appendData:[NSData dataWithBytes:&len length:SFSIOHandler_INT_BYTE_SIZE]];
	}
	else
    {
		short len = [binData length];
		len = CFSwapInt16(len);
		[writeBuffer appendBytes:&len length:SFSIOHandler_SHORT_BYTE_SIZE];
	}

	[writeBuffer appendData:binData];
	
	if (_bitSwarm.connected)
    {
        [self.udpc sendData:writeBuffer];
        
		if (_bitSwarm.sfs.debug)
        {
			[_log info:[NSString stringWithFormat:@"UDP Handshake written: %@", [DefaultObjectDumpFormatter hexDump:writeBuffer bytesPerLine:-1]]];
		}

        _packetId = [NSNumber numberWithInt:[_packetId intValue] + 1];
	}
}

-(void)send:(NSData *) writeBuffer
{
	if (self.inited)
    {
        @try
        {
            [self.udpc sendData:writeBuffer];
            
            if (_bitSwarm.sfs.debug)
            {
                [_log info:[NSString stringWithFormat:@"UDP Data written: %@", [DefaultObjectDumpFormatter hexDump:writeBuffer bytesPerLine:-1]]];
            }
        }
        @catch (NSException *e)
        {
            [_log info:[NSString stringWithFormat:@"WriteUDP operation failed due to I/O Error: %@", e]];
        }
	}
    else
    {
        [_log warn:[NSString stringWithFormat:@"UDP protocol is not initialized yet. Pleas use the -(void)initUDP method."]];
    }
}

-(NSNumber *)nextUdpPacketId
{
    NSNumber *number = [NSNumber numberWithInt:[_packetId intValue]];
    int value = [number intValue];
    
    return [NSNumber numberWithInt:value + 1];
}


@end
