//
//  TNLRequestOperationQueue.m
//  TwitterNetworkLayer
//
//  Created on 5/23/14.
//  Copyright © 2020 Twitter, Inc. All rights reserved.
//

#include <objc/message.h>
#include <stdatomic.h>

#import "NSURLResponse+TNLAdditions.h"
#import "NSURLSessionConfiguration+TNLAdditions.h"
#import "TNL_Project.h"
#import "TNLBackgroundURLSessionTaskOperationManager.h"
#import "TNLBackoff.h"
#import "TNLGlobalConfiguration.h"
#import "TNLNetwork.h"
#import "TNLNetworkObserver.h"
#import "TNLPriority.h"
#import "TNLRequest.h"
#import "TNLRequestAuthorizer.h"
#import "TNLRequestOperation_Project.h"
#import "TNLRequestOperationQueue_Project.h"
#import "TNLResponse.h"
#import "TNLURLSessionTaskOperation.h"

NS_ASSUME_NONNULL_BEGIN

NSString * const TNLBackgroundRequestOperationDidCompleteNotification = @"TNLBackgroundRequestOperationDidCompleteNotification";
NSString * const TNLBackgroundRequestURLRequestKey = @"URLRequest";
NSString * const TNLBackgroundRequestResponseKey = @"response";
NSString * const TNLBackgroundRequestURLSessionConfigurationIdentifierKey = @"identifier";
NSString * const TNLBackgroundRequestURLSessionTaskIdentifierKey = @"taskIdentifier";
NSString * const TNLBackgroundRequestURLSessionSharedContainerIdentifierKey = @"sharedContainerIdentifier";

static void _GlobalRequestOperationQueueAddOperation(TNLRequestOperation *op);

static volatile atomic_int_fast64_t __attribute__((aligned(8))) sGlobalExecutingConnectionCount = ATOMIC_VAR_INIT(0);
static NSMapTable<NSString *, TNLRequestOperationQueue *> *sGlobalRequestOperationQueueMapTable = nil;
static NSMutableSet<id<TNLNetworkObserver>> *sGlobalNetworkObservers = nil;
static NSOperationQueue *sGlobalRequestOperationQueue = nil;
static NSHashTable<TNLRequestOperation *> *sGlobalAutoDependencyOperations = nil;

//use NSMutableArray instead of NSMutableOrderedSet as it avoids an expensive class load when accessed during +(void)load,
//which for a collection with only a few elements and a few lookups is a worthwhile tradeoff
static NSMutableArray<id<TNLHTTPHeaderProvider>> *sGlobalHeaderProviders = nil;

static dispatch_queue_t _GlobalOperationQueueQueue(void);
static dispatch_queue_t _GlobalOperationQueueQueue()
{
    static dispatch_queue_t sGlobalOperationQueueQueue = NULL;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sGlobalOperationQueueQueue = dispatch_queue_create("com.TNL.operation.queue.global.queue", DISPATCH_QUEUE_SERIAL);
    });
    return sGlobalOperationQueueQueue;
}

static void _GlobalEnqueueOperation(NSOperation *op);
static void _GlobalEnqueueOperation(NSOperation *op)
{
    @try {
        [sGlobalRequestOperationQueue addOperation:op];
    } @catch (NSException *exception) {
        TNLLogError(@"[%@ addOperation:%@] - %@", sGlobalRequestOperationQueue, op, exception);
        @throw exception;
    }
}

static NSArray<TNLRequestOperation *> * __nullable _GlobalAutoDependencyOperations(void);
static NSArray<TNLRequestOperation *> * __nullable _GlobalAutoDependencyOperations()
{
    __block NSArray<TNLRequestOperation *> *ops = nil;
    tnl_dispatch_sync_autoreleasing(_GlobalOperationQueueQueue(), ^{
        ops = [sGlobalAutoDependencyOperations allObjects];
    });
    return ops;
}

static void _GlobalAddAutoDependencyOperation(TNLRequestOperation *op);
static void _GlobalAddAutoDependencyOperation(TNLRequestOperation *op)
{
    tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
        [sGlobalAutoDependencyOperations addObject:op];
    });
}

#if 0 // no use case a.t.m.
static void _GlobalRemoveAutoDependencyOperation(TNLRequestOperation *op);
static void _GlobalRemoveAutoDependencyOperation(TNLRequestOperation *op)
{
    tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
        [sGlobalAutoDependencyOperations removeObject:op];
    });
}
#endif

static void _GlobalApplyAutoDependenciesToOperation(TNLRequestOperation *op);
static void _GlobalApplyAutoDependenciesToOperation(TNLRequestOperation *op)
{
    const NSInteger dependencyThreshold = (NSInteger)[TNLGlobalConfiguration sharedInstance].operationAutomaticDependencyPriorityThreshold;
    if (dependencyThreshold < NSIntegerMax) {
        if (op.priority > dependencyThreshold) {
            // Auto dependency operation encountered!
            _GlobalAddAutoDependencyOperation(op);
        } else {
            // Add outstanding auto dependency operations as dependencies
            NSArray<TNLRequestOperation *> *autoDependencies = _GlobalAutoDependencyOperations();
            if (autoDependencies.count > 0) {
                TNLLogInformation(@"Marking %@ dependent on %tu higher priority operations", op, autoDependencies.count);
                for (TNLRequestOperation *depOp in autoDependencies) {
                    [op addDependency:depOp];
                }
            }
        }
    }
}

@interface TNLRequestOperationQueue (NSURLSessionDelegate) <NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>
@end

TNL_OBJC_DIRECT_MEMBERS
@interface TNLRequestOperationQueue ()
@property (nonatomic, readonly) dispatch_queue_t sessionStateQueue;
@end

#pragma mark - TNLRequestOperationQueue

@implementation TNLRequestOperationQueue
{
    id<TNLNetworkObserver> _networkObserver;
    NSUInteger _suspendCount;
    NSMutableArray<TNLRequestOperation *> *_stagedRequestOperations;
}

+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sGlobalRequestOperationQueueMapTable = [NSMapTable strongToWeakObjectsMapTable];
        sGlobalAutoDependencyOperations = [NSHashTable weakObjectsHashTable];

        sGlobalRequestOperationQueue = [[NSOperationQueue alloc] init];
        sGlobalRequestOperationQueue.name = @"com.TNL.global.request.operation.queue";
        sGlobalRequestOperationQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
        if ([sGlobalRequestOperationQueue respondsToSelector:@selector(setQualityOfService:)]) {
            sGlobalRequestOperationQueue.qualityOfService = (NSQualityOfServiceUtility + NSQualityOfServiceUserInitiated / 2);
        }
    });
}

+ (NSOperationQueue *)globalRequestOperationQueue
{
    return sGlobalRequestOperationQueue;
}

+ (void)addGlobalNetworkObserver:(id<TNLNetworkObserver>)observer
{
    tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
        if (!sGlobalNetworkObservers) {
            sGlobalNetworkObservers = [[NSMutableSet alloc] init];
        }
        [sGlobalNetworkObservers addObject:observer];
    });
}

+ (void)removeGlobalNetworkObserver:(id<TNLNetworkObserver>)observer
{
    tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
        [sGlobalNetworkObservers removeObject:observer];
    });
}

+ (NSArray<id<TNLNetworkObserver>> *)allGlobalNetworkObservers
{
    __block NSArray<id<TNLNetworkObserver>> *allGlobalNetworkObservers = nil;
    tnl_dispatch_sync_autoreleasing(_GlobalOperationQueueQueue(), ^{
        allGlobalNetworkObservers = [sGlobalNetworkObservers allObjects];
    });
    return allGlobalNetworkObservers;
}

+ (void)addGlobalHeaderProvider:(id<TNLHTTPHeaderProvider>)provider
{
    tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
        if (!sGlobalHeaderProviders) {
            sGlobalHeaderProviders = [[NSMutableArray alloc] init];
        }
        // remove then add to make sure provider is the latest
        [sGlobalHeaderProviders removeObject:provider];
        [sGlobalHeaderProviders addObject:provider];
    });
}

+ (void)removeGlobalHeaderProvider:(id<TNLHTTPHeaderProvider>)provider
{
    tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
        [sGlobalHeaderProviders removeObject:provider];
    });
}

+ (nullable NSArray<id<TNLHTTPHeaderProvider>> *)allGlobalHeaderProviders
{
    __block NSArray* providers = nil;
    tnl_dispatch_sync_autoreleasing(_GlobalOperationQueueQueue(), ^{
        providers = [sGlobalHeaderProviders copy];
    });
    return providers;
}


- (instancetype)init
{
    [self doesNotRecognizeSelector:_cmd];
    abort();
    return nil;
}

- (instancetype)initWithIdentifier:(NSString *)identifier
{
    if (self = [super init]) {
        TNLIncrementObjectCount([self class]);

        _identifier = [identifier copy];
        NSMutableCharacterSet *hostCharSet = [NSMutableCharacterSet characterSetWithRange:NSMakeRange('a', 'z' - 'a' + 1)];
        [hostCharSet addCharactersInRange:NSMakeRange('A', 'Z' - 'A' + 1)];
        [hostCharSet addCharactersInRange:NSMakeRange('0', '9' - '0' + 1)];
        [hostCharSet addCharactersInRange:NSMakeRange('.', 1)];
        [hostCharSet invert];
        if (_identifier.length == 0 || [_identifier rangeOfCharacterFromSet:hostCharSet].location != NSNotFound) {
            @throw [NSException exceptionWithName:NSInvalidArgumentException
                                           reason:[NSString stringWithFormat:@"%@ (%@) must be called with a valid URL host/domain string", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]
                                         userInfo:@{ @"identifier" : (_identifier) ?: [NSNull null] }];
        }

        _sessionStateQueue = dispatch_queue_create([identifier stringByAppendingString:@".operation.queue.state.queue"].UTF8String, DISPATCH_QUEUE_SERIAL);
        _stagedRequestOperations = [[NSMutableArray alloc] init];

        __block BOOL didRegister = NO;
        dispatch_sync(_GlobalOperationQueueQueue(), ^{
            didRegister = [sGlobalRequestOperationQueueMapTable objectForKey:self.identifier] == nil;
            if (didRegister) {
                [sGlobalRequestOperationQueueMapTable setObject:self forKey:self.identifier];
            }
        });
        if (!didRegister) {
            @throw [NSException exceptionWithName:NSInvalidArgumentException
                                           reason:[NSString stringWithFormat:@"%@ already exists with identifier = '%@'", NSStringFromClass([self class]), _identifier]
                                         userInfo:@{ @"identifier" : _identifier }];
        }
    }
    return self;
}

- (void)dealloc
{
    TNLDecrementObjectCount([self class]);
}

+ (instancetype)defaultOperationQueue
{
    static TNLRequestOperationQueue *sDefaultOperationQueue = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sDefaultOperationQueue = [[self alloc] initWithIdentifier:@"com.twitter.http.operation.queue.default"];
    });
    return sDefaultOperationQueue;
}

#pragma mark Background Events

#if TARGET_OS_IPHONE // == IOS + WATCH + TV
+ (BOOL)handleBackgroundURLSessionEvents:(nullable NSString *)identifier
                       completionHandler:(dispatch_block_t)completionHandler
{
    return [[TNLURLSessionManager sharedInstance] handleBackgroundURLSessionEvents:identifier
                                                                 completionHandler:completionHandler];
}
#endif

#pragma mark Mutable properties

- (void)setNetworkObserver:(nullable id<TNLNetworkObserver>)networkObserver
{
    tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{
        self->_networkObserver = networkObserver;
    });
}

- (nullable id<TNLNetworkObserver>)networkObserver
{
    __block id<TNLNetworkObserver> observer;
    dispatch_sync(_sessionStateQueue, ^{
        observer = self->_networkObserver;
    });
    return observer;
}

#pragma mark Suspension

- (void)suspend
{
    if (self == [TNLRequestOperationQueue defaultOperationQueue]) {
        TNLAssertNever();
        return;
    }

    METHOD_LOG();

    tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{
        self->_suspendCount++;
    });
}

- (void)resume
{
    if (self == [TNLRequestOperationQueue defaultOperationQueue]) {
        TNLAssertNever();
        return;
    }

    METHOD_LOG();

    tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{
        if (self->_suspendCount > 0) {
            self->_suspendCount--;
        }
        if (0 == self->_suspendCount) {
            [self->_stagedRequestOperations sortUsingComparator:^NSComparisonResult(TNLRequestOperation *obj1, TNLRequestOperation *obj2) {
                NSOperationQueuePriority priority1 = [obj1 queuePriority];
                NSOperationQueuePriority priority2 = [obj2 queuePriority];

                if (priority1 > priority2) {
                    return NSOrderedAscending;  // highest to lowest
                } else if (priority1 < priority2) {
                    return NSOrderedDescending; // highest to lowest
                }
                return NSOrderedSame;
            }];

            for (TNLRequestOperation *op in self->_stagedRequestOperations) {
                if (!op.isFinished && !op.isExecuting && !op.isCancelled) {
                    _GlobalRequestOperationQueueAddOperation(op);
                }
            }
            [self->_stagedRequestOperations removeAllObjects];
        }
    });
}

#pragma mark Enqueue

- (void)enqueueRequestOperation:(TNLRequestOperation *)op
{
    if (!op) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:@"TNLRequestOperation argument cannot be nil!"
                                     userInfo:@{ @"operationQueue" : self } ];
    } else if (op.requestOperationQueue != nil) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:@"TNLRequestOperation provided was already enqueued!"
                                     userInfo:@{ @"requestOperation" : op, @"operationQueue" : self } ];
    } else {
        [op enqueueToOperationQueue:self];
    }
}

- (TNLRequestOperation *)enqueueRequest:(nullable id<TNLRequest>)request
                             completion:(nullable TNLRequestDidCompleteBlock)completion
{
    TNLRequestOperation *op = [TNLRequestOperation operationWithRequest:request
                                                             completion:completion];
    [self enqueueRequestOperation:op];
    return op;
}

- (void)syncAddRequestOperation:(TNLRequestOperation *)op
{
    dispatch_sync(_sessionStateQueue, ^{
        if (self->_suspendCount > 0) {
            [self->_stagedRequestOperations addObject:op];
        } else {
            _GlobalRequestOperationQueueAddOperation(op);
        }
    });
}

- (void)clearQueuedRequestOperation:(TNLRequestOperation *)op
{
    tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{
        [self->_stagedRequestOperations removeObject:op];
    });
}

#pragma mark Cancel

- (void)_cancelAllStagedRequestOperations:(id<TNLRequestOperationCancelSource>)source
                          underlyingError:(nullable NSError *)optionalUnderlyingError TNL_OBJC_DIRECT
{
    tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{
        NSArray<TNLRequestOperation *> *ops = [self->_stagedRequestOperations copy];
        [self->_stagedRequestOperations removeAllObjects];
        for (TNLRequestOperation *op in ops) {
            [op cancelWithSource:source
                 underlyingError:optionalUnderlyingError];
        }
    });
}

- (void)cancelAllWithSource:(id<TNLRequestOperationCancelSource>)source
            underlyingError:(nullable NSError *)optionalUnderlyingError
{
    [self _cancelAllStagedRequestOperations:source
                            underlyingError:optionalUnderlyingError];
    [[TNLURLSessionManager sharedInstance] cancelAllForQueue:self
                                                      source:source
                                             underlyingError:optionalUnderlyingError];
}

- (void)cancelAllWithSource:(id<TNLRequestOperationCancelSource>)source
{
    [self cancelAllWithSource:source
              underlyingError:nil];
}

#pragma mark Task Operation

- (void)findURLSessionTaskOperationForRequestOperation:(TNLRequestOperation *)op
                                              complete:(TNLRequestOperationQueueFindTaskOperationCompleteBlock)complete
{
    [[TNLURLSessionManager sharedInstance] findURLSessionTaskOperationForRequestOperationQueue:self
                                                                              requestOperation:op
                                                                                      complete:complete];
}

#pragma mark Request Events

- (void)operationDidStart:(TNLRequestOperation *)op
{
    [self _executeWithMatchingSelector:@selector(tnl_requestOperationDidStart:)
                                 block:^(id<TNLNetworkObserver> observer) {
        [observer tnl_requestOperationDidStart:op];
    }];
}

- (void)operation:(TNLRequestOperation *)op
        didStartAttemptWithMetrics:(TNLAttemptMetrics *)metrics
{
    NSURLRequest *URLRequest = op.currentURLRequest;

    [self _executeWithMatchingSelector:@selector(tnl_requestOperation:didStartAttemptRequest:metrics:)
                                 block:^(id<TNLNetworkObserver> observer) {
        [observer tnl_requestOperation:op
                didStartAttemptRequest:URLRequest
                               metrics:metrics];
    }];
}

- (void)operation:(TNLRequestOperation *)op
        didCompleteAttempt:(TNLResponse *)response
        disposition:(TNLAttemptCompleteDisposition)disposition
{
    [self _executeWithMatchingSelector:@selector(tnl_requestOperation:didCompleteAttemptWithIntermediateResponse:disposition:)
                                 block:^(id<TNLNetworkObserver> observer) {
        [observer tnl_requestOperation:op
                  didCompleteAttemptWithIntermediateResponse:response
                  disposition:disposition];
    }];
}

- (void)operation:(TNLRequestOperation *)op
        didCompleteWithResponse:(TNLResponse *)response
{
    [self _executeWithMatchingSelector:@selector(tnl_requestOperation:didCompleteWithResponse:)
                                 block:^(id<TNLNetworkObserver> observer) {
        [observer tnl_requestOperation:op
               didCompleteWithResponse:response];
    }];
}

- (void)taskOperation:(TNLURLSessionTaskOperation *)op
        didCompleteAttempt:(TNLResponse *)response
{
    TNLRequestOperation *requestOp = [op synthesizeRequestOperation];
    [self _executeWithMatchingSelector:@selector(tnl_requestOperation:didCompleteWithResponse:)
                                 block:^(id<TNLNetworkObserver> observer) {
        [observer tnl_requestOperation:requestOp
               didCompleteWithResponse:response];
    }];
}

#pragma mark Private

- (void)_executeWithMatchingSelector:(SEL)selector
                               block:(void(^)(id<TNLNetworkObserver> matchingObserver))matchingBlock TNL_OBJC_DIRECT
{
    tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
        NSSet<id<TNLNetworkObserver>> *observers = [sGlobalNetworkObservers copy];
        tnl_dispatch_async_autoreleasing(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            for (id<TNLNetworkObserver> observer in observers) {
                if ([observer respondsToSelector:selector]) {
                    matchingBlock(observer);
                }
            }
        });
    });
    tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{
        if ([self->_networkObserver respondsToSelector:selector]) {
            matchingBlock(self->_networkObserver);
        }
    });
}

@end

#pragma mark - Functions

static void _GlobalRequestOperationQueueAddOperation(TNLRequestOperation *op)
{
    _GlobalApplyAutoDependenciesToOperation(op);
    _GlobalEnqueueOperation(op);
}

@implementation TNLNetwork

+ (BOOL)hasExecutingNetworkConnections
{
    return atomic_load(&sGlobalExecutingConnectionCount) > 0;
}

+ (void)incrementExecutingNetworkConnections
{
    if (atomic_fetch_add(&sGlobalExecutingConnectionCount, 1) == 0) {
        tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:TNLNetworkExecutingNetworkConnectionsDidUpdateNotification
                                                                object:nil
                                                              userInfo:@{ TNLNetworkExecutingNetworkConnectionsExecutingKey : @YES }];
        });
    }
}

+ (void)decrementExecutingNetworkConnections
{
    const int64_t result = atomic_fetch_sub(&sGlobalExecutingConnectionCount, 1);
    if (1 == result) {
        tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{
            // Post from main thread
            [[NSNotificationCenter defaultCenter] postNotificationName:TNLNetworkExecutingNetworkConnectionsDidUpdateNotification
                                                                object:nil
                                                              userInfo:@{ TNLNetworkExecutingNetworkConnectionsExecutingKey : @NO }];
        });
    } else if (result <= 0) {
        TNLLogWarning(@"%@ was called too many times!  Executing connections count is now negative!", NSStringFromSelector(_cmd));
    }
}

+ (void)backoffSignalEncounteredForURL:(NSURL *)URL
                                  host:(nullable NSString *)host
                   responseHTTPHeaders:(nullable NSDictionary<NSString *, NSString *> *)headers
{
    [[TNLURLSessionManager sharedInstance] backoffSignalEncounteredForURL:URL
                                                                     host:host
                                                      responseHTTPHeaders:headers];
}

+ (void)HTTPURLResponseEncounteredOutsideOfTNL:(NSHTTPURLResponse *)response
                                          host:(nullable NSString *)host
{
    NSURL *URL = response.URL;
    if (!URL) {
        return;
    }

    const TNLHTTPStatusCode statusCode = response.statusCode;
    NSDictionary *headers = response.allHeaderFields;
    const BOOL shouldSignal = [[TNLGlobalConfiguration sharedInstance].backoffSignaler tnl_shouldSignalBackoffForURL:URL
                                                                                                                host:host
                                                                                                          statusCode:statusCode
                                                                                                     responseHeaders:headers];
    if (!shouldSignal) {
        return;
    }

    [self backoffSignalEncounteredForURL:URL
                                    host:host
                     responseHTTPHeaders:headers];
}

+ (void)applyBackoffDependenciesToOperation:(NSOperation *)op
                                    withURL:(NSURL *)URL
                                       host:(nullable NSString *)host
                          isLongPollRequest:(BOOL)isLongPoll
{
    [[TNLURLSessionManager sharedInstance] applyBackoffDependenciesToOperation:op
                                                                       withURL:URL
                                                                          host:host
                                                             isLongPollRequest:isLongPoll];
}

@end

NS_ASSUME_NONNULL_END
