//
//  OAuthSwiftClient.swift
//  OAuthSwift
//
//  Created by Dongri Jin on 6/21/14.
//  Copyright (c) 2014 Dongri Jin. All rights reserved.
//

import Foundation

var OAuthSwiftDataEncoding: NSStringEncoding = NSUTF8StringEncoding

public class OAuthSwiftClient: NSObject {

    private(set) public var credential: OAuthSwiftCredential
    public var paramsLocation: OAuthSwiftHTTPRequest.ParamsLocation = .AuthorizationHeader

    static let separator: String = "\r\n"
    static var separatorData: NSData = {
        return OAuthSwiftClient.separator.dataUsingEncoding(OAuthSwiftDataEncoding)!
    }()

    // MARK: init
    public init(consumerKey: String, consumerSecret: String) {
        self.credential = OAuthSwiftCredential(consumer_key: consumerKey, consumer_secret: consumerSecret)
    }
    
    public init(consumerKey: String, consumerSecret: String, accessToken: String, accessTokenSecret: String) {
        self.credential = OAuthSwiftCredential(oauth_token: accessToken, oauth_token_secret: accessTokenSecret)
        self.credential.consumer_key = consumerKey
        self.credential.consumer_secret = consumerSecret
    }

    // MARK: client methods
    public func get(urlString: String, parameters: [String: AnyObject] = [:], headers: [String:String]? = nil, success: OAuthSwiftHTTPRequest.SuccessHandler?, failure: OAuthSwiftHTTPRequest.FailureHandler?) {
        self.request(urlString, method: .GET, parameters: parameters, headers: headers, success: success, failure: failure)
    }
    
    public func post(urlString: String, parameters: [String: AnyObject] = [:], headers: [String:String]? = nil, success: OAuthSwiftHTTPRequest.SuccessHandler?, failure: OAuthSwiftHTTPRequest.FailureHandler?) {
        self.request(urlString, method: .POST, parameters: parameters, headers: headers, success: success, failure: failure)
    }

    public func put(urlString: String, parameters: [String: AnyObject] = [:], headers: [String:String]? = nil, success: OAuthSwiftHTTPRequest.SuccessHandler?, failure: OAuthSwiftHTTPRequest.FailureHandler?) {
        self.request(urlString, method: .PUT, parameters: parameters, headers: headers,success: success, failure: failure)
    }

    public func delete(urlString: String, parameters: [String: AnyObject] = [:], headers: [String:String]? = nil, success: OAuthSwiftHTTPRequest.SuccessHandler?, failure: OAuthSwiftHTTPRequest.FailureHandler?) {
        self.request(urlString, method: .DELETE, parameters: parameters, headers: headers,success: success, failure: failure)
    }

    public func patch(urlString: String, parameters: [String: AnyObject] = [:], headers: [String:String]? = nil, success: OAuthSwiftHTTPRequest.SuccessHandler?, failure: OAuthSwiftHTTPRequest.FailureHandler?) {
        self.request(urlString, method: .PATCH, parameters: parameters, headers: headers,success: success, failure: failure)
    }
    
    public func request(url: String, method: OAuthSwiftHTTPRequest.Method, parameters: [String: AnyObject] = [:], headers: [String:String]? = nil, checkTokenExpiration: Bool = true, success: OAuthSwiftHTTPRequest.SuccessHandler?, failure: OAuthSwiftHTTPRequest.FailureHandler?) {
        
        if checkTokenExpiration && self.credential.isTokenExpired()  {
            let errorInfo = [NSLocalizedDescriptionKey: NSLocalizedString("The provided token is expired.", comment:"Token expired, retrieve new token by using the refresh token")]
            
            if let failureHandler = failure {
                failureHandler(error: NSError(domain: OAuthSwiftErrorDomain, code: OAuthSwiftErrorCode.TokenExpiredError.rawValue, userInfo: errorInfo))
            }
            
            return
        }
        
        if let request = makeRequest(url, method: method, parameters: parameters, headers: headers) {
            
            request.successHandler = success
            request.failureHandler = failure
            request.start()
        }
    }

    public func makeRequest(request: NSURLRequest) -> OAuthSwiftHTTPRequest {
        let request = OAuthSwiftHTTPRequest(request: request, paramsLocation: self.paramsLocation)
        return makeOAuthSwiftHTTPRequest(request)
    }

    public func makeRequest(urlString: String, method: OAuthSwiftHTTPRequest.Method, parameters: [String: AnyObject] = [:], headers: [String:String]? = nil) -> OAuthSwiftHTTPRequest? {
        guard let url = NSURL(string: urlString) else {
            return nil
        }

        let request = OAuthSwiftHTTPRequest(URL: url, method: method, parameters: parameters, paramsLocation: self.paramsLocation)
        if let addHeaders = headers {
            request.headers = addHeaders
        }
        return makeOAuthSwiftHTTPRequest(request)
    }

    public func makeOAuthSwiftHTTPRequest(request: OAuthSwiftHTTPRequest) -> OAuthSwiftHTTPRequest {
        var requestHeaders = [String:String]()
        var signatureUrl = request.URL
        var signatureParameters = request.parameters

        // Check if body must be hashed (oauth1)
        let body: NSData? = nil
        if request.HTTPMethod.isBody {
            if let contentType = request.headers["Content-Type"]?.lowercaseString {

                if contentType.rangeOfString("application/json") != nil {
                    // TODO: oauth_body_hash create body before signing if implementing body hashing
                    /*do {
                    let jsonData: NSData = try NSJSONSerialization.dataWithJSONObject(parameters, options: [])
                    request.HTTPBody = jsonData
                    requestHeaders["Content-Length"] = "\(jsonData.length)"
                    body = jsonData
                    }
                    catch {
                    }*/

                    signatureParameters = [:] // parameters are not used for general signature (could only be used for body hashing
                }
                // else other type are not supported, see setupRequestForOAuth()
            }
        }

        // Need to account for the fact that some consumers will have additional parameters on the
        // querystring, including in the case of fetching a request token. Especially in the case of
        // additional parameters on the request, authorize, or access token exchanges, we need to
        // normalize the URL and add to the parametes collection.

        var queryStringParameters = Dictionary<String, AnyObject>()
        let urlComponents = NSURLComponents(URL: request.URL, resolvingAgainstBaseURL: false )
        if let queryItems = urlComponents?.queryItems {
            for queryItem in queryItems {
                let value = queryItem.value?.safeStringByRemovingPercentEncoding ?? ""
                queryStringParameters.updateValue(value, forKey: queryItem.name)
            }
        }

        // According to the OAuth1.0a spec, the url used for signing is ONLY scheme, path, and query
        if(queryStringParameters.count>0)
        {
            urlComponents?.query = nil
            // This is safe to unwrap because these just came from an NSURL
            signatureUrl = urlComponents?.URL ?? request.URL
        }
        signatureParameters = signatureParameters.join(queryStringParameters)

        switch self.paramsLocation {
        case .AuthorizationHeader:
            //Add oauth parameters in the Authorization header
            requestHeaders += self.credential.makeHeaders(signatureUrl, method: request.HTTPMethod, parameters: signatureParameters, body: body)
        case .RequestURIQuery:
            //Add oauth parameters as request parameters
            request.parameters += self.credential.authorizationParametersWithSignatureForMethod(request.HTTPMethod, url: signatureUrl, parameters: signatureParameters, body: body)
        }

        request.headers = requestHeaders + request.headers
        request.dataEncoding = OAuthSwiftDataEncoding
        
        return request
    }

    public func postImage(urlString: String, parameters: Dictionary<String, AnyObject>, image: NSData, success: OAuthSwiftHTTPRequest.SuccessHandler?, failure: OAuthSwiftHTTPRequest.FailureHandler?) {
        self.multiPartRequest(urlString, method: .POST, parameters: parameters, image: image, success: success, failure: failure)
    }

    func multiPartRequest(url: String, method: OAuthSwiftHTTPRequest.Method, parameters: Dictionary<String, AnyObject>, image: NSData, success: OAuthSwiftHTTPRequest.SuccessHandler?, failure: OAuthSwiftHTTPRequest.FailureHandler?) {

        if let request = makeRequest(url, method: method, parameters: parameters) {
            
            var paramImage = [String: AnyObject]()
            paramImage["media"] = image
            let boundary = "AS-boundary-\(arc4random())-\(arc4random())"
            let type = "multipart/form-data; boundary=\(boundary)"
            let body = self.multiPartBodyFromParams(paramImage, boundary: boundary)
            
            request.HTTPBody = body
            request.headers += ["Content-Type": type] // "Content-Length": body.length.description
            
            request.successHandler = success
            request.failureHandler = failure
            request.start()
        }
    }

    public func multiPartBodyFromParams(parameters: [String: AnyObject], boundary: String) -> NSData {
        let data = NSMutableData()

        let prefixString = "--\(boundary)\r\n"
        let prefixData = prefixString.dataUsingEncoding(OAuthSwiftDataEncoding)!

        
        for (key, value) in parameters {
            var sectionData: NSData
            var sectionType: String?
            var sectionFilename: String?
            if  let multiData = value as? NSData where key == "media" {
                sectionData = multiData
                sectionType = "image/jpeg"
                sectionFilename = "file"
            } else {
                sectionData = "\(value)".dataUsingEncoding(OAuthSwiftDataEncoding)!
            }

            data.appendData(prefixData)
            let multipartData = OAuthSwiftMultipartData(name: key, data: sectionData, fileName: sectionFilename, mimeType: sectionType)
            data.appendMultipartData(multipartData, encoding: OAuthSwiftDataEncoding, separatorData: OAuthSwiftClient.separatorData)
        }

        let endingString = "--\(boundary)--\r\n"
        let endingData = endingString.dataUsingEncoding(OAuthSwiftDataEncoding)!
        data.appendData(endingData)
        return data
    }
    
    public func postMultiPartRequest(url: String, method: OAuthSwiftHTTPRequest.Method, parameters: Dictionary<String, AnyObject>, multiparts: Array<OAuthSwiftMultipartData> = [], checkTokenExpiration: Bool = true, success: OAuthSwiftHTTPRequest.SuccessHandler?, failure: OAuthSwiftHTTPRequest.FailureHandler?) {
        
        if let request = makeRequest(url, method: method, parameters: parameters) {

            let boundary = "POST-boundary-\(arc4random())-\(arc4random())"
            let type = "multipart/form-data; boundary=\(boundary)"
            let body = self.multiDataFromObject(parameters, multiparts: multiparts, boundary: boundary)

            request.HTTPBody = body
            request.headers += ["Content-Type": type] // "Content-Length": body.length.description
            
            request.successHandler = success
            request.failureHandler = failure
            request.start()
        }
    }

    func multiDataFromObject(object: [String:AnyObject], multiparts: Array<OAuthSwiftMultipartData>, boundary: String) -> NSData? {
        let data = NSMutableData()

        let prefixString = "--\(boundary)\r\n"
        let prefixData = prefixString.dataUsingEncoding(OAuthSwiftDataEncoding)!

        for (key, value) in object {
            guard let valueData = "\(value)".dataUsingEncoding(OAuthSwiftDataEncoding) else {
                continue
            }
            data.appendData(prefixData)
            let multipartData = OAuthSwiftMultipartData(name: key, data: valueData, fileName: nil, mimeType: nil)
            data.appendMultipartData(multipartData, encoding: OAuthSwiftDataEncoding, separatorData: OAuthSwiftClient.separatorData)
        }

        for multipart in multiparts {
            data.appendData(prefixData)
            data.appendMultipartData(multipart, encoding: OAuthSwiftDataEncoding, separatorData: OAuthSwiftClient.separatorData)
        }

        let endingString = "--\(boundary)--\r\n"
        let endingData = endingString.dataUsingEncoding(OAuthSwiftDataEncoding)!
        data.appendData(endingData)

        return data
    }

}
