//
//  SFSIOHandler.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 "SFSIOHandler.h"
#import "PendingPacket.h"
#import "PacketHeader.h"
#import "Logger.h"
#import "SFSProtocolCodec.h"
#import "PacketReadState.h"
#import "SFSException.h"
#import "DefaultObjectDumpFormatter.h"
#import "BitSwarmClient.h"
#import "SmartFox2XClient.h"
#import "IMessage.h"
#import "BBClient.h"

NSInteger const SFSIOHandler_SHORT_BYTE_SIZE = 2;
NSInteger const SFSIOHandler_INT_BYTE_SIZE = 4;

@implementation SFSIOHandler
 

@synthesize bitSwarm = _bitSwarm;
@synthesize log = _log;
@synthesize readState = _readState;
@synthesize pendingPacket = _pendingPacket;
@dynamic codec;


-(id)initWithBitSwarmClient:(BitSwarmClient *)bitSwarm {
	if ((self = [super init])) {
		self.bitSwarm = bitSwarm;
		self.log = [Logger getInstance];
		self.readState = PacketReadState_WAIT_NEW_PACKET;
		self.codec = [[SFSProtocolCodec alloc] initWithIoHandler:self bitSwarm:bitSwarm];
		
        _emptyBuffer = [NSData data];
        _packetEncrypter = [[DefaultPacketEncrypter alloc] initWithBitSwarm:bitSwarm];
	}
	
	return self;
}

-(id <IProtocolCodec>)codec {
	return _protocolCodec;
}

-(void)setCodec:(id <IProtocolCodec>)codec {
	_protocolCodec = codec;
}

-(NSData *)resizeByteArray:(NSData *)array pos:(NSInteger)pos len:(NSInteger)len {
	
	NSRange range = {pos, len};
	NSMutableData *newData = [NSMutableData dataWithData:[array subdataWithRange:range]];
	
	return newData;
}

-(NSData *)handlePacketData:(NSData *)data
{
	int remaining = (int)_pendingPacket.header.expectedLen - (int)[_pendingPacket.buffer length];
	BOOL isThereMore = ([data length] > remaining);
	
	if (_bitSwarm.sfs.debug)
	{
		[_log debug:[NSString stringWithFormat:@"Handling Data: %ld, previous state: %ld / %ld", (long)[data length], (long)[_pendingPacket.buffer length], (long)_pendingPacket.header.expectedLen]];
	}
	
	
	if ([data length] >= remaining) {
		[_pendingPacket.buffer appendBytes:[data bytes] length:remaining];
		
		if (_bitSwarm.sfs.debug)
			[_log debug:@"<<< Packet Complete >>>"];
		
        // Handle encryption
        if (_pendingPacket.header.encrypted)
        {
            NSData* decoded = [_packetEncrypter decode:_pendingPacket.buffer];
            if (decoded)
                _pendingPacket.buffer.data = decoded;
            else
                @throw ([NSException exceptionWithName:@"Failed to decode encrypted packet" reason:@"Details may be found by checking the API debug." userInfo:nil]);
        }
		
		//handle Compression
		if (_pendingPacket.header.compressed) {
			_pendingPacket.buffer = [NSMutableData dataWithData:[_pendingPacket.buffer zlibInflate]];
		}
		
		[_protocolCodec onPacketRead:_pendingPacket.buffer];
		
		_readState = PacketReadState_WAIT_NEW_PACKET;
	}
	else {
		[_pendingPacket.buffer appendData:data];
	}
	
	if (isThereMore) {
		data = [self resizeByteArray:data pos:remaining len:[data length] - remaining];
	}
	else {
		data = _emptyBuffer;
	}
	
	return data;
}

-(NSData *)handleDataSizeFragment:(NSData *)data
{
	if (_bitSwarm.sfs.debug)
		[_log debug:[NSString stringWithFormat:@"Handling Size fragment. Data: %ld", (long)[data length]]];
	
	int remaining = _pendingPacket.header.bigSized ? (int)(4 - [_pendingPacket.buffer length]) : (int)(2 - [_pendingPacket.buffer length]);
	
	if ([data length] >= remaining) {
		
		[_pendingPacket.buffer appendBytes:[data bytes] length:remaining];
		
		int32_t dataSize = 0;
		if (_pendingPacket.header.bigSized)
        {
			[_pendingPacket.buffer getBytes:&dataSize length:4];
            dataSize = CFSwapInt32(dataSize);
		}
		else
        {
			[_pendingPacket.buffer getBytes:&dataSize length:2];
            dataSize = CFSwapInt16(dataSize);
		}
        
		if (_bitSwarm.sfs.debug)
			[_log debug:[NSString stringWithFormat:@"DataSize is ready:%d bytes.", dataSize]];
		
		if (_bitSwarm.sfs.debug)
			[_log debug:[NSString stringWithFormat:@"DataSize is ready:%ld bytes.", (long)dataSize]];
		
		_pendingPacket.header.expectedLen = dataSize;
		_pendingPacket.buffer = [NSMutableData data];
		
		_readState = PacketReadState_WAIT_DATA;
		
		if ([data length] > remaining) {
			data = [self resizeByteArray:data pos:remaining len:[data length] - remaining];
		}
		else {
			data = _emptyBuffer;
		}
	}
	
	else {
		[_pendingPacket.buffer appendData:data];
		data = _emptyBuffer;
	}
	
	return data;

}

-(NSData *)handleDataSize:(NSData *)data
{
	if (_bitSwarm.sfs.debug)
		[_log debug:[NSString stringWithFormat:@"Handling Header Size. Size: %ld (%@)", (long)[data length], (_pendingPacket.header.bigSized ? @"big" : @"small")]];
	
	int dataSize32 = -1;
	int dataSize16 = -1;
	int sizeBytes = 2;
	
	if (_pendingPacket.header.bigSized) {
		
		if ([data length] >= 4) {
			[data getBytes:&dataSize32 length:4];
			dataSize32 = CFSwapInt32(dataSize32);
		}
		
		sizeBytes = 4;
	}
	
	else {
		if ([data length] >= 2) {
			[data getBytes:&dataSize16 length:2];
			dataSize16 = CFSwapInt16(dataSize16);
		}
	}
	
	if (dataSize16 != -1 || dataSize32 != -1)
	{
		_pendingPacket.header.expectedLen = _pendingPacket.header.bigSized ? dataSize32 : dataSize16;
		
		if (_bitSwarm.sfs.debug)
			[_log debug:[NSString stringWithFormat:@"Expected Size: %ld", (long)_pendingPacket.header.expectedLen]];
		
		data = [self resizeByteArray:data pos:sizeBytes len:[data length] - sizeBytes];
		_readState = PacketReadState_WAIT_DATA;
	}
	else
	{
		_readState = PacketReadState_WAIT_DATA_SIZE_FRAGMENT;
		
		[_pendingPacket.buffer appendData:data];
		
		data = _emptyBuffer;
	}

	return data;
}

-(NSData *)handleNewPacket:(NSData *)data
{
	if (_bitSwarm.sfs.debug)
		[_log debug:@"Handling New Packet"];
	
	int8_t headerByte;
	
	if (_bitSwarm.sfs.debug)
		[_log debug:[NSString stringWithFormat:@">> Header byte: %d", headerByte]];
	
	[data getBytes:&headerByte length:1];
	
	if (!((headerByte & 128) > 0)) {
		@throw [SFSException exceptionWithReason:[NSString stringWithFormat:@"Unexpected header byte: %d", headerByte]];
	}
	
	PacketHeader *header = [PacketHeader fromBinary:headerByte];
	
	self.pendingPacket = [[PendingPacket alloc] initWithPacketHeader:header];
	
	_readState = PacketReadState_WAIT_DATA_SIZE;
	
	return [self resizeByteArray:data pos:1 len:[data length] - 1];
	
}

-(void)onDataRead:(NSData *)data
{
	if (_bitSwarm.sfs.debug) {
		[_log info:[NSString stringWithFormat:@"Data Read: %@", [DefaultObjectDumpFormatter hexDump:data bytesPerLine:-1]]];
	}
	
	if ([data length] == 0) {
		@throw [SFSException exceptionWithReason:@"Unexpected empty packet data: no readable bytes available!"];
	}
	
	while ([data length] > 0) {
		if (_readState == PacketReadState_WAIT_NEW_PACKET) {
			data = [self handleNewPacket:data];
		}
		if (_readState == PacketReadState_WAIT_DATA_SIZE) {
			data = [self handleDataSize:data];
		}
		if (_readState == PacketReadState_WAIT_DATA_SIZE_FRAGMENT) {
			data = [self handleDataSizeFragment:data];
		}
		if (_readState == PacketReadState_WAIT_DATA) {
			data = [self handlePacketData:data];
		}
	}
}

-(void)onDataWrite:(id <IMessage>)message
{
	NSMutableData *writeBuffer = [NSMutableData data];
	NSData *binData = [[message content] toBinary];
	BOOL isCompressed = NO;
    BOOL isEncrypted = NO;
	
	// 1 --> Handle Compression
	if ([binData length] > _bitSwarm.compressionThreshold)
    {
		binData = [binData zlibDeflate];
		isCompressed = YES;
	}
    
    if ([binData length] > _bitSwarm.maxMessageSize)
    {
        /*
         * Houston we have problem!
         * The outgoing message is bigger than what the server allows
         * We should stop here and provide an error to the developer
         */

        NSString* errMesg = [NSString stringWithFormat:@"Outgoing message too big: %lu, Server limit is: %ld", (unsigned long)[binData length], (long)_bitSwarm.maxMessageSize];
        
        @throw ([NSException exceptionWithName:errMesg reason:nil userInfo:nil]);
    }
    
    // 2 --> Handle Encryption
    
    /*
     * Updated March 28th 2018
     * -------------------------------
     * When reconnecting we need to send a non-encrypted handshake even if the previous
     * connection was encrypted. After this step the session will continue from where it started
     * with active encryption.
     *
     * The 2nd condition makes sure that a packet is not encrypted if the engine is in "reconnecting" state
     *
     */
    if (_bitSwarm.cryptoKey != nil  && !_bitSwarm.isReconnecting)
    {
        NSData* encrypted = [_packetEncrypter encode:binData];
        
        if (encrypted != nil)
        {
            binData = encrypted;
            isEncrypted = true;
        }
        else @throw ([NSException exceptionWithName:@"Failed to encrypt outgoing packet" reason:nil userInfo:nil]);
    }
    

	int sizeBytes = SFSIOHandler_SHORT_BYTE_SIZE;
	
	if ([binData length] > 65535)
		sizeBytes = SFSIOHandler_INT_BYTE_SIZE;

	// BlueBoxed flag is not implemented yet
	PacketHeader *packetHeader = [[PacketHeader alloc] initWithParams:isEncrypted compressed:isCompressed blueBoxed:NO bigSized:sizeBytes == SFSIOHandler_INT_BYTE_SIZE];

	// 1. Write packet header byte
	int8_t encodedHeader = [packetHeader encode];
	[writeBuffer appendData:[NSData dataWithBytes:&encodedHeader length:1]];
	
	// 2. Write packet size
	if (sizeBytes > SFSIOHandler_SHORT_BYTE_SIZE)
    {
		int len = (int)[binData length];
		len = CFSwapInt32(len);
		[writeBuffer appendData:[NSData dataWithBytes:&len length:4]];
	}
	else
    {
		short len = [binData length];
		len = CFSwapInt16(len);
		[writeBuffer appendBytes:&len length:2];
	}
	// 3. Write actual packet data
	[writeBuffer appendData:binData];
	
	// 4. send in hyperspace    
	if (_bitSwarm.connected)
    {
        
        if (_bitSwarm.useBlueBox)
        {
            [[_bitSwarm httpSocket] send:writeBuffer];            
        }
        else
        {
            if([message isUDP])
            {
                [_bitSwarm writeToUDPSocket:writeBuffer];
            }
            else
            {
                [_bitSwarm writeToSocket:writeBuffer];
            }
        }
        
        
        if (_bitSwarm.sfs.debug)
        {
            [_log info:[NSString stringWithFormat:@"Data Write: %@", [DefaultObjectDumpFormatter hexDump:writeBuffer bytesPerLine:-1]]];
        }
        
	}
}

@end
