/*
 * Copyright (c) 2015,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.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.Message;
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.DirectlyConnectedDevice;
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.RequestMessage;
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.InputStream;
import java.security.GeneralSecurityException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
import java.util.UUID;
import java.util.Locale;

public class DirectlyConnectedDeviceSampleService extends Service {
    private static final String ERROR_TAG = "IOT_ERROR";
    private static final String MSG_TAG = "IOT";
    private Context context;

    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 long POLL_INTERVAL = 5000;
    private static final String DISABLE_LONG_POLLING_PROPERTY =
            "com.oracle.iot.client.disable_long_polling";
    private static boolean useLongPolling=true;


    private static final HumiditySensor sensor =
        new HumiditySensor(UUID.randomUUID().toString());

    private static int lastThreshold;

    // This sample uses the RequestDispatcher utility
    static final RequestDispatcher requestDispatcher =
            RequestDispatcher.getInstance();

    private static DirectlyConnectedDevice dcd;

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

    private int maxMaxHygroThreshold = 95;
    private int minMaxHygroThreshold = 80;

    private int rememberMaxThreshold = 0;
    PowerManager.WakeLock wakeLock;

    /**
     * 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<IDirectlyConnectedDeviceSampleServiceCallback> mCallbacks
            = new RemoteCallbackList<IDirectlyConnectedDeviceSampleServiceCallback>();

    NotificationManager mNM;

    @Override
    public void onCreate() {
        InputStream configInStream = null;
        boolean bksProvided = false;
        try {
            configInStream = (InputStream) getApplicationContext().getAssets().open("trustedAssets.properties");
            Properties config = new Properties();
            config.load(configInStream);
            String disableLongPolling = config.getProperty(DISABLE_LONG_POLLING_PROPERTY, "");
            if (disableLongPolling.toLowerCase(Locale.ROOT).equals("true")) {
                System.setProperty(DISABLE_LONG_POLLING_PROPERTY,"true");
                useLongPolling = false;
            }
        }catch(Exception e){

        }
        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 (IDirectlyConnectedDeviceSampleService.class.getName().equals(intent.getAction())) {
            return mBinder;
        }
        if (IDirectlyConnectedDeviceSampleServiceSecondary.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(rememberMaxThreshold != 0) {
            sensor.setMaxThreshold(rememberMaxThreshold);
        }
        context = getApplicationContext();
        Toast.makeText(this, "Service started.", Toast.LENGTH_LONG).show();
        new Thread() {
            @Override
            public void run() {
                runSample();
            }
        }.start();

        return START_NOT_STICKY;
    }

    /**
     * The IDirectlyConnectedDeviceSampleService Interface is defined through IDL
     */
    private final IDirectlyConnectedDeviceSampleService.Stub mBinder =
            new IDirectlyConnectedDeviceSampleService.Stub() {
        public void registerCallback(IDirectlyConnectedDeviceSampleServiceCallback cb) {
            if (cb != null) mCallbacks.register(cb);
        }
        public void unregisterCallback(IDirectlyConnectedDeviceSampleServiceCallback cb) {
            if (cb != null) mCallbacks.unregister(cb);
        }
    };
    /**
     * A secondary DCD Sample Service interface to the service.
     */
    private final IDirectlyConnectedDeviceSampleServiceSecondary.Stub mSecondaryBinder =
            new IDirectlyConnectedDeviceSampleServiceSecondary.Stub() {
                public int getPid() {
                    return Process.myPid();
                }
                public String getMessage() {
                    messageSent = true;
                    return messageToBeSent;
                }
                public int getMaxThreshold() {
                    return sensor.getMaxThreshold();
                }
                public void increaseMaxThreshold() {
                    int mt = sensor.getMaxThreshold() + 1;
                    sensor.setMaxThreshold(mt);
                    if(mt >= maxMaxHygroThreshold) {
                        display(getResources().getText(R.string.max_hygro_reached).toString());
                    }
                }
                public void decreaseMaxThreshold() {
                    int mt = sensor.getMaxThreshold() - 1;
                    sensor.setMaxThreshold(mt);
                    if(mt <= minMaxHygroThreshold) {
                        display(getResources().getText(R.string.min_hygro_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 DCD_MAX_HYGRO_MSG = 3;
    private static final int ERROR_MSG = 4;

    /**
     * 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(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 DCD_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).maxHygro(sensor.getMaxThreshold() + "");
                        } 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 {
            DirectlyConnectedDevice dcd;
            SharedPreferences mSharedPref = getApplicationContext().getSharedPreferences(
                    getString(R.string.preference_file_key), Context.MODE_PRIVATE);
            final boolean useDevicePolicy = mSharedPref.getBoolean("useDevicePolicy", true);

            // Create the directly-connected device instance
            if (mSharedPref.getBoolean(getString(R.string.use_provided_bks), false)) {
                dcd = new DirectlyConnectedDevice(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");
                }
                dcd = new DirectlyConnectedDevice(mSharedPref.getString(getString(R.string.ta_file_path),""), mSharedPref.getString(getString(R.string.ta_password),""),context);
            }

            if (!dcd.isActivated()) {
                dcd.activate(HUMIDITY_SENSOR_MODEL_URN);
            }
            final String endpointId = dcd.getEndpointId();
            // Register a handler for the device model
            RequestDispatcher requestDispatcher = RequestDispatcher.getInstance();
            requestDispatcher.registerRequestHandler(
                    endpointId,
                    "deviceModels/" + HUMIDITY_SENSOR_MODEL_URN,
                    new RequestHandler() {
                        @Override
                        public ResponseMessage handleRequest(RequestMessage requestMessage) {

                            StatusCode statusCode = null;

                            final String resourcePath = requestMessage.getURL();
                            final String attributesUrn =
                                    "deviceModels/" + HUMIDITY_SENSOR_MODEL_URN + "/attributes/";

                            if (attributesUrn.regionMatches(0, resourcePath, 0, attributesUrn.length())) {
                                final String attribute = resourcePath.substring(attributesUrn.length());
                                final byte[] body = requestMessage.getBody();
                                statusCode = handleAttributesRequest(sensor, body, endpointId);

                            } else {
                                statusCode = StatusCode.BAD_REQUEST;
                            }
                            return new ResponseMessage.Builder(requestMessage)
                                    .statusCode(statusCode)
                                    .build();
                        }
                    }
            );
            if(rememberMaxThreshold != 0) {
                sensor.setMaxThreshold(rememberMaxThreshold);
            }
            // The default threshold is 80. Assume this is the value when starting.
            lastThreshold = 80;
            rememberMaxThreshold = sensor.getMaxThreshold();
            int threshold = sensor.getMaxThreshold();

            // a flag to make sure alerts are only sent when crossing the threshold
            boolean alerted = false;

            final MainLogic mainLogic = useDevicePolicy
                    ? new WithDevicePolicies(dcd, sensor)
                    :  new WithoutDevicePolicies(dcd, sensor);



            // Main loop for single-threaded example.
            while (!stopped) {

                mainLogic.processSensorData();
                // Wait 5 seconds before sending next readings.
                Thread.sleep(5000);
                lastThreshold = threshold;
            }
        } catch (Exception e) {
            e.printStackTrace();
            Log.d(ERROR_TAG, e.toString());
            if(e.getMessage() != null) {                // Tell the user
                displayError(e.getMessage());
            } else {
                // Tell the user
                displayError("Error. No message.");
            }
        }
    }

    private StatusCode handleAttributesRequest(HumiditySensor sensor,
                                               byte[] jsonPayload, String endpointId) {

        final Number data = getData("value", jsonPayload);
        if (data != null) {
            final int threshold = data.intValue();
            display(getFormattedTime() + " : " +
                    endpointId + " : Request : \"" +
                    MAX_THRESHOLD_ATTRIBUTE + "\"=" + threshold);
            sensor.setMaxThreshold(threshold);
        } else {
            Log.d(ERROR_TAG, "No threshold value ");
            return StatusCode.BAD_REQUEST;
        }

        return StatusCode.ACCEPTED;
    }

    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;
        }
    }

    private void display(String string) {
        Log.d(MSG_TAG, string);
        messageToBeSent = string;
        messageSent = false;
        mHandler.sendEmptyMessage(REPORT_MSG);
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        mHandler.sendEmptyMessage(DCD_MAX_HYGRO_MSG);
    }

    private void displayError(String string) {
        Log.d(ERROR_TAG, "*** Display Error:" + 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;
        sensor.setMaxThreshold(rememberMaxThreshold);
        // Cancel the persistent notification.
        mNM.cancelAll();
        showNotification(false);
        // Tell the user we stopped.
        Toast.makeText(this, R.string.dcd_service_stopped, Toast.LENGTH_SHORT).show();

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

        mHandler.removeMessages(REPORT_MSG);
        mHandler.removeMessages(DCD_MAX_HYGRO_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.dcd_service_started) :
                getText(R.string.dcd_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.dcd_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.dcd_service_started :
                R.string.dcd_service_stopped, notification);
    }

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

    private abstract class MainLogic {

        // 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;

        protected final DirectlyConnectedDevice directlyConnectedDevice;
        protected final HumiditySensor humiditySensor;

        protected MainLogic(DirectlyConnectedDevice directlyConnectedDevice, HumiditySensor humiditySensor) {
            this.humiditySensor = humiditySensor;
            this.directlyConnectedDevice = directlyConnectedDevice;
            prevMaxHumidityThreshold = humiditySensor.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() throws IOException, GeneralSecurityException;
    }


    private class WithoutDevicePolicies extends MainLogic {

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

        private WithoutDevicePolicies(DirectlyConnectedDevice directlyConnectedDevice,
                                      HumiditySensor humiditySensor) {
            super(directlyConnectedDevice, humiditySensor);
        }

        //
        // 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() throws IOException, GeneralSecurityException {

            final int humidity = humiditySensor.getHumidity();

            // Send data from the indirectly-connected device
            final DataMessage.Builder dataMessageBuilder =
                    new DataMessage.Builder();

            StringBuilder consoleMessage =
                    new StringBuilder(getFormattedTime());

            dataMessageBuilder.format("urn:com:oracle:iot:device:humidity_sensor:attributes")
                    .source(directlyConnectedDevice.getEndpointId())
                    .dataItem("humidity", humidity);
            consoleMessage.append(" : ")
                    .append(directlyConnectedDevice.getEndpointId())
                    .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;
                dataMessageBuilder.dataItem("maxThreshold", humidityThreshold);
                consoleMessage.append(",\"maxThreshold\"=")
                        .append(humidityThreshold);
            }

            DataMessage dataMessage = dataMessageBuilder.build();

            display(consoleMessage.toString());
            directlyConnectedDevice.send(dataMessage);

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

                        consoleMessage = new StringBuilder(new Date().toString())
                                .append(" : ")
                                .append(directlyConnectedDevice.getEndpointId())
                                .append(" : Alert : \"humidity\"=")
                                .append(humidity);

                        display(consoleMessage.toString());
                        directlyConnectedDevice.send(alertMessage);
                    } catch (Exception e) {
                        System.err.println("could not raise alert: " + e.getMessage());
                    }
                }
            }
        }
    }

    private class WithDevicePolicies extends MainLogic {

        private WithDevicePolicies(DirectlyConnectedDevice directlyConnectedDevice,
                                   HumiditySensor humiditySensor) {
            super(directlyConnectedDevice, humiditySensor);
        }

        //
        // 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() throws IOException, GeneralSecurityException {

            final int humidity = humiditySensor.getHumidity();

            // Send data from the indirectly-connected device
            final DataMessage.Builder dataMessageBuilder =
                    new DataMessage.Builder();

            StringBuilder consoleMessage =
                    new StringBuilder(getFormattedTime());

            dataMessageBuilder.format("urn:com:oracle:iot:device:humidity_sensor:attributes")
                    .source(directlyConnectedDevice.getEndpointId())
                    .dataItem("humidity", humidity);

            consoleMessage.append(" : ")
                    .append(directlyConnectedDevice.getEndpointId())
                    .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;
                dataMessageBuilder.dataItem("maxThreshold", humidityThreshold);
                consoleMessage.append(",\"maxThreshold\"=")
                        .append(humidityThreshold);
            }

            DataMessage dataMessage = dataMessageBuilder.build();

            display(consoleMessage.toString());
            directlyConnectedDevice.offer(dataMessage);

        }
    }

}
