/*
 * Copyright (c) 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.
 */
package com.oracle.iot.sample.obd;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.location.Location;
import android.util.Log;
import com.oracle.iot.sample.GpsLocationListener;
import oracle.iot.client.device.Alert;
import oracle.iot.client.device.VirtualDevice;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;

public class ObdSensor {
    private final static String TAG = ObdSensor.class.getSimpleName();

    public String hardwareId;
    public ObdData obdData, prevObdData;
    public GpsLocationListener gpsLocationListener;
    public BluetoothCommManager bluetoothCommManager;
    public String alertMessages = "";
    VirtualDevice virtualDevice;
    BluetoothDevice obdDevice;
    ObdDeviceType obdDeviceType;
    Obd2ElmSensor elmSensor;
    Obd2FreematicsSensor freematicsSensor;
    ObdDeviceService.MessageIntf messageIntf;
    boolean useDevicePolicy;

    public interface IObdDeviceInterface {
        void obdDataReceived(String data);
        void obdErrorOccurred(String error);
    }

    public ObdSensor(VirtualDevice virtualDevice, String id, ObdDeviceService.MessageIntf messageIntf,
                     BluetoothDevice obdDevice, ObdDeviceType obdDeviceType, boolean useDevicePolicy) throws IOException, InterruptedException {
        hardwareId = id;
        this.virtualDevice = virtualDevice;
        obdData = new ObdData();
        prevObdData = new ObdData();
        this.gpsLocationListener = GpsLocationListener.getInstance();
        this.bluetoothCommManager = BluetoothCommManager.getInstance();
        this.obdDevice = obdDevice;
        this.messageIntf = messageIntf;
        this.obdDeviceType = obdDeviceType;
        this.useDevicePolicy = useDevicePolicy;
        switch (obdDeviceType) {
            case ELM:
                elmSensor = new Obd2ElmSensor();
                break;
            case FREEMATICS:
                freematicsSensor = new Obd2FreematicsSensor();
                break;
        }
    }

    public void discConnect() {
        switch (obdDeviceType) {
            case ELM:
                break;
            case FREEMATICS:
                freematicsSensor.disconnect();
                break;
        }
    }

    public String handleSpeedAlerts() {
        String alertMessages = "";
        Integer prevSpeedVal = prevObdData.getSpeed();
        Integer currentSpeedVal = obdData.getSpeed();
        boolean vehicleStarted = false;
        boolean vehicleStopped = false;

        if (currentSpeedVal == null) {
            return alertMessages;
        }

        if (prevSpeedVal == null ) {
            if (currentSpeedVal > 5) {
                vehicleStarted = true;
            } else {
                vehicleStopped = false;
            }
        } else {
            if (prevSpeedVal == 0 && currentSpeedVal > 0) {
                vehicleStarted = true;
            } else if (prevSpeedVal >0 && currentSpeedVal == 0){
                vehicleStopped = true;
            }
        }

        if (vehicleStarted) {
            alertMessages = "ALERT: Vehicle started.\n";
            messageIntf.displayStatus(alertMessages);
            Alert alert = virtualDevice.createAlert(ObdDeviceModelAttributes.VEHICLE_STARTED_ALERT_URN.toString());
            alert.set(ObdDeviceModelAttributes.VEHICLE_SPEED_ATTRIBUTE.toString(), currentSpeedVal);
            alert.raise();
        }
        if (vehicleStopped) {
            alertMessages = "ALERT: Vehicle stopped.\n";
            messageIntf.displayStatus(alertMessages);
            Alert alert = virtualDevice.createAlert(ObdDeviceModelAttributes.VEHICLE_STOPPED_ALERT_URN.toString());
            alert.raise();
        }
        return alertMessages;
    }

    public void updateObdData() throws IOException, InterruptedException {
        switch (obdDeviceType) {
            case FREEMATICS:
                break;
            case ELM:
                elmSensor.updateObdData();
                break;
        }
    }

    String setAttribute(ObdPID pid, Object currentVal, Class type) {
        String typename = type.getSimpleName();
        String attributeName = pid.getAttribute();
        // Convert the unit system of values from Metric to US units for 17.1.5
        String value = ObdPID.getFormattedValue(pid, currentVal);
        switch (typename) {
            case "Integer": {
                int val = Integer.parseInt(currentVal.toString());
                if (pid.isAttribute()) {
                    if (useDevicePolicy) {
                        virtualDevice.offer(attributeName, val);
                    } else {
                        virtualDevice.set(attributeName, val);
                    }
                }
                break;
            }
            case "Double": {
                double val = Double.parseDouble(currentVal.toString());
                if (pid.isAttribute()) {
                    if (useDevicePolicy) {
                        virtualDevice.offer(attributeName, val);
                    } else {
                        virtualDevice.set(attributeName, val);
                    }
                }
                break;
            }
            case "String": {
                String val = currentVal.toString();
                if (pid.isAttribute()) {
                    if (useDevicePolicy) {
                        virtualDevice.offer(attributeName, val);
                    } else {
                        virtualDevice.set(attributeName, val);
                    }
                }
            }
        }
        String pidAttr = pid.getDescription();
        String displayMsg = pidAttr + "," + value + "," + pid.getUnits() + "\n";
        return displayMsg;
    }


    synchronized public String processObdEvents() throws IOException, InterruptedException {
        String displayMsg = "";
        boolean isSpeedOrMAFChanged = false;
        // First get the latest OBD values from the OBD dongle.
        updateObdData();
        if (!useDevicePolicy) {
            handleSpeedAlerts();
        }
        virtualDevice.update();
        for (ObdPID pid: ObdPID.values()) {
            Object currentVal = obdData.getCurrentPidValue(pid);
            if (currentVal == null) continue;
            Object prevVal = prevObdData.getCurrentPidValue(pid);
            if (!pid.isAttribute()) continue;
            if (prevVal == null) {
                displayMsg += setAttribute(pid,currentVal,pid.getType());
            } else if (!prevVal.equals(currentVal)) {
                if (pid == ObdPID.SPEED || pid == ObdPID.MASS_AIRFLOW_RATE) {
                    isSpeedOrMAFChanged = true;
                } else {
                    displayMsg += setAttribute(pid, currentVal, pid.getType());
                }
            }
        }
        // For average fuel economy calculation in server, both Speed and MAF needs to be set togther in message.
        if (isSpeedOrMAFChanged) {
            Object currentSpeed = obdData.getCurrentPidValue(ObdPID.SPEED);
            displayMsg += setAttribute(ObdPID.SPEED, currentSpeed, ObdPID.SPEED.getType() );

            Object currentMAF = obdData.getCurrentPidValue(ObdPID.MASS_AIRFLOW_RATE);
            displayMsg += setAttribute(ObdPID.MASS_AIRFLOW_RATE, currentMAF, ObdPID.MASS_AIRFLOW_RATE.getType());
        }
        if (!displayMsg.trim().isEmpty()) {
            messageIntf.displayMessage(displayMsg);
        }
        virtualDevice.finish();
        prevObdData = obdData.copyData();
        return displayMsg;
    }

    public class Obd2FreematicsSensor implements IObdDeviceInterface {

        private String hardwareId;
        String currentObdData = "";
        String lastObdData = "";
        private static final String delim = "\r\n\r\n";

        public Obd2FreematicsSensor() throws IOException, InterruptedException {
            messageIntf.displayStatus("INFO: Connecting with Freematics Sensor... \n");
            boolean isConnected = bluetoothCommManager.connect(obdDevice, this);
            if (isConnected) {
                messageIntf.displayStatus("INFO: Connected. Waiting for OBD data \n");
            } else {
                messageIntf.displayError("ERROR: Connection to Freematics sensor failed \n");
            }
        }

        public void disconnect() {
            bluetoothCommManager.disconnect();
        }

        @Override
        synchronized public void obdDataReceived(String data) {
            updateObdData(data);
        }

        public boolean updateObdData(String receivedData) {
            boolean fullPacketReceived = false;
            if (receivedData != null) {
                if (receivedData.length()==1 && receivedData.charAt(0)==1) {
                    return false;
                }
                currentObdData = currentObdData + receivedData;
                if ( currentObdData.contains(delim) || currentObdData.contains("bytes") ) {
                    int index = -1;
                    if (currentObdData.contains("bytes")) {
                        index = currentObdData.indexOf("bytes");
                    } else {
                        index = currentObdData.indexOf(delim);
                    }
                    if (index < 0) {
                        return false;
                    }
                    //lastObdData = currentObdData + receivedData;
                    lastObdData = currentObdData.substring(0, index);
                    Log.v(TAG, lastObdData);
                    messageIntf.displayStatus("INFO: Receiving OBD Data.. \n");
                    parseObdData(lastObdData);
                    updateLocationData();
                    if (currentObdData.length() > index + 4) {
                        currentObdData = currentObdData.substring(index + 4);
                    } else {
                        // start the next OBD update from device.
                        currentObdData = "";
                    }
                    fullPacketReceived = true;
                }
            }

            return fullPacketReceived;
        }

        private void updateLocationData() {
            Location currentLocation = gpsLocationListener.getCurrentLocation();
            if (currentLocation != null) {
                obdData.currentObdDataMap.put(ObdPID.SPEED.LATITUDE, currentLocation.getLatitude());
                obdData.currentObdDataMap.put(ObdPID.SPEED.LONGITUDE, currentLocation.getLongitude());
                obdData.currentObdDataMap.put(ObdPID.SPEED.ALTITUDE, currentLocation.getAltitude());
            }
        }

        private int economyAverageCount = 0;
        private Double totalFuelEconomyValue = 0.0;

        void parseObdData(String data) {
            String lines[] = data.split("\\r\\n");
            for (String line: lines) {
                if (!line.contains(","))  continue;
                String values[] = line.split(",");
                if (values == null || values.length < 2 ) continue;
                String cmd = values[0];
                String value = values[1];
                ObdPID obdCommand = ObdPID.getCommand(cmd);
                if (obdCommand == null) continue;
                Object objValue = null;
                switch (obdCommand) {
                    case NUMBER_OF_DTC: {
                        Integer integerVal = obdData.getIntegerVal(value);
                        if (integerVal != null) {
                            int val = integerVal;
                            int nDTC = (val & 0x7F);
                            integerVal = nDTC;
                        }
                        objValue = integerVal;
                        break;
                    }
                    case SPEED:
                    case RUNTIME: {
                        Integer integerVal = obdData.getIntegerVal(value);
                        if (integerVal != null) {
                            objValue = integerVal;
                        }
                        break;
                    }
                    case RPM: {
                        Integer intVal = obdData.getIntegerVal(value);
                        if (intVal != null) {
                            objValue = new Double(intVal);
                        }
                        break;
                    }
                    case ENGINE_COOLANT_TEMP: {
                        Integer intVal = obdData.getIntegerVal(value);
                        if (intVal != null) {
                            objValue = intVal;
                        }
                        break;
                    }
                    case MASS_AIRFLOW_RATE: {
                        if (value.contains(".")) {
                            objValue = obdData.getDoubleVal(value);
                        } else {
                            Integer intVal = obdData.getIntegerVal(value);
                            if (intVal != null) {
                                objValue = intVal.doubleValue();
                            }
                        }
                        break;
                    }
                    case DISTANCE_DTC: {
                        Integer distance = obdData.getIntegerVal(value);
                        distance = Math.abs(distance);
                        if (distance != null) {
                            objValue = distance;
                        }
                        break;
                    }
                    case THROTTLE: {
                        Integer throttleVal = obdData.getIntegerVal(value);
                        if (throttleVal != null) {
                            throttleVal++;
                            objValue = new Double(throttleVal);
                        }
                        break;
                    }
                    case LATITUDE:
                        break;
                    case LONGITUDE:
                        break;
                    case ALTITUDE:
                        break;
                }
                obdData.currentObdDataMap.put(obdCommand, objValue);
            }
        }

        @Override
        public void obdErrorOccurred(String error) {
            //System.out.println("error:" + error);
            messageIntf.displayError(error);
        }
    }

    /**
     * A simulated humidity sensor for use in the samples. The sensor has
     * humidity and maximum threshold attributes.
     */
    public class Obd2ElmSensor  {

        BluetoothSocket sock;
        boolean btInitiaized = false;
        InputStream in;
        OutputStream out;
        VirtualDevice virtualDevice;
        BluetoothDevice obdDevice;

        public Obd2ElmSensor() throws IOException, InterruptedException {
            this.obdDevice = obdDevice;
            sock = bluetoothCommManager.connectBtSerial(obdDevice);
            btInitiaized = (sock != null);
            if (btInitiaized) {
                in = sock.getInputStream();
                out = sock.getOutputStream();
            }
        }

        public void updateObdData() throws IOException, InterruptedException {
            if (!btInitiaized) return;
        }
    }

    public static enum ObdDeviceType {
        FREEMATICS,
        ELM,
        XIRGO
    }

    class ObdData {
        String currentObdData = "";
        String lastObdData = "";

        HashMap<ObdPID,Object> currentObdDataMap = new HashMap<ObdPID, Object>();

        public Object getCurrentPidValue(ObdPID pid) {
            return currentObdDataMap.get(pid);
        }

        private static final String delim = "\r\n\r\n";

        public ObdData copyData() {
            ObdData prevData = new ObdData();
            prevData.currentObdDataMap.putAll(currentObdDataMap);
            return prevData;
        }

        public  Integer getSpeed() {
            Integer speedVal = null;
            if ((currentObdDataMap.size() > 0) && currentObdDataMap.containsKey(ObdPID.SPEED)) {
                speedVal = (Integer) currentObdDataMap.get(ObdPID.SPEED);
            }
            return speedVal;
        }

        public  Integer getNumberDtc() {
            Integer nDtc = null;
            if ((currentObdDataMap.size() > 0) && currentObdDataMap.containsKey(ObdPID.NUMBER_OF_DTC)) {
                nDtc = (Integer) currentObdDataMap.get(ObdPID.NUMBER_OF_DTC);
            }
            return nDtc;
        }

        public Integer getIntegerVal(String value) {
            Integer integerVal = null;
            try {
                integerVal = Integer.parseInt(value);
            } catch (Exception ex) {
              integerVal = null;
            }
            return integerVal;
        }

        public Double getDoubleVal(String value) {
            Double doubleVal = null;
            try {
                doubleVal = Double.parseDouble(value);
            } catch (Exception ex) {
                doubleVal = null;
            }
            return doubleVal;
        }

    }
}
