/*
 * Copyright (c) 2017 Oracle and/or its affiliates. All rights reserved.
 *
 * This software is dual-licensed to you under the MIT License (MIT) and
 * the Universal Permissive License (UPL). See the LICENSE file in the root
 * directory for license terms. You may choose either license, or both.
 */

package com.oracle.iot.sample;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
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.IOException;
import java.security.GeneralSecurityException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import oracle.iot.client.DeviceModel;
import oracle.iot.client.device.GatewayDevice;
import oracle.iot.client.device.VirtualDevice;

/**
 * This sample is a NFC gateway that tracks assets tagged by NFC tags and their location.
 */
public class NFCGatewayService extends Service {
    private static final String ERROR_TAG = "IOT_ERROR";
    private static final String MSG_TAG = "IOT";
    private static GatewayDevice gatewayDevice;
    private Context context;

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

    private Thread workerThread;

    private final static String GPS_SENSOR_MODEL_URN = "urn:com:oracle:iot:device:gps_sensor";
    DeviceModel gpsDeviceModel;
    private final static String ATTRIBUTE_LATITUDE = "ora_latitude";
    private final static String ATTRIBUTE_LONGITUDE = "ora_longitude";
    private final static String ATTRIBUTE_ALTITUDE = "ora_altitude";

    private final static String NFC_GW_TAG = "NFC_Gateway";

    private final NFCSensor sensor = NFCSensor.getInstance();

    /**
     * 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;
    HashMap<String, VirtualDevice> mapVirtualDevices = new HashMap<>();

    @Override
    public void onCreate() {

        // Set a callback for changes in network connectivity
        getApplicationContext().registerReceiver(new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                //
                // Set "oracle.iot.client.network_cost" depending on connectivity
                //
                final ConnectivityManager cm =
                        (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);

                final NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
                if (activeNetwork != null) {
                    int type = activeNetwork.getType();
                    Log.d("IOT_INFO", "active network type: " + type);
                    switch(type) {
                        case ConnectivityManager.TYPE_ETHERNET:
                        case ConnectivityManager.TYPE_WIFI:
                            System.setProperty("oracle.iot.client.network_cost", "ETHERNET");
                            break;
                        case ConnectivityManager.TYPE_MOBILE:
                            System.setProperty("oracle.iot.client.network_cost", "CELLULAR");
                            break;
                        default:
                            System.setProperty("oracle.iot.client.network_cost", "SATELLITE");
                    }
                }
                Log.d("IOT_INFO", "oracle.iot.client.network_cost: " + System.getProperty("oracle.iot.client.network_cost", "not set"));            }
        },  new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));

        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();
        context = this;
        stopped = false;

        //Toast.makeText(this, "Service started.", Toast.LENGTH_LONG).show();
        workerThread = new Thread() {
            @Override
            public void run() {
                runSample();
            }
        };
        workerThread.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() {
                    return messageToBeSent;
                }
                public void kill() {
                    // not used
                }

                public void tagReceived(String tagValue) {

                }
            };

    @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 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(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 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 {
            Log.d(NFC_GW_TAG, "Getting preference file key");
            SharedPreferences mSharedPref = getApplicationContext().getSharedPreferences(
                    getString(R.string.preference_file_key), Context.MODE_PRIVATE);

            // Create the directly-connected device instance
            if(mSharedPref.getBoolean(getString(R.string.use_provided_bks), true)){
                Log.d(NFC_GW_TAG, "Using existing BKS");
                gatewayDevice = new GatewayDevice(context);
            }else{
                Log.d(NFC_GW_TAG, "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),""));
            }

            String endpointId;

            if (!gatewayDevice.isActivated()) {
                display(":Activating Gateway Device... "  );
                // 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();
                endpointId = gatewayDevice.getEndpointId();
                display(":Activated: " + endpointId);
            } else {
                endpointId = gatewayDevice.getEndpointId();
                display(":Gateway Device: " + endpointId );
            }

            gpsDeviceModel = gatewayDevice.getDeviceModel(GPS_SENSOR_MODEL_URN);
            if (gpsDeviceModel == null) {
                displayError("GPS device model has not been defined");
                return;
            }

            display(":Please Swipe NFC tag(s) to send location of Tag to IoT CS:");
            while (!stopped) {
                // Wait for new NFC tag found intent to be processed and the tagQueue to be updated.
                NFCSensor.TagData tagData = sensor.getNextTagData();
                handleNfcTagDetected(tagData);
            }

        } catch (Exception e) {
            e.printStackTrace();
            Log.d(NFC_GW_TAG, "runSample: " + e.toString());
            if(e.getMessage() != null) {
                // Tell the user
                displayError(e.getMessage());
            } else {
                // Tell the user
                displayError("Error. No message.");
            }
        }
    }

    void handleNfcTagDetected(NFCSensor.TagData tagData) throws IOException, GeneralSecurityException {
        VirtualDevice gpsDevice;
            // If this tag has not been read before, register a new ICD device through gateway.
        if ((gpsDevice = mapVirtualDevices.get(tagData.tagSerialNumber)) == null) {
            Log.d(NFC_GW_TAG,"NFC Tag: " + tagData.tagSerialNumber);
            Map<String, String> metaData = new HashMap<>();
            metaData.put(GatewayDevice.MANUFACTURER, tagData.tagManufacturer);
            metaData.put( GatewayDevice.MODEL_NUMBER, tagData.tagModelNumber);
            metaData.put(GatewayDevice.SERIAL_NUMBER, tagData.tagSerialNumber);
            String endpointId = gatewayDevice.registerDevice(tagData.tagSerialNumber, metaData,
                    GPS_SENSOR_MODEL_URN);
            gpsDevice = gatewayDevice.createVirtualDevice(endpointId, gpsDeviceModel);
            mapVirtualDevices.put(tagData.tagSerialNumber, gpsDevice);
        }

        double latitude = tagData.location.getLatitude();
        double longitude = tagData.location.getLongitude();
        double altitude = tagData.location.getAltitude();

        StringBuilder msg = new StringBuilder(
                new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString());

        msg.append(" : " + tagData.tagSerialNumber + "\n");

        gpsDevice.update()
                .set(ATTRIBUTE_LATITUDE, latitude)
                .set(ATTRIBUTE_LONGITUDE, longitude)
                .set(ATTRIBUTE_ALTITUDE, altitude)
                .finish();

        msg.append(" Set Latitude: " + latitude + "\n");
        msg.append(" Set Longitude: " + longitude + "\n");
        msg.append(" Set Altitude: " + altitude+ "\n");

        display(msg.toString());
    }

    private void display(String s) {
        Log.d(MSG_TAG, s);
        messageToBeSent = s;
        mHandler.sendEmptyMessage(REPORT_MSG);
    }

    private void displayError(String string) {
        Log.d(ERROR_TAG, "Error Message: "+string);
        messageToBeSent = string;
        mHandler.sendEmptyMessage(ERROR_MSG);
    }

    @Override
    public void onDestroy() {
        // power management: let the CPU go to sleep
        wakeLock.release();

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

        stopped = true;
        // Unregister all callbacks.
        mCallbacks.kill();

        mHandler.removeMessages(REPORT_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);
    }
}
