/* Copyright 2014 Google Inc. All rights reserved.
 *
 * 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.
 */

#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif

#import "GTMSessionFetcher/GTMSessionUploadFetcher.h"

#if TARGET_OS_OSX && GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH
// To reconnect background sessions on Mac outside +load requires importing and linking
// AppKit to access the NSApplicationDidFinishLaunching symbol.
#import <AppKit/AppKit.h>
#endif

static NSString *const kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey = @"_upChunk";
static NSString *const kGTMSessionIdentifierUploadFileURLMetadataKey = @"_upFileURL";
static NSString *const kGTMSessionIdentifierUploadFileLengthMetadataKey = @"_upFileLen";
static NSString *const kGTMSessionIdentifierUploadLocationURLMetadataKey = @"_upLocURL";
static NSString *const kGTMSessionIdentifierUploadMIMETypeMetadataKey = @"_uploadMIME";
static NSString *const kGTMSessionIdentifierUploadChunkSizeMetadataKey = @"_upChSize";
static NSString *const kGTMSessionIdentifierUploadCurrentOffsetMetadataKey = @"_upOffset";
static NSString *const kGTMSessionIdentifierUploadAllowsCellularAccess = @"_upAllowsCellularAccess";

static NSString *const kGTMSessionHeaderXGoogUploadChunkGranularity =
    @"X-Goog-Upload-Chunk-Granularity";
static NSString *const kGTMSessionHeaderXGoogUploadCommand = @"X-Goog-Upload-Command";
static NSString *const kGTMSessionHeaderXGoogUploadContentLength = @"X-Goog-Upload-Content-Length";
static NSString *const kGTMSessionHeaderXGoogUploadContentType = @"X-Goog-Upload-Content-Type";
static NSString *const kGTMSessionHeaderXGoogUploadOffset = @"X-Goog-Upload-Offset";
static NSString *const kGTMSessionHeaderXGoogUploadProtocol = @"X-Goog-Upload-Protocol";
static NSString *const kGTMSessionXGoogUploadProtocolResumable = @"resumable";
static NSString *const kGTMSessionHeaderXGoogUploadSizeReceived = @"X-Goog-Upload-Size-Received";
static NSString *const kGTMSessionHeaderXGoogUploadStatus = @"X-Goog-Upload-Status";
static NSString *const kGTMSessionHeaderXGoogUploadURL = @"X-Goog-Upload-URL";
static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.;
static const NSTimeInterval kDefaultMinUploadRetryInterval = 1.0;

// Property of chunk fetchers identifying the parent upload fetcher.  Non-retained NSValue.
static NSString *const kGTMSessionUploadFetcherChunkParentKey = @"_uploadFetcherChunkParent";

int64_t const kGTMSessionUploadFetcherUnknownFileSize = -1;

int64_t const kGTMSessionUploadFetcherStandardChunkSize = (int64_t)LLONG_MAX;

#if TARGET_OS_IPHONE
int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize =
    10 * 1024 * 1024;  // 10 MB for iOS, watchOS, tvOS
#else
int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize =
    100 * 1024 * 1024;  // 100 MB for macOS
#endif

typedef NS_ENUM(NSUInteger, GTMSessionUploadFetcherStatus) {
  kStatusUnknown,
  kStatusActive,
  kStatusFinal,
  kStatusCancelled,
};

NSString *const kGTMSessionFetcherUploadLocationObtainedNotification =
    @"kGTMSessionFetcherUploadLocationObtainedNotification";
NSString *const kGTMSessionFetcherUploadInitialBackoffStartedNotification =
    @"kGTMSessionFetcherUploadInitialBackoffStartedNotification";

#if !GTMSESSION_BUILD_COMBINED_SOURCES
@interface GTMSessionFetcher (ProtectedMethods)

// Access to non-public method on the parent fetcher class.
- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks;
- (void)createSessionIdentifierWithMetadata:(NSDictionary *)metadata;
- (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(id)target
                                                didFinishSelector:(SEL)finishedSelector;
- (void)invokeOnCallbackQueue:(dispatch_queue_t)callbackQueue
             afterUserStopped:(BOOL)afterStopped
                        block:(void (^)(void))block;
- (NSTimer *)retryTimer;
- (void)beginFetchForRetry;

@property(readwrite, strong) NSData *downloadedData;
- (void)releaseCallbacks;

- (NSInteger)statusCodeUnsynchronized;

- (BOOL)userStoppedFetching;

@end
#endif  // !GTMSESSION_BUILD_COMBINED_SOURCES

@interface GTMSessionUploadFetcher ()

// Changing readonly to readwrite.
@property(atomic, strong, readwrite) NSURLRequest *lastChunkRequest;
@property(atomic, readwrite, assign) int64_t currentOffset;

// Internal properties.
@property(strong, atomic, nullable) GTMSessionFetcher *fetcherInFlight;  // Synchronized on self.

@property(assign, atomic, getter=isSubdataGenerating) BOOL subdataGenerating;
@property(assign, atomic) BOOL shouldInitiateOffsetQuery;
@property(assign, atomic) int64_t uploadGranularity;
@property(assign, atomic) BOOL allowsCellularAccess;

@end

@implementation GTMSessionUploadFetcher {
  GTMSessionFetcher *_chunkFetcher;

  // We'll call through to the delegate's completion handler.
  GTMSessionFetcherCompletionHandler _delegateCompletionHandler;
  dispatch_queue_t _delegateCallbackQueue;

  // The initial fetch's body length and bytes actually sent are
  // needed for calculating progress during subsequent chunk uploads
  int64_t _initialBodyLength;
  int64_t _initialBodySent;

  // The upload server address for the chunks of this upload session.
  NSURL *_uploadLocationURL;

  // _uploadData, _uploadDataProvider, or _uploadFileHandle may be set, but only one.
  NSData *_uploadData;
  NSFileHandle *_uploadFileHandle;
  GTMSessionUploadFetcherDataProvider _uploadDataProvider;
  NSURL *_uploadFileURL;
  int64_t _uploadFileLength;
  NSString *_uploadMIMEType;
  int64_t _chunkSize;
  int64_t _uploadGranularity;
  double _uploadRetryFactor;
  NSTimeInterval _nextUploadRetryInterval;
  NSTimeInterval _maxUploadRetryInterval;
  NSTimeInterval _minUploadRetryInterval;
  BOOL _isPaused;
  BOOL _isRestartedUpload;
  BOOL _shouldInitiateOffsetQuery;

  NSTimer *_uploadRetryTimer;
  // Tied to useBackgroundSession property, since this property is applicable to chunk fetchers.
  BOOL _useBackgroundSessionOnChunkFetchers;

  // We keep the latest offset into the upload data just for progress reporting.
  int64_t _currentOffset;

  NSDictionary *_recentChunkReponseHeaders;
  NSInteger _recentChunkStatusCode;

  // For waiting, we need to know the fetcher in flight, if any, and if subdata generation
  // is in progress.
  GTMSessionFetcher *_fetcherInFlight;
  BOOL _isSubdataGenerating;
  BOOL _isCancelInFlight;

  GTMSessionUploadFetcherCancellationHandler _cancellationHandler;
}

- (NSTimeInterval)nextUploadRetryIntervalUnsynchronized {
  GTMSessionCheckSynchronized(self);

  // The next wait interval is the factor (2.0) times the last interval,
  // but never less than the minimum interval.
  NSTimeInterval secs = _nextUploadRetryInterval * _uploadRetryFactor;
  if (_maxUploadRetryInterval > 0) {
    secs = MIN(secs, _maxUploadRetryInterval);
  }
  secs = MAX(secs, _minUploadRetryInterval);

  return secs;
}

+ (void)load {
#if GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH && TARGET_OS_IPHONE
  NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  [nc addObserver:self
         selector:@selector(reconnectFetchersForBackgroundSessionsOnAppLaunch:)
             name:UIApplicationDidFinishLaunchingNotification
           object:nil];
#elif GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH && TARGET_OS_OSX
  NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  [nc addObserver:self
         selector:@selector(reconnectFetchersForBackgroundSessionsOnAppLaunch:)
             name:NSApplicationDidFinishLaunchingNotification
           object:nil];
#else
  [self uploadFetchersForBackgroundSessions];
#endif
}

+ (void)reconnectFetchersForBackgroundSessionsOnAppLaunch:(NSNotification *)notification {
  // Give all other app-did-launch handlers a chance to complete before
  // reconnecting the fetchers. Not doing this may lead to reconnecting
  // before the app delegate has a chance to run.
  dispatch_async(dispatch_get_main_queue(), ^{
    [self uploadFetchersForBackgroundSessions];
  });
}

+ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request
                          uploadMIMEType:(NSString *)uploadMIMEType
                               chunkSize:(int64_t)chunkSize
                          fetcherService:(GTMSessionFetcherService *)fetcherService {
  GTMSessionUploadFetcher *fetcher = [self uploadFetcherWithRequest:request
                                                     fetcherService:fetcherService];
  [fetcher setLocationURL:nil
            uploadMIMEType:uploadMIMEType
                 chunkSize:chunkSize
      allowsCellularAccess:request.allowsCellularAccess];
  return fetcher;
}

+ (instancetype)uploadFetcherWithLocation:(nullable NSURL *)uploadLocationURL
                           uploadMIMEType:(NSString *)uploadMIMEType
                                chunkSize:(int64_t)chunkSize
                           fetcherService:(nullable GTMSessionFetcherService *)fetcherServiceOrNil {
  return [self uploadFetcherWithLocation:uploadLocationURL
                          uploadMIMEType:uploadMIMEType
                               chunkSize:chunkSize
                    allowsCellularAccess:YES
                          fetcherService:fetcherServiceOrNil];
}

+ (instancetype)uploadFetcherWithLocation:(nullable NSURL *)uploadLocationURL
                           uploadMIMEType:(NSString *)uploadMIMEType
                                chunkSize:(int64_t)chunkSize
                     allowsCellularAccess:(BOOL)allowsCellularAccess
                           fetcherService:(GTMSessionFetcherService *)fetcherService {
  GTMSessionUploadFetcher *fetcher = [self uploadFetcherWithRequest:nil
                                                     fetcherService:fetcherService];
  [fetcher setLocationURL:uploadLocationURL
            uploadMIMEType:uploadMIMEType
                 chunkSize:chunkSize
      allowsCellularAccess:allowsCellularAccess];
  return fetcher;
}

+ (instancetype)uploadFetcherForSessionIdentifierMetadata:(NSDictionary *)metadata {
  GTMSESSION_ASSERT_DEBUG(
      [metadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] boolValue],
      @"Session identifier metadata is not for an upload fetcher: %@", metadata);

  NSNumber *uploadFileLengthNum = metadata[kGTMSessionIdentifierUploadFileLengthMetadataKey];
  GTMSESSION_ASSERT_DEBUG(uploadFileLengthNum != nil,
                          @"Session metadata missing an UploadFileSize");
  if (uploadFileLengthNum == nil) return nil;

  int64_t uploadFileLength = [uploadFileLengthNum longLongValue];
  GTMSESSION_ASSERT_DEBUG(uploadFileLength >= 0, @"Session metadata UploadFileSize is unknown");

  NSString *uploadFileURLString = metadata[kGTMSessionIdentifierUploadFileURLMetadataKey];
  GTMSESSION_ASSERT_DEBUG(uploadFileURLString, @"Session metadata missing an UploadFileURL");
  if (uploadFileURLString == nil) return nil;

  NSURL *uploadFileURL = [NSURL URLWithString:uploadFileURLString];
  // There used to be a call here to NSURL checkResourceIsReachableAndReturnError: to check for the
  // existence of the file (also tried NSFileManager fileExistsAtPath:). We've determined
  // empirically that the check can fail at startup even when the upload file does in fact exist.
  // For now, we'll go ahead and restore the background upload fetcher. If the file doesn't exist,
  // it will fail later.

  NSString *uploadLocationURLString = metadata[kGTMSessionIdentifierUploadLocationURLMetadataKey];
  NSURL *uploadLocationURL =
      uploadLocationURLString ? [NSURL URLWithString:uploadLocationURLString] : nil;

  NSString *uploadMIMEType = metadata[kGTMSessionIdentifierUploadMIMETypeMetadataKey];
  int64_t uploadChunkSize =
      [metadata[kGTMSessionIdentifierUploadChunkSizeMetadataKey] longLongValue];
  if (uploadChunkSize <= 0) {
    uploadChunkSize = kGTMSessionUploadFetcherStandardChunkSize;
  }
  int64_t currentOffset =
      [metadata[kGTMSessionIdentifierUploadCurrentOffsetMetadataKey] longLongValue];

  BOOL allowsCellularAccess = YES;
  if (metadata[kGTMSessionIdentifierUploadAllowsCellularAccess]) {
    allowsCellularAccess = [metadata[kGTMSessionIdentifierUploadAllowsCellularAccess] boolValue];
  }

  GTMSESSION_ASSERT_DEBUG(currentOffset <= uploadFileLength,
                          @"CurrentOffset (%lld) exceeds UploadFileSize (%lld)", currentOffset,
                          uploadFileLength);
  if (currentOffset > uploadFileLength) return nil;

  GTMSessionUploadFetcher *uploadFetcher = [self uploadFetcherWithLocation:uploadLocationURL
                                                            uploadMIMEType:uploadMIMEType
                                                                 chunkSize:uploadChunkSize
                                                      allowsCellularAccess:allowsCellularAccess
                                                            fetcherService:nil];
  // Set the upload file length before setting the upload file URL tries to determine the length.
  [uploadFetcher setUploadFileLength:uploadFileLength];

  // Remove the private keys (or any invalid keys) from the sessionUserInfo to maintain that api's
  // type contract.
  NSSet *keysToRemove = [metadata keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
    return ![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSString class]] ||
           [key hasPrefix:@"_"];
  }];
  NSMutableDictionary *sessionUserInfo = [metadata mutableCopy];
  [sessionUserInfo removeObjectsForKeys:[keysToRemove allObjects]];
  uploadFetcher.sessionUserInfo = sessionUserInfo;

  uploadFetcher.uploadFileURL = uploadFileURL;
  uploadFetcher.useBackgroundSession = YES;
  uploadFetcher.currentOffset = currentOffset;
  uploadFetcher.delegateCallbackQueue = uploadFetcher.callbackQueue;
  uploadFetcher.allowedInsecureSchemes = @[ @"http" ];  // Allowed on restored upload fetcher.
  return uploadFetcher;
}

+ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request
                          fetcherService:(GTMSessionFetcherService *)fetcherService {
  // Internal utility method for instantiating fetchers
  GTMSessionUploadFetcher *fetcher;
  if ([fetcherService isKindOfClass:[GTMSessionFetcherService class]]) {
    fetcher = [fetcherService fetcherWithRequest:request fetcherClass:self];
  } else {
    fetcher = [self fetcherWithRequest:request];
  }
  fetcher.useBackgroundSession = YES;
  return fetcher;
}

+ (NSPointerArray *)uploadFetcherPointerArrayForBackgroundSessions {
  static NSPointerArray *gUploadFetcherPointerArrayForBackgroundSessions = nil;

  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    gUploadFetcherPointerArrayForBackgroundSessions = [NSPointerArray weakObjectsPointerArray];
  });
  return gUploadFetcherPointerArrayForBackgroundSessions;
}

+ (instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier {
  return [self uploadFetcherForSessionIdentifier:sessionIdentifier];
}

+ (instancetype)uploadFetcherForSessionIdentifier:(NSString *)sessionIdentifier {
  GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
  NSArray *uploadFetchersForBackgroundSessions = [self uploadFetchersForBackgroundSessions];
  for (GTMSessionUploadFetcher *uploadFetcher in uploadFetchersForBackgroundSessions) {
    if ([uploadFetcher.chunkFetcher.sessionIdentifier isEqual:sessionIdentifier]) {
      return uploadFetcher;
    }
  }
  return nil;
}

+ (NSArray *)uploadFetchersForBackgroundSessions {
  NSMutableSet *restoredSessionIdentifiers = [[NSMutableSet alloc] init];
  NSMutableArray *uploadFetchers = [[NSMutableArray alloc] init];
  NSPointerArray *uploadFetcherPointerArray = [self uploadFetcherPointerArrayForBackgroundSessions];

  // Collect the background session upload fetchers that are still in memory.
  @synchronized(uploadFetcherPointerArray) {
    [uploadFetcherPointerArray compact];
    for (GTMSessionUploadFetcher *uploadFetcher in uploadFetcherPointerArray) {
      NSString *sessionIdentifier = uploadFetcher.chunkFetcher.sessionIdentifier;
      if (sessionIdentifier) {
        [restoredSessionIdentifiers addObject:sessionIdentifier];
        [uploadFetchers addObject:uploadFetcher];
      }
    }
  }  // @synchronized(uploadFetcherPointerArray)

  // The system may have other ongoing background upload sessions. Restore upload fetchers for those
  // too.
  NSArray *fetchers = [GTMSessionFetcher fetchersForBackgroundSessions];
  for (GTMSessionFetcher *fetcher in fetchers) {
    NSString *sessionIdentifier = fetcher.sessionIdentifier;
    if (!sessionIdentifier || [restoredSessionIdentifiers containsObject:sessionIdentifier]) {
      continue;
    }
    NSDictionary *sessionIdentifierMetadata = [fetcher sessionIdentifierMetadata];
    if (sessionIdentifierMetadata == nil) {
      continue;
    }
    if (![sessionIdentifierMetadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey]
            boolValue]) {
      continue;
    }
    GTMSessionUploadFetcher *uploadFetcher =
        [self uploadFetcherForSessionIdentifierMetadata:sessionIdentifierMetadata];
    if (uploadFetcher == nil) {
      // Something went wrong with this upload fetcher, so kill the restored chunk fetcher.
      [fetcher stopFetching];
      continue;
    }
    [uploadFetchers addObject:uploadFetcher];
    uploadFetcher->_chunkFetcher = fetcher;
    uploadFetcher->_fetcherInFlight = fetcher;
    [uploadFetcher attachSendProgressBlockToChunkFetcher:fetcher];
    fetcher.completionHandler =
        [fetcher completionHandlerWithTarget:uploadFetcher
                           didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];

    GTMSESSION_LOG_DEBUG(@"%@ restoring upload fetcher %@ for chunk fetcher %@", [self class],
                         uploadFetcher, fetcher);
  }
  return uploadFetchers;
}

- (void)setUploadData:(NSData *)data {
  BOOL changed = NO;

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_uploadData != data) {
      _uploadData = data;
      changed = YES;
    }
  }
  if (changed) {
    [self setupRequestHeaders];
  }
}

- (NSData *)uploadData {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _uploadData;
  }
}

- (void)setUploadFileHandle:(NSFileHandle *)fh {
  BOOL changed = NO;

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_uploadFileHandle != fh) {
      _uploadFileHandle = fh;
      changed = YES;
    }
  }
  if (changed) {
    [self setupRequestHeaders];
  }
}

- (NSFileHandle *)uploadFileHandle {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _uploadFileHandle;
  }
}

- (void)setUploadFileURL:(NSURL *)uploadURL {
  BOOL changed = NO;

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_uploadFileURL != uploadURL) {
      _uploadFileURL = uploadURL;
      changed = YES;
    }
  }
  if (changed) {
    [self setupRequestHeaders];
  }
}

- (NSURL *)uploadFileURL {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _uploadFileURL;
  }
}

- (void)setUploadFileLength:(int64_t)fullLength {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_uploadFileLength == kGTMSessionUploadFetcherUnknownFileSize &&
        fullLength != kGTMSessionUploadFetcherUnknownFileSize) {
      _uploadFileLength = fullLength;
    }
  }
}

- (void)setUploadDataLength:(int64_t)fullLength
                   provider:(GTMSessionUploadFetcherDataProvider)block {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _uploadDataProvider = [block copy];
    _uploadFileLength = fullLength;
  }
  [self setupRequestHeaders];
}

- (GTMSessionUploadFetcherDataProvider)uploadDataProvider {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _uploadDataProvider;
  }
}

- (void)setUploadMIMEType:(NSString *)uploadMIMEType {
  GTMSESSION_ASSERT_DEBUG(0, @"TODO: disallow setUploadMIMEType by making declaration readonly");
  // (and uploadMIMEType, chunksize, currentOffset)
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _uploadMIMEType = uploadMIMEType;
  }
}

- (NSString *)uploadMIMEType {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _uploadMIMEType;
  }
}

- (int64_t)chunkSize {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _chunkSize;
  }
}

- (void)setupRequestHeaders {
  GTMSessionCheckNotSynchronized(self);

#if DEBUG
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    int hasData = (_uploadData != nil) ? 1 : 0;
    int hasFileHandle = (_uploadFileHandle != nil) ? 1 : 0;
    int hasFileURL = (_uploadFileURL != nil) ? 1 : 0;
    int hasUploadDataProvider = (_uploadDataProvider != nil) ? 1 : 0;
    __unused int numberOfSources = hasData + hasFileHandle + hasFileURL + hasUploadDataProvider;
    GTMSESSION_ASSERT_DEBUG(numberOfSources == 1, @"Need just one upload source (%d)",
                            numberOfSources);
  }  // @synchronized(self)
#endif

  // Add our custom headers to the initial request indicating the data
  // type and total size to be delivered later in the chunk requests.
  NSMutableURLRequest *mutableRequest = [self.request mutableCopy];

  GTMSESSION_ASSERT_DEBUG((mutableRequest == nil) != (_uploadLocationURL == nil),
                          @"Request and location are mutually exclusive");
  if (!mutableRequest) return;

  [mutableRequest setValue:kGTMSessionXGoogUploadProtocolResumable
        forHTTPHeaderField:kGTMSessionHeaderXGoogUploadProtocol];
  [mutableRequest setValue:@"start" forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
  [mutableRequest setValue:_uploadMIMEType
        forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentType];
  [mutableRequest setValue:@([self fullUploadLength]).stringValue
        forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentLength];

  NSString *method = mutableRequest.HTTPMethod;
  if (method == nil || [method caseInsensitiveCompare:@"GET"] == NSOrderedSame) {
    [mutableRequest setHTTPMethod:@"POST"];
  }

  // Ensure the user agent header identifies this to the upload server as a
  // GTMSessionUploadFetcher client.  The /1 can be incremented in the unlikely circumstance
  // we need to make a bug fix in the client that the server can recognize.
  NSString *const kUserAgentStub = @"(GTMSUF/1)";
  NSString *userAgent = [mutableRequest valueForHTTPHeaderField:@"User-Agent"];
  if (userAgent == nil || [userAgent rangeOfString:kUserAgentStub].location == NSNotFound) {
    if (userAgent.length == 0) {
      userAgent = GTMFetcherStandardUserAgentString(nil);
    }
    userAgent = [userAgent stringByAppendingFormat:@" %@", kUserAgentStub];
    [mutableRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
  }
  [self setRequest:mutableRequest];
}

- (void)setLocationURL:(nullable NSURL *)location
          uploadMIMEType:(NSString *)uploadMIMEType
               chunkSize:(int64_t)chunkSize
    allowsCellularAccess:(BOOL)allowsCellularAccess {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    GTMSESSION_ASSERT_DEBUG(chunkSize > 0, @"chunk size is zero");

    _allowsCellularAccess = allowsCellularAccess;

    // When resuming an upload, set the known upload target URL.
    _uploadLocationURL = location;

    _uploadMIMEType = uploadMIMEType;
    _chunkSize = chunkSize;

    // Indicate that we've not yet determined the file handle's length
    _uploadFileLength = kGTMSessionUploadFetcherUnknownFileSize;

    // Indicate that we've not yet determined the upload fetcher status
    _recentChunkStatusCode = -1;

    // If this is restarting an upload begun by another fetcher,
    // the location is specified but the request is nil
    _isRestartedUpload = (location != nil);
  }  // @synchronized(self)
}

- (int64_t)fullUploadLength {
  int64_t result;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_uploadData) {
      result = (int64_t)_uploadData.length;
    } else {
      if (_uploadFileLength == kGTMSessionUploadFetcherUnknownFileSize) {
        if (_uploadFileHandle) {
          // First time through, seek to end to determine file length
          _uploadFileLength = (int64_t)[_uploadFileHandle seekToEndOfFile];
        } else if (_uploadDataProvider) {
          // _uploadFileLength is set when the _uploadDataProvider is set.
          GTMSESSION_ASSERT_DEBUG(_uploadFileLength >= 0, @"No uploadDataProvider length set");
        } else {
          NSNumber *filesizeNum;
          NSError *valueError;
          if ([_uploadFileURL getResourceValue:&filesizeNum
                                        forKey:NSURLFileSizeKey
                                         error:&valueError]) {
            _uploadFileLength = filesizeNum.longLongValue;
          } else {
            GTMSESSION_ASSERT_DEBUG(NO, @"Cannot get file size: %@\n  %@", valueError,
                                    _uploadFileURL.path);
            _uploadFileLength = 0;
          }
        }
      }
      result = _uploadFileLength;
    }
  }  // @synchronized(self)
  return result;
}

// Make a subdata of the upload data.
- (void)generateChunkSubdataWithOffset:(int64_t)offset
                                length:(int64_t)length
                              response:(GTMSessionUploadFetcherDataProviderResponse)response {
  GTMSessionUploadFetcherDataProvider uploadDataProvider = self.uploadDataProvider;
  if (uploadDataProvider) {
    uploadDataProvider(offset, length, response);
    return;
  }

  NSData *uploadData = self.uploadData;
  if (uploadData) {
    // NSData provided.
    NSData *resultData;
    if (offset == 0 && length == (int64_t)uploadData.length) {
      resultData = uploadData;
    } else {
      int64_t dataLength = (int64_t)uploadData.length;
      // Ensure our range is valid.  b/18007814
      if (offset + length > dataLength) {
        NSString *errorMessage = [NSString
            stringWithFormat:
                @"Range invalid for upload data.  offset: %lld\tlength: %lld\tdataLength: %lld",
                offset, length, dataLength];
        GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
        response(nil, kGTMSessionUploadFetcherUnknownFileSize,
                 [self uploadChunkUnavailableErrorWithDescription:errorMessage]);
        return;
      }
      NSRange range = NSMakeRange((NSUInteger)offset, (NSUInteger)length);

      @try {
        resultData = [uploadData subdataWithRange:range];
      } @catch (NSException *exception) {
        NSString *errorMessage = exception.description;
        GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
        response(nil, kGTMSessionUploadFetcherUnknownFileSize,
                 [self uploadChunkUnavailableErrorWithDescription:errorMessage]);
        return;
      }
    }
    response(resultData, kGTMSessionUploadFetcherUnknownFileSize, nil);
    return;
  }
  NSURL *uploadFileURL = self.uploadFileURL;
  if (uploadFileURL) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      [self generateChunkSubdataFromFileURL:uploadFileURL
                                     offset:offset
                                     length:length
                                   response:response];
    });
    return;
  }
  GTMSESSION_ASSERT_DEBUG(_uploadFileHandle, @"Unexpectedly missing upload data package");
  NSFileHandle *uploadFileHandle = self.uploadFileHandle;
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [self generateChunkSubdataFromFileHandle:uploadFileHandle
                                      offset:offset
                                      length:length
                                    response:response];
  });
}

- (void)generateChunkSubdataFromFileHandle:(NSFileHandle *)fileHandle
                                    offset:(int64_t)offset
                                    length:(int64_t)length
                                  response:(GTMSessionUploadFetcherDataProviderResponse)response {
  NSData *resultData;
  NSError *error;
  @try {
    [fileHandle seekToFileOffset:(unsigned long long)offset];
    resultData = [fileHandle readDataOfLength:(NSUInteger)length];
  } @catch (NSException *exception) {
    GTMSESSION_ASSERT_DEBUG(NO, @"uploadFileHandle failed to read, %@", exception);
    error = [self uploadChunkUnavailableErrorWithDescription:exception.description];
  }
  // The response always re-dispatches to the main thread, so we skip doing that here.
  response(resultData, kGTMSessionUploadFetcherUnknownFileSize, error);
}

- (void)generateChunkSubdataFromFileURL:(NSURL *)fileURL
                                 offset:(int64_t)offset
                                 length:(int64_t)length
                               response:(GTMSessionUploadFetcherDataProviderResponse)response {
  GTMSessionCheckNotSynchronized(self);

  NSData *resultData;
  NSError *error;
  int64_t fullUploadLength = [self fullUploadLength];
  NSData *mappedData =
      [NSData dataWithContentsOfURL:fileURL
                            options:NSDataReadingMappedAlways + NSDataReadingUncached
                              error:&error];
  if (!mappedData) {
    // We could not create an NSData by memory-mapping the file.
#if TARGET_IPHONE_SIMULATOR
    // NSTemporaryDirectory() can differ in the simulator between app restarts,
    // yet the contents for the new path remains unchanged, so try the latest temp path.
    if ([error.domain isEqual:NSCocoaErrorDomain] && (error.code == NSFileReadNoSuchFileError)) {
      NSString *filename = [fileURL lastPathComponent];
      NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
      NSURL *newFileURL = [NSURL fileURLWithPath:filePath];
      if (![newFileURL isEqual:fileURL]) {
        [self generateChunkSubdataFromFileURL:newFileURL
                                       offset:offset
                                       length:length
                                     response:response];
        return;
      }
    }
#endif

    // If the file is just too large to create an NSData for, or if for some other reason we can't
    // map it, create an NSFileHandle instead to read a subset into an NSData.
#if DEBUG
    NSNumber *fileSizeNum;
    BOOL hasFileSize = [fileURL getResourceValue:&fileSizeNum forKey:NSURLFileSizeKey error:NULL];
    GTMSESSION_LOG_DEBUG(@"Note: uploadFileURL is falling back to creating upload chunks by reading"
                         @" an NSFileHandle since uploadFileURL failed to map the upload file,"
                         @" file size %@, %@",
                         hasFileSize ? fileSizeNum : @"unknown", error);
#endif

    NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:fileURL error:&error];
    if (fileHandle != nil) {
      [self generateChunkSubdataFromFileHandle:fileHandle
                                        offset:offset
                                        length:length
                                      response:response];
      return;
    }
    GTMSESSION_ASSERT_DEBUG(NO, @"uploadFileURL failed to read, %@", error);
    // Fall through with the error.
  } else {
    // Successfully created an NSData by memory-mapping the file.
    if ((NSUInteger)(offset + length) > mappedData.length) {
      NSString *errorMessage = [NSString
          stringWithFormat:@"Range invalid for upload data.  offset: %lld\tlength: "
                           @"%lld\tdataLength: %lld\texpected UploadLength: %lld",
                           offset, length, (long long)mappedData.length, fullUploadLength];
      GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
      response(nil, kGTMSessionUploadFetcherUnknownFileSize,
               [self uploadChunkUnavailableErrorWithDescription:errorMessage]);
      return;
    }
    if (offset > 0 || length < fullUploadLength) {
      NSRange range = NSMakeRange((NSUInteger)offset, (NSUInteger)length);
      @try {
        resultData = [mappedData subdataWithRange:range];
      } @catch (NSException *exception) {
        NSString *errorMessage = exception.description;
        GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
        response(nil, kGTMSessionUploadFetcherUnknownFileSize,
                 [self uploadChunkUnavailableErrorWithDescription:errorMessage]);
        return;
      }
    } else {
      resultData = mappedData;
    }
  }
  // The response always re-dispatches to the main thread, so we skip re-dispatching here.
  response(resultData, kGTMSessionUploadFetcherUnknownFileSize, error);
}

- (NSError *)uploadChunkUnavailableErrorWithDescription:(NSString *)description {
  // The description in the userInfo is intended as a clue to programmers, not
  // for client code to examine or rely on.
  NSDictionary *userInfo = @{@"description" : description};
  return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
                             code:GTMSessionFetcherErrorUploadChunkUnavailable
                         userInfo:userInfo];
}

- (NSError *)prematureFailureErrorWithUserInfo:(NSDictionary *)userInfo {
  // An error for if we get an unexpected status from the upload server or
  // otherwise cannot continue.  This is an issue beyond the upload protocol;
  // there's no way the client can do anything useful except give up.
  NSError *error = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain
                                       code:501  // Not implemented
                                   userInfo:userInfo];
  return error;
}

+ (GTMSessionUploadFetcherStatus)uploadStatusFromResponseHeaders:(NSDictionary *)responseHeaders {
  NSString *statusString = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus];
  if ([statusString isEqual:@"active"]) {
    return kStatusActive;
  }
  if ([statusString isEqual:@"final"]) {
    return kStatusFinal;
  }
  if ([statusString isEqual:@"cancelled"]) {
    return kStatusCancelled;
  }
  return kStatusUnknown;
}

#pragma mark Method overrides affecting the initial fetch only

- (void)setCompletionHandler:(GTMSessionFetcherCompletionHandler)handler {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _delegateCompletionHandler = handler;
  }
}

- (void)setDelegateCallbackQueue:(nullable dispatch_queue_t)queue {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _delegateCallbackQueue = queue;
  }
}

- (nullable dispatch_queue_t)delegateCallbackQueue {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _delegateCallbackQueue;
  }
}

- (BOOL)isRestartedUpload {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _isRestartedUpload;
  }
}

- (nullable GTMSessionFetcher *)chunkFetcher {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _chunkFetcher;
  }
}

- (void)setChunkFetcher:(nullable GTMSessionFetcher *)fetcher {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _chunkFetcher = fetcher;
  }
}

- (void)setFetcherInFlight:(nullable GTMSessionFetcher *)fetcher {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _fetcherInFlight = fetcher;
  }
}

- (nullable GTMSessionFetcher *)fetcherInFlight {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _fetcherInFlight;
  }
}

- (void)setCancellationHandler:
    (nullable GTMSessionUploadFetcherCancellationHandler)cancellationHandler {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _cancellationHandler = cancellationHandler;
  }
}

- (nullable GTMSessionUploadFetcherCancellationHandler)cancellationHandler {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _cancellationHandler;
  }
}

- (void)beginFetchForRetry {
  GTMSessionCheckNotSynchronized(self);

  // Override the superclass to reset the initial body length and fetcher-in-flight,
  // then call the superclass implementation.
  [self setInitialBodyLength:[self bodyLength]];

  GTMSESSION_ASSERT_DEBUG(self.fetcherInFlight == nil, @"unexpected fetcher in flight: %@",
                          self.fetcherInFlight);
  self.fetcherInFlight = self;
  [super beginFetchForRetry];
}

- (void)destroyUploadRetryTimer {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);
    [_uploadRetryTimer invalidate];
    _uploadRetryTimer = nil;
  }
}

- (void)beginFetchWithCompletionHandler:(GTMSessionFetcherCompletionHandler)handler {
  GTMSessionCheckNotSynchronized(self);

  [self setInitialBodyLength:[self bodyLength]];
  if (_minUploadRetryInterval <= 0.0) {
    _minUploadRetryInterval = kDefaultMinUploadRetryInterval;
  }
  if (_maxUploadRetryInterval <= 0.0) {
    _maxUploadRetryInterval = kDefaultMaxUploadRetryInterval;
  }
  if (_uploadRetryFactor <= 0.0) {
    _uploadRetryFactor = 2.0;
  }

  // We'll hold onto the superclass's callback queue so we can invoke the handler
  // even after the superclass has released the queue and its callback handler, as
  // happens during auth failure.
  [self setDelegateCallbackQueue:self.callbackQueue];
  self.completionHandler = handler;

  if ([self isRestartedUpload]) {
    // When restarting an upload, we know the destination location for chunk fetches,
    // but we need to query to find the initial offset.
    if (![self isPaused]) {
      [self sendQueryForUploadOffsetWithFetcherProperties:self.properties];
    }
    return;
  }
  // We don't want to call into the client's completion block immediately
  // after the finish of the initial connection (the delegate is called only
  // when uploading finishes), so we substitute our own completion block to be
  // called when the initial connection finishes
  GTMSESSION_ASSERT_DEBUG(self.fetcherInFlight == nil, @"unexpected fetcher in flight: %@",
                          self.fetcherInFlight);

  self.fetcherInFlight = self;
  [super beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
    self.fetcherInFlight = nil;
    // callback

    BOOL hasTestBlock = (self.testBlock != nil);
    if (![self isRestartedUpload] && !hasTestBlock) {
      if (error == nil) {
        [self beginChunkFetches];
      } else {
        if ([self retryTimer] == nil) {
          [self invokeFinalCallbackWithData:nil error:error shouldInvalidateLocation:YES];
        }
      }
    } else {
      // If there was no initial request, then this fetch is resuming some
      // other uploadFetcher's initial request, and the superclass's connection
      // is never used, so at this point we call the user's actual completion
      // block.
      if (!hasTestBlock) {
        [self invokeFinalCallbackWithData:data error:error shouldInvalidateLocation:YES];
      } else {
        // There was a test block, so we won't do chunk fetches, but we simulate obtaining
        // the data to be uploaded from the upload data provider block or the file handle,
        // and then call back.
        [self generateChunkSubdataWithOffset:0
                                      length:[self fullUploadLength]
                                    response:^(NSData *generateData, int64_t fullUploadLength,
                                               NSError *generateError) {
                                      [self invokeFinalCallbackWithData:data
                                                                  error:error
                                               shouldInvalidateLocation:YES];
                                    }];
      }
    }
  }];
}

- (void)beginChunkFetches {
  GTMSessionCheckNotSynchronized(self);

#if DEBUG
  // The initial response of the resumable upload protocol should have an
  // empty body
  //
  // This assert typically happens because the upload create/edit link URL was
  // not supplied with the request, and the server is thus expecting a non-
  // resumable request/response.
  if (self.downloadedData.length > 0) {
    NSData *downloadedData = self.downloadedData;
    __unused NSString *str = [[NSString alloc] initWithData:downloadedData
                                                   encoding:NSUTF8StringEncoding];
    GTMSESSION_ASSERT_DEBUG(NO, @"unexpected response data (uploading to the wrong URL?)\n%@", str);
  }
#endif

  // We need to get the upload URL from the location header to continue.
  NSDictionary *responseHeaders = [self responseHeaders];

  [self retrieveUploadChunkGranularityFromResponseHeaders:responseHeaders];

  GTMSessionUploadFetcherStatus uploadStatus =
      [[self class] uploadStatusFromResponseHeaders:responseHeaders];
  GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown,
                          @"beginChunkFetches has unexpected upload status for headers %@",
                          responseHeaders);

  BOOL isPrematureStop = (uploadStatus == kStatusFinal) || (uploadStatus == kStatusCancelled);

  NSString *uploadLocationURLStr = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadURL];
  BOOL hasUploadLocation = (uploadLocationURLStr.length > 0);

  if (isPrematureStop || !hasUploadLocation) {
    GTMSESSION_ASSERT_DEBUG(NO, @"Premature failure: upload-status:\"%@\"  location:%@",
                            [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus],
                            uploadLocationURLStr);
    // We cannot continue since we do not know the location to use
    // as our upload destination.
    NSDictionary *userInfo = nil;
    NSData *downloadedData = self.downloadedData;
    if (downloadedData.length > 0) {
      userInfo = @{kGTMSessionFetcherStatusDataKey : downloadedData};
    }
    NSError *failureError = [self prematureFailureErrorWithUserInfo:userInfo];
    [self invokeFinalCallbackWithData:nil error:failureError shouldInvalidateLocation:YES];
    return;
  }

  self.uploadLocationURL = [NSURL URLWithString:uploadLocationURLStr];

  NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  [nc postNotificationName:kGTMSessionFetcherUploadLocationObtainedNotification object:self];

  // we've now sent all of the initial post body data, so we need to include
  // its size in future progress indicator callbacks
  [self setInitialBodySent:[self initialBodyLength]];

  // just in case the user paused us during the initial fetch...
  if (![self isPaused]) {
    [self uploadNextChunkWithOffset:0];
  }
}

- (void)URLSession:(NSURLSession *)session
                        task:(NSURLSessionTask *)task
             didSendBodyData:(int64_t)bytesSent
              totalBytesSent:(int64_t)totalBytesSent
    totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
  // Overrides the superclass.
  [self invokeDelegateWithDidSendBytes:bytesSent
                        totalBytesSent:totalBytesSent
              totalBytesExpectedToSend:totalBytesExpectedToSend + [self fullUploadLength]];
}

- (BOOL)shouldReleaseCallbacksUponCompletion {
  // Overrides the superclass.

  // We don't want the superclass to release the delegate and callback
  // blocks once the initial fetch has finished
  //
  // This is invoked for only successful completion of the connection;
  // an error always will invoke and release the callbacks
  return NO;
}

- (void)invokeFinalCallbackWithData:(NSData *)data
                              error:(NSError *)error
           shouldInvalidateLocation:(BOOL)shouldInvalidateLocation {
  dispatch_queue_t queue;
  GTMSessionFetcherCompletionHandler handler;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (shouldInvalidateLocation) {
      _uploadLocationURL = nil;
    }

    // Wait to dispatch the completion handler until after releasing callbacks. Because
    // action on the upload fetcher often takes place on a background queue, there can
    // be issues with CI tests failing due to load making the dispatched callback to
    // execute and the tests resume and assert callbacks have been released prior to that
    // actually occurring.
    //
    // However under normal operation this should also be a perfectly fine change.
    queue = _delegateCallbackQueue;
    handler = _delegateCompletionHandler;
  }  // @synchronized(self)

  [self releaseUploadAndBaseCallbacks:!self.userStoppedFetching];

  if (queue && handler) {
    [self invokeOnCallbackQueue:queue
               afterUserStopped:NO
                          block:^{
                            handler(data, error);
                          }];
  }
}

- (void)releaseUploadAndBaseCallbacks:(BOOL)shouldReleaseCancellation {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _delegateCallbackQueue = nil;
    _delegateCompletionHandler = nil;
    _uploadDataProvider = nil;
    if (shouldReleaseCancellation) {
      _cancellationHandler = nil;
    }
  }

  // Release the base class's callbacks, too, if needed.
  [self releaseCallbacks];
}

- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks {
  GTMSessionCheckNotSynchronized(self);
  [self destroyUploadRetryTimer];

  // Clear _fetcherInFlight when stopped. Moved from stopFetching, since that's a public method,
  // where this method does the work. Fixes issue clearing value when retryBlock included.
  GTMSessionFetcher *fetcherInFlight = self.fetcherInFlight;
  if (fetcherInFlight == self) {
    self.fetcherInFlight = nil;
  }

  [super stopFetchReleasingCallbacks:shouldReleaseCallbacks];

  if (shouldReleaseCallbacks) {
    [self releaseUploadAndBaseCallbacks:NO];
  }
}

#pragma mark Chunk fetching methods

- (void)uploadNextChunkWithOffset:(int64_t)offset {
  // use the properties in each chunk fetcher
  NSDictionary *props = [self properties];

  [self uploadNextChunkWithOffset:offset fetcherProperties:props];
}

- (void)sendQueryForUploadOffsetWithFetcherProperties:(NSDictionary *)props {
  GTMSessionFetcher *queryFetcher = [self uploadFetcherWithProperties:props isQueryFetch:YES];
  queryFetcher.bodyData = [NSData data];

  NSString *originalComment = self.comment;
  [queryFetcher
      setCommentWithFormat:@"%@ (query offset)", originalComment ? originalComment : @"upload"];

  [queryFetcher setRequestValue:@"query" forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];

  self.fetcherInFlight = queryFetcher;
  [queryFetcher beginFetchWithDelegate:self
                     didFinishSelector:@selector(queryFetcher:finishedWithData:error:)];
}

- (void)queryFetcher:(GTMSessionFetcher *)queryFetcher
    finishedWithData:(NSData *)data
               error:(NSError *)error {
  self.fetcherInFlight = nil;

  NSDictionary *responseHeaders = [queryFetcher responseHeaders];
  NSString *sizeReceivedHeader;

  GTMSessionUploadFetcherStatus uploadStatus =
      [[self class] uploadStatusFromResponseHeaders:responseHeaders];
  GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown || error != nil,
                          @"query fetcher completion has unexpected upload status for headers %@",
                          responseHeaders);

  if (error == nil) {
    sizeReceivedHeader = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadSizeReceived];

    if (uploadStatus == kStatusCancelled ||
        (uploadStatus == kStatusActive && sizeReceivedHeader == nil)) {
      NSDictionary *userInfo = nil;
      if (data.length > 0) {
        userInfo = @{kGTMSessionFetcherStatusDataKey : data};
      }
      error = [self prematureFailureErrorWithUserInfo:userInfo];
    }
  }

  if (error == nil) {
    int64_t offset = [sizeReceivedHeader longLongValue];
    int64_t fullUploadLength = [self fullUploadLength];
    if (uploadStatus == kStatusFinal ||
        (offset >= fullUploadLength &&
         fullUploadLength != kGTMSessionUploadFetcherUnknownFileSize)) {
      // Handle we're done
      [self chunkFetcher:queryFetcher finishedWithData:data error:nil];
    } else {
      [self retrieveUploadChunkGranularityFromResponseHeaders:responseHeaders];
      [self uploadNextChunkWithOffset:offset];
    }
  } else {
    // Handle query error
    [self chunkFetcher:queryFetcher finishedWithData:data error:error];
  }
}

- (void)sendCancelUploadWithFetcherProperties:(NSDictionary *)props {
  @synchronized(self) {
    _isCancelInFlight = YES;
  }
  GTMSessionFetcher *cancelFetcher = [self uploadFetcherWithProperties:props isQueryFetch:YES];
  cancelFetcher.bodyData = [NSData data];

  NSString *originalComment = self.comment;
  [cancelFetcher
      setCommentWithFormat:@"%@ (cancel)", originalComment ? originalComment : @"upload"];

  [cancelFetcher setRequestValue:@"cancel" forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];

  self.fetcherInFlight = cancelFetcher;
  [cancelFetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
    self.fetcherInFlight = nil;
    if (![self triggerCancellationHandlerForFetch:cancelFetcher data:data error:error]) {
      if (error) {
        GTMSESSION_LOG_DEBUG(@"cancelFetcher %@", error);
      }
    }
    @synchronized(self) {
      self->_isCancelInFlight = NO;
    }
  }];
}

- (void)uploadNextChunkWithOffset:(int64_t)offset fetcherProperties:(NSDictionary *)props {
  GTMSessionCheckNotSynchronized(self);

  // Example chunk headers:
  //  X-Goog-Upload-Command: upload, finalize
  //  X-Goog-Upload-Offset: 0
  //  Content-Length: 2000000
  //  Content-Type: image/jpeg
  //
  //  {bytes 0-1999999}

  // The chunk upload URL requires no authentication header.
  GTMSessionFetcher *chunkFetcher = [self uploadFetcherWithProperties:props isQueryFetch:NO];
  [self attachSendProgressBlockToChunkFetcher:chunkFetcher];
  int64_t chunkSize = [self updateChunkFetcher:chunkFetcher forChunkAtOffset:offset];
  BOOL isUploadingFileURL = (self.uploadFileURL != nil);
  int64_t fullUploadLength = [self fullUploadLength];

  // The chunk size may have changed, so determine again if we're uploading the full file.
  BOOL isUploadingFullFile =
      (offset == 0 && fullUploadLength != kGTMSessionUploadFetcherUnknownFileSize &&
       chunkSize >= fullUploadLength);
  if (isUploadingFullFile && isUploadingFileURL) {
    // The data is the full upload file URL.
    chunkFetcher.bodyFileURL = self.uploadFileURL;
    [self beginChunkFetcher:chunkFetcher offset:offset];
  } else {
    // Make an NSData for the subset for this upload chunk.
    self.subdataGenerating = YES;
    [self generateChunkSubdataWithOffset:offset
                                  length:chunkSize
                                response:^(NSData *chunkData, int64_t uploadFileLength,
                                           NSError *chunkError) {
                                  // The subdata methods may leave us on a background thread.
                                  dispatch_async(dispatch_get_main_queue(), ^{
                                    self.subdataGenerating = NO;

                                    // dont allow the updating of fileLength for uploads not using a
                                    // data provider as they should know the file length before the
                                    // upload starts.
                                    if (self.uploadDataProvider != nil && uploadFileLength > 0) {
                                      [self setUploadFileLength:uploadFileLength];
                                      // Update the command and content-length headers if this is
                                      // the last chunk to be sent.
                                      if (offset + chunkSize >= uploadFileLength) {
                                        int64_t updatedChunkSize =
                                            [self updateChunkFetcher:chunkFetcher
                                                    forChunkAtOffset:offset];
                                        if (updatedChunkSize == 0) {
                                          // Calling beginChunkFetcher early when there is no more
                                          // data to send allows us to properly handle nil chunkData
                                          // below without having to account for the case where we
                                          // are just finalizing the file.
                                          chunkFetcher.bodyData = [[NSData alloc] init];
                                          [self beginChunkFetcher:chunkFetcher offset:offset];
                                          return;
                                        }
                                      }
                                    }

                                    if (chunkData == nil) {
                                      NSError *responseError = chunkError;
                                      if (!responseError) {
                                        responseError =
                                            [self uploadChunkUnavailableErrorWithDescription:
                                                      @"chunkData is nil"];
                                      }
                                      [self invokeFinalCallbackWithData:nil
                                                                  error:responseError
                                               shouldInvalidateLocation:YES];
                                      return;
                                    }

                                    BOOL didWriteFile = NO;
                                    if (isUploadingFileURL) {
                                      // Make a temporary file with the data subset.
                                      NSString *tempName =
                                          [NSString stringWithFormat:@"GTMUpload_temp_%@",
                                                                     [[NSUUID UUID] UUIDString]];
                                      NSString *tempPath = [NSTemporaryDirectory()
                                          stringByAppendingPathComponent:tempName];
                                      NSError *writeError;
                                      didWriteFile = [chunkData writeToFile:tempPath
                                                                    options:NSDataWritingAtomic
                                                                      error:&writeError];
                                      if (didWriteFile) {
                                        chunkFetcher.bodyFileURL = [NSURL fileURLWithPath:tempPath];
                                      } else {
                                        GTMSESSION_LOG_DEBUG(@"writeToFile failed: %@\n%@",
                                                             writeError, tempPath);
                                      }
                                    }
                                    if (!didWriteFile) {
                                      chunkFetcher.bodyData = [chunkData copy];
                                    }
                                    [self beginChunkFetcher:chunkFetcher offset:offset];
                                  });
                                }];
  }
}

- (void)beginUploadRetryTimer {
  if (![NSThread isMainThread]) {
    // Defer creating and starting the timer until we're on the main thread to ensure it has
    // a run loop.
    dispatch_async(dispatch_get_main_queue(), ^{
      [self beginUploadRetryTimer];
    });
    return;
  }

  [self destroyUploadRetryTimer];

  if (_nextUploadRetryInterval == 0.0) {
    [self.chunkFetcher beginFetchWithDelegate:self
                            didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
    return;
  }
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    NSTimeInterval nextInterval = _nextUploadRetryInterval;
    NSTimeInterval maxInterval = _maxUploadRetryInterval;
    NSTimeInterval newInterval = MIN(nextInterval, (maxInterval > 0 ? maxInterval : DBL_MAX));
    NSTimeInterval newIntervalTolerance = (newInterval / 10) > 1.0 ?: 1.0;

    _nextUploadRetryInterval = newInterval;

    _uploadRetryTimer = [NSTimer timerWithTimeInterval:newInterval
                                                target:self
                                              selector:@selector(uploadRetryTimerFired:)
                                              userInfo:nil
                                               repeats:NO];
    _uploadRetryTimer.tolerance = newIntervalTolerance;
    [[NSRunLoop mainRunLoop] addTimer:_uploadRetryTimer forMode:NSDefaultRunLoopMode];
  }  // @synchronized(self)
  NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  [nc postNotificationName:kGTMSessionFetcherUploadInitialBackoffStartedNotification object:self];
}

- (void)uploadRetryTimerFired:(NSTimer *)timer {
  [self destroyUploadRetryTimer];

  NSOperationQueue *queue = self.sessionDelegateQueue;
  [queue addOperationWithBlock:^{
    [self.chunkFetcher beginFetchWithDelegate:self
                            didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
  }];
}

- (NSTimer *)uploadRetryTimer {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _uploadRetryTimer;
  }  // @synchronized(self)
}

- (NSTimeInterval)maxUploadRetryInterval {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _maxUploadRetryInterval;
  }  // @synchronized(self)
}
- (void)setMaxUploadRetryInterval:(NSTimeInterval)secs {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (secs > 0) {
      _maxUploadRetryInterval = secs;
    } else {
      _maxUploadRetryInterval = kDefaultMaxUploadRetryInterval;
    }
  }  // @synchronized(self)
}
- (void)setMinUploadRetryInterval:(NSTimeInterval)secs {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (secs > 0) {
      _minUploadRetryInterval = secs;
    } else {
      _minUploadRetryInterval = kDefaultMinUploadRetryInterval;
    }
  }  // @synchronized(self)
}

- (NSTimeInterval)minUploadRetryInterval {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _minUploadRetryInterval;
  }  // @synchronized(self)
}

- (void)beginChunkFetcher:(GTMSessionFetcher *)chunkFetcher offset:(int64_t)offset {
  // Track the current offset for progress reporting
  self.currentOffset = offset;

  // Hang on to the fetcher in case we need to cancel it.  We set these before beginning the
  // chunk fetch so the observers notified of chunk fetches can inspect the upload fetcher to
  // match to the chunk.
  self.chunkFetcher = chunkFetcher;
  self.fetcherInFlight = chunkFetcher;

  // Update the last chunk request, including any request headers.
  self.lastChunkRequest = chunkFetcher.request;

  if (_nextUploadRetryInterval < _maxUploadRetryInterval) {
    [self beginUploadRetryTimer];

  } else {
    NSError *responseError =
        [self uploadChunkUnavailableErrorWithDescription:@"Retry Limit Reached"];
    [self invokeFinalCallbackWithData:nil error:responseError shouldInvalidateLocation:NO];
  }
}

- (void)attachSendProgressBlockToChunkFetcher:(GTMSessionFetcher *)chunkFetcher {
  chunkFetcher.sendProgressBlock =
      ^(int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
        // The total bytes expected include the initial body and the full chunked
        // data, independent of how big this fetcher's chunk is.
        int64_t initialBodySent = [self bodyLength];  // TODO(grobbins) use [self initialBodySent]
        int64_t totalSent = initialBodySent + self.currentOffset + totalBytesSent;
        int64_t totalExpected = initialBodySent + [self fullUploadLength];

        [self invokeDelegateWithDidSendBytes:bytesSent
                              totalBytesSent:totalSent
                    totalBytesExpectedToSend:totalExpected];
      };
}

- (NSDictionary *)uploadSessionIdentifierMetadata {
  NSMutableDictionary *metadata = [NSMutableDictionary dictionary];
  metadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] = @YES;
  GTMSESSION_ASSERT_DEBUG(self.uploadFileURL,
                          @"Invalid upload fetcher to create session identifier for metadata");
  metadata[kGTMSessionIdentifierUploadFileURLMetadataKey] = [self.uploadFileURL absoluteString];
  metadata[kGTMSessionIdentifierUploadFileLengthMetadataKey] = @([self fullUploadLength]);

  if (self.uploadLocationURL) {
    metadata[kGTMSessionIdentifierUploadLocationURLMetadataKey] =
        [self.uploadLocationURL absoluteString];
  }
  if (self.uploadMIMEType) {
    metadata[kGTMSessionIdentifierUploadMIMETypeMetadataKey] = self.uploadMIMEType;
  }
  metadata[kGTMSessionIdentifierUploadChunkSizeMetadataKey] = @(self.chunkSize);
  metadata[kGTMSessionIdentifierUploadCurrentOffsetMetadataKey] = @(self.currentOffset);
  metadata[kGTMSessionIdentifierUploadAllowsCellularAccess] = @(self.request.allowsCellularAccess);

  return metadata;
}

- (GTMSessionFetcher *)uploadFetcherWithProperties:(NSDictionary *)properties
                                      isQueryFetch:(BOOL)isQueryFetch {
  GTMSessionCheckNotSynchronized(self);

  // Common code to make a request for a query command or for a chunk upload.
  NSURL *uploadLocationURL = self.uploadLocationURL;
  NSMutableURLRequest *chunkRequest = [NSMutableURLRequest requestWithURL:uploadLocationURL];
  [chunkRequest setHTTPMethod:@"PUT"];

  // copy the user-agent from the original connection
  // n.b. that self.request is nil for upload fetchers created with an existing upload location
  // URL.
  NSURLRequest *origRequest = self.request;

  chunkRequest.allowsCellularAccess = origRequest.allowsCellularAccess;
  if (!origRequest) {
    chunkRequest.allowsCellularAccess = _allowsCellularAccess;
  }
  NSString *userAgent = [origRequest valueForHTTPHeaderField:@"User-Agent"];
  if (userAgent.length > 0) {
    [chunkRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
  }

  [chunkRequest setValue:kGTMSessionXGoogUploadProtocolResumable
      forHTTPHeaderField:kGTMSessionHeaderXGoogUploadProtocol];

  // To avoid timeouts when debugging, copy the timeout of the initial fetcher.
  NSTimeInterval origTimeout = [origRequest timeoutInterval];
  [chunkRequest setTimeoutInterval:origTimeout];

  //
  // Make a new chunk fetcher.
  //
  GTMSessionFetcher *chunkFetcher = [GTMSessionFetcher fetcherWithRequest:chunkRequest];
  chunkFetcher.callbackQueue = self.callbackQueue;
  chunkFetcher.sessionUserInfo = self.sessionUserInfo;
  chunkFetcher.configurationBlock = self.configurationBlock;
  chunkFetcher.allowedInsecureSchemes = self.allowedInsecureSchemes;
  chunkFetcher.allowLocalhostRequest = self.allowLocalhostRequest;
  chunkFetcher.allowInvalidServerCertificates = self.allowInvalidServerCertificates;
  chunkFetcher.stopFetchingTriggersCompletionHandler = self.stopFetchingTriggersCompletionHandler;
  chunkFetcher.useUploadTask = !isQueryFetch;

  if (self.uploadFileURL && !isQueryFetch && self.useBackgroundSession) {
    [chunkFetcher createSessionIdentifierWithMetadata:[self uploadSessionIdentifierMetadata]];
  }

  // Give the chunk fetcher the same properties as the previous chunk fetcher
  chunkFetcher.properties = [properties mutableCopy];
  [chunkFetcher setProperty:[NSValue valueWithNonretainedObject:self]
                     forKey:kGTMSessionUploadFetcherChunkParentKey];

  // copy other fetcher settings to the new fetcher
  chunkFetcher.retryEnabled = self.retryEnabled;
  chunkFetcher.maxRetryInterval = self.maxRetryInterval;

  if ([self isRetryEnabled]) {
    // We interpose our own retry method both so we can change the request to ask the server to
    // tell us where to resume the chunk.
    chunkFetcher.retryBlock =
        ^(BOOL suggestedWillRetry, NSError *chunkError, GTMSessionFetcherRetryResponse response) {
          void (^finish)(BOOL) = ^(BOOL shouldRetry) {
            // We'll retry by sending an offset query.
            if (shouldRetry) {
              self.shouldInitiateOffsetQuery = !isQueryFetch;

              // We don't know what our actual offset is anymore, but the server will tell us.
              self.currentOffset = 0;
            }
            // We don't actually want to retry this specific fetcher.
            response(NO);
          };

          GTMSessionFetcherRetryBlock retryBlock = self.retryBlock;
          if (retryBlock) {
            // Ask the client, then call the finish block above.
            retryBlock(suggestedWillRetry, chunkError, finish);
          } else {
            finish(suggestedWillRetry);
          }
        };
  }

  return chunkFetcher;
}

- (void)chunkFetcher:(GTMSessionFetcher *)chunkFetcher
    finishedWithData:(NSData *)data
               error:(NSError *)error {
  BOOL hasDestroyedOldChunkFetcher = NO;
  self.fetcherInFlight = nil;

  NSDictionary *responseHeaders = [chunkFetcher responseHeaders];
  GTMSessionUploadFetcherStatus uploadStatus =
      [[self class] uploadStatusFromResponseHeaders:responseHeaders];
  GTMSESSION_ASSERT_DEBUG(
      uploadStatus != kStatusUnknown || error != nil || self.wasCreatedFromBackgroundSession,
      @"chunk fetcher completion has kStatusUnknown upload status for headers %@ fetcher %@",
      responseHeaders, self);
  BOOL isUploadStatusStopped = (uploadStatus == kStatusFinal || uploadStatus == kStatusCancelled);

  // Check if the fetcher was actually querying. If it failed, do not retry,
  // as it would enter an infinite retry loop.
  NSString *uploadCommand =
      chunkFetcher.request.allHTTPHeaderFields[kGTMSessionHeaderXGoogUploadCommand];
  BOOL isQueryFetch = [uploadCommand isEqual:@"query"];

  // TODO
  // Maybe here we can check to see if the request had x goog content length set. (the file length
  // one).
  NSString *previousContentLengthValue =
      [chunkFetcher.request valueForHTTPHeaderField:@"Content-Length"];
  // The Content-Length header may not be present if the chunk fetcher was recreated from
  // a background session.
  BOOL hasKnownChunkSize = (previousContentLengthValue != nil);
  int64_t previousContentLength = [previousContentLengthValue longLongValue];

  BOOL needsQuery = (!hasKnownChunkSize && !isUploadStatusStopped);

  if (error || (needsQuery && !isQueryFetch)) {
    NSInteger status = error.code;

    // Status 4xx indicates a bad offset in the Google upload protocol. However, do not retry status
    // 404 per spec, nor if the upload size appears to have been zero (since the server will just
    // keep asking us to retry.)
    if (self.shouldInitiateOffsetQuery || (needsQuery && !isQueryFetch) ||
        ([error.domain isEqual:kGTMSessionFetcherStatusDomain] && status >= 400 && status <= 499 &&
         status != 404 && uploadStatus == kStatusActive && previousContentLength > 0)) {
      self.shouldInitiateOffsetQuery = NO;
      [self destroyChunkFetcher];
      hasDestroyedOldChunkFetcher = YES;
      @synchronized(self) {
        GTMSessionMonitorSynchronized(self);
        _nextUploadRetryInterval = self.nextUploadRetryIntervalUnsynchronized;
      }

      [self sendQueryForUploadOffsetWithFetcherProperties:chunkFetcher.properties];
    } else {
      // Some unexpected status has occurred; handle it as we would a regular
      // object fetcher failure.
      [self invokeFinalCallbackWithData:data error:error shouldInvalidateLocation:NO];
    }
  } else {
    int64_t newOffset;
    // The chunk has uploaded successfully.
    NSString *uploadSizeReceived =
        [chunkFetcher.responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadSizeReceived];

    if (uploadSizeReceived) {
      newOffset = [uploadSizeReceived longLongValue];
    } else {
      newOffset = self.currentOffset + previousContentLength;
    }
#if DEBUG
    // Verify that if we think all of the uploading data has been sent, the server responded with
    // the "final" upload status.
    __unused BOOL hasUploadAllData = (newOffset == [self fullUploadLength]);
    __unused BOOL isFinalStatus = (uploadStatus == kStatusFinal);
    GTMSESSION_ASSERT_DEBUG(hasUploadAllData == isFinalStatus || !hasKnownChunkSize,
                            @"uploadStatus:%@  newOffset:%lld (%lld + %lld)  fullUploadLength:%lld"
                            @" chunkFetcher:%@ requestHeaders:%@ responseHeaders:%@",
                            [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus],
                            newOffset, self.currentOffset, previousContentLength,
                            [self fullUploadLength], chunkFetcher,
                            chunkFetcher.request.allHTTPHeaderFields, responseHeaders);
#endif
    if (isUploadStatusStopped || (!_uploadData && _uploadFileLength == 0) ||
        (_currentOffset > _uploadFileLength && _uploadFileLength > 0)) {
      // This was the last chunk.
      if (error == nil && uploadStatus == kStatusCancelled) {
        // Report cancelled status as an error.
        NSDictionary *userInfo = nil;
        if (data.length > 0) {
          userInfo = @{kGTMSessionFetcherStatusDataKey : data};
        }
        data = nil;
        error = [self prematureFailureErrorWithUserInfo:userInfo];
      } else {
        // The upload is in final status.
        //
        // Take the chunk fetcher's data as the superclass data.
        self.downloadedData = data;
        self.statusCode = chunkFetcher.statusCode;
      }

      // we're done
      [self invokeFinalCallbackWithData:data error:error shouldInvalidateLocation:YES];
    } else {
      // Start the next chunk.
      self.currentOffset = newOffset;

      // We want to destroy this chunk fetcher before creating the next one, but
      // we want to pass on its properties
      NSDictionary *props = [chunkFetcher properties];

      // We no longer need to be able to cancel this chunkFetcher.  Destroy it
      // before we create a new chunk fetcher.
      [self destroyChunkFetcher];
      hasDestroyedOldChunkFetcher = YES;
      [self uploadNextChunkWithOffset:newOffset fetcherProperties:props];
    }
  }
  if (!hasDestroyedOldChunkFetcher) {
    [self destroyChunkFetcher];
  }
}

- (void)destroyChunkFetcher {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_fetcherInFlight == _chunkFetcher) {
      _fetcherInFlight = nil;
    }

    [_chunkFetcher stopFetching];

    NSURL *chunkFileURL = _chunkFetcher.bodyFileURL;
    BOOL wasTemporaryUploadFile = ![chunkFileURL isEqual:_uploadFileURL];
    if (wasTemporaryUploadFile) {
      NSError *error;
      [[NSFileManager defaultManager] removeItemAtURL:chunkFileURL error:&error];
      if (error) {
        GTMSESSION_LOG_DEBUG(@"removingItemAtURL failed: %@\n%@", error, chunkFileURL);
      }
    }

    _recentChunkReponseHeaders = _chunkFetcher.responseHeaders;

    // To avoid retain cycles, remove all properties except the parent identifier.
    _chunkFetcher.properties =
        @{kGTMSessionUploadFetcherChunkParentKey : [NSValue valueWithNonretainedObject:self]};

    _chunkFetcher.retryBlock = nil;
    _chunkFetcher.sendProgressBlock = nil;
    _chunkFetcher = nil;
  }  // @synchronized(self)
}

// This method calculates the proper values to pass to the client's send progress block.
//
// The actual total bytes sent include the initial body sent, plus the
// offset into the batched data prior to the current chunk fetcher

- (void)invokeDelegateWithDidSendBytes:(int64_t)bytesSent
                        totalBytesSent:(int64_t)totalBytesSent
              totalBytesExpectedToSend:(int64_t)totalBytesExpected {
  GTMSessionCheckNotSynchronized(self);

  // The clang included with Xcode 13.3 betas added a -Wunused-but-set-variable warning,
  // which doesn't (yet) skip variables annotated with objc_precie_lifetime. Since that
  // warning is not available in all Xcodes, turn off the -Wunused warning group entirely.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused"
  // Ensure the chunk fetcher survives the callback in case the user pauses the upload process.
  __block GTMSessionFetcher *holdFetcher = self.chunkFetcher;
#pragma clang diagnostic pop

  [self invokeOnCallbackQueue:self.delegateCallbackQueue
             afterUserStopped:NO
                        block:^{
                          GTMSessionFetcherSendProgressBlock sendProgressBlock =
                              self.sendProgressBlock;
                          if (sendProgressBlock) {
                            sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpected);
                          }
                          holdFetcher = nil;
                        }];
}

- (void)retrieveUploadChunkGranularityFromResponseHeaders:(NSDictionary *)responseHeaders {
  GTMSessionCheckNotSynchronized(self);

  // Standard granularity for Google uploads is 256K.
  NSString *chunkGranularityHeader =
      [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadChunkGranularity];
  self.uploadGranularity = chunkGranularityHeader.longLongValue;
}

#pragma mark -

- (BOOL)isPaused {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _isPaused;
  }  // @synchronized(self)
}

- (void)pauseFetching {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _isPaused = YES;
  }  // @synchronized(self)

  // Pausing just means stopping the current chunk from uploading;
  // when we resume, we will send a query request to the server to
  // figure out what bytes to resume sending.
  //
  // We won't try to cancel the initial data upload, but rather will check
  // for being paused in beginChunkFetches.
  [self destroyChunkFetcher];
}

- (void)resumeFetching {
  BOOL wasPaused;

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    wasPaused = _isPaused;
    _isPaused = NO;
  }  // @synchronized(self)

  if (wasPaused) {
    [self sendQueryForUploadOffsetWithFetcherProperties:self.properties];
  }
}

- (void)stopFetching {
  // Overrides the superclass
  [self destroyChunkFetcher];

  // If we think the server is waiting for more data, then tell it there won't be more.
  if (self.uploadLocationURL) {
    [self sendCancelUploadWithFetcherProperties:[self properties]];
    self.uploadLocationURL = nil;
  } else {
    [self invokeOnCallbackQueue:self.callbackQueue
               afterUserStopped:YES
                          block:^{
                            // Repeated calls to stopFetching may cause this path to be reached
                            // despite having sent a real cancel request, check here to ensure that
                            // the cancellation handler invocation which fires will definitely be
                            // for the real request sent previously.
                            @synchronized(self) {
                              if (self->_isCancelInFlight) {
                                return;
                              }
                            }
                            [self triggerCancellationHandlerForFetch:nil data:nil error:nil];
                          }];
  }

  [super stopFetching];
}

// Fires the cancellation handler, returning whether there was a handler to be fired.
- (BOOL)triggerCancellationHandlerForFetch:(GTMSessionFetcher *)fetcher
                                      data:(NSData *)data
                                     error:(NSError *)error {
  GTMSessionUploadFetcherCancellationHandler handler = self.cancellationHandler;
  if (handler) {
    handler(fetcher, data, error);
    self.cancellationHandler = nil;
    return YES;
  }
  return NO;
}

#pragma mark -

- (int64_t)updateChunkFetcher:(GTMSessionFetcher *)chunkFetcher forChunkAtOffset:(int64_t)offset {
  BOOL isUploadingFileURL = (self.uploadFileURL != nil);

  // Upload another chunk, meeting server-required granularity.
  int64_t chunkSize = self.chunkSize;

  int64_t fullUploadLength = [self fullUploadLength];
  BOOL isFileLengthKnown = fullUploadLength >= 0;

  BOOL isUploadingFullFile = (offset == 0 && isFileLengthKnown && chunkSize >= fullUploadLength);
  if (!isUploadingFileURL || !isUploadingFullFile) {
    // We're not uploading the entire file and given the file URL.  Since we'll be
    // allocating a subdata block for a chunk, we need to bound it to something that
    // won't blow the process's memory.
    if (chunkSize > kGTMSessionUploadFetcherMaximumDemandBufferSize) {
      chunkSize = kGTMSessionUploadFetcherMaximumDemandBufferSize;
    }
  }

  int64_t granularity = self.uploadGranularity;
  if (granularity > 0) {
    if (chunkSize < granularity) {
      chunkSize = granularity;
    } else {
      chunkSize = chunkSize - (chunkSize % granularity);
    }
  }

  GTMSESSION_ASSERT_DEBUG(offset <= fullUploadLength || fullUploadLength == 0,
                          @"offset %lld exceeds data length %lld", offset, fullUploadLength);

  if (granularity > 0 && offset < fullUploadLength) {
    offset = offset - (offset % granularity);
  }

  // If the chunk size is bigger than the remaining data, or else
  // it's close enough in size to the remaining data that we'd rather
  // avoid having a whole extra http fetch for the leftover bit, then make
  // this chunk size exactly match the remaining data size
  NSString *command;
  int64_t thisChunkSize = chunkSize;

  BOOL isChunkTooBig = (thisChunkSize >= (fullUploadLength - offset));
  BOOL isChunkAlmostBigEnough = (fullUploadLength - offset - 2500 < thisChunkSize);
  BOOL isFinalChunk = (isChunkTooBig || isChunkAlmostBigEnough) && isFileLengthKnown;
  if (isFinalChunk) {
    thisChunkSize = fullUploadLength - offset;
    if (thisChunkSize > 0) {
      command = @"upload, finalize";
    } else {
      command = @"finalize";
    }
  } else {
    command = @"upload";
  }
  NSString *lengthStr = @(thisChunkSize).stringValue;
  NSString *offsetStr = @(offset).stringValue;

  [chunkFetcher setRequestValue:command forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
  [chunkFetcher setRequestValue:lengthStr forHTTPHeaderField:@"Content-Length"];
  [chunkFetcher setRequestValue:offsetStr forHTTPHeaderField:kGTMSessionHeaderXGoogUploadOffset];
  if (_uploadFileLength != kGTMSessionUploadFetcherUnknownFileSize) {
    [chunkFetcher setRequestValue:@([self fullUploadLength]).stringValue
               forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentLength];
  }

  // Append the range of bytes in this chunk to the fetcher comment.
  NSString *baseComment = self.comment;
  [chunkFetcher setCommentWithFormat:@"%@ (%lld-%lld)", baseComment ? baseComment : @"upload",
                                     offset, MAX(0, offset + thisChunkSize - 1)];

  return thisChunkSize;
}

// Public properties.
// clang-format off
@synthesize currentOffset = _currentOffset,
            allowsCellularAccess = _allowsCellularAccess,
            delegateCompletionHandler = _delegateCompletionHandler,
            chunkFetcher = _chunkFetcher,
            lastChunkRequest = _lastChunkRequest,
            subdataGenerating = _subdataGenerating,
            shouldInitiateOffsetQuery = _shouldInitiateOffsetQuery,
            uploadGranularity = _uploadGranularity,
            uploadRetryFactor = _uploadRetryFactor;
// clang-format on

// Internal properties.
@dynamic fetcherInFlight;
@dynamic activeFetcher;
@dynamic statusCode;
@dynamic delegateCallbackQueue;

+ (void)removePointer:(void *)pointer fromPointerArray:(NSPointerArray *)pointerArray {
  for (NSUInteger index = 0, count = pointerArray.count; index < count; ++index) {
    void *pointerAtIndex = [pointerArray pointerAtIndex:index];
    if (pointerAtIndex == pointer) {
      [pointerArray removePointerAtIndex:index];
      return;
    }
  }
}

- (BOOL)useBackgroundSession {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _useBackgroundSessionOnChunkFetchers;
  }  // @synchronized(self
}

- (void)setUseBackgroundSession:(BOOL)useBackgroundSession {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_useBackgroundSessionOnChunkFetchers != useBackgroundSession) {
      _useBackgroundSessionOnChunkFetchers = useBackgroundSession;
      NSPointerArray *uploadFetcherPointerArrayForBackgroundSessions =
          [[self class] uploadFetcherPointerArrayForBackgroundSessions];
      @synchronized(uploadFetcherPointerArrayForBackgroundSessions) {
        if (_useBackgroundSessionOnChunkFetchers) {
          [uploadFetcherPointerArrayForBackgroundSessions addPointer:(__bridge void *)self];
        } else {
          [[self class] removePointer:(__bridge void *)self
                     fromPointerArray:uploadFetcherPointerArrayForBackgroundSessions];
        }
      }  // @synchronized(uploadFetcherPointerArrayForBackgroundSessions)
    }
  }  // @synchronized(self)
}

- (BOOL)canFetchWithBackgroundSession {
  // The initial upload fetcher is always a foreground session; the
  // useBackgroundSession property will apply only to chunk fetchers,
  // not to queries.
  return NO;
}

- (NSDictionary *)responseHeaders {
  GTMSessionCheckNotSynchronized(self);
  // Overrides the superclass

  // If asked for the fetcher's response, use the most recent chunk fetcher's response,
  // since the original request's response lacks useful information like the actual
  // Content-Type.
  NSDictionary *dict = self.chunkFetcher.responseHeaders;
  if (dict) {
    return dict;
  }

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_recentChunkReponseHeaders) {
      return _recentChunkReponseHeaders;
    }
  }  // @synchronized(self

  // No chunk fetcher yet completed, so return whatever we have from the initial fetch.
  return [super responseHeaders];
}

- (NSInteger)statusCodeUnsynchronized {
  GTMSessionCheckSynchronized(self);

  if (_recentChunkStatusCode != -1) {
    // Overrides the superclass to indicate status appropriate to the initial
    // or latest chunk fetch
    return _recentChunkStatusCode;
  } else {
    return [super statusCodeUnsynchronized];
  }
}

- (void)setStatusCode:(NSInteger)val {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _recentChunkStatusCode = val;
  }
}

- (int64_t)initialBodyLength {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _initialBodyLength;
  }
}

- (void)setInitialBodyLength:(int64_t)length {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _initialBodyLength = length;
  }
}

- (int64_t)initialBodySent {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _initialBodySent;
  }
}

- (void)setInitialBodySent:(int64_t)length {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _initialBodySent = length;
  }
}

- (NSURL *)uploadLocationURL {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _uploadLocationURL;
  }
}

- (void)setUploadLocationURL:(NSURL *)locationURL {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _uploadLocationURL = locationURL;
  }
}

- (GTMSessionFetcher *)activeFetcher {
  GTMSessionFetcher *result = self.fetcherInFlight;
  if (result) return result;

  return self;
}

- (BOOL)isFetching {
  // If there is an active chunk fetcher, then the upload fetcher is considered
  // to still be fetching.
  if (self.fetcherInFlight != nil) return YES;

  return [super isFetching];
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds {
  NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];

  while (self.fetcherInFlight || self.subdataGenerating) {
    if ([timeoutDate timeIntervalSinceNow] < 0) return NO;

    if (self.subdataGenerating) {
      // Allow time for subdata generation.
      NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001];
      [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
    } else {
      // Wait for any chunk or query fetchers that still have pending callbacks or
      // notifications.
      BOOL timedOut;

      if (self.fetcherInFlight == self) {
        timedOut = ![super waitForCompletionWithTimeout:timeoutInSeconds];
      } else {
        timedOut = ![self.fetcherInFlight waitForCompletionWithTimeout:timeoutInSeconds];
      }
      if (timedOut) return NO;
    }
  }
  return YES;
}
#pragma clang diagnostic pop

@end

@implementation GTMSessionFetcher (GTMSessionUploadFetcherMethods)

- (GTMSessionUploadFetcher *)parentUploadFetcher {
  NSValue *property = [self propertyForKey:kGTMSessionUploadFetcherChunkParentKey];
  if (!property) return nil;

  GTMSessionUploadFetcher *uploadFetcher = property.nonretainedObjectValue;

  GTMSESSION_ASSERT_DEBUG([uploadFetcher isKindOfClass:[GTMSessionUploadFetcher class]],
                          @"Unexpected parent upload fetcher class: %@", [uploadFetcher class]);
  return uploadFetcher;
}

@end
