/*
 *  Copyright © 2016, 2017, Oracle and/or its affiliates. All rights reserved.
 *  This software is dual-licensed to you under the MIT License (MIT) and
 *  the Universal Permissive License (UPL). See the LICENSE file in the root
 *  directory for license terms. You may choose either license, or both.
 */
import Foundation
import DeviceLib

/**
 * This sample is a gateway that presents multiple simple sensors as virtual
 * devices to the IoT server. The simple sensors are polled based on
 * the `updateInterval` passed to the `GatewayDeviceSample` constructor from
 * the application and the virtual devices are updated.
 *
 * The sample runs in two modes: with policies or without policies. By
 * default, the sample runs without policies. To enable the use of
 * policies, set the value of `USE_POLICY` in shared/Globals.swift.
 * If provisioning the device using File or Network provisioing, then a
 * 'switch' will be displayed allowing the user to chooe the mode of operation.
 *
 * When running with policies, the device policies must be uploaded to
 * the server. The sample reads the humidity and temperature values
 * from the sensors and calls the `VirtualDevice.offer` method,
 * which applies the policies. Data and alert messages are sent
 * to the server depending on the configuration of the device policies.
 *
 * When running without policies, the sample reads the sensor data and
 * calls the `VirtualDevice.set` method, which results in a
 * data message being sent to the server. The sample itself must
 * generate alerts if the sensor values exceed any thresholds defined
 * by a sensor's data model.
 *
 * Devices managed by the gateway can be created to be restricted to this 
 * gateway or not, meaning they are not or are allowed to "roam" and be
 * managed by other gateways. This behavior is controlled by the variable
 * `RESTRICTED_ICD` declared in `shared/Globals.swift`. The default value is
 * `true`, indirectly connected devices are not allowed to "roam".
 */

/*
 * A Tag type for a sensor's state
 */
protocol SensorState {
}

/**
 * State variables for a TemperatureSensor
 */
class TemperatureSensorState : SensorState {
    var wasOff:Bool = false
    var prevMinTemp:Double = 0.0
    var prevMaxTemp:Double = 0.0
    var prevMinThreshold:Int = 0
    var prevMaxThreshold:Int = 0
    var alerted:Bool = false
    var tooColdAlert:Alert?
    var tooHotAlert:Alert?
}

/**
 * State variables for a HumiditySensor
 */
class HumiditySensorState : SensorState {
    var alerted:Bool = false
    var prevMaxThreshold:Int = 0
    var alert:Alert?
}

/**
 * The device class aggregates state and variables for a sensor represented
 * by a virtual device.
 */
class Device {
    let deviceModel:String
    let restricted:Bool
    let sensor:AnyObject
    let metaData:[String:String]
    let hardwareId:String
    let state:SensorState
    let update:(Device)->()
    
    // The registered icd endpoint id
    var endpointId:String?
    var virtualDevice:VirtualDevice?
    
    init(sensor:AnyObject, hardwareId:String,
         deviceModel:String, metaData:[String:String],
         restricted:Bool,
         state:SensorState,
         update: @escaping (Device)->()) {

        self.sensor = sensor
        self.hardwareId = hardwareId
        self.deviceModel = deviceModel
        self.metaData = metaData
        self.restricted = restricted
        self.state = state
        self.update = update
    }
}

/* The device models managed in this sample */
let HUMIDITY_SENSOR_MODEL_URN:String =
    "urn:com:oracle:iot:device:humidity_sensor"
let TEMPERATURE_SENSOR_MODEL_URN:String =
    "urn:com:oracle:iot:device:temperature_sensor"

/* The device model attributes */
let HUMIDITY_ATTRIBUTE = "humidity"
let TEMPERATURE_ATTRIBUTE = "temp"
let MAX_THRESHOLD_ATTRIBUTE = "maxThreshold"
let MIN_THRESHOLD_ATTRIBUTE = "minThreshold"
let UNIT_ATTRIBUTE = "unit"
let START_TIME_ATTRIBUTE = "startTime"
let MIN_TEMP_ATTRIBUTE = "minTemp"
let MAX_TEMP_ATTRIBUTE = "maxTemp"

/* The alert format urns */
let TOO_HUMID_ALERT = HUMIDITY_SENSOR_MODEL_URN + ":too_humid"
let TOO_COLD_ALERT = TEMPERATURE_SENSOR_MODEL_URN + ":too_cold"
let TOO_HOT_ALERT = TEMPERATURE_SENSOR_MODEL_URN + ":too_hot"

/**
 * The `GatewayDeviceSample` class implements the gateway behavior for managing
 * `HUMIDITY_SENSOR_MODEL_URN` and `TEMPERATURE_SENSOR_MODE_URN` devices. It 
 * manages one humidity and one temperature sensor device. It can manage more
 * than one device each, but the View for the sample renders data for only one
 * humidity and one temperature sensor.
 *
 * The superclass `GatewayDeviceSampleBase` implements the behavior all gateways
 * must implement to be a gateway to IoT server. It acvitivates the gateway,
 * registers the indirectly connected devices (the sensors), and periodically
 * reads the sensors and updates the virtual devices.
 *
 * Note that there are some assumptons about how the UI and the View code
 * render data. The `GatewayDeviceSample` class knows that the
 * endpoint id of the device is displayed with the current device data. The
 * endpoint id is not included with the displayed information of each attribute
 * update or alert or onChange notifiation.
 */
public class GatewayDeviceSample : GatewayDeviceSampleBase {

    /* The callbacks implemented by the View to render data and runtime
     * information produced by the gateway.
     */
    var updateCallback:([String:Any])->()?
    var displayCallback:(String)->()?
    
    
    /**
     * This sample can be used with policies, or without policies. By default,
     * the sample does not use policies. Set the property
     * `USE_POLICY` declared in `shared/Globals.swift` to true to use policies.
     */
    let usePolicy:Bool!
    
    /**
     * The GatewayDeviceSample constructor
     */
    public init(path:String, password:String, updateInterval:Int,
                usePolicy:Bool,
                updateCallback: @escaping([String:Any])->(),
                errorCallback: @escaping (String)->(),
                displayCallback: @escaping (String)->()) throws {
        
        self.updateCallback = updateCallback
        self.displayCallback = displayCallback
        self.usePolicy = usePolicy

        try super.init(path:path, password:password,
            deviceModels: [
                TEMPERATURE_SENSOR_MODEL_URN, HUMIDITY_SENSOR_MODEL_URN
            ],
            updateInterval: updateInterval,
            errorCallback: errorCallback)
    }
    
    //
    // When running the sample with policies, the sample must do more work.
    // The sample has to update minTemp and maxTemp, and generate alerts if
    // the sensor values exceed, or fall below, thresholds. This logic can
    // be handled by the client library with the right set of policies in
    // place. Compare this method to that of the WithPolicies inner class.
    //
    private func processHumiditySensorDataWithPolicies(device:Device) {
        guard let virtualDevice:VirtualDevice = device.virtualDevice else {
            return
        }
        let sensor:HumiditySensor = device.sensor as! HumiditySensor
        
        // No need for endpoint ID
        let ATTR_FORMAT = "%@ : Offer : \"%@\"=%@"
        let humidity:Int = sensor.getHumidity()
        let str = String(format:ATTR_FORMAT,
                         formatDate(date:Date()),
                         HUMIDITY_ATTRIBUTE,
                         humidity.description)

        do {
            _ = try virtualDevice.update().offer(attributeName: HUMIDITY_ATTRIBUTE,
                                                 attributeValue: humidity as AnyObject)
            displayCallback(str)
            updateCallback(getHumidityData(device:device))
            try virtualDevice.finish()
        } catch {
            errorCallback("\(error)")
        }
    }
    
    private func processHumiditySensorDataWithoutPolicies(device:Device) {
        
        guard let virtualDevice:VirtualDevice = device.virtualDevice else {
            return
        }
        let humiditySensor:HumiditySensor = device.sensor as! HumiditySensor
        let state:HumiditySensorState = device.state as! HumiditySensorState
        let ALERT_FORMAT = "%@ : Alert : \"%@\"=%@"
        let ATTR_FORMAT = "%@ : Set : \"%@\"=%@"
        
        do {
            let humidity = humiditySensor.getHumidity()
            var str = String(format:ATTR_FORMAT,
                             formatDate(date:Date()),
                             HUMIDITY_ATTRIBUTE, humidity.description)
            
            // Set the virtualilzed humidity sensor value in an 'update'. If
            // the humidity maxThreshold has changed, that maxThreshold attribute
            // will be added to the update (see the block below). A call to
            // 'finish' will commit the update.
            let _ = try virtualDevice.update().set(
                attributeName: HUMIDITY_ATTRIBUTE,
                attributeValue: humidity as AnyObject)
            displayCallback(str)
            try virtualDevice.finish()
            let humidityThreshold = humiditySensor.getMaxThreshold()
            if humidity > humidityThreshold {
                if !state.alerted {
                    state.alerted = true
                    str = String(format:ALERT_FORMAT,
                                 formatDate(date:Date()),
                                 HUMIDITY_ATTRIBUTE, humidity.description)
                    displayCallback(str)
                    var alert:Alert? = state.alert
                    if alert == nil {
                        alert = try virtualDevice.createAlert(format: TOO_HUMID_ALERT)
                    }
                    try alert!.set(fieldName: HUMIDITY_ATTRIBUTE,
                                   value: humidity as AnyObject).raise()
                }
            } else {
                state.alerted = false
            }
            updateCallback(getHumidityData(device: device))
        } catch {
            errorCallback("updateHumidityVirtualDevice(): \(error)")
        }
    }
    
    private func processTemperatureSensorDataWithPolicies(device:Device) {
        guard let virtualDevice:VirtualDevice = device.virtualDevice else {
            return
        }
        let state:TemperatureSensorState =
            device.state as! TemperatureSensorState
        let temperatureSensor:TemperatureSensor =
            device.sensor as! TemperatureSensor
        if !temperatureSensor.isPoweredOn() {
            state.wasOff = true
            return
        }
        
        let ATTR_FORMAT = "%@ : Offer : \"%@\"=%@"
        let temperature:Double = temperatureSensor.getTemp()
        var str = String(format:ATTR_FORMAT,
                         formatDate(date:Date()),
                         TEMPERATURE_ATTRIBUTE,
                         temperature.description)
        do {
            // This set can trigger an "Illegal value" error if USE_POLCIES == true
            // but there is no policy in effect.
            _ = try virtualDevice.update().offer(attributeName: TEMPERATURE_ATTRIBUTE,
                                                 attributeValue: temperature as AnyObject)
            if state.wasOff {
                state.wasOff = false
                let startTime =
                    Date(timeIntervalSince1970:
                        TimeInterval(temperatureSensor.getStartTime()))
                str = str + String(format:",\"%@\"=",
                                   START_TIME_ATTRIBUTE,
                                   formatDate(date:startTime))
                _ = try virtualDevice.offer(attributeName: START_TIME_ATTRIBUTE,
                                            attributeValue: startTime as AnyObject)
            }
            
            try virtualDevice.finish()
        } catch {
            errorCallback("\(error)")
        }
        displayCallback(str)
        updateCallback(getTemperatureData(device: device))
    }
    
    private func processTemperatureSensorDataWithoutPolicies(device:Device) {
        
        guard let virtualDevice:VirtualDevice = device.virtualDevice else {
            return
        }
        let tempSensor:TemperatureSensor = device.sensor as! TemperatureSensor
        let state:TemperatureSensorState = device.state as! TemperatureSensorState
        let ALERT_FORMAT = "%@ : Alert : \"%@\"=%@, \"%@\"=%@, \"%@\"=%@"
        let ATTR_FORMAT = "%@ \"%@\"=%@"
        
        //Update Temperature sensor if on
        if !tempSensor.isPoweredOn() {
            state.wasOff = false
            return
        }
        
        do {
            var comma:String = ""
            var str = String(format:"%@ : Set : ",
                             formatDate(date:Date()))
            
            _ = virtualDevice.update()
            
            let temperature:Double = tempSensor.getTemp()
            str = String(format:ATTR_FORMAT,
                         str, TEMPERATURE_ATTRIBUTE, temperature.description)
            comma = ","
            
            // Note that this does not prevent the "Illegal value" error
            // from an "offer" if USE_POLICIES == true but there is no policy
            // in effect
            
            // Data model has temp range as [-20,80]. If data is out
            // of range, do not set the attribute in the virtual device.
            // Note: without this check, the set would throw an
            // IllegalArgumentException.
            if -20 < temperature && temperature < 80 {
                _ = try virtualDevice.set(attributeName: TEMPERATURE_ATTRIBUTE,
                                          attributeValue: temperature as AnyObject)
            } else {
                str = str + " (out of range, not updated)"
            }
            
            let unit:String = tempSensor.getUnit()
            if state.wasOff {
                state.wasOff = false
                let restartTime:Int64 = tempSensor.getStartTime()
                
                str = String(format:ATTR_FORMAT,
                             str + comma, UNIT_ATTRIBUTE, unit)
                comma = ","
                str = String(format:ATTR_FORMAT,
                             str + comma, START_TIME_ATTRIBUTE,
                             formatDate(date:Date(timeIntervalSince1970:
                                TimeInterval(restartTime))))
                
                _ = try virtualDevice.set(attributeName:START_TIME_ATTRIBUTE,
                                          attributeValue:restartTime as AnyObject)
                    .set(attributeName: UNIT_ATTRIBUTE,
                         attributeValue: unit as AnyObject)
            }
            
            let minTemp:Double = tempSensor.getMinTemp()
            if minTemp != state.prevMinTemp {
                state.prevMinTemp = minTemp
                str = String(format:ATTR_FORMAT,
                             str + comma,MIN_TEMP_ATTRIBUTE, minTemp.description)
                comma = ","
                _ = try virtualDevice.set(attributeName: MIN_TEMP_ATTRIBUTE,
                                          attributeValue: minTemp as AnyObject)
            }
            let maxTemp:Double = tempSensor.getMaxTemp()
            if maxTemp != state.prevMaxTemp {
                state.prevMaxTemp = maxTemp
                str = String(format:ATTR_FORMAT,
                             str + comma, MAX_TEMP_ATTRIBUTE, maxTemp.description)
                comma = ","
                _ = try virtualDevice.set(attributeName: MAX_TEMP_ATTRIBUTE,
                                          attributeValue: maxTemp as AnyObject)
            }
            
            displayCallback(str)
            updateCallback(getTemperatureData(device: device))
            
            try virtualDevice.finish()
            
            let minThreshold:Int = tempSensor.getMinThreshold()
            let maxThreshold:Int = tempSensor.getMaxThreshold()
            if temperature > Double(maxThreshold) {
                if !state.alerted {
                    state.alerted = true
                    str = String(format:ALERT_FORMAT,
                                 formatDate(date:Date()),
                                 TEMPERATURE_ATTRIBUTE,
                                 temperature.description,
                                 MAX_THRESHOLD_ATTRIBUTE,
                                 maxThreshold.description,
                                 UNIT_ATTRIBUTE, unit)
                    
                    var alert:Alert? = state.tooHotAlert
                    if alert == nil {
                        alert = try virtualDevice.createAlert(format:TOO_HOT_ALERT)
                    }
                    
                    try alert!.set(fieldName: TEMPERATURE_ATTRIBUTE,
                                   value: temperature as AnyObject)
                        .set(fieldName: MAX_THRESHOLD_ATTRIBUTE,
                             value: maxThreshold as AnyObject)
                        .set(fieldName: UNIT_ATTRIBUTE,
                             value: unit as AnyObject).raise()
                    displayCallback(str)
                }
            } else if temperature < Double(minThreshold) {
                if !state.alerted {
                    state.alerted = true
                    str = String(format:ALERT_FORMAT,
                                 formatDate(date:Date()),
                                 TEMPERATURE_ATTRIBUTE,
                                 temperature.description,
                                 MIN_THRESHOLD_ATTRIBUTE,
                                 minThreshold.description,
                                 UNIT_ATTRIBUTE, unit)
                    
                    var alert:Alert? = state.tooColdAlert
                    if alert == nil {
                        alert = try virtualDevice.createAlert(format:TOO_COLD_ALERT)
                    }
                    
                    try alert!.set(fieldName: TEMPERATURE_ATTRIBUTE,
                                   value: temperature as AnyObject)
                        .set(fieldName: MIN_THRESHOLD_ATTRIBUTE,
                             value: minThreshold as AnyObject)
                        .set(fieldName: UNIT_ATTRIBUTE,
                             value: unit as AnyObject).raise()
                    displayCallback(str)
                }
            } else {
                state.alerted = false
            }
            
        } catch {
            errorCallback("updateTemperatureVirtualDevice(): \(error)")
        }
    }
    /**
     * Subclesses implement this method.
     */
    override
    func processSensorData(device:Device) {
       device.update(device)
    }
    
    /*
     * This method is called after the gateway endpointId is established.
     * It is used as a key to create and persist the hardware id of a sensor
     * which is used to register the device.
     */
    override
    func getDevices(deviceModels:[String], endpointId:String) -> [Device] {
        var devices:[Device] = [Device]()
        for deviceModel in deviceModels {
            switch(deviceModel) {
            case HUMIDITY_SENSOR_MODEL_URN:
                devices.append(getHumiditySensor(endpointId: endpointId))
                break
            case TEMPERATURE_SENSOR_MODEL_URN:
                devices.append(getTemperatureSensor(endpointId: endpointId))
                break
            default:
                print("getDevices: Unknown device model: \(deviceModel)")
                break
            }
        }
        return devices
    }
    
    /*
     * Intialize VirtualDevice default values and setup control handlers
     */
    override
    func configureVirtualDevice(deviceModel:String, device:Device) {
        do {
            switch(deviceModel) {
            case HUMIDITY_SENSOR_MODEL_URN:
                let sensor:HumiditySensor = device.sensor as! HumiditySensor
                if let defaultThreshold:AnyObject =
                    try device.virtualDevice?.get(attributeName: MAX_THRESHOLD_ATTRIBUTE) {
                    sensor.setMaxThreshold(threshold: defaultThreshold as! Int)
                }
                let state = device.state as! HumiditySensorState
                state.prevMaxThreshold = sensor.getMaxThreshold()
                setHumidityControlHandlers(device:device)
                break
            case TEMPERATURE_SENSOR_MODEL_URN:
                let sensor:TemperatureSensor = device.sensor as! TemperatureSensor
                if let defaultMinThreshold =
                    try device.virtualDevice?.get(attributeName: MIN_THRESHOLD_ATTRIBUTE) {
                    sensor.setMinThreshold(threshold: defaultMinThreshold as! Int)
                }
                if let defaultMaxThreshold =
                    try device.virtualDevice?.get(attributeName: MAX_THRESHOLD_ATTRIBUTE) {
                    sensor.setMaxThreshold(threshold: defaultMaxThreshold as! Int)
                }
                setTemperatureControlHandlers(device:device)
                break
            default:
                print("configureDevice(): Unknown device model: \(deviceModel)")
                break
            }
        } catch {
            errorCallback("\(error)")
        }
    }

    /*
     * Set callbacks on the virtual devices to set attributes and
     * control the actual devices.
     */

    func setHumidityControlHandlers(device:Device) {
        let ON_CHANGE_FORMAT:String = "%@ : onChange : \"%@\"=%@"
        let ON_ERROR_FORMAT:String = "%@ : onError : \"%@\""

        do {
            /*
             * For the humiditySensor model, the maxThreshold attribute can be
             * written. Create a callback for setting the maxThreshold on the
             * humidity sensor device.
             */
            try device.virtualDevice?.setOnChange(
                    attributeName: MAX_THRESHOLD_ATTRIBUTE, callback: { event in


                let namedValue:NamedValue = event.getNamedValue()
                let value: Int = namedValue.getValue() as! Int
                        
                let sensor:HumiditySensor = device.sensor as! HumiditySensor
                let state:HumiditySensorState = device.state as! HumiditySensorState
                if state.prevMaxThreshold != value {
                    state.prevMaxThreshold = sensor.getMaxThreshold()
                }
                        
                let str = String(format: ON_CHANGE_FORMAT,
                                 formatDate(date:Date()),
                                 MAX_THRESHOLD_ATTRIBUTE,
                                 "\(value)")
                    
                sensor.setMaxThreshold(threshold: value)
                self.displayCallback(str)
            })
            
            /*
             * Create a handler for errors that may be generated when trying
             * to set values on the virtual device. The same callback is used
             * for both virtual devices.
             */
            device.virtualDevice?.setOnError(callback: { event in
                //let vd: VirtualDevice = event.getVirtualDevice()
                let errorMsg = event.getMessage()
                let str = String(format:ON_ERROR_FORMAT,
                    formatDate(date:Date()),
                    //vd.getEndpointId(), 
                    errorMsg)
                self.displayCallback(str)
            })
        } catch {
            errorCallback("\(error)")
        }
    }
    
    func setTemperatureControlHandlers(device:Device) {
        let ON_CHANGE_FORMAT:String = "%@ : onChange : "
        let ON_ERROR_FORMAT:String = "%@ : onError : %@"

        let sensor:TemperatureSensor = device.sensor as! TemperatureSensor

        /*
         * For the temperatureSensor model, the min and max threshold values
         * can be written. Create a callback that will handle setting
         * the value on the temperature sensor device.
         */
        device.virtualDevice?.setOnChange(callback: { event in
            var str:String = String(format:ON_CHANGE_FORMAT, formatDate(date:Date()))
            var notimpl:Bool = false
            
            // Update the indirectly connected device with the new value.
            var namedValue:NamedValue? = event.getNamedValue()
            var comma:String = ""
            while namedValue != nil {
                let attribute:String = namedValue!.getName()
                let value:Int = namedValue!.getValue() as! Int
                
                if attribute == MAX_THRESHOLD_ATTRIBUTE {
                    if sensor.isPoweredOn() {
                        sensor.setMaxThreshold(threshold: value)
                    }
                } else if attribute == MIN_THRESHOLD_ATTRIBUTE {
                    if sensor.isPoweredOn() {
                        sensor.setMinThreshold(threshold: value)
                    }
                } else {
                    notimpl = true
                }
                str = str + comma +
                    (notimpl ? "\"" + attribute + " not implemented\"" :
                               "\"" + attribute + "\"=\(value)")
                comma = ","
                notimpl = false
                namedValue = namedValue?.next()
            }
            /*
             * After this callback returns without an
             * exception, the virtual device attributes
             * will be updated locally and on the server.
             */
            if !sensor.isPoweredOn() {
                str = str + " ignored. Device is powered off"
            }
            self.displayCallback(str)
        })
        
        // Manage update errors for the temperature sensor indirectly connected 
        // device.
        device.virtualDevice?.setOnError(callback: { event in
            let errorMsg = event.getMessage()
            let str = String(format:ON_ERROR_FORMAT,
                formatDate(date:Date()),
                errorMsg)
            self.displayCallback(str)
        })

        // Handle actions:

        do {
            let ON_CALL_FORMAT:String = "%@ : Call : %@"
            /*
             * The temperatureSensor model has a 'power' action, which
             * takes a boolean argument: true to power on the simulated device,
             * false to power off the simulated device. Create a callback that
             * will handle calling power on and power off on the temperature
             * sensor device.
             */
            try device.virtualDevice?.setOnCall(actionName: "power",
                                    callback: { (virtualDevice, data) in
                
                let on = data as! Bool
                let isOn = sensor.isPoweredOn()
                if on != isOn {
                    sensor.power(on: on)
                }
                let str = String(format: ON_CALL_FORMAT,
                    formatDate(date:Date()),
                    "\"power\"=\(on)")
                self.displayCallback(str)
            })
            /*
             * The temperatureSensor model has a 'reset' action, which
             * resets the temperature sensor to factory defaults. Create
             * a callback that will handle calling reset on the temperature
             * sensor device.
             */
            try device.virtualDevice?.setOnCall(actionName: "reset",
                                    callback: { (virtualDevice, data) in
                
                // Note JCL does not include min and max temp
                var str = String(format:ON_CALL_FORMAT,
                        formatDate(date:Date()),
                         "reset: minTemp: " +
                            String(format: "%.2f", sensor.getMinTemp()) +
                            ", maxTemp: " +
                            String(format: "%.2f", sensor.getMaxTemp()))
                if sensor.isPoweredOn() {
                    sensor.reset()
                } else {
                    str = str + " ignored. Device is powered off"
                }
                self.displayCallback(str)
                /*
                 * After the sensor is reset, next poll of the
                 * sensor will yield new min and max temp values
                 * and update the values in the VirtualDevice.
                 */
            })
        } catch {
            print("setTemperatureControlHandlesr():onCall(reset/power): " + "\(error)")
            errorCallback("\(error)")
        }
    }
    
    
    /*
     * ["temperature":(String, Int64, Double, Double, Double, Int, Int)]
     * (endpointId,startTime,temperature,minTemp,maxTemp,minThreshold,maxThreshold)
     */
    func getTemperatureData(device:Device) -> [String:Any] {
        var data:[String:Any] = [String:Any]()
        let sensor:TemperatureSensor = device.sensor as! TemperatureSensor
        do {
            // When using policies, the value in the virtual device may
            // not have been updated since last "offer" was called.
            // If the value is nil use the local sensor value.
            data["temperature"] =
                (device.endpointId!,
                sensor.getStartTime(),
                try device.virtualDevice!.get(attributeName: TEMPERATURE_ATTRIBUTE) as? Double ??
                    sensor.getTemp(),
                try device.virtualDevice!.get(attributeName: MIN_TEMP_ATTRIBUTE) as? Double ??
                    sensor.getMinTemp(),
                try device.virtualDevice!.get(attributeName: MAX_TEMP_ATTRIBUTE) as? Double ??
                    sensor.getMaxTemp(),
                try device.virtualDevice!.get(attributeName: MIN_THRESHOLD_ATTRIBUTE) as? Int ??
                    sensor.getMinThreshold(),
                try device.virtualDevice!.get(attributeName: MAX_THRESHOLD_ATTRIBUTE) as? Int ??
                    sensor.getMaxThreshold())
        } catch {
            data["temperature"] =
                (device.endpointId!, sensor.getStartTime(),
                sensor.getTemp(), sensor.getMinTemp(), sensor.getMaxTemp(),
                sensor.getMinThreshold(),sensor.getMaxThreshold())
        }
        return data
    }
    
    //
    // The hardware id for the sensor instances are fabricated
    // from the gateway's endpoint id and a suffix, the combination of
    // which is likely to be unique. On subsequent runs of the
    // GatewayDeviceSample (with the same gateway device), the
    // endpoint id from the previously registered indirectly connected
    // device will be returned by the registerDevice call. In other
    // words, the same indirectly connected device will be used.
    //
    func getTemperatureSensor(endpointId:String) -> Device {
        let key = endpointId + "_Sample_TS"
        let persistentData = UserDefaults.standard
        var id:String? = persistentData.string(forKey: key)
        if id == nil {
            id = UUID().uuidString
            persistentData.set(id, forKey: key)
        }
        let tempSensor = TemperatureSensor(id: id!)
        let metaData = [
            AdvancedGatewayDevice.MANUFACTURER:tempSensor.getManufacturer(),
            AdvancedGatewayDevice.MODEL_NUMBER:tempSensor.getModelNumber(),
            AdvancedGatewayDevice.SERIAL_NUMBER:tempSensor.getSerialNumber()
        ]
        return Device(sensor:tempSensor,
                      hardwareId:tempSensor.getHardwareId(),
                      deviceModel:TEMPERATURE_SENSOR_MODEL_URN,
                      metaData: metaData,
                      restricted:RESTRICTED_ICD,
                      state: TemperatureSensorState(),
                      update: (usePolicy ?
                        processTemperatureSensorDataWithPolicies :
                        processTemperatureSensorDataWithoutPolicies))
    }

    /*
     * ["humidity":(String, Int, Int)] for humidity data
     * (endpointId,humidity,maxThreshold)
     */
    func getHumidityData(device:Device) -> [String:Any] {
        var data:[String:Any] = [String:Any]()
        do {
            // When using policies, the value in the virtual device may
            // not have been updated since last "offer" was called.
            // If the value is nil use the local sensor value.
            data["humidity"] = (device.endpointId!,
                 try device.virtualDevice!.get(attributeName: HUMIDITY_ATTRIBUTE) as? Int ??
                    (device.sensor as! HumiditySensor).getHumidity(),
                 try device.virtualDevice!.get(attributeName: MAX_THRESHOLD_ATTRIBUTE) as? Int ??
                    (device.sensor as! HumiditySensor).getMaxThreshold())
        } catch {
            let sensor = device.sensor as! HumiditySensor
            data["humidity"] = (device.endpointId!, sensor.getHumidity(), sensor.getMaxThreshold())
        }
        return data
    }
    
    func getHumiditySensor(endpointId:String) -> Device {
        let key = endpointId + "_Sample_HS"
        let persistentData = UserDefaults.standard
        var id:String? = persistentData.string(forKey: key)
        if id == nil {
            id = UUID().uuidString
            persistentData.set(id, forKey: key)
        }
        let humiditySensor = HumiditySensor(id: id!)
        let metaData = [
            AdvancedGatewayDevice.MANUFACTURER:humiditySensor.getManufacturer(),
            AdvancedGatewayDevice.MODEL_NUMBER:humiditySensor.getModelNumber(),
            AdvancedGatewayDevice.SERIAL_NUMBER:humiditySensor.getSerialNumber()
        ]
        return Device(sensor:humiditySensor,
                      hardwareId:humiditySensor.getHardwareId(),
                      deviceModel:HUMIDITY_SENSOR_MODEL_URN,
                      metaData: metaData,
                      restricted:RESTRICTED_ICD,
                      state:HumiditySensorState(),
                      update: (usePolicy ?
                        processHumiditySensorDataWithPolicies :
                        processHumiditySensorDataWithoutPolicies))
    }
}

public class GatewayDeviceSampleBase {
    
    var gateway:GatewayDevice?
    
    /* The callbacks implemented by the View to render errors
     */
    var errorCallback:(String)->()?
    
    /* Update the virtual devices at a rate defined by this `updateInterval` in
     * seconds
     */
    let updateInterval:Double!
    /* The schedule timer that periodically updates the virtual devices at
     * the rate of `updateInterval`
     */
    var timer:Timer!

    
    /* The supported device models */
    let deviceModels:[String]!
    
    /* This dispatch queue synchronizes access to the devices array */
    let deviceQueue:DispatchQueue = DispatchQueue(label:"Device Queue",
                                                  qos:.utility)
    /* Managed devices that are ready to run */
    var devices:[String:Device] = [String:Device]()
    func addDevice(device:Device) {
        deviceQueue.sync() {
            devices[device.endpointId!] = device
        }
    }
    
    /* Make sure displays don't overwrite each other */
    let displayQueue:DispatchQueue = DispatchQueue(label:"Display Queue",
                                                   qos:.utility)
    
    // The GatewayDeviceSample interface requires a sensor update interval
    // and callbacks to update the View
    public init(path:String, password:String, deviceModels:[String],
                updateInterval:Int,
                errorCallback: @escaping (String)->()) throws {
        self.deviceModels = deviceModels
        self.errorCallback = errorCallback
        self.updateInterval = Double(updateInterval)
        
        gateway = try GatewayDevice(path: path, password: password)
        
        DispatchQueue.main.async(execute: {
            self.timer = Timer.scheduledTimer(timeInterval: self.updateInterval,
                target: self,
                selector: #selector(GatewayDeviceSample.updateVirtualDevices),
                userInfo: nil, repeats: true)
        })
    }
    
    /**
     * Subclesses implement this method.
     */
    func processSensorData(device:Device) {
        fatalError("Subclass must implement 'processSensorData'")
    }
    
    /**
     * Subclasses implement this method to configure the virtual devices
     * with virtual device control handlers and set default values in the
     * sensors.
     */
    func configureVirtualDevice(deviceModel:String, device:Device) {
        fatalError("Subclass must implement 'configureVirtualDevice`")
    }
    
    /**
     * Subclasses implement this method and return the set of devices that 
     * it manages.
     */
    func getDevices(deviceModels:[String], endpointId:String) -> [Device] {
        fatalError("Subclass must implement 'getDevices'")
    }
    
    /*
     * Create a virtualDevice.
     * The virtualDevice instance is recorded in device.
     */
    func createVirtualDevice(device:Device) {
        do {
            try self.gateway!.getDeviceModel(deviceModelUrn: device.deviceModel,
                                             callback: {(deviceModel,error) in
                if error != nil {
                    self.errorCallback("\(error!)")
                    return
                }
                do {
                    device.virtualDevice =
                        try self.gateway!.createVirtualDevice(
                            deviceId: device.endpointId!,
                            deviceModel: deviceModel!)
                    self.configureVirtualDevice(deviceModel:device.deviceModel,
                                                device:device)
                    // The device is ready to run
                    self.addDevice(device:device)
                } catch {
                    self.errorCallback("\(error)")
                }
            })
        } catch {
            errorCallback("\(error)")
        }
    }
    
    /*
     * This method creates and initializes local sensors using "getDevices()" and
     * registers those devices. If the icd's were previously registered
     * they will not be recreated in the cloud.
     */
    func registerDevices(endpointId:String, deviceModels:[String]) {
        /*
         * Once the Gateway endpoint id is obtained, the managed devices are
         * created and then registered.
         * When they are registered their endpoint ids are recorded.
         * Then virtual devices are created for each one.
         */
        for device in self.getDevices(deviceModels:deviceModels,
                                      endpointId:endpointId) {
            do {
                try self.gateway!.registerDevice(restricted: device.restricted,
                                                 hardwareId: device.hardwareId,
                                                 metaData: device.metaData,
                                                 deviceModels: device.deviceModel,
                                                 callback: { endpointId, error in
                    if error != nil {
                        self.errorCallback("\(error!)")
                        return
                    }
                    // Record the icd's endpointId
                    device.endpointId = endpointId
                    self.createVirtualDevice(device:device)
                })
            } catch {
                self.errorCallback("\(error)")
            }
        }
        
    }
    
    /*
     * Activate the gatewy
     * Create and initialize sensors
     * Register sensors and create virtual devices for each one
     * Start the update loop.
     */
    public func start() {
        /*
         * First the Gateway must be activated. The endpointId is used
         * to uniquely identify icd's. Once the endpointId is obtained
         * local sensors can be created and registered.
         * If the gateway is already activated, then the endpointId is
         * is known, and previously registered icd's with the gateway
         * can be recreated without creating new icd's in the cloud.
         */
        do {
            if !self.gateway!.isActivated() {
                try self.gateway!.activate(callback: {endpointId, error in
                    if error != nil {
                        self.errorCallback("\(error!)")
                        return
                    }
                    // the gateway has this endpointId set on it during
                    // the activate call
                    
                    // For each device create a task that registers the device, and
                    // creates a virtual device for it.
                    self.registerDevices(endpointId:endpointId!,
                                         deviceModels: self.deviceModels)
                })
            } else {
                self.registerDevices(endpointId:self.gateway!.getEndpointId(),
                                     deviceModels:deviceModels)
            }
        } catch {
            errorCallback("\(error)")
        }
    }
    
    // Update the humidity and temperature sensor virtual devices attributes.
    @objc func updateVirtualDevices() {
        self.deviceQueue.sync() {
            for (_,device) in self.devices {
                processSensorData(device:device)
            }
        }
    }

    public func stop() {
        
    }
    
    public func close() throws {
        try gateway?.close()
        gateway = nil
    }
    
    deinit {
        do {
            try gateway?.close()
        } catch {
            print("Caught exception closing GatewayDevice.\n" +
                "\(error)")
        }
        gateway = nil
    }
}

