// Copyright 2020 Google LLC
//
// 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.

#import "FirebasePerformance/Sources/Instrumentation/Network/Delegates/FPRNSURLConnectionDelegateInstrument.h"

#import "FirebasePerformance/Sources/Instrumentation/FPRClassInstrumentor.h"
#import "FirebasePerformance/Sources/Instrumentation/FPRInstrument_Private.h"
#import "FirebasePerformance/Sources/Instrumentation/FPRNetworkTrace.h"
#import "FirebasePerformance/Sources/Instrumentation/FPRSelectorInstrumentor.h"
#import "FirebasePerformance/Sources/Instrumentation/Network/Delegates/FPRNSURLConnectionDelegate.h"
#import "FirebasePerformance/Sources/Instrumentation/Network/FPRNetworkInstrumentHelpers.h"

#pragma mark - NSURLConnectionDelegate methods

/** Returns the dispatch queue for all instrumentation to occur on. */
static dispatch_queue_t GetInstrumentationQueue() {
  static dispatch_queue_t queue;
  static dispatch_once_t token;
  dispatch_once(&token, ^{
    queue = dispatch_queue_create("com.google.FPRNSURLConnectionDelegateInstrument",
                                  DISPATCH_QUEUE_SERIAL);
  });
  return queue;
}

/** Instruments connection:didFailWithError:.
 *
 *  @param instrumentor The FPRClassInstrumentor to add the selector instrumentor to.
 */
FOUNDATION_STATIC_INLINE
NS_EXTENSION_UNAVAILABLE("Firebase Performance is not supported for extensions.")
void InstrumentConnectionDidFailWithError(FPRClassInstrumentor *instrumentor) {
  SEL selector = @selector(connection:didFailWithError:);
  FPRSelectorInstrumentor *selectorInstrumentor =
      [instrumentor instrumentorForInstanceSelector:selector];
  if (selectorInstrumentor) {
    IMP currentIMP = selectorInstrumentor.currentIMP;
    [selectorInstrumentor
        setReplacingBlock:^(id object, NSURLConnection *connection, NSError *error) {
          FPRNetworkTrace *trace = [FPRNetworkTrace networkTraceFromObject:connection];
          [trace didCompleteRequestWithResponse:nil error:error];
          [FPRNetworkTrace removeNetworkTraceFromObject:connection];
          typedef void (*OriginalImp)(id, SEL, NSURLConnection *, NSError *);
          ((OriginalImp)currentIMP)(object, selector, connection, error);
        }];
  }
}

#pragma mark - NSURLConnectionDataDelegate methods

/** Instruments connection:willSendRequest:redirectResponse:.
 *
 *  @param instrumentor The FPRClassInstrumentor to add the selector instrumentor to.
 */
FOUNDATION_STATIC_INLINE
NS_EXTENSION_UNAVAILABLE("Firebase Performance is not supported for extensions.")
void InstrumentConnectionWillSendRequestRedirectResponse(FPRClassInstrumentor *instrumentor) {
  SEL selector = @selector(connection:willSendRequest:redirectResponse:);
  FPRSelectorInstrumentor *selectorInstrumentor =
      [instrumentor instrumentorForInstanceSelector:selector];
  if (selectorInstrumentor) {
    IMP currentIMP = selectorInstrumentor.currentIMP;
    [selectorInstrumentor setReplacingBlock:^(id object, NSURLConnection *connection,
                                              NSURLRequest *request, NSURLResponse *response) {
      FPRNetworkTrace *trace = [FPRNetworkTrace networkTraceFromObject:connection];
      [trace checkpointState:FPRNetworkTraceCheckpointStateResponseReceived];
      typedef NSURLRequest *(*OriginalImp)(id, SEL, NSURLConnection *, NSURLRequest *,
                                           NSURLResponse *);
      return ((OriginalImp)currentIMP)(object, selector, connection, request, response);
    }];
  }
}

/** Instruments connection:didReceiveResponse:.
 *
 *  @param instrumentor The FPRClassInstrumentor to add the selector instrumentor to.
 */
FOUNDATION_STATIC_INLINE
NS_EXTENSION_UNAVAILABLE("Firebase Performance is not supported for extensions.")
void InstrumentConnectionDidReceiveResponse(FPRClassInstrumentor *instrumentor) {
  SEL selector = @selector(connection:didReceiveResponse:);
  FPRSelectorInstrumentor *selectorInstrumentor =
      [instrumentor instrumentorForInstanceSelector:selector];
  if (selectorInstrumentor) {
    IMP currentIMP = selectorInstrumentor.currentIMP;
    [selectorInstrumentor
        setReplacingBlock:^(id object, NSURLConnection *connection, NSURLResponse *response) {
          FPRNetworkTrace *trace = [FPRNetworkTrace networkTraceFromObject:connection];
          if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
            trace.responseCode = (int32_t)((NSHTTPURLResponse *)response).statusCode;
          }
          [trace checkpointState:FPRNetworkTraceCheckpointStateResponseReceived];
          typedef void (*OriginalImp)(id, SEL, NSURLConnection *, NSURLResponse *);
          ((OriginalImp)currentIMP)(object, selector, connection, response);
        }];
  }
}

/** Instruments connection:didReceiveData:.
 *
 *  @param instrumentor The FPRClassInstrumentor to add the selector instrumentor to.
 */
FOUNDATION_STATIC_INLINE
NS_EXTENSION_UNAVAILABLE("Firebase Performance is not supported for extensions.")
void InstrumentConnectionDidReceiveData(FPRClassInstrumentor *instrumentor) {
  SEL selector = @selector(connection:didReceiveData:);
  FPRSelectorInstrumentor *selectorInstrumentor =
      [instrumentor instrumentorForInstanceSelector:selector];
  if (selectorInstrumentor) {
    IMP currentIMP = selectorInstrumentor.currentIMP;
    [selectorInstrumentor
        setReplacingBlock:^(id object, NSURLConnection *connection, NSData *data) {
          FPRNetworkTrace *trace = [FPRNetworkTrace networkTraceFromObject:connection];
          [trace checkpointState:FPRNetworkTraceCheckpointStateResponseReceived];
          trace.responseSize += data.length;
          typedef void (*OriginalImp)(id, SEL, NSURLConnection *, NSData *);
          ((OriginalImp)currentIMP)(object, selector, connection, data);
        }];
  }
}

/** Instruments connection:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite:.
 *
 *  @param instrumentor The FPRClassInstrumentor to add the selector instrumentor to.
 */
FOUNDATION_STATIC_INLINE
NS_EXTENSION_UNAVAILABLE("Firebase Performance is not supported for extensions.")
void InstrumentConnectionAllTheTotals(FPRClassInstrumentor *instrumentor) {
  SEL selector = @selector(connection:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite:);
  FPRSelectorInstrumentor *selectorInstrumentor =
      [instrumentor instrumentorForInstanceSelector:selector];
  if (selectorInstrumentor) {
    IMP currentIMP = selectorInstrumentor.currentIMP;
    [selectorInstrumentor
        setReplacingBlock:^(id object, NSURLConnection *connection, NSInteger bytesWritten,
                            NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite) {
          FPRNetworkTrace *trace = [FPRNetworkTrace networkTraceFromObject:connection];
          trace.requestSize = totalBytesWritten;
          if (totalBytesWritten >= totalBytesExpectedToWrite) {
            [trace checkpointState:FPRNetworkTraceCheckpointStateRequestCompleted];
          }
          typedef void (*OriginalImp)(id, SEL, NSURLConnection *, NSInteger, NSInteger, NSInteger);
          ((OriginalImp)currentIMP)(object, selector, connection, bytesWritten, totalBytesWritten,
                                    totalBytesExpectedToWrite);
        }];
  }
}

/** Instruments connectionDidFinishLoading:.
 *
 *  @param instrumentor The FPRClassInstrumentor to add the selector instrumentor to.
 */
FOUNDATION_STATIC_INLINE
NS_EXTENSION_UNAVAILABLE("Firebase Performance is not supported for extensions.")
void InstrumentConnectionDidFinishLoading(FPRClassInstrumentor *instrumentor) {
  SEL selector = @selector(connectionDidFinishLoading:);
  FPRSelectorInstrumentor *selectorInstrumentor =
      [instrumentor instrumentorForInstanceSelector:selector];
  if (selectorInstrumentor) {
    IMP currentIMP = selectorInstrumentor.currentIMP;
    [selectorInstrumentor setReplacingBlock:^(id object, NSURLConnection *connection) {
      FPRNetworkTrace *trace = [FPRNetworkTrace networkTraceFromObject:connection];
      [trace didCompleteRequestWithResponse:nil error:nil];
      [FPRNetworkTrace removeNetworkTraceFromObject:connection];
      typedef void (*OriginalImp)(id, SEL, NSURLConnection *);
      ((OriginalImp)currentIMP)(object, selector, connection);
    }];
  }
}

/** Instruments connection:didWriteData:totalBytesWritten:expectedTotalBytes:.
 *
 *  @param instrumentor The FPRClassInstrumentor to add the selector instrumentor to.
 */
FOUNDATION_STATIC_INLINE
NS_EXTENSION_UNAVAILABLE("Firebase Performance is not supported for extensions.")
void InstrumentConnectionDidWriteDataTotalBytesWrittenExpectedTotalBytes(
    FPRClassInstrumentor *instrumentor) {
  SEL selector = @selector(connection:didWriteData:totalBytesWritten:expectedTotalBytes:);
  FPRSelectorInstrumentor *selectorInstrumentor =
      [instrumentor instrumentorForInstanceSelector:selector];
  if (selectorInstrumentor) {
    IMP currentIMP = selectorInstrumentor.currentIMP;
    [selectorInstrumentor
        setReplacingBlock:^(id object, NSURLConnection *connection, long long bytesWritten,
                            long long totalBytesWritten, long long expectedTotalBytes) {
          FPRNetworkTrace *trace = [FPRNetworkTrace networkTraceFromObject:connection];
          trace.requestSize = totalBytesWritten;
          typedef void (*OriginalImp)(id, SEL, NSURLConnection *, long long, long long, long long);
          ((OriginalImp)currentIMP)(object, selector, connection, bytesWritten, totalBytesWritten,
                                    expectedTotalBytes);
        }];
  }
}

/** Instruments connectionDidFinishDownloading:destinationURL:.
 *
 *  @param instrumentor The FPRClassInstrumentor to add the selector instrumentor to.
 */
FOUNDATION_STATIC_INLINE
NS_EXTENSION_UNAVAILABLE("Firebase Performance is not supported for extensions.")
void InstrumentConnectionDidFinishDownloadingDestinationURL(FPRClassInstrumentor *instrumentor) {
  SEL selector = @selector(connectionDidFinishDownloading:destinationURL:);
  FPRSelectorInstrumentor *selectorInstrumentor =
      [instrumentor instrumentorForInstanceSelector:selector];
  if (selectorInstrumentor) {
    IMP currentIMP = selectorInstrumentor.currentIMP;
    [selectorInstrumentor
        setReplacingBlock:^(id object, NSURLConnection *connection, NSURL *destinationURL) {
          FPRNetworkTrace *trace = [FPRNetworkTrace networkTraceFromObject:connection];
          [trace didReceiveFileURL:destinationURL];
          [trace didCompleteRequestWithResponse:nil error:nil];
          [FPRNetworkTrace removeNetworkTraceFromObject:connection];
          typedef void (*OriginalImp)(id, SEL, NSURLConnection *, NSURL *);
          ((OriginalImp)currentIMP)(object, selector, connection, destinationURL);
        }];
  }
}

#pragma mark - Helper functions
FOUNDATION_STATIC_INLINE
NS_EXTENSION_UNAVAILABLE("Firebase Performance is not supported for extensions.")
void CopySelector(SEL selector, FPRObjectInstrumentor *instrumentor) {
  static Class fromClass = Nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    fromClass = [FPRNSURLConnectionDelegate class];
  });
  if (![instrumentor.instrumentedObject respondsToSelector:selector]) {
    [instrumentor copySelector:selector fromClass:fromClass isClassSelector:NO];
  }
}

#pragma mark - FPRNSURLConnectionDelegateInstrument

@implementation FPRNSURLConnectionDelegateInstrument

- (void)registerInstrumentors {
  // Do nothing by default. classes will be instrumented on-demand upon discovery.
}

- (void)registerClass:(Class)aClass {
  dispatch_sync(GetInstrumentationQueue(), ^{
    // If this class has already been instrumented, just return.
    FPRClassInstrumentor *instrumentor = [[FPRClassInstrumentor alloc] initWithClass:aClass];
    if (![self registerClassInstrumentor:instrumentor]) {
      return;
    }

    InstrumentConnectionDidFailWithError(instrumentor);
    InstrumentConnectionWillSendRequestRedirectResponse(instrumentor);
    InstrumentConnectionDidReceiveResponse(instrumentor);
    InstrumentConnectionDidReceiveData(instrumentor);
    InstrumentConnectionAllTheTotals(instrumentor);
    InstrumentConnectionDidFinishLoading(instrumentor);
    InstrumentConnectionDidWriteDataTotalBytesWrittenExpectedTotalBytes(instrumentor);
    InstrumentConnectionDidFinishDownloadingDestinationURL(instrumentor);

    [instrumentor swizzle];
  });
}

- (void)registerObject:(id)object {
  dispatch_sync(GetInstrumentationQueue(), ^{
    if ([object respondsToSelector:@selector(gul_class)]) {
      return;
    }

    if (![self isObjectInstrumentable:object]) {
      return;
    }

    FPRObjectInstrumentor *instrumentor = [[FPRObjectInstrumentor alloc] initWithObject:object];

    // Register the non-swizzled versions of these methods.
    CopySelector(@selector(connection:didFailWithError:), instrumentor);
    CopySelector(@selector(connection:willSendRequest:redirectResponse:), instrumentor);
    CopySelector(@selector(connection:didReceiveResponse:), instrumentor);
    CopySelector(@selector(connection:didReceiveData:), instrumentor);
    CopySelector(@selector(connection:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite:),
                 instrumentor);
    if (![object respondsToSelector:@selector(connectionDidFinishDownloading:destinationURL:)]) {
      CopySelector(@selector(connectionDidFinishLoading:), instrumentor);
    }

    CopySelector(@selector(connection:didWriteData:totalBytesWritten:expectedTotalBytes:),
                 instrumentor);
    if (![object respondsToSelector:@selector(connectionDidFinishLoading:)]) {
      CopySelector(@selector(connectionDidFinishDownloading:destinationURL:), instrumentor);
    }

    [instrumentor swizzle];
  });
}

@end
