//
//  BBClient.m
//  SFS2X
//
//  Created by Wayne Helman on 2012-08-14.
//  Copyright (c) 2012 A51 Integrated | http://a51integrated.com. All rights reserved.
//

#import "BBClient.h"
#import "DefaultObjectDumpFormatter.h"
#import "NSString+Encode.h"


NSString const * BB_DEFAULT_HOST = @"localhost";

/*
    Since release 1.7.5 (Aug 2018) we're supporting BlueBox via HTTPS
    since Apple does not allow HTTP connections anymore.
 
    For this reason the default BB HTTP port is actually the SFS2X default HTTPS port
    i.e. 8443
*/
int const BB_DEFAULT_PORT = 8443;
NSString * const BB_SERVLET = @"BlueBox/BlueBox.do";
NSString * const BB_NULL = @"null";

NSString * const CMD_CONNECT = @"connect";
NSString * const CMD_POLL = @"poll";
NSString * const CMD_DATA = @"data";
NSString * const CMD_DISCONNECT = @"disconnect";
NSString * const ERR_INVALID_SESSION = @"err01";

NSString * const SFS_HTTP = @"sfsHttp";
NSString * const SEP = @"|";

NSTimeInterval const MIN_POLL_SPEED = 20; // ms
NSTimeInterval const MAX_POLL_SPEED = 10000; // ms
NSTimeInterval const DEFAULT_POLL_SPEED = 700; // ms

static const char _base64EncodingTable[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

static const short _base64DecodingTable[256] = {
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -2, -1, -1, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 62, -2, -2, -2, 63,
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -2, -2, -2, -2, -2, -2,
    -2,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -2, -2, -2, -2, -2,
    -2, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2
};


@implementation BBClient

@synthesize isDebug         = _debug;
@synthesize host            = _host;
@synthesize port            = _port;
@synthesize sessionId       = _sessId;
@synthesize pollSpeed       = _pollSpeed;


-(void)dealloc
{
    _bbUrl = nil;
    _log = nil;

    
    CFRelease(_requests);
    
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(poll) object:nil ];
    
}


-(id)initWithBitSwarm:(BitSwarmClient *) bsClient debug:(BOOL)debug
{
	if ((self = [super init]))
    {
        _debug = debug;
        _delegate = bsClient;
        
        //_pollSpeed = DEFAULT_POLL_SPEED/1000;
        
        _log = [Logger getInstance];
        
        _requests = CFDictionaryCreateMutable(kCFAllocatorDefault,
                                  0,
                                  &kCFTypeDictionaryKeyCallBacks,
                                  &kCFTypeDictionaryValueCallBacks);
        
	}
	return self;
}


-(BOOL)isConnected
{
    return (self.sessionId != nil) ? YES : NO;
}

-(void)pollSpeed:(NSTimeInterval)value
{
    _pollSpeed = ((value >= MIN_POLL_SPEED && value <= MAX_POLL_SPEED) ? value : DEFAULT_POLL_SPEED) / 1000;
}

-(void)checkStatus
{
    if (!_isConnected)
    {
        @throw [NSException exceptionWithName:@"IllegalOperationError" reason:@"Can't send data, BlueBox connection is not active" userInfo:nil];
    }
}

-(void)connect:(NSString * )host usingPort:(int) port
{
    if (_isConnected)
    {
        @throw [NSException exceptionWithName:@"IllegalOperationError" reason:@"BlueBox session is already connected" userInfo:nil];
    }
    
    _host = host;
    _port = port;

    _bbUrl = [NSString stringWithFormat:@"%@%@%@%d%@%@", @"https://", _host, @":", _port, @"/", BB_SERVLET];
    
    [self sendRequest:CMD_CONNECT data:nil];
}


-(void)send:(NSData *)data
{
    [self checkStatus];
    [self sendRequest:CMD_DATA data:data];
}


-(void)disconnect
{
    [self sendRequest:CMD_DISCONNECT data:nil];
}


-(void)close
{
    [self handleConnectionLost: false ];
}
     

// NSURLConnection delegate methods
     
-(void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
{
    //[_log info: @"[ BB-didReceiveData ]:"];
    NSMutableDictionary *connectionInfo = CFDictionaryGetValue(_requests, (__bridge void *) connection);
    [[connectionInfo objectForKey:@"receivedData"] appendData:data];
}
     
-(void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response
{
   // [_log info: @"[ BB-didReceiveResponse ]:"];
}
     
-(void)connectionDidFinishLoading:(NSURLConnection*)connection
{
    [self handleData: connection]; // Deal with the data
}

 -(void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
{
    [_delegate onBBError:[[BBEvent alloc] initWithType:BBEvent_IO_ERROR params:[NSDictionary dictionaryWithObject:error forKey:@"message"]]];
}

// END NSURLConnection delegate methods
     
-(void)handleData:(NSURLConnection *) connection
{
    NSMutableDictionary *connectionInfo = CFDictionaryGetValue(_requests, (__bridge const void *)(connection));
    NSData * requestData = [connectionInfo objectForKey:@"receivedData"];
    
    NSString * rawData = [[NSString alloc] initWithData:requestData encoding:NSASCIIStringEncoding];
    
    if (_debug)
    {
        [_log info:[NSString stringWithFormat:@"[ BB-Receive: %@ ]", rawData]];
    }
    
    if([rawData length] == 0)
    {
        CFDictionaryRemoveValue(_requests, (__bridge const void *)(connection));
        return;
    }
    
    rawData = [rawData stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    
    // Obtain splitted params
    NSArray * reqBits = [rawData componentsSeparatedByString:SEP];
    
    NSString * cmd = [reqBits objectAtIndex: 0];
    NSString * data = [reqBits objectAtIndex: 1];
    
    if ([cmd isEqualToString:CMD_CONNECT])
    {
        self.sessionId = data;
        _isConnected = YES;
        
        NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], @"success", nil];
        [_delegate onBBConnect:[[BBEvent alloc] initWithType:BBEvent_CONNECT params:params]];
        
        // Start the polling cycle
        [self poll];
    }
    
    else if ([cmd isEqualToString:CMD_POLL])
    {
        NSData *dataVal;
        
        // Decode Base64-Encoded string to real ByteArray
        if (![data isEqualToString:BB_NULL])
        {
            dataVal = [self decodeResponse:data];
        }
        else
        {
            dataVal =[ @"null" dataUsingEncoding:NSUTF8StringEncoding];

        }
        
        // Pre-launch next polling request
        if (_isConnected)
        {
            // Old system
            /*
            [self performSelector:@selector(poll) withObject:nil afterDelay: _pollSpeed];
             */
            NSRunLoopMode loopMode = [_delegate getRunLoopMode];
            
            // New in 1.7.3
            [self performSelector:@selector(poll) withObject:nil afterDelay: _pollSpeed inModes:@[ loopMode ]];
        }
        
        // Dispatch the event
        [_delegate onBBData: [[BBEvent alloc] initWithType:BBEvent_DATA params:[NSDictionary dictionaryWithObject:dataVal forKey:@"data"]]];
        
    }

    // Connection was lost
    else if ([cmd isEqualToString:ERR_INVALID_SESSION])
    {
        [self handleConnectionLost: true ];
    }
    
    CFDictionaryRemoveValue(_requests, (__bridge const void *)(connection));
    
}
     

-(void)poll
{
    [self sendRequest: CMD_POLL data:nil];
}
    
-(void)sendRequest:(NSString *)cmd data:(NSData *)data
{
    // Prepare request
    NSMutableURLRequest *sfsRequest = [[NSMutableURLRequest alloc] init];
	[sfsRequest setURL:[NSURL URLWithString:_bbUrl]];
	[sfsRequest setCachePolicy:NSURLCacheStorageNotAllowed];
	[sfsRequest setTimeoutInterval:30];
	[sfsRequest setHTTPShouldHandleCookies:NO];
	[sfsRequest setHTTPMethod:@"POST"];
    
    NSString * vars = [NSString stringWithFormat:@"%@=%@", SFS_HTTP, [[self encodeRequest:cmd data:data] encodeString: NSUTF8StringEncoding]];
    
    if (_debug)
    {
        [_log info:[NSString stringWithFormat:@"[ BB-Send ]: %@", vars ]];
    }
    
    [sfsRequest setHTTPBody:[vars dataUsingEncoding:NSUTF8StringEncoding]];
    
    // Updated in 1.7.3 -- July 2017
    NSURLConnection* sfsConnection = [[NSURLConnection alloc] initWithRequest:sfsRequest delegate:self startImmediately:NO];
    
    // Obtain the run loop mode from BitSwarmClient instance
    [sfsConnection scheduleInRunLoop:[NSRunLoop currentRunLoop]
                          forMode:[_delegate getRunLoopMode]];
    
    
    [sfsConnection start];
    
    CFDictionaryAddValue(_requests, (__bridge const void *)(sfsConnection),
                         (__bridge const void *)([NSMutableDictionary
                          dictionaryWithObject:[NSMutableData data]
                          forKey:@"receivedData"]));
    
}
    
     
     
-(void)handleConnectionLost:(BOOL)fireEvent
{
    if (_isConnected)
    {
        _isConnected = NO;
        self.sessionId = nil;
        
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(poll) object:nil ];
        
        // Fire event to Bitswarm client
        if (fireEvent)
        {
            [_delegate onBBDisconnect:[[BBEvent alloc] initWithType:BBEvent_DISCONNECT params:[NSDictionary dictionary]]];
        }
    }
}
     
-(NSString *)encodeRequest:(NSString *)cmd data:(NSData *)data
{
    NSString * en_data;
    
    if (cmd == nil)
    {
        cmd = BB_NULL;
    }
    
    if (data == nil)
    {
        en_data = BB_NULL;
    }
    else
    {
        en_data = [self encodeBase64WithData: data];
    }
    
    NSString * encoded = [NSString stringWithFormat:@"%@%@%@%@%@", (self.sessionId == nil) ? BB_NULL : self.sessionId, SEP, cmd, SEP, en_data];

    return encoded;
}
     
-(NSData *)decodeResponse:(NSString *)rawData
{
    return [self decodeBase64WithString:rawData];
}

 
- (NSString *)encodeBase64WithString:(NSString *)strData
{
    return [self encodeBase64WithData:[strData dataUsingEncoding:NSUTF8StringEncoding]];
}





- (NSString *)encodeBase64WithData:(NSData *)data
{
     const unsigned char * objRawData = [data bytes];
     char * objPointer;
     char * strResult;
     
     // Get the Raw Data length and ensure we actually have data
     size_t intLength = [data length];
     if (intLength == 0) return nil;
     
     // Setup the String-based Result placeholder and pointer within that placeholder
     strResult = (char *)calloc(((intLength + 2) / 3) * 4, sizeof(char));
     objPointer = strResult;
     
     // Iterate through everything
     while (intLength > 2) { // keep going until we have less than 24 bits
         *objPointer++ = _base64EncodingTable[(objRawData[0] & 0xfc) >> 2];
         *objPointer++ = _base64EncodingTable[((objRawData[0] & 0x03) << 4) | (objRawData[1] >> 4)];
         *objPointer++ = _base64EncodingTable[((objRawData[1] & 0x0f) << 2) | (objRawData[2] >> 6)];
         *objPointer++ = _base64EncodingTable[objRawData[2] & 0x3f];
         
         // we just handled 3 octets (24 bits) of data
         objRawData += 3;
         intLength -= 3;
     }
     
     // now deal with the tail end of things
     if (intLength != 0) {
         *objPointer++ = _base64EncodingTable[(objRawData[0] & 0xfc) >> 2];
         if (intLength > 1) {
             *objPointer++ = _base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)];
             *objPointer++ = _base64EncodingTable[(objRawData[1] & 0x0f) << 2];
             *objPointer++ = '=';
         } else {
             *objPointer++ = _base64EncodingTable[(objRawData[0] & 0x03) << 4];
             *objPointer++ = '=';
             *objPointer++ = '=';
         }
     }
     
     NSString *strToReturn = [[NSString alloc] initWithBytesNoCopy:strResult length:objPointer - strResult encoding:NSASCIIStringEncoding freeWhenDone:YES];
     return strToReturn;
}

- (NSData *)decodeBase64WithString:(NSString *)strBase64
{
     const char * objPointer = [strBase64 cStringUsingEncoding:NSASCIIStringEncoding];
     if (objPointer == NULL)  return nil;
     size_t intLength = strlen(objPointer);
     int intCurrent;
     int i = 0, j = 0, k;
     
    char * objResult;
     objResult = calloc(intLength, sizeof(char));
     
     // Run through the whole string, converting as we go
     while ( ((intCurrent = *objPointer++) != '\0') && (intLength-- > 0) ) {
         if (intCurrent == '=') {
             if (*objPointer != '=' && ((i % 4) == 1)) {// || (intLength > 0)) {
                 // the padding character is invalid at this point -- so this entire string is invalid
                 free(objResult);
                 return nil;
             }
             continue;
         }
         
         intCurrent = _base64DecodingTable[intCurrent];
         if (intCurrent == -1) {
             // we're at a whitespace -- simply skip over
             continue;
         } else if (intCurrent == -2) {
             // we're at an invalid character
             free(objResult);
             return nil;
         }
         
         switch (i % 4) {
             case 0:
                 objResult[j] = intCurrent << 2;
                 break;
                 
             case 1:
                 objResult[j++] |= intCurrent >> 4;
                 objResult[j] = (intCurrent & 0x0f) << 4;
                 break;
                 
             case 2:
                 objResult[j++] |= intCurrent >>2;
                 objResult[j] = (intCurrent & 0x03) << 6;
                 break;
                 
             case 3:
                 objResult[j++] |= intCurrent;
                 break;
         }
         i++;
     }
     
     // mop things up if we ended on a boundary
     k = j;
     if (intCurrent == '=') {
         switch (i % 4) {
             case 1:
                 // Invalid state
                 free(objResult);
                 return nil;
                 
             case 2:
                 k++;
                 // flow through
             case 3:
                 objResult[k] = 0;
         }
     }
     
     // Cleanup and setup the return NSData
     return [[NSData alloc] initWithBytesNoCopy:objResult length:j freeWhenDone:YES];
}
     

@end
