package com.notificationFramework.stimulusStrategy;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.IBinder;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

import com.notificationFramework.sedentary.frontEnd.R;
import com.notificationFramework.sedentary.frontEnd.SaveFile;

import java.util.Calendar;


/**
 * Created by Peter De Jonckheere on 09/01/2018.
 * <p>
 * The strategy which detects sedentary behaviour using the Accelerometer sensor provided by the
 * Android hardware. It uses the previous readings to determine if movement has occurred. Upon
 * determining a significant enough movement which lasts for a period of 15 seconds, an alarm which
 * is set for one notification period from the current time is cancelled. The Accelerometer sensor
 * reports in a streaming fashion and so uses significant power on the device and the listener for
 * the sensor is constantly registered.
 * </p>
 */

public class Accelerometer extends Service implements StimulusStrategy, SensorEventListener {

    /**
     * The alarm manager instance which is used to manage alarms via the OS
     */
    private AlarmManager am;
    /**
     * An instance of Shared Preferences used throughout the class to obtain and save settings
     */
    private SharedPreferences preferences;
    /**
     * The previous 3 readings of the accelerometer
     */
    private float[] history = new float[3];
    /**
     * The previous timestamp of the accelerometer reading
     */
    private long historyTime;
    /**
     * The last recorded number of minutes moved
     */
    private int prevMinutes = 0;
    /**
     * The current number of minutes moved
     */
    private int minutes;
    /**
     * The current number of seconds moved up to 60
     */
    private double seconds = 0;
    /**
     * The arbitrary default value of sensitivity
     */
    private double sensitivity = 0.8;

    /**
     * An enum which defines values which can be used to alter the sensitivity from within the
     * application
     */
    private enum sensitivityLevel {
        ZERO, VERY_HIGH, HIGH, NORMAL, LOW, VERY_LOW;
    }

    /**
     * The method which is called when the Accelerometer service is started. Sets up a number of
     * fields then delegates for the daily progress and clock to be set up.
     *
     * @param intent  the intent used to start this service
     * @param flags   additional information about this service
     * @param startId the unique identifier for this service
     * @return the conditions under which the OS should treat this service
     * @see android.app.Service
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        historyTime = SystemClock.elapsedRealtimeNanos();
        preferences = getSharedPreferences(getString(R.string.preference_file_key),
                Context.MODE_PRIVATE);
        sensitivity = sensitivity * sensitivityLevel.valueOf(
                preferences.getString(getString(R.string.accel_sensitivity), "NORMAL")).ordinal();
        SensorManager mSensorManager =
                (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);
        //1 second used as the sampling period
        try {
            mSensorManager.registerListener(this,
                    mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
                    600000000);
        } catch (NullPointerException e) {
            Log.e("SENSOR", "FAILED TO FIND ACCELEROMTER");
        }
        am = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
        setUpDailyProgress();
        setUpClock();
        return START_STICKY;
    }

    /**
     * Uses the Shared Preferences to set up the daily progress and determine if the daily progress
     * should be reset for a new day. This is done by storing the current day in the Shared
     * Preferences and only changing this when the current calendar does not match this day.
     */
    private void setUpDailyProgress() {
        int currentDay = Calendar.getInstance().get(Calendar.DAY_OF_YEAR);
        if (currentDay == preferences.getInt(getString(R.string.progress_day), 0)) {
            minutes = preferences.getInt(getString(R.string.daily_progress), 0);
            if (minutes >= preferences.getInt(getString(R.string.daily_goal_set), getResources().getInteger(R.integer.daily_goal_minutes))
                    && !(preferences.getBoolean((getString(R.string.goal_met)), false))) {
                goalNotify();
                preferences.edit().putBoolean(getString(R.string.goal_met), true).commit();
            }
        } else {
            minutes = 0;
            SharedPreferences.Editor editor = preferences.edit();
            editor.putInt(getString(R.string.progress_day), currentDay);
            editor.putInt(getString(R.string.daily_progress), minutes);
            editor.putBoolean(getString(R.string.goal_met), false);
            editor.commit();
        }
        prevMinutes = minutes;
    }

    /**
     * Sets up the alarm which will trigger the broadcast to the stimulus class, hence triggering a
     * notification. The true implementation uses the notification period stored in Shared
     * Preferences and a test implementation is also present which uses an arbitrary short time.
     */
    private void setUpClock() {
        Intent i = new Intent(getBaseContext(),
                com.notificationFramework.stimulus.SedentaryStimulus.class);
        PendingIntent pi = PendingIntent.getBroadcast(getBaseContext(),
                R.integer.alarm_rc, i, PendingIntent.FLAG_UPDATE_CURRENT);
        int interval = preferences.getInt(getString(R.string.daily_goal), getResources().getInteger(R.integer.notify_period_minutes));
        am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                SystemClock.elapsedRealtime() + (1000 * 60 * interval), pi);
        //test
        // am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 3000, pi);
    }


    /**
     * The monitor method is called when a movement is identified and uses identical intents and
     * pending intents to cancel the alarm set previously. This is also recorded as a movement in
     * the log and a new alarm is set.
     */
    public void monitor() {
        Intent i = new Intent(getBaseContext(),
                com.notificationFramework.stimulus.SedentaryStimulus.class);
        PendingIntent pi = PendingIntent.getBroadcast(getBaseContext(),
                R.integer.alarm_rc, i, PendingIntent.FLAG_UPDATE_CURRENT);
        am.cancel(pi);
        SaveFile.recordNotification(0, 0, 1, this);
        setUpClock();
    }

    /**
     * A method added after the user trial to trigger once daily if a goal has been reached. A local
     * broadcast is then sent to the relevant stimulus class and on to the relevant notification
     * class.
     */
    public void goalNotify() {
        Intent i = new Intent(getBaseContext(),
                com.notificationFramework.stimulus.GoalStimulus.class);
        SaveFile.recordNotification(1, 1, 0, this);
        LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
        lbm.sendBroadcast(i);
    }

    /**
     * Required to be implemented by implementing the SensorEventListener interface. Carries out the
     * main work of the class in determining if a movement has taken place based on the values
     * passed in in the event param and the previous values stored. This is done by determining if
     * multiple values have changed either positively or negatively by the sensitivity level. In
     * this case the timestamp is then used to determine the length of time for which the movement
     * lasts and this is recorded as the minute value in Shared Preferences.
     *
     * @param event the sensor event which has occurred (here an Accelerometer event)
     * @see android.hardware.SensorEventListener
     */
    @Override
    public void onSensorChanged(SensorEvent event) {
        int moved = 0;
        //For each x,y and z plane determine if significant movement has occurred.
        for (int i = 0; i < history.length; i++) {
            if ((-1 * sensitivity) > (history[i] - event.values[i])
                    || (history[i] - event.values[i]) > sensitivity) {
                if (!((history[i] - event.values[i]) > 10
                        && !(-10 > (history[i]) - event.values[i]))) {
                    moved++;
                }
            }
        }
        //Update the previous values
        for (int i = 0; i < history.length; i++) {
            history[i] = event.values[i];
        }
        //If all 3 planes had movement
        if (moved >= 3) {
            //Update seconds and cancel the alarm if the time is greater than the last seconds
            //value
            if (((event.timestamp - historyTime) / 1000000000) > (seconds + 15)) {
                seconds = seconds + 15;
                monitor();
            }
            //Update minutes if the time is greater than a minute or seconds has reached 60
            if ((((event.timestamp - historyTime) / 1000000000) > 60) || (seconds > 60)) {
                historyTime = event.timestamp;
                seconds = 0;
                minutes++;
            }
        }
        //If movement has not occurred update minutes
        else if (minutes > prevMinutes) {
            SharedPreferences.Editor editor = preferences.edit();
            editor.putInt(getString(R.string.daily_progress), minutes);
            editor.commit();
            prevMinutes = minutes;
        }
    }

    /**
     * Unused as accuracy is not a major factor when using previous values. Required by
     * SensorEventListener
     *
     * @param sensor as in SensorEventListener
     * @param i as in SensorEventListener
     * @see android.hardware.SensorEventListener
     */
    @Override
    public void onAccuracyChanged(Sensor sensor, int i) {

    }


    /**
     * The method which reponds to a bind request for this service.
     *
     * @param intent the intent used to rqeuest binding of this service
     * @return null as no bind requests are required.
     * @see android.app.Service
     **/
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}