/*
 * 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.nio.ByteBuffer;

/**
 * Representation of an Eddystone Beacon.
 */
public class EddystoneBeacon extends Beacon{
    private static final int UUID_POSITION = 2;
    private static final int MSSI_BYTE_POSITION = 1;
    private static final int INSTANCE_POSITION = 12;
    private static final byte[] EDDYSTONE_UID_PREAMBLE = {(byte)0xAA,(byte)0xFE,(byte)0x00};
    private static final byte[] EDDYSTONE_TLM_PREAMBLE = {(byte)0xAA,(byte)0xFE,(byte)0x20};

    /**
     * Simulation variables
     */
    private int xVolt = 0;
    private final double minVoltRange = 0.1;
    private final double maxVoltRange = 3000.0;
    private double lastVoltPoint = maxVoltRange;
    private final double amplitudeVolt = .10f;

    private int xTemp = 0;
    private final double minTempRange = 0.0;
    private final double maxTempRange = 2560.0;
    private double lastTempPoint = minTempRange * .30f;
    private final double amplitudeTemp = maxTempRange * .10f + 1f;

    TLMData tlm;

    private EddystoneBeacon(String identifier, String UUID, String serialNumber, String address,
                            String modelNumber, int mssi, byte[] rawData, boolean isSimulated) {
        super(identifier, UUID, serialNumber, address, modelNumber, mssi, rawData, Type.EDDYSTONE, isSimulated);
        this.tlm = new TLMData(-128*256, 0);

        if (isSimulated) {
            this.xTemp = Math.abs(serialNumber.hashCode())%360;
            this.lastTempPoint = maxTempRange * (xTemp + 1) / 360;
            this.xVolt = Math.abs(serialNumber.hashCode())%360;
        }
    }

    public double getTemperature() {
        if (!isSimulated) {
            return tlm.temperature;
        } else {
            return getSimulatedTemperature();
        }
    }

    public double getVoltage() {
        if (!isSimulated) {
            return tlm.voltage;
        } else {
            return getSimulatedVoltage();
        }
    }

    private double getSimulatedTemperature() {
        final double delta = amplitudeTemp * Math.sin(Math.toRadians(xTemp));
        xTemp += 14;
        double temp = lastTempPoint + delta;

        if (temp > maxTempRange || temp < minTempRange)
            temp = lastTempPoint - delta;

        lastTempPoint = temp;

        return Math.round(temp);
    }

    private double getSimulatedVoltage() {
        final double delta = Math.abs(amplitudeVolt * Math.sin(Math.toRadians(xVolt)));
        xVolt += 14;
        double volt = lastVoltPoint - delta;

        if (volt < minVoltRange)
            volt = minVoltRange;

        lastVoltPoint = volt;

        return Math.round(volt);
    }

    public void setTLM(byte[] packet) {
        TLMData newTlm = TLMData.parseTLM(packet);
        if (newTlm != null)
            tlm = newTlm;
    }

    public static EddystoneBeacon getInstance(byte packet[], String address, String name){
        int len = packet.length;
        int start;
        //Find the start of the Eddystone-UID frame in the packet data
        for (start = 0; start < len; start++) {
            if (len - start > 19) {
                int i = 0;
                for (i = 0; i < EDDYSTONE_UID_PREAMBLE.length; i++) {
                    if (packet[start + i] != EDDYSTONE_UID_PREAMBLE[i])
                        break;
                }
                if (i == EDDYSTONE_UID_PREAMBLE.length)
                    break;
            }
        }
        start+=2;

        byte[] ns = new byte[10];
        byte[] ins = new byte[6];
        //Convert mssi from 0m away to 1m away
        int mssi = packet[start + MSSI_BYTE_POSITION] - 41;
        System.arraycopy(packet, start + UUID_POSITION, ns, 0, 10);
        System.arraycopy(packet, start + INSTANCE_POSITION, ins, 0, 6);
        String namespace = getString(ns);
        String instance = getString(ins);
        String UUID = namespace+":"+instance;
        String identifier = UUID;
        String serialNumber = instance;
        byte[] rawData = packet.clone();
        String modelNumber = name!=null?name:"none";
        return new EddystoneBeacon(identifier, UUID, serialNumber, address,
                modelNumber, mssi, rawData, false);
    }

    public static EddystoneBeacon getSimulatedInstance(byte minorNum, String beaconUUID){
        byte[] minor = new byte[2];
        minor[1] = minorNum;
        int mssi = -67;
        String UUID = beaconUUID;
        String majorVersion = MAJOR_NUM;
        String minorVersion = getString(minor);
        String identifier = UUID + ":" + majorVersion + ":" +  minorVersion;
        String serialNumber = majorVersion + ":" + minorVersion;
        byte[] rawData = new byte[1];
        String modelNumber = "Sample Eddystone";
        String address = "Simulated Bluetooth Device";

        return new EddystoneBeacon(identifier, UUID, serialNumber, address,
                modelNumber, mssi, rawData, true);
    }

    @Override
    public String toString(){
        StringBuffer sb = new StringBuffer();
        sb.append("Identifier: " + identifier + "\n");
        sb.append("UUID: " + UUID + "\n");
        sb.append("Model Number: " + modelNumber + "\n");
        sb.append("Serial Number: " + serialNumber + "\n");
        sb.append("Address: " + address + "\n");
        sb.append("MSSI: " + mssi + "\n");
        if (!isSimulated) {
            sb.append("RSSI: " + getRssi() + " (" + rssiRaw + ")\n");
            sb.append("Distance: " + getDistance() + " m (" + calculateDistance(rssiRaw) + " m)\n");
        } else {
            sb.append("RSSI: " + getRssi() + "\n");
            sb.append("Distance: " + getDistance() + "\n");
        }
        sb.append("Temperature: " + getTemperature()/256.0 + " deg C\n");
        sb.append("Battery Voltage: " + getVoltage()/1000.0 + " V\n");
        return sb.toString();
    }

    /**
     * Representation of telemetry data for an Eddystone-TLM packet.
     */
    static class TLMData {
        private static final byte VOLTAGE_OFFSET = 2;
        private static final byte TEMPERATURE_OFFSET = 4;

        /**
         *  Temperature measured in degrees Celsius, expressed in signed 8.8 fixed-point notation.
         */
        double temperature;

        /**
         * Battery charge in millivolts, expressed as 1 mV per bit.
         */
        double voltage;

        TLMData(double temperature, double voltage) {
            this.temperature = temperature;
            this.voltage = voltage;
        }

        /**
         * Parses an Eddystone-TLM frame to get the temperature and battery voltage data.
         * @param packet the ScanRecord containing an Eddystone-TLM frame
         * @return a new TLMData containing temperature and batter voltage values, or
         *         null if the packet does not contain a proper Eddystone-TLM frame.
         */
        public static TLMData parseTLM(byte[] packet) {
            double temperature;
            double voltage;
            int len = packet.length;
            int start;

            //Find the start of the Eddystone-TLM frame in the packet data
            for (start = 0; start < len; start++) {
                if (len - start > 19) {
                    int i;
                    for (i = 0; i < EDDYSTONE_TLM_PREAMBLE.length; i++) {
                        if (packet[start + i] != EDDYSTONE_TLM_PREAMBLE[i])
                            break;
                    }
                    if (i == EDDYSTONE_TLM_PREAMBLE.length)
                        break;
                } else
                    return null;
            }
            start+=2;

            temperature = ByteBuffer.wrap(packet, start+TEMPERATURE_OFFSET, 2).getShort();
            voltage = ByteBuffer.wrap(packet, start+VOLTAGE_OFFSET, 2).getShort();

            return new TLMData(temperature, voltage);
        }
    }
}

