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

package com.oracle.iot.sample.obd;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Toast;
import com.oracle.iot.sample.*;
import oracle.iot.client.DeviceModel;
import oracle.iot.client.device.Alert;
import oracle.iot.client.device.DirectlyConnectedDevice;
import oracle.iot.client.device.VirtualDevice;
import java.io.FileOutputStream;
import java.io.IOException;

import java.io.IOException;
import java.security.GeneralSecurityException;

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

    private Obd2Device obd2Device;
    private Context context;

    private boolean messageSent = false;
    private String messageToBeSent = "";
    BluetoothCommManager bluetoothCommManager;
    PowerManager.WakeLock wakeLock;
    ObdSensor.ObdDeviceType obdDeviceType;

    MessageIntf messageIntf = new MessageIntf() {
        @Override
        public void displayMessage(String message) {
            Log.d(MSG_TAG, message);
            messageToBeSent = message;
            messageSent = false;
            mHandler.sendEmptyMessage(DATA_MSG);
        }

        public void displayStatus(String message) {
            Log.d(MSG_TAG, message);
            messageToBeSent = message;
            messageSent = false;
            mHandler.sendEmptyMessage(STATUS_MSG);
        }

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

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

    NotificationManager mNM;
    LocationManager locationManager;
    String provider;
    GpsLocationListener gpsLocationListener;
    BluetoothDevice obdBluetoothDevice;

    void startGpsListener() {
        // Set Location Manager to get GPS location of device
        locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        gpsLocationListener = GpsLocationListener.getInstance();

        Criteria criteria = new Criteria();
        criteria.setPowerRequirement(Criteria.POWER_LOW); // Chose your desired power consumption level.
        criteria.setAccuracy(Criteria.ACCURACY_FINE); // Choose your accuracy requirement.
        criteria.setSpeedRequired(true); // Chose if speed for first location fix is required.
        criteria.setAltitudeRequired(false); // Choose if you use altitude.
        criteria.setBearingRequired(false); // Choose if you use bearing.
        provider = locationManager.getBestProvider(criteria, true);
        Location lastKnownLocation = findCurrentLocation();
        if (lastKnownLocation != null) {
            gpsLocationListener = GpsLocationListener.getInstance();
            gpsLocationListener.onLocationChanged(lastKnownLocation);
        }
        locationManager.requestLocationUpdates(provider, 0, 0, gpsLocationListener);
    }

    private Location findCurrentLocation() {
        Location currentLocation;
        // Get the current/last known location.
        currentLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
        if (currentLocation == null) {
            currentLocation = locationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);
            if (currentLocation == null) {
                currentLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
            }
        }
        return currentLocation;
    }


    @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 (IObdDeviceService.class.getName().equals(intent.getAction())) {
            return mBinder;
        }
        return null;
    }

    boolean useDevicePolicy = false;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        obdBluetoothDevice = intent.getParcelableExtra("ObdDevice");
        String deviceTypeName = intent.getStringExtra("ObdDeviceType");
        obdDeviceType = ObdSensor.ObdDeviceType.valueOf(deviceTypeName);
        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 = getApplicationContext();
        //Toast.makeText(this, "Service started now.", Toast.LENGTH_LONG).show();

        SharedPreferences mSharedPref = getApplicationContext().getSharedPreferences(
                getString(R.string.preference_file_key), Context.MODE_PRIVATE);
        useDevicePolicy = mSharedPref.getBoolean("useDevicePolicy", false);

        startGpsListener();

        // Create the directly-connected device instance
        try {
            if (mSharedPref.getBoolean(getString(R.string.use_provided_bks), false)) {
                obd2Device = new Obd2Device(context);
            } else {
                SharedPreferences pref = getApplicationContext().getSharedPreferences(
                    getString(R.string.preference_file_key), Context.MODE_PRIVATE);
                String taStoreFile = mSharedPref.getString(getString(R.string.ta_file_path), "");
                String taPassword = mSharedPref.getString(getString(R.string.ta_password), "");
                try (FileOutputStream fos = openFileOutput(getString(R.string.last_known), Context.MODE_PRIVATE)) {
                    fos.write(taStoreFile.getBytes());
                    Log.d(ERROR_TAG, "Write file " + pref.getString(getString(R.string.ta_file_path), ""));
                } catch (IOException ex) {
                    Log.d(ERROR_TAG, "Can not safe to internal storage");
                }
                obd2Device = new Obd2Device(taStoreFile, taPassword);
            }
            obd2Device.start();
        }
        catch (Exception ex) {
            messageIntf.displayError(ex.getMessage());
        }

        return START_NOT_STICKY;
    }

     /**
       * The IObdDeviceService Interface is defined through IDL
       */
    private final IObdDeviceService.Stub mBinder =  new IObdDeviceService.Stub() {
        public void registerCallback(IObdDeviceServiceCallback cb) {
            if (cb != null) mCallbacks.register(cb);
        }
        public void unregisterCallback(IObdDeviceServiceCallback cb) {
            if (cb != null) mCallbacks.unregister(cb);
        }
    };
    /**
     * A secondary DCD Sample Service interface to the service.
     */

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

    private static final int DATA_MSG = 1;
    private static final int STATUS_MSG = 2;
    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 DATA_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 STATUS_MSG: {
                    // Broadcast to all clients the new value.
                    final int N = mCallbacks.beginBroadcast();
                    for (int i=0; i<N; i++) {
                        try {
                            mCallbacks.getBroadcastItem(i).statusChanged(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                                                                                                                                                                                                                                                                                                                                                                                                                               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);
            }
        }
    };

    @Override
    public void onDestroy() {
        obd2Device.stopDeviceThread();
        obd2Device.interrupt();
        try {
            obd2Device.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.obd_service_stopped, Toast.LENGTH_SHORT).show();

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

        mHandler.removeMessages(DATA_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.obd_service_started) :
                getText(R.string.obd_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.obd_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.obd_service_started :
                R.string.obd_service_stopped, notification);
    }

    public class Obd2Device extends Thread {

        String assetFileName;
        String password;
        DirectlyConnectedDevice dcd;
        Context context;
        ObdSensor sensor;
        VirtualDevice virtualDevice;
        Alert alert;
        DeviceModel dcdModel;
        BluetoothCommManager bluetoothCommManager;

        boolean isContextAvailable = false;

        void initDevice() throws IOException, GeneralSecurityException, InterruptedException {
            String filename = assetFileName.replaceAll(".*/","");
            messageIntf.displayStatus("INFO: Connecting to IOT CS \n");
            if (isContextAvailable) {
                dcd = new DirectlyConnectedDevice(context);
            } else {
                dcd = new DirectlyConnectedDevice(assetFileName, password,context);
            }
            if (!dcd.isActivated()) {
                messageIntf.displayStatus("Activating: " + filename + "\n");
                dcd.activate(ObdDeviceModelAttributes.OBD2_SENSOR_MODEL_URN.toString());
            }

            dcdModel = dcd.getDeviceModel(ObdDeviceModelAttributes.OBD2_SENSOR_MODEL_URN.toString());
            virtualDevice = dcd.createVirtualDevice(dcd.getEndpointId(), dcdModel);
            messageIntf.displayStatus("INFO: Connected to IOT CS. \n");

            bluetoothCommManager = BluetoothCommManager.getInstance();
            sensor = new ObdSensor(virtualDevice, virtualDevice.getEndpointId(), messageIntf,
                            obdBluetoothDevice, obdDeviceType,useDevicePolicy);
        }

        Obd2Device(String assetFileName, String password) throws GeneralSecurityException, IOException {
            this.assetFileName = assetFileName;
            this.password = password;
        }

        Obd2Device(Context context) throws GeneralSecurityException, IOException {
            this.context = context;
            isContextAvailable = true;
        }

        public void run() {
            try {
                initDevice();
                runDeviceThread();
            } catch (Exception ex) {
                if (ex.getMessage() != null) {
                    messageIntf.displayError(ex.getMessage());
                } else {
                    messageIntf.displayError("Unknown Error");
                }
            }
        }

        boolean stopped = false;

        public void stopDeviceThread() {
            stopped = true;
        }

        public void runDeviceThread() {
            while (!stopped && !isInterrupted()) {
                try {
                    String displayMsg = sensor.processObdEvents();
                    /*if (!displayMsg.trim().isEmpty()) {
                        messageIntf.displayStatus(displayMsg);
                    }*/
                    // Wait 1 second before sending next reading.
                    Thread.sleep(1000);
                } catch (Exception ex) {
                    if (ex.getMessage() != null) {
                        messageIntf.displayError(ex.getMessage());
                    } else {
                        messageIntf.displayError("Unknown Error.");
                    }
                }
            }
            sensor.discConnect();
        }
    }

    interface MessageIntf {
        void displayMessage(String message);
        void displayStatus(String message);
        void displayError(String string);
    }
}

