package nodomain.freeyourgadget.gadgetbridge.service.devices.cycling_sensor.support;

import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;

import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.db.CyclingSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.NotifyAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ReadAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;

public class CyclingSensorSupport extends CyclingSensorBaseSupport {
    static class CyclingSpeedCadenceMeasurement {
        private static final int FLAG_REVOLUTION_DATA_PRESENT = 1 << 0;
        private static final int FLAG_CADENCE_DATA_PRESENT    = 1 << 1;
        public boolean revolutionDataPresent = false;
        public boolean cadenceDataPresent = false;
        public int revolutionCount;
        public int lastRevolutionTimeTicks;
        private int crankRevolutionCount;
        private int lastCrankRevolutionTimeTicks;

        public static CyclingSpeedCadenceMeasurement fromPayload(byte[] payload) throws RuntimeException {
            if(payload.length < 7){
                throw new RuntimeException("wrong payload length");
            }

            ByteBuffer buffer = ByteBuffer
                    .wrap(payload)
                    .order(ByteOrder.LITTLE_ENDIAN);

            byte flags = buffer.get();

            boolean revolutionDataPresent = (flags | FLAG_REVOLUTION_DATA_PRESENT) == FLAG_REVOLUTION_DATA_PRESENT;
            boolean cadenceDataPresent = (flags | FLAG_CADENCE_DATA_PRESENT) == FLAG_CADENCE_DATA_PRESENT;
            CyclingSpeedCadenceMeasurement result = new CyclingSpeedCadenceMeasurement();

            if(revolutionDataPresent){
                result.revolutionDataPresent = true;
                result.revolutionCount = buffer.getInt() & 0xFFFFFFFF; // remove sign
                result.lastRevolutionTimeTicks = buffer.getShort() & 0xFFFF;
            }

            if(cadenceDataPresent){
                result.cadenceDataPresent = true;
                result.crankRevolutionCount = buffer.getInt();
                result.lastCrankRevolutionTimeTicks = buffer.getShort();
            }

            return result;
        }

        @NonNull
        @Override
        public String toString() {
            return String.format("Measurement revolutions: %d, time ticks %d", revolutionCount, lastRevolutionTimeTicks);
        }
    }

    public final static UUID UUID_CYCLING_SENSOR_SERVICE =
            UUID.fromString("00001816-0000-1000-8000-00805f9b34fb");
    public final static UUID UUID_CYCLING_SENSOR_CSC_MEASUREMENT =
            UUID.fromString("00002a5b-0000-1000-8000-00805f9b34fb");

    private static final Logger logger = LoggerFactory.getLogger(CyclingSensorSupport.class);

    private long persistenceInterval;
    private long nextPersistenceTimestamp = 0;

    private float wheelCircumferenceMeters;

    private CyclingSpeedCadenceMeasurement lastReportedMeasurement = null;
    private long lastMeasurementTime = 0;

    private BluetoothGattCharacteristic batteryCharacteristic = null;

    public CyclingSensorSupport() {
        super(logger);

        addSupportedService(UUID_CYCLING_SENSOR_SERVICE);
        addSupportedService(BatteryInfoProfile.SERVICE_UUID);
    }

    @Override
    public void onSendConfiguration(String config) {
        switch (config){
            case DeviceSettingsPreferenceConst.PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL:
            case DeviceSettingsPreferenceConst.PREF_CYCLING_SENSOR_WHEEL_DIAMETER:
                loadConfiguration();
                break;
        }
    }

    private void loadConfiguration(){
        Prefs deviceSpecificPrefs = new Prefs(
                GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress())
        );
        persistenceInterval = deviceSpecificPrefs.getInt(DeviceSettingsPreferenceConst.PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL, 60) * 1000;
        nextPersistenceTimestamp = 0;
        
        float wheelDiameter = deviceSpecificPrefs.getFloat(DeviceSettingsPreferenceConst.PREF_CYCLING_SENSOR_WHEEL_DIAMETER, 29);
        wheelCircumferenceMeters = (float)(wheelDiameter * 2.54 * Math.PI) / 100;
    }

    @Override
    protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
        builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));

        BluetoothGattCharacteristic measurementCharacteristic =
                getCharacteristic(UUID_CYCLING_SENSOR_CSC_MEASUREMENT);

        builder.add(new NotifyAction(measurementCharacteristic, true));

        builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
        batteryCharacteristic = getCharacteristic(BatteryInfoProfile.UUID_CHARACTERISTIC_BATTERY_LEVEL);

        if(batteryCharacteristic != null){
            builder.add(new ReadAction(batteryCharacteristic));
        }

        loadConfiguration();

        gbDevice.setFirmwareVersion("1.0.0");

        return builder;
    }

    private void handleMeasurementCharacteristic(BluetoothGattCharacteristic characteristic){
        byte[] value = characteristic.getValue();
        if(value == null || value.length < 7){
            logger.error("Measurement characteristic value length smaller than 7");
            return;
        }

        CyclingSpeedCadenceMeasurement measurement = null;

        try {
            measurement = CyclingSpeedCadenceMeasurement.fromPayload(value);
        }catch (RuntimeException e){
            // do nothing, measurement stays null
        }

        if(measurement == null){
            return;
        }

        if(!measurement.revolutionDataPresent){
            return;
        }

        handleCyclingSpeedMeasurement(measurement);
    }

    private void handleCyclingSpeedMeasurement(CyclingSpeedCadenceMeasurement currentMeasurement) {
        logger.debug("Measurement " + currentMeasurement);

        long now = System.currentTimeMillis();

        Float speed = null;

        long lastMeasurementDelta = (now - lastMeasurementTime);

        if(lastMeasurementDelta <= 30_000){
            int ticksPassed = currentMeasurement.lastRevolutionTimeTicks - lastReportedMeasurement.lastRevolutionTimeTicks;
            // every second is subdivided in 1024 ticks
            int millisDelta = (int)(ticksPassed * (1000f / 1024f));

            if(millisDelta > 0) {
                int revolutionsDelta = currentMeasurement.revolutionCount - lastReportedMeasurement.revolutionCount;

                float revolutionsPerSecond = revolutionsDelta * (1000f / millisDelta);

                speed = revolutionsPerSecond * wheelCircumferenceMeters;
            }
        }

        lastReportedMeasurement = currentMeasurement;
        lastMeasurementTime = now;

        CyclingSample sample = new CyclingSample();

        if (currentMeasurement.revolutionDataPresent) {
            sample.setRevolutionCount(currentMeasurement.revolutionCount);
            sample.setSpeed(speed);
            sample.setDistance(currentMeasurement.revolutionCount * wheelCircumferenceMeters);
        }

        sample.setTimestamp(now);

        Intent liveIntent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES);
        liveIntent.putExtra(GBDevice.EXTRA_DEVICE, getDevice());
        liveIntent.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
        liveIntent.putExtra("EXTRA_DEVICE_ADDRESS", getDevice().getAddress());
        LocalBroadcastManager.getInstance(getContext())
                .sendBroadcast(liveIntent);


        if(now < nextPersistenceTimestamp){
            // too early
            return;
        }

        nextPersistenceTimestamp = now + persistenceInterval;

        try(DBHandler handler = GBApplication.acquireDB()) {
            DaoSession session = handler.getDaoSession();

            CyclingSampleProvider sampleProvider =
                    new CyclingSampleProvider(getDevice(), session);

            Device databaseDevice = DBHelper.getDevice(getDevice(), session);
            User databaseUser = DBHelper.getUser(session);
            sample.setDevice(databaseDevice);
            sample.setUser(databaseUser);

            sampleProvider.addSample(sample);
        } catch (Exception e) {
            // throw new RuntimeException(e);
            logger.error("failed adding DB cycling sample");
        }
    }

    @Override
    public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        byte[] value = characteristic.getValue();

        if(characteristic.equals(batteryCharacteristic) && value != null && value.length == 1){
            GBDeviceEventBatteryInfo info = new GBDeviceEventBatteryInfo();
            info.level = characteristic.getValue()[0];
            handleGBDeviceEvent(info);
        }

        return true;
    }

    @Override
    public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        if(characteristic.getUuid().equals(UUID_CYCLING_SENSOR_CSC_MEASUREMENT)){
            handleMeasurementCharacteristic(characteristic);
            return true;
        }
        return false;
    }
}
