/* Copyright (c) 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//
//  GTMHTTPFetcherService.m
//

#import "GTMHTTPFetcherService.h"

@interface GTMHTTPFetcher (ServiceMethods)
- (BOOL)beginFetchMayDelay:(BOOL)mayDelay
              mayAuthorize:(BOOL)mayAuthorize;
@end

@interface GTMHTTPFetcherService ()
@property (retain, readwrite) NSDictionary *delayedHosts;
@property (retain, readwrite) NSDictionary *runningHosts;

- (void)detachAuthorizer;
@end

@implementation GTMHTTPFetcherService

@synthesize maxRunningFetchersPerHost = maxRunningFetchersPerHost_,
            userAgent = userAgent_,
            timeout = timeout_,
            runLoopModes = runLoopModes_,
            credential = credential_,
            proxyCredential = proxyCredential_,
            cookieStorageMethod = cookieStorageMethod_,
            shouldFetchInBackground = shouldFetchInBackground_,
            fetchHistory = fetchHistory_;

- (id)init {
  self = [super init];
  if (self) {
    fetchHistory_ = [[GTMHTTPFetchHistory alloc] init];
    delayedHosts_ = [[NSMutableDictionary alloc] init];
    runningHosts_ = [[NSMutableDictionary alloc] init];
    cookieStorageMethod_ = kGTMHTTPFetcherCookieStorageMethodFetchHistory;

    maxRunningFetchersPerHost_ = 10;
}
  return self;
}

- (void)dealloc {
  [self detachAuthorizer];

  [delayedHosts_ release];
  [runningHosts_ release];
  [fetchHistory_ release];
  [userAgent_ release];
  [runLoopModes_ release];
  [credential_ release];
  [proxyCredential_ release];
  [authorizer_ release];

  [super dealloc];
}

#pragma mark Generate a new fetcher

- (id)fetcherWithRequest:(NSURLRequest *)request
            fetcherClass:(Class)fetcherClass {
  GTMHTTPFetcher *fetcher = [fetcherClass fetcherWithRequest:request];

  fetcher.fetchHistory = self.fetchHistory;
  fetcher.runLoopModes = self.runLoopModes;
  fetcher.cookieStorageMethod = self.cookieStorageMethod;
  fetcher.credential = self.credential;
  fetcher.proxyCredential = self.proxyCredential;
  fetcher.shouldFetchInBackground = self.shouldFetchInBackground;
  fetcher.authorizer = self.authorizer;
  fetcher.service = self;

  NSString *userAgent = self.userAgent;
  if ([userAgent length] > 0
      && [request valueForHTTPHeaderField:@"User-Agent"] == nil) {
    [fetcher.mutableRequest setValue:userAgent
                  forHTTPHeaderField:@"User-Agent"];
  }

  NSTimeInterval timeout = self.timeout;
  if (timeout > 0.0) {
    [fetcher.mutableRequest setTimeoutInterval:timeout];
  }

  return fetcher;
}

- (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request {
  return [self fetcherWithRequest:request
                     fetcherClass:[GTMHTTPFetcher class]];
}

- (GTMHTTPFetcher *)fetcherWithURL:(NSURL *)requestURL {
  return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
}

- (GTMHTTPFetcher *)fetcherWithURLString:(NSString *)requestURLString {
  return [self fetcherWithURL:[NSURL URLWithString:requestURLString]];
}

#pragma mark Queue Management

- (void)addRunningFetcher:(GTMHTTPFetcher *)fetcher
                  forHost:(NSString *)host {
  // Add to the array of running fetchers for this host, creating the array
  // if needed
  NSMutableArray *runningForHost = [runningHosts_ objectForKey:host];
  if (runningForHost == nil) {
    runningForHost = [NSMutableArray arrayWithObject:fetcher];
    [runningHosts_ setObject:runningForHost forKey:host];
  } else {
    [runningForHost addObject:fetcher];
  }
}

- (void)addDelayedFetcher:(GTMHTTPFetcher *)fetcher
                  forHost:(NSString *)host {
  // Add to the array of delayed fetchers for this host, creating the array
  // if needed
  NSMutableArray *delayedForHost = [delayedHosts_ objectForKey:host];
  if (delayedForHost == nil) {
    delayedForHost = [NSMutableArray arrayWithObject:fetcher];
    [delayedHosts_ setObject:delayedForHost forKey:host];
  } else {
    [delayedForHost addObject:fetcher];
  }
}

- (BOOL)isDelayingFetcher:(GTMHTTPFetcher *)fetcher {
  BOOL isDelayed;
  @synchronized(self) {
    NSString *host = [[[fetcher mutableRequest] URL] host];
    NSArray *delayedForHost = [delayedHosts_ objectForKey:host];
    NSUInteger idx = [delayedForHost indexOfObjectIdenticalTo:fetcher];
    isDelayed = (delayedForHost != nil) && (idx != NSNotFound);
  }
  return isDelayed;
}

- (BOOL)fetcherShouldBeginFetching:(GTMHTTPFetcher *)fetcher {
  // Entry point from the fetcher
  @synchronized(self) {
    NSString *host = [[[fetcher mutableRequest] URL] host];

    if ([host length] == 0) {
#if DEBUG
      NSAssert1(0, @"%@ lacks host", fetcher);
#endif
      return YES;
    }

    NSMutableArray *runningForHost = [runningHosts_ objectForKey:host];
    if (runningForHost != nil
        && [runningForHost indexOfObjectIdenticalTo:fetcher] != NSNotFound) {
#if DEBUG
      NSAssert1(0, @"%@ was already running", fetcher);
#endif
      return YES;
    }

    // We'll save the host that serves as the key for this fetcher's array
    // to avoid any chance of the underlying request changing, stranding
    // the fetcher in the wrong array
    fetcher.serviceHost = host;
    fetcher.thread = [NSThread currentThread];

    if (maxRunningFetchersPerHost_ == 0
        || maxRunningFetchersPerHost_ > [runningForHost count]) {
      [self addRunningFetcher:fetcher forHost:host];
      return YES;
    } else {
      [self addDelayedFetcher:fetcher forHost:host];
      return NO;
    }
  }
  return YES;
}

// Fetcher start and stop methods, invoked on the appropriate thread for
// the fetcher
- (void)startFetcherOnCurrentThread:(GTMHTTPFetcher *)fetcher {
  [fetcher beginFetchMayDelay:NO
                 mayAuthorize:YES];
}

- (void)startFetcher:(GTMHTTPFetcher *)fetcher {
  NSThread *thread = [fetcher thread];
  if ([thread isEqual:[NSThread currentThread]]) {
    // Same thread
    [self startFetcherOnCurrentThread:fetcher];
  } else {
    // Different thread
    [self performSelector:@selector(startFetcherOnCurrentThread:)
                 onThread:thread
               withObject:fetcher
            waitUntilDone:NO];
  }
}

- (void)stopFetcherOnCurrentThread:(GTMHTTPFetcher *)fetcher {
  [fetcher stopFetching];
}

- (void)stopFetcher:(GTMHTTPFetcher *)fetcher {
  NSThread *thread = [fetcher thread];
  if ([thread isEqual:[NSThread currentThread]]) {
    // Same thread
    [self stopFetcherOnCurrentThread:fetcher];
  } else {
    // Different thread
    [self performSelector:@selector(stopFetcherOnCurrentThread:)
                 onThread:thread
               withObject:fetcher
            waitUntilDone:NO];
  }
}

- (void)fetcherDidStop:(GTMHTTPFetcher *)fetcher {
  // Entry point from the fetcher
  @synchronized(self) {
    NSString *host = fetcher.serviceHost;
    if (!host) {
      // fetcher has been stopped previously
      return;
    }

    NSMutableArray *runningForHost = [runningHosts_ objectForKey:host];
    [runningForHost removeObject:fetcher];

    NSMutableArray *delayedForHost = [delayedHosts_ objectForKey:host];
    [delayedForHost removeObject:fetcher];

    while ([delayedForHost count] > 0
           && [runningForHost count] < maxRunningFetchersPerHost_) {
      // Start another delayed fetcher running, scanning for the minimum
      // priority value, defaulting to FIFO for equal priorities
      GTMHTTPFetcher *nextFetcher = nil;
      for (GTMHTTPFetcher *delayedFetcher in delayedForHost) {
        if (nextFetcher == nil
            || delayedFetcher.servicePriority < nextFetcher.servicePriority) {
          nextFetcher = delayedFetcher;
        }
      }

      [self addRunningFetcher:nextFetcher forHost:host];
      runningForHost = [runningHosts_ objectForKey:host];

      [delayedForHost removeObjectIdenticalTo:nextFetcher];
      [self startFetcher:nextFetcher];
    }

    if ([runningForHost count] == 0) {
      // None left; remove the empty array
      [runningHosts_ removeObjectForKey:host];
    }

    if ([delayedForHost count] == 0) {
      [delayedHosts_ removeObjectForKey:host];
    }

    // The fetcher is no longer in the running or the delayed array,
    // so remove its host and thread properties
    fetcher.serviceHost = nil;
    fetcher.thread = nil;
  }
}

- (NSUInteger)numberOfFetchers {
  NSUInteger running = [self numberOfRunningFetchers];
  NSUInteger delayed = [self numberOfDelayedFetchers];
  return running + delayed;
}

- (NSUInteger)numberOfRunningFetchers {
  NSUInteger sum = 0;
  for (NSString *host in runningHosts_) {
    NSArray *fetchers = [runningHosts_ objectForKey:host];
    sum += [fetchers count];
  }
  return sum;
}

- (NSUInteger)numberOfDelayedFetchers {
  NSUInteger sum = 0;
  for (NSString *host in delayedHosts_) {
    NSArray *fetchers = [delayedHosts_ objectForKey:host];
    sum += [fetchers count];
  }
  return sum;
}

- (void)stopAllFetchers {
  // Remove fetchers from the delayed list to avoid fetcherDidStop: from
  // starting more fetchers running as a side effect of stopping one
  NSArray *delayedForHosts = [delayedHosts_ allValues];
  [delayedHosts_ removeAllObjects];

  for (NSArray *delayedForHost in delayedForHosts) {
    for (GTMHTTPFetcher *fetcher in delayedForHost) {
      [self stopFetcher:fetcher];
    }
  }

  NSArray *runningForHosts = [runningHosts_ allValues];
  [runningHosts_ removeAllObjects];

  for (NSArray *runningForHost in runningForHosts) {
    for (GTMHTTPFetcher *fetcher in runningForHost) {
      [self stopFetcher:fetcher];
    }
  }
}

#pragma mark Fetch History Settings

// Turn on data caching to receive a copy of previously-retrieved objects.
// Otherwise, fetches may return status 304 (No Change) rather than actual data
- (void)setShouldCacheETaggedData:(BOOL)flag {
  self.fetchHistory.shouldCacheETaggedData = flag;
}

- (BOOL)shouldCacheETaggedData {
  return self.fetchHistory.shouldCacheETaggedData;
}

- (void)setETaggedDataCacheCapacity:(NSUInteger)totalBytes {
  self.fetchHistory.memoryCapacity = totalBytes;
}

- (NSUInteger)ETaggedDataCacheCapacity {
  return self.fetchHistory.memoryCapacity;
}

- (void)setShouldRememberETags:(BOOL)flag {
  self.fetchHistory.shouldRememberETags = flag;
}

- (BOOL)shouldRememberETags {
  return self.fetchHistory.shouldRememberETags;
}

// reset the ETag cache to avoid getting a Not Modified status
// based on prior queries
- (void)clearETaggedDataCache {
  [self.fetchHistory clearETaggedDataCache];
}

- (void)clearHistory {
  [self clearETaggedDataCache];
  [self.fetchHistory removeAllCookies];
}

#pragma mark Synchronous Wait for Unit Testing

- (void)waitForCompletionOfAllFetchersWithTimeout:(NSTimeInterval)timeoutInSeconds {
  NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];

  while ([self numberOfFetchers] > 0
         && [giveUpDate timeIntervalSinceNow] > 0) {
    // Run the current run loop 1/1000 of a second to give the networking
    // code a chance to work
    NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001];
    [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
  }
}

#pragma mark Accessors

- (NSDictionary *)runningHosts {
  return runningHosts_;
}

- (void)setRunningHosts:(NSDictionary *)dict {
  [runningHosts_ autorelease];
  runningHosts_ = [dict mutableCopy];
}

- (NSDictionary *)delayedHosts {
  return delayedHosts_;
}

- (void)setDelayedHosts:(NSDictionary *)dict {
  [delayedHosts_ autorelease];
  delayedHosts_ = [dict mutableCopy];
}

- (id <GTMFetcherAuthorizationProtocol>)authorizer {
  return authorizer_;
}

- (void)setAuthorizer:(id <GTMFetcherAuthorizationProtocol>)obj {
  if (obj != authorizer_) {
    [self detachAuthorizer];
  }

  [authorizer_ autorelease];
  authorizer_ = [obj retain];

  // Use the fetcher service for the authorization fetches if the auth
  // object supports fetcher services
  if ([authorizer_ respondsToSelector:@selector(setFetcherService:)]) {
    [authorizer_ setFetcherService:self];
  }
}

- (void)detachAuthorizer {
  // This method is called by the fetcher service's dealloc and setAuthorizer:
  // methods; do not override.
  //
  // The fetcher service retains the authorizer, and the authorizer has a
  // weak pointer to the fetcher service (a non-zeroing pointer for
  // compatibility with iOS 4 and Mac OS X 10.5/10.6.)
  //
  // When this fetcher service no longer uses the authorizer, we want to remove
  // the authorizer's dependence on the fetcher service.  Authorizers can still
  // function without a fetcher service.
  if ([authorizer_ respondsToSelector:@selector(fetcherService)]) {
    GTMHTTPFetcherService *authFS = [authorizer_ fetcherService];
    if (authFS == self) {
      [authorizer_ setFetcherService:nil];
    }
  }
}

@end
