/*
 *  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 DeviceLib

public class DataConnector {
    
    private static let IBEACON_SENSOR_MODEL_URN = "urn:com:oracle:iot:device:location:ibeacon"
    private static let EDDYSTONE_SENSOR_MODEL_URN = "urn:com:oracle:iot:device:location:eddystone-tlm-uid"
    private static let BEACON_RSSI_ATTRIBUTE = "ora_rssi"
    private static let BEACON_TXPOWER_ATTRIBUTE = "ora_txPower"
    private static let EDDYSTONE_TEMP_ATTRIBUTE = "temperature"
    private static let EDDYSTONE_VOLT_ATTRIBUTE = "batteryVoltage"
    
    /**
     * Only information from beacons currently within this maximum distance in 
     * meters will be reported.
     */
    private static let MAX_DISTANCE = 100.0
    
    /**
     * Set simulated to true to use simulated beacons or false for real beacons.
     */
    private static let simulated: Bool = false
    
    /**
     * Number of simulated beacons to create when doing beacon simulation.
     */
    private static let DEFAULT_NUM_BEACONS = 2
    
    private var timer: Timer = Timer()
    
    private var outputText: String = ""
    
    private var iBeaconDM: DeviceModel!
    private var eddystoneDM: DeviceModel!
    
    var gd: GatewayDevice?
    
    // Error message in case the initialization of the device failed.
    private var errorMessage = String()
      
    public static func getMaxDistance() -> Double {
        return DataConnector.MAX_DISTANCE
    }

    public init() throws {
        gd = try GatewayDevice(path: Provisioning.getProvisioningFilePath(),
                            password: Provisioning.getProvisioningPassword())
        
        if (!DataConnector.simulated) {
            if (scanner == nil) {
                scanner = BeaconScanner()
            }
        }
    }
    
    internal func isDeviceInitialized() -> Bool {
        return self.gd != nil ? true : false
    }
    
    internal func getErrorMessage() -> String {
        return errorMessage
    }
    
    public func getOutputText() -> String {
        return self.outputText
    }
    
    public func resetOutputText() {
        self.outputText = ""
    }

    public func stop() {
        stopped = true
        self.outputText = ""
    }
    
    public func unstop() {
        stopped = false
        self.timer.invalidate()
    }
    
    public func close() {
        if(self.gd != nil) {
            do {
                try gd!.close()
            } catch {
                print("Issue while closing Gateway device.")
            }
            gd = nil
        }
    }
    
    func getCurrentDateString() -> String {
        let formatter = DateFormatter()
        formatter.timeStyle = .medium
        formatter.dateStyle = .medium
        let currentDateTime = Date()
        return formatter.string(from: currentDateTime)
    }
    
    func display(string: String, beacon: Beacon) {
        if (!stopped) {
            if selectedBeacon != nil &&
                    selectedBeacon.getEndpointID() == beacon.getEndpointID() {
                self.outputText += "\n" + string
            }
        }
    }
    
    // Prepare the gateway device.
    func pgd(callback: @escaping (ClientError?) -> Void) {
        prepareGatewayDevice(callback: { error in
            if (error == nil) {
                if (DataConnector.simulated) {
                    let abeaconUUID = (self.gd?.getEndpointId())! + "_Sample"
                    var newBeaconSensor: Beacon!
                    
                    for i in 0..<DataConnector.DEFAULT_NUM_BEACONS {
                        if i % 2 == 0 {
                            newBeaconSensor = IBeacon.getSimulatedInstance(minorNum: UInt8(i),
                                                                           beaconUUID: abeaconUUID)
                        }
                        else {
                            newBeaconSensor = EddystoneBeacon.getSimulatedInstance(minorNum: UInt8(i),
                                                                                   beaconUUID: abeaconUUID)
                        }
                        beaconList.append(newBeaconSensor)
                    }
                }
            } else {
                print(error as Any)
            }
            callback(error)
        })
    }
    
    // Activate the gateway device.
    func prepareGatewayDevice(callback: @escaping (ClientError?) -> Void) {
        if !(gd!.isActivated()) {
            do {
                try gd!.activate(callback: { endpointId, error in
                    callback(error)
                })
            } catch  {
                print(error)
            }
        } else {
            callback(nil)
        }
    }
    
    // Register indirectly connected devices.
    func registerIndirectlyConnectedDevices(callback: @escaping (ClientError?) -> Void) {
        for beacon in beaconList {
            if beacon.getEndpointID() == nil {
                var beaconMetaData: [String : String] = [ : ]
                var dm: String
                beaconMetaData[GatewayDevice.MANUFACTURER] = beacon.getManufacturer()
                beaconMetaData[GatewayDevice.MODEL_NUMBER] = beacon.getModelNumber()
                beaconMetaData[GatewayDevice.SERIAL_NUMBER] = beacon.getSerialNumber()
                beaconMetaData[GatewayDevice.DEVICE_CLASS] = beacon.getDeviceClass()
                beaconMetaData[GatewayDevice.PROTOCOL] = beacon.getProtocol()
                beaconMetaData[GatewayDevice.PROTOCOL_DEVICE_ID] = beacon.getAddress()
                beaconMetaData[GatewayDevice.PROTOCOL_DEVICE_CLASS] = beacon.getType().rawValue
                beaconMetaData["UUID"] = beacon.getUUID()
                
                if beacon.getType() == .IBEACON {
                    let iBeacon = beacon as! IBeacon
                    beaconMetaData["major"] = iBeacon.getMajorVersion()
                    beaconMetaData["minor"] = iBeacon.getMinorVersion()
                    dm = DataConnector.IBEACON_SENSOR_MODEL_URN
                } else {
                    dm = DataConnector.EDDYSTONE_SENSOR_MODEL_URN
                }
                
                do {
                    try self.gd!.registerDevice(restricted: beacon.isRestricted(),
                            hardwareId: beacon.getIdentifier(), metaData: beaconMetaData,
                            deviceModels: dm, callback: { endpointIdIBeacon, rderrorT in
                        if (rderrorT != nil) {
                            callback(rderrorT)
                            return
                        }
                        beacon.setEndpointID(endpointID: endpointIdIBeacon!)
                        do {
                            if beacon.getType() == .IBEACON {
                                try beacon.setVirtualizedBeacon(
                                    virtualizedBeacon: self.gd!.createVirtualDevice(
                                        deviceId: beacon.getEndpointID()!,
                                        deviceModel: self.iBeaconDM))
                            } else {
                                try beacon.setVirtualizedBeacon(
                                    virtualizedBeacon: self.gd!.createVirtualDevice(
                                            deviceId: beacon.getEndpointID()!,
                                            deviceModel: self.eddystoneDM))
                            }
                            let mssi = beacon.getMssi()
                            let _ = try beacon.getVirtualizedBeacon()!.set(
                                attributeName: DataConnector.BEACON_TXPOWER_ATTRIBUTE,
                                attributeValue: mssi as AnyObject)
                            let messageBeacon = self.getCurrentDateString() +
                                " : Set : \"ora_tx_power\" = " + mssi.description
                            self.display(string: messageBeacon, beacon: beacon)
                        } catch  {
                            print (error)
                        }
                    })
                } catch {
                    print (error)
                }
            }
        }
        callback(nil)
    }
    
    // Create virtual devices for iBeacon and EddystoneBeacon sensors.
    func getDeviceModels(callback: @escaping (ClientError?) -> Void) {
            do {
                try self.gd!.getDeviceModel(deviceModelUrn: DataConnector.IBEACON_SENSOR_MODEL_URN,
                                            callback: { (returnDeviceModelT, error) in
                    if (error != nil) {
                        callback(error)
                    }
                    if let returnObjectT = returnDeviceModelT {
                        self.iBeaconDM = returnObjectT
                        
                        do {
                            try self.gd!.getDeviceModel(deviceModelUrn: DataConnector.EDDYSTONE_SENSOR_MODEL_URN,
                                    callback: { (returnDeviceModelH, error) in
                                if let returnObjectH = returnDeviceModelH {
                                    self.eddystoneDM = returnObjectH
                                }
                                callback(error)
                            })
                        } catch  {
                            print(error)
                        }
                    }
                })
            } catch {
                print (error)
            }
    }
    
    func startLoop() {
        // Timers must be started on the main thread.
        // This code is executed from the Alamofire response
        // queue. This queue has been changed to be NOT the main
        // queue.
        DispatchQueue.main.async(execute: {
            self.timer = Timer.scheduledTimer(timeInterval: 5, target: self,
                        selector: #selector(DataConnector.updateVirtualDevices),
                        userInfo: nil, repeats: true)
        })
    }

    // Create virtual devices (IBeacon and EddystoneBeacon) and start 
    // attribute updates.
    func startSample() {
        self.getDeviceModels(callback: { error in
            if (error == nil) {
                self.registerIndirectlyConnectedDevices(callback: { error in
                    if (error == nil) {
                        self.startLoop()
                    } else {
                        print(error!)
                    }
                })
            } else {
                print(error!)
            }
        })
    }
    
    // Update the humidity and temperature sensor virtual devices attributes.
    @objc private func updateVirtualDevices() {
        if !stopped {
            self.registerIndirectlyConnectedDevices(callback: { error in
                if (error != nil) {
                    print(error!)
                }
            })

            for beacon in beaconList {
                if beacon.getVirtualizedBeacon() != nil {
                    do {
                        let rssi = beacon.getRssi()
                        if (rssi == 0) {
                            continue
                        }
                        if beacon.getDistance() <= DataConnector.MAX_DISTANCE {
                            // Update IBeacon sensor
                            if beacon.getType() == .IBEACON {
                                let iBeacon = beacon as! IBeacon
                                
                                let _ = try iBeacon.getVirtualizedBeacon()!.set(
                                    attributeName: DataConnector.BEACON_RSSI_ATTRIBUTE,
                                    attributeValue: rssi as AnyObject)
                                
                                let messageIBeacon = self.getCurrentDateString() +
                                    " : Set : \"ora_rssi\" = " + rssi.description
                                self.display(string: messageIBeacon, beacon: beacon)
                            } else {
                                
                                // Update EddystoneBeacon sensor
                                let eddyBeacon = beacon as! EddystoneBeacon
                                
                                let temp = eddyBeacon.getTemperature()
                                let volt = eddyBeacon.getVoltage()
                                
                                try eddyBeacon.getVirtualizedBeacon()!.update()
                                    .set(attributeName: DataConnector.BEACON_RSSI_ATTRIBUTE,
                                         attributeValue: rssi as AnyObject)
                                    .set(attributeName: DataConnector.EDDYSTONE_TEMP_ATTRIBUTE,
                                         attributeValue: temp as AnyObject)
                                    .set(attributeName: DataConnector.EDDYSTONE_VOLT_ATTRIBUTE,
                                         attributeValue: volt as AnyObject)
                                    .finish()
                                
                                let messageEddyBeacon = self.getCurrentDateString() +
                                    " : Set : \"ora_rssi\" = " + rssi.description +
                                    " \"temperature\" = " + temp.description +
                                    " \"batteryVoltage\" = " + volt.description
                                
                                self.display(string: messageEddyBeacon, beacon: beacon)
                            }
                        }
                    } catch {
                        print("Failed updating virtual devices.")
                    }
                }
            }
        }
    }
}
