/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

import AGConnectDatabase

@objc(AGCCloudDBPlugin) class AGCCloudDBPlugin : CDVPlugin {
    
    private lazy var dbZoneList = ConcurrentHashMap<String, AGCCloudDBZone>()
    private var agcCloudDB: AGConnectCloudDB? = nil
    private lazy var listenerHandlerList = ConcurrentHashMap<String, ConcurrentHashMap<String, AGCCloudDBListenerHandler>>()
    
    private var cloudDBClassError: String = String.init()
    private var cloudDBNullError: String = String.init()
    private var dbZoneNullError: String = String.init()
    private var ERROR_PARAMETER_MESSAGE: String = String.init()
    
    private var id: String = String.init()
    private var listenerId: String = String.init()
    private var cloudDBZoneObjectArray: [Any] = []
    private var className: String = String.init()
    private var clazz: AGCCloudDBObject.Type = AGCCloudDBObject.self
    
    private var queryObject: Dictionary<String, Any> = Dictionary<String, Any>()
    private var queryPolicy: Int = Int.init()
    
    private var fieldName: String = String.init()
    
    private var transactionsArray: [Any] = []
    
    private var dbZone: AGCCloudDBZone = AGCCloudDBZone.init()
    private var query: AGCCloudDBQuery = AGCCloudDBQuery.init()
    private var policy: AGCCloudDBQueryPolicy = AGCCloudDBQueryPolicy.default
    
    
    /// All the AGC CloudDB API's can be reached via AGCCloudDBViewModel class instance
    private lazy var viewModel: AGCCloudDBViewModel = AGCCloudDBViewModel()
    
    override func pluginInitialize() {
        super.pluginInitialize()
        cloudDBClassError = "Class cannot be found."
        cloudDBNullError = "CloudDB is null."
        dbZoneNullError = "CloudDBZone is null, try re-open it."
        ERROR_PARAMETER_MESSAGE = "Error arguments paramater. methodName : "
    }
    enum MethodName: String {
        // MARK: - AGCCloudDBService
        case initialize
        case createObjectType
        case getCloudDBZoneConfigs
        case openCloudDBZone2
        case openCloudDBZone
        case addEventListener
        case addDataEncryptionKeyListener
        case deleteCloudDBZone
        case closeCloudDBZone
        case enableNetwork
        case disableNetwork
        case updateDataEncryptionKey
        case setUserKey
        
        // MARK: - CloudDBZoneService
        case getCloudDBZoneConfig
        case runTransaction
        case executeUpsert
        case executeDelete
        case executeQuery
        case executeAverageQuery
        case executeSumQuery
        case executeMaximumQuery
        case executeMinimalQuery
        case executeCountQuery
        case executeQueryUnsynced
        case subscribeSnapshot
        case unsubscribeSnapshot
    }
    
    
    // MARK: - AGCCloudDBService
    @objc(AGCCloudDBService:)
    func AGCCloudDBService(command: CDVInvokedUrlCommand){
        guard let methodName = command.arguments[0] as? String else {
            sendError(
                message: "Error AGCCloudDBProviderImpl name paramater. Should be module name: AGCCloudDBProviderImpl",
                command.methodName, command.callbackId
            )
            return
        }
        AGCCloudDBLog.showInPanel(message: methodName, type: .call)
        switch methodName {
        case MethodName.initialize.rawValue:
            var errorPointer: NSError? = nil
            AGConnectCloudDB.initEnvironment(&errorPointer)
            agcCloudDB = AGConnectCloudDB.shareInstance()
            if let error = errorPointer {
                sendError(error: error, methodName, command.callbackId)
            }else {
                sendSuccess(methodName, command.callbackId)
            }
            break
        case MethodName.createObjectType.rawValue:
            guard let agcCloudDB = agcCloudDB else {
                return sendError(message: cloudDBNullError.description, methodName, command.callbackId)
            }
            var errorPointer: NSError? = nil
            agcCloudDB.createObjectType(AGCCloudDBObjectTypeInfoHelper.obtainObjectTypeInfo(), error: &errorPointer)
            if let error = errorPointer {
                sendError(error: error, methodName, command.callbackId)
            }else {
                sendSuccess(methodName, command.callbackId)
            }
            break
        case MethodName.getCloudDBZoneConfigs.rawValue:
            guard let agcCloudDB = agcCloudDB else {
                return sendError(message: cloudDBNullError.description, methodName, command.callbackId)
            }
            let array: NSMutableArray = []
            for zoneConfig in agcCloudDB.zoneConfigs() {
                var map: Dictionary<String, Any> = Dictionary<String, Any>()
                map["accessProperty"] = zoneConfig.accessMode.rawValue
                map["capacity"] = zoneConfig.capacity
                map["cloudDBZoneName"] = zoneConfig.zoneName
                map["isPersistenceAvailable"] = zoneConfig.persistence
                map["syncProperty"] = zoneConfig.syncMode.rawValue
                map["isEncrypted"] = zoneConfig.encrypted
                array.add(map)
            }
            self.commandDelegate.send(CDVPluginResult (
                status: CDVCommandStatus_OK,
                messageAs: array as? [Any]
            ), callbackId: command.callbackId)
            break
        case MethodName.openCloudDBZone2.rawValue:
            guard let agcCloudDB = agcCloudDB else {
                return sendError(message: cloudDBNullError.description, methodName, command.callbackId)
            }
            guard let config = command.arguments[1] as? Dictionary<String, Any> else {
                sendError(message: "Error config paramater.", methodName, command.callbackId)
                return
            }
            guard let isAllowToCreate = command.arguments[2] as? Bool else {
                sendError(message: "Error isAllowToCreate paramater.", methodName, command.callbackId)
                return
            }
            guard let dbZoneId = command.arguments[3] as? String else {
                sendError(message: "Error id paramater.", methodName, command.callbackId)
                return
            }
            if let zoneConfig = handleConfig(map: config) {
                if(zoneConfig.isEqual(nil)) {
                    sendError(message: "Error config paramater.", methodName, command.callbackId)
                }
                viewModel.openCloudDBZone2(cloudDB: agcCloudDB, zoneConfig: zoneConfig, isAllowToCreate: isAllowToCreate) { [weak self] (dbZone, error) in
                    guard let strongSelf = self else { return }
                    if let error = error as NSError? {
                        strongSelf.sendError(error: error, methodName, command.callbackId)
                    }
                    if let dbZone =  dbZone as? AGCCloudDBZone {
                        strongSelf.dbZoneList[dbZoneId] = dbZone
                        strongSelf.sendSuccess(methodName, command.callbackId)
                    }
                }
            } else {
                self.sendError(message: "Config could not be initialized.", methodName, command.callbackId)
            }
            break
        case MethodName.openCloudDBZone.rawValue:
            guard let agcCloudDB = agcCloudDB else {
                return sendError(message: cloudDBNullError.description, methodName, command.callbackId)
            }
            guard let config = command.arguments[1] as? Dictionary<String, Any> else {
                sendError(message: "Error config paramater.", methodName, command.callbackId)
                return
            }
            guard let isAllowToCreate = command.arguments[2] as? Bool else {
                sendError(
                    message: "Error isAllowToCreate paramater.",
                    command.methodName, command.callbackId
                )
                return
            }
            guard let dbZoneId = command.arguments[3] as? String else {
                sendError(message: "Error id paramater.", methodName, command.callbackId)
                return
            }
            if let zoneConfig = handleConfig(map: config) {
                
                if(zoneConfig.isEqual(nil)) {
                    sendError(message: "Error config paramater.", methodName, command.callbackId)
                }
                viewModel.openCloudDBZone(cloudDB: agcCloudDB, zoneConfig: zoneConfig, isAllowToCreate: isAllowToCreate) { [weak self] (dbZone, error) in
                    guard let strongSelf = self else { return }
                    if let error = error as NSError? {
                        strongSelf.sendError(error: error, methodName, command.callbackId)
                    }
                    if let dbZone =  dbZone as? AGCCloudDBZone {
                        strongSelf.dbZoneList[dbZoneId] = dbZone
                        strongSelf.sendSuccess(methodName, command.callbackId)
                    }
                }
            }else {
                sendError(message: "Config could not be initialized.", methodName, command.callbackId)
            }
            break
        case MethodName.addEventListener.rawValue:
            guard let agcCloudDB = agcCloudDB else {
                return sendError(message: cloudDBNullError.description, methodName, command.callbackId)
            }
            agcCloudDB.addEventCallback() { result in
                self.commandDelegate.evalJs("javascript:window.runAGCEvent('onUserKeyChange',\(AGCCloudDBEventType.userKeyChanged.rawValue));")
            }
            sendSuccess(methodName, command.callbackId)
            break
        case MethodName.addDataEncryptionKeyListener.rawValue:
            guard let agcCloudDB = agcCloudDB else {
                return sendError(message: cloudDBNullError.description, methodName, command.callbackId)
            }
            guard let needFetchDataEncryptionKey = command.arguments[1] as? Bool else {
                sendError(
                    message: "Error needFetchDataEncryptionKey paramater.", methodName, command.callbackId)
                return
            }
            agcCloudDB.addDataEncryptionKeyCallback { () -> Bool in
                self.commandDelegate.evalJs("javascript:window.runAGCEvent('onDataKeyChange',\(needFetchDataEncryptionKey));")
                return needFetchDataEncryptionKey
            }
            sendSuccess(methodName, command.callbackId)
            break
            
        case MethodName.deleteCloudDBZone.rawValue:
            guard let agcCloudDB = agcCloudDB else {
                return sendError(message: cloudDBNullError.description, methodName, command.callbackId)
            }
            guard let zoneName = command.arguments[1] as? String else {
                sendError(
                    message: "Error zoneName paramater.",
                    command.methodName, command.callbackId
                )
                return
            }
            do {
                try AGCCloudDBObjC.delete(agcCloudDB, zoneName: zoneName)
            } catch {
                return sendError(error: (error as NSError), methodName, command.callbackId)
            }
            self.sendSuccess(methodName, command.callbackId)
            break
        case MethodName.closeCloudDBZone.rawValue:
            guard let agcCloudDB = agcCloudDB else {
                return sendError(message: cloudDBNullError.description, methodName, command.callbackId)
            }
            guard let id = command.arguments[1] as? String else {
                sendError(
                    message: "Error id paramater.",
                    command.methodName, command.callbackId
                )
                return
            }
            let dbZone = getZoneFromList(id: id)
            do {
                try AGCCloudDBObjC.close(agcCloudDB, dbZone: dbZone)
            } catch {
                return sendError(error: (error as NSError), methodName, command.callbackId)
            }
            self.dbZoneList.removeValue(forKey: id)
            sendSuccess(methodName, command.callbackId)
            break
            
        case MethodName.enableNetwork.rawValue:
            guard let agcCloudDB = agcCloudDB else {
                return sendError(message: cloudDBNullError.description, methodName, command.callbackId)
            }
            guard let zoneName = command.arguments[1] as? String else {
                sendError(
                    message: "Error zoneName paramater.", methodName, command.callbackId)
                return
            }
            let result = agcCloudDB.enableNetwork(zoneName)
            if result == true {
                self.sendSuccess(methodName, command.callbackId)
            }else {
                self.sendError(message: "CloudDBZone does not exist.", code: 5, methodName, command.callbackId)
            }
            break
        case MethodName.disableNetwork.rawValue:
            guard let agcCloudDB = agcCloudDB else {
                return sendError(message: cloudDBNullError.description, methodName, command.callbackId)
            }
            guard let zoneName = command.arguments[1] as? String else {
                sendError(message: "Error zoneName paramater.", methodName, command.callbackId)
                return
            }
            let result = agcCloudDB.disableNetwork(zoneName)
            if result == true {
                self.sendSuccess(methodName, command.callbackId)
            }else {
                self.sendError(message: "CloudDBZone does not exist.", code: 5, methodName, command.callbackId)
            }
            break
        case MethodName.updateDataEncryptionKey.rawValue:
            guard let agcCloudDB = agcCloudDB else {
                return sendError(message: cloudDBNullError.description, methodName, command.callbackId)
            }
            viewModel.updateDataEncryptionKey(cloudDB: agcCloudDB) { [weak self] _, error in
                guard let strongSelf = self else { return }
                if let error = error as NSError? {
                    strongSelf.sendError(error: error, methodName, command.callbackId)
                }else {
                    strongSelf.commandDelegate.send(CDVPluginResult (
                        status: CDVCommandStatus_OK,
                        messageAs: true
                    ), callbackId: command.callbackId)
                    
                    AGCCloudDBLog.showInPanel(message: methodName, type: .success)
                }
            }
            break
            
        case MethodName.setUserKey.rawValue:
            guard let agcCloudDB = agcCloudDB else {
                return sendError(message: cloudDBNullError.description, methodName, command.callbackId)
            }
            guard let userKey = command.arguments[1] as? String else {
                sendError(message: "User key must not be null.", code: 1, command.methodName, command.callbackId)
                return
            }
            let userRekey = command.arguments[2] as? String
            viewModel.setUserKey(cloudDB: agcCloudDB, userKey: userKey, userReKey: userRekey) { [weak self] _, error   in
                guard let strongSelf = self else { return }
                if let error = error as NSError? {
                    strongSelf.sendError(error: error, methodName, command.callbackId)
                }else {
                    strongSelf.commandDelegate.send(CDVPluginResult (
                        status: CDVCommandStatus_OK,
                        messageAs: true
                    ), callbackId: command.callbackId)
                    
                    AGCCloudDBLog.showInPanel(message: methodName, type: .success)
                }
            }
            break
        default:
            sendError(message: "Error method name. methodName : ", methodName, command.callbackId)
        }
        
    }
    
    // MARK: - CloudDBZoneService
    @objc(CloudDBZoneService:)
    func CloudDBZoneService(command: CDVInvokedUrlCommand){
        
        guard let methodName = command.arguments[0] as? String else {
            sendError(
                message: "Error AGCCloudDBProviderImpl name paramater. Should be module name: AGCCloudDBProviderImpl",
                command.methodName, command.callbackId
            )
            return
        }
        AGCCloudDBLog.showInPanel(message: methodName, type: .call)
        
        let isSet = setCloudDBZoneServiceMethodVarible(methodName, command)
        if (isSet == false) {
            return
        }
        switch methodName {
        case MethodName.executeUpsert.rawValue:
            var instanceArray = [AGCCloudDBObject]()
            for case let object in cloudDBZoneObjectArray {
                guard let object = object as? Dictionary<String, Any> else {
                    return sendError(message: "Casting error.", methodName, command.callbackId)
                }
                guard let instance = mapToObject(clazz: clazz, map: object) else {
                    return sendError(message: "Upsert object failed", methodName, command.callbackId)
                }
                instanceArray.append(instance)
            }
            viewModel.executeUpsert(dbZone: dbZone, instanceArray: instanceArray) { [weak self] (result, error) in
                guard let strongSelf = self else { return }
                if let error = error as NSError? {
                    strongSelf.sendError(error: error, methodName, command.callbackId)
                }
                if let result = result as? Int {
                    strongSelf.commandDelegate.send(CDVPluginResult (
                        status: CDVCommandStatus_OK,
                        messageAs: result
                    ), callbackId: command.callbackId)
                    AGCCloudDBLog.showInPanel(message: methodName, type: .success)
                }
            }
            break
        case MethodName.executeDelete.rawValue:
            var instanceArray = [AGCCloudDBObject]()
            for case let object in cloudDBZoneObjectArray {
                guard let object = object as? Dictionary<String, Any> else {
                    return sendError(message: "Casting error.", methodName, command.callbackId)
                }
                if let instance = mapToObject(clazz: clazz, map: object) {
                    instanceArray.append(instance)
                }
            }
            viewModel.executeDelete(dbZone: dbZone, instanceArray: instanceArray) { [weak self] (result, error) in
                guard let strongSelf = self else { return }
                if let error = error as NSError? {
                    strongSelf.sendError(error: error, methodName, command.callbackId)
                }
                if let result = result as? Int {
                    strongSelf.commandDelegate.send(CDVPluginResult (
                        status: CDVCommandStatus_OK,
                        messageAs: result
                    ), callbackId: command.callbackId)
                    AGCCloudDBLog.showInPanel(message: methodName, type: .success)
                }
            }
            break
        case MethodName.executeQuery.rawValue:
            viewModel.executeQuery(dbZone: dbZone, query: query, queryPolicy: policy) { [weak self] (snapshot, error) in
                guard let strongSelf = self else { return }
                if let error = error as NSError? {
                    strongSelf.sendError(error: error, methodName, command.callbackId)
                }
                if let snapshot = snapshot as? AGCCloudDBSnapshot {
                    var dictionary = Dictionary<String, Any?>()
                    dictionary["isFromCloud"] = snapshot.isFromCloud()
                    dictionary["hasPendingWrites"] = snapshot.hasPendingWrites()
                    if let snapshotObjects = self?.resolveSnapshot(array: snapshot.snapshotObjects()) {
                        dictionary["snapshotObjects"] = snapshotObjects
                    }
                    if let deletedObjects = self?.resolveSnapshot(array: snapshot.deletedObjects()) {
                        dictionary["deletedObjects"] = deletedObjects
                    }
                    if let upsertedObjects = self?.resolveSnapshot(array: snapshot.upsertedObjects()) {
                        dictionary["upsertedObjects"] = upsertedObjects
                    }
                    strongSelf.commandDelegate.send(CDVPluginResult (
                        status: CDVCommandStatus_OK,
                        messageAs: dictionary as [AnyHashable : Any]
                    ), callbackId: command.callbackId)
                    AGCCloudDBLog.showInPanel(message: methodName, type: .success)
                }
            }
            break
        case MethodName.executeAverageQuery.rawValue:
            viewModel.executeQueryAverage(dbZone: dbZone, query: query, queryPolicy: policy, fieldName: fieldName) { [weak self] (result, error) in
                guard let strongSelf = self else { return }
                if let error = error as NSError? {
                    strongSelf.sendError(error: error, methodName, command.callbackId)
                }
                if let result = result {
                    guard let result = result as? Double else {
                        strongSelf.sendError(message: "Result cast error.", methodName, command.callbackId)
                        return
                    }
                    strongSelf.commandDelegate.send(CDVPluginResult (
                        status: CDVCommandStatus_OK,
                        messageAs: result.description
                    ), callbackId: command.callbackId)
                    AGCCloudDBLog.showInPanel(message: methodName, type: .success)
                }
            }
            break
        case MethodName.executeSumQuery.rawValue:
            viewModel.executeSumQuery(dbZone: dbZone, query: query, queryPolicy: policy, fieldName: fieldName) { [weak self] (result, error) in
                guard let strongSelf = self else { return }
                if let error = error as NSError? {
                    strongSelf.sendError(error: error, methodName, command.callbackId)
                }
                if let result = result {
                    guard let result = result as? NSNumber else {
                        strongSelf.sendError(message: "Result cast error.", methodName, command.callbackId)
                        return
                    }
                    strongSelf.commandDelegate.send(CDVPluginResult (
                        status: CDVCommandStatus_OK,
                        messageAs: result.stringValue
                    ), callbackId: command.callbackId)
                    AGCCloudDBLog.showInPanel(message: methodName, type: .success)
                }
            }
            break
        case MethodName.executeMaximumQuery.rawValue:
            viewModel.executeMaximumQuery(dbZone: dbZone, query: query, queryPolicy: policy, fieldName: fieldName) { [weak self] (result, error) in
                guard let strongSelf = self else { return }
                if let error = error as NSError? {
                    strongSelf.sendError(error: error, methodName, command.callbackId)
                }
                if let result = result {
                    guard let result = result as? NSNumber else {
                        strongSelf.sendError(message: "Result cast error.", methodName, command.callbackId)
                        return
                    }
                    strongSelf.commandDelegate.send(CDVPluginResult (
                        status: CDVCommandStatus_OK,
                        messageAs: result.stringValue
                    ), callbackId: command.callbackId)
                    AGCCloudDBLog.showInPanel(message: methodName, type: .success)
                }
            }
            break
        case MethodName.executeMinimalQuery.rawValue:
            viewModel.executeMinimalQuery(dbZone: dbZone, query: query, queryPolicy: policy, fieldName: fieldName) { [weak self] (result, error) in
                guard let strongSelf = self else { return }
                if let error = error as NSError? {
                    strongSelf.sendError(error: error, methodName, command.callbackId)
                }
                if let result = result {
                    guard let result = result as? NSNumber else {
                        strongSelf.sendError(message: "Result cast error.", methodName, command.callbackId)
                        return
                    }
                    strongSelf.commandDelegate.send(CDVPluginResult (
                        status: CDVCommandStatus_OK,
                        messageAs: result.stringValue
                    ), callbackId: command.callbackId)
                    AGCCloudDBLog.showInPanel(message: methodName, type: .success)
                }
            }
            break
        case MethodName.executeCountQuery.rawValue:
            viewModel.executeCountQuery(dbZone: dbZone, query: query, queryPolicy: policy, fieldName: fieldName) { [weak self] (result, error) in
                guard let strongSelf = self else { return }
                if let error = error as NSError? {
                    strongSelf.sendError(error: error, methodName, command.callbackId)
                }
                if let result = result {
                    guard let result = result as? NSInteger else {
                        strongSelf.sendError(message: "Result cast error.", methodName, command.callbackId)
                        return
                    }
                    strongSelf.commandDelegate.send(CDVPluginResult (
                        status: CDVCommandStatus_OK,
                        messageAs: result.description
                    ), callbackId: command.callbackId)
                    AGCCloudDBLog.showInPanel(message: methodName, type: .success)
                }
            }
            break
        case MethodName.executeQueryUnsynced.rawValue:
            viewModel.executeQueryUnsynced(dbZone: dbZone, query: query) { [weak self] (snapshot, error) in
                guard let strongSelf = self else { return }
                if let error = error as NSError? {
                    strongSelf.sendError(error: error, methodName, command.callbackId)
                }
                if let snapshot = snapshot as? AGCCloudDBSnapshot {
                    var dictionary = Dictionary<String, Any?>()
                    dictionary["isFromCloud"] = snapshot.isFromCloud()
                    dictionary["hasPendingWrites"] = snapshot.hasPendingWrites()
                    if let snapshotObjects = strongSelf.resolveSnapshot(array: snapshot.snapshotObjects()) {
                        dictionary["snapshotObjects"] = snapshotObjects
                    }
                    if let deletedObjects = strongSelf.resolveSnapshot(array: snapshot.deletedObjects()) {
                        dictionary["deletedObjects"] = deletedObjects
                    }
                    if let upsertedObjects = strongSelf.resolveSnapshot(array: snapshot.upsertedObjects()) {
                        dictionary["upsertedObjects"] = upsertedObjects
                    }
                    strongSelf.commandDelegate.send(CDVPluginResult (
                        status: CDVCommandStatus_OK,
                        messageAs: dictionary as [AnyHashable : Any]
                    ), callbackId: command.callbackId)
                    AGCCloudDBLog.showInPanel(message: methodName, type: .success)
                }
            }
            break
        case MethodName.runTransaction.rawValue:
            var result = [String: Any]()
            var errorPointer: NSError? = nil
            dbZone.runTransaction { [self] (function) -> Bool in
                guard let function = function else { return false }
                for case let transactions in transactionsArray {
                    guard let transactions = transactions as? Dictionary<String, Any> else { return false }
                    var instanceArray = [AGCCloudDBObject]()
                    guard let operation = transactions["operation"] as? String else { return false }
                    guard let className = transactions["className"] as? String else { return false }
                    
                    if(operation == "upsert" || operation == "delete") {
                        guard let array = transactions["data"] as? [Any] else { return false }
                        guard let clazz = classFromString(className: className) else {
                            sendError(message: cloudDBClassError.description, methodName, command.callbackId)
                            return false
                        }
                        for case let object in array {
                            guard let object = object as? Dictionary<String, Any> else { return false }
                            guard let instance = mapToObject(clazz: clazz, map: object) else {
                                return false
                            }
                            instanceArray.append(instance)
                        }
                    }
                    switch operation {
                    case "upsert":
                        do {
                            try AGCCloudDBObjC.catchException {
                                function.executeUpsert(instanceArray)
                            }
                        }
                        catch {
                            return false
                        }
                        break
                    case "delete":
                        do {
                            try AGCCloudDBObjC.catchException {
                                function.executeDelete(instanceArray)
                            }
                        }
                        catch {
                            return false
                        }
                        break
                    case "query":
                        guard let queryObject = transactions["data"] as? Dictionary<String, Any> else { return false }
                        guard let query = queryBuilder(map: queryObject, command.callbackId) as AGCCloudDBQuery? else { return false }
                        var queryResultArray: [Any] = []
                        do {
                            try AGCCloudDBObjC.catchException {
                                queryResultArray = function.execute(query, error: &errorPointer)
                            }
                        }
                        catch {
                            return false
                        }
                        guard let queryResult = self.resolveSnapshot(array: queryResultArray) else { return false }
                        result["queryResult"] = queryResult
                        break
                    default:
                        break
                    }
                }
                return true
            } onCompleted: { (error) in
                if error != nil {
                    if let error = error as NSError? {
                        self.sendError(error: error, methodName, command.callbackId)
                    }
                    return
                }else {
                    result["isSuccessful"] = true
                    self.commandDelegate.send(CDVPluginResult (
                        status: CDVCommandStatus_OK,
                        messageAs: result
                    ), callbackId: command.callbackId)
                }
                AGCCloudDBLog.showInPanel(message: methodName, type: .success)
            }
            break
        case MethodName.subscribeSnapshot.rawValue:
            let listenerHandler = viewModel.subscribeSnapshot(dbZone: dbZone, query: query, queryPolicy: policy) { [weak self] (snapshot, error) in
                guard let id = command.arguments[3] as? String else {
                    AGCCloudDBLog.showInPanel(message: "subscribeSnapshot:listener", type: .fail)
                    return
                }
                guard let listenerId = command.arguments[4] as? String else {
                    AGCCloudDBLog.showInPanel(message: "subscribeSnapshot:listener", type: .fail)
                    return
                }
                
                guard let strongSelf = self else { return }
                var dictionary = Dictionary<String, Any?>()
                dictionary["id"] = listenerId
                let eventName = "onSnapshotUpdate\(id)\(listenerId)"
                if let error = error as NSError? {
                    var errorData = Dictionary<String, Any?>()
                    errorData["code"] = error.code
                    errorData["message"] = error.userInfo["NSLocalizedDescription"]
                    dictionary["error"] = errorData
                    do {
                        let jsonErrorData = try JSONSerialization.data(withJSONObject: (dictionary), options: JSONSerialization.WritingOptions.prettyPrinted)
                        let jsonErrorString = NSString(data: jsonErrorData, encoding: String.Encoding.utf8.rawValue)! as String
                        strongSelf.commandDelegate.evalJs("javascript:window.runAGCEvent('\(eventName)', \(jsonErrorString));")
                    } catch {
                        strongSelf.commandDelegate.evalJs("javascript:window.runAGCEvent('\(eventName)', {'error': 'JSONSerialization'});")
                    }
                }
                if let snapshot = snapshot as? AGCCloudDBSnapshot {
                    var snapshotDictionary = Dictionary<String, Any?>()
                    snapshotDictionary["isFromCloud"] = snapshot.isFromCloud()
                    snapshotDictionary["hasPendingWrites"] = snapshot.hasPendingWrites()
                    if let snapshotObjects = strongSelf.resolveSnapshot(array: snapshot.snapshotObjects()) {
                        snapshotDictionary["snapshotObjects"] = snapshotObjects
                    }
                    if let deletedObjects = strongSelf.resolveSnapshot(array: snapshot.deletedObjects()) {
                        snapshotDictionary["deletedObjects"] = deletedObjects
                    }
                    if let upsertedObjects = strongSelf.resolveSnapshot(array: snapshot.upsertedObjects()) {
                        snapshotDictionary["upsertedObjects"] = upsertedObjects
                    }
                    dictionary["data"] = snapshotDictionary
                    do {
                        let jsonData = try JSONSerialization.data(withJSONObject: (dictionary), options: JSONSerialization.WritingOptions.prettyPrinted)
                        let jsonString = NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue)! as String
                        strongSelf.commandDelegate.evalJs("javascript:window.runAGCEvent('\(eventName)', \(jsonString));")
                    } catch {
                        strongSelf.commandDelegate.evalJs("javascript:window.runAGCEvent('\(eventName)', {'error': 'JSONSerialization'});")
                    }
                }
            }
            if let listenerHandler = listenerHandler as AGCCloudDBListenerHandler? {
                if let zoneListeners = self.listenerHandlerList[id] {
                    zoneListeners[listenerId] = listenerHandler
                    self.listenerHandlerList[id] = zoneListeners
                } else {
                    let listenerDic: ConcurrentHashMap<String, AGCCloudDBListenerHandler> = ConcurrentHashMap.init(dict: [listenerId: listenerHandler])
                    self.listenerHandlerList[id] = listenerDic
                }
                self.sendSuccess(methodName, command.callbackId)
            } else {
                return self.sendError(message: "subscribeSnapshot failed.", methodName, command.callbackId)
            }
            break
        case MethodName.unsubscribeSnapshot.rawValue:
            if let zoneListeners = listenerHandlerList[id] {
                if let listenerHandler = zoneListeners[listenerId] {
                    listenerHandler.remove()
                    zoneListeners.removeValue(forKey: listenerId)
                    if zoneListeners.isEmpty {
                        listenerHandlerList.removeValue(forKey: id)
                    } else {
                        listenerHandlerList[id] = zoneListeners
                    }
                    self.sendSuccess(methodName, command.callbackId)
                } else {
                    return sendError(message: "Subscription could not be found.", methodName, command.callbackId)
                }
            } else {
                return sendError(message: "Subscription could not be found.", methodName, command.callbackId)
            }
            break
        default:
            sendError(message: "Error method name. methodName : ", methodName, command.callbackId)
        }
    }
    
    // MARK: Private Helper Functions
    
    private func setCloudDBZoneServiceMethodVarible(_ methodName: String, _ command: CDVInvokedUrlCommand) -> Bool {
        if (methodName == MethodName.executeUpsert.rawValue ||
                methodName == MethodName.executeDelete.rawValue) {
            guard let className = command.arguments[1] as? String else {
                sendError(message: "Error className paramater.", command.methodName, command.callbackId)
                return false
            }
            self.className = className
            
            guard let cloudDBZoneObjectArray = command.arguments[2] as? [Any] else {
                sendError(message: "Error cloudDBZoneObjectArray paramater.", command.methodName, command.callbackId)
                return false
            }
            self.cloudDBZoneObjectArray = cloudDBZoneObjectArray
            
            guard let id = command.arguments[3] as? String else {
                sendError( message: "Error id paramater.", command.methodName, command.callbackId)
                return false
            }
            self.id = id
            
            guard let dbZone = getZoneFromList(id: id) else {
                sendError(message: dbZoneNullError.description, code: 9, methodName, command.callbackId)
                return false
            }
            self.dbZone = dbZone
            guard let clazz = classFromString(className: className) else {
                sendError(message: cloudDBClassError.description, methodName, command.callbackId)
                return false
            }
            self.clazz = clazz
        } else if (methodName == MethodName.executeQuery.rawValue) {
            guard let queryObject = command.arguments[1] as? Dictionary<String, Any> else {
                sendError(message: "Error queryObject paramater.", command.methodName, command.callbackId)
                return false
            }
            self.queryObject = queryObject
            
            guard let queryPolicy = command.arguments[2] as? Int else {
                sendError(message: "Error queryPolicy paramater.", command.methodName, command.callbackId)
                return false
            }
            self.queryPolicy = queryPolicy
            
            guard let id = command.arguments[3] as? String else {
                sendError(message: "Error id paramater.", command.methodName, command.callbackId)
                return false
            }
            self.id = id
            
            guard let dbZone = getZoneFromList(id: id) else {
                sendError(message: dbZoneNullError.description, code: 9, methodName, command.callbackId)
                return false
            }
            self.dbZone = dbZone
            guard let query = queryBuilder(map: queryObject, command.callbackId) as AGCCloudDBQuery? else { return false}
            self.query = query
            guard let policy = getQueryPolicy(policyValue: queryPolicy, command.callbackId) else { return false}
            self.policy = policy
        }  else if (methodName == MethodName.subscribeSnapshot.rawValue) {
            guard let queryObject = command.arguments[1] as? Dictionary<String, Any> else {
                sendError(message: "Error queryObject paramater.", command.methodName, command.callbackId)
                return false
            }
            self.queryObject = queryObject
            
            guard let queryPolicy = command.arguments[2] as? Int else {
                sendError(message: "Error queryPolicy paramater.", command.methodName, command.callbackId)
                return false
            }
            self.queryPolicy = queryPolicy
            
            guard let id = command.arguments[3] as? String else {
                sendError(message: "Error id paramater.", command.methodName, command.callbackId)
                return false
            }
            self.id = id
            
            guard let listenerId = command.arguments[4] as? String else {
                sendError(message: "Error listenerId paramater.", command.methodName, command.callbackId)
                return false
            }
            self.listenerId = listenerId
            guard let dbZone = getZoneFromList(id: id) else {
                sendError(message: dbZoneNullError.description, code: 9, methodName, command.callbackId)
                return false
            }
            self.dbZone = dbZone
            guard let query = queryBuilder(map: queryObject, command.callbackId) as AGCCloudDBQuery? else { return false}
            self.query = query
            guard let policy = getQueryPolicy(policyValue: queryPolicy, command.callbackId) else { return false}
            self.policy = policy
        } else if (methodName == MethodName.executeAverageQuery.rawValue ||
                    methodName == MethodName.executeSumQuery.rawValue ||
                    methodName == MethodName.executeMaximumQuery.rawValue ||
                    methodName == MethodName.executeMinimalQuery.rawValue ||
                    methodName == MethodName.executeCountQuery.rawValue) {
            guard let queryObject = command.arguments[1] as? Dictionary<String, Any> else {
                sendError(message: "Error queryObject paramater.",command.methodName, command.callbackId)
                return false
            }
            self.queryObject = queryObject
            
            guard let fieldName = command.arguments[2] as? String else {
                sendError(message: "Error fieldName paramater.", command.methodName, command.callbackId)
                return false
            }
            self.fieldName = fieldName
            
            guard let queryPolicy = command.arguments[3] as? Int else {
                sendError(message: "Error queryPolicy paramater.", command.methodName, command.callbackId)
                return false
            }
            self.queryPolicy = queryPolicy
            
            guard let id = command.arguments[4] as? String else {
                sendError(message: "Error id paramater.", command.methodName, command.callbackId)
                return false
            }
            self.id = id
            
            guard let dbZone = getZoneFromList(id: id) else {
                sendError(message: dbZoneNullError.description, code: 9, methodName, command.callbackId)
                return false
            }
            self.dbZone = dbZone
            guard let query = queryBuilder(map: queryObject, command.callbackId) as AGCCloudDBQuery? else { return false }
            self.query = query
            guard let policy = getQueryPolicy(policyValue: queryPolicy, command.callbackId) else { return false }
            self.policy = policy
        } else if (methodName == MethodName.runTransaction.rawValue) {
            guard let id = command.arguments[2] as? String else {
                sendError(message: "Error id paramater.", command.methodName, command.callbackId )
                return false
            }
            self.id = id
            
            guard let dbZone = getZoneFromList(id: id) else {
                sendError(message: dbZoneNullError.description, code: 9, methodName, command.callbackId)
                return false
            }
            self.dbZone = dbZone
            
            guard let transactionsArray = command.arguments[1] as? [Any] else {
                sendError(message: "Error transactionsArray paramater.", command.methodName, command.callbackId)
                return false
            }
            self.transactionsArray = transactionsArray
            
        } else if (methodName == MethodName.executeQueryUnsynced.rawValue) {
            guard let queryObject = command.arguments[1] as? Dictionary<String, Any> else {
                sendError(message: "Error queryObject paramater.", command.methodName, command.callbackId)
                return false
            }
            self.queryObject = queryObject
            
            guard let id = command.arguments[2] as? String else {
                sendError(message: "Error id paramater.", command.methodName, command.callbackId)
                return false
            }
            self.id = id
            
            guard let dbZone = getZoneFromList(id: id) else {
                sendError(message: dbZoneNullError.description, code: 9, methodName, command.callbackId)
                return false
            }
            self.dbZone = dbZone
            
            guard let query = queryBuilder(map: queryObject, command.callbackId) as AGCCloudDBQuery? else { return false}
            self.query = query
            
        } else if (methodName == MethodName.unsubscribeSnapshot.rawValue) {
            guard let id = command.arguments[1] as? String else {
                sendError(message: "Error id paramater.", command.methodName, command.callbackId)
                return false
            }
            self.id = id
            
            guard let listenerId = command.arguments[2] as? String else {
                sendError(message: "Error listenerId paramater.", command.methodName, command.callbackId)
                return false
            }
            self.listenerId = listenerId
        }
        return true
    }
    
    private func sendError(error: NSError, _ methodName: String, _ callbackId: String){
        var errorDict = [String: Any]()
        errorDict["code"] = error.code
        errorDict["message"] = error.userInfo["NSLocalizedDescription"] ?? error.domain
        self.commandDelegate.send(CDVPluginResult (
            status: CDVCommandStatus_ERROR,
            messageAs: errorDict
        ), callbackId: callbackId)
        AGCCloudDBLog.showInPanel(message: methodName, type: .fail)
    }
    private func sendError(message: String, code: Int = 1, _ methodName: String, _ callbackId: String){
        self.commandDelegate.send(CDVPluginResult (
            status: CDVCommandStatus_ERROR,
            messageAs: ["message": message, "code": code]
        ), callbackId: callbackId)
        AGCCloudDBLog.showInPanel(message: methodName, type: .fail)
    }
    
    private func sendSuccess(_ methodName: String, _ callbackId: String){
        self.commandDelegate.send(CDVPluginResult (
            status: CDVCommandStatus_OK
        ), callbackId: callbackId)
        AGCCloudDBLog.showInPanel(message: methodName, type: .success)
    }
    
    private func getZoneFromList(id: String) -> AGCCloudDBZone? {
        return self.dbZoneList[id]
    }
    
    private func mapToObject(clazz: AGCCloudDBObject.Type, map: Dictionary<String, Any>) -> AGCCloudDBObject? {
        let instance = clazz.init()
        let types = getPropsWithTypes(object: clazz)
        for case let (key,value) in map {
            if ((types[key]?.isEmpty) == nil) {
                break
            }
            let value = setTypeOfValue(value: value, fieldName: key, types: types)
            instance.setValue(value, forKey: key)
        }
        return instance
    }
    
    private func classFromString(className: String) -> AGCCloudDBObject.Type? {
        let cls = NSClassFromString(className) as? AGCCloudDBObject.Type
        return cls
        
    }
    
    private func resolveSnapshot(array: [Any]) -> [Dictionary<String, Any?>]? {
        var dicArray: [Dictionary<String, Any?>] = []
        for element in array {
            guard let object = element as? AGCCloudDBObject else { return nil }
            let dic = self.convertObjectToDic(object: object)
            dicArray.append(dic)
        }
        return dicArray
    }
    
    private func convertObjectToDic(object: AGCCloudDBObject) -> [String: Any?] {
        var dic: [String: Any?] = [:]
        let types = getPropsWithTypes(object: object.classForCoder)
        for type in types {
            if type.value == "AGCCloudDBText" {
                if let cloudDBText = object.value(forKey: type.key) as? AGCCloudDBText {
                    dic[type.key] = cloudDBText.text
                }
            }else if type.value == "NSDate" {
                if let cloudDBDate = object.value(forKey: type.key) as? NSDate {
                    dic[type.key] = (UInt64((cloudDBDate.timeIntervalSince1970 * 1000).rounded())).description
                }
            }else if type.value == "NSData" {
                if let cloudDBData = object.value(forKey: type.key) as? NSData {
                    var array: [Double] = []
                    for byte in cloudDBData {
                        array.append(Double(byte))
                    }
                    dic[type.key] = array
                }
            }else if type.value == "NSNumber<AGCLong>" {
                if let numberValue = object.value(forKey: type.key) as? NSNumber {
                    dic[type.key] = numberValue.description
                }
            }else if type.value == "NSNumber<AGCFloat>" {
                if let numberValue = object.value(forKey: type.key) as? Float {
                    dic[type.key] = numberValue.description
                }
            }else if type.value == "NSNumber<AGCDouble>" {
                if let numberValue = object.value(forKey: type.key) as? Double {
                    dic[type.key] = numberValue.description
                }
            }else {
                dic[type.key] = object.value(forKey: type.key)
            }
        }
        return dic
    }
    
    private func setTypeOfValue(value: Any, fieldName: String, types: Dictionary<String, String>) -> Any {
        if types[fieldName] == "AGCCloudDBText" {
            if let strValue = value as? String {
                let text = AGCCloudDBText.createText(strValue)
                return text
            }
        }else if types[fieldName] == "NSDate" {
            if let timeInterval = value as? Int {
                let date = Date(timeIntervalSince1970: TimeInterval(timeInterval) / 1000)
                return date
            }
        }else if types[fieldName] == "NSData" {
            if let str = value as? [UInt8] {
                let nsData = NSData(bytes: str, length: str.count)
                return nsData
            }
        }else if types[fieldName] == "NSNumber<AGCLong>" {
            if let str = value as? String {
                if let number = Int(str) {
                    return NSNumber(value: number)
                }
            }
        }
        return value
    }
    
    private func getPropNames (object: AnyClass) -> [String] {
        var outCount : UInt32 = 0
        let properties = class_copyPropertyList(object.self, &outCount)
        var propertiesArray: [String] = []
        for i : UInt32 in 0..<outCount
        {
            let strKey : NSString = NSString(cString: property_getName(properties![Int(i)]), encoding: String.Encoding.utf8.rawValue)!
            propertiesArray.append(strKey as String)
        }
        return propertiesArray
    }
    
    private func getPropsWithTypes (object: AnyClass) -> Dictionary<String, String> {
        var outCount : UInt32 = 0
        let properties = class_copyPropertyList(object.self, &outCount)
        var propertiesArray: Dictionary<String, String> = [:]
        for i : UInt32 in 0..<outCount
        {
            let strKey : NSString = NSString(cString: property_getName(properties![Int(i)]), encoding: String.Encoding.utf8.rawValue)!
            let typeDef: String = NSString(cString: property_getAttributes(properties![Int(i)])!, encoding: String.Encoding.utf8.rawValue)! as String
            let typeName = typeDef.split(separator: "\"")
            propertiesArray[strKey as String] = String(typeName[1])
        }
        return propertiesArray
    }
    
    private func getQueryPolicy(policyValue: Int, _ callbackId: String) -> AGCCloudDBQueryPolicy? {
        if policyValue == AGCCloudDBQueryPolicy.default.rawValue {
            return AGCCloudDBQueryPolicy.default
        }else if policyValue == AGCCloudDBQueryPolicy.cloud.rawValue {
            return AGCCloudDBQueryPolicy.cloud
        }else if policyValue == AGCCloudDBQueryPolicy.local.rawValue {
            return AGCCloudDBQueryPolicy.local
        }else {
            let error = NSError(domain: "Query from the local or cloud database failed.", code: 5, userInfo: nil)
            self.sendError(error: error, "queryBuilder", callbackId)
            return nil
        }
    }
    private func queryBuilder(map: Dictionary<String, Any>, _ callbackId: String ) -> AGCCloudDBQuery? {
        guard let className = map["className"] as? String else {
            self.sendError(message: "AGCCloudDBQuery object failure", "queryBuilder", callbackId)
            return nil
        }
        guard let clazz = classFromString(className: className) else {
            self.sendError(message: cloudDBClassError.description, "queryBuilder", callbackId)
            return nil
        }
        let types = getPropsWithTypes(object: clazz)
        let query = AGCCloudDBQuery.where(clazz)
        guard let queryElements = map["queryElements"] as? [Dictionary<String, Any>] else {
            self.sendError(message: "AGCCloudDBQuery object failure", "queryBuilder", callbackId)
            return nil
        }
        do {
            try AGCCloudDBObjC.catchException {
                self.makeQuery(queryElements: queryElements, query: query, types: types, clazz: clazz, className: className)
            }
        }
        catch {
            if let error = error as NSError? {
                sendError(error: error, "queryBuilder", callbackId)
                return nil
            }
        }
        return query
    }
    
    private func makeQuery(queryElements: [Dictionary<String, Any>], query: AGCCloudDBQuery, types: Dictionary<String, String>, clazz: AGCCloudDBObject.Type, className: String) {
        for case let queryElement in queryElements {
            guard let operation = queryElement["operation"] as? String else { break }
            if let fieldName = queryElement["fieldName"] as? String {
                if let value = queryElement["value"] {
                    let value = setTypeOfValue(value: value, fieldName: fieldName, types: types)
                    self.queryAdd(value: value, fieldName: fieldName, operation: operation, query: query, types: types)
                } else {
                    queryAdd(onlyFieldName: fieldName, operation: operation, query: query)
                }
            } else {
                if operation == "limit" {
                    if let value = queryElement["value"] as? Double {
                        if let offset = queryElement["offset"] as? Double {
                            query.limit(Int32.init(value), offset: Int32.init(offset))
                        } else {
                            query.limit(Int32.init(value))
                        }
                    }
                }
                if let value = queryElement["value"] {
                    queryAdd(onlyValue: value, operation: operation, query: query, clazz: clazz)
                }
            }
        }
    }
    
    
    private func queryAdd(onlyFieldName fieldName: String, operation: String, query: AGCCloudDBQuery) {
        switch operation {
        case "isNull":
            query.isNull(fieldName)
        case "isNotNull":
            query.isNotNull(fieldName)
        case "orderByAsc":
            query.order(byAsc: fieldName)
        case "orderByDesc":
            query.order(byDesc: fieldName)
        default:
            break
        }
    }
    
    private func queryAdd(onlyValue value: Any, operation: String, query: AGCCloudDBQuery, clazz: AGCCloudDBObject.Type) {
        guard let value = value as? Dictionary<String, Any> else { return }
        guard let object = mapToObject(clazz: clazz, map: value) as AGCCloudDBObject? else { return }
        switch operation {
        case "startAt":
            query.start(at: object)
        case "startAfter":
            query.start(after: object)
        case "endAt":
            query.end(at: object)
        case "endBefore":
            query.end(before: object)
        default:
            break
        }
    }
    
    private func queryAdd(value: Any, fieldName: String, operation: String, query: AGCCloudDBQuery, types: Dictionary<String, String>){
        switch operation {
        case "equalTo":
            query.equal(to: value, forField: fieldName)
        case "notEqualTo":
            query.notEqual(to: value, forField: fieldName)
        case "greaterThan":
            query.greaterThan(value, forField: fieldName)
        case "greaterThanOrEqualTo":
            query.greaterThanOrEqual(to: value, forField: fieldName)
        case "lessThan":
            query.lessThan(value, forField: fieldName)
        case "lessThanOrEqualTo":
            query.lessThanOrEqual(to: value, forField: fieldName)
        case "in":
            if let value = handleArrayTypes(value: value, fieldName: fieldName, query: query, types: types) {
                query.inArray(value, forField: fieldName)
            }
        case "beginsWith":
            query.begins(with: value, forField: fieldName)
        case "endsWith":
            query.ends(with: value, forField: fieldName)
        case "contains":
            query.contains(value, forField: fieldName)
        default:
            break
        }
    }
    
    private func handleArrayTypes(value: Any, fieldName: String, query: AGCCloudDBQuery, types: Dictionary<String, String>) -> [Any]?{
        if let array = value as? [Any] {
            var typedValues: [Any] = []
            for item in array {
                let typedValue = setTypeOfValue(value: item, fieldName: fieldName, types: types)
                typedValues.append(typedValue)
            }
            return typedValues
        }
        return nil
    }
    
    private func handleConfig(map: Dictionary<String, Any>) -> AGCCloudDBZoneConfig? {
        guard let cloudDBZoneName = map["cloudDBZoneName"] as? String else { return nil }
        guard let syncRaw = map["syncProperty"] as? Int else { return nil }
        guard let syncProperty = AGCCloudDBZoneSyncMode.init(rawValue: syncRaw) else { return nil }
        guard let accessRaw = map["accessProperty"] as? Int else { return nil }
        guard let accessProperty = AGCCloudDBZoneAccessMode.init(rawValue: accessRaw) else { return nil }
        let zoneConfig = AGCCloudDBZoneConfig.init(zoneName: cloudDBZoneName, syncMode: syncProperty, accessMode: accessProperty)
        
        if let persistence = map["persistenceEnabled"] as? Bool {
            zoneConfig.persistence = persistence
        }
        if(zoneConfig.persistence) {
            if let capacity = map["capacity"] as? Int {
                zoneConfig.capacity = capacity
            }
        }
        let isEncrypted = map["isEncrypted"] as? Bool ?? false
        if (isEncrypted == true) {
            let key = map["key"] as? String
            let reKey = map["reKey"] as? String
            if ((!(key==nil)) && reKey==nil){
                zoneConfig.setEncryptKey(key, rekey: nil)
            } else if (key==nil && (!(reKey==nil))) {
                zoneConfig.setEncryptKey(nil, rekey: reKey)
            } else if (!(key==nil && reKey==nil)) {
                zoneConfig.setEncryptKey(key, rekey: reKey)
            }
        }
        return zoneConfig
    }
}
