//
//  The template was designed by Hưng Nguyễn. 
//  Website, application (native/flutter), hosting, SEO.
//  Email: hungnguyen.it36@gmail.com
//

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

import RxSwift
import XMLMapper

class Envelope: XMLMappable {
    var nodeName: String!

    var body: Any?

    required init?(map: XMLMap) {}

    func mapping(map: XMLMap) {
        body = map.XML["soap:Body"]
    }
}

enum HttpMethod: String{
    case post = "POST"
    case get = "GET"
    case put = "PUT"
    case del = "DELETE"
}

typealias ResponseBlock = (_ error:Error?,_ response: Envelope?) ->()
typealias ErrorBlock = (_ error:NSError?) ->()
typealias UploadProgessBlock = ((_ progess:Double)->Void)

//MARK: Base
 class APIRequest {
    
    static let shared = APIRequest()
    
    var enableSSL:Bool = false //_USING_HTTPS_PROTOCOL_
    var boolEncoding = BoolEncoding.numeric
    var arrayEncoding = ArrayEncoding.brackets
    let ctrl = "\r\n"
    lazy var globalQueue:OperationQueue = {
        let queue = OperationQueue()
        queue.name = "com.dtd.APIRequest.OperationQueue"
        return queue
    }()
    
    func request(method: HttpMethod = .post, soapAction: String, parameters: [String: Any]) -> Observable<Envelope>{
        return Observable.create { (observer) -> Disposable in
            self._request(url: AppConfig.API_URL, method: method, soapAction: soapAction, parameters: parameters) { error, response in
                if let error = error {
                    observer.onError(error)
                } else if let env = response {
                    observer.onNext(env)
                    observer.onCompleted()
                } else {
                    observer.onError(TrackerError.selfError)
                }
            } _: { error in
                if let error = error {
                    observer.onError(error)
                } else {
                    observer.onError(TrackerError.selfError)
                }
            }
            return Disposables.create()
        }
    }
    
    func _request(url:String, method: HttpMethod, soapAction: String , parameters: [String: Any], _ completion:@escaping ResponseBlock,_ errorHandlerBlock:ErrorBlock?){
        guard let urlRequest =  URL(string: url) else {
            let error = NSError(domain:"1000", code:404, userInfo:nil)
            completion(error,nil)
            return
            
        }
        
        let postData = "<?xml version=\"1.0\" encoding=\"utf-8\"?> <soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"> <soap:Body> <\(soapAction) xmlns=\"http://NCR.SpartReal.Sanyo.MobileIF/\"> \(convertBody(parameters)) </\(soapAction)> </soap:Body> </soap:Envelope>".data(using: .utf8)

        var request = URLRequest(url: urlRequest,timeoutInterval: 60)
        request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type")
        request.httpMethod = method.rawValue
        request.httpBody = postData
        request.addValue("114.179.20.205", forHTTPHeaderField: "Host")
        request.addValue("http://NCR.SpartReal.Sanyo.MobileIF/\(soapAction)", forHTTPHeaderField: "SOAPAction")
        
        print("curl command: " + request.cURL)

        let task = URLSession.shared.dataTask(with: request) { data, response, error in
           
            DispatchQueue.main.async {
                if let data = data {
                    do {
                        let xml = try XMLSerialization.xmlObject(with: data, options: [.default, .cdataAsString])
                        let env = XMLMapper<Envelope>().map(XMLObject: xml)
                        completion(nil, env)
                    }  catch(let error) {
                        APIRequestHandleError.shared.handleError(error)
                        completion(error, nil)
                    }
                }else{
                    APIRequestHandleError.shared.handleError(error)
                    completion(error, nil)
                }
            }
        }

        task.resume()
    }
}


//MARK: Encode
extension APIRequest {
    
    private func convertBody(_ parameters: [String: Any]) -> String {
        var components: [(String, String)] = []
        
        for key in parameters.keys.sorted(by: <) {
            let value = parameters[key]!
            components += queryComponents(fromKey: key, value: value)
        }
        return components.map { "<\($0)>\($1)</\($0)>" }.joined(separator: " ")
    }
    
    private func query(_ parameters: [String: Any]) -> String {
        var components: [(String, String)] = []
        
        for key in parameters.keys.sorted(by: <) {
            let value = parameters[key]!
            components += queryComponents(fromKey: key, value: value)
        }
        return components.map { "\($0)=\($1)" }.joined(separator: "&")
    }
    
    public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
        var components: [(String, String)] = []
        
        if let dictionary = value as? [String: Any] {
            for (nestedKey, value) in dictionary {
                components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
            }
        } else if let array = value as? [Any] {
            for value in array {
                components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
            }
        } else if let value = value as? NSNumber {
            if value.isBool {
                components.append((escape(key), escape(boolEncoding.encode(value: value.boolValue))))
            } else {
                components.append((escape(key), escape("\(value)")))
            }
        } else if let bool = value as? Bool {
            components.append((escape(key), escape(boolEncoding.encode(value: bool))))
        } else {
            components.append((escape(key), escape("\(value)")))
        }
        
        return components
    }
    public func escape(_ string: String) -> String {
        let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
        let subDelimitersToEncode = "!$&'()*+,;="
        
        var allowedCharacterSet = CharacterSet.urlQueryAllowed
        allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
        
        var escaped = ""
        
        if #available(iOS 8.3, *) {
            escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
        } else {
            let batchSize = 50
            var index = string.startIndex
            
            while index != string.endIndex {
                let startIndex = index
                let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex
                let range = startIndex..<endIndex
                
                let substring = string[range]
                
                escaped += substring.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? String(substring)
                
                index = endIndex
            }
        }
        
        return escaped
    }
    
    public enum BoolEncoding {
        case numeric, literal
        
        func encode(value: Bool) -> String {
            switch self {
            case .numeric:
                return value ? "1" : "0"
            case .literal:
                return value ? "true" : "false"
            }
        }
    }
    
    public enum ArrayEncoding {
        case brackets, noBrackets
        
        func encode(key: String) -> String {
            switch self {
            case .brackets:
                return "\(key)[]"
            case .noBrackets:
                return key
            }
        }
    }
}

extension NSNumber {
    fileprivate var isBool: Bool { return CFBooleanGetTypeID() == CFGetTypeID(self) }
}
