#include "WWWConnection.h"

// WARNING: this MUST be c decl (NSString ctor will be called after +load, so we cant really change its value)

// If you need to communicate with HTTPS server with self signed certificate you might consider UnityWWWConnectionSelfSignedCertDelegate
// Though use it on your own risk. Blindly accepting self signed certificate is prone to MITM attack

//const char* WWWDelegateClassName      = "UnityWWWConnectionSelfSignedCertDelegate";
const char* WWWDelegateClassName        = "UnityWWWConnectionDelegate";
const char* WWWRequestProviderClassName = "UnityWWWRequestDefaultProvider";
static NSOperationQueue *webOperationQueue;

@interface UnityWWWConnectionDelegate ()
@property (readwrite, nonatomic) void*                         udata;
@property (readwrite, retain, nonatomic) NSURL*                url;
@property (readwrite, retain, nonatomic) NSString*             user;
@property (readwrite, retain, nonatomic) NSString*             password;
@property (readwrite, retain, nonatomic) NSMutableURLRequest*  request;
@property (readwrite, retain, nonatomic) NSURLConnection*      connection;
@property (nonatomic)                    BOOL                  manuallyHandleRedirect;
@property (readwrite, retain, nonatomic) NSOutputStream*       outputStream;
@end


@implementation UnityWWWConnectionDelegate
{
    // link to unity WWW implementation
    void*               _udata;

    // connection parameters
    NSMutableURLRequest* _request;
    // connection that we manage
    NSURLConnection*    _connection;

    // NSURLConnection do not quite handle user:pass@host urls
    // so we need to extract user/pass ourselves
    NSURL*              _url;
    NSString*           _user;
    NSString*           _password;

    // response
    NSInteger           _status;
    size_t              _estimatedLength;
    size_t              _dataRecievd;
    int                 _retryCount;
    NSOutputStream*     _outputStream;
}

@synthesize url         = _url;
@synthesize user        = _user;
@synthesize password    = _password;
@synthesize request     = _request;
@synthesize connection  = _connection;

@synthesize udata       = _udata;
@synthesize outputStream = _outputStream;

- (NSURL*)extractUserPassFromUrl:(NSURL*)url
{
    self.user       = url.user;
    self.password   = url.password;

    // strip user/pass from url
    NSString* newUrl = [NSString stringWithFormat: @"%@://%@%s%s%@%s%s",
                        url.scheme, url.host,
                        url.port ? ":" : "", url.port ? [[url.port stringValue] UTF8String] : "",
                        url.path,
                        url.fragment ? "#" : "", url.fragment ? [url.fragment UTF8String] : ""
        ];
    return [NSURL URLWithString: newUrl];
}

- (id)initWithURL:(NSURL*)url udata:(void*)udata;
{
    self->_retryCount = 0;
    if ((self = [super init]))
    {
        self.url    = url.user != nil ? [self extractUserPassFromUrl: url] : url;
        self.udata  = udata;

        if ([url.scheme caseInsensitiveCompare: @"http"] == NSOrderedSame)
            NSLog(@"You are using download over http. Currently Unity adds NSAllowsArbitraryLoads to Info.plist to simplify transition, but it will be removed soon. Please consider updating to https.");
    }

    return self;
}

+ (id)newDelegateWithURL:(NSURL*)url udata:(void*)udata
{
    Class target = NSClassFromString([NSString stringWithUTF8String: WWWDelegateClassName]);
    NSAssert([target isSubclassOfClass: [UnityWWWConnectionDelegate class]], @"You MUST subclass UnityWWWConnectionDelegate");

    return [[target alloc] initWithURL: url udata: udata];
}

+ (id)newDelegateWithCStringURL:(const char*)url udata:(void*)udata
{
    return [UnityWWWConnectionDelegate newDelegateWithURL: [NSURL URLWithString: [NSString stringWithUTF8String: url]] udata: udata];
}

+ (NSMutableURLRequest*)newRequestForHTTPMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers
{
    Class target = NSClassFromString([NSString stringWithUTF8String: WWWRequestProviderClassName]);
    NSAssert([target conformsToProtocol: @protocol(UnityWWWRequestProvider)], @"You MUST implement UnityWWWRequestProvider protocol");

    return [target allocRequestForHTTPMethod: method url: url headers: headers];
}

- (void)abort
{
    [self.connection cancel];
}

- (void)cleanup
{
    [_connection cancel];
    _connection = nil;
    _request = nil;
}

// NSURLConnection Delegate Methods
- (NSURLRequest *)connection:(NSURLConnection *)connection
    willSendRequest:(NSURLRequest *)request
    redirectResponse:(NSURLResponse *)response;
{
    if (response && self.manuallyHandleRedirect)
    {
        // notify TransportiPhone of the redirect and signal to process the next response.
        if ([response isKindOfClass: [NSHTTPURLResponse class]])
        {
            NSHTTPURLResponse *httpresponse = (NSHTTPURLResponse*)response;
            NSMutableDictionary *headers = [httpresponse.allHeaderFields mutableCopy];
            // grab the correct URL from the request that would have
            // automatically been called through NSURLConnection.
            // The reason we do this is that WebRequestProto's state needs to
            // get updated internally, so we intercept redirects, cancel the current
            // NSURLConnection, notify WebRequestProto and let it construct a new
            // request from the updated URL
            [headers setObject: [request.URL absoluteString] forKey: @"Location"];
            httpresponse = [[NSHTTPURLResponse alloc] initWithURL: response.URL statusCode: httpresponse.statusCode HTTPVersion: nil headerFields: headers];
            [self handleResponse: httpresponse];
        }
        else
        {
            [self handleResponse: response];
        }
        [connection cancel];
        return nil;
    }
    return request;
}

- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response
{
    [self handleResponse: response];
}

- (void)handleResponse:(NSURLResponse*)response
{
    NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
    NSDictionary* respHeader = [httpResponse allHeaderFields];
    NSEnumerator* headerEnum = [respHeader keyEnumerator];

    self->_status = [httpResponse statusCode];
    UnityReportWWWStatus(self.udata, (int)self->_status);

    for (id headerKey = [headerEnum nextObject]; headerKey; headerKey = [headerEnum nextObject])
        UnityReportWWWResponseHeader(self.udata, [headerKey UTF8String], [[respHeader objectForKey: headerKey] UTF8String]);

    long long contentLength = [response expectedContentLength];

    // ignore any data that we might have recieved during a redirect
    self->_estimatedLength  =  contentLength > 0 && (self->_status / 100 != 3) ? contentLength : 0;
    self->_dataRecievd = 0;
    UnityReportWWWReceivedResponse(self.udata, (unsigned int)self->_estimatedLength);
}

- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
{
    UnityReportWWWReceivedData(self.udata, data.bytes, (unsigned int)[data length], (unsigned int)self->_estimatedLength);
}

- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
{
    UnityReportWWWNetworkError(self.udata, (int)[error code]);
    UnityReportWWWFinishedLoadingData(self.udata);
}

- (void)connectionDidFinishLoading:(NSURLConnection*)connection
{
    UnityReportWWWFinishedLoadingData(self.udata);
}

- (void)connection:(NSURLConnection*)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite
{
    UnityReportWWWSentData(self.udata, (unsigned int)totalBytesWritten, (unsigned int)totalBytesExpectedToWrite);
    if (_outputStream != nil)
    {
        unsigned dataSize;
        const UInt8* bytes = (const UInt8*)UnityWWWGetUploadData(_udata, &dataSize);
        unsigned transmitted = [_outputStream write: bytes maxLength: dataSize];
        UnityWWWConsumeUploadData(_udata, transmitted);
        if (transmitted >= dataSize)
        {
            [_outputStream close];
            _outputStream = nil;
        }
    }
}

- (BOOL)connection:(NSURLConnection*)connection handleAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
{
    return NO;
}

- (void)connection:(NSURLConnection*)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
{
    if ([[challenge protectionSpace] authenticationMethod] == NSURLAuthenticationMethodServerTrust)
    {
        [challenge.sender performDefaultHandlingForAuthenticationChallenge: challenge];
    }
    else
    {
        BOOL authHandled = [self connection: connection handleAuthenticationChallenge: challenge];

        if (authHandled == NO)
        {
            self->_retryCount++;

            // Empty user or password
            if (self->_retryCount > 1 || self.user == nil || [self.user length] == 0 || self.password == nil || [self.password length]  == 0)
            {
                [[challenge sender] cancelAuthenticationChallenge: challenge];
                return;
            }

            NSURLCredential* newCredential =
                [NSURLCredential credentialWithUser: self.user password: self.password persistence: NSURLCredentialPersistenceNone];

            [challenge.sender useCredential: newCredential forAuthenticationChallenge: challenge];
        }
    }
}

@end


@implementation UnityWWWConnectionSelfSignedCertDelegate

- (BOOL)connection:(NSURLConnection*)connection handleAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
{
    if ([[challenge.protectionSpace authenticationMethod] isEqualToString: @"NSURLAuthenticationMethodServerTrust"])
    {
        [challenge.sender useCredential: [NSURLCredential credentialForTrust: challenge.protectionSpace.serverTrust]
         forAuthenticationChallenge: challenge];

        return YES;
    }

    return [super connection: connection handleAuthenticationChallenge: challenge];
}

@end


@implementation UnityWWWRequestDefaultProvider
+ (NSMutableURLRequest*)allocRequestForHTTPMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers
{
    NSMutableURLRequest* request = [[NSMutableURLRequest alloc] init];
    [request setURL: url];
    [request setHTTPMethod: method];
    [request setAllHTTPHeaderFields: headers];

    return request;
}

@end

//
// unity interface
//

extern "C" void UnitySendWWWConnection(void* connection, const void* data, unsigned length, bool blockImmediately, unsigned long timeoutSec)
{
    UnityWWWConnectionDelegate* delegate = (__bridge UnityWWWConnectionDelegate*)connection;

    NSMutableURLRequest* request = delegate.request;

    if (length > 0)
    {
        if (data != nil)
        {
            [request setHTTPBody: [NSData dataWithBytes: data length: length]];
            [request setValue: [NSString stringWithFormat: @"%d", length] forHTTPHeaderField: @"Content-Length"];
        }
        else
        {
            CFReadStreamRef readStream;
            CFWriteStreamRef writeStream;
            CFStreamCreateBoundPair(kCFAllocatorDefault, &readStream, &writeStream, 1024);
            [request setHTTPBodyStream: (__bridge NSInputStream*)readStream];
            [request setValue: @"chunked" forHTTPHeaderField: @"Transfer-Encoding"];

            CFWriteStreamOpen(writeStream);
            unsigned dataSize;
            const void* bytes = UnityWWWGetUploadData(delegate.udata, &dataSize);
            unsigned transmitted = CFWriteStreamWrite(writeStream, (UInt8*)bytes, dataSize);
            UnityWWWConsumeUploadData(delegate.udata, transmitted);
            if (transmitted >= dataSize)
                CFWriteStreamClose(writeStream);
            else
                delegate.outputStream = (__bridge NSOutputStream*)writeStream;
        }
    }

    [request setTimeoutInterval: timeoutSec];

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        webOperationQueue = [[NSOperationQueue alloc] init];
        webOperationQueue.maxConcurrentOperationCount = [NSProcessInfo processInfo].activeProcessorCount * 5;
        webOperationQueue.name = @"com.unity3d.WebOperationQueue";
    });

    delegate.connection = [[NSURLConnection alloc] initWithRequest: request delegate: delegate startImmediately: NO];
    delegate.manuallyHandleRedirect = YES;
    [delegate.connection setDelegateQueue: webOperationQueue];
    [delegate.connection start];
}

extern "C" void* UnityStartWWWConnectionCustom(void* udata, const char* methodString, const void* headerDict, const char* url)
{
    UnityWWWConnectionDelegate* delegate = [UnityWWWConnectionDelegate newDelegateWithCStringURL: url udata: udata];

    delegate.request = [UnityWWWConnectionDelegate newRequestForHTTPMethod: [NSString stringWithUTF8String: methodString] url: delegate.url headers: (__bridge NSDictionary*)headerDict];

    return (__bridge_retained void*)delegate;
}

extern "C" bool UnityBlockWWWConnectionIsDone(void* connection)
{
    UnityWWWConnectionDelegate* delegate = (__bridge UnityWWWConnectionDelegate*)connection;
    return (delegate.request == nil);
}

extern "C" void UnityDestroyWWWConnection(void* connection)
{
    UnityWWWConnectionDelegate* delegate = (__bridge_transfer UnityWWWConnectionDelegate*)connection;

    [delegate cleanup];
    delegate = nil;
}

extern "C" void UnityShouldCancelWWW(const void* connection)
{
    UnityWWWConnectionDelegate* delegate = (__bridge UnityWWWConnectionDelegate*)connection;
    [delegate.connection cancel];
}
