//
//  DONSURLSessionClient.h
//  DOCore
//
//  Created by Ben Dolman on 10/16/14.
//
//

#import <Foundation/Foundation.h>
#import <Mantle/Mantle.h>
@class DONSURLSessionClientRequest;
@protocol DONSURLSessionClientDelegate;

/**
 * Creates and returns a new, unique identifier appropriate for use the identifier for a background
 * NSURLSession. The returned identifier is in the following format:
 * 
 * <bundle_identifier>.<uuid>
 *
 */
extern NSString *DOUniqueSessionIdentifier();

typedef NS_ENUM(NSInteger, DONSURLSessionClientState)
{
    DONSURLSessionClientStateInvalidated, /** The client has been invalidated */
    DONSURLSessionClientStateSuspended, /** The client is suspended */
    DONSURLSessionClientStateIdle, /** The client is not currently attempting to transferring data */
    DONSURLSessionClientStateActive /** The client is attempting to transfer data */
};

/**
 * Wraps a single NSURLSession instance to provide additional features such as identifier-based requests, 
 * request throttling, HTTP error code awareness, retries and the ability to suspend an entire session.
 */
@interface DONSURLSessionClient : NSObject

+ (NSString *)stringForState:(DONSURLSessionClientState)state;

/**
 * HTTP response codes that indicate a request was successful.
 * The default is 200-299.
 */
@property (copy) NSIndexSet *successStatusCodes;
/**
 * HTTP response codes that indicate a fatal error. Responses with these codes will not be retried.
 * The default is 400-499.
 */
@property (copy) NSIndexSet *fatalStatusCodes;

/**
 * The maximum number of NSURLSessionTasks that should exist at one time. The client
 * will call the delegate on-demand to create new ones. 0 indicates no limit.
 * The default is 0.
 */
@property (assign) NSUInteger maxInFlightRequests;
/**
 * The maximum number of times to retry an individual request. Once exceeded the error is considered fatal
 * and the delgate is notified.
 * The default is 0.
 */
@property (assign) NSUInteger maxRetriesPerRequest;
/**
 * Whether received data should be saved and returned to the delegate. If YES, then any response data
 * will be included on the task's `clientResponseData` property.
 * The default is YES.
 */
@property (assign) BOOL saveResponseData;
/**
 * Whether or not to allow invalid SSL certificates. This should usually only be enabled in development.
 * The default is NO.
 */
@property (assign) BOOL allowsInvalidSSLCertificate;

/**
 * The current state of the client
 */
@property (assign, readonly) DONSURLSessionClientState state;
/**
 * The number of requests currently in progress. Even if the client is suspended, outstanding requests
 * are considered to be in flight.
 */
@property (assign, readonly) NSInteger inFlightRequestCount;
/**
 * The total number of requests currently being managed by the client. This number increases immediately
 * after adding a request and decreases only when a request is cancelled or has completed (either successfully
 * or with a fatal error).
 */
@property (assign, readonly) NSInteger totalRequestCount;

/**
 * The session configuration specified in the init method.
 */
@property (copy, readonly) NSURLSessionConfiguration *sessionConfiguration;
/**
 * The delegate specified in the init method.
 */
@property (weak, readonly) id<DONSURLSessionClientDelegate> delegate;
/**
 * The queue on which to deliver calls to delegate methods (with one exception, see below).
 */
@property (strong, readonly) NSOperationQueue *delegateQueue;


/**
 * Creates a new session client instance. The client starts in a suspended state. You must call `resume` to
 * begin network transfers.
 *
 * @param configuration The configuration that will be used to create the NSURLSession instance. If this is a
 *                      background session you should not create more than one client for that identifier. Required.
 * @param delegate The delegate. Required.
 * @param delegateQueue The queue on which to invoke delegate methods. If nil then a serial queue is created. Optional.
 */
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration
                                    delegate:(id<DONSURLSessionClientDelegate>)delegate
                               delegateQueue:(NSOperationQueue *)delegateQueue;

/**
 * Immediately invalidates the session and cancels all requests. Once invalidated the client cannot be used for
 * further requests.
 */
- (void)invalidateAndCancel;

/**
 * Temporarily suspends in-progress requests (by calling `suspend` on each in-flight NSURLSessionTask) and
 * halts further creation of tasks.
 */
- (void)suspend;

/**
 * Resumes a suspended client. Does nothing if the client is not currently suspended.
 */
- (void)resume;

/**
 * Adds a new request to be managed by the client. If an task already exists with the same identifier
 * it is cancelled and removed (treated as though cancelRequestsWithIdentifier was called on it).
 */
- (void)addRequest:(DONSURLSessionClientRequest *)request;
/**
 * Cancels a request with the specified identifier. No further delegate calls will be made
 * for that request. Does nothing if the identifier is unknown.
 */
- (void)cancelRequestWithIdentifier:(NSString *)requestIdentifier;
/**
 * Returns the request for a particular identifier. Returns nil if identifier is unknown.
 */
- (DONSURLSessionClientRequest *)requestForIdentifier:(NSString *)requestIdentifier;

@end


@protocol DONSURLSessionClientDelegate <NSObject>

@required

/**
 * The client calls this whenever it's time to create a new task for a request. The delegate must
 * return a task created using the specified session.
 *
 * @note Unlike the other delegate methods this one may be called from any thread.
 *
 * @param client The client asking for the task
 * @param request The request for which a task should be created
 * @param session The session in which the task must be created.
 */
- (NSURLSessionTask *)sessionClient:(DONSURLSessionClient *)client
                     taskForRequest:(DONSURLSessionClientRequest *)request
                          inSession:(NSURLSession *)session;

@optional

/**
 * Called when a request has completed (either successfully or with a fatal error). Called
 * on the delegate queue.
 *
 * @param client The client that executed the request
 * @param task The task that was executed. The task may have some of the properties specified in the
 *             NSURLSessionTask(DONSURLSessionClient) category. If the task is a file download task then the
 *             file will be specified in the clientDownloadURL property. The client must deal with that data
 *             (read the file, move it, etc.) before returning from this method. If this property is nil then
 *             error will be set.
 * @param request The original request
 * @param error If the request failed this will contain the error. If the response completed but the HTTP
 *              status code was unacceptable then the error domain will be DOHTTPErrorDomain and the errorCode
 *              will be the HTTP response code.
 */
- (void)sessionClient:(DONSURLSessionClient *)client
      didCompleteTask:(NSURLSessionTask *)task
           forRequest:(DONSURLSessionClientRequest *)request
                error:(NSError *)error;

/**
 * After calling `resume` for the first time the client will create the NSURLSession and fetch all
 * current tasks. Once it has finished it will invoke this to let the delegate know that the client
 * is now aware of all in-progress tasks.
 *
 * If you are using a background session configuration then you should typically wait until this is
 * called before adding or checking for requests.
 */
- (void)sessionClientDidFinishReconnectingToSession:(DONSURLSessionClient *)client;

/**
 * A pass-through call from -[NSURLSessionDelegate URLSessionDidFinishEventsForBackgroundURLSession:]
 */
- (void)sessionClientDidFinishEventsForBackgroundURLSessionClient:(DONSURLSessionClient *)client;

@end

/**
 * Represents a request that can be managed by a session client.
 */
@interface DONSURLSessionClientRequest : MTLModel

@property (copy, readonly) NSString *requestIdentifier;
@property (copy, readonly) id<NSCoding> userInfo;

/**
 * Initializes a new request
 *
 * @param identifier A client-specified identifier for this request
 * @param userInfo An optional userInfo object.
 */
- (instancetype)initWithRequestIdentifier:(NSString *)identifier
                                 userInfo:(id<NSCoding>)userInfo;

@end

/**
 * Category methods on NSURLSessionTask
 */
@interface NSURLSessionTask (DONSURLSessionClient)

/**
 * If the tasks's `response` property is a subclass of NSHTTPURLResponse then it is casted as such and returned.
 * Otherwise this returns nil.
 */
@property (strong, readonly) NSHTTPURLResponse *HTTPResponse;

/**
 * If -[DONSURLSessionClient saveResponseData] is YES then the response data is saved into this property before
 * being returned to the delegate in the completion method.
 */
@property (strong, readonly) NSData *clientResponseData;

/**
 * If the task is a NSURLSessionDownloadTask then the download URL is saved into this property. This is the 
 * file URL that is created by the NSURLSession so the delegate must do something with this data (read it, move it,
 * etc.) before returning from the delegate completion method.
 */
@property (strong, readonly) NSURL *clientDownloadURL;

@end