text: //
  //  SessionDelegate.swift
  //
  //  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
  //
  //  Permission is hereby granted, free of charge, to any person obtaining a copy
  //  of this software and associated documentation files (the "Software"), to deal
  //  in the Software without restriction, including without limitation the rights
  //  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  //  copies of the Software, and to permit persons to whom the Software is
  //  furnished to do so, subject to the following conditions:
  //
  //  The above copyright notice and this permission notice shall be included in
  //  all copies or substantial portions of the Software.
  //
  //  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  //  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  //  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  //  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  //  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  //  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  //  THE SOFTWARE.
  //
  
  import Foundation
  
  /// Responsible for handling all delegate callbacks for the underlying session.
  open class SessionDelegate: NSObject {
  
      // MARK: URLSessionDelegate Overrides
  
      /// Overrides default behavior for URLSessionDelegate method `urlSession(_:didBecomeInvalidWithError:)`.
      open var sessionDidBecomeInvalidWithError: ((URLSession, Error?) -> Void)?
  
      /// Overrides default behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)`.
      open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
  
      /// Overrides all behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)` and requires the caller to call the `completionHandler`.
      open var sessionDidReceiveChallengeWithCompletion: ((URLSession, URLAuthenticationChallenge, @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)?
  
      /// Overrides default behavior for URLSessionDelegate method `urlSessionDidFinishEvents(forBackgroundURLSession:)`.
      open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)?
  
      // MARK: URLSessionTaskDelegate Overrides
  
      /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`.
      open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
  
      /// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` and
      /// requires the caller to call the `completionHandler`.
      open var taskWillPerformHTTPRedirectionWithCompletion: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void)?
  
      /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didReceive:completionHandler:)`.
      open var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
  
      /// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:didReceive:completionHandler:)` and
      /// requires the caller to call the `completionHandler`.
      open var taskDidReceiveChallengeWithCompletion: ((URLSession, URLSessionTask, URLAuthenticationChallenge, @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)?
  
      /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:needNewBodyStream:)`.
      open var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)?
  
      /// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:needNewBodyStream:)` and
      /// requires the caller to call the `completionHandler`.
      open var taskNeedNewBodyStreamWithCompletion: ((URLSession, URLSessionTask, @escaping (InputStream?) -> Void) -> Void)?
  
      /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)`.
      open var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)?
  
      /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didCompleteWithError:)`.
      open var taskDidComplete: ((URLSession, URLSessionTask, Error?) -> Void)?
  
      // MARK: URLSessionDataDelegate Overrides
  
      /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:completionHandler:)`.
      open var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)?
  
      /// Overrides all behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:completionHandler:)` and
      /// requires caller to call the `completionHandler`.
      open var dataTaskDidReceiveResponseWithCompletion: ((URLSession, URLSessionDataTask, URLResponse, @escaping (URLSession.ResponseDisposition) -> Void) -> Void)?
  
      /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didBecome:)`.
      open var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)?
  
      /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:)`.
      open var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)?
  
      /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)`.
      open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?
  
      /// Overrides all behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)` and
      /// requires caller to call the `completionHandler`.
      open var dataTaskWillCacheResponseWithCompletion: ((URLSession, URLSessionDataTask, CachedURLResponse, @escaping (CachedURLResponse?) -> Void) -> Void)?
  
      // MARK: URLSessionDownloadDelegate Overrides
  
      /// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didFinishDownloadingTo:)`.
      open var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> Void)?
  
      /// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)`.
      open var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?
  
      /// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)`.
      open var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)?
  
      // MARK: URLSessionStreamDelegate Overrides
  
  #if !os(watchOS)
  
      /// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:readClosedFor:)`.
      @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
      open var streamTaskReadClosed: ((URLSession, URLSessionStreamTask) -> Void)? {
          get {
              return _streamTaskReadClosed as? (URLSession, URLSessionStreamTask) -> Void
          }
          set {
              _streamTaskReadClosed = newValue
          }
      }
  
      /// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:writeClosedFor:)`.
      @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
      open var streamTaskWriteClosed: ((URLSession, URLSessionStreamTask) -> Void)? {
          get {
              return _streamTaskWriteClosed as? (URLSession, URLSessionStreamTask) -> Void
          }
          set {
              _streamTaskWriteClosed = newValue
          }
      }
  
      /// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:betterRouteDiscoveredFor:)`.
      @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
      open var streamTaskBetterRouteDiscovered: ((URLSession, URLSessionStreamTask) -> Void)? {
          get {
              return _streamTaskBetterRouteDiscovered as? (URLSession, URLSessionStreamTask) -> Void
          }
          set {
              _streamTaskBetterRouteDiscovered = newValue
          }
      }
  
      /// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:streamTask:didBecome:outputStream:)`.
      @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
      open var streamTaskDidBecomeInputAndOutputStreams: ((URLSession, URLSessionStreamTask, InputStream, OutputStream) -> Void)? {
          get {
              return _streamTaskDidBecomeInputStream as? (URLSession, URLSessionStreamTask, InputStream, OutputStream) -> Void
          }
          set {
              _streamTaskDidBecomeInputStream = newValue
          }
      }
  
      var _streamTaskReadClosed: Any?
      var _streamTaskWriteClosed: Any?
      var _streamTaskBetterRouteDiscovered: Any?
      var _streamTaskDidBecomeInputStream: Any?
  
  #endif
  
      // MARK: Properties
  
      var retrier: RequestRetrier?
      weak var sessionManager: SessionManager?
  
      var requests: [Int: Request] = [:]
      private let lock = NSLock()
  
      /// Access the task delegate for the specified task in a thread-safe manner.
      open subscript(task: URLSessionTask) -> Request? {
          get {
              lock.lock() ; defer { lock.unlock() }
              return requests[task.taskIdentifier]
          }
          set {
              lock.lock() ; defer { lock.unlock() }
              requests[task.taskIdentifier] = newValue
          }
      }
  
      // MARK: Lifecycle
  
      /// Initializes the `SessionDelegate` instance.
      ///
      /// - returns: The new `SessionDelegate` instance.
      public override init() {
          super.init()
      }
  
      // MARK: NSObject Overrides
  
      /// Returns a `Bool` indicating whether the `SessionDelegate` implements or inherits a method that can respond
      /// to a specified message.
      ///
      /// - parameter selector: A selector that identifies a message.
      ///
      /// - returns: `true` if the receiver implements or inherits a method that can respond to selector, otherwise `false`.
      open override func responds(to selector: Selector) -> Bool {
          #if !os(macOS)
              if selector == #selector(URLSessionDelegate.urlSessionDidFinishEvents(forBackgroundURLSession:)) {
                  return sessionDidFinishEventsForBackgroundURLSession != nil
              }
          #endif
  
          #if !os(watchOS)
              if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
                  switch selector {
                  case #selector(URLSessionStreamDelegate.urlSession(_:readClosedFor:)):
                      return streamTaskReadClosed != nil
                  case #selector(URLSessionStreamDelegate.urlSession(_:writeClosedFor:)):
                      return streamTaskWriteClosed != nil
                  case #selector(URLSessionStreamDelegate.urlSession(_:betterRouteDiscoveredFor:)):
                      return streamTaskBetterRouteDiscovered != nil
                  case #selector(URLSessionStreamDelegate.urlSession(_:streamTask:didBecome:outputStream:)):
                      return streamTaskDidBecomeInputAndOutputStreams != nil
                  default:
                      break
                  }
              }
          #endif
  
          switch selector {
          case #selector(URLSessionDelegate.urlSession(_:didBecomeInvalidWithError:)):
              return sessionDidBecomeInvalidWithError != nil
          case #selector(URLSessionDelegate.urlSession(_:didReceive:completionHandler:)):
              return (sessionDidReceiveChallenge != nil  || sessionDidReceiveChallengeWithCompletion != nil)
          case #selector(URLSessionTaskDelegate.urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)):
              return (taskWillPerformHTTPRedirection != nil || taskWillPerformHTTPRedirectionWithCompletion != nil)
          case #selector(URLSessionDataDelegate.urlSession(_:dataTask:didReceive:completionHandler:)):
              return (dataTaskDidReceiveResponse != nil || dataTaskDidReceiveResponseWithCompletion != nil)
          default:
              return type(of: self).instancesRespond(to: selector)
          }
      }
  }
  
  // MARK: - URLSessionDelegate
  
  extension SessionDelegate: URLSessionDelegate {
      /// Tells the delegate that the session has been invalidated.
      ///
      /// - parameter session: The session object that was invalidated.
      /// - parameter error:   The error that caused invalidation, or nil if the invalidation was explicit.
      open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
          sessionDidBecomeInvalidWithError?(session, error)
      }
  
      /// Requests credentials from the delegate in response to a session-level authentication request from the
      /// remote server.
      ///
      /// - parameter session:           The session containing the task that requested authentication.
      /// - parameter challenge:         An object that contains the request for authentication.
      /// - parameter completionHandler: A handler that your delegate method must call providing the disposition
      ///                                and credential.
      open func urlSession(
          _ session: URLSession,
          didReceive challenge: URLAuthenticationChallenge,
          completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
      {
          guard sessionDidReceiveChallengeWithCompletion == nil else {
              sessionDidReceiveChallengeWithCompletion?(session, challenge, completionHandler)
              return
          }
  
          var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
          var credential: URLCredential?
  
          if let sessionDidReceiveChallenge = sessionDidReceiveChallenge {
              (disposition, credential) = sessionDidReceiveChallenge(session, challenge)
          } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
              let host = challenge.protectionSpace.host
  
              if
                  let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host),
                  let serverTrust = challenge.protectionSpace.serverTrust
              {
                  if serverTrustPolicy.evaluate(serverTrust, forHost: host) {
                      disposition = .useCredential
                      credential = URLCredential(trust: serverTrust)
                  } else {
                      disposition = .cancelAuthenticationChallenge
                  }
              }
          }
  
          completionHandler(disposition, credential)
      }
  
  #if !os(macOS)
  
      /// Tells the delegate that all messages enqueued for a session have been delivered.
      ///
      /// - parameter session: The session that no longer has any outstanding requests.
      open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
          sessionDidFinishEventsForBackgroundURLSession?(session)
      }
  
  #endif
  }
  
  // MARK: - URLSessionTaskDelegate
  
  extension SessionDelegate: URLSessionTaskDelegate {
      /// Tells the delegate that the remote server requested an HTTP redirect.
      ///
      /// - parameter session:           The session containing the task whose request resulted in a redirect.
      /// - parameter task:              The task whose request resulted in a redirect.
      /// - parameter response:          An object containing the server’s response to the original request.
      /// - parameter request:           A URL request object filled out with the new location.
      /// - parameter completionHandler: A closure that your handler should call with either the value of the request
      ///                                parameter, a modified URL request object, or NULL to refuse the redirect and
      ///                                return the body of the redirect response.
      open func urlSession(
          _ session: URLSession,
          task: URLSessionTask,
          willPerformHTTPRedirection response: HTTPURLResponse,
          newRequest request: URLRequest,
          completionHandler: @escaping (URLRequest?) -> Void)
      {
          guard taskWillPerformHTTPRedirectionWithCompletion == nil else {
              taskWillPerformHTTPRedirectionWithCompletion?(session, task, response, request, completionHandler)
              return
          }
  
          var redirectRequest: URLRequest? = request
  
          if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection {
              redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request)
          }
  
          completionHandler(redirectRequest)
      }
  
      /// Requests credentials from the delegate in response to an authentication request from the remote server.
      ///
      /// - parameter session:           The session containing the task whose request requires authentication.
      /// - parameter task:              The task whose request requires authentication.
      /// - parameter challenge:         An object that contains the request for authentication.
      /// - parameter completionHandler: A handler that your delegate method must call providing the disposition
      ///                                and credential.
      open func urlSession(
          _ session: URLSession,
          task: URLSessionTask,
          didReceive challenge: URLAuthenticationChallenge,
          completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
      {
          guard taskDidReceiveChallengeWithCompletion == nil else {
              taskDidReceiveChallengeWithCompletion?(session, task, challenge, completionHandler)
              return
          }
  
          if let taskDidReceiveChallenge = taskDidReceiveChallenge {
              let result = taskDidReceiveChallenge(session, task, challenge)
              completionHandler(result.0, result.1)
          } else if let delegate = self[task]?.delegate {
              delegate.urlSession(
                  session,
                  task: task,
                  didReceive: challenge,
                  completionHandler: completionHandler
              )
          } else {
              urlSession(session, didReceive: challenge, completionHandler: completionHandler)
          }
      }
  
      /// Tells the delegate when a task requires a new request body stream to send to the remote server.
      ///
      /// - parameter session:           The session containing the task that needs a new body stream.
      /// - parameter task:              The task that needs a new body stream.
      /// - parameter completionHandler: A completion handler that your delegate method should call with the new body stream.
      open func urlSession(
          _ session: URLSession,
          task: URLSessionTask,
          needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
      {
          guard taskNeedNewBodyStreamWithCompletion == nil else {
              taskNeedNewBodyStreamWithCompletion?(session, task, completionHandler)
              return
          }
  
          if let taskNeedNewBodyStream = taskNeedNewBodyStream {
              completionHandler(taskNeedNewBodyStream(session, task))
          } else if let delegate = self[task]?.delegate {
              delegate.urlSession(session, task: task, needNewBodyStream: completionHandler)
          }
      }
  
      /// Periodically informs the delegate of the progress of sending body content to the server.
      ///
      /// - parameter session:                  The session containing the data task.
      /// - parameter task:                     The data task.
      /// - parameter bytesSent:                The number of bytes sent since the last time this delegate method was called.
      /// - parameter totalBytesSent:           The total number of bytes sent so far.
      /// - parameter totalBytesExpectedToSend: The expected length of the body data.
      open func urlSession(
          _ session: URLSession,
          task: URLSessionTask,
          didSendBodyData bytesSent: Int64,
          totalBytesSent: Int64,
          totalBytesExpectedToSend: Int64)
      {
          if let taskDidSendBodyData = taskDidSendBodyData {
              taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
          } else if let delegate = self[task]?.delegate as? UploadTaskDelegate {
              delegate.URLSession(
                  session,
                  task: task,
                  didSendBodyData: bytesSent,
                  totalBytesSent: totalBytesSent,
                  totalBytesExpectedToSend: totalBytesExpectedToSend
              )
          }
      }
  
  #if !os(watchOS)
  
      /// Tells the delegate that the session finished collecting metrics for the task.
      ///
      /// - parameter session: The session collecting the metrics.
      /// - parameter task:    The task whose metrics have been collected.
      /// - parameter metrics: The collected metrics.
      @available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
      @objc(URLSession:task:didFinishCollectingMetrics:)
      open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
          self[task]?.delegate.metrics = metrics
      }
  
  #endif
  
      /// Tells the delegate that the task finished transferring data.
      ///
      /// - parameter session: The session containing the task whose request finished transferring data.
      /// - parameter task:    The task whose request finished transferring data.
      /// - parameter error:   If an error occurred, an error object indicating how the transfer failed, otherwise nil.
      open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
          /// Executed after it is determined that the request is not going to be retried
          let completeTask: (URLSession, URLSessionTask, Error?) -> Void = { [weak self] session, task, error in
              guard let strongSelf = self else { return }
  
              strongSelf.taskDidComplete?(session, task, error)
  
              strongSelf[task]?.delegate.urlSession(session, task: task, didCompleteWithError: error)
  
              var userInfo: [String: Any] = [Notification.Key.Task: task]
  
              if let data = (strongSelf[task]?.delegate as? DataTaskDelegate)?.data {
                  userInfo[Notification.Key.ResponseData] = data
              }
  
              NotificationCenter.default.post(
                  name: Notification.Name.Task.DidComplete,
                  object: strongSelf,
                  userInfo: userInfo
              )
  
              strongSelf[task] = nil
          }
  
          guard let request = self[task], let sessionManager = sessionManager else {
              completeTask(session, task, error)
              return
          }
  
          // Run all validations on the request before checking if an error occurred
          request.validations.forEach { $0() }
  
          // Determine whether an error has occurred
          var error: Error? = error
  
          if request.delegate.error != nil {
              error = request.delegate.error
          }
  
          /// If an error occurred and the retrier is set, asynchronously ask the retrier if the request
          /// should be retried. Otherwise, complete the task by notifying the task delegate.
          if let retrier = retrier, let error = error {
              retrier.should(sessionManager, retry: request, with: error) { [weak self] shouldRetry, timeDelay in
                  guard shouldRetry else { completeTask(session, task, error) ; return }
  
                  DispatchQueue.utility.after(timeDelay) { [weak self] in
                      guard let strongSelf = self else { return }
  
                      let retrySucceeded = strongSelf.sessionManager?.retry(request) ?? false
  
                      if retrySucceeded, let task = request.task {
                          strongSelf[task] = request
                          return
                      } else {
                          completeTask(session, task, error)
                      }
                  }
              }
          } else {
              completeTask(session, task, error)
          }
      }
  }
  
  // MARK: - URLSessionDataDelegate
  
  extension SessionDelegate: URLSessionDataDelegate {
      /// Tells the delegate that the data task received the initial reply (headers) from the server.
      ///
      /// - parameter session:           The session containing the data task that received an initial reply.
      /// - parameter dataTask:          The data task that received an initial reply.
      /// - parameter response:          A URL response object populated with headers.
      /// - parameter completionHandler: A completion handler that your code calls to continue the transfer, passing a
      ///                                constant to indicate whether the transfer should continue as a data task or
      ///                                should become a download task.
      open func urlSession(
          _ session: URLSession,
          dataTask: URLSessionDataTask,
          didReceive response: URLResponse,
          completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
      {
          guard dataTaskDidReceiveResponseWithCompletion == nil else {
              dataTaskDidReceiveResponseWithCompletion?(session, dataTask, response, completionHandler)
              return
          }
  
          var disposition: URLSession.ResponseDisposition = .allow
  
          if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse {
              disposition = dataTaskDidReceiveResponse(session, dataTask, response)
          }
  
          completionHandler(disposition)
      }
  
      /// Tells the delegate that the data task was changed to a download task.
      ///
      /// - parameter session:      The session containing the task that was replaced by a download task.
      /// - parameter dataTask:     The data task that was replaced by a download task.
      /// - parameter downloadTask: The new download task that replaced the data task.
      open func urlSession(
          _ session: URLSession,
          dataTask: URLSessionDataTask,
          didBecome downloadTask: URLSessionDownloadTask)
      {
          if let dataTaskDidBecomeDownloadTask = dataTaskDidBecomeDownloadTask {
              dataTaskDidBecomeDownloadTask(session, dataTask, downloadTask)
          } else {
              self[downloadTask]?.delegate = DownloadTaskDelegate(task: downloadTask)
          }
      }
  
      /// Tells the delegate that the data task has received some of the expected data.
      ///
      /// - parameter session:  The session containing the data task that provided data.
      /// - parameter dataTask: The data task that provided data.
      /// - parameter data:     A data object containing the transferred data.
      open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
          if let dataTaskDidReceiveData = dataTaskDidReceiveData {
              dataTaskDidReceiveData(session, dataTask, data)
          } else if let delegate = self[dataTask]?.delegate as? DataTaskDelegate {
              delegate.urlSession(session, dataTask: dataTask, didReceive: data)
          }
      }
  
      /// Asks the delegate whether the data (or upload) task should store the response in the cache.
      ///
      /// - parameter session:           The session containing the data (or upload) task.
      /// - parameter dataTask:          The data (or upload) task.
      /// - parameter proposedResponse:  The default caching behavior. This behavior is determined based on the current
      ///                                caching policy and the values of certain received headers, such as the Pragma
      ///                                and Cache-Control headers.
      /// - parameter completionHandler: A block that your handler must call, providing either the original proposed
      ///                                response, a modified version of that response, or NULL to prevent caching the
      ///                                response. If your delegate implements this method, it must call this completion
      ///                                handler; otherwise, your app leaks memory.
      open func urlSession(
          _ session: URLSession,
          dataTask: URLSessionDataTask,
          willCacheResponse proposedResponse: CachedURLResponse,
          completionHandler: @escaping (CachedURLResponse?) -> Void)
      {
          guard dataTaskWillCacheResponseWithCompletion == nil else {
              dataTaskWillCacheResponseWithCompletion?(session, dataTask, proposedResponse, completionHandler)
              return
          }
  
          if let dataTaskWillCacheResponse = dataTaskWillCacheResponse {
              completionHandler(dataTaskWillCacheResponse(session, dataTask, proposedResponse))
          } else if let delegate = self[dataTask]?.delegate as? DataTaskDelegate {
              delegate.urlSession(
                  session,
                  dataTask: dataTask,
                  willCacheResponse: proposedResponse,
                  completionHandler: completionHandler
              )
          } else {
              completionHandler(proposedResponse)
          }
      }
  }
  
  // MARK: - URLSessionDownloadDelegate
  
  extension SessionDelegate: URLSessionDownloadDelegate {
      /// Tells the delegate that a download task has finished downloading.
      ///
      /// - parameter session:      The session containing the download task that finished.
      /// - parameter downloadTask: The download task that finished.
      /// - parameter location:     A file URL for the temporary file. Because the file is temporary, you must either
      ///                           open the file for reading or move it to a permanent location in your app’s sandbox
      ///                           container directory before returning from this delegate method.
      open func urlSession(
          _ session: URLSession,
          downloadTask: URLSessionDownloadTask,
          didFinishDownloadingTo location: URL)
      {
          if let downloadTaskDidFinishDownloadingToURL = downloadTaskDidFinishDownloadingToURL {
              downloadTaskDidFinishDownloadingToURL(session, downloadTask, location)
          } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
              delegate.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
          }
      }
  
      /// Periodically informs the delegate about the download’s progress.
      ///
      /// - parameter session:                   The session containing the download task.
      /// - parameter downloadTask:              The download task.
      /// - parameter bytesWritten:              The number of bytes transferred since the last time this delegate
      ///                                        method was called.
      /// - parameter totalBytesWritten:         The total number of bytes transferred so far.
      /// - parameter totalBytesExpectedToWrite: The expected length of the file, as provided by the Content-Length
      ///                                        header. If this header was not provided, the value is
      ///                                        `NSURLSessionTransferSizeUnknown`.
      open func urlSession(
          _ session: URLSession,
          downloadTask: URLSessionDownloadTask,
          didWriteData bytesWritten: Int64,
          totalBytesWritten: Int64,
          totalBytesExpectedToWrite: Int64)
      {
          if let downloadTaskDidWriteData = downloadTaskDidWriteData {
              downloadTaskDidWriteData(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
          } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
              delegate.urlSession(
                  session,
                  downloadTask: downloadTask,
                  didWriteData: bytesWritten,
                  totalBytesWritten: totalBytesWritten,
                  totalBytesExpectedToWrite: totalBytesExpectedToWrite
              )
          }
      }
  
      /// Tells the delegate that the download task has resumed downloading.
      ///
      /// - parameter session:            The session containing the download task that finished.
      /// - parameter downloadTask:       The download task that resumed. See explanation in the discussion.
      /// - parameter fileOffset:         If the file's cache policy or last modified date prevents reuse of the
      ///                                 existing content, then this value is zero. Otherwise, this value is an
      ///                                 integer representing the number of bytes on disk that do not need to be
      ///                                 retrieved again.
      /// - parameter expectedTotalBytes: The expected length of the file, as provided by the Content-Length header.
      ///                                 If this header was not provided, the value is NSURLSessionTransferSizeUnknown.
      open func urlSession(
          _ session: URLSession,
          downloadTask: URLSessionDownloadTask,
          didResumeAtOffset fileOffset: Int64,
          expectedTotalBytes: Int64)
      {
          if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
              downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
          } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
              delegate.urlSession(
                  session,
                  downloadTask: downloadTask,
                  didResumeAtOffset: fileOffset,
                  expectedTotalBytes: expectedTotalBytes
              )
          }
      }
  }
  
  // MARK: - URLSessionStreamDelegate
  
  #if !os(watchOS)
  
  @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
  extension SessionDelegate: URLSessionStreamDelegate {
      /// Tells the delegate that the read side of the connection has been closed.
      ///
      /// - parameter session:    The session.
      /// - parameter streamTask: The stream task.
      open func urlSession(_ session: URLSession, readClosedFor streamTask: URLSessionStreamTask) {
          streamTaskReadClosed?(session, streamTask)
      }
  
      /// Tells the delegate that the write side of the connection has been closed.
      ///
      /// - parameter session:    The session.
      /// - parameter streamTask: The stream task.
      open func urlSession(_ session: URLSession, writeClosedFor streamTask: URLSessionStreamTask) {
          streamTaskWriteClosed?(session, streamTask)
      }
  
      /// Tells the delegate that the system has determined that a better route to the host is available.
      ///
      /// - parameter session:    The session.
      /// - parameter streamTask: The stream task.
      open func urlSession(_ session: URLSession, betterRouteDiscoveredFor streamTask: URLSessionStreamTask) {
          streamTaskBetterRouteDiscovered?(session, streamTask)
      }
  
      /// Tells the delegate that the stream task has been completed and provides the unopened stream objects.
      ///
      /// - parameter session:      The session.
      /// - parameter streamTask:   The stream task.
      /// - parameter inputStream:  The new input stream.
      /// - parameter outputStream: The new output stream.
      open func urlSession(
          _ session: URLSession,
          streamTask: URLSessionStreamTask,
          didBecome inputStream: InputStream,
          outputStream: OutputStream)
      {
          streamTaskDidBecomeInputAndOutputStreams?(session, streamTask, inputStream, outputStream)
      }
  }
  
  #endif
  