/*
 * Copyright (c) 2016 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.
 */

package com.oracle.iot.sample;

import java.util.Arrays;
import java.util.Locale;


import oracle.iot.client.device.VirtualDevice;

/**
 * Basic representation of a beacon.
 */
public class Beacon {
    public enum Type {
        IBEACON("iBeacon"),
        EDDYSTONE("Eddystone"),
        EDDYSTONE_TLM("Eddystone_TLM"),
        NONE("None");

        final String alias;
        Type(String alias) {
            this.alias = alias;
        }

        public String alias() {
            return this.alias;
        }
    }

    private static final byte[] IBEACON_PREAMBLE = {(byte)0x02,(byte)0x01,(byte)0x06,(byte)0x1A,
            (byte)0xFF,(byte)0x4C,(byte)0x00,(byte)0x02,(byte)0x15};
    private static final byte[] EDDYSTONE_SERVICE_UID = {(byte)0xAA,(byte)0xFE};
    private static final byte EDDYSTONE_UID_FRAME = (byte)0x00;
    private static final byte EDDYSTONE_TLM_FRAME = (byte)0x20;

    /**
     * Number of data points to average over for RSSI smoothing
     */
    private static final int WINDOW_SIZE = 20;

    /**
     * Interval to wait between messages sent to the server, measured in number of data points
     * received from the beacon
     */
    private static final int SEND_INTERVAL = 10;

    /**
     * Simulation variables and constants
     */
    private int x = 0;
    private final double minRssiRange = -119.0;
    private final double maxRssiRange = -0.1;
    private double lastPoint = minRssiRange * .30f;
    private final double amplitude = minRssiRange * .10f + 1f;
    private final int numSteps = 10;
    private int step = 0;
    private double nextPoint;
    static final String MAJOR_NUM = "BEAC";

    VirtualDevice virtualizedBeacon;
    final String identifier;
    final String UUID;
    final String address;
    final String modelNumber;
    final String serialNumber;
    int[] rssis;
    int lastRssi = 0;
    int rssiCount;
    final int mssi;
    boolean isRegistered = false;
    final byte[] rawData;
    boolean readyToSend = false;
    int rssiRaw;
    final Type type;
    final boolean isSimulated;

    public VirtualDevice getVirtualizedBeacon() {
        return virtualizedBeacon;
    }

    public void setVirtualizedBeacon(VirtualDevice virtualizedBeacon) {
        this.virtualizedBeacon = virtualizedBeacon;
    }

    public Type getType() {
        return type;
    }

    public String getIdentifier() {
        return identifier;
    }

    public String getUUID() {
        return UUID;
    }

    public String getAddress() {
        return address;
    }

    public String getModelNumber() {
        return modelNumber;
    }

    public String getSerialNumber() {
        return serialNumber;
    }

    public int getRssi() {
        if (!isSimulated) {
            lastRssi = smoothData(rssiCount, rssis);
            return lastRssi;
        } else {
            lastRssi = simulateRSSI();
            return lastRssi;
        }
    }

    private int smoothData(int count, int[] data) {
        int average;
        int len = count<data.length?count:data.length;
        if (len < 1)
            return 0;

        //copy the data to include in the calculation
        int[] tempData = Arrays.copyOfRange(data, 0, len);
        Arrays.sort(tempData);

        //calculate the mean of the middle 80% of data
        int sum = 0;
        int discard = (int) Math.round(0.1*len);
        for (int i=discard; i<len-discard; i++) {
            sum += tempData[i];
        }
        average = (int)(sum*1.0/(len-2*discard));
        return average;
    }

    private synchronized int simulateRSSI() {
        if (step == 0) {
            final double delta = amplitude * Math.sin(Math.toRadians(x));
            x += 14;
            double rssi = lastPoint + delta;

            if (rssi > maxRssiRange || rssi < minRssiRange)
                rssi = lastPoint - delta;

            nextPoint = rssi;
        }

        double incrementalRssi = lastPoint + (nextPoint - lastPoint)/numSteps*(step + 1);

        if (step == numSteps - 1) {
            step = 0;
            lastPoint = nextPoint;
        } else {
            step++;
        }

        return (int)Math.round(incrementalRssi);
    }

    public void setRssi(int rssi) {
        //Put the new rssi value at the next index in the array
        this.rssis[rssiCount++%WINDOW_SIZE] = rssi;
        if (rssiCount == Integer.MAX_VALUE) {
            rssiCount = Integer.MAX_VALUE%WINDOW_SIZE+WINDOW_SIZE;
        }
        //Determine if it is time to send out the average rssi value yet
        if (rssiCount%SEND_INTERVAL == SEND_INTERVAL-1) {
            readyToSend = true;
        }
        this.rssiRaw = rssi;
    }

    public boolean isReadyToSend() {
        if (isSimulated) {
            return true;
        }

        if (readyToSend) {
            readyToSend = false;
            return true;
        }
        return false;
    }

    public int getMssi() {
        return mssi;
    }

    static String getString(byte[] data){
        StringBuffer sb = new StringBuffer();
        for(int i = 0; i < data.length; i++){
            int d = data[i] & 0xFF;
            if(d < 0x10){
                sb.append("0" + Integer.toHexString(d));
            }else{
                sb.append("" + Integer.toHexString(d));
            }
        }
        return sb.toString();
    }

    public double getDistance() {
        if (lastRssi != 0) {
            return calculateDistance(lastRssi);
        }
        return calculateDistance(getRssi());
    }

    double calculateDistance(int rssi) {
        if (rssi == 0) {
            return -1.0; // cant determine
        }
        double ratio = rssi * 1.0 / mssi;
        if (ratio < 1.0) {
            return Math.pow(ratio, 10);
        } else {
            double accuracy = (0.89976) * Math.pow(ratio, 7.7095) + 0.111;
            // 0.89976, 7.7095 and 0.111 are the three constants calculated when solving for a best fit curve to our measured data points.
            return accuracy;
        }
    }

    public Beacon (String identifier, String UUID, String serialNumber, String address,
                   String modelNumber, int mssi, byte[] rawData, Type type, boolean isSimulated){
        this.address = address;
        this.UUID = UUID;
        this.identifier = identifier;
        this.serialNumber = serialNumber;
        this.rssis = new int[WINDOW_SIZE];
        this.rssiCount = 0;
        this.modelNumber = modelNumber;
        this.mssi = mssi;
        this.rawData = rawData;
        this.type = type;
        this.isSimulated = isSimulated;

        if (isSimulated) {
            this.x = Math.abs(serialNumber.hashCode())%360;
            this.lastPoint = minRssiRange * (x + 1) / 360;
        }
    }

    /**
     * Parses the raw data from a ScanRecord data packet to determine the beacon specification.
     * @param rawBytes the bytes from a ScanRecord to parse
     * @return Type.EDDYSTONE for an Eddystone-UID packet
     *         Type.EDDYSTONE_TLM for an Eddystone-TLM packet
     *         Type.IBEACON for an iBeacon packet
     *         Type.NONE for any other type
     */
    public static Type getPacketType(byte[] rawBytes) {
        int len = rawBytes.length;
        if (len >= 30) {
            int i;
            for (i = 0; i < IBEACON_PREAMBLE.length; i++) {
                if (i == 2)
                    continue;
                if (rawBytes[i] != IBEACON_PREAMBLE[i])
                    break;
            }
            if (i == IBEACON_PREAMBLE.length)
                return Type.IBEACON;
        }
        for (int start = 0; start < len; start++) {
            if (len - start > 19) {
                int i;
                for (i = 0; i < EDDYSTONE_SERVICE_UID.length; i++) {
                    if (rawBytes[start + i] != EDDYSTONE_SERVICE_UID[i])
                        break;
                }
                if (i == EDDYSTONE_SERVICE_UID.length) {
                    if (rawBytes[start + i] == EDDYSTONE_UID_FRAME)
                        return Type.EDDYSTONE;
                    else if (rawBytes[start + i] == EDDYSTONE_TLM_FRAME)
                        return Type.EDDYSTONE_TLM;
                }
            }
        }
        return Type.NONE;
    }

    public static String parseSensorInfo(Beacon beacon){
        String info="";
        if(beacon.getType() == Beacon.Type.EDDYSTONE) {
            String[] uuid = beacon.getUUID().split(":");
            info += "<b>Namespace:</b> " + uuid[0] + "<br>";
            info += "<b>Instance:</b> " + beacon.getSerialNumber() + "<br>";
        }
        else {
            info += "<b>UUID:</b> " + beacon.getUUID() + "<br>";
            info += "<b>Major:</b> " + ((IBeacon)beacon).getMajorVersion() + ", <b>Minor:</b> " + ((IBeacon)beacon).getMinorVersion() + "<br>";
        }
        info += "<b>Signal Strength Measured:</b> " + beacon.getMssi() + ", ";
        if (beacon.lastRssi != 0) {
            info += "<b>Received:</b> " + beacon.lastRssi + "<br>";
        } else {
            info += "<b>Received:</b>  <br>";
        }
        info += String.format(Locale.ROOT, "<b>Distance:</b> %7.3f m<br>", beacon.getDistance());

        if(beacon.getType() == Beacon.Type.EDDYSTONE) {
            info += String.format(Locale.ROOT, "<b>Temperature:</b> %7.3f °C<br>", ((EddystoneBeacon)beacon).getTemperature() / 256.0);
            info += String.format(Locale.ROOT, "<b>Battery Voltage:</b> %7.3f V<br>", ((EddystoneBeacon)beacon).getVoltage() / 1000.0);
        }

        return info;
    }
}

