package com.eveningoutpost.dexdrip.Models;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.PowerManager;
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.Delete;
import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils;
import com.eveningoutpost.dexdrip.BestGlucose;
import com.eveningoutpost.dexdrip.GcmActivity;
import com.eveningoutpost.dexdrip.Home;
import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.EGVRecord;
import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.SensorRecord;
import com.eveningoutpost.dexdrip.Models.UserError.Log;
import com.eveningoutpost.dexdrip.R;
import com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService;
import com.eveningoutpost.dexdrip.Services.SyncService;
import com.eveningoutpost.dexdrip.ShareModels.ShareUploadableBg;
import com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder;
import com.eveningoutpost.dexdrip.UtilityModels.BgSendQueue;
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
import com.eveningoutpost.dexdrip.UtilityModels.Inevitable;
import com.eveningoutpost.dexdrip.UtilityModels.Notifications;
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
import com.eveningoutpost.dexdrip.UtilityModels.UploaderQueue;
import com.eveningoutpost.dexdrip.UtilityModels.WholeHouse;
import com.eveningoutpost.dexdrip.calibrations.CalibrationAbstract;
import com.eveningoutpost.dexdrip.messages.BgReadingMessage;
import com.eveningoutpost.dexdrip.messages.BgReadingMultiMessage;
import com.eveningoutpost.dexdrip.utils.DexCollectionType;
import com.eveningoutpost.dexdrip.utils.SqliteRejigger;
import com.eveningoutpost.dexdrip.wearintegration.WatchUpdaterService;
import com.eveningoutpost.dexdrip.xdrip;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;
import com.google.gson.internal.bind.DateTypeAdapter;
import com.squareup.wire.Wire;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.UUID;

import static com.eveningoutpost.dexdrip.calibrations.PluggableCalibration.getCalibrationPluginFromPreferences;
import static com.eveningoutpost.dexdrip.calibrations.PluggableCalibration.newCloseSensorData;

@Table(name = "BgReadings", id = BaseColumns._ID)
public class BgReading extends Model implements ShareUploadableBg {

    private final static String TAG = BgReading.class.getSimpleName();
    private final static String TAG_ALERT = TAG + " AlertBg";
    private final static String PERSISTENT_HIGH_SINCE = "persistent_high_since";
    public static final double AGE_ADJUSTMENT_TIME = 86400000 * 1.9;
    public static final double AGE_ADJUSTMENT_FACTOR = .45;
    //TODO: Have these as adjustable settings!!
    public final static double BESTOFFSET = (60000 * 0); // Assume readings are about x minutes off from actual!

    public static final int BG_READING_ERROR_VALUE = 38; // error marker
    public static final int BG_READING_MINIMUM_VALUE = 39;
    public static final int BG_READING_MAXIMUM_VALUE = 400;

    private static volatile long earliest_backfill = 0;

    @Column(name = "sensor", index = true)
    public Sensor sensor;

    @Column(name = "calibration", index = true, onDelete = Column.ForeignKeyAction.CASCADE)
    public Calibration calibration;

    @Expose
    @Column(name = "timestamp", index = true)
    public long timestamp;

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

    @Expose
    @Column(name = "raw_data")
    public volatile double raw_data;

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

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

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

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

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

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

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

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

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

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

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

    @Expose
    @Column(name = "rc")
    public double rc;
    @Expose
    // TODO unification with wear support ConflictAction.REPLACE for wear, done with rejig below
    @Column(name = "uuid", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
    public String uuid;

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

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

    // mapped to the no longer used "synced" to keep DB Scheme compatible
    @Expose
    @Column(name = "snyced")
    public boolean ignoreForStats;

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

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

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

    @Expose
    @Column(name = "dg_mgdl")
    public double dg_mgdl = 0d;

    @Expose
    @Column(name = "dg_slope")
    public double dg_slope = 0d;

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

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

    public synchronized static void updateDB() {
        final String[] updates = new String[]{"ALTER TABLE BgReadings ADD COLUMN dg_mgdl REAL;",
                "ALTER TABLE BgReadings ADD COLUMN dg_slope REAL;",
                "ALTER TABLE BgReadings ADD COLUMN dg_delta_name TEXT;",
                "ALTER TABLE BgReadings ADD COLUMN source_info TEXT;"};
        for (String patch : updates) {
            try {
                SQLiteUtils.execSql(patch);
            } catch (Exception e) {
            }
        }

        // needs different handling on wear
        if (JoH.areWeRunningOnAndroidWear()) {
            BgSendQueue.emptyQueue();
            SqliteRejigger.rejigSchema("BgReadings", "uuid TEXT UNIQUE ON CONFLICT FAIL", "uuid TEXT UNIQUE ON CONFLICT REPLACE");
            SqliteRejigger.rejigSchema("BgReadings", "uuid TEXT UNIQUE ON CONFLICT IGNORE", "uuid TEXT UNIQUE ON CONFLICT REPLACE");
            SqliteRejigger.rejigSchema("BgSendQueue", "BgReadings_temp", "BgReadings");
        }

    }

    public double getDg_mgdl(){
        if(dg_mgdl != 0) return dg_mgdl;
        return calculated_value;
    }

    public double getDg_slope(){
        if(dg_mgdl != 0) return dg_slope;
        if(calculated_value_slope !=0) return calculated_value_slope;
        return currentSlope();
    }

    public String getDg_deltaName(){
        if(dg_mgdl != 0 && dg_delta_name != null) return dg_delta_name;
        return slopeName();
    }

    public double calculated_value_mmol() {
        return mmolConvert(calculated_value);
    }

    public void injectDisplayGlucose(BestGlucose.DisplayGlucose displayGlucose) {
        //displayGlucose can be null. E.g. when out of order values come in
        if (displayGlucose != null) {
            if (Math.abs(displayGlucose.timestamp - timestamp) < Constants.MINUTE_IN_MS * 10) {
                dg_mgdl = displayGlucose.mgdl;
                dg_slope = displayGlucose.slope;
                dg_delta_name = displayGlucose.delta_name;
                // TODO we probably should reflect the display glucose delta here as well for completeness
                this.save();
            } else {
                if (JoH.ratelimit("cannotinjectdg", 30)) {
                    UserError.Log.e(TAG, "Cannot inject display glucose value as time difference too great: " + JoH.dateTimeText(displayGlucose.timestamp) + " vs " + JoH.dateTimeText(timestamp));
                }
            }
        }
    }

    public double mmolConvert(double mgdl) {
        return mgdl * Constants.MGDL_TO_MMOLL;
    }

    public String displayValue(Context context) {
        final String unit = Pref.getString("units", "mgdl");
        final DecimalFormat df = new DecimalFormat("#");
        final double this_value = getDg_mgdl();
        if (this_value >= 400) {
            return "HIGH";
        } else if (this_value >= 40) {
            if (unit.equals("mgdl")) {
                df.setMaximumFractionDigits(0);
                return df.format(this_value);
            } else {
                df.setMaximumFractionDigits(1);
                return df.format(mmolConvert(this_value));
            }
        } else {
            return "LOW";
            // TODO doesn't understand special low values
        }
    }

    public static double activeSlope() {
        BgReading bgReading = BgReading.lastNoSenssor();
        if (bgReading != null) {
            double slope = (2 * bgReading.a * (new Date().getTime() + BESTOFFSET)) + bgReading.b;
            Log.i(TAG, "ESTIMATE SLOPE" + slope);
            return slope;
        }
        return 0;
    }

    public static double activePrediction() {
        BgReading bgReading = BgReading.lastNoSenssor();
        if (bgReading != null) {
            double currentTime = new Date().getTime();
            if (currentTime >= bgReading.timestamp + (60000 * 7)) {
                currentTime = bgReading.timestamp + (60000 * 7);
            }
            double time = currentTime + BESTOFFSET;
            return ((bgReading.a * time * time) + (bgReading.b * time) + bgReading.c);
        }
        return 0;
    }


    public static double calculateSlope(BgReading current, BgReading last) {
        if (current.timestamp == last.timestamp || current.calculated_value == last.calculated_value) {
            return 0;
        } else {
            return (last.calculated_value - current.calculated_value) / (last.timestamp - current.timestamp);
        }
    }

    public static double currentSlope() {
        return currentSlope(Home.get_follower());
    }

    public static double currentSlope(boolean is_follower) {
        List<BgReading> last_2 = BgReading.latest(2, is_follower);
        if ((last_2 != null) && (last_2.size() == 2)) {
            double slope = calculateSlope(last_2.get(0), last_2.get(1));
            return slope;
        } else {
            return 0d;
        }
    }


    //*******CLASS METHODS***********//
    // Dexcom Bluetooth Share
    public static void create(EGVRecord[] egvRecords, long addativeOffset, Context context) {
        for (EGVRecord egvRecord : egvRecords) {
            BgReading.create(egvRecord, addativeOffset, context);
        }
    }

    public static void create(SensorRecord[] sensorRecords, long addativeOffset, Context context) {
        for (SensorRecord sensorRecord : sensorRecords) {
            BgReading.create(sensorRecord, addativeOffset, context);
        }
    }

    public static void create(SensorRecord sensorRecord, long addativeOffset, Context context) {
        Log.i(TAG, "create: gonna make some sensor records: " + sensorRecord.getUnfiltered());
        if (BgReading.is_new(sensorRecord, addativeOffset)) {
            BgReading bgReading = new BgReading();
            Sensor sensor = Sensor.currentSensor();
            Calibration calibration = Calibration.getForTimestamp(sensorRecord.getSystemTime().getTime() + addativeOffset);
            if (sensor != null && calibration != null) {
                bgReading.sensor = sensor;
                bgReading.sensor_uuid = sensor.uuid;
                bgReading.calibration = calibration;
                bgReading.calibration_uuid = calibration.uuid;
                bgReading.raw_data = (sensorRecord.getUnfiltered() / 1000);
                bgReading.filtered_data = (sensorRecord.getFiltered() / 1000);
                bgReading.timestamp = sensorRecord.getSystemTime().getTime() + addativeOffset;
                if (bgReading.timestamp > new Date().getTime()) {
                    return;
                }
                bgReading.uuid = UUID.randomUUID().toString();
                bgReading.time_since_sensor_started = bgReading.timestamp - sensor.started_at;
                bgReading.calculateAgeAdjustedRawValue();
                bgReading.save();
            }
        }
    }

    // Dexcom Bluetooth Share
    public static void create(EGVRecord egvRecord, long addativeOffset, Context context) {
        BgReading bgReading = BgReading.getForTimestamp(egvRecord.getSystemTime().getTime() + addativeOffset);
        Log.i(TAG, "create: Looking for BG reading to tag this thing to: " + egvRecord.getBGValue());
        if (bgReading != null) {
            bgReading.calculated_value = egvRecord.getBGValue();
            if (egvRecord.getBGValue() <= 13) {
                Calibration calibration = bgReading.calibration;
                double firstAdjSlope = calibration.first_slope + (calibration.first_decay * (Math.ceil(new Date().getTime() - calibration.timestamp) / (1000 * 60 * 10)));
                double calSlope = (calibration.first_scale / firstAdjSlope) * 1000;
                double calIntercept = ((calibration.first_scale * calibration.first_intercept) / firstAdjSlope) * -1;
                bgReading.raw_calculated = (((calSlope * bgReading.raw_data) + calIntercept) - 5);
            }
            Log.i(TAG, "create: NEW VALUE CALCULATED AT: " + bgReading.calculated_value);
            bgReading.calculated_value_slope = bgReading.slopefromName(egvRecord.getTrend().friendlyTrendName());
            bgReading.noise = egvRecord.noiseValue();
            String friendlyName = egvRecord.getTrend().friendlyTrendName();
            if (friendlyName.compareTo("NONE") == 0 ||
                    friendlyName.compareTo("NOT_COMPUTABLE") == 0 ||
                    friendlyName.compareTo("NOT COMPUTABLE") == 0 ||
                    friendlyName.compareTo("OUT OF RANGE") == 0 ||
                    friendlyName.compareTo("OUT_OF_RANGE") == 0) {
                bgReading.hide_slope = true;
            }
            bgReading.save();
            bgReading.find_new_curve();
            bgReading.find_new_raw_curve();
            //context.startService(new Intent(context, Notifications.class));
            Notifications.start(); // this may not be needed as it is duplicated in handleNewBgReading
            BgSendQueue.handleNewBgReading(bgReading, "create", context);
        }
    }

    public static BgReading getForTimestamp(double timestamp) {
        Sensor sensor = Sensor.currentSensor();
        if (sensor != null) {
            BgReading bgReading = new Select()
                    .from(BgReading.class)
                    .where("Sensor = ? ", sensor.getId())
                    .where("timestamp <= ?", (timestamp + (60 * 1000))) // 1 minute padding (should never be that far off, but why not)
                    .where("calculated_value = 0")
                    .where("raw_calculated = 0")
                    .orderBy("timestamp desc")
                    .executeSingle();
            if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < (3 * 60 * 1000)) { //cool, so was it actually within 4 minutes of that bg reading?
                Log.i(TAG, "getForTimestamp: Found a BG timestamp match");
                return bgReading;
            }
        }
        Log.d(TAG, "getForTimestamp: No luck finding a BG timestamp match");
        return null;
    }

    // used in wear
    public static BgReading getForTimestampExists(double timestamp) {
        Sensor sensor = Sensor.currentSensor();
        if (sensor != null) {
            BgReading bgReading = new Select()
                    .from(BgReading.class)
                    .where("Sensor = ? ", sensor.getId())
                    .where("timestamp <= ?", (timestamp + (60 * 1000))) // 1 minute padding (should never be that far off, but why not)
                    .orderBy("timestamp desc")
                    .executeSingle();
            if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < (3 * 60 * 1000)) { //cool, so was it actually within 4 minutes of that bg reading?
                Log.i(TAG, "getForTimestamp: Found a BG timestamp match");
                return bgReading;
            }
        }
        Log.d(TAG, "getForTimestamp: No luck finding a BG timestamp match");
        return null;
    }

    public static BgReading getForPreciseTimestamp(long timestamp, long precision) {
        return getForPreciseTimestamp(timestamp, precision, true);
    }

    public static BgReading getForPreciseTimestamp(long timestamp, long precision, boolean lock_to_sensor) {
        final Sensor sensor = Sensor.currentSensor();
        if ((sensor != null) || !lock_to_sensor) {
            final BgReading bgReading = new Select()
                    .from(BgReading.class)
                    .where(lock_to_sensor ? "Sensor = ?" : "timestamp > ?", (lock_to_sensor ? sensor.getId() : 0))
                    .where("timestamp <= ?", (timestamp + precision))
                    .where("timestamp >= ?", (timestamp - precision))
                    .orderBy("abs(timestamp - " + timestamp + ") asc")
                    .executeSingle();
            if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < precision) { //cool, so was it actually within precision of that bg reading?
                //Log.d(TAG, "getForPreciseTimestamp: Found a BG timestamp match");
                return bgReading;
            }
        }
        Log.d(TAG, "getForPreciseTimestamp: No luck finding a BG timestamp match: " + JoH.dateTimeText((long) timestamp) + " precision:" + precision + " Sensor: " + ((sensor == null) ? "null" : sensor.getId()));
        return null;
    }


    public static boolean is_new(SensorRecord sensorRecord, long addativeOffset) {
        double timestamp = sensorRecord.getSystemTime().getTime() + addativeOffset;
        Sensor sensor = Sensor.currentSensor();
        if (sensor != null) {
            BgReading bgReading = new Select()
                    .from(BgReading.class)
                    .where("Sensor = ? ", sensor.getId())
                    .where("timestamp <= ?", (timestamp + (60 * 1000))) // 1 minute padding (should never be that far off, but why not)
                    .orderBy("timestamp desc")
                    .executeSingle();
            if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < (3 * 60 * 1000)) { //cool, so was it actually within 4 minutes of that bg reading?
                Log.i(TAG, "isNew; Old Reading");
                return false;
            }
        }
        Log.i(TAG, "isNew: New Reading");
        return true;
    }

    public static BgReading create(double raw_data, double filtered_data, Context context, Long timestamp) {
        return create(raw_data, filtered_data, context, timestamp, false);
    }

    public static BgReading create(double raw_data, double filtered_data, Context context, Long timestamp, boolean quick) {
        if (context == null) context = xdrip.getAppContext();
        BgReading bgReading = new BgReading();
        final Sensor sensor = Sensor.currentSensor();
        if (sensor == null) {
            Log.i("BG GSON: ", bgReading.toS());
            return bgReading;
        }

        if (raw_data == 0) {
            Log.e(TAG,"Warning: raw_data is 0 in BgReading.create()");
        }

        Calibration calibration = Calibration.lastValid();
        if (calibration == null) {
            Log.d(TAG, "create: No calibration yet");
            bgReading.sensor = sensor;
            bgReading.sensor_uuid = sensor.uuid;
            bgReading.raw_data = (raw_data / 1000);
            bgReading.filtered_data = (filtered_data / 1000);
            bgReading.timestamp = timestamp;
            bgReading.uuid = UUID.randomUUID().toString();
            bgReading.time_since_sensor_started = bgReading.timestamp - sensor.started_at;
            bgReading.calibration_flag = false;

            bgReading.calculateAgeAdjustedRawValue();

            bgReading.save();
            bgReading.perform_calculations();
            BgSendQueue.sendToPhone(context);
        } else {
            Log.d(TAG, "Calibrations, so doing everything: " + calibration.uuid);
            bgReading = createFromRawNoSave(sensor, calibration, raw_data, filtered_data, timestamp);

            bgReading.save();

            // used when we are not fast inserting data
            if (!quick) {
                bgReading.perform_calculations();

                if (JoH.ratelimit("opportunistic-calibration", 60)) {
                    BloodTest.opportunisticCalibration();
                }

                //context.startService(new Intent(context, Notifications.class));
                // allow this instead to be fired inside handleNewBgReading when noise will have been injected already
            }

            bgReading.postProcess(quick);

        }

        Log.i("BG GSON: ", bgReading.toS());

        return bgReading;
    }

    public void postProcess(final boolean quick) {
        injectNoise(true); // Add noise parameter for nightscout
        injectDisplayGlucose(BestGlucose.getDisplayGlucose()); // Add display glucose for nightscout
        BgSendQueue.handleNewBgReading(this, "create", xdrip.getAppContext(), Home.get_follower(), quick);
    }

    public static BgReading createFromRawNoSave(Sensor sensor, Calibration calibration, double raw_data, double filtered_data, long timestamp) {
        final BgReading bgReading = new BgReading();
        if (sensor == null) {
            sensor = Sensor.currentSensor();
            if (sensor == null) {
                return bgReading;
            }
        }
        if (calibration == null) {
            calibration = Calibration.lastValid();
            if (calibration == null) {
                return bgReading;
            }
        }

        bgReading.sensor = sensor;
        bgReading.sensor_uuid = sensor.uuid;
        bgReading.calibration = calibration;
        bgReading.calibration_uuid = calibration.uuid;
        bgReading.raw_data = (raw_data / 1000);
        bgReading.filtered_data = (filtered_data / 1000);
        bgReading.timestamp = timestamp;
        bgReading.uuid = UUID.randomUUID().toString();
        bgReading.time_since_sensor_started = bgReading.timestamp - sensor.started_at;

        bgReading.calculateAgeAdjustedRawValue();

        if (calibration.check_in) {
            double firstAdjSlope = calibration.first_slope + (calibration.first_decay * (Math.ceil(new Date().getTime() - calibration.timestamp) / (1000 * 60 * 10)));
            double calSlope = (calibration.first_scale / firstAdjSlope) * 1000;
            double calIntercept = ((calibration.first_scale * calibration.first_intercept) / firstAdjSlope) * -1;
            bgReading.calculated_value = (((calSlope * bgReading.raw_data) + calIntercept) - 5);
            bgReading.filtered_calculated_value = (((calSlope * bgReading.ageAdjustedFiltered()) + calIntercept) - 5);

        } else {
            BgReading lastBgReading = BgReading.last();
            if (lastBgReading != null && lastBgReading.calibration != null) {
                Log.d(TAG, "Create calibration.uuid=" + calibration.uuid + " bgReading.uuid: " + bgReading.uuid + " lastBgReading.calibration_uuid: " + lastBgReading.calibration_uuid + " lastBgReading.calibration.uuid: " + lastBgReading.calibration.uuid);
                Log.d(TAG, "Create lastBgReading.calibration_flag=" + lastBgReading.calibration_flag + " bgReading.timestamp: " + bgReading.timestamp + " lastBgReading.timestamp: " + lastBgReading.timestamp + " lastBgReading.calibration.timestamp: " + lastBgReading.calibration.timestamp);
                Log.d(TAG, "Create lastBgReading.calibration_flag=" + lastBgReading.calibration_flag + " bgReading.timestamp: " + JoH.dateTimeText(bgReading.timestamp) + " lastBgReading.timestamp: " + JoH.dateTimeText(lastBgReading.timestamp) + " lastBgReading.calibration.timestamp: " + JoH.dateTimeText(lastBgReading.calibration.timestamp));
                if (lastBgReading.calibration_flag == true && ((lastBgReading.timestamp + (60000 * 20)) > bgReading.timestamp) && ((lastBgReading.calibration.timestamp + (60000 * 20)) > bgReading.timestamp)) {
                    lastBgReading.calibration.rawValueOverride(BgReading.weightedAverageRaw(lastBgReading.timestamp, bgReading.timestamp, lastBgReading.calibration.timestamp, lastBgReading.age_adjusted_raw_value, bgReading.age_adjusted_raw_value), xdrip.getAppContext());
                    newCloseSensorData();
                }
            }

            if ((bgReading.raw_data != 0) && (bgReading.raw_data * 2 == bgReading.filtered_data)) {
                Log.wtf(TAG, "Filtered data is exactly double raw - this is completely wrong - dead transmitter? - blocking glucose calculation");
                bgReading.calculated_value = 0;
                bgReading.filtered_calculated_value = 0;
                bgReading.hide_slope = true;
            } else if (!SensorSanity.isRawValueSane(bgReading.raw_data)) {
                Log.wtf(TAG, "Raw data fails sanity check! " + bgReading.raw_data);
                bgReading.calculated_value = 0;
                bgReading.filtered_calculated_value = 0;
                bgReading.hide_slope = true;
            } else {

                // calculate glucose number from raw
                final CalibrationAbstract.CalibrationData pcalibration;
                final CalibrationAbstract plugin = getCalibrationPluginFromPreferences(); // make sure do this only once

                if ((plugin != null) && ((pcalibration = plugin.getCalibrationData()) != null) && (Pref.getBoolean("use_pluggable_alg_as_primary", false))) {
                    Log.d(TAG, "USING CALIBRATION PLUGIN AS PRIMARY!!!");
                    if (plugin.isCalibrationSane(pcalibration)) {
                        bgReading.calculated_value = (pcalibration.slope * bgReading.age_adjusted_raw_value) + pcalibration.intercept;
                        bgReading.filtered_calculated_value = (pcalibration.slope * bgReading.ageAdjustedFiltered()) + calibration.intercept;
                    } else {
                        UserError.Log.wtf(TAG, "Calibration plugin failed intercept sanity check: " + pcalibration.toS());
                        Home.toaststaticnext("Calibration plugin failed intercept sanity check");
                    }
                } else {
                    bgReading.calculated_value = ((calibration.slope * bgReading.age_adjusted_raw_value) + calibration.intercept);
                    bgReading.filtered_calculated_value = ((calibration.slope * bgReading.ageAdjustedFiltered()) + calibration.intercept);
                }

                updateCalculatedValueToWithinMinMax(bgReading);
            }
        }

        // LimiTTer can send 12 to indicate problem with NFC reading.
        if ((!calibration.check_in) && (raw_data == 12) && (filtered_data == 12)) {
            // store the raw value for sending special codes, note updateCalculatedValue would try to nix it
            bgReading.calculated_value = raw_data;
            bgReading.filtered_calculated_value = filtered_data;
        }
        return  bgReading;
    }

    public static boolean isRawMarkerValue(final double raw_data) {
        return raw_data == BgReading.SPECIAL_G5_PLACEHOLDER
                || raw_data == BgReading.SPECIAL_RAW_NOT_AVAILABLE;
    }


    static void updateCalculatedValueToWithinMinMax(BgReading bgReading) {
        // TODO should this really be <10 other values also special??
        if (bgReading.calculated_value < 10) {
            bgReading.calculated_value = BG_READING_ERROR_VALUE;
            bgReading.hide_slope = true;
        } else {
            bgReading.calculated_value = Math.min(BG_READING_MAXIMUM_VALUE, Math.max(BG_READING_MINIMUM_VALUE, bgReading.calculated_value));
        }
        Log.i(TAG, "NEW VALUE CALCULATED AT: " + bgReading.calculated_value);
    }

    // Used by xDripViewer
    public static void create(Context context, double raw_data, double age_adjusted_raw_value, double filtered_data, Long timestamp,
                              double calculated_bg, double calculated_current_slope, boolean hide_slope) {

        BgReading bgReading = new BgReading();
        Sensor sensor = Sensor.currentSensor();
        if (sensor == null) {
            Log.w(TAG, "No sensor, ignoring this bg reading");
            return;
        }

        Calibration calibration = Calibration.lastValid();
        if (calibration == null) {
            Log.d(TAG, "create: No calibration yet");
            bgReading.sensor = sensor;
            bgReading.sensor_uuid = sensor.uuid;
            bgReading.raw_data = (raw_data / 1000);
            bgReading.age_adjusted_raw_value = age_adjusted_raw_value;
            bgReading.filtered_data = (filtered_data / 1000);
            bgReading.timestamp = timestamp;
            bgReading.uuid = UUID.randomUUID().toString();
            bgReading.calculated_value = calculated_bg;
            bgReading.calculated_value_slope = calculated_current_slope;
            bgReading.hide_slope = hide_slope;

            bgReading.save();
            bgReading.perform_calculations();
        } else {
            Log.d(TAG, "Calibrations, so doing everything bgReading = " + bgReading);
            bgReading.sensor = sensor;
            bgReading.sensor_uuid = sensor.uuid;
            bgReading.calibration = calibration;
            bgReading.calibration_uuid = calibration.uuid;
            bgReading.raw_data = (raw_data / 1000);
            bgReading.age_adjusted_raw_value = age_adjusted_raw_value;
            bgReading.filtered_data = (filtered_data / 1000);
            bgReading.timestamp = timestamp;
            bgReading.uuid = UUID.randomUUID().toString();
            bgReading.calculated_value = calculated_bg;
            bgReading.calculated_value_slope = calculated_current_slope;
            bgReading.hide_slope = hide_slope;

            bgReading.save();
        }
        BgSendQueue.handleNewBgReading(bgReading, "create", context);

        Log.i("BG GSON: ", bgReading.toS());
    }

    public static void pushBgReadingSyncToWatch(BgReading bgReading, boolean is_new) {
        Log.d(TAG, "pushTreatmentSyncToWatch Add treatment to UploaderQueue.");
        if (Pref.getBooleanDefaultFalse("wear_sync")) {
            if (UploaderQueue.newEntryForWatch(is_new ? "insert" : "update", bgReading) != null) {
                SyncService.startSyncService(3000); // sync in 3 seconds
            }
        }
    }

    public String displaySlopeArrow() {
        return slopeToArrowSymbol(this.dg_mgdl > 0 ? this.dg_slope * 60000 : this.calculated_value_slope * 60000);
    }

    public static String activeSlopeArrow() {
        double slope = (float) (BgReading.activeSlope() * 60000);
        return slopeToArrowSymbol(slope);
    }

    public static String slopeToArrowSymbol(double slope) {
        if (slope <= (-3.5)) {
            return "\u21ca";// ⇊
        } else if (slope <= (-2)) {
            return "\u2193"; // ↓
        } else if (slope <= (-1)) {
            return "\u2198"; // ↘
        } else if (slope <= (1)) {
            return "\u2192"; // →
        } else if (slope <= (2)) {
            return "\u2197"; // ↗
        } else if (slope <= (3.5)) {
            return "\u2191"; // ↑
        } else {
            return "\u21c8"; // ⇈
        }
    }

    public String slopeArrow() {
        return slopeToArrowSymbol(this.calculated_value_slope * 60000);
    }

    public  String slopeName() {
        double slope_by_minute = calculated_value_slope * 60000;
        String arrow = "NONE";
        if (slope_by_minute <= (-3.5)) {
            arrow = "DoubleDown";
        } else if (slope_by_minute <= (-2)) {
            arrow = "SingleDown";
        } else if (slope_by_minute <= (-1)) {
            arrow = "FortyFiveDown";
        } else if (slope_by_minute <= (1)) {
            arrow = "Flat";
        } else if (slope_by_minute <= (2)) {
            arrow = "FortyFiveUp";
        } else if (slope_by_minute <= (3.5)) {
            arrow = "SingleUp";
        } else if (slope_by_minute <= (40)) {
            arrow = "DoubleUp";
        }
        if (hide_slope) {
            arrow = "NOT COMPUTABLE";
        }
        return arrow;
    }

    public static String slopeName(double slope_by_minute) {
        String arrow = "NONE";
        if (slope_by_minute <= (-3.5)) {
            arrow = "DoubleDown";
        } else if (slope_by_minute <= (-2)) {
            arrow = "SingleDown";
        } else if (slope_by_minute <= (-1)) {
            arrow = "FortyFiveDown";
        } else if (slope_by_minute <= (1)) {
            arrow = "Flat";
        } else if (slope_by_minute <= (2)) {
            arrow = "FortyFiveUp";
        } else if (slope_by_minute <= (3.5)) {
            arrow = "SingleUp";
        } else if (slope_by_minute <= (40)) {
            arrow = "DoubleUp";
        }
        return arrow;
    }

    public static double slopefromName(String slope_name) {
        if (slope_name == null) return 0;
        double slope_by_minute = 0;
        if (slope_name.compareTo("DoubleDown") == 0) {
            slope_by_minute = -3.5;
        } else if (slope_name.compareTo("SingleDown") == 0) {
            slope_by_minute = -2;
        } else if (slope_name.compareTo("FortyFiveDown") == 0) {
            slope_by_minute = -1;
        } else if (slope_name.compareTo("Flat") == 0) {
            slope_by_minute = 0;
        } else if (slope_name.compareTo("FortyFiveUp") == 0) {
            slope_by_minute = 2;
        } else if (slope_name.compareTo("SingleUp") == 0) {
            slope_by_minute = 3.5;
        } else if (slope_name.compareTo("DoubleUp") == 0) {
            slope_by_minute = 4;
        } else if (isSlopeNameInvalid(slope_name)) {
            slope_by_minute = 0;
        }
        return slope_by_minute / 60000;
    }

    public static boolean isSlopeNameInvalid(String slope_name) {
        if (slope_name.compareTo("NOT_COMPUTABLE") == 0 ||
                slope_name.compareTo("NOT COMPUTABLE") == 0 ||
                slope_name.compareTo("OUT_OF_RANGE") == 0 ||
                slope_name.compareTo("OUT OF RANGE") == 0 ||
                slope_name.compareTo("NONE") == 0) {
            return true;
        } else {
            return false;
        }
    }

    // Get a slope arrow based on pure guessed defaults so we can show it prior to calibration
    public static String getSlopeArrowSymbolBeforeCalibration() {
        final List<BgReading> last = BgReading.latestUnCalculated(2);
        if ((last!=null) && (last.size()==2)) {
            final double guess_slope = 1; // This is the "Default" slope for Dex and LimiTTer
            final double time_delta = (last.get(0).timestamp-last.get(1).timestamp);
            if (time_delta<=(BgGraphBuilder.DEXCOM_PERIOD * 2)) {
                final double estimated_delta = (last.get(0).age_adjusted_raw_value * guess_slope) - (last.get(1).age_adjusted_raw_value * guess_slope);
                final double estimated_delta2 = (last.get(0).raw_data * guess_slope) - (last.get(1).raw_data * guess_slope);
                Log.d(TAG, "SlopeArrowBeforeCalibration: guess delta: " + estimated_delta + " delta2: " + estimated_delta2 + " timedelta: " + time_delta);
                return slopeToArrowSymbol(estimated_delta / (time_delta / 60000));
            } else { return ""; }
        } else {
            return "";
        }
    }

    public static boolean last_within_minutes(final int mins) {
        return last_within_millis(mins * 60000);
    }

    public static boolean last_within_millis(final long millis) {
        final BgReading reading = last();
        return reading != null && ((JoH.tsl() - reading.timestamp) < millis);
    }

    public boolean within_millis(final long millis) {
        return ((JoH.tsl() - this.timestamp) < millis);
    }

    public boolean isStale() {
        return !within_millis(Home.stale_data_millis());
    }

    public static BgReading last()
    {
        return BgReading.last(Home.get_follower());
    }

    public static BgReading last(boolean is_follower) {
        if (is_follower) {
            return new Select()
                    .from(BgReading.class)
                    .where("calculated_value != 0")
                    .where("raw_data != 0")
              //      .where("timestamp <= ?", JoH.tsl())
                    .orderBy("timestamp desc")
                    .executeSingle();
        } else {
            Sensor sensor = Sensor.currentSensor();
            if (sensor != null) {
                return new Select()
                        .from(BgReading.class)
                        .where("Sensor = ? ", sensor.getId())
                        .where("calculated_value != 0")
                        .where("raw_data != 0")
                //        .where("timestamp <= ?", JoH.tsl())
                        .orderBy("timestamp desc")
                        .executeSingle();
            }
        }
        return null;
    }

    public static List<BgReading> latest_by_size(int number) {
        final Sensor sensor = Sensor.currentSensor();
        if (sensor == null) return null;
        return new Select()
                .from(BgReading.class)
                .where("Sensor = ? ", sensor.getId())
                .where("raw_data != 0")
                .orderBy("timestamp desc")
                .limit(number)
                .execute();
    }

    public static BgReading lastNoSenssor() {
        return new Select()
                .from(BgReading.class)
                .where("calculated_value != 0")
                .where("raw_data != 0")
            //    .where("timestamp <= ?", JoH.tsl())
                .orderBy("timestamp desc")
                .executeSingle();
    }

    public static List<BgReading> latest(int number) {
        return latest(number, Home.get_follower());
    }

    public static List<BgReading> latest(int number, boolean is_follower) {
        if (is_follower) {
            // exclude sensor information when working as a follower
            return new Select()
                    .from(BgReading.class)
                    .where("calculated_value != 0")
                    .where("raw_data != 0")
            //        .where("timestamp <= ?", JoH.tsl())
                    .orderBy("timestamp desc")
                    .limit(number)
                    .execute();
        } else {
            Sensor sensor = Sensor.currentSensor();
            if (sensor == null) {
                return null;
            }
            return new Select()
                    .from(BgReading.class)
                    .where("Sensor = ? ", sensor.getId())
                    .where("calculated_value != 0")
                    .where("raw_data != 0")
              //      .where("timestamp <= ?", JoH.tsl())
                    .orderBy("timestamp desc")
                    .limit(number)
                    .execute();
        }
    }

    public static boolean isDataStale() {
        final BgReading last = lastNoSenssor();
        if (last == null) return true;
        return JoH.msSince(last.timestamp) > Home.stale_data_millis();
    }


    public static List<BgReading> latestUnCalculated(int number) {
        Sensor sensor = Sensor.currentSensor();
        if (sensor == null) { return null; }
        return new Select()
                .from(BgReading.class)
                .where("Sensor = ? ", sensor.getId())
                .where("raw_data != 0")
                .orderBy("timestamp desc")
                .limit(number)
                .execute();
    }

    public static List<BgReading> latestForGraph(int number, double startTime) {
        return latestForGraph(number, (long) startTime, Long.MAX_VALUE);
    }

    public static List<BgReading> latestForGraph(int number, long startTime) {
        return latestForGraph(number, startTime, Long.MAX_VALUE);
    }

    public static List<BgReading> latestForGraph(int number, long startTime, long endTime) {
        return new Select()
                .from(BgReading.class)
                .where("timestamp >= " + Math.max(startTime, 0))
                .where("timestamp <= " + endTime)
                .where("calculated_value != 0")
                .where("raw_data != 0")
                .orderBy("timestamp desc")
                .limit(number)
                .execute();
    }

    public static List<BgReading> latestForGraphSensor(int number, long startTime, long endTime) {
        Sensor sensor = Sensor.currentSensor();
        if (sensor == null) { return null; }
        return new Select()
                .from(BgReading.class)
                .where("Sensor = ? ", sensor.getId())
                .where("timestamp >= " + Math.max(startTime, 0))
                .where("timestamp <= " + endTime)
                .where("calculated_value != 0")
                .where("raw_data != 0")
                .where("calibration_uuid != \"\"")
                .orderBy("timestamp desc")
                .limit(number)
                .execute();
    }

    public static List<BgReading> latestForSensorAsc(int number, long startTime, long endTime, boolean follower) {
        if (follower) {
            return new Select()
                    .from(BgReading.class)
                    .where("timestamp >= ?", Math.max(startTime, 0))
                    .where("timestamp <= ?", endTime)
                    .where("calculated_value != 0")
                    .where("raw_data != 0")
                    .orderBy("timestamp asc")
                    .limit(number)
                    .execute();
        } else {
            final Sensor sensor = Sensor.currentSensor();
            if (sensor == null) {
                return null;
            }
            return new Select()
                    .from(BgReading.class)
                    .where("Sensor = ? ", sensor.getId())
                    .where("timestamp >= ?", Math.max(startTime, 0))
                    .where("timestamp <= ?", endTime)
                    .where("calculated_value != 0")
                    .where("raw_data != 0")
                    .orderBy("timestamp asc")
                    .limit(number)
                    .execute();
        }
    }

    public static List<BgReading> latestForSensorAsc(int number, long startTime, long endTime) {
        return latestForSensorAsc(number, startTime, endTime, false);
    }


    public static List<BgReading> latestForGraphAsc(int number, long startTime) {//KS
        return latestForGraphAsc(number, startTime, Long.MAX_VALUE);
    }

    public static List<BgReading> latestForGraphAsc(int number, long startTime, long endTime) {//KS
        return new Select()
                .from(BgReading.class)
                .where("timestamp >= " + Math.max(startTime, 0))
                .where("timestamp <= " + endTime)
                .where("calculated_value != 0")
                .where("raw_data != 0")
                .orderBy("timestamp asc")
                .limit(number)
                .execute();
    }

    public static BgReading readingNearTimeStamp(double startTime) {
        final double margin = (4 * 60 * 1000);
        final DecimalFormat df = new DecimalFormat("#");
        df.setMaximumFractionDigits(1);
        return new Select()
                .from(BgReading.class)
                .where("timestamp >= " + df.format(startTime - margin))
                .where("timestamp <= " + df.format(startTime + margin))
                .where("calculated_value != 0")
                .where("raw_data != 0")
                .executeSingle();
    }

    public static List<BgReading> last30Minutes() {
        double timestamp = (new Date().getTime()) - (60000 * 30);
        return new Select()
                .from(BgReading.class)
                .where("timestamp >= " + timestamp)
                .where("calculated_value != 0")
                .where("raw_data != 0")
                .orderBy("timestamp desc")
                .execute();
    }

    public static boolean isDataSuitableForDoubleCalibration() {
        final List<BgReading> uncalculated = BgReading.latestUnCalculated(3);
        if (uncalculated.size() < 3) return false;
        final ProcessInitialDataQuality.InitialDataQuality idq = ProcessInitialDataQuality.getInitialDataQuality(uncalculated);
        if (!idq.pass) {
            UserError.Log.d(TAG, "Data quality failure for double calibration: " + idq.advice);
        }
        return idq.pass || Pref.getBooleanDefaultFalse("bypass_calibration_quality_check");
    }


    public static List<BgReading> futureReadings() {
        double timestamp = new Date().getTime();
        return new Select()
                .from(BgReading.class)
                .where("timestamp > " + timestamp)
                .orderBy("timestamp desc")
                .execute();
    }

    // used in wear
    public static BgReading findByUuid(String uuid) {
        return new Select()
                .from(BgReading.class)
                .where("uuid = ?", uuid)
                .executeSingle();
    }

    public static double estimated_bg(double timestamp) {
        timestamp = timestamp + BESTOFFSET;
        BgReading latest = BgReading.last();
        if (latest == null) {
            return 0;
        } else {
            return (latest.a * timestamp * timestamp) + (latest.b * timestamp) + latest.c;
        }
    }

    public static double estimated_raw_bg(double timestamp) {
        timestamp = timestamp + BESTOFFSET;
        double estimate;
        BgReading latest = BgReading.last();
        if (latest == null) {
            Log.i(TAG, "No data yet, assume perfect!");
            estimate = 160;
        } else {
            estimate = (latest.ra * timestamp * timestamp) + (latest.rb * timestamp) + latest.rc;
        }
        Log.i(TAG, "ESTIMATE RAW BG" + estimate);
        return estimate;
    }

    public static void bgReadingInsertFromJson(String json)
    {
        bgReadingInsertFromJson(json, true);
    }

    private static void FixCalibration(BgReading bgr) {
        if (bgr.calibration_uuid == null || "".equals(bgr.calibration_uuid)) {
            Log.d(TAG, "Bgr with no calibration, doing nothing");
            return;
        }
        Calibration calibration = Calibration.byuuid(bgr.calibration_uuid);
        if (calibration == null) {
            Log.i(TAG, "received Unknown calibration: " + bgr.calibration_uuid + " asking for sensor upate...");
            GcmActivity.requestSensorCalibrationsUpdate();
        } else {
            bgr.calibration = calibration;
        }
    }

    public BgReading noRawWillBeAvailable() {
        raw_data = SPECIAL_RAW_NOT_AVAILABLE;
        save();
        return this;
    }

    public BgReading appendSourceInfo(String info) {
        if ((source_info == null) || (source_info.length() == 0)) {
            source_info = info;
        } else {
            if (!source_info.startsWith(info) && (!source_info.contains("::" + info))) {
                source_info += "::" + info;
            } else {
                UserError.Log.e(TAG, "Ignoring duplicate source info " + source_info + " -> " + info);
            }
        }
        return this;
    }

    public boolean isBackfilled() {
        return raw_data == SPECIAL_G5_PLACEHOLDER;
    }

    public boolean isRemote() {
        return filtered_data == SPECIAL_REMOTE_PLACEHOLDER;
    }

    public static final double SPECIAL_RAW_NOT_AVAILABLE = -0.1279;
    public static final double SPECIAL_G5_PLACEHOLDER = -0.1597;
    public static final double SPECIAL_FOLLOWER_PLACEHOLDER = -0.1486;
    public static final double SPECIAL_REMOTE_PLACEHOLDER = -0.1375;

    public static BgReading bgReadingInsertFromG5(double calculated_value, long timestamp) {
        return bgReadingInsertFromG5(calculated_value, timestamp, null);
    }

                       // TODO can these methods be unified to reduce duplication
                                                               // TODO remember to sync this with wear code base
    public static synchronized BgReading bgReadingInsertFromG5(double calculated_value, long timestamp, String sourceInfoAppend) {

        final Sensor sensor = Sensor.currentSensor();
        if (sensor == null) {
            Log.w(TAG, "No sensor, ignoring this bg reading");
            return null;
        }
        // TODO slope!!
        final BgReading existing = getForPreciseTimestamp(timestamp, Constants.MINUTE_IN_MS);
        if (existing == null) {
            final BgReading bgr = new BgReading();
            bgr.sensor = sensor;
            bgr.sensor_uuid = sensor.uuid;
            bgr.time_since_sensor_started = JoH.msSince(sensor.started_at); // is there a helper for this?
            bgr.timestamp = timestamp;
            bgr.uuid = UUID.randomUUID().toString();
            bgr.calculated_value = calculated_value;
            bgr.raw_data = SPECIAL_G5_PLACEHOLDER; // placeholder
            bgr.appendSourceInfo("G5 Native");
            if (sourceInfoAppend != null && sourceInfoAppend.length() > 0) {
                bgr.appendSourceInfo(sourceInfoAppend);
            }
            bgr.save();
            if (JoH.ratelimit("sync wakelock", 15)) {
                final PowerManager.WakeLock linger = JoH.getWakeLock("G5 Insert", 4000);
            }
            Inevitable.stackableTask("NotifySyncBgr", 3000, () -> notifyAndSync(bgr));
            return bgr;
        } else {
            return existing;
        }
    }

    public static synchronized BgReading bgReadingInsertMedtrum(double calculated_value, long timestamp, String sourceInfoAppend, double raw_data) {

        final Sensor sensor = Sensor.currentSensor();
        if (sensor == null) {
            Log.w(TAG, "No sensor, ignoring this bg reading");
            return null;
        }
        // TODO slope!!
        final BgReading existing = getForPreciseTimestamp(timestamp, Constants.MINUTE_IN_MS);
        if (existing == null) {
            final BgReading bgr = new BgReading();
            bgr.sensor = sensor;
            bgr.sensor_uuid = sensor.uuid;
            bgr.time_since_sensor_started = JoH.msSince(sensor.started_at); // is there a helper for this?
            bgr.timestamp = timestamp;
            bgr.uuid = UUID.randomUUID().toString();
            bgr.calculated_value = calculated_value;
            bgr.raw_data = raw_data / 1000d;
            bgr.filtered_data = bgr.raw_data;
            if (sourceInfoAppend != null && sourceInfoAppend.equals("Backfill")) {
                bgr.raw_data = BgReading.SPECIAL_G5_PLACEHOLDER;
            } else {
                bgr.calculateAgeAdjustedRawValue();
            }
            bgr.appendSourceInfo("Medtrum Native");
            if (sourceInfoAppend != null && sourceInfoAppend.length() > 0) {
                bgr.appendSourceInfo(sourceInfoAppend);
            }
            bgr.save();
            if (JoH.ratelimit("sync wakelock", 15)) {
                final PowerManager.WakeLock linger = JoH.getWakeLock("Medtrum Insert", 4000);
            }
            Inevitable.task("NotifySyncBgr" + bgr.timestamp, 3000, () -> notifyAndSync(bgr));
            if (bgr.isBackfilled()) {
                handleResyncWearAfterBackfill(bgr.timestamp);
            }
            return bgr;
        } else {
            return existing;
        }
    }
    public static synchronized BgReading bgReadingInsertLibre2(double calculated_value, long timestamp, double raw_data) {

        final Sensor sensor = Sensor.currentSensor();
        if (sensor == null) {
            Log.w(TAG, "No sensor, ignoring this bg reading");
            return null;
        }
        // TODO slope!!
        final BgReading existing = getForPreciseTimestamp(timestamp, Constants.MINUTE_IN_MS);
        if (existing == null) {
            Calibration calibration = Calibration.lastValid();
            final BgReading bgReading = new BgReading();
            if (calibration == null) {
                Log.d(TAG, "create: No calibration yet");
                bgReading.sensor = sensor;
                bgReading.sensor_uuid = sensor.uuid;
                bgReading.raw_data = raw_data;
                bgReading.age_adjusted_raw_value = raw_data;
                bgReading.filtered_data = raw_data;
                bgReading.timestamp = timestamp;
                bgReading.uuid = UUID.randomUUID().toString();
                bgReading.calculated_value = calculated_value;
                bgReading.calculated_value_slope = 0;
                bgReading.hide_slope = false;
                bgReading.appendSourceInfo("Libre2 Native");
                bgReading.find_slope();

                bgReading.save();
                bgReading.perform_calculations();
                bgReading.postProcess(false);

            } else {
                Log.d(TAG, "Calibrations, so doing everything bgReading = " + bgReading);
                bgReading.sensor = sensor;
                bgReading.sensor_uuid = sensor.uuid;
                bgReading.calibration = calibration;
                bgReading.calibration_uuid = calibration.uuid;
                bgReading.raw_data = raw_data ;
                bgReading.age_adjusted_raw_value = raw_data;
                bgReading.filtered_data = raw_data;
                bgReading.timestamp = timestamp;
                bgReading.uuid = UUID.randomUUID().toString();

                bgReading.calculated_value = ((calibration.slope * calculated_value) + calibration.intercept);
                bgReading.filtered_calculated_value = ((calibration.slope * bgReading.ageAdjustedFiltered()) + calibration.intercept);

                bgReading.calculated_value_slope = 0;
                bgReading.hide_slope = false;
                bgReading.appendSourceInfo("Libre2 Native");

                BgReading.updateCalculatedValueToWithinMinMax(bgReading);

                bgReading.find_slope();
                bgReading.save();

                bgReading.postProcess(false);

            }

           return bgReading;
        } else {
            return existing;
        }
    }

    public static void handleResyncWearAfterBackfill(final long earliest) {
        if (earliest_backfill == 0 || earliest < earliest_backfill) earliest_backfill = earliest;
        if (WatchUpdaterService.isEnabled()) {
            Inevitable.task("wear-backfill-sync", 10000, () -> {
                WatchUpdaterService.startServiceAndResendDataIfNeeded(earliest_backfill);
                earliest_backfill = 0;
            });
        }
    }

    public void setRemoteMarker() {
        filtered_data = SPECIAL_REMOTE_PLACEHOLDER;
    }


    public static void notifyAndSync(final BgReading bgr) {
        final boolean recent = bgr.isCurrent();
        if (recent) {
            Notifications.start(); // may not be needed as this is duplicated in handleNewBgReading
            // probably not wanted for G5 internal values?
            //bgr.injectNoise(true); // Add noise parameter for nightscout
            //bgr.injectDisplayGlucose(BestGlucose.getDisplayGlucose()); // Add display glucose for nightscout
        }
        BgSendQueue.handleNewBgReading(bgr, "create", xdrip.getAppContext(), Home.get_follower(), !recent); // pebble and widget and follower
    }

    public static BgReading bgReadingInsertFromJson(String json, boolean do_notification) {
        return bgReadingInsertFromJson(json, do_notification, WholeHouse.isEnabled());
    }

    public static BgReading bgReadingInsertFromJson(String json, boolean do_notification, boolean force_sensor) {
        if ((json == null) || (json.length() == 0)) {
            Log.e(TAG, "bgreadinginsertfromjson passed a null or zero length json");
            return null;
        }
        final BgReading bgr = fromJSON(json);
        if (bgr != null) {
            try {
                if (readingNearTimeStamp(bgr.timestamp) == null) {
                    FixCalibration(bgr);
                    if (force_sensor) {
                        final Sensor forced_sensor = Sensor.currentSensor();
                        if (forced_sensor != null) {
                            bgr.sensor = forced_sensor;
                            bgr.sensor_uuid = forced_sensor.uuid;
                        }
                        if (Pref.getBooleanDefaultFalse("illustrate_remote_data")) {
                            bgr.setRemoteMarker();
                        }
                    }
                    final long now = JoH.tsl();
                    if (bgr.timestamp > now) {
                        UserError.Log.wtf(TAG, "Received a bg reading that appears to be in the future: " + JoH.dateTimeText(bgr.timestamp) + " vs " + JoH.dateTimeText(now));
                    }
                    bgr.save();
                    if (do_notification) {
                        Notifications.start(); // this may not be needed as it fires in handleNewBgReading
                        //xdrip.getAppContext().startService(new Intent(xdrip.getAppContext(), Notifications.class)); // alerts et al
                        BgSendQueue.handleNewBgReading(bgr, "create", xdrip.getAppContext(), Home.get_follower()); // pebble and widget and follower
                    }
                } else {
                    Log.d(TAG, "Ignoring duplicate bgr record due to timestamp: " + json);
                }
            } catch (Exception e) {
                Log.d(TAG, "Could not save BGR: " + e.toString());
            }
        } else {
            Log.e(TAG,"Got null bgr from json");
        }
        return bgr;
    }

    // TODO this method shares some code with above.. merge
    public static void bgReadingInsertFromInt(int value, long timestamp, boolean do_notification) {
        // TODO sanity check data!

        if ((value <= 0) || (timestamp <= 0)) {
            Log.e(TAG, "Invalid data fed to InsertFromInt");
            return;
        }

        BgReading bgr = new BgReading();

        if (bgr != null) {
            bgr.uuid = UUID.randomUUID().toString();

            bgr.timestamp = timestamp;
            bgr.calculated_value = value;


            // rough code for testing!
            bgr.filtered_calculated_value = value;
            bgr.raw_data = value;
            bgr.age_adjusted_raw_value = value;
            bgr.filtered_data = value;

            final Sensor forced_sensor = Sensor.currentSensor();
            if (forced_sensor != null) {
                bgr.sensor = forced_sensor;
                bgr.sensor_uuid = forced_sensor.uuid;
            }

            try {
                if (readingNearTimeStamp(bgr.timestamp) == null) {
                    bgr.save();
                    bgr.find_slope();
                    if (do_notification) {
                       // xdrip.getAppContext().startService(new Intent(xdrip.getAppContext(), Notifications.class)); // alerts et al
                        Notifications.start(); // this may not be needed as it is duplicated in handleNewBgReading
                    }
                    BgSendQueue.handleNewBgReading(bgr, "create", xdrip.getAppContext(), false, !do_notification); // pebble and widget
                } else {
                    Log.d(TAG, "Ignoring duplicate bgr record due to timestamp: " + timestamp);
                }
            } catch (Exception e) {
                Log.d(TAG, "Could not save BGR: " + e.toString());
            }
        } else {
            Log.e(TAG,"Got null bgr from create");
        }
    }

    public static BgReading byUUID(String uuid) {
        if (uuid == null) return null;
        return new Select()
                .from(BgReading.class)
                .where("uuid = ?", uuid)
                .executeSingle();
    }

    public static BgReading byid(long id) {
        return new Select()
                .from(BgReading.class)
                .where("_ID = ?", id)
                .executeSingle();
    }

    public static BgReading fromJSON(String json) {
        if (json.length()==0)
        {
            Log.d(TAG,"Empty json received in bgreading fromJson");
            return null;
        }
        try {
            Log.d(TAG, "Processing incoming json: " + json);
           return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json,BgReading.class);
        } catch (Exception e) {
            Log.d(TAG, "Got exception parsing BgReading json: " + e.toString());
            Home.toaststaticnext("Error on BGReading sync, probably decryption key mismatch");
            return null;
        }
    }

    private BgReadingMessage toMessageNative() {
        return new BgReadingMessage.Builder()
                .timestamp(timestamp)
                //.a(a)
                //.b(b)
                //.c(c)
                .age_adjusted_raw_value(age_adjusted_raw_value)
                .calculated_value(calculated_value)
                .filtered_calculated_value(filtered_calculated_value)
                .calibration_flag(calibration_flag)
                .raw_calculated(raw_calculated)
                .raw_data(raw_data)
                .calculated_value_slope(calculated_value_slope)
                //.calibration_uuid(calibration_uuid)
                .uuid(uuid)
                .build();
    }

    public byte[] toMessage() {
        final List<BgReading> btl = new ArrayList<>();
        btl.add(this);
        return toMultiMessage(btl);
    }

    public static byte[] toMultiMessage(List<BgReading> bgl) {
        if (bgl == null) return null;
        final List<BgReadingMessage> BgReadingMessageList = new ArrayList<>();
        for (BgReading bg : bgl) {
            BgReadingMessageList.add(bg.toMessageNative());
        }
        return BgReadingMultiMessage.ADAPTER.encode(new BgReadingMultiMessage(BgReadingMessageList));
    }

    private static final long CLOSEST_READING_MS = 290000;
    private static void processFromMessage(BgReadingMessage btm) {
        if ((btm != null) && (btm.uuid != null) && (btm.uuid.length() == 36)) {
            BgReading bg = byUUID(btm.uuid);
            if (bg != null) {
                // we already have this uuid and we don't have a circumstance to update the record, so quick return here
                return;
            }
            if (bg == null) {
                bg = getForPreciseTimestamp(Wire.get(btm.timestamp, BgReadingMessage.DEFAULT_TIMESTAMP), CLOSEST_READING_MS, false);
                if (bg != null) {
                    UserError.Log.wtf(TAG, "Error matches a different uuid with the same timestamp: " + bg.uuid + " vs " + btm.uuid + " skipping!");
                    return;
                }
                bg = new BgReading();
            }

            bg.timestamp = Wire.get(btm.timestamp, BgReadingMessage.DEFAULT_TIMESTAMP);
            bg.calculated_value = Wire.get(btm.calculated_value, BgReadingMessage.DEFAULT_CALCULATED_VALUE);
            bg.filtered_calculated_value = Wire.get(btm.filtered_calculated_value, BgReadingMessage.DEFAULT_FILTERED_CALCULATED_VALUE);
            bg.calibration_flag = Wire.get(btm.calibration_flag, BgReadingMessage.DEFAULT_CALIBRATION_FLAG);
            bg.raw_calculated = Wire.get(btm.raw_calculated, BgReadingMessage.DEFAULT_RAW_CALCULATED);
            bg.raw_data = Wire.get(btm.raw_data, BgReadingMessage.DEFAULT_RAW_DATA);
            bg.calculated_value_slope = Wire.get(btm.calculated_value_slope, BgReadingMessage.DEFAULT_CALCULATED_VALUE_SLOPE);
            bg.calibration_uuid = btm.calibration_uuid;
            bg.uuid = btm.uuid;
            bg.save();
        } else {
            UserError.Log.wtf(TAG, "processFromMessage uuid is null or invalid");
        }
    }

    public synchronized static void processFromMultiMessage(byte[] payload) {
        try {
            final BgReadingMultiMessage bgmm = BgReadingMultiMessage.ADAPTER.decode(payload);
            if ((bgmm != null) && (bgmm.bgreading_message != null)) {
                for (BgReadingMessage btm : bgmm.bgreading_message) {
                    processFromMessage(btm);
                }
                Home.staticRefreshBGCharts();
            }
        } catch (IOException | NullPointerException | IllegalStateException e) {
            UserError.Log.e(TAG, "exception processFromMessage: " + e);
        }
    }

    public String toJSON(boolean sendCalibration) {
        final JSONObject jsonObject = new JSONObject();
        try {
            jsonObject.put("uuid", uuid);
            jsonObject.put("a", a); // how much of this do we actually need?
            jsonObject.put("b", b);
            jsonObject.put("c", c);
            jsonObject.put("timestamp", timestamp);
            jsonObject.put("age_adjusted_raw_value", age_adjusted_raw_value);
            jsonObject.put("calculated_value", calculated_value);
            jsonObject.put("filtered_calculated_value", filtered_calculated_value);
            jsonObject.put("calibration_flag", calibration_flag);
            jsonObject.put("filtered_data", filtered_data);
            jsonObject.put("raw_calculated", raw_calculated);
            jsonObject.put("raw_data", raw_data);
            try {
                jsonObject.put("calculated_value_slope", calculated_value_slope);
            } catch (JSONException e) {
                jsonObject.put("hide_slope", true); // calculated value slope is NaN - hide slope should already be true locally too
            }
            if (sendCalibration) {
                jsonObject.put("calibration_uuid", calibration_uuid);
            }
            //   jsonObject.put("sensor", sensor);
            return jsonObject.toString();
        } catch (JSONException e) {
            UserError.Log.wtf(TAG, "Error producing in toJSON: " + e);
            if (Double.isNaN(a)) Log.e(TAG, "a is NaN");
            if (Double.isNaN(b)) Log.e(TAG, "b is NaN");
            if (Double.isNaN(c)) Log.e(TAG, "c is NaN");
            if (Double.isNaN(age_adjusted_raw_value)) Log.e(TAG, "age_adjusted_raw_value is NaN");
            if (Double.isNaN(calculated_value)) Log.e(TAG, "calculated_value is NaN");
            if (Double.isNaN(filtered_calculated_value)) Log.e(TAG, "filtered_calculated_value is NaN");
            if (Double.isNaN(filtered_data)) Log.e(TAG, "filtered_data is NaN");
            if (Double.isNaN(raw_calculated)) Log.e(TAG, "raw_calculated is NaN");
            if (Double.isNaN(raw_data)) Log.e(TAG, "raw_data is NaN");
            if (Double.isNaN(calculated_value_slope)) Log.e(TAG, "calculated_value_slope is NaN");
            return "";
        }
    }

    public static void deleteALL() {
        try {
            SQLiteUtils.execSql("delete from BgSendQueue");
            SQLiteUtils.execSql("delete from BgReadings");
            Log.d(TAG, "Deleting all BGReadings");
        } catch (Exception e) {
            Log.e(TAG, "Got exception running deleteALL " + e.toString());
        }
    }

    public static void deleteRandomData() {
        Random rand = new Random();
        int  minutes_ago_end = rand.nextInt(120);
        int  minutes_ago_start = minutes_ago_end + rand.nextInt(35)+5;
        long ts_start = JoH.tsl() - minutes_ago_start * Constants.MINUTE_IN_MS;
        long ts_end = JoH.tsl() - minutes_ago_end * Constants.MINUTE_IN_MS;
        UserError.Log.d(TAG,"Deleting random bgreadings: "+JoH.dateTimeText(ts_start)+" -> "+JoH.dateTimeText(ts_end));
        testDeleteRange(ts_start, ts_end);
    }

    public static void testDeleteRange(long start_time, long end_time) {
        List<BgReading> bgrs = new Delete()
                .from(BgReading.class)
                .where("timestamp < ?", end_time)
                .where("timestamp > ?",start_time)
                .execute();
       // UserError.Log.d("OB1TEST","Deleted: "+bgrs.size()+" records");
    }

    public static List<BgReading> cleanup(int retention_days) {
        return new Delete()
                .from(BgReading.class)
                .where("timestamp < ?", JoH.tsl() - (retention_days * Constants.DAY_IN_MS))
                .execute();
    }

    public static void cleanupOutOfRangeValues() {
        new Delete()
                .from(BgReading.class)
                .where("timestamp > ?", JoH.tsl() - (3 * Constants.DAY_IN_MS))
                .where("calculated_value > ?", 324)
                .execute();
    }


    // used in wear
    public static void cleanup(long timestamp) {
        try {
            SQLiteUtils.execSql("delete from BgSendQueue");
            List<BgReading> data = new Select()
                    .from(BgReading.class)
                    .where("timestamp < ?", timestamp)
                    .orderBy("timestamp desc")
                    .execute();
            if (data != null) Log.d(TAG, "cleanup BgReading size=" + data.size());
            new Cleanup().execute(data);
        } catch (Exception e) {
            Log.e(TAG, "Got exception running cleanup " + e.toString());
        }
    }

    // used in wear
    private static class Cleanup extends AsyncTask<List<BgReading>, Integer, Boolean> {
        @Override
        protected Boolean doInBackground(List<BgReading>... errors) {
            try {
                for(BgReading data : errors[0]) {
                    data.delete();
                }
                return true;
            } catch(Exception e) {
                return false;
            }
        }
    }


    //*******INSTANCE METHODS***********//
    public void perform_calculations() {
        find_new_curve();
        find_new_raw_curve();
        find_slope();
    }

    public void find_slope() {
        List<BgReading> last_2 = BgReading.latest(2);

        // FYI: By default, assertions are disabled at runtime. Add "-ea" to commandline to enable.
        // https://docs.oracle.com/javase/7/docs/technotes/guides/language/assert.html
        assert last_2.get(0).uuid.equals(this.uuid)
                : "Invariant condition not fulfilled: calculating slope and current reading wasn't saved before";

        if ((last_2 != null) && (last_2.size() == 2)) {
            calculated_value_slope = calculateSlope(this, last_2.get(1));
            save();
        } else if ((last_2 != null) && (last_2.size() == 1)) {
            calculated_value_slope = 0;
            save();
        } else {
            if (JoH.ratelimit("no-bg-couldnt-find-slope", 15)) {
                Log.w(TAG, "NO BG? COULDNT FIND SLOPE!");
            }
        }
    }


    public void find_new_curve() {
        JoH.clearCache();
        List<BgReading> last_3 = BgReading.latest(3);
        if ((last_3 != null) && (last_3.size() == 3)) {
            BgReading latest = last_3.get(0);
            BgReading second_latest = last_3.get(1);
            BgReading third_latest = last_3.get(2);

            double y3 = latest.calculated_value;
            double x3 = latest.timestamp;
            double y2 = second_latest.calculated_value;
            double x2 = second_latest.timestamp;
            double y1 = third_latest.calculated_value;
            double x1 = third_latest.timestamp;

            a = y1/((x1-x2)*(x1-x3))+y2/((x2-x1)*(x2-x3))+y3/((x3-x1)*(x3-x2));
            b = (-y1*(x2+x3)/((x1-x2)*(x1-x3))-y2*(x1+x3)/((x2-x1)*(x2-x3))-y3*(x1+x2)/((x3-x1)*(x3-x2)));
            c = (y1*x2*x3/((x1-x2)*(x1-x3))+y2*x1*x3/((x2-x1)*(x2-x3))+y3*x1*x2/((x3-x1)*(x3-x2)));

            Log.i(TAG, "find_new_curve: BG PARABOLIC RATES: "+a+"x^2 + "+b+"x + "+c);

            save();
        } else if ((last_3 != null) && (last_3.size() == 2)) {

            Log.i(TAG, "find_new_curve: Not enough data to calculate parabolic rates - assume Linear");
                BgReading latest = last_3.get(0);
                BgReading second_latest = last_3.get(1);

                double y2 = latest.calculated_value;
                double x2 = latest.timestamp;
                double y1 = second_latest.calculated_value;
                double x1 = second_latest.timestamp;

                if(y1 == y2) {
                    b = 0;
                } else {
                    b = (y2 - y1)/(x2 - x1);
                }
                a = 0;
                c = -1 * ((latest.b * x1) - y1);

            Log.i(TAG, ""+latest.a+"x^2 + "+latest.b+"x + "+latest.c);
                save();
            } else {
            Log.i(TAG, "find_new_curve: Not enough data to calculate parabolic rates - assume static data");
            a = 0;
            b = 0;
            c = calculated_value;

            Log.i(TAG, ""+a+"x^2 + "+b+"x + "+c);
            save();
        }
    }

    public void calculateAgeAdjustedRawValue(){
        final double adjust_for = AGE_ADJUSTMENT_TIME - time_since_sensor_started;
        if ((adjust_for > 0) && (!DexCollectionType.hasLibre())) {
            age_adjusted_raw_value = ((AGE_ADJUSTMENT_FACTOR * (adjust_for / AGE_ADJUSTMENT_TIME)) * raw_data) + raw_data;
            Log.i(TAG, "calculateAgeAdjustedRawValue: RAW VALUE ADJUSTMENT FROM:" + raw_data + " TO: " + age_adjusted_raw_value);
        } else {
            age_adjusted_raw_value = raw_data;
        }
    }

    void find_new_raw_curve() {
        JoH.clearCache();
        final List<BgReading> last_3 = BgReading.latest(3);
        if ((last_3 != null) && (last_3.size() == 3)) {

            final BgReading latest = last_3.get(0);
            final BgReading second_latest = last_3.get(1);
            final BgReading third_latest = last_3.get(2);

            double y3 = latest.age_adjusted_raw_value;
            double x3 = latest.timestamp;
            double y2 = second_latest.age_adjusted_raw_value;
            double x2 = second_latest.timestamp;
            double y1 = third_latest.age_adjusted_raw_value;
            double x1 = third_latest.timestamp;

            ra = y1/((x1-x2)*(x1-x3))+y2/((x2-x1)*(x2-x3))+y3/((x3-x1)*(x3-x2));
            rb = (-y1*(x2+x3)/((x1-x2)*(x1-x3))-y2*(x1+x3)/((x2-x1)*(x2-x3))-y3*(x1+x2)/((x3-x1)*(x3-x2)));
            rc = (y1*x2*x3/((x1-x2)*(x1-x3))+y2*x1*x3/((x2-x1)*(x2-x3))+y3*x1*x2/((x3-x1)*(x3-x2)));

            Log.i(TAG, "find_new_raw_curve: RAW PARABOLIC RATES: "+ra+"x^2 + "+rb+"x + "+rc);
            save();
        } else if ((last_3 != null) && (last_3.size()) == 2) {
            BgReading latest = last_3.get(0);
            BgReading second_latest = last_3.get(1);

            double y2 = latest.age_adjusted_raw_value;
            double x2 = latest.timestamp;
            double y1 = second_latest.age_adjusted_raw_value;
            double x1 = second_latest.timestamp;
            if(y1 == y2) {
                rb = 0;
            } else {
                rb = (y2 - y1)/(x2 - x1);
            }
            ra = 0;
            rc = -1 * ((latest.rb * x1) - y1);

            Log.i(TAG, "find_new_raw_curve: Not enough data to calculate parabolic rates - assume Linear data");

            Log.i(TAG, "RAW PARABOLIC RATES: "+ra+"x^2 + "+rb+"x + "+rc);
            save();
        } else {
            Log.i(TAG, "find_new_raw_curve: Not enough data to calculate parabolic rates - assume static data");
            BgReading latest_entry = BgReading.lastNoSenssor();
            ra = 0;
            rb = 0;
            if (latest_entry != null) {
                rc = latest_entry.age_adjusted_raw_value;
            } else {
                rc = 105;
            }

            save();
        }
    }
    private static double weightedAverageRaw(double timeA, double timeB, double calibrationTime, double rawA, double rawB) {
        final double relativeSlope = (rawB -  rawA)/(timeB - timeA);
        final double relativeIntercept = rawA - (relativeSlope * timeA);
        return ((relativeSlope * calibrationTime) + relativeIntercept);
    }

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

    public String timeStamp() {
        return JoH.dateTimeText(timestamp);
    }

    public int noiseValue() {
        if(noise == null || noise.compareTo("") == 0) {
            return 1;
        } else {
            return Integer.valueOf(noise);
        }
    }

    public BgReading injectNoise(boolean save) {
        final BgReading bgReading = this;
        if (JoH.msSince(bgReading.timestamp) > Constants.MINUTE_IN_MS * 20) {
            bgReading.noise = "0";
        } else {
            BgGraphBuilder.refreshNoiseIfOlderThan(bgReading.timestamp);
            if (BgGraphBuilder.last_noise > BgGraphBuilder.NOISE_HIGH) {
                bgReading.noise = "4";
            } else if (BgGraphBuilder.last_noise > BgGraphBuilder.NOISE_TOO_HIGH_FOR_PREDICT) {
                bgReading.noise = "3";
            } else if (BgGraphBuilder.last_noise > BgGraphBuilder.NOISE_TRIGGER) {
                bgReading.noise = "2";
            }
        }
        if (save) bgReading.save();
        return bgReading;
    }

    // list(0) is the most recent reading.
    public static List<BgReading> getXRecentPoints(int NumReadings) {
        List<BgReading> latest = BgReading.latest(NumReadings);
        if (latest == null || latest.size() != NumReadings) {
            // for less than NumReadings readings, we can't tell what the situation
            //
            Log.d(TAG_ALERT, "getXRecentPoints we don't have enough readings, returning null");
            return null;
        }
        // So, we have at least three values...
        for(BgReading bgReading : latest) {
            Log.d(TAG_ALERT, "getXRecentPoints - reading: time = " + bgReading.timestamp + " calculated_value " + bgReading.calculated_value);
        }

        // now let's check that they are relevant. the last reading should be from the last 5 minutes,
        // x-1 more readings should be from the last (x-1)*5 minutes. we will allow 5 minutes for the last
        // x to allow one packet to be missed.
        if (new Date().getTime() - latest.get(NumReadings - 1).timestamp > (NumReadings * 5 + 6) * 60 * 1000) {
            Log.d(TAG_ALERT, "getXRecentPoints we don't have enough points from the last " + (NumReadings * 5 + 6) + " minutes, returning null");
            return null;
        }
        return latest;

    }

    public static boolean checkForPersistentHigh() {

        // skip if not enabled
        if (!Pref.getBooleanDefaultFalse("persistent_high_alert_enabled")) return false;


        List<BgReading> last = BgReading.latest(1);
        if ((last != null) && (last.size()>0)) {

            final long now = JoH.tsl();
            final long since = now - last.get(0).timestamp;
            // only process if last reading <10 mins
            if (since < 600000) {
                // check if exceeding high
                if (last.get(0).calculated_value >
                        Home.convertToMgDlIfMmol(
                                JoH.tolerantParseDouble(Pref.getString("highValue", "170")))) {

                    final double this_slope = last.get(0).calculated_value_slope * 60000;
                    //Log.d(TAG, "CheckForPersistentHigh: Slope: " + JoH.qs(this_slope));

                    // if not falling
                    if (this_slope > 0) {
                        final long high_since = Pref.getLong(PERSISTENT_HIGH_SINCE, 0);
                        if (high_since == 0) {
                            // no previous persistent high so set start as now
                            Pref.setLong(PERSISTENT_HIGH_SINCE, now);
                            Log.d(TAG, "Registering start of persistent high at time now");
                        } else {
                            final long high_for_mins = (now - high_since) / (1000 * 60);
                            long threshold_mins;
                            try {
                                threshold_mins = Long.parseLong(Pref.getString("persistent_high_threshold_mins", "60"));
                            } catch (NumberFormatException e) {
                                threshold_mins = 60;
                                Home.toaststaticnext("Invalid persistent high for longer than minutes setting: using 60 mins instead");
                            }
                            if (high_for_mins > threshold_mins) {
                                // we have been high for longer than the threshold - raise alert

                                // except if alerts are disabled
                                if (Pref.getLong("alerts_disabled_until", 0) > new Date().getTime()) {
                                    Log.i(TAG, "checkforPersistentHigh: Notifications are currently disabled cannot alert!!");
                                    return false;
                                }
                                Log.i(TAG, "Persistent high for: " + high_for_mins + " mins -> alerting");
                                Notifications.persistentHighAlert(xdrip.getAppContext(), true, xdrip.getAppContext().getString(R.string.persistent_high_for_greater_than) + (int) high_for_mins + xdrip.getAppContext().getString(R.string.space_mins));

                            } else {
                                Log.d(TAG, "Persistent high below time threshold at: " + high_for_mins);
                            }
                        }
                    }
                } else {
                    // not high - cancel any existing
                    if (Pref.getLong(PERSISTENT_HIGH_SINCE,0)!=0)
                    {
                        Log.i(TAG,"Cancelling previous persistent high as we are no longer high");
                     Pref.setLong(PERSISTENT_HIGH_SINCE, 0); // clear it
                        Notifications.persistentHighAlert(xdrip.getAppContext(), false, ""); // cancel it
                    }
                }
            }
        }
        return false; // actually we should probably return void as we do everything inside this method
    }

    public static void checkForRisingAllert(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        Boolean rising_alert = prefs.getBoolean("rising_alert", false);
        if(!rising_alert) {
            return;
        }
        if(prefs.getLong("alerts_disabled_until", 0) > new Date().getTime()){
            Log.i("NOTIFICATIONS", "checkForRisingAllert: Notifications are currently disabled!!");
            return;
        }

        String riseRate = prefs.getString("rising_bg_val", "2");
        float friseRate = 2;

        try
        {
            friseRate = Float.parseFloat(riseRate);
        }
        catch (NumberFormatException nfe)
        {
            Log.e(TAG_ALERT, "checkForRisingAllert reading falling_bg_val failed, continuing with 2", nfe);
        }
        Log.d(TAG_ALERT, "checkForRisingAllert will check for rate of " + friseRate);

        boolean riseAlert = checkForDropRiseAllert(friseRate, false);
        Notifications.RisingAlert(context, riseAlert);
    }


    public static void checkForDropAllert(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        Boolean falling_alert = prefs.getBoolean("falling_alert", false);
        if(!falling_alert) {
            return;
        }
        if(prefs.getLong("alerts_disabled_until", 0) > new Date().getTime()){
            Log.d("NOTIFICATIONS", "checkForDropAllert: Notifications are currently disabled!!");
            return;
        }

        String dropRate = prefs.getString("falling_bg_val", "2");
        float fdropRate = 2;

        try
        {
            fdropRate = Float.parseFloat(dropRate);
        }
        catch (NumberFormatException nfe)
        {
            Log.e(TAG_ALERT, "reading falling_bg_val failed, continuing with 2", nfe);
        }
        Log.i(TAG_ALERT, "checkForDropAllert will check for rate of " + fdropRate);

        boolean dropAlert = checkForDropRiseAllert(fdropRate, true);
        Notifications.DropAlert(context, dropAlert);
    }

    // true say, alert is on.
    private static boolean checkForDropRiseAllert(float MaxSpeed, boolean drop) {
        Log.d(TAG_ALERT, "checkForDropRiseAllert called drop=" + drop);
        List<BgReading> latest = getXRecentPoints(4);
        if(latest == null) {
            Log.d(TAG_ALERT, "checkForDropRiseAllert we don't have enough points from the last 15 minutes, returning false");
            return false;
        }
        float time3 = (latest.get(0).timestamp - latest.get(3).timestamp) / 60000;
        double bg_diff3 = latest.get(3).calculated_value - latest.get(0).calculated_value;
        if (!drop) {
            bg_diff3 *= (-1);
        }
        Log.i(TAG_ALERT, "bg_diff3=" + bg_diff3 + " time3 = " + time3);
        if(bg_diff3 < time3 * MaxSpeed) {
            Log.d(TAG_ALERT, "checkForDropRiseAllert for latest 4 points not fast enough, returning false");
            return false;
        }
        // we should alert here, but if the last measurement was less than MaxSpeed / 2, I won't.


        float time1 = (latest.get(0).timestamp - latest.get(1).timestamp) / 60000;
        double bg_diff1 = latest.get(1).calculated_value - latest.get(0).calculated_value;
        if (!drop) {
            bg_diff1 *= (-1);
        }

        if(time1 > 7.0) {
            Log.d(TAG_ALERT, "checkForDropRiseAllert the two points are not close enough, returning true");
            return true;
        }
        if(bg_diff1 < time1 * MaxSpeed /2) {
            Log.d(TAG_ALERT, "checkForDropRiseAllert for latest 2 points not fast enough, returning false");
            return false;
        }
        Log.d(TAG_ALERT, "checkForDropRiseAllert returning true speed is " + (bg_diff3 / time3));
        return true;
    }

    // Make sure that this function either sets the alert or removes it.
    public static boolean getAndRaiseUnclearReading(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        if(prefs.getLong("alerts_disabled_until", 0) > new Date().getTime()){
            Log.d("NOTIFICATIONS", "getAndRaiseUnclearReading Notifications are currently disabled!!");
            UserNotification.DeleteNotificationByType("bg_unclear_readings_alert");
            return false;
        }

        Boolean bg_unclear_readings_alerts = prefs.getBoolean("bg_unclear_readings_alerts", false);
        if (!bg_unclear_readings_alerts
                || !DexCollectionType.hasFiltered()
                || Ob1G5CollectionService.usingG6()
                || Ob1G5CollectionService.usingNativeMode()) {
            Log.d(TAG_ALERT, "getUnclearReading returned false since feature is disabled");
            UserNotification.DeleteNotificationByType("bg_unclear_readings_alert");
            return false;
        }
        Long UnclearTimeSetting = Long.parseLong(prefs.getString("bg_unclear_readings_minutes", "90")) * 60000;

        Long UnclearTime = BgReading.getUnclearTime(UnclearTimeSetting);

        if (UnclearTime >= UnclearTimeSetting ) {
            Log.d("NOTIFICATIONS", "Readings have been unclear for too long!!");
            Notifications.bgUnclearAlert(context);
            return true;
        }

        UserNotification.DeleteNotificationByType("bg_unclear_readings_alert");

        if (UnclearTime > 0 ) {
            Log.d(TAG_ALERT, "We are in an clear state, but not for too long. Alerts are disabled");
            return true;
        }

        return false;
    }
    /*
     * This function comes to check weather we are in a case that we have an allert but since things are
     * getting better we should not do anything. (This is only in the case that the alert was snoozed before.)
     * This means that if this is a low alert, and we have two readings in the last 15 minutes, and
     * either we have gone in 10 in the last two readings, or we have gone in 3 in the last reading, we
     * don't play the alert again, but rather wait for the alert to finish.
     *  I'll start with having the same values for the high alerts.
    */

    public static boolean trendingToAlertEnd(Context context, boolean above) {
        // TODO: check if we are not in an UnclerTime.
        Log.d(TAG_ALERT, "trendingToAlertEnd called");

        List<BgReading> latest = getXRecentPoints(3);
        if(latest == null) {
            Log.d(TAG_ALERT, "trendingToAlertEnd we don't have enough points from the last 15 minutes, returning false");
            return false;
        }

        if(above == false) {
            // This is a low alert, we should be going up
            if((latest.get(0).calculated_value - latest.get(1).calculated_value > 4) ||
               (latest.get(0).calculated_value - latest.get(2).calculated_value > 10)) {
                Log.d(TAG_ALERT, "trendingToAlertEnd returning true for low alert");
                return true;
            }
        } else {
            // This is a high alert we should be heading down
            if((latest.get(1).calculated_value - latest.get(0).calculated_value > 4) ||
               (latest.get(2).calculated_value - latest.get(0).calculated_value > 10)) {
                Log.d(TAG_ALERT, "trendingToAlertEnd returning true for high alert");
                return true;
            }
        }
        Log.d(TAG_ALERT, "trendingToAlertEnd returning false, not in the right direction (or not fast enough)");
        return false;

    }

    // Should that be combined with noiseValue?
    private Boolean Unclear() {
        Log.d(TAG_ALERT, "Unclear filtered_data=" + filtered_data + " raw_data=" + raw_data);
        return raw_data > filtered_data * 1.3 || raw_data < filtered_data * 0.7;
    }

    /*
     * returns the time (in ms) that the state is not clear and no alerts should work
     * The base of the algorithm is that any period can be bad or not. bgReading.Unclear() tells that.
     * a non clear bgReading means MAX_INFLUANCE time after it we are in a bad position
     * Since this code is based on heuristics, and since times are not accurate, boundary issues can be ignored.
     *
     * interstingTime is the period to check. That is if the last period is bad, we want to know how long does it go bad...
     * */

    // The extra 120,000 is to allow the packet to be delayed for some time and still be counted in that group
    // Please don't use for MAX_INFLUANCE a number that is complete multiply of 5 minutes (300,000)
    static final int MAX_INFLUANCE = 30 * 60000 - 120000; // A bad point means data is untrusted for 30 minutes.
    private static Long getUnclearTimeHelper(List<BgReading> latest, Long interstingTime, final Long now) {

        // The code ignores missing points (that is they some times are treated as good and some times as bad.
        // If this bothers someone, I believe that the list should be filled with the missing points as good and continue to run.

        Long LastGoodTime = 0l; // 0 represents that we are not in a good part

        Long UnclearTime = 0l;
        for(BgReading bgReading : latest) {
            // going over the readings from latest to first
            if(bgReading.timestamp < now - (interstingTime + MAX_INFLUANCE)) {
                // Some readings are missing, we can stop checking
                break;
            }
            if(bgReading.timestamp <= now - MAX_INFLUANCE  && UnclearTime == 0) {
                Log.d(TAG_ALERT, "We did not have a problematic reading for MAX_INFLUANCE time, so now all is well");
                return 0l;

            }
            if (bgReading.Unclear()) {
                // here we assume that there are no missing points. Missing points might join the good and bad values as well...
                // we should have checked if we have a period, but it is hard to say how to react to them.
                Log.d(TAG_ALERT, "We have a bad reading, so setting UnclearTime to " + bgReading.timestamp);
                UnclearTime = bgReading.timestamp;
                LastGoodTime = 0l;
            } else {
                if (LastGoodTime == 0l) {
                    Log.d(TAG_ALERT, "We are starting a good period at "+ bgReading.timestamp);
                    LastGoodTime = bgReading.timestamp;
                } else {
                    // we have some good period, is it good enough?
                    if(LastGoodTime - bgReading.timestamp >= MAX_INFLUANCE) {
                        // Here UnclearTime should be already set, otherwise we will return a toob big value
                        if (UnclearTime ==0) {
                            Log.wtf(TAG_ALERT, "ERROR - UnclearTime must not be 0 here !!!");
                        }
                        Log.d(TAG_ALERT, "We have a good period from " + bgReading.timestamp + " to " + LastGoodTime + "returning " + (now - UnclearTime +5 *60000));
                        return now - UnclearTime + 5 *60000;
                    }
                }
            }
        }
        // if we are here, we have a problem... or not.
        if(UnclearTime == 0l) {
            Log.d(TAG_ALERT, "Since we did not find a good period, but we also did not find a single bad value, we assume things are good");
            return 0l;
        }
        Log.d(TAG_ALERT, "We scanned all over, but could not find a good period. we have a bad value, so assuming that the whole period is bad" +
                " returning " + interstingTime);
        // Note that we might now have all the points, and in this case, since we don't have a good period I return a bad period.
        return interstingTime;

    }

    // This is to enable testing of the function, by passing different values
    public static Long getUnclearTime(Long interstingTime) {
        List<BgReading> latest = BgReading.latest((interstingTime.intValue() + MAX_INFLUANCE)/ 60000 /5 );
        if (latest == null) {
            return 0L;
        }
        final Long now = new Date().getTime();
        return getUnclearTimeHelper(latest, interstingTime, now);

    }

    public static Long getTimeSinceLastReading() {
        BgReading bgReading = BgReading.last();
        if (bgReading != null) {
            return (new Date().getTime() - bgReading.timestamp);
        }
        return (long) 0;
    }

    public double usedRaw() {
        Calibration calibration = Calibration.lastValid();
        if (calibration != null && calibration.check_in) {
            return raw_data;
        }
        return age_adjusted_raw_value;
    }

    public boolean isCurrent() {
        return JoH.msSince(timestamp) < Constants.MINUTE_IN_MS * 2;
    }

    public double ageAdjustedFiltered(){
        double usedRaw = usedRaw();
        if(usedRaw == raw_data || raw_data == 0d){
            return filtered_data;
        } else {
            // adjust the filtered_data with the same factor as the age adjusted raw value
            return filtered_data * (usedRaw/raw_data);
        }
    }

    // ignores calibration checkins for speed
    public double ageAdjustedFiltered_fast() {
        // adjust the filtered_data with the same factor as the age adjusted raw value
        return filtered_data * (age_adjusted_raw_value / raw_data);
    }

    // the input of this function is a string. each char can be g(=good) or b(=bad) or s(=skip, point unmissed).
    static List<BgReading> createlatestTest(String input, Long now) {
        Random randomGenerator = new Random();
        List<BgReading> out = new LinkedList<BgReading> ();
        char[] chars=  input.toCharArray();
        for(int i=0; i < chars.length; i++) {
            BgReading bg = new BgReading();
            int rand = randomGenerator.nextInt(20000) - 10000;
            bg.timestamp = now - i * 5 * 60000 + rand;
            bg.raw_data = 150;
            if(chars[i] == 'g') {
                bg.filtered_data = 151;
            } else if (chars[i] == 'b') {
                bg.filtered_data = 110;
            } else {
                continue;
            }
            out.add(bg);
        }
        return out;


    }
    static void TestgetUnclearTime(String input, Long interstingTime, Long expectedResult) {
        final Long now = new Date().getTime();
        List<BgReading> readings = createlatestTest(input, now);
        Long result = getUnclearTimeHelper(readings, interstingTime * 60000, now);
        if (result >= expectedResult * 60000 - 20000 && result <= expectedResult * 60000+20000) {
            Log.d(TAG_ALERT, "Test passed");
        } else {
            Log.d(TAG_ALERT, "Test failed expectedResult = " + expectedResult + " result = "+ result / 60000.0);
        }

    }

    public static void TestgetUnclearTimes() {
        TestgetUnclearTime("gggggggggggggggggggggggg", 90l, 0l * 5);
        TestgetUnclearTime("bggggggggggggggggggggggg", 90l, 1l * 5);
        TestgetUnclearTime("bbgggggggggggggggggggggg", 90l, 2l *5 );
        TestgetUnclearTime("gbgggggggggggggggggggggg", 90l, 2l * 5);
        TestgetUnclearTime("gbgggbggbggbggbggbggbgbg", 90l, 18l * 5);
        TestgetUnclearTime("bbbgggggggbbgggggggggggg", 90l, 3l * 5);
        TestgetUnclearTime("ggggggbbbbbbgggggggggggg", 90l, 0l * 5);
        TestgetUnclearTime("ggssgggggggggggggggggggg", 90l, 0l * 5);
        TestgetUnclearTime("ggssbggssggggggggggggggg", 90l, 5l * 5);
        TestgetUnclearTime("bb",                       90l, 18l * 5);

        // intersting time is 2 minutes, we should always get 0 (in 5 minutes units
        TestgetUnclearTime("gggggggggggggggggggggggg", 2l, 0l  * 5);
        TestgetUnclearTime("bggggggggggggggggggggggg", 2l, 2l);
        TestgetUnclearTime("bbgggggggggggggggggggggg", 2l, 2l);
        TestgetUnclearTime("gbgggggggggggggggggggggg", 2l, 2l);
        TestgetUnclearTime("gbgggbggbggbggbggbggbgbg", 2l, 2l);

        // intersting time is 10 minutes, we should always get 0 (in 5 minutes units
        TestgetUnclearTime("gggggggggggggggggggggggg", 10l, 0l  * 5);
        TestgetUnclearTime("bggggggggggggggggggggggg", 10l, 1l * 5);
        TestgetUnclearTime("bbgggggggggggggggggggggg", 10l, 2l * 5);
        TestgetUnclearTime("gbgggggggggggggggggggggg", 10l, 2l * 5);
        TestgetUnclearTime("gbgggbggbggbggbggbggbgbg", 10l, 2l * 5);
        TestgetUnclearTime("bbbgggggggbbgggggggggggg", 10l, 2l * 5);
        TestgetUnclearTime("ggggggbbbbbbgggggggggggg", 10l, 0l * 5);
        TestgetUnclearTime("ggssgggggggggggggggggggg", 10l, 0l * 5);
        TestgetUnclearTime("ggssbggssggggggggggggggg", 10l, 2l * 5);
        TestgetUnclearTime("bb",                       10l, 2l * 5);
    }

    public int getSlopeOrdinal() {
        double slope_by_minute = calculated_value_slope * 60000;
        int ordinal = 0;
        if(!hide_slope) {
            if (slope_by_minute <= (-3.5)) {
                ordinal = 7;
            } else if (slope_by_minute <= (-2)) {
                ordinal = 6;
            } else if (slope_by_minute <= (-1)) {
                ordinal = 5;
            } else if (slope_by_minute <= (1)) {
                ordinal = 4;
            } else if (slope_by_minute <= (2)) {
                ordinal = 3;
            } else if (slope_by_minute <= (3.5)) {
                ordinal = 2;
            } else {
                ordinal = 1;
            }
        }
        return ordinal;
    }

    public int getMgdlValue() {
        return (int) calculated_value;
    }

    public long getEpochTimestamp() {
        return timestamp;
    }
}