//////////////////////////////////////////////////////////////////////////////////////////////////
//
//  HTTPTask.swift
//
//  Created by Dalton Cherry on 6/3/14.
//  Copyright (c) 2014 Vluxe. All rights reserved.
//
//////////////////////////////////////////////////////////////////////////////////////////////////

import Foundation

/// HTTP Verbs.
///
/// - GET: For GET requests.
/// - POST: For POST requests.
/// - PUT: For PUT requests.
/// - HEAD: For HEAD requests.
/// - DELETE: For DELETE requests.
/// - PATCH: For PATCH requests.
public enum HTTPMethod: String {
    case GET = "GET"
    case POST = "POST"
    case PUT = "PUT"
    case HEAD = "HEAD"
    case DELETE = "DELETE"
    case PATCH = "PATCH"
}

/// Object representation of a HTTP Response.
public class HTTPResponse {
    /// The header values in HTTP response.
    public var headers: Dictionary<String,String>?
    /// The mime type of the HTTP response.
    public var mimeType: String?
    /// The suggested filename for a downloaded file.
    public var suggestedFilename: String?
    /// The body or response data of the HTTP response.
    public var responseObject: AnyObject?
    /// The status code of the HTTP response.
    public var statusCode: Int?
    /// The URL of the HTTP response.
    public var URL: NSURL?
    /// The Error of the HTTP response (if there was one).
    public var error: NSError?
    ///Returns the response as a string
    public var text: String? {
        if let d = self.responseObject as? NSData {
            return  NSString(data: d, encoding: NSUTF8StringEncoding) as? String
        } else if let val: AnyObject = self.responseObject {
            return  "\(val)"
        }
        return nil
    }
    //get the description of the response
    public var description: String {
        var buffer = ""
        if let u = self.URL {
            buffer += "URL:\n\(u)\n\n"
        }
        if let code = self.statusCode {
            buffer += "Status Code:\n\(code)\n\n"
        }
        if let heads = self.headers {
            buffer += "Headers:\n"
            for (key, value) in heads {
                buffer += "\(key): \(value)\n"
            }
            buffer += "\n"
        }
        if let s = self.text {
            buffer += "Payload:\n\(s)\n"
        }
        return buffer
    }
}

/// Holds the blocks of the background task.
class BackgroundBlocks {
    // these 2 only get used for background download/upload since they have to be delegate methods
    var completionHandler:((HTTPResponse) -> Void)?
    var progress:((Double) -> Void)?
    
    /** 
        Initializes a new Background Block
        
        :param: completionHandler The closure that is run when a HTTP Request finished.
        :param: progress The closure that is run on the progress of a HTTP Upload or Download.
    */
    init(_ completionHandler: ((HTTPResponse) -> Void)?,_ progress: ((Double) -> Void)?) {
        self.completionHandler = completionHandler
        self.progress = progress
    }
}

/// Subclass of NSOperation for handling and scheduling HTTPTask on a NSOperationQueue.
public class HTTPOperation : NSOperation {
    private var task: NSURLSessionDataTask!
    private var running = false
    
    /// Controls if the task is finished or not.
    private var done = false
    
    //MARK: Subclassed NSOperation Methods
    
    /// Returns if the task is asynchronous or not. NSURLSessionTask requests are asynchronous.
    override public var asynchronous: Bool {
        return true
    }
    
    /// Returns if the task is current running.
    override public var executing: Bool {
        return running
    }
    
    /// Returns if the task is finished.
    override public var finished: Bool {
        return done
    }
    
    /// Starts the task.
    override public func start() {
        if cancelled {
            self.willChangeValueForKey("isFinished")
            done = true
            self.didChangeValueForKey("isFinished")
            return
        }

        self.willChangeValueForKey("isExecuting")
        self.willChangeValueForKey("isFinished")

        running = true
        done = false
        
        self.didChangeValueForKey("isExecuting")
        self.didChangeValueForKey("isFinished")

        task.resume()
    }
    
    /// Cancels the running task.
    override public func cancel() {
        super.cancel()
        task.cancel()
    }
    
    /// Sets the task to finished.
    public func finish() {
        self.willChangeValueForKey("isExecuting")
        self.willChangeValueForKey("isFinished")
        
        running = false
        done = true
        
        self.didChangeValueForKey("isExecuting")
        self.didChangeValueForKey("isFinished")
    }
}

/// Configures NSURLSession Request for HTTPOperation. Also provides convenience methods for easily running HTTP Request.
public class HTTPTask : NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate {
    var backgroundTaskMap = Dictionary<String,BackgroundBlocks>()
    //var sess: NSURLSession?
    
    public var baseURL: String?
    public var requestSerializer = HTTPRequestSerializer()
    public var responseSerializer: HTTPResponseSerializer?
    //This gets called on auth challenges. If nil, default handling is use.
    //Returning nil from this method will cause the request to be rejected and cancelled
    public var auth:((NSURLAuthenticationChallenge) -> NSURLCredential?)?
    
    //This is for doing SSL pinning
    public var security: HTTPSecurity?
    
    //MARK: Public Methods
    
    /// A newly minted HTTPTask for your enjoyment.
    public override init() {
        super.init()
    }
    
    /** 
        Creates a HTTPOperation that can be scheduled on a NSOperationQueue. Called by convenience HTTP verb methods below.
    
        :param: url The url you would like to make a request to.
        :param: method The HTTP method/verb for the request.
        :param: parameters The parameters are HTTP parameters you would like to send.
        :param: completionHandler The closure that is run when a HTTP Request finished.
    
        :returns: A freshly constructed HTTPOperation to add to your NSOperationQueue.
    */
    public func create(url: String, method: HTTPMethod, parameters: Dictionary<String,AnyObject>!, completionHandler:((HTTPResponse) -> Void)!) ->  HTTPOperation? {
        
        var serialResponse = HTTPResponse()
        let serialReq = createRequest(url, method: method, parameters: parameters)
        if let err = serialReq.error {
            if let handler = completionHandler {
                serialResponse.error = err
                handler(serialResponse)
            }
            return nil
        }
        let opt = HTTPOperation()
        let config = NSURLSessionConfiguration.defaultSessionConfiguration()
        let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
        let task = session.dataTaskWithRequest(serialReq.request,
            completionHandler: {(data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
                if let handler = completionHandler {
                    if let hresponse = response as? NSHTTPURLResponse {
                        serialResponse.headers = hresponse.allHeaderFields as? Dictionary<String,String>
                        serialResponse.mimeType = hresponse.MIMEType
                        serialResponse.suggestedFilename = hresponse.suggestedFilename
                        serialResponse.statusCode = hresponse.statusCode
                        serialResponse.URL = hresponse.URL
                    }
                    serialResponse.error = error
                    if let d = data {
                        serialResponse.responseObject = d
                        if let resSerializer = self.responseSerializer, let resp = response {
                            let resObj = resSerializer.responseObjectFromResponse(resp, data: d)
                            serialResponse.responseObject = resObj.object
                            serialResponse.error = resObj.error
                        }
                        if let code = serialResponse.statusCode where serialResponse.statusCode > 299 {
                            serialResponse.error = self.createError(code)
                        }
                    }
                    handler(serialResponse)
                }
                opt.finish()
            })
        opt.task = task
        return opt
    }
    
    /**
    Creates a HTTPOperation as a HTTP GET request and starts it for you.
    
    :param: url The url you would like to make a request to.
    :param: parameters The parameters are HTTP parameters you would like to send.
    :param: completionHandler The closure that is run when a HTTP Request finished.
    */
    public func GET(url: String, parameters: Dictionary<String,AnyObject>?, completionHandler:((HTTPResponse) -> Void)!) {
        if let opt = self.create(url, method:.GET, parameters: parameters,completionHandler: completionHandler) {
            opt.start()
        }
    }
    
    /**
        Creates a HTTPOperation as a HTTP POST request and starts it for you.
        
        :param: url The url you would like to make a request to.
        :param: parameters The parameters are HTTP parameters you would like to send.
        :param: completionHandler The closure that is run when a HTTP Request finished.
    */
    public func POST(url: String, parameters: Dictionary<String,AnyObject>?, completionHandler:((HTTPResponse) -> Void)!) {
        if let opt = self.create(url, method:.POST, parameters: parameters,completionHandler: completionHandler) {
            opt.start()
        }
    }
    
    /**
    Creates a HTTPOperation as a HTTP PATCH request and starts it for you.
    
    :param: url The url you would like to make a request to.
    :param: parameters The parameters are HTTP parameters you would like to send.
    :param: completionHandler The closure that is run when a HTTP Request finished.
    */
    public func PATCH(url: String, parameters: Dictionary<String,AnyObject>?, completionHandler:((HTTPResponse) -> Void)!) {
        if let opt = self.create(url, method:.PATCH, parameters: parameters,completionHandler: completionHandler) {
            opt.start()
        }
    }
    
    
    /**
        Creates a HTTPOperation as a HTTP PUT request and starts it for you.
        
        :param: url The url you would like to make a request to.
        :param: parameters The parameters are HTTP parameters you would like to send.
        :param: completionHandler The closure that is run when a HTTP Request finished.
    */
    public func PUT(url: String, parameters: Dictionary<String,AnyObject>?, completionHandler:((HTTPResponse) -> Void)!) {
        if let opt = self.create(url, method:.PUT, parameters: parameters,completionHandler: completionHandler) {
            opt.start()
        }
    }
    
    /**
        Creates a HTTPOperation as a HTTP DELETE request and starts it for you.
        
        :param: url The url you would like to make a request to.
        :param: parameters The parameters are HTTP parameters you would like to send.
        :param: completionHandler The closure that is run when a HTTP Request finished.
    */
    public func DELETE(url: String, parameters: Dictionary<String,AnyObject>?, completionHandler:((HTTPResponse) -> Void)!)  {
        if let opt = self.create(url, method:.DELETE, parameters: parameters,completionHandler: completionHandler) {
            opt.start()
        }
    }
    
    /**
        Creates a HTTPOperation as a HTTP HEAD request and starts it for you.
        
        :param: url The url you would like to make a request to.
        :param: parameters The parameters are HTTP parameters you would like to send.
        :param: completionHandler The closure that is run when a HTTP Request finished.
    */
    public func HEAD(url: String, parameters: Dictionary<String,AnyObject>?, completionHandler:((HTTPResponse) -> Void)!) {
        if let opt = self.create(url, method:.HEAD, parameters: parameters,completionHandler: completionHandler) {
            opt.start()
        }
    }
    
    /**
        Creates and starts a HTTPOperation to download a file in the background.
    
        :param: url The url you would like to make a request to.
        :param: method The HTTP method you want to use. Default is GET.
        :param: parameters The parameters are HTTP parameters you would like to send.
        :param: progress The progress returned in the progress closure is between 0 and 1.
        :param: completionHandler The closure that is run when the HTTP Request finishes. The HTTPResponse responseObject object will be a fileURL. You MUST copy the fileURL return in HTTPResponse.responseObject to a new location before using it (e.g. your documents directory).
    */
    public func download(url: String, method: HTTPMethod = .GET, parameters: Dictionary<String,AnyObject>?,progress:((Double) -> Void)!, completionHandler:((HTTPResponse) -> Void)!) -> NSURLSessionDownloadTask? {
        let serialReq = createRequest(url,method: method, parameters: parameters)
        if let err = serialReq.error {
            if let handler = completionHandler {
                var res = HTTPResponse()
                res.error = err
                handler(res)
            }
            return nil
        }
        let ident = createBackgroundIdent()
        let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(ident)
        let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
        let task = session.downloadTaskWithRequest(serialReq.request)
        backgroundTaskMap[ident] = BackgroundBlocks(completionHandler,progress)
        //this does not have to be queueable as Apple's background dameon *should* handle that.
        task.resume()
        return task
    }
    
    /**
    Creates and starts a HTTPOperation to upload a file in the background.
    
    :param: url The url you would like to make a request to.
    :param: method The HTTP method you want to use. Default is POST.
    :param: parameters The parameters are HTTP parameters you would like to send.
    :param: progress The progress returned in the progress closure is between 0 and 1.
    :param: completionHandler The closure that is run when a HTTP Request finished.
    */
    public func upload(url: String, method: HTTPMethod = .POST, parameters: Dictionary<String,AnyObject>?,progress:((Double) -> Void)!, completionHandler:((HTTPResponse) -> Void)!) -> NSURLSessionTask? {
        let serialReq = createRequest(url,method: method, parameters: parameters)
        if let err = serialReq.error {
            if let handler = completionHandler {
                var res = HTTPResponse()
                res.error = err
                handler(res)
            }
            return nil
        }
        let ident = createBackgroundIdent()
        let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(ident)
        let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
        let task = session.uploadTaskWithStreamedRequest(serialReq.request)
        backgroundTaskMap[ident] = BackgroundBlocks(completionHandler,progress)
        task.resume()
        return task
    }
    
    //MARK: Private Helper Methods
    
    /**
        Creates and starts a HTTPOperation to download a file in the background.
    
        :param: url The url you would like to make a request to.
        :param: method The HTTP method/verb for the request.
        :param: parameters The parameters are HTTP parameters you would like to send.
    
        :returns: A NSURLRequest from configured requestSerializer.
    */
    private func createRequest(url: String, method: HTTPMethod, parameters: Dictionary<String,AnyObject>!) -> (request: NSURLRequest, error: NSError?) {
        var urlVal = url
        //probably should change the 'http' to something more generic
        if  let base = self.baseURL where !url.hasPrefix("http") {
            var split = url.hasPrefix("/") ? "" : "/"
            urlVal = "\(base)\(split)\(url)"
        }
        if let encoded = urlVal.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding) {
            if let u = NSURL(string: encoded) {
                return self.requestSerializer.createRequest(u, method: method, parameters: parameters)
            }
        }
        return (NSURLRequest(),createError(-1001))
    }
    
    /**
        Creates a random string to use for the identifier of the background download/upload requests.
    
        :returns: Identifier String.
    */
    private func createBackgroundIdent() -> String {
        let letters = "abcdefghijklmnopqurstuvwxyz"
        var str = ""
        for var i = 0; i < 14; i++ {
            let start = Int(arc4random() % 14)
            str.append(letters[advance(letters.startIndex,start)])
        }
        return "com.vluxe.swifthttp.request.\(str)"
    }
    
    /**
        Creates a random string to use for the identifier of the background download/upload requests.
        
        :param: code Code for error.
        
        :returns: An NSError.
    */
    private func createError(code: Int) -> NSError {
        var text = "An error occured"
        if code == 404 {
            text = "Page not found"
        } else if code == 401 {
            text = "Access denied"
        } else if code == -1001 {
            text = "Invalid URL"
        }
        return NSError(domain: "HTTPTask", code: code, userInfo: [NSLocalizedDescriptionKey: text])
    }
    
    
    /**
        Creates a random string to use for the identifier of the background download/upload requests.
        
        :param: identifier The identifier string.
        
        :returns: An NSError.
    */
    private func cleanupBackground(identifier: String) {
        backgroundTaskMap.removeValueForKey(identifier)
    }
    
    //MARK: NSURLSession Delegate Methods
    
    /// Method for authentication challenge.
    public func URLSession(session: NSURLSession, task: NSURLSessionTask, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential!) -> Void) {
        if let sec = security where challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            let space = challenge.protectionSpace
            if let trust = space.serverTrust {
                if sec.isValid(trust, domain: space.host) {
                    completionHandler(.UseCredential, NSURLCredential(trust: trust))
                    return
                }
            }
            completionHandler(.CancelAuthenticationChallenge, nil)
            return
            
        } else if let a = auth {
            let cred = a(challenge)
            if let c = cred {
                completionHandler(.UseCredential, c)
                return
            }
            completionHandler(.RejectProtectionSpace, nil)
            return
        }
        completionHandler(.PerformDefaultHandling, nil)
    }
    
    //MARK: Methods for background download/upload
    
    ///update the download/upload progress closure
    func handleProgress(session: NSURLSession, totalBytesExpected: Int64, currentBytes: Int64) {
        if session.configuration.valueForKey("identifier") != nil { //temp workaround for radar: 21097168
            let increment = 100.0/Double(totalBytesExpected)
            var current = (increment*Double(currentBytes))*0.01
            if current > 1 {
                current = 1;
            }
            if let blocks = backgroundTaskMap[session.configuration.identifier] {
                if blocks.progress != nil {
                    blocks.progress!(current)
                }
            }
        }
    }
    
    //call the completionHandler closure for upload/download requests
    func handleFinish(session: NSURLSession, task: NSURLSessionTask, response: AnyObject) {
        if session.configuration.valueForKey("identifier") != nil { //temp workaround for radar: 21097168
            if let blocks = backgroundTaskMap[session.configuration.identifier] {
                if let handler = blocks.completionHandler {
                    var resp = HTTPResponse()
                    if let hresponse = task.response as? NSHTTPURLResponse {
                        resp.headers = hresponse.allHeaderFields as? Dictionary<String,String>
                        resp.mimeType = hresponse.MIMEType
                        resp.suggestedFilename = hresponse.suggestedFilename
                        resp.statusCode = hresponse.statusCode
                        resp.URL = hresponse.URL
                    }
                    resp.responseObject = response
                    if let code = resp.statusCode where resp.statusCode > 299 {
                        resp.error = self.createError(code)
                    }
                    handler(resp)
                }
            }
            cleanupBackground(session.configuration.identifier)
        }
    }
    
    /// Called when the background task failed.
    public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
        if let err = error {
            if session.configuration.valueForKey("identifier") != nil { //temp workaround for radar: 21097168
                if let blocks = backgroundTaskMap[session.configuration.identifier] {
                    if let handler = blocks.completionHandler {
                        var res = HTTPResponse()
                        res.error = err
                        handler(res)
                    }
                }
                cleanupBackground(session.configuration.identifier)
            }
        }
    }
    
    /// The background download finished and reports the url the data was saved to.
    func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL!) {
        handleFinish(session, task: downloadTask, response: location)
    }
    
    /// Will report progress of background download
    func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        handleProgress(session, totalBytesExpected: totalBytesExpectedToWrite, currentBytes:totalBytesWritten)
    }
    
    /// The background download finished, don't have to really do anything.
    public func URLSessionDidFinishEventsForBackgroundURLSession(session: NSURLSession) {
    }
    
    /// The background upload finished and reports the response.
    func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData!) {
        handleFinish(session, task: dataTask, response: data)
    }
    
    ///Will report progress of background upload
    public func URLSession(session: NSURLSession, task: NSURLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
        handleProgress(session, totalBytesExpected: totalBytesExpectedToSend, currentBytes:totalBytesSent)
    }
    
    //implement if we want to support partial file upload/download
    func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
    }
}
