/*
 *  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 presents a simple sensor as virtual device to the IoT
 * server. The simple sensor is polled based on the `updateInterval` passed
 * to the `DirectlyConnectedDeviceSample` constructor from the application and
 * the virtual device is 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 property 'USE_POLICIE" defined 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 sensor data 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 value from the sensor exceeds
 * based on the maximum threshold of the sensor's data model.
 */

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

/* The device model attributes */
let HUMIDITY_ATTRIBUTE = "humidity"
let MAX_THRESHOLD_ATTRIBUTE = "maxThreshold"

/* The alert format urns */
let TOO_HUMID_ALERT = HUMIDITY_SENSOR_MODEL_URN + ":too_humid"

/**
 * The `DirectlyConnectedDeviceSample` class implements the behavior for a
 * `HUMIDITY_SENSOR_MODEL_URN` device.
 *
 * The superclass `DirectlyConnectedDeviceSampleBase` implements the behavior 
 * that all `DirectlyConnectedDevices` must implement to be a directly connected 
 * device to the iOT Cloud. It activates the DirectlyConnectedDevice,
 * creates the virtual device and periodically
 * reads the sensor and updates the virtual devices.
 *
 * Note that there are some assumptons about how the UI and the View code
 * render data. The `DirectlyConnectedDeviceSample` 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 DirectlyConnectedDeviceSample : DirectlyConnectedDeviceSampleBase {

    /* Humidity sensor device */
    var humiditySensor:HumiditySensor?
    
    /* Sensor state */
    var alerted:Bool = false
    var prevMaxThreshold:Int = 0
    var alert:Alert?
    
    /* The callbacks implemented by the View to render data and runtime
     * information produced by the gateway.
     */
    var displayCallback:(String)->()?
    var updateCallback:([String : Any]) -> ()?
    
    /**
     * 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.
     */
    var usePolicy:Bool = false

    /**
     * The DirectlyConnectedDeviceSample constructor
     */
    public init(path:String, password:String, updateInterval:Int,
                usePolicy:Bool,
                updateCallback: @escaping([String:Any])->(),
                errorCallback: @escaping (String)->(),
                displayCallback: @escaping (String)->()
        ) throws {
        
        // Create the local sensor device
        humiditySensor =  HumiditySensor(id:UUID().uuidString)
        self.updateCallback = updateCallback
        self.displayCallback = displayCallback
        self.usePolicy = usePolicy

        try super.init(path:path, password:password,
            deviceModel: HUMIDITY_SENSOR_MODEL_URN,
            updateInterval: updateInterval,
            errorCallback: errorCallback)
    }
    
    //
    // When running with policies, we only need to "offer" the
    // humidity value. Policies can be set on the server to
    // generate alerts, and filter bad data - which had to be
    // handled by the sample code when running without policies.
    // Compare this implementation of processSensorData to that
    // of the WithoutPolicies inner class.
    //
    private func processHumiditySensorDataWithPolicies() {
        let ATTR_FORMAT = "%@ : Offer : \"%@\"=%@"
        let humidity:Int = humiditySensor!.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())
            try virtualDevice?.finish()
        } catch {
            errorCallback("\(error)")
        }
    }
    
    //
    // When running the sample without policies, the sample must do more work.
    // The sample has to generate alerts if the sensor value exceeds the
    // threshold. 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 processHumiditySensorDataWithoutPolicies() {
        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.
            _ = try virtualDevice?.update().set(
                attributeName: HUMIDITY_ATTRIBUTE,
                attributeValue: humidity as AnyObject)
            displayCallback(str)
            try virtualDevice?.finish()
            let humidityThreshold = humiditySensor!.getMaxThreshold()
            if humidity > humidityThreshold {
                if !alerted {
                    alerted = true
                    str = String(format:ALERT_FORMAT,
                                 formatDate(date:Date()),
                                 HUMIDITY_ATTRIBUTE, humidity.description)
                    displayCallback(str)
                    if alert == nil {
                        alert = try virtualDevice?.createAlert(format: TOO_HUMID_ALERT)
                    }
                    try alert?.set(fieldName: HUMIDITY_ATTRIBUTE,
                                   value: humidity as AnyObject).raise()
                }
            } else {
                alerted = false
            }
            updateCallback(getHumidityData())
        } catch {
            errorCallback("updateHumidityVirtualDevice(): \(error)")
        }
    }
    
    override
    func processSensorData() {
        if usePolicy {
            processHumiditySensorDataWithPolicies()
        } else {
            processHumiditySensorDataWithoutPolicies()
        }
    }
    
    /*
     * Intialize VirtualDevice default values and setup control handlers
     */
    override
    func configureVirtualDevice() {
        do {
            if let defaultThreshold:AnyObject =
                try virtualDevice?.get(attributeName: MAX_THRESHOLD_ATTRIBUTE) {
                humiditySensor!.setMaxThreshold(threshold: defaultThreshold as! Int)
            }
            setHumidityControlHandlers()
        } catch {
            errorCallback("\(error)")
        }
    }
    
    /*
     * Monitor the virtual device for requested attribute changes and
     * errors.
     *
     * Since there is only one attribute, maxThreshold, this could have
     * done with using an attribute specific on change handler.
     */
    private func setHumidityControlHandlers() {
        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 virtualDevice?.setOnChange(
                attributeName: MAX_THRESHOLD_ATTRIBUTE,
                callback: { event in
                    
                    let namedValue:NamedValue = event.getNamedValue()
                    let value: Int = namedValue.getValue() as! Int
                    let str = String(format: ON_CHANGE_FORMAT,
                                     formatDate(date:Date()),
                                     MAX_THRESHOLD_ATTRIBUTE,
                                     "\(value)")
                    
                    self.humiditySensor!.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.
             */
            virtualDevice?.setOnError(callback: { event in
                //let vd: VirtualDevice = event.getVirtualDevice()
                let errorMsg = event.getMessage()
                let str = String(format:ON_ERROR_FORMAT,
                                 formatDate(date:Date()),
                                 errorMsg)
                self.displayCallback(str)
            })
        } catch {
            errorCallback("\(error)")
        }
    }
    
    /*
     * ["humidity":(String, Int, Int)] for humidity data
     * (endpointId,humidity,maxThreshold)
     */
    private func getHumidityData() -> [String:Any] {
        var data:[String:Any] = [String:Any]()
        do {
            // Get the data from the virtual device.
            // The data will reflect the value based on policy if a policy
            // is in place and running with USE_POLICIES == true
            data["humidity"] = (self.dcd!.getEndpointId(),
                                try virtualDevice?.get(attributeName: HUMIDITY_ATTRIBUTE) as? Int ??
                                    humiditySensor!.getHumidity(),
                                try virtualDevice?.get(attributeName: MAX_THRESHOLD_ATTRIBUTE) as? Int ??
                                    humiditySensor!.getMaxThreshold())
        } catch {
            // Problem getting data from the virtual device, get it from the
            // sensor directly
            data["humidity"] = (self.dcd!.getEndpointId(), humiditySensor!.getHumidity(),
                                humiditySensor!.getMaxThreshold())
        }
        return data
    }
}

public class DirectlyConnectedDeviceSampleBase {
    
    var dcd:DirectlyConnectedDevice?
    var virtualDevice:VirtualDevice?
    
    /* 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 deviceModel:String
    
    /* This dispatch queue synchronizes access to the devices array */
    let deviceQueue:DispatchQueue = DispatchQueue(label:"Device Queue",
                                                  qos:.utility)

    /* Make sure displays don't overwrite each other */
    let displayQueue:DispatchQueue = DispatchQueue(label:"Display Queue",
                                                   qos:.utility)
    
    // The DirectlyConnectedDevice interface requires a sensor update interval
    // and callbacks to update the View
    public init(path:String, password:String, deviceModel:String,
                updateInterval:Int,
                errorCallback: @escaping (String)->()
        ) throws {
        self.deviceModel = deviceModel
        self.errorCallback = errorCallback
        self.updateInterval = Double(updateInterval)
        
        dcd = try DirectlyConnectedDevice(path: path, password: password)
        
        DispatchQueue.main.async(execute: {
            self.timer = Timer.scheduledTimer(timeInterval: self.updateInterval,
                target: self,
                selector: #selector(DirectlyConnectedDeviceSample.updateVirtualDevice),
                userInfo: nil, repeats: true)
        })
    }

    /**
     * Subclesses implement this method.
     */
    func processSensorData() {
        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() {
        fatalError("Subclass must implement 'configureVirtualDevice`")
    }
    
    /*
     * Create a virtualDevice.
     * The virtualDevice instance is recorded in device.
     */
    func createVirtualDevice(endpointId:String, deviceModel:String) {
        do {
            try dcd!.getDeviceModel(deviceModelUrn:deviceModel,
                                    callback: {(deviceModel,error) in
                if error != nil {
                    self.errorCallback("\(error!)")
                    return
                }
                do {
                    try self.deviceQueue.sync() {
                        self.virtualDevice = try self.dcd!.createVirtualDevice(
                                deviceId: endpointId,
                                deviceModel: deviceModel!)
                        self.configureVirtualDevice()
                    }
                } catch {
                    self.errorCallback("\(error)")
                }
            })
        } catch {
            errorCallback("\(error)")
        }
    }
    
    /*
     * Activate the DirectlyConnectedDevice
     * Create and initialize sensors
     * Create virtual devices for each one
     * Start the update loop.
     */
    public func start() {
        /*
         * First the DirectlyConnectedDevice 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.dcd!.isActivated() {
                try self.dcd!.activate(deviceModels:deviceModel,
                                       callback: {endpointId, error in
                    if error != nil {
                        self.errorCallback("\(error!)")
                        return
                    }
                    // the dcd has this endpointId set on it during
                    // the activate call
                    // create a virtual device
                    self.createVirtualDevice(endpointId:endpointId!,
                                             deviceModel: self.deviceModel)
                })
            } else {
                self.createVirtualDevice(endpointId:self.dcd!.getEndpointId(),
                                         deviceModel:deviceModel)
            }
        } catch {
            errorCallback("\(error)")
        }
    }
    
    // Update the humidity and temperature sensor virtual devices attributes.
    @objc func updateVirtualDevice() {
        self.deviceQueue.sync() {
            if virtualDevice != nil {
                self.processSensorData()
            }
        }
    }

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