package com.eveningoutpost.dexdrip.Models;

import android.os.AsyncTask;
import android.provider.BaseColumns;

import com.eveningoutpost.dexdrip.G5Model.Transmitter;
import com.eveningoutpost.dexdrip.GcmActivity;
import com.eveningoutpost.dexdrip.Home;
import com.eveningoutpost.dexdrip.Models.UserError.Log;

import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Select;
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
import com.eveningoutpost.dexdrip.utils.CheckBridgeBattery;
import com.eveningoutpost.dexdrip.utils.DexCollectionType;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;
import com.google.gson.internal.bind.DateTypeAdapter;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Date;
import java.util.List;
import java.util.UUID;

/**
 * Created by Emma Black on 11/6/14.
 */

@Table(name = "TransmitterData", id = BaseColumns._ID)
public class TransmitterData extends Model {
    private final static String TAG = TransmitterData.class.getSimpleName();

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

    // TODO these should be int or long surely
    @Expose
    @Column(name = "raw_data")
    public double raw_data;

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

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

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

    public static synchronized TransmitterData create(byte[] buffer, int len, Long timestamp) {
        if (len < 6) { return null; }
        final TransmitterData transmitterData = new TransmitterData();
        try {
            if ((buffer[0] == 0x11 || buffer[0] == 0x15) && buffer[1] == 0x00) {
                //this is a dexbridge packet.  Process accordingly.
                Log.i(TAG, "create Processing a Dexbridge packet");
                final ByteBuffer txData = ByteBuffer.allocate(len);
                txData.order(ByteOrder.LITTLE_ENDIAN);
                txData.put(buffer, 0, len);
                transmitterData.raw_data = txData.getInt(2);
                transmitterData.filtered_data = txData.getInt(6);
                //  bitwise and with 0xff (1111....1) to avoid that the byte is treated as signed.
                transmitterData.sensor_battery_level = txData.get(10) & 0xff;
                if (buffer[0] == 0x15) {
                    Log.i(TAG, "create Processing a Dexbridge packet includes delay information");
                    transmitterData.timestamp = timestamp - txData.getInt(16);
                } else {
                    transmitterData.timestamp = timestamp;
                }
                Log.i(TAG, "Created transmitterData record with Raw value of " + transmitterData.raw_data + " and Filtered value of " + transmitterData.filtered_data + " at " + timestamp + " with timestamp " + transmitterData.timestamp);
            } else { //this is NOT a dexbridge packet.  Process accordingly.
                Log.i(TAG, "create Processing a BTWixel or IPWixel packet");
                StringBuilder data_string = new StringBuilder();
                for (int i = 0; i < len; ++i) {
                    data_string.append((char) buffer[i]);
                }
                final String[] data = data_string.toString().split("\\s+");

                if (data.length > 1) {
                    transmitterData.sensor_battery_level = Integer.parseInt(data[1]);
                    if (data.length > 2) {
                        try {
                            Pref.setInt("bridge_battery", Integer.parseInt(data[2]));
                            if (Home.get_master()) {
                                GcmActivity.sendBridgeBattery(Pref.getInt("bridge_battery", -1));
                            }
                            CheckBridgeBattery.checkBridgeBattery();
                        } catch (Exception e) {
                            Log.e(TAG, "Got exception processing classic wixel or limitter battery value: " + e.toString());
                        }
                        if (data.length > 3) {
                            if ((DexCollectionType.getDexCollectionType() == DexCollectionType.LimiTTer)
                                    && (!Pref.getBooleanDefaultFalse("use_transmiter_pl_bluetooth"))) {
                                try {
                                    // reported sensor age in minutes
                                    final Integer sensorAge = Integer.parseInt(data[3]);
                                    if ((sensorAge > 0) && (sensorAge < 200000))
                                        Pref.setInt("nfc_sensor_age", sensorAge);
                                } catch (Exception e) {
                                    Log.e(TAG, "Got exception processing field 4 in classic limitter protocol: " + e);
                                }
                            }
                        }
                    }
                }
                transmitterData.raw_data = Integer.parseInt(data[0]);
                transmitterData.filtered_data = Integer.parseInt(data[0]);
                // TODO process does_have_filtered_here with extended protocol
                transmitterData.timestamp = timestamp;
            }

            //Stop allowing readings that are older than the last one - or duplicate data, its bad! (from savek-cc)
            final TransmitterData lastTransmitterData = TransmitterData.last();
            if (lastTransmitterData != null && lastTransmitterData.timestamp >= transmitterData.timestamp) {
                Log.e(TAG, "Rejecting TransmitterData constraint: last: " + JoH.dateTimeText(lastTransmitterData.timestamp) + " >= this: " + JoH.dateTimeText(transmitterData.timestamp));
                return null;
            }
            if (lastTransmitterData != null && lastTransmitterData.raw_data == transmitterData.raw_data && Math.abs(lastTransmitterData.timestamp - transmitterData.timestamp) < (Constants.MINUTE_IN_MS * 2)) {
                Log.e(TAG, "Rejecting identical TransmitterData constraint: last: " + JoH.dateTimeText(lastTransmitterData.timestamp) + " due to 2 minute rule this: " + JoH.dateTimeText(transmitterData.timestamp));
                return null;
            }
            final Calibration lastCalibration = Calibration.lastValid();
            if (lastCalibration != null && lastCalibration.timestamp > transmitterData.timestamp) {
                Log.e(TAG, "Rejecting historical TransmitterData constraint: calib: " + JoH.dateTimeText(lastCalibration.timestamp) + " > this: " + JoH.dateTimeText(transmitterData.timestamp));
                return null;
            }

            transmitterData.uuid = UUID.randomUUID().toString();
            transmitterData.save();
            return transmitterData;
        }catch(Exception e)
        {
            Log.e(TAG, "Got exception processing fields in protocol: " + e);
        }
        return null;
    }

    public static synchronized TransmitterData create(int raw_data, int filtered_data, int sensor_battery_level, long timestamp) {
        TransmitterData lastTransmitterData = TransmitterData.last();
        if (lastTransmitterData != null && lastTransmitterData.raw_data == raw_data && Math.abs(lastTransmitterData.timestamp - new Date().getTime()) < (Constants.MINUTE_IN_MS * 2)) { //Stop allowing duplicate data, its bad!
            return null;
        }

        TransmitterData transmitterData = new TransmitterData();
        transmitterData.sensor_battery_level = sensor_battery_level;
        transmitterData.raw_data = raw_data;
        transmitterData.filtered_data = filtered_data;
        transmitterData.timestamp = timestamp;
        transmitterData.uuid = UUID.randomUUID().toString();
        transmitterData.save();
        if (JoH.areWeRunningOnAndroidWear()) {
            saveWearBattery();
        }
        return transmitterData;
    }

    private static void saveWearBattery() {

        int wearBatteryLevel = CheckBridgeBattery.getBatteryLevel(Home.getAppContext());
        Log.i(TAG, "create wearBatteryLevel=" + wearBatteryLevel);
        Pref.setInt("bridge_battery", wearBatteryLevel);//TODO confirm wear battery should be used as bridge
        CheckBridgeBattery.checkBridgeBattery();

    }

    public static synchronized TransmitterData create(int raw_data ,int sensor_battery_level, long timestamp) {
        TransmitterData lastTransmitterData = TransmitterData.last();
        if (lastTransmitterData != null && lastTransmitterData.raw_data == raw_data && Math.abs(lastTransmitterData.timestamp - new Date().getTime()) < (Constants.MINUTE_IN_MS * 2)) { //Stop allowing duplicate data, its bad!
            return null;
        }

        TransmitterData transmitterData = new TransmitterData();
        transmitterData.sensor_battery_level = sensor_battery_level;
        transmitterData.raw_data = raw_data ;
        transmitterData.timestamp = timestamp;
        transmitterData.uuid = UUID.randomUUID().toString();
        transmitterData.save();
        return transmitterData;
    }

    public static TransmitterData last() {
        return new Select()
                .from(TransmitterData.class)
                .orderBy("_ID desc")
                .executeSingle();
    }


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

    public static List<TransmitterData> latestForGraphAsc(int number, long startTime, long endTime) {//KS
        return new Select()
                .from(TransmitterData.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 List<TransmitterData> last(int count) {
        return new Select()
                .from(TransmitterData.class)
                .orderBy("_ID desc")
                .limit(count)
                .execute();
    }

    public static TransmitterData lastByTimestamp() {
        return new Select()
                .from(TransmitterData.class)
                .orderBy("timestamp desc")
                .executeSingle();
    }

    public static TransmitterData getForTimestamp(double timestamp) {//KS
        try {
            Sensor sensor = Sensor.currentSensor();
            if (sensor != null) {
                TransmitterData bgReading = new Select()
                        .from(TransmitterData.class)
                        .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;
                }
            }
        } catch (Exception e) {
            Log.e(TAG,"getForTimestamp() Got exception on Select : "+e.toString());
            return null;
        }
        Log.d(TAG, "getForTimestamp: No luck finding a BG timestamp match");
        return null;
    }

    public static void cleanup(long timestamp) {
        List<TransmitterData> transmitterData = new Select()
                .from(TransmitterData.class)
                .where("timestamp < ?", timestamp)
                .orderBy("timestamp desc")
                .execute();
        if (transmitterData != null) Log.d(TAG, "cleanup TransmitterData size=" + transmitterData.size());
        new Cleanup().execute(transmitterData);

    }

    public static TransmitterData findByUuid(String uuid) {//KS
        try {
            return new Select()
                    .from(TransmitterData.class)
                    .where("uuid = ?", uuid)
                    .executeSingle();
        } catch (Exception e) {
            Log.e(TAG,"findByUuid() Got exception on Select : "+e.toString());
            return null;
        }
    }

    public String toS() {//KS
        Gson gson = new GsonBuilder()
                .excludeFieldsWithoutExposeAnnotation()
                .registerTypeAdapter(Date.class, new DateTypeAdapter())
                .serializeSpecialFloatingPointValues()
                .create();

        return gson.toJson(this);
    }

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


    public static void updateTransmitterBatteryFromSync(final int battery_level) {
        try {
            TransmitterData td = TransmitterData.last();
            if ((td == null) || (td.raw_data!=0))
            {
                td=TransmitterData.create(0,battery_level,(long)JoH.ts());
                Log.d(TAG,"Created new fake transmitter data record for battery sync");
                if (td==null) return;
            }
            if ((battery_level != td.sensor_battery_level) || ((JoH.ts()-td.timestamp)>(1000*60*60))) {
                td.sensor_battery_level = battery_level;
                td.timestamp = (long)JoH.ts(); // freshen timestamp on this bogus record for system status
                Log.d(TAG,"Saving synced sensor battery, new level: "+battery_level);
                td.save();
            } else {
                Log.d(TAG,"Synced sensor battery level same as existing: "+battery_level);
            }
        } catch (Exception e) {
            Log.e(TAG,"Got exception updating sensor battery from sync: "+e.toString());
        }
    }

    private static double roundRaw(TransmitterData td) {
        return JoH.roundDouble(td.raw_data,3);
    }
    private static double roundFiltered(TransmitterData td) {
        return JoH.roundDouble(td.filtered_data,3);
    }

    public static boolean unchangedRaw() {
        final List<TransmitterData> items = last(3);
        if (items != null && items.size() == 3) {
            return (roundRaw(items.get(0)) == roundRaw(items.get(1))
                    && roundRaw(items.get(0)) == roundRaw(items.get(2))
                    && roundFiltered(items.get(0)) == roundFiltered(items.get(1))
                    && roundFiltered(items.get(0)) == roundFiltered(items.get(2)));
        }
        return false;
    }

}