/*
 * Copyright (c) 2015,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.ext;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Toast;
import java.io.FileOutputStream;
import java.io.IOException;

import com.oracle.iot.client.device.GatewayDevice;
import com.oracle.iot.client.device.util.MessageDispatcher;
import com.oracle.iot.client.device.util.RequestDispatcher;
import com.oracle.iot.client.device.util.RequestHandler;
import com.oracle.iot.client.message.AlertMessage;
import com.oracle.iot.client.message.DataMessage;
import com.oracle.iot.client.message.Message;
import com.oracle.iot.client.message.RequestMessage;
import com.oracle.iot.client.message.Resource;
import com.oracle.iot.client.message.ResourceMessage;
import com.oracle.iot.client.message.ResponseMessage;
import com.oracle.iot.client.message.StatusCode;
import com.oracle.iot.sample.R;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;

/**
 * GatewayDeviceSampleService simulates a gateway that presents mulitple simple sensors
 * to the IoT server.
 * It uses the MessageDispatcher utility to send messages and to handle
 * resource requests from the server.
 */
public class GatewayDeviceSampleService extends Service {
    private static final String ERROR_TAG = "IOT_ERROR";
    private static final String MSG_TAG = "IOT";

    private static final String HUMIDITY_SENSOR_MODEL_URN =
        "urn:com:oracle:iot:device:humidity_sensor";
    private static final String HUMIDITY_ATTRIBUTE = "humidity";
    private static final String MAX_THRESHOLD_ATTRIBUTE = "maxThreshold";
    private static final String TOO_HUMID_ALERT =
        HUMIDITY_SENSOR_MODEL_URN + ":too_humid";

    private static final String TEMPERATURE_SENSOR_MODEL_URN =
        "urn:com:oracle:iot:device:temperature_sensor";
    private static final String TEMPERATURE_ATTRIBUTE = "temp";
    private static final String MIN_THRESHOLD_ATTRIBUTE = "minThreshold";
    private static final String MIN_TEMP_ATTRIBUTE = "minTemp";
    private static final String MAX_TEMP_ATTRIBUTE = "maxTemp";
    private static final String TOO_COLD_ALERT =
        TEMPERATURE_SENSOR_MODEL_URN + ":too_cold";
    private static final String TOO_HOT_ALERT =
        TEMPERATURE_SENSOR_MODEL_URN + ":too_hot";

    private static final boolean printDelivered = false;

    private static GatewayDevice gatewayDevice;
    private Context context;

    private int maxMaxThermoThreshold = 70;
    private int minMaxThermoThreshold = 55;
    private int maxMaxHygroThreshold = 95;
    private int minMaxHygroThreshold = 80;

    private int rememberHygroMaxThreshold = 0;
    private int rememberThermoMaxThreshold = 0;

    private boolean messageSent = false;
    private String messageToBeSent = "";
    private boolean stopped = false;

    private static TemperatureSensor temperatureSensor;

    private static HumiditySensor humiditySensor;

    boolean useDevicePolicy = false;

    /**
     * This is a list of callbacks that have been registered with the
     * service.  Note that this is package scoped (instead of private) so
     * that it can be accessed more efficiently from inner classes.
     */
    final RemoteCallbackList<IGatewayDeviceSampleServiceCallback> mCallbacks
            = new RemoteCallbackList<IGatewayDeviceSampleServiceCallback>();

    NotificationManager mNM;
    PowerManager.WakeLock wakeLock;


    @Override
    public void onCreate() {
        mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
        // Display a notification about us starting.
        showNotification(true);
    }

    @Override
    public IBinder onBind(Intent intent) {
        // Select the interface to return.
        if (IGatewayDeviceSampleService.class.getName().equals(intent.getAction())) {
            return mBinder;
        }
        if (IGatewayDeviceSampleServiceSecondary.class.getName().equals(intent.getAction())) {
            return mSecondaryBinder;
        }
        return null;
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                "IoTWakeLock");
        // power management: Keep the CPU on so we can continue to send messages back to the server
        wakeLock.acquire();
        stopped = false;
        if(rememberHygroMaxThreshold != 0) {
            humiditySensor.setMaxThreshold(rememberHygroMaxThreshold);
        }
        if(rememberThermoMaxThreshold != 0) {
            temperatureSensor.setMaxThreshold(rememberThermoMaxThreshold);
        }
        context = this;
        Toast.makeText(this, "Service started.", Toast.LENGTH_LONG).show();
        new Thread() {
            @Override
            public void run() {
                runSample();
            }
        }.start();

        return START_NOT_STICKY;
    }

    /**
     * The IGatewayDeviceSampleService Interface is defined through IDL
     */
    private final IGatewayDeviceSampleService.Stub mBinder =
            new IGatewayDeviceSampleService.Stub() {
        public void registerCallback(IGatewayDeviceSampleServiceCallback cb) {
            if (cb != null) mCallbacks.register(cb);
        }
        public void unregisterCallback(IGatewayDeviceSampleServiceCallback cb) {
            if (cb != null) mCallbacks.unregister(cb);
        }
    };
    /**
     * A secondary GD Sample Service interface to the service.
     */
    private final IGatewayDeviceSampleServiceSecondary.Stub mSecondaryBinder =
            new IGatewayDeviceSampleServiceSecondary.Stub() {
                public int getPid() {
                    return Process.myPid();
                }
                public String getMessage() {
                    messageSent = true;
                    return messageToBeSent;
                }
                public int getHygroMaxThreshold() {
                    return humiditySensor.getMaxThreshold();
                }
                public void increaseHygroMaxThreshold() {
                    int mt = humiditySensor.getMaxThreshold() + 1;
                    humiditySensor.setMaxThreshold(mt);
                    if(mt >= maxMaxHygroThreshold) {
                        display(getResources().getText(R.string.max_hygro_reached).toString());
                    }
                }
                public void decreaseHygroMaxThreshold() {
                    int mt = humiditySensor.getMaxThreshold() - 1;
                    humiditySensor.setMaxThreshold(mt);
                    if(mt <= minMaxHygroThreshold) {
                        display(getResources().getText(R.string.min_hygro_reached).toString());
                    }
                }
                public int getThermoMaxThreshold() {
                    return temperatureSensor.getMaxThreshold();
                }
                public void increaseThermoMaxThreshold() {
                    int mt = temperatureSensor.getMaxThreshold() + 1;
                    temperatureSensor.setMaxThreshold(mt);
                    if(mt >= maxMaxThermoThreshold) {
                        display(getResources().getText(R.string.max_thermo_reached).toString());
                    }
                }
                public void decreaseThermoMaxThreshold() {
                    int mt = temperatureSensor.getMaxThreshold() - 1;
                    temperatureSensor.setMaxThreshold(mt);
                    if(mt <= minMaxThermoThreshold) {
                        display(getResources().getText(R.string.min_thermo_reached).toString());
                    }
                }
                public void kill() {
                    // not used
                }
            };

    @Override
    public void onTaskRemoved(Intent rootIntent) {
        Toast.makeText(this, "Task removed: " + rootIntent, Toast.LENGTH_LONG).show();
    }

    private static final int REPORT_MSG = 1;
    private static final int GD_MAX_HYGRO_MSG = 4;
    private static final int GD_MAX_THERMO_MSG = 5;
    private static final int ERROR_MSG = 6;

    /**
     * Our Handler used to execute operations on the main thread.  This is used
     * to schedule increments of our value.
     */
    private final Handler mHandler = new Handler() {
        @Override public void handleMessage(android.os.Message msg) {
            switch (msg.what) {

                case REPORT_MSG: {
                    // Broadcast to all clients the new value.
                    final int N = mCallbacks.beginBroadcast();
                    for (int i=0; i<N; i++) {
                        try {
                            mCallbacks.getBroadcastItem(i).valueChanged(messageToBeSent);
                        } catch (RemoteException e) {
                            // The RemoteCallbackList will take care of removing
                            // the dead object for us.
                        }
                    }
                    mCallbacks.finishBroadcast();

                } break;
                case GD_MAX_HYGRO_MSG: {
                    // Broadcast to all clients the max threshold value.
                    final int N = mCallbacks.beginBroadcast();
                    for (int i=0; i<N; i++) {
                        try {
                            mCallbacks.getBroadcastItem(i).maxGdHygro(humiditySensor.getMaxThreshold() + "");
                        } catch (RemoteException e) {
                            // The RemoteCallbackList will take care of removing
                            // the dead object for us.
                        }
                    }
                    mCallbacks.finishBroadcast();

                } break;
                case GD_MAX_THERMO_MSG: {
                    // Broadcast to all clients the max threshold value.
                    final int N = mCallbacks.beginBroadcast();
                    for (int i=0; i<N; i++) {
                        try {
                            mCallbacks.getBroadcastItem(i).maxGdThermo(temperatureSensor.getMaxThreshold() + ":"+temperatureSensor.getMinThreshold());
                        } catch (RemoteException e) {
                            // The RemoteCallbackList will take care of removing
                            // the dead object for us.
                        }
                    }
                    mCallbacks.finishBroadcast();

                } break;
                case ERROR_MSG: {
                    // Broadcast to all clients the new value.
                    final int N = mCallbacks.beginBroadcast();
                    for (int i = 0; i < N; i++) {
                        try {
                            mCallbacks.getBroadcastItem(i).errorMessage(messageToBeSent);
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                    }
                    mCallbacks.finishBroadcast();
                } break;
                default:
                    super.handleMessage(msg);
            }
        }
    };

    public void runSample() {
        try {
            // Create the Gateway instance
            try {
                SharedPreferences mSharedPref = getApplicationContext().getSharedPreferences(
                        getString(R.string.preference_file_key), Context.MODE_PRIVATE);
                useDevicePolicy = mSharedPref.getBoolean("useDevicePolicy", false);

                // Create the directly-connected device instance
                if (mSharedPref.getBoolean(getString(R.string.use_provided_bks), false)) {
                    Log.d("DCD_DEBUG", "Using existing BKS");
                    gatewayDevice = new GatewayDevice(context);
                } else {
                    SharedPreferences pref = getApplicationContext().getSharedPreferences(
                        getString(R.string.preference_file_key), Context.MODE_PRIVATE);
                    try (FileOutputStream fos = openFileOutput(getString(R.string.last_known), Context.MODE_PRIVATE)) {
                        fos.write(pref.getString(getString(R.string.ta_file_path), "").getBytes());
                    } catch (IOException ex) {
                        Log.d(ERROR_TAG, "Can not safe to internal storage");
                    }
                    Log.d("DCD_DEBUG", "Using BKS" + mSharedPref.getString(getString(R.string.ta_file_path),"") + "---" + mSharedPref.getString(getString(R.string.ta_password),"") + " --------");

                    gatewayDevice = new GatewayDevice(mSharedPref.getString(getString(R.string.ta_file_path),""), mSharedPref.getString(getString(R.string.ta_password),""));
                }
            } catch (GeneralSecurityException dse) {
                // Tell the user
                displayError(dse.getMessage());
            }

            if (!gatewayDevice.isActivated()) {
                // If the device has not been activated, connect to the server
                // using client-credentials and activate the client to
                // obtain the private key. The private key is then persisted.
                gatewayDevice.activate();
            }

            temperatureSensor = new TemperatureSensor(gatewayDevice.getEndpointId() + "_Sample_TS");
            humiditySensor = new HumiditySensor(gatewayDevice.getEndpointId() + "_Sample_HS");

            String temperatureSensorEndpointId = null;
            final String humiditySensorEndpointId;

            boolean isRegistered = false;

            Map<String,String> metaData = new HashMap<String, String>();
            metaData.put(
                    GatewayDevice.MANUFACTURER,
                    temperatureSensor.getManufacturer());
            metaData.put(
                    GatewayDevice.MODEL_NUMBER,
                    temperatureSensor.getModelNumber());
            metaData.put(
                    GatewayDevice.SERIAL_NUMBER,
                    temperatureSensor.getSerialNumber());

            temperatureSensorEndpointId =
                    gatewayDevice.registerDevice(
                            true, // Do not allow other Gateways to send message on behalf of this device
                            temperatureSensor.getHardwareId(),
                            metaData,
                            TEMPERATURE_SENSOR_MODEL_URN);
            isRegistered  = true;

            metaData = new HashMap<String, String>();
            metaData.put(
                    GatewayDevice.MANUFACTURER,
                    humiditySensor.getManufacturer());
            metaData.put(
                    GatewayDevice.MODEL_NUMBER,
                    humiditySensor.getModelNumber());
            metaData.put(
                    GatewayDevice.SERIAL_NUMBER,
                    humiditySensor.getSerialNumber());

            humiditySensorEndpointId =
                    gatewayDevice.registerDevice(
                            true, // Do not allow other Gateways to send message on behalf of this device
                            humiditySensor.getHardwareId(),
                            metaData,
                            HUMIDITY_SENSOR_MODEL_URN);

            // This sample uses the MessageDispatcher utility for queuing and
            // dispatching messages, and for invoking the resource request
            // handlers.
            final MessageDispatcher messageDispatcher =
                    MessageDispatcher.getMessageDispatcher(gatewayDevice);

            messageDispatcher.setOnError(new MessageDispatcher.ErrorCallback() {
                @Override
                public void failed(List<Message> messages,
                                   Exception exception) {
                    display(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString() + " : " +
                            gatewayDevice.getEndpointId() +
                            " : OnFailed : \"size\"=" + messages.size() +
                            "\"exception\"=" + exception.getMessage());
                }
            });

            messageDispatcher.setOnDelivery(
                    new MessageDispatcher.DeliveryCallback() {
                        @Override
                public void delivered(List<Message> messages) {
                    if (printDelivered) {
                        display(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString()+ " : " +
                                gatewayDevice.getEndpointId() +
                                " : OnDelivered : \"size\"=" + messages.size());
                    }
                }
            });

            // For each managed device
            // - register the device
            // - register its resources, only one per device in this example
            // - register a request handler resource requests. The device
            //   instance in this request handler in this example.
            //
            // If the device endpoint is not null, that means it has been
            // previously registered. Its resource has also been registered
            // and must only have its request handler registered with the
            // local dispatcher.
            //
            // If the device endpoint is null then it is an unregistered
            // device must be registered along with its resources.

            // Record the discovered device's endpoint id and metadata

            registerDeviceResources(messageDispatcher, temperatureSensor,
                 temperatureSensorEndpointId, "deviceModels/" +
                 TEMPERATURE_SENSOR_MODEL_URN + "/attributes",
                 MAX_THRESHOLD_ATTRIBUTE, MIN_THRESHOLD_ATTRIBUTE,
                 isRegistered);
            registerDeviceResources(messageDispatcher, temperatureSensor,
                temperatureSensorEndpointId, "deviceModels/" +
                TEMPERATURE_SENSOR_MODEL_URN + "/actions/reset", null, null,
                isRegistered);
            registerDeviceResources(messageDispatcher, temperatureSensor,
                    temperatureSensorEndpointId, "deviceModels/" +
                            TEMPERATURE_SENSOR_MODEL_URN + "/actions/power", null, null,
                    isRegistered);

            registerDeviceResources(messageDispatcher, humiditySensor,
                    humiditySensorEndpointId, "deviceModels/" +
                            HUMIDITY_SENSOR_MODEL_URN + "/attributes",
                    MAX_THRESHOLD_ATTRIBUTE, MIN_THRESHOLD_ATTRIBUTE,
                    isRegistered);

            final RequestDispatcher requestDispatcher =
                messageDispatcher.getRequestDispatcher();

            updateHumiditySensor(messageDispatcher, humiditySensorEndpointId,
                    humiditySensor, false);

            updateTemperatureSensor(messageDispatcher,
                    temperatureSensorEndpointId,
                    temperatureSensor
                    , false, false);

            // Only generate an alert when the threshold is crossed
            boolean humidityAlerted = false;
            boolean tempAlerted = false;
            boolean wasOff = false;
            if(rememberHygroMaxThreshold != 0) {
                humiditySensor.setMaxThreshold(rememberHygroMaxThreshold);
            }
            if(rememberThermoMaxThreshold != 0) {
                temperatureSensor.setMaxThreshold(rememberThermoMaxThreshold);
            }
            rememberHygroMaxThreshold = humiditySensor.getMaxThreshold();
            rememberThermoMaxThreshold = temperatureSensor.getMaxThreshold();
            //
            // Which sensor data is processed depends on whether or not policies are used.
            // Compare the code in the processSensorData method of the WithPolicies class
            // to that of the WithoutPolicies class.
            //
            final MainLogic mainLogic = useDevicePolicy
                    ? new WithDevicePolicies(messageDispatcher,
                    humiditySensor, humiditySensorEndpointId,
                    temperatureSensor, temperatureSensorEndpointId)
                    :  new WithoutDevicePolicies(messageDispatcher,
                    humiditySensor, humiditySensorEndpointId,
                    temperatureSensor, temperatureSensorEndpointId);

            while (!stopped) {
                try {
                    mainLogic.processSensorData();
                } catch(Exception e) {
                    e.printStackTrace();
                    if(e.getMessage() != null) {
                        // Tell the user
                        displayError(e.getMessage());
                    } else {
                        // Tell the user
                        displayError("Error. No message.");
                    }
                }

                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }

            }

        } catch (Exception e) {
            e.printStackTrace();
            if(e.getMessage() != null) {
                // Tell the user
                displayError(e.getMessage());
            } else {
                // Tell the user
                displayError("Error. No message.");
            }
        }

        System.exit(-1);
    }

    private static <T> T getData(String key, byte[] payload) {

        T data = null;
        try {
            String jsonPayload = new String(payload);
            JSONObject jsonObject = new JSONObject(jsonPayload);
            Object jsonValue = jsonObject.get(key);
            if (jsonValue == null) {
                data = null;
            } else if (jsonValue instanceof Boolean) {
                data = (T)((Boolean)jsonValue);
            } else if (jsonValue instanceof Number) {
                data = (T)((Number)jsonValue);
            } else if (jsonValue instanceof String) {
                data = (T)((String)jsonValue);
            } else {
                data = null;
                System.err.println("unexpected json type " +
                        jsonValue.getClass().getName());
            }
        } catch (JSONException e) {
            if(e.getMessage() != null) {
                Log.d(ERROR_TAG, e.getMessage());
            } else {
                Log.d(ERROR_TAG, "Error. No message.");
            }
        } catch (NullPointerException e) {
            // code assumes jsonObject.get(key) returns non-null
            if(e.getMessage() != null) {
                Log.d(ERROR_TAG, e.getMessage());
            } else {
                Log.d(ERROR_TAG, "Error. No message.");
            }
        } finally {
            return data;
        }
    }

    /**
     * Legacy convenience method to register device resources until
     * VirtualDevice is available.
     * @param device the managed device
     * @param isRegistered true if the device has been previously registered.
     * @throws IOException if sending resource message fails
     * @throws GeneralSecurityException if sending resource message fails
     */
    private void registerDeviceResources
            (final MessageDispatcher messageDispatcher,
             final Object device,
             final String sensorEndpointId,
             String path,
             final String maxThresholdAttribute,
             final String minThresholdAttribute,
             boolean isRegistered) throws IOException,
                                        GeneralSecurityException {

        final RequestDispatcher requestDispatcher =
                messageDispatcher.getRequestDispatcher();

        Resource resource = new Resource.Builder()
                .endpointName(sensorEndpointId)
                .method(Resource.Method.PUT)
                .path(path)
                .name(path)
                .build();

        if (!isRegistered) {
            // Only register the resource once, when the
            // device is seen for the first time
            ResourceMessage resourceMessage =
                    new ResourceMessage.Builder()
                            .endpointName(sensorEndpointId)
                            .source(gatewayDevice.getEndpointId())
                            .register(resource)
                            .build();
            // register the resource with the server
            messageDispatcher.queue(resourceMessage);
        }

        // Register the device as the request handler to the local
        // request dispatcher.
        requestDispatcher.registerRequestHandler(sensorEndpointId,
            resource.getPath(), new RequestHandler() {
                            @Override
                public ResponseMessage handleRequest(
                         RequestMessage requestMessage) {
                                StatusCode statusCode = null;
                                try {
                                    final ResponseMessage.Builder builder =
                                            new ResponseMessage.Builder(requestMessage);

                                    byte[] body = requestMessage.getBody();
                        String method =
                            requestMessage.getMethod().toUpperCase(Locale.ROOT);
                                    String url = requestMessage.getURL();
                                    boolean update = false;
                                    boolean alertMax;
                                    boolean alertMin = alertMax = false;

                                    if ("POST".equals(method)) {
                                        if (handlePost(requestMessage, device)) {
                                            statusCode = StatusCode.ACCEPTED;
                                            if (url.endsWith("/actions/reset")) {
                                                // updateTemperatureSensor ? then
                                                // update = true;
                                            } else if (url.endsWith("/attributes")) {
                                                update = alertMin = alertMax = true;
                                            }
                                        }
                        } else if ("PUT".equals(method) &&
                                   handlePut(requestMessage, device)) {
                                        statusCode = StatusCode.ACCEPTED;
                                        update = alertMin = alertMax = true;
                                    } else {
                                        statusCode = StatusCode.BAD_REQUEST;
                                    }

                                    if (update) {
                                        if (device instanceof TemperatureSensor) {
                                TemperatureSensor temperatureSensor =
                                    (TemperatureSensor)device;
                                updateTemperatureSensor(messageDispatcher,
                                    sensorEndpointId, temperatureSensor,
                                    alertMin, alertMax);
                                        } else {
                                HumiditySensor humiditySensor =
                                    (HumiditySensor)device;
                                updateHumiditySensor(messageDispatcher,
                                    sensorEndpointId, humiditySensor,
                                    alertMax);
                                        }
                                    }

                                    return new ResponseMessage.Builder(requestMessage)
                                            .statusCode(statusCode)
                                            .build();
                                } catch (Exception e) {
                                    e.printStackTrace();
                                    return null;
                                }
                            }
            });
    }

    private static Integer getIntegerValue(String jsonRequestBody,
            JSONObject jsonObject, String paramName)
            throws UnsupportedEncodingException {
        Number jsonNumber = (Number)jsonObject.opt(paramName);
        Integer value = null;
        if (jsonNumber != null) {
            try {
                value = jsonNumber.intValue();
                return value;
            } catch (NumberFormatException nfe) {
            }
        }
        return value;
    }

    private boolean handlePost(RequestMessage requestMessage,
            Object device) {
        final String path = requestMessage.getURL();
        final String dURN = "deviceModels/" + TEMPERATURE_SENSOR_MODEL_URN;

        if (!path.startsWith(dURN))
            return false;

        String pathSuffix = path.substring(dURN.length());
        boolean isAttributes = pathSuffix.startsWith("/attributes");

        if (!isAttributes && !pathSuffix.startsWith("/actions"))
            return false;

        String endpointId = requestMessage.getDestination();
        boolean accepted = false;
        try {
            TemperatureSensor temperatureSensor = (TemperatureSensor)device;
            String jsonRequestBody =
                new String(requestMessage.getBody(), "utf-8");
            JSONObject jsonObject = new JSONObject(jsonRequestBody);
            Integer value = null;
            if (isAttributes) {
                Iterator<String> keys = jsonObject.keys();
                while (keys.hasNext()) {
                    String attributeName = keys.next();
                    value = getIntegerValue(jsonRequestBody, jsonObject,
                            attributeName);
                    if (value != null) {
                        if ("minThreshold".equals(attributeName)) {
                            int min = value.intValue();
                            display(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString() + " : " +
                                    endpointId + " : Request : \"minThreshold\"=" +
                                    min);
                            temperatureSensor.setMinThreshold(min);
                            accepted = true;
                        } else if ("maxThreshold".equals(attributeName)) {
                            int max = value.intValue();
                            display(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString()+ " : " +
                                    endpointId + " : Request : \"maxThreshold\"=" +
                                    max);
                            temperatureSensor.setMaxThreshold(max);
                            accepted = true;
                        }
                    }
                }
                accepted = true;
            } else if (pathSuffix.startsWith("/actions/reset")) {
                display(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString() + " : " +
                        endpointId + " : Request : \"reset\"");
                temperatureSensor.reset();
                accepted = true;
            } else if (pathSuffix.startsWith("/actions/power")) {
                Boolean bvalue = getData("value",
                        requestMessage.getBody());
                boolean on = bvalue.booleanValue();
                boolean isOn = temperatureSensor.isPoweredOn();
                display(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString() + " : " +
                        endpointId + " : Request : \"power\"=" +
                        on);
                if (on != isOn) {
                    temperatureSensor.power(on);
                }
                accepted = true;
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
        return accepted;
    }

    private boolean handlePut(RequestMessage requestMessage,
                              Object device) {
        final String path = requestMessage.getURL();
        if (path.endsWith("/attributes/maxThreshold") || path.endsWith("/attributes/minThreshold")) {
            try {
                String endpointId = requestMessage.getDestination();
                String jsonRequestBody =
                        new String(requestMessage.getBody(), "utf-8");
                JSONObject jsonObject = new JSONObject(jsonRequestBody);
                Integer value = null;
                Iterator<String> keys = jsonObject.keys();
                while (keys.hasNext()) {
                    String attributeName = keys.next();
                    value = getIntegerValue(jsonRequestBody, jsonObject,
                            attributeName);
                    if (value != null) {
                        break;
                    }
                }

                if (value == null) {
                    return false;
                }
                if (path.endsWith("maxThreshold")) {
                    int max = value.intValue();
                    display(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString() + " : " +
                            endpointId + " : Request : \"maxThreshold\"=" +
                            max);
                    if (device instanceof TemperatureSensor) {
                        ((TemperatureSensor) device).setMaxThreshold(max);
                    } else {
                        ((HumiditySensor) device).setMaxThreshold(max);
                    }
                } else {
                    int min = value.intValue();
                    display(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString() + " : " +
                            endpointId + " : Request : \"minThreshold\"=" +
                            min);
                    ((TemperatureSensor) device).setMinThreshold(min);
                }
                return true;
            } catch(UnsupportedEncodingException ue) {
                ue.printStackTrace();
                return false;
            } catch(Exception e) {
                e.printStackTrace();
                return false;
            }
        }

        return false;
    }


    private void updateTemperatureSensor(
            MessageDispatcher messageDispatcher,
            String temperatureSensorEndpointId,
            TemperatureSensor temperatureSensor,
            boolean alertMin, boolean alertMax) {
        double temp = temperatureSensor.getTemp();
        String unit = temperatureSensor.getUnit();
        double minTemp = temperatureSensor.getMinTemp();
        double maxTemp = temperatureSensor.getMaxTemp();
        int minTempThreshold = temperatureSensor.getMinThreshold();
        int maxTempThreshold = temperatureSensor.getMaxThreshold();
		Date startTime = temperatureSensor.getStartTime();

        com.oracle.iot.client.message.DataMessage.Builder tempBuilder =
                new com.oracle.iot.client.message.DataMessage.Builder()
                        .format(TEMPERATURE_SENSOR_MODEL_URN + ":attributes")
                        .source(temperatureSensorEndpointId)
                        .dataItem("temp", temp)
                        .dataItem("unit", unit);

        StringBuilder msg = new StringBuilder(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString());
        msg.append(" : ");
        msg.append(temperatureSensorEndpointId);
        msg.append(" : Data : \"temp\"=");
        msg.append(temp);
        msg.append(":\"unit\"=");
        msg.append(unit);

        if (!alertMin && !alertMax) {
            tempBuilder
					.dataItem("startTime",temperatureSensor.getStartTime().getTime())
                    .dataItem("minTemp", temperatureSensor.getMinTemp())
                    .dataItem("maxTemp", temperatureSensor.getMaxTemp());
			msg.append(":\"startTime\"=");
            msg.append(startTime);	
            msg.append(":\"minTemp\"=");
            msg.append(minTemp);
            msg.append(":\"maxTemp\"=");
            msg.append(maxTemp);
            // 0 is the default minThreshold
            // The minThreshold does not have to be sent if it is the default value.
            if (minTempThreshold != 0) {
                tempBuilder.dataItem("minThreshold", minTempThreshold);
                msg.append(":\"minThreshold\"=");
                msg.append(minTempThreshold);
            }
            // 70 is the default maxThreshold.
            // The maxThreshold does not have to be sent if it is the default value.
            if (maxTempThreshold != 70) {
                tempBuilder.dataItem("maxThreshold", maxTempThreshold);
                msg.append(":\"maxThreshold\"=");
                msg.append(maxTempThreshold);
            }
        } else {
            if (alertMin) {
                tempBuilder.dataItem("minThreshold",
                        temperatureSensor.getMinThreshold());
                msg.append(":\"minThreshold\"=");
                msg.append(minTempThreshold);
            }

            if (alertMax) {
                tempBuilder.dataItem("maxThreshold",
                        temperatureSensor.getMaxThreshold());
                msg.append(",\"maxThreshold\"=");
                msg.append(maxTempThreshold);
            }
        }

        display(msg.toString());

        messageDispatcher.queue(tempBuilder.build());
    }

    private void updateHumiditySensor(
            MessageDispatcher messageDispatcher,
            String humiditySensorEndpointId, HumiditySensor humiditySensor,
            boolean alertMax) {
        StringBuilder msg = new StringBuilder(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString());
        msg.append(" : ");
        msg.append(humiditySensorEndpointId);
        msg.append(" : Data : ");

        DataMessage.Builder humidityBuilder =
                new DataMessage.Builder()
                        .format(HUMIDITY_SENSOR_MODEL_URN + ":attributes")
                        .source(humiditySensorEndpointId);

        if (!alertMax) {
            int humidity = humiditySensor.getHumidity();

            msg.append("\"humidity\"=");
            msg.append(humidity);

            humidityBuilder.dataItem(HUMIDITY_ATTRIBUTE, humidity);
        }

        int maxThreshold = humiditySensor.getMaxThreshold();

        // 80 is the default maxThreshold.
        // The maxThreshold does not have to be sent if it is the default value.
        if (maxThreshold != 80) {
            humidityBuilder.dataItem(MAX_THRESHOLD_ATTRIBUTE, maxThreshold);
            msg.append(",\"maxThreshold\"=");
            msg.append(maxThreshold);
        }

        display(msg.toString());

        messageDispatcher.queue(humidityBuilder.build());
    }

    private abstract class MainLogic {

        // Flag to know whether the temperature sensor was off and is now on.
        protected boolean wasOff = false;

        // The previous maximum humidity threshold.
        // If the new maximum threshold is not equal to the previous,
        // the new value will be sent in the data message.
        protected int prevMaxHumidityThreshold;

        // The previous minimum threshold for the temperature sensor.
        // If the new minimum temperature threshold is not equal to the previous,
        // the new value will be sent in the data message.
        protected int prevMinTempThreshold;

        // The previous maximum threshold for the temperature sensor.
        // If the new maximum temperature threshold is not equal to the previous,
        // the new value will be sent in the data message.
        protected int prevMaxTempThreshold;

        protected final MessageDispatcher messageDispatcher;
        protected final HumiditySensor humiditySensor;
        protected final String humiditySensorEndpointId;
        protected final TemperatureSensor temperatureSensor;
        protected final String temperatureSensorEndpointId;

        protected MainLogic(MessageDispatcher messageDispatcher,
                            HumiditySensor humiditySensor,
                            String humiditySensorEndpointId,
                            TemperatureSensor temperatureSensor,
                            String temperatureSensorEndpointId) {

            this.messageDispatcher = messageDispatcher;
            this.humiditySensor = humiditySensor;
            this.humiditySensorEndpointId = humiditySensorEndpointId;
            this.temperatureSensor = temperatureSensor;
            this.temperatureSensorEndpointId = temperatureSensorEndpointId;

            prevMaxHumidityThreshold = humiditySensor.getMaxThreshold();
            prevMinTempThreshold = temperatureSensor.getMinThreshold();
            prevMaxTempThreshold = temperatureSensor.getMaxThreshold();
        }

        // This method allows for differentiation between the processing
        // of sensor data when policies are in use from when policies are
        // not in use.
        public abstract void processSensorData();
    }

    String getFormattedTime() {
        String dateStr = new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString();
        return dateStr;
    }

    private class WithoutDevicePolicies extends MainLogic {

        // Flags to ensure an alert is only generated when the threshold is crossed.
        private boolean humidityAlerted = false;
        private boolean tempAlerted = false;

        // The previous minimum temperature.
        // If the new minimum temperature is less than the previous,
        // update "minTemp" in the virtual TemperatureSensor
        private double prevMinTemp = Double.MAX_VALUE;

        // The previous maximum temperature.
        // If the new maximum temperature is greater than the previous,
        // update "maxTemp" in the virtual TemperatureSensor
        private double prevMaxTemp = Double.MIN_VALUE;


        private WithoutDevicePolicies(MessageDispatcher messageDispatcher,
                                      HumiditySensor humiditySensor,
                                      String humiditySensorEndpointId,
                                      TemperatureSensor temperatureSensor,
                                      String temperatureSensorEndpointId) {
            super(messageDispatcher, humiditySensor, humiditySensorEndpointId,
                    temperatureSensor, temperatureSensorEndpointId);
        }

        //
        // When running the sample without policies, the sample must do more work.
        // The sample has to generate alerts if the sensor value exceeds the
        // threshold. This logic can be handled by the client library with the
        // right set of policies in place. Compare this method to that of the
        // WithPolicies inner class.
        //
        @Override
        public void processSensorData() {

            // Send data from the indirectly-connected device
            final int humidity = humiditySensor.getHumidity();
            DataMessage.Builder humidityMessageBuilder =
                    new DataMessage.Builder()
                            .format("urn:com:oracle:iot:device:humidity_sensor:attributes")
                            .source(humiditySensorEndpointId)
                            .dataItem("humidity", humidity);

            StringBuilder consoleMessage = new StringBuilder(getFormattedTime())
                    .append(" : ")
                    .append(humiditySensorEndpointId)
                    .append(" : Data : \"humidity\"=")
                    .append(humidity);

            // Only send maxThreshold if it has changed, or is not the default
            // Note that this is handled automatically when using device virtualization
            final int humidityThreshold = humiditySensor.getMaxThreshold();
            if (prevMaxHumidityThreshold != humidityThreshold) {
                humidityMessageBuilder.dataItem("maxThreshold", humidityThreshold);
                prevMaxHumidityThreshold = humidityThreshold;
                consoleMessage
                        .append(",\"maxThreshold\"=")
                        .append(humidityThreshold);
            }

            display(consoleMessage.toString());
            messageDispatcher.queue(humidityMessageBuilder.build());

            if (humidity > humidityThreshold) {
                if (!humidityAlerted) {
                    humidityAlerted = true;
                    final AlertMessage alertMessage =
                            new AlertMessage.Builder()
                                    .format("urn:com:oracle:iot:device:humidity_sensor:too_humid")
                                    .source(humiditySensorEndpointId)
                                    .description("max threshold crossed")
                                    .dataItem("humidity", humidity)
                                    .severity(AlertMessage.Severity.CRITICAL)
                                    .build();

                    consoleMessage = new StringBuilder(getFormattedTime())
                            .append(" : ")
                            .append(humiditySensorEndpointId)
                            .append(" : Alert : \"humidity\"=")
                            .append(humidity);

                    display(consoleMessage.toString());
                    messageDispatcher.queue(alertMessage);
                }
            } else {
                humidityAlerted = false;
            }

            // if temperature sensor is powered off, do not queue a message for temp
            if (temperatureSensor.isPoweredOn()) {

                final double temperature = temperatureSensor.getTemp();
                final double minTemp = temperatureSensor.getMinTemp();
                final double maxTemp = temperatureSensor.getMaxTemp();
                final String unit = temperatureSensor.getUnit();

                com.oracle.iot.client.message.DataMessage.Builder temperatureMessageBuilder =
                        new com.oracle.iot.client.message.DataMessage.Builder()
                                .format("urn:com:oracle:iot:device:temperature_sensor:attributes")
                                .source(temperatureSensorEndpointId)
                                .dataItem("temp", temperature);

                consoleMessage = new StringBuilder(getFormattedTime())
                        .append(" : ")
                        .append(temperatureSensorEndpointId)
                        .append(" : Data : \"temp\"=")
                        .append(temperature);

                if (wasOff) {
                    wasOff = false;
                    Date startTime = temperatureSensor.getStartTime();
                    consoleMessage
                            .append(",\"startTime\"=").append(startTime);
                    temperatureMessageBuilder
                            .dataItem("startTime", startTime.getTime());
                }

                if (minTemp != prevMinTemp) {
                    prevMinTemp = minTemp;
                    temperatureMessageBuilder
                            .dataItem("minTemp", minTemp);
                    consoleMessage
                            .append(",\"minTemp\"=")
                            .append(minTemp);
                }

                if (maxTemp != prevMaxTemp) {
                    prevMaxTemp = maxTemp;
                    temperatureMessageBuilder
                            .dataItem("maxTemp", maxTemp);
                    consoleMessage
                            .append(",\"maxTemp\"=")
                            .append(maxTemp);
                }

                // There is no need to set minThreshold if the value has not changed.
                // Note that this is handled automatically when using device virtualization
                final int minTempThreshold = temperatureSensor.getMinThreshold();
                if (minTempThreshold != prevMinTempThreshold) {
                    prevMinTempThreshold = minTempThreshold;
                    temperatureMessageBuilder
                            .dataItem("minThreshold", minTempThreshold);
                    consoleMessage
                            .append(",\"minThreshold\"=")
                            .append(minTempThreshold);
                }

                // There is no need to set maxThreshold if the value has not changed.
                // Note that this is handled automatically when using device virtualization
                final int maxTempThreshold = temperatureSensor.getMaxThreshold();
                if (maxTempThreshold != prevMaxTempThreshold) {
                    prevMaxTempThreshold = maxTempThreshold;
                    temperatureMessageBuilder
                            .dataItem("maxThreshold", maxTempThreshold);
                    consoleMessage
                            .append(",\"maxThreshold\"=")
                            .append(maxTempThreshold);
                }

                display(consoleMessage.toString());
                messageDispatcher.queue(temperatureMessageBuilder.build());

                if (temperature < minTempThreshold) {
                    if (!tempAlerted) {
                        tempAlerted = true;
                        final AlertMessage alertMessage = new AlertMessage.Builder()
                                .format("urn:com:oracle:iot:device:temperature_sensor:too_cold")
                                .source(temperatureSensorEndpointId)
                                .description("min threshold crossed")
                                .dataItem("temp", temperature)
                                .dataItem("unit", temperatureSensor.getUnit())
                                .severity(AlertMessage.Severity.CRITICAL)
                                .build();

                        consoleMessage = new StringBuilder(new Date().toString())
                                .append(" : ")
                                .append(temperatureSensorEndpointId)
                                .append(" : Alert : \"temp\"=")
                                .append(temperature)
                                .append(",\"minThreshold\"=")
                                .append(minTempThreshold);

                        display(consoleMessage.toString());
                        messageDispatcher.queue(alertMessage);
                    }

                } else if (temperature > maxTempThreshold) {
                    if (!tempAlerted) {
                        tempAlerted = true;
                        final AlertMessage alertMessage = new AlertMessage.Builder()
                                .format("urn:com:oracle:iot:device:temperature_sensor:too_hot")
                                .source(temperatureSensorEndpointId)
                                .description("max threshold crossed")
                                .dataItem("temp", temperature)
                                // unit is required by the too_hot format in the device model
                                .dataItem("unit", temperatureSensor.getUnit())
                                .severity(AlertMessage.Severity.CRITICAL)
                                .build();

                        consoleMessage = new StringBuilder(new Date().toString())
                                .append(" : ")
                                .append(temperatureSensorEndpointId)
                                .append(" : Alert : \"temp\"=")
                                .append(temperature)
                                .append(",\"maxThreshold\"=")
                                .append(maxTempThreshold);

                        display(consoleMessage.toString());
                        messageDispatcher.queue(alertMessage);
                    }
                } else {
                    tempAlerted = false;
                }


            } else {
                wasOff = true;
            }
        }
    }

    private class WithDevicePolicies extends MainLogic {

        private WithDevicePolicies(MessageDispatcher messageDispatcher,
                                   HumiditySensor humiditySensor,
                                   String humiditySensorEndpointId,
                                   TemperatureSensor temperatureSensor,
                                   String temperatureSensorEndpointId) {
            super(messageDispatcher, humiditySensor, humiditySensorEndpointId,
                    temperatureSensor, temperatureSensorEndpointId);
        }

        //
        // When running with policies, we only need to "offer" the
        // humidity value. Policies can be set on the server to
        // generate alerts, and filter bad data - which had to be
        // handled by the sample code when running without policies.
        // Compare this implementation of processSensorData to that
        // of the WithoutPolicies inner class.
        //
        @Override
        public void processSensorData() {

            // Send data from the indirectly-connected device
            final int humidity = humiditySensor.getHumidity();
            DataMessage.Builder humidityMessageBuilder =
                    new DataMessage.Builder()
                            .format("urn:com:oracle:iot:device:humidity_sensor:attributes")
                            .source(humiditySensorEndpointId)
                            .dataItem("humidity", humidity);

            StringBuilder consoleMessage = new StringBuilder(getFormattedTime())
                    .append(" : ")
                    .append(humiditySensorEndpointId)
                    .append(" : Data : \"humidity\"=")
                    .append(humidity);

            // Send humidityThreshold if it has changed.
            // Note that this is handled automatically when using device virtualization.
            int humidityThreshold = humiditySensor.getMaxThreshold();
            if (prevMaxHumidityThreshold != humidityThreshold) {
                prevMaxHumidityThreshold = humidityThreshold;
                humidityMessageBuilder.dataItem("maxThreshold", humidityThreshold);
                consoleMessage.append(",\"maxThreshold\"=")
                        .append(humidityThreshold);
            }

            display(consoleMessage.toString());
            messageDispatcher.offer(humidityMessageBuilder.build());

            // if temperature sensor is powered off, do not queue a message for temp
            if (temperatureSensor.isPoweredOn()) {

                final double temperature = temperatureSensor.getTemp();

                DataMessage.Builder temperatureMessageBuilder =
                        new DataMessage.Builder()
                                .format("urn:com:oracle:iot:device:temperature_sensor:attributes")
                                .source(temperatureSensorEndpointId)
                                .dataItem("temp", temperature);

                consoleMessage = new StringBuilder(getFormattedTime())
                        .append(" : ")
                        .append(temperatureSensorEndpointId)
                        .append(" : Data : \"temp\"=")
                        .append(temperature);

                if (wasOff) {
                    wasOff = false;
                    Date startTime = temperatureSensor.getStartTime();
                    consoleMessage
                            .append(",\"startTime\"=").append(startTime);
                    temperatureMessageBuilder
                            .dataItem("startTime", startTime.getTime());
                }

                // There is no need to set minThreshold if the value has not changed.
                // Note that this is handled automatically when using device virtualization.
                final int minTempThreshold = temperatureSensor.getMinThreshold();
                if (minTempThreshold != prevMinTempThreshold) {
                    prevMinTempThreshold = minTempThreshold;
                    temperatureMessageBuilder
                            .dataItem("minThreshold", minTempThreshold);
                    consoleMessage
                            .append(",\"minThreshold\"=")
                            .append(minTempThreshold);
                }

                // There is no need to set maxThreshold if the value has not changed.
                // Note that this is handled automatically when using device virtualization.
                final int maxTempThreshold = temperatureSensor.getMaxThreshold();
                if (maxTempThreshold != prevMaxTempThreshold) {
                    prevMaxTempThreshold = maxTempThreshold;
                    temperatureMessageBuilder
                            .dataItem("maxThreshold", maxTempThreshold);
                    consoleMessage
                            .append(",\"maxThreshold\"=")
                            .append(maxTempThreshold);
                }

                display(consoleMessage.toString());
                messageDispatcher.offer(temperatureMessageBuilder.build());

            } else {
                wasOff = true;
            }
        }
    }

    private void display(String s) {
        Log.d(MSG_TAG, s);
        messageToBeSent = s;
        messageSent = false;
        mHandler.sendEmptyMessage(REPORT_MSG);
        if(humiditySensor!=null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            mHandler.sendEmptyMessage(GD_MAX_HYGRO_MSG);
        }
        if(temperatureSensor!=null) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            mHandler.sendEmptyMessage(GD_MAX_THERMO_MSG);
        }
    }

    private void displayError(String string) {
        Log.d(ERROR_TAG, "Error Message: "+string);
        messageToBeSent = string;
        mHandler.sendEmptyMessage(ERROR_MSG);
        try {
            Thread.sleep(8000);
        } catch(InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.exit(-1);
    }

    @Override
    public void onDestroy() {
        // power management: let the CPU go to sleep
        if (wakeLock != null) wakeLock.release();
        stopped = true;
        humiditySensor.setMaxThreshold(rememberHygroMaxThreshold);
        temperatureSensor.setMaxThreshold(rememberThermoMaxThreshold);
        // Cancel the persistent notification.
        mNM.cancelAll();
        showNotification(false);
        // Tell the user we stopped.
        Toast.makeText(this, R.string.gd_service_stopped, Toast.LENGTH_SHORT).show();

        // Unregister all callbacks.
        mCallbacks.kill();

        mHandler.removeMessages(REPORT_MSG);
        mHandler.removeMessages(GD_MAX_HYGRO_MSG);
        mHandler.removeMessages(GD_MAX_THERMO_MSG);
    }

    /**
     * Show a notification while this service is running.
     */
    private void showNotification(boolean serviceStarted) {
        // In this sample, we'll use the same text for the ticker and the expanded notification
        CharSequence text = serviceStarted ? getText(R.string.gd_service_started) :
                getText(R.string.gd_service_stopped);
        // The PendingIntent to launch our activity if the user selects this notification
        PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
                new Intent(this, MainActivity.class), 0);
        // Set the info for the views that show in the notification panel.
        Notification notification = new Notification.Builder(this)
                .setSmallIcon(R.drawable.mini)  // the status icon
                .setTicker(text)  // the status text
                .setWhen(System.currentTimeMillis())  // the time stamp
                .setContentTitle(getText(R.string.gd_service_label))  // the label of the entry
                .setContentText(text)  // the contents of the entry
                .setContentIntent(contentIntent)  // The intent to send when the entry is clicked
                .build();
        // Send the notification.
        // We use a string id because it is a unique number.  We use it later to cancel.
        mNM.notify(serviceStarted ? R.string.gd_service_started :
                R.string.gd_service_stopped, notification);
    }
}
