<commit_before>//
//  WukongClient.swift
//  Wukong
//
//  Created by Qusic on 7/22/17.
//  Copyright © 2017 Qusic. All rights reserved.
//

import UIKit
import JavaScriptCore
import SwiftWebSocket

protocol WukongDelegate: class {
    func wukongDidLoadScript()
    func wukongDidFailLoadScript()
    func wukongDidLaunch()
    func wukongDidThrowException(_ exception: String)
    func wukongRequestOpenURL(_ url: String)
}

class WukongClient: NSObject {

    static let sharedInstance = WukongClient()

    private var context: JSContext!
    private weak var delegate: WukongDelegate!

    private var client: JSValue { return context.globalObject.forProperty(Constant.Script.client).forProperty(Constant.Script.main) }
    private var action: JSValue { return context.globalObject.forProperty(Constant.Script.client).forProperty(Constant.Script.action) }
    private var selector: JSValue { return context.globalObject.forProperty(Constant.Script.client).forProperty(Constant.Script.selector) }
    private var platform: JSValue { return context.globalObject.forProperty(Constant.Script.platform) }
    private var store: JSValue { return context.globalObject.forProperty(Constant.Script.store) }

    func run(_ delegate: WukongDelegate) {
        guard context == nil else { return }
        self.delegate = delegate
        ScriptLoader.sharedInstance.load(online: true) { [unowned self] (script) in
            self.setupContext(script)
        }
    }

    func reload() {
        guard context != nil else { return }
        ScriptLoader.sharedInstance.load(online: false) { [unowned self] (script) in
            self.setupContext(script)
        }
    }

    private func setupContext(_ script: String?) {
        guard let script = script else {
            print("Client:", "failed")
            delegate.wukongDidFailLoadScript()
            return
        }
        print("Client:", "loaded")
        delegate.wukongDidLoadScript()
        context = JSContext()!
        context.exceptionHandler = { [unowned self] (context, exception) in
            context?.exception = exception
            if let exception = exception?.toString() {
                print("Client:", "exception", exception)
                self.delegate.wukongDidThrowException(exception)
            }
        }
        context.evaluateScript(script)
        context.globalObject.setValue({
            let apiURL: (String, String) -> String = { (scheme, endpoint) in "\(scheme)s://\(Constant.URL.api)\(endpoint)" }
            var networkHook = JSValue(undefinedIn: context)!
            return [
                "App": [
                    "url": unsafeBitCast({ [unowned self] () in
                        let channel = self.getState([.channel, .name]) ?? ""
                        return "wukong://\(channel)"
                    } as @convention(block) () -> String, to: AnyObject.self),
                    "webview": unsafeBitCast({ [unowned self] (url) in
                        print("Client:", "open", url)
                        self.delegate.wukongRequestOpenURL(url)
                    } as @convention(block) (String) -> Void, to: AnyObject.self),
                    "reload": unsafeBitCast({ [unowned self] () in
                        DispatchQueue.main.async {
                            self.reload()
                        }
                    } as @convention(block) () -> Void, to: AnyObject.self)
                ],
                "Network": [
                    "url": unsafeBitCast({ (scheme, endpoint) in
                        return apiURL(scheme, endpoint)
                    } as @convention(block) (String, String) -> String, to: AnyObject.self),
                    "http": unsafeBitCast({ [unowned self] (method, endpoint, body) in
                        guard let url = URL(string: apiURL("http", endpoint)) else { return self.jsPromise() }
                        var request = URLRequest(url: url)
                        request.httpMethod = method
                        if method != "GET", let data = try? JSONSerialization.data(withJSONObject: body, options: []) {
                            request.httpBody = data
                            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
                        }
                        return self.jsPromise { [unowned self] (resolve, reject) in
                            URLSession.apiSession.dataTask(with: request) { [unowned self] (data, response, error) in
                                guard error == nil else {
                                    print("HTTP:", method, endpoint, error?.localizedDescription ?? "unknown error")
                                    reject(self.jsError(error?.localizedDescription))
                                    return
                                }
                                guard let response = response as? HTTPURLResponse else {
                                    print("HTTP:", method, endpoint, "unknown response")
                                    reject(self.jsError("No response"))
                                    return
                                }
                                let status = response.statusCode
                                let error = 200 ... 299 ~= status ? NSNull() : HTTPURLResponse.localizedString(forStatusCode: status) as Any
                                networkHook.call(withArguments: [method, endpoint, status, error])
                                if let exception = self.context.exception {
                                    self.context.exception = nil
                                    print("HTTP:", method, endpoint, exception)
                                    reject(exception)
                                } else {
                                    var object: Any = ""
                                    if let data = data {
                                        if let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) {
                                            object = json
                                        } else if let string = String(data: data, encoding: .utf8) {
                                            object = string
                                        }
                                    }
                                    print("HTTP:", method, endpoint, "ok")
                                    resolve(JSValue(object: object, in: self.context))
                                }
                            }.resume()
                        }
                    } as @convention(block) (String, String, [String: Any]) -> JSValue, to: AnyObject.self),
                    "websocket": unsafeBitCast({ (endpoint, handler) in
                        guard let url = URL(string: apiURL("ws", endpoint)) else { return }
                        var request = URLRequest(url: url)
                        if let cookieUrl = URL(string: apiURL("http", endpoint)),
                            let cookies = URLSession.apiSession.configuration.httpCookieStorage?.cookies(for: cookieUrl) {
                            let headers = HTTPCookie.requestHeaderFields(with: cookies)
                            headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
                        }
                        let websocket = WebSocket(request: request)
                        typealias SendFunction = @convention(block) (String, [String: Any]) -> Void
                        let send = unsafeBitCast({ (eventName, eventData) in
                            var object = eventData
                            object["eventName"] = eventName
                            guard let data = try? JSONSerialization.data(withJSONObject: object, options: []) else { return }
                            websocket.send(data: data)
                        } as SendFunction, to: AnyObject.self)
                        guard let emit = handler.call(withArguments: [send]) else {
                            websocket.close()
                            return
                        }
                        var timer: Timer? = nil
                        websocket.event.open = {
                            print("WebSocket:", "open")
                            emit.call(withArguments: ["open"])
                            timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak send] (timer) in
                                if let send = send {
                                    unsafeBitCast(send, to: SendFunction.self)("ping", [:])
                                } else {
                                    timer.invalidate()
                                }
                            }
                            timer?.tolerance = 1
                        }
                        websocket.event.close = { (code, reason, clean) in
                            print("WebSocket:", "close", code, reason, clean)
                            emit.call(withArguments: ["close"])
                            timer?.invalidate()
                            DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak websocket] in
                                websocket?.open()
                            }
                        }
                        websocket.event.error = { (error) in
                            print("WebSocket:", "error", error)
                            emit.call(withArguments: ["error"])
                        }
                        websocket.event.message = { (message) in
                            guard let string = message as? String else { return }
                            guard let data = string.data(using: .utf8) else { return }
                            guard var object = (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as? [String: Any] else { return }
                            guard let name = object["eventName"] else { return }
                            object.removeValue(forKey: "eventName")
                            print("WebSocket:", "event", name)
                            emit.call(withArguments: [name, object])
                        }
                    } as @convention(block) (String, JSValue) -> Void, to: AnyObject.self),
                    "hook": unsafeBitCast({ (callback) in
                        networkHook = callback
                    } as @convention(block) (JSValue) -> Void, to: AnyObject.self)
                ],
                "Database": [
                    "get": unsafeBitCast({ (key) in
                        return UserDefaults.clientDefaults.string(forKey: key)
                    } as @convention(block) (String) -> String?, to: AnyObject.self),
                    "set": unsafeBitCast({ (key, value) in
                        UserDefaults.clientDefaults.set(value, forKey: key)
                    } as @convention(block) (String, String) -> Void, to: AnyObject.self),
                    "remove": unsafeBitCast({ (key) in
                        UserDefaults.clientDefaults.removeObject(forKey: key)
                    } as @convention(block) (String) -> Void, to: AnyObject.self)
                ]
            ]
        }(), forProperty: Constant.Script.platform)
        context.globalObject.setValue({<commit_msg>memory leaks?<commit_after>//
//  WukongClient.swift
//  Wukong
//
//  Created by Qusic on 7/22/17.
//  Copyright © 2017 Qusic. All rights reserved.
//

import UIKit
import JavaScriptCore
import SwiftWebSocket

protocol WukongDelegate: class {
    func wukongDidLoadScript()
    func wukongDidFailLoadScript()
    func wukongDidLaunch()
    func wukongDidThrowException(_ exception: String)
    func wukongRequestOpenURL(_ url: String)
}

class WukongClient: NSObject {

    static let sharedInstance = WukongClient()

    private var context: JSContext!
    private weak var delegate: WukongDelegate!

    private var client: JSValue { return context.globalObject.forProperty(Constant.Script.client).forProperty(Constant.Script.main) }
    private var action: JSValue { return context.globalObject.forProperty(Constant.Script.client).forProperty(Constant.Script.action) }
    private var selector: JSValue { return context.globalObject.forProperty(Constant.Script.client).forProperty(Constant.Script.selector) }
    private var platform: JSValue { return context.globalObject.forProperty(Constant.Script.platform) }
    private var store: JSValue { return context.globalObject.forProperty(Constant.Script.store) }

    func run(_ delegate: WukongDelegate) {
        guard context == nil else { return }
        self.delegate = delegate
        ScriptLoader.sharedInstance.load(online: true) { [unowned self] (script) in
            self.setupContext(script)
        }
    }

    func reload() {
        guard context != nil else { return }
        ScriptLoader.sharedInstance.load(online: false) { [unowned self] (script) in
            self.setupContext(script)
        }
    }

    private func setupContext(_ script: String?) {
        guard let script = script else {
            print("Client:", "failed")
            delegate.wukongDidFailLoadScript()
            return
        }
        print("Client:", "loaded")
        delegate.wukongDidLoadScript()
        context = JSContext()!
        context.exceptionHandler = { [unowned self] (context, exception) in
            context?.exception = exception
            if let exception = exception?.toString() {
                print("Client:", "exception", exception)
                self.delegate.wukongDidThrowException(exception)
            }
        }
        context.evaluateScript(script)
        context.globalObject.setValue({
            let apiURL: (String, String) -> String = { (scheme, endpoint) in "\(scheme)s://\(Constant.URL.api)\(endpoint)" }
            var networkHook: JSManagedValue? = nil
            return [
                "App": [
                    "url": unsafeBitCast({ [unowned self] () in
                        let channel = self.getState([.channel, .name]) ?? ""
                        return "wukong://\(channel)"
                    } as @convention(block) () -> String, to: AnyObject.self),
                    "webview": unsafeBitCast({ [unowned self] (url) in
                        print("Client:", "open", url)
                        self.delegate.wukongRequestOpenURL(url)
                    } as @convention(block) (String) -> Void, to: AnyObject.self),
                    "reload": unsafeBitCast({ [unowned self] () in
                        DispatchQueue.main.async {
                            self.reload()
                        }
                    } as @convention(block) () -> Void, to: AnyObject.self)
                ],
                "Network": [
                    "url": unsafeBitCast({ (scheme, endpoint) in
                        return apiURL(scheme, endpoint)
                    } as @convention(block) (String, String) -> String, to: AnyObject.self),
                    "http": unsafeBitCast({ [unowned self] (method, endpoint, body) in
                        guard let url = URL(string: apiURL("http", endpoint)) else { return self.jsPromise() }
                        var request = URLRequest(url: url)
                        request.httpMethod = method
                        if method != "GET", let data = try? JSONSerialization.data(withJSONObject: body, options: []) {
                            request.httpBody = data
                            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
                        }
                        return self.jsPromise { [unowned self] (resolve, reject) in
                            URLSession.apiSession.dataTask(with: request) { [unowned self] (data, response, error) in
                                guard error == nil else {
                                    print("HTTP:", method, endpoint, error?.localizedDescription ?? "unknown error")
                                    reject(self.jsError(error?.localizedDescription))
                                    return
                                }
                                guard let response = response as? HTTPURLResponse else {
                                    print("HTTP:", method, endpoint, "unknown response")
                                    reject(self.jsError("No response"))
                                    return
                                }
                                let status = response.statusCode
                                let error = 200 ... 299 ~= status ? NSNull() : HTTPURLResponse.localizedString(forStatusCode: status) as Any
                                _ = networkHook?.value?.call(withArguments: [method, endpoint, status, error])
                                if let exception = self.context.exception {
                                    self.context.exception = nil
                                    print("HTTP:", method, endpoint, exception)
                                    reject(exception)
                                } else {
                                    var object: Any = ""
                                    if let data = data {
                                        if let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) {
                                            object = json
                                        } else if let string = String(data: data, encoding: .utf8) {
                                            object = string
                                        }
                                    }
                                    print("HTTP:", method, endpoint, "ok")
                                    resolve(JSValue(object: object, in: self.context))
                                }
                            }.resume()
                        }
                    } as @convention(block) (String, String, [String: Any]) -> JSValue, to: AnyObject.self),
                    "websocket": unsafeBitCast({ (endpoint, handler) in
                        guard let url = URL(string: apiURL("ws", endpoint)) else { return }
                        var request = URLRequest(url: url)
                        if let cookieUrl = URL(string: apiURL("http", endpoint)),
                            let cookies = URLSession.apiSession.configuration.httpCookieStorage?.cookies(for: cookieUrl) {
                            let headers = HTTPCookie.requestHeaderFields(with: cookies)
                            headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
                        }
                        let websocket = WebSocket(request: request)
                        typealias SendFunction = @convention(block) (String, [String: Any]) -> Void
                        let send = unsafeBitCast({ (eventName, eventData) in
                            var object = eventData
                            object["eventName"] = eventName
                            guard let data = try? JSONSerialization.data(withJSONObject: object, options: []) else { return }
                            websocket.send(data: data)
                        } as SendFunction, to: AnyObject.self)
                        guard let emit = JSManagedValue(value: handler.call(withArguments: [send]), andOwner: websocket) else {
                            websocket.close()
                            return
                        }
                        var timer: Timer? = nil
                        let checkStatus = { [weak websocket] () -> Bool in
                            if emit.value == nil {
                                websocket?.close()
                                timer?.invalidate()
                                return false
                            }
                            return true
                        }
                        websocket.event.open = {
                            print("WebSocket:", "open")
                            guard checkStatus() else { return }
                            _ = emit.value?.call(withArguments: ["open"])
                            timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak send] (timer) in
                                if let send = send {
                                    unsafeBitCast(send, to: SendFunction.self)("ping", [:])
                                } else {
                                    timer.invalidate()
                                }
                            }
                            timer?.tolerance = 1
                        }
                        websocket.event.close = { (code, reason, clean) in
                            print("WebSocket:", "close", code, reason, clean)
                            guard checkStatus() else { return }
                            _ = emit.value?.call(withArguments: ["close"])
                            timer?.invalidate()
                            DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak websocket] in
                                websocket?.open()
                            }
                        }
                        websocket.event.error = { (error) in
                            print("WebSocket:", "error", error)
                            guard checkStatus() else { return }
                            _ = emit.value?.call(withArguments: ["error"])
                        }
                        websocket.event.message = { (message) in
                            guard checkStatus() else { return }
                            guard let string = message as? String else { return }
                            guard let data = string.data(using: .utf8) else { return }
                            guard var object = (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as? [String: Any] else { return }
                            guard let name = object["eventName"] else { return }
                            object.removeValue(forKey: "eventName")
                            print("WebSocket:", "event", name)
                            _ = emit.value?.call(withArguments: [name, object])
                        }
                    } as @convention(block) (String, JSValue) -> Void, to: AnyObject.self),
                    "hook": unsafeBitCast({ [unowned self] (callback) in
                        networkHook = JSManagedValue(value: callback, andOwner: self.context)
                    } as @convention(block) (JSValue) -> Void, to: AnyObject.self)
                ],
                "Database": [
                    "get": unsafeBitCast({ (key) in
                        return UserDefaults.clientDefaults.string(forKey: key)
                    } as @convention(block) (String) -> String?, to: AnyObject.self),
                    "set": unsafeBitCast({ (key, value) in
                        UserDefaults.clientDefaults.set(value, forKey: key)
                    } as @convention(block) (String, String) -> Void, to: AnyObject.self),
                    "remove": unsafeBitCast({ (key) in
                        UserDefaults.clientDefaults.removeObject(forKey: key)
                    } as @convention(block) (String) -> Void, to: AnyObject.self)
                ]
            ]
        }(), forProperty: Constant.Script.platform)
        context.globalObject.setValue({
{'fraction_non_alphanumeric': 0.09621289662231321, 'fraction_numerical': 0.0019617877857386556, 'mean_word_length': 1.271089588377724, 'pattern_counts': {'":': 26, '<': 3, '<?xml version=': 0, '>': 28, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 63, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2610978', 'n_tokens_mistral': 5407, 'n_tokens_neox': 5132, 'n_words': 1293}