//
//  HTTP.m
//  Afficheur
//
//  Created by kichi on 08/04/06.
//  Copyright 2008 Katsuhiko Ichinose. All rights reserved.
//

#import "HTTP.h"
#import "NSStringOgreKit.h"
#import <openssl/bio.h>
#import <openssl/buffer.h>
#import <openssl/evp.h>
#import <OgreKit/OgreKit.h>
#import <JSON/JSON.h>

static NSString *ReqKeyReq		= @"request";
static NSString *ReqKeyDelegate	= @"delegate";

@implementation HTTP

+ (NSString *)base64String:(NSString *)string
{
	const char *cstr = [string cStringUsingEncoding:NSASCIIStringEncoding];
	BIO *b64 = BIO_new(BIO_f_base64());
	BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
	BIO *bio = BIO_new(BIO_s_mem());
	bio = BIO_push(b64, bio);
	BIO_write(bio, cstr, strlen(cstr));
	BIO_flush(bio);
	BUF_MEM *ptr;
	BIO_get_mem_ptr(b64, &ptr);
	NSString *res = [NSString stringWithCString:ptr->data length:ptr->length];
	BIO_free_all(bio);
	return res;
}

+ (NSString *)base64StringWithBytes:(const char *)cstr
							 length:(int)length
{
	BIO *b64 = BIO_new(BIO_f_base64());
	BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
	BIO *bio = BIO_new(BIO_s_mem());
	bio = BIO_push(b64, bio);
	BIO_write(bio, cstr, length);
	BIO_flush(bio);
	BUF_MEM *ptr;
	BIO_get_mem_ptr(b64, &ptr);
	NSString *res = [NSString stringWithCString:ptr->data length:ptr->length];
	BIO_free_all(bio);
	return res;
}

+ (NSString *)stringWithBase64String:(NSString *)string
{
	const char *cstr = [string cStringUsingEncoding:NSASCIIStringEncoding];
	int len = strlen(cstr);
	char *buf = malloc(len);
	memset(buf, 0, len);
	BIO *b64 = BIO_new(BIO_f_base64());
	BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
	BIO *bio = BIO_new_mem_buf((char *)cstr, len);
	bio = BIO_push(b64, bio);
	BIO_read(bio, buf, len);
	BIO_free_all(bio);
	NSString *res = [NSString stringWithCString:buf length:strlen(buf)];
	return res;
}

+ (NSString *)urlEncode:(NSString *)string
{
	return (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
															   (CFStringRef)string,
															   NULL,
															   NULL,
															   kCFStringEncodingUTF8);
}

+ (NSString *)urlEncode:(NSString *)string
		   withEncoding:(CFStringEncoding)encoding
{
	return (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
															   (CFStringRef)string,
															   NULL,
															   NULL,
															   encoding);
}

+ (NSString *)urlEncode:(NSString *)string
			  unescaped:(NSString *)unescaped
				escaped:(NSString *)escaped
{
	return (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
															   (CFStringRef)string,
															   (CFStringRef)unescaped,
															   (CFStringRef)escaped,
															   kCFStringEncodingUTF8);
}

+ (NSString *)urlEncode:(NSString *)string
			  unescaped:(NSString *)unescaped
				escaped:(NSString *)escaped
			   encoding:(CFStringEncoding)encoding
{
	return (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
															   (CFStringRef)string,
															   (CFStringRef)unescaped,
															   (CFStringRef)escaped,
															   encoding);
}

+ (NSString *)urlDencode:(NSString *)string
{
	return (NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault,
																			   (CFStringRef)string,
																			   CFSTR(""),
																			   kCFStringEncodingUTF8);
}

+ (BOOL)isHTML:(NSString *)string
{
	if (([string lengthOfRegularExpression:@"^<\\?xml.*\n<!DOCTYPE HTML"
							   withOptions:OgreIgnoreCaseOption | OgreMultilineOption]) ||
		([string lengthOfRegularExpression:@"^<!DOCTYPE HTML"
							   withOptions:OgreIgnoreCaseOption | OgreMultilineOption]) ||
		([string lengthOfRegularExpression:@"^<HTML"
							   withOptions:OgreIgnoreCaseOption | OgreMultilineOption]) ||
		([string lengthOfRegularExpression:@"^\\s+<\\?xml.*\n<!DOCTYPE HTML"
							   withOptions:OgreIgnoreCaseOption | OgreMultilineOption]) ||
		([string lengthOfRegularExpression:@"^\\s+<!DOCTYPE HTML"
							   withOptions:OgreIgnoreCaseOption | OgreMultilineOption]) ||
		([string lengthOfRegularExpression:@"^\\s+<HTML"
							   withOptions:OgreIgnoreCaseOption | OgreMultilineOption]))
	{
		return YES;
	}
	return NO;
}

- (void)getMainThread:(id)object
{
	_mainThread = [NSThread currentThread];
}

- (HTTP *)init
{
	self = (HTTP *)[super init];
	if (self)
	{
		_connection = nil;
		_statusCode = 0;
		_headers = nil;
		_locked = NO;
		_lock = [[NSLock alloc] init];
		_timeout = -1;
		_error = nil;
		_data = nil;
		_redirect = YES;
		_shouldHandleCookies = NO;
		[self setTimeoutInterval:30];
		[self performSelectorOnMainThread:@selector(getMainThread:)
							   withObject:nil
							waitUntilDone:YES];
	}
	return self;
}

- (void)dealloc
{
	if (_data)
	{
		[_data release];
	}
	if (_connection)
	{
		[_connection release];
	}
	if (_headers)
	{
		[_headers release];
	}
	if (_error)
	{
		[_error release];
	}
	if (_lock)
	{
		[_lock release];
	}
	[super dealloc];
}

- (void)setTimeoutInterval:(double)timeout
{
	_timeout = timeout;
}

- (void)setRedirect:(BOOL)redirect
{
	_redirect = redirect;
}

- (BOOL)redirect
{
	return _redirect;
}

- (void)setShouldHandleCookies:(BOOL)shouldHandleCookies
{
	_shouldHandleCookies = shouldHandleCookies;
}

- (BOOL)shouldHandleCookies
{
	return _shouldHandleCookies;
}

- (int)statusCode
{
	return _statusCode;
}

- (NSDictionary *)headers
{
	return _headers;
}

- (void)performLock:(id)object
{
	@synchronized(_lock)
	{
		if (!_locked)
		{
			_locked = YES;
			[_lock lock];
		}
	}
}

- (void)lock
{
	if ([[NSThread currentThread] isEqual:_mainThread])
	{
		[self performLock:nil];
	}
	else
	{
		[self performSelectorOnMainThread:@selector(performLock:)
							   withObject:nil
							waitUntilDone:YES];
	}
}

- (void)performUnlock:(id)object
{
	@synchronized(_lock)
	{
		if (_locked)
		{
			_locked = NO;
			[_lock unlock];
		}
	}
}

- (void)unlock
{
	if ([[NSThread currentThread] isEqual:_mainThread])
	{
		[self performUnlock:nil];
	}
	else
	{
		[self performSelectorOnMainThread:@selector(performUnlock:)
							   withObject:nil
							waitUntilDone:YES];
	}
}

- (void)performRequest:(NSURLRequest *)req
{
	if (_connection)
	{
		[_connection release];
	}
	_connection = [[NSURLConnection alloc] initWithRequest:req delegate:self];
}

- (void)performRequestWithDelegate:(NSDictionary *)dic
{
	NSURLRequest *req = [dic valueForKey:ReqKeyReq];
	id delegate = [dic valueForKey:ReqKeyDelegate];
	if (_connection)
	{
		[_connection release];
	}
	_connection = [[NSURLConnection alloc] initWithRequest:req delegate:delegate];
}

- (void)addHTTPHeader:(NSMutableURLRequest *)req
{
	[req addValue:@"Afficheur" forHTTPHeaderField:@"User-Agent"];
}
																
- (id)post:(NSString *)path
	header:(NSDictionary *)header
	  body:(NSString *)body
{
	//LOG(@"[%@ post] body\n%@", [self className], body);
	//LOG(@"[%@ post]\npath: %@\nheader: %@\nbody: %@", [self className], path, header, body);
	NSURL *url = [NSURL URLWithString:path];
	NSMutableURLRequest *req = [[[NSMutableURLRequest alloc] initWithURL:url] autorelease];
	NSEnumerator *enumerator = [header keyEnumerator];
	id key;
	while (key = [enumerator nextObject])
	{
		[req addValue:[header valueForKey:key] forHTTPHeaderField:key];
	}
	[self addHTTPHeader:req];
	[req setHTTPMethod:@"POST"];
	[req setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
	[req setCachePolicy:NSURLRequestReloadIgnoringCacheData];
	[req setHTTPShouldHandleCookies:_shouldHandleCookies];
	if (_timeout > 0)
	{
		[req setTimeoutInterval:_timeout];
	}
	_statusCode = 0;
	if (_error)
	{
		[_error release];
	}
	_error = nil;
	if (_data)
	{
		[_data release];
	}
	_data = [[NSMutableData alloc] init];
	[self lock];
	[self performSelectorOnMainThread:@selector(performRequest:)
						   withObject:req
						waitUntilDone:YES];
	[_lock lock];
	[_lock unlock];
	if (_error)
	{
		return _error;
	}
	if (_data)
	{
		return [NSData dataWithData:_data];
	}
	return _data;
}

- (id)get:(NSString *)path
   header:(NSDictionary *)header
	 body:(NSString *)body
 delegate:(id)delegate
{
	//LOG(@"[%@ get]\n%@", [self className], path);
	NSURL *url = [NSURL URLWithString:path];
	NSMutableURLRequest *req = [[[NSMutableURLRequest alloc] initWithURL:url] autorelease];
	NSEnumerator *enumerator = [header keyEnumerator];
	id key;
	while (key = [enumerator nextObject])
	{
		[req addValue:[header valueForKey:key] forHTTPHeaderField:key];
	}
	[self addHTTPHeader:req];
	[req setHTTPMethod:@"GET"];
	[req setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
	[req setCachePolicy:NSURLRequestReloadIgnoringCacheData];
	[req setHTTPShouldHandleCookies:_shouldHandleCookies];
	if (_timeout > 0)
	{
		[req setTimeoutInterval:_timeout];
	}
	_statusCode = 0;
	if (_error)
	{
		[_error release];
	}
	_error = nil;
	if (_data)
	{
		[_data release];
	}
	_data = [[NSMutableData alloc] init];
	if (delegate)
	{
		[self performSelectorOnMainThread:@selector(performRequestWithDelegate:)
							   withObject:[NSDictionary dictionaryWithObjectsAndKeys:
										   req, ReqKeyReq,
										   delegate, ReqKeyDelegate,
										   nil]
							waitUntilDone:YES];
		return _connection;
	}
	[self lock];
	[self performSelectorOnMainThread:@selector(performRequest:)
						   withObject:req
						waitUntilDone:YES];
	[_lock lock];
	[_lock unlock];
	if (_error)
	{
		return _error;
	}
	//LOG(@"[%@ get] done\n%@", [self className], path);
	if (_data)
	{
		return [NSData dataWithData:_data];
	}
	return _data;
}

- (id)get:(NSString *)path
   header:(NSDictionary *)header
	 body:(NSString *)body
{
	return [self get:path
			  header:header
				body:body
			delegate:nil];
}

- (id)head:(NSString *)path
	header:(NSDictionary *)header
	  body:(NSString *)body
{
	//LOG(@"[%@ head]\n%@", [self className], path);
	NSURL *url = [NSURL URLWithString:path];
	NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:url];
	NSEnumerator *enumerator = [header keyEnumerator];
	id key;
	while (key = [enumerator nextObject])
	{
		[req addValue:[header valueForKey:key] forHTTPHeaderField:key];
	}
	[self addHTTPHeader:req];
	[req setHTTPMethod:@"HEAD"];
	[req setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
	[req setCachePolicy:NSURLRequestReloadIgnoringCacheData];
	[req setHTTPShouldHandleCookies:_shouldHandleCookies];
	if (_timeout > 0)
	{
		[req setTimeoutInterval:_timeout];
	}
	_statusCode = 0;
	if (_error)
	{
		[_error release];
	}
	_error = nil;
	if (_data)
	{
		[_data release];
	}
	_data = [[NSMutableData alloc] init];
	[self lock];
	[self performSelectorOnMainThread:@selector(performRequest:)
						   withObject:req
						waitUntilDone:YES];
	[_lock lock];
	[_lock unlock];
	if (_error)
	{
		return _error;
	}
	//LOG(@"[%@ head]done\n%@", [self className], path);
	return [NSDictionary dictionaryWithDictionary:[self headers]];
}

- (NSString *)titleOfHTML:(NSString *)string
{
	if ([HTTP isHTML:string])
	{
		return [NSString stringWithString:
				[string replaceWithExpression:@"^.*<TITLE>(.*?)</TITLE>.*$"
									  replace:@"\\1"
									  options:OgreIgnoreCaseOption | OgreMultilineOption]];
	}
	return nil;
}

- (id)determineError:(id)object
			WithJSON:(BOOL)json
{
	id result = @"";
	if ([object isKindOfClass:[NSError class]])
	{
		return object;
	}
	else
	{
		NSString* data = [[[NSString alloc] initWithData:object
												encoding:NSUTF8StringEncoding] autorelease];
		if (_statusCode >= 400)
		{
			result = [NSString stringWithFormat:@"%d : %@", _statusCode,
					  [NSHTTPURLResponse localizedStringForStatusCode:_statusCode]];
			if ([HTTP isHTML:data])
			{
				NSString *title = [self titleOfHTML:data];
				if (title)
				{
					result = [NSString stringWithFormat:@"%@\n%@", result, title];
				}
			}
			else
			{
				if (json)
				{
					@try
					{
						if (![data isEqualToString:@""])
						{
							data = [data replaceWithExpression:@",\\]$"
													   replace:@"]"
													   options:OgreIgnoreCaseOption | OgreMultilineOption];
							data = [data replaceWithExpression:@"\"title\": ,"
													   replace:@"\"title\": \"\","
													   options:OgreIgnoreCaseOption | OgreMultilineOption];
							NSDictionary *json_data = nil;
							@synchronized([NSApplication sharedApplication])
							{
								json_data = [data JSONValue];
							}
							if (json_data)
							{
								NSMutableDictionary *json_dic = [NSMutableDictionary dictionaryWithDictionary:json_data];
								[json_dic setValue:result forKey:@"http_status_code"];
								result = json_dic;
							}
						}
						else
						{
							result = [NSString stringWithFormat:@"%@\n%@", result, data];
						}
					}
					@catch (NSException *exception)
					{
						EXPLOG(@"[%@ determineErrorWithJSON] EXCEPTION\n%@: %@", [self className], [exception name], [exception reason]);
						result = [NSString stringWithFormat:@"%@\n%@", result, data];
					}
				}
				else
				{
					result = [NSString stringWithFormat:@"%@\n%@", result, data];
				}
			}
		}
		else if (json)
		{
			@try
			{
				if (![data isEqualToString:@""])
				{
					data = [data replaceWithExpression:@",\\]$"
											   replace:@"]"
											   options:OgreIgnoreCaseOption | OgreMultilineOption];
					data = [data replaceWithExpression:@"\"title\": ,"
											   replace:@"\"title\": \"\","
											   options:OgreIgnoreCaseOption | OgreMultilineOption];
					@synchronized([NSApplication sharedApplication])
					{
						result = [data JSONValue];
					}
				}
			}
			@catch (NSException *exception)
			{
				EXPLOG(@"[%@ determineErrorWithJSON] EXCEPTION\n%@: %@", [self className], [exception name], [exception reason]);
				if ([HTTP isHTML:data])
				{
					result = [NSString stringWithString:data];
				}
				else
				{
					EXPLOG(@"\"%@\"", data);
					result = [NSString stringWithFormat:@"%@: %@", [exception name], [exception reason]];
				}
			}
		}
		else
		{
			result = [NSString stringWithString:data];
		}
	}
	return result;
}

- (NSString *)errorLocalizedDescription:(NSError *)error
{
	NSDictionary *dic = [error userInfo];
	NSString *desc = [dic valueForKey:@"NSLocalizedDescription"];
	if (desc && [desc isKindOfClass:[NSString class]] && ![desc isEqualToString:@""])
	{
		return [NSString stringWithString:desc];
	}
	return nil;
}

- (NSURLRequest *)connection:(NSURLConnection *)connection
			 willSendRequest:(NSURLRequest *)request
			redirectResponse:(NSURLResponse *)redirectResponse
{
	if (redirectResponse && [redirectResponse isKindOfClass:[NSHTTPURLResponse class]])
	{
		NSHTTPURLResponse *response = (NSHTTPURLResponse *)redirectResponse;
		if (!_redirect) //|| [response statusCode] == 301)
		{
			_statusCode = [response statusCode];
			_headers = [[NSDictionary alloc] initWithDictionary:[response allHeaderFields]];
			//LOG(@"[%@ connection:willSendRequest:redirectResponse:]\n%@\n%@\n%@\n%d:%@\n%@", [self className], connection, request, redirectResponse, _statusCode, [NSHTTPURLResponse localizedStringForStatusCode:_statusCode], _headers);
			[self connectionDidFinishLoading:connection];
			return nil;
		}
	}
	return request;
}

- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response
{
	LOG(@"[%@ didReceiveResponse] %@", [self className], [response className]);
	if (![response isKindOfClass:[NSHTTPURLResponse class]])
	{
		EXPLOG(@"[%@ didReceiveResponse] Unknown response type:%@", [self className], response);
	}
	else
	{
		_statusCode = [(NSHTTPURLResponse *)response statusCode];
		_headers = [[NSDictionary alloc] initWithDictionary:[(NSHTTPURLResponse *)response allHeaderFields]];
		//LOG(@"[%@ didReceiveResponse] %d:%@\n%@", [self className], _statusCode, [NSHTTPURLResponse localizedStringForStatusCode:_statusCode], _headers);
	}
}

- (void)connection:(NSURLConnection *)connection
	didReceiveData:(NSData *)data
{
	LOG(@"[%@ didReceiveData] %@", [self className], [data className]);
	[_data appendData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
	LOG(@"[%@ connectionDidFinishLoading] %@", [self className], [connection className]);
	[self unlock];
}

- (void)connection:(NSURLConnection *)connection
  didFailWithError:(NSError *)error
{
	_error = [[NSError alloc] initWithDomain:[error domain]
										code:[error code]
									userInfo:[error userInfo]];
	LOG(@"[%@ didFailWithError] %@", [self className], _error);
	[self unlock];
}

@end
