package com.eveningoutpost.dexdrip.Models;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;

import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils;
import com.eveningoutpost.dexdrip.Models.UserError.Log;
import com.eveningoutpost.dexdrip.UtilityModels.AlertPlayer;
import com.eveningoutpost.dexdrip.UtilityModels.Notifications;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;
import com.google.gson.internal.bind.DateTypeAdapter;

import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.UUID;

//KS import com.eveningoutpost.dexdrip.Services.ActivityRecognizedService;
//KS import com.eveningoutpost.dexdrip.Services.MissedReadingService;

/**
 * Created by Emma Black on 1/14/15.
 */
@Table(name = "AlertType", id = BaseColumns._ID)
public class AlertType extends Model {

    @Expose
    @Column(name = "name")
    public String name;

    @Expose
    @Column(name = "active")
    public boolean active;

    @Expose
    @Column(name = "volume")
    public int volume;

    @Expose
    @Column(name = "vibrate")
    public boolean vibrate;

    @Expose
    @Column(name = "light")
    public boolean light;

    @Expose
    @Column(name = "override_silent_mode")
    public boolean override_silent_mode;

    @Expose
    @Column(name = "predictive")
    public boolean predictive;

    @Expose
    @Column(name = "time_until_threshold_crossed")
    public double time_until_threshold_crossed;

    // If it is not above, then it must be below.
    @Expose
    @Column(name = "above")
    public boolean above;

    @Expose
    @Column(name = "threshold")
    public double threshold;

    @Expose
    @Column(name = "all_day")
    public boolean all_day;

    @Expose
    @Column(name = "start_time_minutes")
    public int start_time_minutes;  // This have probable be in minutes from start of day. this is not time...

    @Expose
    @Column(name = "end_time_minutes")
    public int end_time_minutes;

    @Expose
    @Column(name = "minutes_between") //??? what is the difference between minutes_between and default_snooze ???
    public int minutes_between; // The idea here was if ignored it will go off again each x minutes, snooze would be if it was aknowledged and dismissed it will go off again in y minutes
    // that said, Im okay with doing away with the minutes between and just doing it at a set 5 mins like dex

    @Expose
    @Column(name = "default_snooze")
    public int default_snooze;

    @Expose
    @Column(name = "text") // ??? what's that? is it different from name?
    public String text; // I figured if we wanted some special text, Its

    @Expose
    @Column(name = "mp3_file")
    public String mp3_file;

    @Expose
    @Column(name = "uuid", index = true)
    public String uuid;

    public final static String LOW_ALERT_55 = "c5f1999c-4ec5-449e-adad-3980b172b920";
    private final static String TAG = Notifications.class.getSimpleName();
    private final static String TAG_ALERT = "AlertBg";
    private static boolean patched = false;

    // This shouldn't be needed but it seems it is
    private static void fixUpTable() {
        if (patched) return;
        String[] patchup = {
                "CREATE TABLE AlertType (_id INTEGER PRIMARY KEY AUTOINCREMENT);",
                "ALTER TABLE AlertType ADD COLUMN above INTEGER;",
                "ALTER TABLE AlertType ADD COLUMN active INTEGER;",
                "ALTER TABLE AlertType ADD COLUMN all_day INTEGER;",
                "ALTER TABLE AlertType ADD COLUMN default_snooze INTEGER;",
                "ALTER TABLE AlertType ADD COLUMN end_time_minutes INTEGER;",
                "ALTER TABLE AlertType ADD COLUMN minutes_between INTEGER;",
                "ALTER TABLE AlertType ADD COLUMN override_silent_mode INTEGER;",
                "ALTER TABLE AlertType ADD COLUMN start_time_minutes INTEGER;",
                "ALTER TABLE AlertType ADD COLUMN vibrate INTEGER;",
                "ALTER TABLE AlertType ADD COLUMN mp3_file TEXT;",
                "ALTER TABLE AlertType ADD COLUMN threshold REAL;",
                "ALTER TABLE AlertType ADD COLUMN uuid TEXT;",
                "CREATE INDEX index_AlertType_uuid on AlertType(uuid);",
                "ALTER TABLE AlertType ADD COLUMN volume INTEGER;",
                "ALTER TABLE AlertType ADD COLUMN light INTEGER;",
                "ALTER TABLE AlertType ADD COLUMN predictive INTEGER;",
                "ALTER TABLE AlertType ADD COLUMN text TEXT;",
                "ALTER TABLE AlertType ADD COLUMN time_until_threshold_crossed REAL;"
        };


        for (String patch : patchup) {
            try {
                SQLiteUtils.execSql(patch);
                Log.e(TAG, "Processed patch should not have succeeded!!: " + patch);
            } catch (Exception e) {
                // Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString());
            }
        }
        patched = true;
    }



    public static AlertType get_alert(String uuid) {

        try {
            return new Select()
            .from(AlertType.class)
            .where("uuid = ? ", uuid)
            .executeSingle();
        }
        catch (Exception e) {
            return null;
        }
    }

    /*
     * This function has 3 needs. In the case of "unclear state" return null
     * In the case of "unclear state" for more than predefined time, return the "55" alert
     * In case that alerts are turned off, only return the 55.
     */
    public static AlertType get_highest_active_alert(Context context, double bg) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        if(prefs.getLong("alerts_disabled_until", 0) > new Date().getTime()){
            Log.d("NOTIFICATIONS", "Notifications are currently disabled!!");
            return null;
        }

        if (bg <= 14) { // Special dexcom codes should not set off low alarms
            return null;
        }

        AlertType at;
        at = get_highest_active_alert_helper(bg, prefs);
        if (at != null) {
            Log.d(TAG_ALERT, "get_highest_active_alert_helper returned alert uuid = " + at.uuid + " alert name = " + at.name);
        } else {
            Log.d(TAG_ALERT, "get_highest_active_alert_helper returned NULL");
        }
        return at;
    }

    private static AlertType filter_alert_on_stale(AlertType alert, SharedPreferences prefs)
    {
        // this should already be happening in notifications.java but it doesn't seem to work so adding here as well
        if (prefs.getBoolean("disable_alerts_stale_data", false)) {
            final int stale_minutes = Math.max(6, Integer.parseInt(prefs.getString("disable_alerts_stale_data_minutes", "15")) + 2);
            if (!BgReading.last_within_minutes(stale_minutes)) {
                Log.w(TAG, "Blocking alarm raise as data older than: " + stale_minutes);
                return null; // block
            }
        }
        return alert; // allow
    }

    // bg_minute is the estimatin of the bg change rate
    private static AlertType get_highest_active_alert_helper(double bg, SharedPreferences prefs) {
        // Chcek the low alerts

        final double offset = 0;//KS TODO ActivityRecognizedService.raise_limit_due_to_vehicle_mode() ? ActivityRecognizedService.getVehicle_mode_adjust_mgdl() : 0;

        if(prefs.getLong("low_alerts_disabled_until", 0) > new Date().getTime()){
            Log.i("NOTIFICATIONS", "get_highest_active_alert_helper: Low alerts are currently disabled!! Skipping low alerts");

        } else {
            List<AlertType> lowAlerts  = new Select()
                    .from(AlertType.class)
                    .where("threshold >= ?", bg-offset)
                    .where("above = ?", false)
                    .orderBy("threshold asc")
                    .execute();

            for (AlertType lowAlert : lowAlerts) {
                if(lowAlert.should_alarm(bg-offset)) {
                    return filter_alert_on_stale(lowAlert,prefs);
                }
            }
        }


        // If no low alert found or low alerts disabled, check higher alert.
        if(prefs.getLong("high_alerts_disabled_until", 0) > new Date().getTime()){
            Log.i("NOTIFICATIONS", "get_highest_active_alert_helper: High alerts are currently disabled!! Skipping high alerts");
            ;
        } else {
            List<AlertType> HighAlerts  = new Select()
                    .from(AlertType.class)
                    .where("threshold <= ?", bg)
                    .where("above = ?", true)
                    .orderBy("threshold desc")
                    .execute();

            for (AlertType HighAlert : HighAlerts) {
                //Log.e(TAG, "Testing high alert " + HighAlert.toString());
                if(HighAlert.should_alarm(bg)) {
                    return filter_alert_on_stale(HighAlert,prefs);
                }
            }
        }
        // no alert found
        return null;
    }

    // returns true, if one allert is up and the second is down
    public static boolean OpositeDirection(AlertType a1, AlertType a2) {
        if (a1.above != a2.above) {
            return true;
        }
        return false;
    }

    // Checks if a1 is more important than a2. returns the higher one
    public static AlertType HigherAlert(AlertType a1, AlertType a2) {
        if (a1.above && !a2.above) {
            return a2;
        }
        if (!a1.above && a2.above) {
            return a1;
        }
        if (a1.above && a2.above) {
            // both are high, the higher the better
            if (a1.threshold > a2.threshold) {
                return a1;
            } else {
                return a2;
            }
        }
        if (a1.above || a2.above) {
            Log.wtf(TAG, "a1.above and a2.above must be false");
        }
        // both are low, the lower the better
        if (a1.threshold < a2.threshold) {
            return a1;
        } else {
            return a2;
        }
    }

    public static void remove_all() {
        fixUpTable();
        List<AlertType> Alerts  = new Select()
        .from(AlertType.class)
        .execute();

        for (AlertType alert : Alerts) {
            alert.delete();
        }
        ActiveBgAlert.ClearData();
    }

    public static void add_alert(
            String uuid,
            String name,
            boolean above,
            double threshold,
            boolean all_day,
            int minutes_between,
            String mp3_file,
            int start_time_minutes,
            int end_time_minutes,
            boolean override_silent_mode,
            int snooze,
            boolean vibrate,
            boolean active) {
        AlertType at = new AlertType();
        at.name = name;
        at.above = above;
        at.threshold = threshold;
        at.all_day = all_day;
        at.minutes_between = minutes_between;
        at.uuid = uuid != null? uuid : UUID.randomUUID().toString();
        at.active = active;
        at.mp3_file = mp3_file;
        at.start_time_minutes = start_time_minutes;
        at.end_time_minutes = end_time_minutes;
        at.override_silent_mode = override_silent_mode;
        at.default_snooze = snooze;
        at.vibrate = vibrate;
        at.save();
    }

    public static void update_alert(
            String uuid,
            String name,
            boolean above,
            double threshold,
            boolean all_day,
            int minutes_between,
            String mp3_file,
            int start_time_minutes,
            int end_time_minutes,
            boolean override_silent_mode,
            int snooze,
            boolean vibrate,
            boolean active) {

        fixUpTable();

        final AlertType at = get_alert(uuid);
        if (at == null) {
            Log.e(TAG, "Alert Type null during update");
            return;
        }
        at.name = name;
        at.above = above;
        at.threshold = threshold;
        at.all_day = all_day;
        at.minutes_between = minutes_between;
        at.uuid = uuid;
        at.active = active;
        at.mp3_file = mp3_file;
        at.start_time_minutes = start_time_minutes;
        at.end_time_minutes = end_time_minutes;
        at.override_silent_mode = override_silent_mode;
        at.default_snooze = snooze;
        at.vibrate = vibrate;
        at.save();
    }
    public static void remove_alert(String uuid) {
        AlertType alert = get_alert(uuid);
		if(alert != null) {
	        alert.delete();
        }
    }

    public String toString() {

        String name = "name: " + this.name;
        String above = "above: " + this.above;
        String threshold = "threshold: " + this.threshold;
        String all_day = "all_day: " + this.all_day;
        String time = "Start time: " + this.start_time_minutes + " end time: "+ this.end_time_minutes;
        String minutes_between = "minutes_between: " + this.minutes_between;
        String uuid = "uuid: " + this.uuid;

        return name + " " + above + " " + threshold + " "+ all_day + " " +time +" " + minutes_between + " uuid" + uuid;
    }

    public static void print_all() {
        List<AlertType> Alerts  = new Select()
            .from(AlertType.class)
            .execute();

        Log.d(TAG,"List of all alerts");
        for (AlertType alert : Alerts) {
            Log.d(TAG, alert.toString());
        }
    }

    public String toS() {
        Gson gson = new GsonBuilder()
                .excludeFieldsWithoutExposeAnnotation()
                .registerTypeAdapter(Date.class, new DateTypeAdapter())
                .serializeSpecialFloatingPointValues()
                .create();
        return gson.toJson(this);
    }

    public static List<AlertType> getAll(boolean above) {
        String order;
        if (above) {
            order = "threshold asc";
        } else {
            order = "threshold desc";
        }
        List<AlertType> alerts  = new Select()
            .from(AlertType.class)
            .where("above = ?", above)
            .orderBy(order)
            .execute();

        return alerts;
    }

    public static List<AlertType> getAllActive() {
        List<AlertType> alerts  = new Select()
                .from(AlertType.class)
                .where("active = ?", true)
                .execute();

        return alerts;
    }

    public static boolean activeLowAlertExists() {
        List<AlertType> alerts = getAll(false);
        if(alerts == null) {
            return false;
        }
        for (AlertType alert : alerts) {
            if(alert.active) {
                return true;
            }
        }
        return false;
    }

    // This function is used to make sure that we always have a static alert on 55 low.
    // This alert will not be editable/removable.
    public static void CreateStaticAlerts() {
        if(get_alert(LOW_ALERT_55) == null) {
            add_alert(LOW_ALERT_55, "low alert ", false, 55, true, 1, null, 0, 0, true, 20, true, true);
        }
    }


    public static void testAll(Context context) {
        remove_all();
        add_alert(null, "high alert 1", true, 180, true, 10, null, 0, 0, true, 20, true, true);
        add_alert(null, "high alert 2", true, 200, true, 10, null, 0, 0, true, 20, true, true);
        add_alert(null, "high alert 3", true, 220, true, 10, null, 0, 0, true, 20, true, true);
        print_all();
        AlertType a1 = get_highest_active_alert(context, 190);
        Log.d(TAG, "a1 = " + a1.toString());
        AlertType a2 = get_highest_active_alert(context, 210);
        Log.d(TAG, "a2 = " + a2.toString());


        AlertType a3 = get_alert(a1.uuid);
        Log.d(TAG, "a1 == a3 ? need to see true " + (a1==a3) + a1 + " " + a3);

        add_alert(null, "low alert 1", false, 80, true, 10, null, 0, 0, true, 20, true, true);
        add_alert(null, "low alert 2", false, 60, true, 10, null, 0, 0, true, 20, true, true);

        AlertType al1 = get_highest_active_alert(context, 90);
        Log.d(TAG, "al1 should be null  " + al1);
        al1 = get_highest_active_alert(context, 80);
        Log.d(TAG, "al1 = " + al1.toString());
        AlertType al2 = get_highest_active_alert(context, 50);
        Log.d(TAG, "al2 = " + al2.toString());

        Log.d(TAG, "HigherAlert(a1, a2) = a1?" +  (HigherAlert(a1,a2) == a2));
        Log.d(TAG, "HigherAlert(al1, al2) = al1?" +  (HigherAlert(al1,al2) == al2));
        Log.d(TAG, "HigherAlert(a1, al1) = al1?" +  (HigherAlert(a1,al1) == al1));
        Log.d(TAG, "HigherAlert(al1, a2) = al1?" +  (HigherAlert(al1,a2) == al1));

        // Make sure we do not influance on real data...
        remove_all();

    }


    private boolean in_time_frame() {
        return s_in_time_frame(all_day, start_time_minutes, end_time_minutes);
    }
    
    static public boolean  s_in_time_frame(boolean s_all_day, int s_start_time_minutes, int s_end_time_minutes) {
        if (s_all_day) {
            //Log.e(TAG, "in_time_frame returning true " );
            return true;
        }
        // time_now is the number of minutes that have passed from the start of the day.
        Calendar rightNow = Calendar.getInstance();
        int time_now = toTime(rightNow.get(Calendar.HOUR_OF_DAY), rightNow.get(Calendar.MINUTE));
        Log.d(TAG, "time_now is " + time_now + " minutes" + " start_time " + s_start_time_minutes + " end_time " + s_end_time_minutes);
        if(s_start_time_minutes < s_end_time_minutes) {
            if (time_now >= s_start_time_minutes && time_now <= s_end_time_minutes) {
                return true;
            }
        } else {
            if (time_now >= s_start_time_minutes || time_now <= s_end_time_minutes) {
                return true;
            }
        }
        return false;
    }

    private boolean beyond_threshold(double bg) {
        if (above && bg >= threshold) {
//            Log.e(TAG, "beyond_threshold returning true " );
            return true;
        } else if (!above && bg <= threshold) {
            return true;
        }
        return false;
    }

    private boolean trending_to_threshold(double bg) {
        if (!predictive) { return false; }
        if (above && bg >= threshold) {
            return true;
        } else if (!above && bg <= threshold) {
            return true;
        }
        return false;
    }
    
     public long getNextAlertTime(Context ctx) {
         int time = minutes_between;
         if (time < 1 || AlertPlayer.isAscendingMode(ctx)) {
             time = 1;
         }
         Calendar calendar = Calendar.getInstance();
         return calendar.getTimeInMillis() + (time * 60000);
     }

    public boolean should_alarm(double bg) {
//        Log.e(TAG, "should_alarm called active =  " + active );
        if(in_time_frame() && active && (beyond_threshold(bg) || trending_to_threshold(bg))) {
            return true;
        } else {
            return false;
        }
    }

    public static void testAlert(
        String name,
        boolean above,
        double threshold,
        boolean all_day,
        int minutes_between,
        String mp3_file,
        int start_time_minutes,
        int end_time_minutes,
        boolean override_silent_mode,
        int snooze,
        boolean vibrate,
        Context context) {
            AlertType at = new AlertType();
            at.name = name;
            at.above = above;
            at.threshold = threshold;
            at.all_day = all_day;
            at.minutes_between = minutes_between;
            at.uuid = UUID.randomUUID().toString();
            at.active = true;
            at.mp3_file = mp3_file;
            at.start_time_minutes = start_time_minutes;
            at.end_time_minutes = end_time_minutes;
            at.override_silent_mode = override_silent_mode;
            at.default_snooze = snooze;
            at.vibrate = vibrate;
            AlertPlayer.getPlayer().startAlert(context, false, at, "TEST", false);
    }

    // Time is calculated in minutes. that is 01:20 means 80 minutes.

    // This functions are a bit tricky. We can only set time from 00:00 to 23:59 which leaves one minute out. this is because we ignore the
    // seconds. so if the user has set 23:59 we will consider this as 24:00
    // This will be done at the code that reads the time from the ui.



    // return the minutes part of the time
    public static int time2Minutes(int minutes) {
        return (minutes - 60*time2Hours(minutes)) ;
    }

 // return the hours part of the time
    public static int time2Hours(int minutes) {
        return minutes / 60;
    }

    // create the time from hours and minutes.
    public static int toTime(int hours, int minutes) {
        return hours * 60 + minutes;
    }
    
    // Convert all settings to a string and save it in the references. This is needed to allow it's backup. 
    public static boolean toSettings(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        List<AlertType> alerts  = new Select()
            .from(AlertType.class)
            .execute();

        Gson gson = new GsonBuilder()
                .excludeFieldsWithoutExposeAnnotation()
                .registerTypeAdapter(Date.class, new DateTypeAdapter())
                .serializeSpecialFloatingPointValues()
                .create();
        String output =  gson.toJson(alerts);
        Log.e(TAG, "Created the string " + output);
        prefs.edit().putString("saved_alerts", output).commit();

        return true;

    }


    // Read all alerts from preference key and write them to db.
    public static boolean fromSettings(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        String savedAlerts = prefs.getString("saved_alerts", "");
        if (savedAlerts.isEmpty()) {
            Log.i(TAG, "read saved_alerts string and it is empty");
            return true;
        }
        Log.i(TAG, "read alerts string " + savedAlerts);

        AlertType[] newAlerts = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(savedAlerts, AlertType[].class);
        if (newAlerts == null) {
            Log.e(TAG, "newAlerts is null");
            return true;
        }

        Log.i(TAG, "read successfuly " + newAlerts.length);
        // Now delete all existing alerts if we managed to unpack the json
        try {
            List<AlertType> alerts = new Select()
                    .from(AlertType.class)
                    .execute();
            for (AlertType alert : alerts) {
                alert.delete();
            }
        } catch (NullPointerException e) {
            Log.e(TAG, "Got null pointer exception: " + e);
        }

        try {
            for (AlertType alert : newAlerts) {
                Log.e(TAG, "Saving alert " + alert.name);
                alert.save();
            }
        } catch (NullPointerException e) {
            Log.e(TAG, "Got null pointer exception 2: " + e);
        }
        // Delete the string, so next time we will not load the data
        prefs.edit().putString("saved_alerts", "").apply();
        return true;

    }
    
}