// -*- mode: ObjC; tab-width: 8; c-basic-offset: 2; -*-

//

//  CSURLConnection.m

//  Coriolis CDMaker

//

//  Created by Chris Suter on 5/05/06.

//  Copyright 2006 Coriolis Systems Ltd. All rights reserved.

//


/* USAGE:


   To use this class in a WebView (for the purposes of using it in

   a modal window), you need to do the following:


     [CSURLConnection poseAsClass:[NSURLConnection class]];

     [CSURLConnection enableModalSupport];

 

   And when you're done:

 

     [CSURLConnection disableModalSupport];

 

 */


/* What this all does is run the NSURLConnection in a separate thread

   with it's own run loop so that it doesn't matter what mode the run

   loop on the main thread is running with.  Each time the run loop on

   the worker thread is woken up to be processed, we pause the main

   thread.


   The lock is used to determine which thread is allowed to run.  A

   condition of 0 means the main thread is running; 1 means the worker

   thread is running. */


#import "CSURLConnection.h"


@class CSURLConnectionProxy;

@class CSDelegateProxy;


@protocol CSURLConnectionHelperMethods


- (oneway void)wakeUpWorkerThread;

- (oneway void)reallyInitWithRequest;


@end


@interface CSURLConnectionHelper : NSObject <CSURLConnectionHelperMethods> {

  CSURLConnection *urlConnection;

  CSURLConnectionProxy *urlConnectionProxy;

  CSDelegateProxy *delegateProxy;

  NSConditionLock *lock;

  id delegate;

  NSConnection *connection;

  NSInvocation *invocation;

  NSThread *ourThread;

  BOOL sleeping;

  BOOL waitingToPause;

  NSURLRequest *request;


  NSDistantObject<CSURLConnectionHelperMethods> *helperProxy;

}


- (CSURLConnectionHelper *)initWithURLConnection:(CSURLConnection *)theURLConnection

      urlConnectionProxy:(CSURLConnectionProxy *)theURLConnectionProxy

request:(NSURLRequest *)urlRequest

delegate:(id)delegate;

- (void)willWakeup:(BOOL)sendPause;

- (void)willSleep;

- (CSURLConnection *)urlConnection;

- (id)delegate;

@end


@interface CSURLConnection (private)

- (CSURLConnection *)reallyInitWithRequest:(NSURLRequest *)request

  delegate:(id)delegate;

@end


@interface CSDelegateProxy : NSProxy {

  CSURLConnectionHelper *helper;

}


- (CSDelegateProxy *)initWithHelper:(CSURLConnectionHelper *)helper;


@end


@interface CSURLConnectionProxy : NSProxy {

  CSURLConnectionHelper *helper;

}

@end


void runLoopStateChange (CFRunLoopObserverRef observer,

CFRunLoopActivity activity,

void *info)

{

#pragma unused (observer)


  CSURLConnectionHelper *helper = (CSURLConnectionHelper *)info;

  

  if (activity == kCFRunLoopBeforeWaiting)

    [helper willSleep];

  else

    [helper willWakeup:NO];

}


@implementation CSURLConnectionHelper


- (CSURLConnectionHelper *)initWithURLConnection:(CSURLConnection *)theURLConnection

      urlConnectionProxy:(CSURLConnectionProxy *)theURLConnectionProxy

request:(NSURLRequest *)theRequest

delegate:(id)theDelegate

{

  if ((self = [super init])) {

    urlConnection = theURLConnection;

    urlConnectionProxy = theURLConnectionProxy;

    request = theRequest;

    delegate = theDelegate;


    /* We need a mechanism to wake the worker thread up from the main

       thread.  Using Distributed Objects is probably a bit overkill

       for this purpose but nevermind.  On Leopard there are other

       ways of doing it but we need something to work on older

       versions. */

    NSPort *port1 = [NSPort port];

    NSPort *port2 = [NSPort port];

    

    connection = [[NSConnection alloc] initWithReceivePort:port1

  sendPort:port2];

    

    lock = [[NSConditionLock alloc] initWithCondition:1];

    

    delegateProxy = [[CSDelegateProxy alloc] initWithHelper:self];

    

    [NSThread detachNewThreadSelector:@selector (thread)

    toTarget:self

  withObject:nil];

  

    // Wait until the thread has started

    [lock lockWhenCondition:0];

    [lock unlockWithCondition:1];

    

    /* We have to do this before we start the URLConnection because

       otherwise events for NSURLConnection can be processed before

       we've returned the response to the caller. */

    helperProxy = [[connection rootProxy] retain];

    [helperProxy setProtocolForProxy:@protocol (CSURLConnectionHelperMethods)];

    

    /* This has to be one-way, otherwise you might get a dead-lock if

       any NSURLConnection events get triggered. */

    [helperProxy reallyInitWithRequest];


    [lock lockWhenCondition:0];

    

    if (!urlConnection) {

      [connection invalidate];

      [self release];

      return nil;

    }

  }


  return self;

}


- (void)dealloc

{

  [connection invalidate];

  [connection release];

  [lock release];

  [urlConnection release];

  [helperProxy release];

  [delegateProxy release];

  [super dealloc];

}


// Runs on worker thread

- (oneway void)wakeUpWorkerThread

{

  // Don't need to do anything

}


- (oneway void)reallyInitWithRequest

{

  urlConnection = [urlConnection reallyInitWithRequest:request

      delegate:delegateProxy];

  /* Here we must sleep and then wakeup since the main thread needs to

     return the initialisation result before we do any more. */

  [self willSleep];

  [self willWakeup:YES];

}


- (void)willSleep

{

  sleeping = YES;

  [lock unlockWithCondition:0];

}


- (void)willWakeup:(BOOL)sendPause

{

  if (sendPause || ![lock tryLock]) {

    waitingToPause = YES;

    [self performSelectorOnMainThread:@selector (pauseMainThread)

  withObject:nil

waitUntilDone:NO];

    [lock lockWhenCondition:1];

    waitingToPause = NO;

  }


  sleeping = NO;


  while (invocation) {

    [invocation invokeWithTarget:urlConnection];

    invocation = nil;

    [lock unlockWithCondition:0];

    [lock lockWhenCondition:1];

  }

}


- (id)delegate

{

  return delegate;

}


- (CSURLConnection *)urlConnection

{

  return urlConnection;

}


- (void)invokeOnMainThread:(NSInvocation *)anInvocation

{

  unsigned i;

  NSMethodSignature *methodSignature = [anInvocation methodSignature];

  id arg;

  const char *argType = @encode (CSURLConnection *);

  

  for (i = 0; i < [methodSignature numberOfArguments]; ++i) {

    if (!strcmp ([methodSignature getArgumentTypeAtIndex:i], argType)) {

      [anInvocation getArgument:&arg atIndex:i];

      if (arg == urlConnection) {

[anInvocation setArgument:&urlConnectionProxy

  atIndex:i];

      }

    }

  }

  

  if ([NSThread currentThread] != ourThread)

    [anInvocation invokeWithTarget:delegate];

  else {

    invocation = anInvocation;

    for (;;) {

      [lock unlockWithCondition:0];

      [lock lockWhenCondition:1];

      if (!invocation)

return;

      [invocation invokeWithTarget:urlConnection];

      invocation = nil;

    }

  }

}


- (void)invokeOnWorkerThread:(NSInvocation *)anInvocation

{

  if (!invocation)

    [helperProxy wakeUpWorkerThread];

  invocation = anInvocation;

  for (;;) {

    [lock unlockWithCondition:1];

    [lock lockWhenCondition:0];

    if (!invocation)

      return;

    [invocation invokeWithTarget:delegate];

    invocation = nil;

  }

}


- (void)thread

{

  NSAutoreleasePool *pool, *outerPool;

  NSRunLoop *rl;

  NSConnection *workerConnection;

  

  outerPool = [[NSAutoreleasePool alloc] init];

  

  ourThread = [NSThread currentThread];

  

  [lock lockWhenCondition:1];

  

  workerConnection = [NSConnection

      connectionWithReceivePort:[connection sendPort]

sendPort:[connection receivePort]];

  

  [workerConnection setRootObject:self];

  [workerConnection retain];

  

  rl = [NSRunLoop currentRunLoop];

  

  CFRunLoopObserverContext context;

  

  memset (&context, 0, sizeof (context));

  context.info = self;

  

  CFRunLoopObserverRef observer

    = CFRunLoopObserverCreate(NULL, 

      kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting,

      true, 

      0, runLoopStateChange,

      &context);

  

  CFRunLoopAddObserver ([rl getCFRunLoop],

observer,

kCFRunLoopDefaultMode);


  for (;;) {

    pool = [[NSAutoreleasePool alloc] init];

    if (![workerConnection isValid] || !urlConnectionProxy) {

      [pool release];

      break;

    }

    [rl runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

    [pool release];

  }


  [urlConnection release];

  urlConnection = nil;

  CFRelease (observer);

  [workerConnection invalidate];

  [workerConnection release];

  [lock unlockWithCondition:0];

  

  [outerPool release];

}


- (void)terminate

{

  [connection invalidate];

  urlConnectionProxy = nil;

}


// We pause the main thread to allow worker thread to do something

- (void)pauseMainThread

{

  if (!waitingToPause)

    return;

  

  /* We need to retain ourselves in case an invocation causes us to be

     released. */

  [self retain];

  

  [lock unlockWithCondition:1];

  

  for (;;) {

    [lock lockWhenCondition:0];

    if (!invocation)

      break;

    [invocation invokeWithTarget:delegate];

    invocation = nil;

    [lock unlockWithCondition:1];

  }


  if (!urlConnectionProxy)

    [lock unlock];

  

  [self release];

}


@end


@implementation CSDelegateProxy


- (CSDelegateProxy *)initWithHelper:(CSURLConnectionHelper *)theHelper

{

  helper = theHelper;

  

  return self;

}


- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

{

  return [[helper delegate] methodSignatureForSelector:aSelector];

}


- (BOOL)respondsToSelector:(SEL)aSelector

{

  return [[helper delegate] respondsToSelector:aSelector];

}


- (void)forwardInvocation:(NSInvocation *)anInvocation

{

  [helper invokeOnMainThread:anInvocation];

}


@end


@implementation CSURLConnectionProxy


- (CSURLConnectionProxy *)initWithURLConnection:(CSURLConnection *)urlConnection

request:(NSURLRequest *)request

      delegate:(id)delegate;

{

  helper = [[CSURLConnectionHelper alloc] initWithURLConnection:urlConnection

    urlConnectionProxy:self

request:request

      delegate:delegate];

  if (!helper) {

    [self release];

    return nil;

  }

  

  return self;

}


- (void)dealloc

{

  [helper terminate];

  [helper release];

  [super dealloc];

}


- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

{

  return [[helper urlConnection] methodSignatureForSelector:aSelector];

}


- (BOOL)respondsToSelector:(SEL)aSelector

{

  return [[helper urlConnection] respondsToSelector:aSelector];

}


- (void)forwardInvocation:(NSInvocation *)anInvocation

{

  [helper invokeOnWorkerThread:anInvocation];

}


@end


@implementation CSURLConnection


static NSThread *mainThread;


+ (void)enableModalSupport

{

  mainThread = [NSThread currentThread];

}


+ (void)disableModalSupport

{

  mainThread = nil;

}


- (CSURLConnectionProxy *)initWithRequest:(NSURLRequest *)request

delegate:(id)delegate

{

  if ([NSThread currentThread] != mainThread) {

    return [super initWithRequest:request

delegate:delegate];

  } else {

    return [[CSURLConnectionProxy alloc] initWithURLConnection:self

      request:request

      delegate:delegate];

  }

}


- (CSURLConnection *)reallyInitWithRequest:(NSURLRequest *)request

  delegate:(id)delegate

{

  return [super initWithRequest:request

      delegate:delegate];

}


@end