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

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.*;
import android.os.Process;
import android.util.Log;
import android.widget.Toast;
import java.io.FileOutputStream;
import java.io.IOException;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import java.util.Locale;

import oracle.iot.client.DeviceModel;
import oracle.iot.client.device.Alert;
import oracle.iot.client.device.DirectlyConnectedDevice;
import oracle.iot.client.device.VirtualDevice;

public class DirectlyConnectedDeviceSampleService 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 HumiditySensor sensor = new HumiditySensor(UUID.randomUUID().toString());

    private static VirtualDevice virtualDevice;

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

    private Context context;

    private boolean messageSent = false;
    private String messageToBeSent = "";
    private boolean stopped = false;
    PowerManager.WakeLock wakeLock;
    Thread sensorThread;
    /**
     * 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() {

        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;
        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", false);
            // 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());
                    Log.d(ERROR_TAG, "Write file " + pref.getString(getString(R.string.ta_file_path), ""));
                } catch (IOException ex) {
                    Log.d(ERROR_TAG, "Can not save 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);
            }

            // Get the device model instance.
            DeviceModel dcdModel =
                    dcd.getDeviceModel(HUMIDITY_SENSOR_MODEL_URN);

            virtualDevice =
                    dcd.createVirtualDevice(dcd.getEndpointId(), dcdModel);

           virtualDevice.setOnError(
                new VirtualDevice.ErrorCallback<VirtualDevice
                        >() {
                       @Override
                    public void onError(VirtualDevice.ErrorEvent<VirtualDevice
                            > event) {
                        VirtualDevice device =  event.getVirtualDevice();
                           Log.d(ERROR_TAG, new Date().toString() + " : onError : " +
                                device.getEndpointId() +
                                " : \"" + event.getMessage());
                       }
                   });

            // Since there is only one attribute maxThreshold, this could have
            // done with using a attribute specific on change handler
            virtualDevice.setOnChange(
                    new VirtualDevice.ChangeCallback<VirtualDevice>() {
                        @Override
                        public void onChange(
                                VirtualDevice.ChangeEvent<VirtualDevice> event) {

                            VirtualDevice virtualDevice = event.getVirtualDevice();
                            VirtualDevice.NamedValue<?> namedValues = event.getNamedValue();
                            StringBuilder msg = new StringBuilder(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString());
                            msg.append(" : ");
                            msg.append(virtualDevice.getEndpointId());
                            msg.append(" : onChange : ");

                            boolean first = true;
                            for (VirtualDevice.NamedValue<?> namedValue = namedValues;
                                 namedValue != null;
                                 namedValue = namedValue.next()) {
                                final String attribute = namedValue.getName();
                                final Object value = namedValue.getValue();
                                if (!first) {
                                    msg.append(',');
                                } else {
                                    first = false;
                                }

                                if (value != null && MAX_THRESHOLD_ATTRIBUTE.equals(attribute)) {
                                    int max = ((Integer)value).intValue();
                                    msg.append("\"maxThreshold\"=");
                                    msg.append(max);
                                    sensor.setMaxThreshold(max);
                                } else {
                                    msg.append("\"" + attribute + "\" not implemented");
                                }
                            }

                            display(msg.toString());
                        }
                    });

            sensorThread = new Thread(
                    new Runnable () {
                        public void run() {
                            // a flag to make sure alerts are only sent when
                            // crossing the threshold
                            boolean alerted = false;
                            VirtualDevice device = DirectlyConnectedDeviceSampleService.virtualDevice;
                            /**
                             * Set useDevicePolicy to true/false to enable/disable using policies in this sample.
                             * Default value: false.
                             */
                            final MainLogic mainLogic = useDevicePolicy
                                    ? new WithPolicies(sensor, device)
                                    :  new WithoutPolicies(sensor, device);

                            while(!stopped && !sensorThread.isInterrupted()) {
                                try {
                                    mainLogic.processSensorData();
                                    // Wait 5 seconds before sending next reading.
                                    Thread.sleep(5000);
                                } catch (Exception e) {
                                    e.printStackTrace();
                                    if(e.getMessage() != null) {
                                        // Tell the user
                                        displayError(e.getMessage());
                                    } else {
                                        // Tell the user
                                        displayError("Error. No message.");
                                    }

                                }
                            }
                        }
                    });

            sensorThread.setDaemon(true);
            sensorThread.start();
            // System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
            if(e.getMessage() != null) {
                // Tell the user
                displayError(e.getMessage());
            } else {
                // Tell the user
                displayError("Error. No message.");
            }
        }
    }

    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, "Error:" + string);
        messageToBeSent = string;
        mHandler.sendEmptyMessage(ERROR_MSG);
        try {
            Thread.sleep(8000);
        } catch(InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public void onDestroy() {
        stopped = true;

        if (sensorThread != null) {
            try {
                sensorThread.interrupt();
                sensorThread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // Cancel the persistent notification.
        mNM.cancelAll();
        showNotification(false);
        // power management: let the CPU go to sleep
        if (wakeLock != null) wakeLock.release();
        // 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 abstract class MainLogic {

        protected final HumiditySensor humiditySensor;
        protected final VirtualDevice virtualHumiditySensor;

        protected MainLogic(HumiditySensor humiditySensor,
                            VirtualDevice virtualHumiditySensor) {
            this.humiditySensor = humiditySensor;
            this.virtualHumiditySensor = virtualHumiditySensor;
        }

        // 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();
    }


    private class WithoutPolicies extends MainLogic {

        //
        // Create an Alert for maximum humidity threshold.
        // Alerts do not have to be created new each time an
        // alerting event occurs.
        //
        private final Alert tooHumidAlert;

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

        WithoutPolicies(HumiditySensor humiditySensor,
                        VirtualDevice virtualHumiditySensor) {
            super(humiditySensor, virtualHumiditySensor);

            tooHumidAlert = virtualHumiditySensor
                    .createAlert("urn:com:oracle:iot:device:humidity_sensor:too_humid");

        }

        //
        // 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() {

            int humidity = humiditySensor.getHumidity();

            String consoleMessage = new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString() + " : " +
                    virtualDevice.getEndpointId() +
                    " : Set : \"humidity\"=" + humidity;

            // Set the virtualilzed humidity sensor value in an 'update'. If
            // the humidity maxThreshold has changed, that maxThreshold attribute
            // will be added to the update (see the block below). A call to
            // 'finish' will commit the update.
            virtualHumiditySensor.update()
                    .set("humidity", humidity);

            display(consoleMessage);
            virtualHumiditySensor.finish();

            int humidityThreshold = humiditySensor.getMaxThreshold();
            if (humidity > humidityThreshold) {
                if (!humidityAlerted) {
                    humidityAlerted = true;
                    consoleMessage =
                            new SimpleDateFormat("HH:mm:ss", Locale.ROOT)
                                    .format(new Date()).toString() + " : " +
                            virtualDevice.getEndpointId() +
                            " : Set : \"humidity\"=" + humidity;
                    display(consoleMessage);
                    tooHumidAlert.set("humidity", humidity).raise();
                }
            } else {
                humidityAlerted = false;
            }

        }
    }

    private class WithPolicies extends MainLogic {

        WithPolicies(HumiditySensor humiditySensor,
                     VirtualDevice virtualHumiditySensor) {
            super(humiditySensor, virtualHumiditySensor);
        }

        //
        // 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() {

            int humidity = humiditySensor.getHumidity();

            String consoleMessage = new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString() + " : " +
                    virtualDevice.getEndpointId() +
                    " : Offer : \"humidity\"=" + humidity;

            display(consoleMessage.toString());

            virtualHumiditySensor.update().offer("humidity", humidity).finish();

        }
    }
}

