package com.eveningoutpost.dexdrip.calibrations;

import com.eveningoutpost.dexdrip.Models.BgReading;
import com.eveningoutpost.dexdrip.Models.JoH;
import com.eveningoutpost.dexdrip.Models.UserError;
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;

import java.util.HashMap;
import java.util.Map;

/**
 * Created by jamorham on 04/10/2016.
 * <p>
 * The idea here is for a standard class format which you can
 * extend to implement your own pluggable calibration algorithms
 * <p>
 * See FixedSlopeExample or Datricsae for examples on doing this
 */

public abstract class CalibrationAbstract {

    private static Map<String, CalibrationData> memory_cache = new HashMap<>();

    private static final double HIGHEST_SANE_INTERCEPT = 39; // max value that intercept can be with positive slope

    /* Overridable methods */

    // boolean responses typically indicate if anything received and processed the call
    // null return values mean unsupported or invalid

    // get the calibration data (caching is handled internally)

    public synchronized CalibrationData getCalibrationData() {
        return getCalibrationData(JoH.tsl() + Constants.HOUR_IN_MS);
    }

    public synchronized CalibrationData getCalibrationData(long until) {
        // default no implementation
        return null;
    }


    // get calibration data at specific timestamp (more advanced)

    public CalibrationData getCalibrationDataAtTime(long timestamp) {
        // default no implementation
        return null;
    }

    // indicate that the cache should be invalidated as BG sample data has changed
    // or time has passed in a way we want to invalidate any existing cache

    public boolean invalidateCache() {
        // default no implementation
        return true;
    }


    // called when any new sensor data is available such as on every reading
    // this could be used to invalidate the cache if this extra data is used;
    public boolean newSensorData() {
        return false;
    }

    // called when any new sensor data is available within 20 minutes of last calibration
    // this could be used to invalidate the cache if this extra data is used;
    public boolean newCloseSensorData() {
        return false;
    }


    // called when new blood glucose data is available or there is a change in existing data
    // by default this invalidates the caches
    public boolean newFingerStickData() {
        return PluggableCalibration.invalidateAllCaches();
    }


    // the name of the alg - should be v.similar to its class name

    public String getAlgorithmName() {
        // default no implementation
        return null;
    }

    // a more detailed description of the basic idea behind the plugin

    public String getAlgorithmDescription() {
        // default no implementation
        return null;
    }


    /* Common utility methods */

    public String getNiceNameAndDescription() {
        String name = getAlgorithmName();
        String description = (name != null) ? getAlgorithmDescription() : "";
        return ((name != null) ? name : "None") + " - " + ((description != null) ? description : "");
    }


    // slower method but for ease of use when calculating a single value

    public double getGlucoseFromSensorValue(double raw) {
        final CalibrationData data = getCalibrationData();
        return getGlucoseFromSensorValue(raw, data);
    }


    // faster method when CalibrationData is passed - could be overridden for non-linear algs

    public double getGlucoseFromSensorValue(double raw, CalibrationData data) {
        if (data == null) return -1;
        if (!isCalibrationSane(data)) return -1;
        return raw * data.slope + data.intercept;
    }

    // faster method when CalibrationData is passed - could be overridden for non-linear algs

    public double getGlucoseFromBgReading(BgReading bgReading, CalibrationData data) {
        if (data == null) return -1;
        if (bgReading == null) return -1;
        if (!isCalibrationSane(data)) {
            recordSanityFailure(data);
            return -1;
        }
        // algorithm can override to decide whether or not to be using age_adjusted_raw
        return bgReading.age_adjusted_raw_value * data.slope + data.intercept;
    }

    public BgReading getBgReadingFromBgReading(BgReading bgReading, CalibrationData data) {
        if (data == null) return null;
        if (bgReading == null) return null;
        // do we need deep clone?
        final BgReading new_bg = (BgReading) JoH.cloneObject(bgReading);
        if (new_bg == null) return null;
        // algorithm can override to decide whether or not to be using age_adjusted_raw
        new_bg.calculated_value = getGlucoseFromBgReading(bgReading, data);
        new_bg.filtered_calculated_value = getGlucoseFromFilteredBgReading(bgReading, data);
        return new_bg;
    }

    public double getGlucoseFromFilteredBgReading(BgReading bgReading, CalibrationData data) {
        if (data == null) return -1;
        if (bgReading == null) return -1;
        if (!isCalibrationSane(data)) {
            recordSanityFailure(data);
            return -1;
        }
        // algorithm can override to decide whether or not to be using age_adjusted_raw
        return bgReading.ageAdjustedFiltered_fast() * data.slope + data.intercept;
    }

    public boolean isCalibrationSane() {
        return isCalibrationSane(JoH.tsl());
    }

    public boolean isCalibrationSane(final long until) {
        final CalibrationData data = getCalibrationData(until);
        return isCalibrationSane(data);
    }

    // Check that intercept is less than the highest allowed value.
    // This does not cater for negative slopes but they are not used by any algorithm at this point.
    public boolean isCalibrationSane(final CalibrationData data) {
        return data != null && !(data.intercept > HIGHEST_SANE_INTERCEPT);
    }

    private void recordSanityFailure(final CalibrationData pcalibration) {
        if (JoH.pratelimit("best-sanity-failure", 600)) {
            UserError.Log.wtf(getAlgorithmName(), "Unable to produce data due to plugin failing sanity check: " + pcalibration.toS());
        }
    }

    // TODO there currently is no way to opt out of this sanity check and for performance reasons
    // TODO we have to be careful about how one is implemented if it is needed.
    public static double getHighestSaneIntercept() {
        return HIGHEST_SANE_INTERCEPT;
    }

    protected static CalibrationData jsonStringToData(String json) {
        try {
            return new Gson().fromJson(json, CalibrationData.class);
        } catch (Exception e) {
            return null;
        }
    }

    protected static String dataToJsonString(CalibrationData data) {
        final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
        try {
            return gson.toJson(data);
        } catch (NullPointerException e) {
            return "";
        }
    }

    // persistent old style cache
    protected static boolean saveDataToCache(String tag, CalibrationData data) {
        final String lookup_tag = "CalibrationDataCache-" + tag;
        memory_cache.put(lookup_tag, data);
        PersistentStore.setString(lookup_tag, dataToJsonString(data));
        return true;
    }

    // memory only cache
    protected static boolean clearMemoryCache() {
        memory_cache.clear();
        return true;
    }

    // memory only cache - TODO possible room for improvement using timestamp as well
    protected static boolean saveDataToCache(String tag, CalibrationData data, long timestamp, long last_calibration) {
        final String lookup_tag = tag + last_calibration;
        memory_cache.put(lookup_tag, data);
        return true;
    }

    // memory only cache
    protected static CalibrationData loadDataFromCache(String tag, long timestamp) {
        final String lookup_tag = tag + timestamp;
        return memory_cache.get(lookup_tag);
    }

    // persistent old style cache
    protected static CalibrationData loadDataFromCache(String tag) {
        final String lookup_tag = "CalibrationDataCache-" + tag;
        if (!memory_cache.containsKey(lookup_tag)) {
            memory_cache.put(lookup_tag, jsonStringToData(PersistentStore.getString(lookup_tag)));
        }
        return memory_cache.get(lookup_tag);
    }

    /* Data Exchange Class */

    // for returning data to xDrip

    public class CalibrationData {
        @Expose
        public double slope;
        @Expose
        public double intercept;
        @Expose
        public long created;

        public CalibrationData(double slope, double intercept) {
            this.slope = slope;
            this.intercept = intercept;
            this.created = JoH.tsl();
        }

        public String toS() {
            return JoH.defaultGsonInstance().toJson(this);
        }
    }
}