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

/**
 * Representation of an Eddystone Beacon.
 */
public class EddystoneBeacon : Beacon {
    
    fileprivate static let EDDYSTONE_TLM_PREAMBLE: [UInt8] = [0xAA, 0xFE,0x20]
    
    /**
     * Simulation variables
     */
    private var xVolt = 0
    private let minVoltRange = 0.1
    private let maxVoltRange = 3000.0
    private var lastVoltPoint: Double!
    private let amplitudeVolt = 0.10
    
    private var xTemp = 0
    private let minTempRange = 0.0
    private let maxTempRange = 2560.0
    private var lastTempPoint: Double!
    private var amplitudeTemp: Double!
    
    var tlm: TLMData!
    
    private init(identifier: String, UUID: String, serialNumber: String,
                 address: String, modelNumber: String, mssi: Int,
                 rawData: [UInt8], isSimulated: Bool, isRestricted: Bool) {
        
        super.init(identifier: identifier, UUID: UUID,
                   serialNumber: serialNumber, address: address,
                   modelNumber: modelNumber, mssi: mssi,
                   rawData: rawData, type: Type.EDDYSTONE,
                   isSimulated: isSimulated, isRestricted: isRestricted)
        
        self.tlm = TLMData(temperature: -128 * 256, voltage: 0)
        self.lastVoltPoint = self.maxVoltRange
        self.lastTempPoint = self.minTempRange * 0.3
        self.amplitudeTemp = self.maxTempRange * 0.10 + 1.0
        self.hardwareId = identifier
        
        if (isSimulated) {
            self.xTemp = abs(serialNumber.hashValue) % 360
            self.lastTempPoint = self.maxTempRange * (Double(self.xTemp) + 1) / 360
            self.xVolt = abs(serialNumber.hashValue) % 360
        }
    }
    
    
    public func getTemperature() -> Double {
        if !self.isSimulated {
            return self.tlm.temperature
        }
        else {
            return getSimulatedTemperature()
        }
    }
    
    fileprivate func getSimulatedTemperature() -> Double {
        let delta = self.amplitudeTemp * sin(self.xTemp.toRadians)
        self.xTemp += 14
        var temp = self.lastTempPoint + delta
        
        if (temp > maxTempRange) || (temp < minTempRange) {
            temp = lastTempPoint - delta
        }
        
        lastTempPoint = temp
        
        return round(temp)
    }
    
    public func getVoltage() -> Double {
        if !self.isSimulated {
            return self.tlm.voltage
        }
        else {
            return getSimulatedVoltage()
        }
    }
    
    fileprivate func getSimulatedVoltage() -> Double {
        let delta = abs(self.amplitudeVolt * sin(self.xVolt.toRadians))
        self.xVolt += 14
        var volt = self.lastVoltPoint - delta
        
        if volt < minVoltRange {
            volt = minVoltRange
        }
        
        self.lastVoltPoint = volt
        
        return round(volt)
    }
    
    
    public func setTLM(packet: [UInt8]) {
        let newTlm = TLMData.parseTLM(packet: packet)
        if newTlm != nil {
            self.tlm = newTlm
        }
    }
    
    
    public static func getSimulatedInstance(minorNum: UInt8, beaconUUID: String) -> EddystoneBeacon {
        var minor = [UInt8](repeating: 0, count: 2)
        minor[1] = minorNum
        let mssi = -67
        let UUID = beaconUUID
        let majorVersion = MAJOR_NUM
        let minorVersion = getString(data: minor)
        let identifier = UUID + ":" + majorVersion + ":" +  minorVersion
        let serialNumber = majorVersion + ":" + minorVersion 
        let rawData = [UInt8](repeating: 0, count: 1)
        let modelNumber = "MN-" + identifier
        let address = "Simulated Bluetooth Device"
        
        return EddystoneBeacon(identifier: identifier, UUID: UUID,
                               serialNumber: serialNumber, address: address,
                               modelNumber: modelNumber, mssi: mssi,
                               rawData: rawData, isSimulated: true,
                               isRestricted: false)
    }

    
    /**
     * Representation of telemetry data for an Eddystone-TLM packet.
     */
    class TLMData {
        private static let VOLTAGE_OFFSET: UInt8 = 2
        private static let TEMPERATURE_OFFSET: UInt8 = 4
        
        /**
         * Temperature measured in degrees Celsius, expressed in signed 8.8
         * fixed-point notation.
         */
        var temperature: Double!
        
        /**
         * Battery charge in millivolts, expressed as 1 mV per bit.
         */
        var voltage: Double!
        
        init(temperature: Double, voltage: Double) {
            self.temperature = temperature;
            self.voltage = voltage;
        }
        
        /**
         * Parses an Eddystone-TLM frame to get the temperature and battery 
         * voltage data.
         * - param packet: The ScanRecord containing an Eddystone-TLM frame
         *
         * - returns: A new TLMData containing temperature and batter voltage 
         *            values, or nil if the packet does not contain a proper 
         *            Eddystone-TLM frame.
         */
        static func parseTLM(packet: [UInt8]) -> TLMData? {
            var temperature: Double!
            var voltage: Double!
            var start: Int!
            
            // Find the start of the Eddystone-TLM frame in the packet data
            for start in 0..<packet.count {
                if (packet.count - start) > 19 {
                    let i = 0
                    for i in 0..<EDDYSTONE_TLM_PREAMBLE.count {
                        if packet[start + i] != EDDYSTONE_TLM_PREAMBLE[i] {
                            break
                        }
                    }
                    if i == EDDYSTONE_TLM_PREAMBLE.count {
                        break
                    }
                }
                else {
                    return nil
                }
            }
            
            start = start + 2
            
            var startIdx: Int = start + Int(TEMPERATURE_OFFSET)
            var stopIdx: Int = start + Int(TEMPERATURE_OFFSET) + 2
            let tempArray:[UInt8] = Array(packet[startIdx..<stopIdx])
            //var u16 = UnsafePointer<UInt16>(tempArray).memory
            
            var u16 = tempArray.withUnsafeBufferPointer {
                ($0.baseAddress!.withMemoryRebound(to: UInt16.self, capacity: 1) { $0 })
                }.pointee
            temperature = Double(u16)
            
            startIdx = start + Int(VOLTAGE_OFFSET)
            stopIdx = start + Int(VOLTAGE_OFFSET) + 2
            let voltArray:[UInt8] = Array(packet[startIdx..<stopIdx])
            u16 = voltArray.withUnsafeBufferPointer {
                ($0.baseAddress!.withMemoryRebound(to: UInt16.self, capacity: 1) { $0 })
                }.pointee
            voltage = Double(u16)
            
            return TLMData(temperature: temperature, voltage: voltage)
        }
    }
}

extension Int {
    var toRadians: Double { return Double(self) * Double.pi / 180 }
}



