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


/**
 * Basic representation of a beacon.
 */
public class Beacon {
    
    public enum `Type` : String{
        case IBEACON = "iBeacon"
        case EDDYSTONE = "Eddystone"
        case EDDYSTONE_TLM = "Eddystone_TLM"
        case NONE = "None"
    }
    
    /**
     * Number of data points to average over for RSSI smoothing
     */
    private static let WINDOW_SIZE = 20
    
    /**
     * Interval to wait between messages sent to the server, measured in number 
     * of data points received from the beacon
     */
    private static let SEND_INTERVAL = 3
    
    /**
     * Simulation variables and constants
     */
    private var x = 0
    private let minRssiRange = -119.0
    private let maxRssiRange = -0.1
    private var lastPoint: Double!
    private let amplitude: Double!
    private let numSteps = 10
    private var step = 0
    private var nextPoint: Double!
    static let MAJOR_NUM = "BEAC"
    
    var virtualizedBeacon: VirtualDevice?
    var endpointID: String?
    let identifier: String!
    let UUID: String!
    let address: String!
    let modelNumber: String!
    let serialNumber: String!
    var hardwareId: String!
    var rssis: [Int]!
    var lastRssi = 0
    var rssiCount: Int!
    let mssi: Int!
    var isRegistered: Bool = false
    let rawData: [UInt8]!
    var readyToSend: Bool = false
    var rssiRaw: Int!
    let type: Type!
    let isSimulated: Bool!
    // If restricted is true the beacon will be a controlled roaming device
    // bound to the Gateway that registers it
    var restricted: Bool = false
    
    init (identifier: String, UUID: String, serialNumber: String,
          address: String, modelNumber: String,
          mssi: Int, rawData: [UInt8], type: Type, isSimulated: Bool,
          isRestricted: Bool){
        self.address = address
        self.UUID = UUID
        self.identifier = identifier
        self.serialNumber = serialNumber
        self.rssis = [Int](repeating: 0, count: Beacon.WINDOW_SIZE)
        self.rssiCount = 0
        self.modelNumber = modelNumber
        self.mssi = mssi
        self.rawData = rawData
        self.type = type
        self.isSimulated = isSimulated
        self.hardwareId = ""
        
        self.lastPoint = self.minRssiRange * 0.30
        self.amplitude = self.minRssiRange * 0.10 + 1.0
        
        if (isSimulated) {
            self.x = abs(serialNumber.hashValue) % 360
            self.lastPoint = self.minRssiRange * (Double(self.x) + 1) / 360
        }
        
        self.restricted = isRestricted
    }
    
    public func getRssi() -> Int {
        if !self.isSimulated {
            self.lastRssi = smoothData(count: rssiCount, data: rssis)
            return self.lastRssi
        }
        else {
            self.lastRssi = simulateRSSI()
            return self.lastRssi
        }
    }
    
    private func smoothData(count: Int, data: [Int]) -> Int {
        var average: Int = 0
        
        let len = count < data.count ? count : data.count
        if (len < 1) {
            return 0
        }
        
        // Copy the data to include in the calculation
        let startIdx: Int = 0
        let stopIdx: Int = len
        
        var tempData:[Int] = Array(data[startIdx..<stopIdx])
        tempData.sort()
        
        // Calculate the mean of the middle 80% of data
        var sum = 0
        let discard = Int(round(0.1 * Double(len)))
        for i in discard..<(len-discard) {
            sum = sum + tempData[i]
        }
        
        average = Int(Double(sum) * 1.0/Double(len - 2 * discard))
        
        return average
    }
    
    private func simulateRSSI() -> Int {
        if self.step == 0 {
            let delta: Double = self.amplitude * sin(self.x.toRadians)
            x += 14
            var rssi: Double = self.lastPoint + delta
            
            if (rssi > self.maxRssiRange) || (rssi < self.minRssiRange) {
                rssi = self.lastPoint - delta
            }
            
            self.nextPoint = rssi
        }
        
        let incrementalRssi: Double = lastPoint + (self.nextPoint - self.lastPoint)/Double(self.numSteps) * (Double(self.step) + 1)
        
        if self.step == self.numSteps - 1 {
            self.step = 0
            self.lastPoint = self.nextPoint
        } else {
            self.step += 1
        }
        
        return Int(round(incrementalRssi))
    }
    
    public func setRssi(rssi: Int) {
        //Put the new rssi value at the next index in the array
        self.rssis[rssiCount % Beacon.WINDOW_SIZE] = rssi
        rssiCount = rssiCount + 1
        
        if (rssiCount == Int.max) {
            rssiCount = Int.max % Beacon.WINDOW_SIZE + Beacon.WINDOW_SIZE
        }
        
        //Determine if it is time to send out the average rssi value yet
        if (rssiCount % Beacon.SEND_INTERVAL == Beacon.SEND_INTERVAL - 1) {
            readyToSend = true
        }
        self.rssiRaw = rssi
    }
    
    
    public func getDistance() -> Double {
        if self.lastRssi != 0 {
            return calculateDistance(rssi: self.lastRssi)
        }
        
        return calculateDistance(rssi: self.getRssi())
    }
    
    func calculateDistance(rssi: Int) -> Double {
        if rssi == 0 {
            return -1.0 // cant determine
        }
        
        let ratio = Double(rssi) / Double(self.mssi)
        
        if ratio < 1.0 {
            return pow(ratio, 10.0)
        }
        else {
            // 0.89976, 7.7095 and 0.111 are the three constants calculated when
            // solving for a best fit curve to our measured data points
            let accuracy = 0.89976 * pow(ratio, 7.7095) + 0.111
            return accuracy
        }
    }
    
    static func getString(data: [UInt8]) -> String{
        var sb = ""
        for i in 0..<data.count {
            let d = data[i] & 0xFF
            if d < 0x10 {
                sb += "0" + String(format: "%X", d)
            }
            else{
                sb += String(format: "%2X", d)
            }
        }
        return sb
    }
    
    public func isReadyToSend() -> Bool {
        if self.isSimulated == true {
            return true
        }
        
        if self.readyToSend == true {
            self.readyToSend = false
            return true
        }
        return false
    }
    
    public func getMssi() -> Int {
        return self.mssi
    }
    
    public func getEndpointID() -> String? {
        return self.endpointID
    }
    
    public func getVirtualizedBeacon() -> VirtualDevice? {
        return self.virtualizedBeacon
    }
    
    public func setEndpointID(endpointID: String) {
        self.endpointID = endpointID
    }
    
    public func setVirtualizedBeacon(virtualizedBeacon: VirtualDevice) {
        self.virtualizedBeacon = virtualizedBeacon
    }
    
    public func getType() -> Type {
        return self.type
    }
    
    public func getIdentifier() -> String {
        return self.identifier
    }
    
    public func getUUID() -> String {
        return self.UUID
    }
    
    public func getAddress() -> String {
        return self.address
    }
    
    public func getModelNumber() -> String {
        return self.modelNumber
    }
    
    public func getSerialNumber() -> String {
        return self.serialNumber
    }
    
    public func getManufacturer() -> String {
        return "Estimote"
    }
    
    public func getDeviceClass() -> String {
        return "LE"
    }
    
    public func getProtocol() -> String {
        return "Bluetooth-LE"
    }
    
    public func getHardwareId() -> String {
        return self.hardwareId
    }
    
    public static func sort(lhs: Beacon, rhs: Beacon) -> Bool {
        return lhs.getDistance() < rhs.getDistance()        
    }
    
    public func isRestricted() -> Bool {
        return self.restricted
    }
}

