//
// NetUtils.m
// GotoBus
//
// Created by haibara on 1/4/15.
//
//

#import "NetUtils.h"
#import <BlocksKit/BlocksKit.h>
#import <RestKit/RKDictionaryUtilities.h>
#import <RestKit/RKURLEncodedSerialization.h>

static NSError *_AFErrorWithUnderlyingError(NSError *error, NSError *underlyingError) {
	if (!error) {
		return underlyingError;
	}
	if (!underlyingError || error.userInfo[NSUnderlyingErrorKey]) {
		return error;
	}
	NSMutableDictionary *mutableUserInfo = [error.userInfo mutableCopy];
	mutableUserInfo[NSUnderlyingErrorKey] = underlyingError;
	return [[NSError alloc] initWithDomain:error.domain code:error.code userInfo:mutableUserInfo];
}

static BOOL _AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger code, NSString *domain) {
	if ([error.domain isEqualToString:domain] && error.code == code) {
		return YES;
	} else if (error.userInfo[NSUnderlyingErrorKey]) {
		return _AFErrorOrUnderlyingErrorHasCodeInDomain(error.userInfo[NSUnderlyingErrorKey], code, domain);
	}
	return NO;
}

static id _AFJSONObjectByRemovingKeysWithNullValues(id JSONObject, NSJSONReadingOptions readingOptions) {
	if ([JSONObject isKindOfClass:[NSArray class]]) {
		NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:[(NSArray *) JSONObject count]];
		for (id value in (NSArray *)JSONObject) {
			[mutableArray addObject:_AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions)];
		}
		return (readingOptions & NSJSONReadingMutableContainers) ? mutableArray : [NSArray arrayWithArray:mutableArray];
	} else if ([JSONObject isKindOfClass:[NSDictionary class]]) {
		NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:JSONObject];
		for (id <NSCopying> key in [(NSDictionary *)JSONObject allKeys]) {
			id value = (NSDictionary *)JSONObject[key];
			if (!value || [value isEqual:[NSNull null]]) {
				[mutableDictionary removeObjectForKey:key];
			} else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]]) {
				mutableDictionary[key] = _AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions);
			}
		}
		return (readingOptions & NSJSONReadingMutableContainers) ? mutableDictionary : [NSDictionary dictionaryWithDictionary:mutableDictionary];
	}
	return JSONObject;
}


@implementation AFEncodedTEXTResponseSerializer

- (id)responseObjectForResponse:(NSURLResponse *)response data:(NSData *)data
 error:(NSError *__autoreleasing *)error {
	if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
		if (!error || _AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
			return nil;
		}
	}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
	NSStringEncoding stringEncoding = self.stringEncoding ? self.stringEncoding : NSUTF8StringEncoding;
#pragma clang diagnostic pop
	if (response.textEncodingName) {
		CFStringEncoding encoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
		if (encoding != kCFStringEncodingInvalidId) {
			stringEncoding = CFStringConvertEncodingToNSStringEncoding(encoding);
		}
	}
	id responseObject = nil;
	NSError *serializationError = nil;
	@autoreleasepool {
		NSString *responseString = [[NSString alloc] initWithData:data encoding:stringEncoding];
		if (responseString) {
			responseObject = responseString;
		} else {
			NSDictionary *userInfo = @{
				NSLocalizedDescriptionKey : [NSString stringWithFormat:NSLocalizedStringFromTable(@"Data failed decoding as a %@ string", @"AFNetworking", nil), [NSString localizedNameOfStringEncoding:stringEncoding]],
				NSLocalizedFailureReasonErrorKey : [NSString stringWithFormat:NSLocalizedStringFromTable(@"Could not decode string: %@", @"AFNetworking", nil), responseString]
			};
			serializationError = [NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo];
		}
	}
	if (error) {
		*error = _AFErrorWithUnderlyingError(serializationError, *error);
	}
	return responseObject;
}

@end


@implementation AFEncodedJSONResponseSerializer

- (id)responseObjectForResponse:(NSURLResponse *)response data:(NSData *)data error:(NSError *__autoreleasing *)error {
	if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
		if (!error || _AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
			return nil;
		}
	}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
	NSStringEncoding stringEncoding = self.stringEncoding ? self.stringEncoding : NSUTF8StringEncoding;
#pragma clang diagnostic pop
	if (response.textEncodingName) {
		CFStringEncoding encoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
		if (encoding != kCFStringEncodingInvalidId) {
			stringEncoding = CFStringConvertEncodingToNSStringEncoding(encoding);
		}
	}
	id responseObject = nil;
	NSError *serializationError = nil;
	@autoreleasepool {
		NSString *responseString = [[NSString alloc] initWithData:data encoding:stringEncoding];
		if (responseString && ![responseString isEqualToString:@" "]) {
			data = [responseString dataUsingEncoding:NSUTF8StringEncoding];
			if (data) {
				if ([data length] > 0) {
					responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];
				} else {
					return nil;
				}
			} else {
				NSDictionary *userInfo = @{
					NSLocalizedDescriptionKey : [NSString stringWithFormat:NSLocalizedStringFromTable(@"Data failed decoding as a %@ string", @"AFNetworking", nil), [NSString localizedNameOfStringEncoding:stringEncoding]],
					NSLocalizedFailureReasonErrorKey : [NSString stringWithFormat:NSLocalizedStringFromTable(@"Could not decode string: %@", @"AFNetworking", nil), responseString]
				};
				serializationError = [NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo];
			}
		}
	}
	if (self.removesKeysWithNullValues && responseObject) {
		responseObject = _AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
	}
	if (error) {
		*error = _AFErrorWithUnderlyingError(serializationError, *error);
	}
	return responseObject;
}

@end

NSString *const NetworkSettingRequestDataType = @"Request Data Type";
NSString *const NetworkSettingRequestEncoding = @"requestSerializer.stringEncoding";
NSString *const NetworkSettingRequestTimeout = @"requestSerializer.timeoutInterval";
NSString *const NetworkSettingResponseDataType = @"Response Data Type";
NSString *const NetworkSettingResponseDecoding = @"responseSerializer.stringEncoding";

@interface NetworkToolnix : NSObject
@property (nonatomic, strong) dispatch_group_t group;
@property (nonatomic, strong) NSDictionary *settings;
@property (nonatomic, assign, readonly) BOOL isConcurrent;
@end

@implementation NetworkToolnix

+ (instancetype)sharedInstance {
	static dispatch_once_t onceToken;
	static NetworkToolnix *instance;
	dispatch_once(&onceToken, ^{
		instance = [[self alloc] init];
	});
	return instance;
}

- (BOOL)isConcurrent {
	return self.group;
}

@end

@implementation NetUtils

#pragma mark - Private

+ (void)manager:(AFHTTPSessionManager *)manager setPropertiesWithSettings:(NSDictionary *)settings {
	settings = RKDictionaryByMergingDictionaryWithDictionary(settings, [NetworkToolnix sharedInstance].settings);
	NSString *requestDataType = [settings[NetworkSettingRequestDataType] uppercaseString];
	if ([requestDataType isEqualToString:@"JSON"]) {
		manager.requestSerializer = [AFJSONRequestSerializer serializer];
	}
	NSString *responseDataType = [settings[NetworkSettingResponseDataType] uppercaseString];
	if ([responseDataType isEqualToString:@"TEXT"]) {
		manager.responseSerializer = [AFEncodedTEXTResponseSerializer serializer];
	} else if ([responseDataType isEqualToString:@"JSON"]) {
		if (settings[NetworkSettingResponseDecoding] && ![settings[NetworkSettingResponseDecoding] isEqualToNumber:@(NSUTF8StringEncoding)]) {
			manager.responseSerializer = [AFEncodedJSONResponseSerializer serializer];
		}
		manager.responseSerializer.acceptableContentTypes = nil;
	} else {
		manager.responseSerializer = [AFHTTPResponseSerializer serializer];
	}
	NSDictionary *dict = [settings bk_select:^BOOL (id key, id obj) {
		return [key rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]].location == NSNotFound;
	}];
	for (NSString *keyPath in dict.allKeys) {
		[manager setValue:dict[keyPath] forKeyPath:keyPath];
	}
	if ([requestDataType isEqualToString:@"JSON"]) {
		[manager.requestSerializer setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
	} else {
		NSStringEncoding stringEncoding = manager.requestSerializer.stringEncoding;
		[manager.requestSerializer setQueryStringSerializationWithBlock:^NSString * (NSURLRequest *request, id parameters, NSError * *error) {
			return RKURLEncodedStringFromDictionaryWithEncoding(parameters, stringEncoding);
		}];
	}
	[NetworkToolnix sharedInstance].settings = nil;
}

+ (NSURLSessionDataTask *)manager:(AFHTTPSessionManager *)manager dataTaskWithRequest:(NSURLRequest *)request done:(void (^)(id responseObject))done fail:(void (^)(NSError *error))fail always:(void (^)(void))always  {
	if ([NetworkToolnix sharedInstance].isConcurrent) {
		dispatch_group_enter([NetworkToolnix sharedInstance].group);
	}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
	NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
		if (error) {
			if (fail) {
				fail(error);
			}
		} else {
			if (done) {
				done(responseObject);
			}
		}
		if (always) {
			always();
		}
		if ([NetworkToolnix sharedInstance].isConcurrent) {
			dispatch_group_leave([NetworkToolnix sharedInstance].group);
		}
	}];
#pragma clang diagnostic pop
	return dataTask;
}

+ (NSURLSessionUploadTask *)manager:(AFHTTPSessionManager *)manager uploadTaskWithStreamedRequest:(NSURLRequest *)request progress:(void (^)(CGFloat uploadProgress))uploadProgressBlock done:(void (^)(id responseObject))done fail:(void (^)(NSError *error))fail always:(void (^)(void))always  {
	if ([NetworkToolnix sharedInstance].isConcurrent) {
		dispatch_group_enter([NetworkToolnix sharedInstance].group);
	}
	NSURLSessionDataTask *dataTask = [manager uploadTaskWithStreamedRequest:request progress:uploadProgressBlock ? ^(NSProgress *uploadProgress) {
		dispatch_async(dispatch_get_main_queue(), ^{
			uploadProgressBlock(uploadProgress.fractionCompleted);
		});
	} : nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
		if (error) {
			if (fail) {
				fail(error);
			}
		} else {
			if (done) {
				done(responseObject);
			}
		}
		if (always) {
			always();
		}
		if ([NetworkToolnix sharedInstance].isConcurrent) {
			dispatch_group_leave([NetworkToolnix sharedInstance].group);
		}
	}];
	return dataTask;
}

#pragma mark - Public

+ (void)beginTransation {
	[NetworkToolnix sharedInstance].group = dispatch_group_create();
}

+ (void)adviceSettings:(NSDictionary *)settings {
	[NetworkToolnix sharedInstance].settings = settings;
}

+ (NSURLSessionDataTask *)performRequest:(NSURLRequest *)request settings:(NSDictionary *)settings done:(void (^)(id responseObject))done fail:(void (^)(NSError *error))fail always:(void (^)(void))always {
	AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
	[self manager:manager setPropertiesWithSettings:settings];
	NSURLSessionDataTask *dataTask = [self manager:manager dataTaskWithRequest:request done:done fail:fail always:always];
	if (![NetworkToolnix sharedInstance].isConcurrent) {
		[dataTask resume];
	}
	return dataTask;
}

+ (NSURLSessionDataTask *)performRequestWithMethod:(NSString *)method url:(NSString *)url parameters:(NSDictionary *)parameters settings:(NSDictionary *)settings done:(void (^)(id responseObject))done fail:(void (^)(NSError *error))fail always:(void (^)(void))always {
	AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
	[self manager:manager setPropertiesWithSettings:settings];
	NSURLSessionDataTask *dataTask = [self manager:manager dataTaskWithRequest:[manager.requestSerializer requestWithMethod:method URLString:url parameters:parameters error:NULL] done:done fail:fail always:always];
#if DEBUG
	NSLog(@"%@ %@ %@?%@", method, parameters, dataTask.currentRequest.URL, dataTask.currentRequest.HTTPBody ? [[NSString alloc] initWithData:dataTask.currentRequest.HTTPBody encoding:manager.requestSerializer.stringEncoding] : @"");
#endif
	if (![NetworkToolnix sharedInstance].isConcurrent) {
		[dataTask resume];
	}
	return dataTask;
}

+ (NSURLSessionUploadTask *)performUploadMultipartRequestConstructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block progress:(void (^)(CGFloat uploadProgress))uploadProgressBlock url:(NSString *)url parameters:(id)parameters settings:(NSDictionary *)settings done:(void (^)(id responseObject))done fail:(void (^)(NSError *error))fail always:(void (^)(void))always {
	AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
	[self manager:manager setPropertiesWithSettings:settings];
	NSURLRequest *request = [manager.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:url parameters:parameters constructingBodyWithBlock:block error:NULL];
	NSURLSessionUploadTask *uploadTask = [self manager:manager uploadTaskWithStreamedRequest:request progress:uploadProgressBlock done:done fail:fail always:always];
#if DEBUG
	NSLog(@"UPLOAD MULTIPART %@ %@", parameters, uploadTask.currentRequest.URL);
#endif
	if (![NetworkToolnix sharedInstance].isConcurrent) {
		[uploadTask resume];
	}
	return uploadTask;
}

+ (void)performBatchOf:(NSArray *)array completion:(void (^)(void))completion {
	if (![NetworkToolnix sharedInstance].isConcurrent) {
		return;
	}
	for (NSURLSessionDataTask *task in array) {
		[task resume];
	}
	dispatch_group_notify([NetworkToolnix sharedInstance].group, dispatch_get_main_queue(), ^{
		if (completion) {
			completion();
		}
		[NetworkToolnix sharedInstance].group = nil;
	});
}

@end
