/*  Copyright (C) 2023-2024 akasaka / Genjitsu Labs

    This file is part of Gadgetbridge.

    Gadgetbridge is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Gadgetbridge is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.text.TextUtils;
import android.widget.Toast;

import androidx.annotation.Nullable;

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

import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3ActivitySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3BehaviorSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3CaloriesSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Constants;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3EnergySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3HeartRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3SettingKeys;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3StressSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Vo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.database.AppSpecificNotificationSettingsRepository;
import nodomain.freeyourgadget.gadgetbridge.entities.Wena3BehaviorSample;
import nodomain.freeyourgadget.gadgetbridge.entities.Wena3CaloriesSample;
import nodomain.freeyourgadget.gadgetbridge.entities.Wena3EnergySample;
import nodomain.freeyourgadget.gadgetbridge.entities.Wena3HeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.entities.Wena3ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.AppSpecificNotificationSetting;
import nodomain.freeyourgadget.gadgetbridge.entities.Wena3StressSample;
import nodomain.freeyourgadget.gadgetbridge.entities.Wena3Vo2Sample;
import nodomain.freeyourgadget.gadgetbridge.externalevents.CalendarReceiver;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.logic.ActivitySyncPacketProcessor;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.logic.parsers.BehaviorPacketParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.logic.parsers.CaloriesPacketParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.logic.parsers.EnergyPacketParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.logic.parsers.HeartRatePacketParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.logic.parsers.StepsPacketParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.logic.parsers.StressPacketParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.logic.parsers.Vo2MaxPacketParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.activity.ActivitySyncDataPacket;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.activity.ActivitySyncStartPacket;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.activity.ActivitySyncTimePacketTypeA;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.activity.ActivitySyncTimePacketTypeB;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.calendar.CalendarEntry;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.notification.NotificationArrival;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.notification.NotificationRemoval;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.notification.defines.LedColor;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.notification.defines.NotificationFlags;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.notification.defines.NotificationKind;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.notification.defines.VibrationKind;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.notification.defines.VibrationOptions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.AlarmListSettings;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.AutoPowerOffSettings;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.BodyPropertiesSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.CalendarNotificationEnableSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.CameraAppTypeSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.DayStartHourSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.DeviceButtonActionSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.DisplaySetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.DoNotDisturbSettings;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.GoalStepsSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.HomeIconOrderSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.MenuIconSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.SingleAlarmSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.StatusPageOrderSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.TimeSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.TimeZoneSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.VibrationSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.defines.DeviceButtonActionId;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.defines.DisplayDesign;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.defines.DisplayOrientation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.defines.FontSize;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.defines.GenderSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.defines.HomeIconId;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.defines.Language;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.defines.MenuIconId;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.defines.StatusPageId;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.settings.defines.VibrationStrength;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.status.BatteryLevelInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.status.DeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.status.MusicInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.status.NotificationServiceStatusRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.status.StatusRequestType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.status.Weather;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.status.WeatherDay;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.status.WeatherReport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;

public class SonyWena3DeviceSupport extends AbstractBTLEDeviceSupport {
    private static final int INCOMING_CALL_ID = 3939;
    private static final Logger LOG = LoggerFactory.getLogger(SonyWena3DeviceSupport.class);
    private String lastMusicInfo = null;
    private MusicStateSpec lastMusicState = null;
    private final List<CalendarEventSpec> calendarEvents = new ArrayList<>();
    private final ActivitySyncPacketProcessor activitySyncHandler = new ActivitySyncPacketProcessor();
    private AppSpecificNotificationSettingsRepository perAppNotificationSettingsRepository = null;

    public SonyWena3DeviceSupport() {
        super(LoggerFactory.getLogger(SonyWena3DeviceSupport.class));
        addSupportedService(SonyWena3Constants.COMMON_SERVICE_UUID);
        addSupportedService(SonyWena3Constants.NOTIFICATION_SERVICE_UUID);
        addSupportedService(SonyWena3Constants.ACTIVITY_LOG_SERVICE_UUID);

        activitySyncHandler.registerParser(new StepsPacketParser());
        activitySyncHandler.registerParser(new HeartRatePacketParser());
        activitySyncHandler.registerParser(new StressPacketParser());
        activitySyncHandler.registerParser(new EnergyPacketParser());
        activitySyncHandler.registerParser(new BehaviorPacketParser());
        activitySyncHandler.registerParser(new CaloriesPacketParser());
        activitySyncHandler.registerParser(new Vo2MaxPacketParser());
    }
    @Override
    public boolean useAutoConnect() {
        return true;
    }

    @Override
    protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
        builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
        if(perAppNotificationSettingsRepository == null) {
            perAppNotificationSettingsRepository = new AppSpecificNotificationSettingsRepository(getDevice());
        }
        getDevice().setFirmwareVersion("...");
        getDevice().setFirmwareVersion2("...");

        sendAllSettings(builder);
        sendAllCalendarEvents(builder);

        // Get battery state
        builder.read(getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_STATE_UUID));

        // Subscribe to updates
        builder.notify(getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_STATE_UUID), true);
        builder.notify(getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID), true);
        builder.notify(getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_INFO_UUID), true);
        builder.notify(getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_MODE_UUID), true);
        builder.notify(getCharacteristic(SonyWena3Constants.NOTIFICATION_SERVICE_CHARACTERISTIC_UUID), true);
        builder.notify(getCharacteristic(SonyWena3Constants.ACTIVITY_LOG_CHARACTERISTIC_UUID), true);

        // Get serial number and firmware version
        builder.read(getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_INFO_UUID));

        // Finally, sync activity data
        requestActivityDataDownload(builder, false);

        builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
        CalendarReceiver.forceSync();
        return builder;
    }

    @Override
    public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        if(characteristic.getUuid().equals(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_STATE_UUID)) {
            BatteryLevelInfo stateInfo = new BatteryLevelInfo(characteristic.getValue());
            handleGBDeviceEvent(stateInfo.toDeviceEvent());
            return true;
        }
        else if (characteristic.getUuid().equals(SonyWena3Constants.NOTIFICATION_SERVICE_CHARACTERISTIC_UUID)) {
            NotificationServiceStatusRequest request = new NotificationServiceStatusRequest(characteristic.getValue());
            if(request.requestType == StatusRequestType.MUSIC_INFO_FETCH.value) {
                LOG.debug("Request for music info received");
                if(lastMusicState != null && lastMusicState.state == MusicStateSpec.STATE_PLAYING && lastMusicInfo != null) {
                    sendMusicInfo(lastMusicInfo);
                }
                return true;
            }
            else if(request.requestType == StatusRequestType.LOCATE_PHONE.value) {
                LOG.debug("Request for find phone received");
                GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
                findPhoneEvent.event = GBDeviceEventFindPhone.Event.START;
                evaluateGBDeviceEvent(findPhoneEvent);
                return true;
            }
            else if(request.requestType == StatusRequestType.BACKGROUND_SYNC_REQUEST.value) {
                Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
                boolean enableSync = prefs.getBoolean(SonyWena3SettingKeys.BACKGROUND_SYNC, true);
                if(enableSync) {
                    LOG.info("Request for background activity sync received");
                    requestActivityDataDownload(null, false);
                }
                return true;
            }
            else if(request.requestType == StatusRequestType.GET_CALENDAR.value) {
                CalendarReceiver.forceSync();
                sendAllCalendarEvents(null);
            }
            else {
                LOG.warn("Unknown NotificationServiceStatusRequest " + request.requestType);
            }
        } else if(characteristic.getUuid().equals(SonyWena3Constants.ACTIVITY_LOG_CHARACTERISTIC_UUID)) {
            ActivitySyncDataPacket asdp = new ActivitySyncDataPacket(characteristic.getValue());
            activitySyncHandler.receivePacket(asdp, getDevice());
        }
        return false;
    }

    @Override
    public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        if(characteristic.getUuid().equals(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_STATE_UUID)) {
            BatteryLevelInfo stateInfo = new BatteryLevelInfo(characteristic.getValue());
            handleGBDeviceEvent(stateInfo.toDeviceEvent());
            return true;
        } else if(characteristic.getUuid().equals(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_INFO_UUID)) {
            DeviceInfo deviceInfo = new DeviceInfo(characteristic.getValue());
            handleGBDeviceEvent(deviceInfo.toDeviceEvent());
            return true;
        }
        return false;
    }

    @Override
    public boolean onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
        return super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
    }


    private void sendCurrentTime(@Nullable TransactionBuilder b) {
        try {
            TransactionBuilder builder = b == null ? performInitialized("updateDateTime") : b;

            TimeZone tz = TimeZone.getDefault();
            Date currentTime = new Date();

            builder.write(
                    getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                    new TimeSetting(currentTime).toByteArray()
            );

            builder.write(
                    getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                    new TimeZoneSetting(tz, currentTime).toByteArray()
            );

            if(b == null) performImmediately(builder);
        } catch (IOException e) {
            LOG.warn("Unable to send current time", e);
        }
    }

    private void sendMusicInfo(@Nullable String musicInfo) {
        try {
            TransactionBuilder builder = performInitialized("updateMusic");

            builder.write(
                    getCharacteristic(SonyWena3Constants.NOTIFICATION_SERVICE_CHARACTERISTIC_UUID),
                    new MusicInfo(musicInfo != null ? musicInfo: "").toByteArray()
            );

            performImmediately(builder);
        } catch (IOException e) {
            LOG.warn("Unable to send music info", e);
        }
    }

    @Override
    public void onFetchRecordedData(int dataTypes) {
        requestActivityDataDownload(null, false);
    }

    private void requestActivityDataDownload(@Nullable TransactionBuilder b, boolean syncAll) {
        try {
            TransactionBuilder builder = b == null ? performInitialized("activitySync") : b;
            BluetoothGattCharacteristic sportsCharacteristic = getCharacteristic(SonyWena3Constants.ACTIVITY_LOG_CHARACTERISTIC_UUID);

            Date stepLastSyncTime = null;
            Date heartLastSyncTime = null;
            Date behaviorLastSyncTime = null;
            Date vo2LastSyncTime = null;
            Date stressLastSyncTime = null;
            Date energyLastSyncTime = null;
            Date caloriesLastSyncTime = null;
            Date eventsLastSyncTime = null; // TODO: find out what this is

            if(!syncAll) {
                try (DBHandler db = GBApplication.acquireDB()) {
                    Wena3HeartRateSample heartSample = new SonyWena3HeartRateSampleProvider(getDevice(), db.getDaoSession()).getLatestSample();
                    if(heartSample != null) {
                        heartLastSyncTime = new Date(heartSample.getTimestamp());
                    }

                    Wena3StressSample stressSample = new SonyWena3StressSampleProvider(getDevice(), db.getDaoSession()).getLatestSample();
                    if(stressSample != null) {
                        stressLastSyncTime = new Date(stressSample.getTimestamp());
                    }

                    Wena3ActivitySample stepsSample = new SonyWena3ActivitySampleProvider(getDevice(), db.getDaoSession()).getLatestActivitySample();
                    if(stepsSample != null) {
                        stepLastSyncTime = new Date(stepsSample.getTimestamp() * 1000L);
                    }

                    Wena3BehaviorSample behaviorSample = new SonyWena3BehaviorSampleProvider(getDevice(), db.getDaoSession()).getLatestSample();
                    if(behaviorSample != null) {
                        behaviorLastSyncTime = new Date(behaviorSample.getTimestamp());
                    }

                    Wena3Vo2Sample vo2Sample = new SonyWena3Vo2SampleProvider(getDevice(), db.getDaoSession()).getLatestSample();
                    if(vo2Sample != null) {
                        vo2LastSyncTime = new Date(vo2Sample.getTimestamp());
                    }

                    Wena3EnergySample energySample = new SonyWena3EnergySampleProvider(getDevice(), db.getDaoSession()).getLatestSample();
                    if(energySample != null) {
                        energyLastSyncTime = new Date(energySample.getTimestamp());
                    }

                    Wena3CaloriesSample caloriesSample = new SonyWena3CaloriesSampleProvider(getDevice(), db.getDaoSession()).getLatestSample();
                    if(caloriesSample != null) {
                        caloriesLastSyncTime = new Date(caloriesSample.getTimestamp());
                    }

                    LOG.info(
                            "Found last sync dates: " +
                            "\nsteps = " + stepLastSyncTime +
                            "\nheart = " + heartLastSyncTime +
                            "\nbehavior = " + behaviorLastSyncTime +
                            "\nvo2 = " + vo2LastSyncTime +
                            "\nstress = " + stressLastSyncTime +
                            "\nenergy = " + energyLastSyncTime +
                            "\ncalories = " + caloriesLastSyncTime +
                            "\nevents = " + eventsLastSyncTime
                    );
                } catch (Exception e) {
                    LOG.warn("Failed to communicate with DB to find last sync timestamps -- syncing everything:", e);
                }
            }

            builder.write(
                    sportsCharacteristic,
                    new ActivitySyncTimePacketTypeA(stepLastSyncTime, heartLastSyncTime, behaviorLastSyncTime, vo2LastSyncTime).toByteArray()
            );

            builder.write(
                    sportsCharacteristic,
                    new ActivitySyncTimePacketTypeB(stressLastSyncTime, energyLastSyncTime, caloriesLastSyncTime, eventsLastSyncTime).toByteArray()
            );

            builder.write(sportsCharacteristic, new ActivitySyncStartPacket().toByteArray());

            if(b == null) performImmediately(builder);
        } catch (IOException e) {
            LOG.warn("Unable to force request a sync", e);
        }
    }

    private void sendWeatherInfo(WeatherReport weather, @Nullable TransactionBuilder b) {
        try {
            TransactionBuilder builder = b == null ? performInitialized("updateWeather") : b;

            builder.write(
                    getCharacteristic(SonyWena3Constants.NOTIFICATION_SERVICE_CHARACTERISTIC_UUID),
                    weather.toByteArray()
            );

            if(b == null) performImmediately(builder);
        } catch (IOException e) {
            LOG.warn("Unable to send current weather", e);
        }
    }

    @Override
    public void onSetMusicInfo(MusicSpec musicSpec) {
        StringBuilder sb = new StringBuilder();
        boolean hasTrackName = musicSpec.track != null && musicSpec.track.trim().length() > 0;
        boolean hasArtistName = musicSpec.artist != null && musicSpec.artist.trim().length() > 0;

        if(hasTrackName) {
            sb.append(musicSpec.track.trim());
        }
        if(hasArtistName && hasTrackName) {
            sb.append(" / ");
        }
        if(hasArtistName) {
            sb.append(musicSpec.artist.trim());
        }

        lastMusicInfo = sb.toString();
        sendMusicInfo(lastMusicInfo);
    }

    @Override
    public void onSetMusicState(MusicStateSpec stateSpec) {
        if(stateSpec.state == MusicStateSpec.STATE_PLAYING && lastMusicInfo != null) {
            sendMusicInfo(lastMusicInfo);
        } else if (stateSpec.state == MusicStateSpec.STATE_STOPPED || stateSpec.state == MusicStateSpec.STATE_PAUSED) {
            sendMusicInfo("");
        }
        lastMusicState = stateSpec;
    }

    @Override
    public void onSetCallState(CallSpec callSpec) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        boolean enableNotifications = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE, false);
        boolean enableCalls = prefs.getBoolean(SonyWena3SettingKeys.RECEIVE_CALLS, true);
        if(!enableCalls || !enableNotifications) {
            LOG.info("Calls are disabled, ignoring");
            return;
        }

        try {
            TransactionBuilder builder = performInitialized("sendCall");

            if(callSpec.command == CallSpec.CALL_INCOMING) {
                LedColor led = LedColor.valueOf(prefs.getString(SonyWena3SettingKeys.DEFAULT_CALL_LED_COLOR, LedColor.WHITE.name()).toUpperCase());
                VibrationKind vibra = VibrationKind.valueOf(prefs.getString(SonyWena3SettingKeys.DEFAULT_CALL_VIBRATION_PATTERN, VibrationKind.CONTINUOUS.name()).toUpperCase());
                boolean vibraContinuous = false;
                int vibraRepeats = prefs.getInt(SonyWena3SettingKeys.DEFAULT_CALL_VIBRATION_REPETITION, 0);
                if(vibraRepeats == 0) {
                    vibraContinuous = true;
                }

                builder.write(
                        getCharacteristic(SonyWena3Constants.NOTIFICATION_SERVICE_CHARACTERISTIC_UUID),
                        new NotificationArrival(
                                NotificationKind.CALL,
                                INCOMING_CALL_ID,
                                callSpec.number,
                                callSpec.name,
                                "",
                                new Date(),
                                new VibrationOptions(vibra, vibraRepeats, vibraContinuous),
                                led,
                                NotificationFlags.NONE
                        ).toByteArray()
                );
            } else {
                builder.write(
                        getCharacteristic(SonyWena3Constants.NOTIFICATION_SERVICE_CHARACTERISTIC_UUID),
                        new NotificationRemoval(NotificationKind.CALL, INCOMING_CALL_ID).toByteArray()
                );
            }

            performImmediately(builder);
        } catch (IOException e) {
            LOG.warn("Unable to send call", e);
        }
    }

    @Override
    public void onNotification(NotificationSpec notificationSpec) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        boolean enableNotifications = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE, false);
        if(!enableNotifications) return;

        try {
            TransactionBuilder builder = performInitialized("sendNotify");

            StringBuilder bodyBuilder = new StringBuilder();

            if(notificationSpec.sender != null && notificationSpec.sender.length() > 0) {
                bodyBuilder.append(notificationSpec.sender);
                bodyBuilder.append(":");
            }

            if(notificationSpec.title != null && notificationSpec.title.length() > 0) {
                if(bodyBuilder.length() > 0) {
                    bodyBuilder.append("\n");
                }
                bodyBuilder.append(notificationSpec.title);
                bodyBuilder.append(":");
            }

            if(notificationSpec.subject != null && notificationSpec.subject.length() > 0) {
                if(bodyBuilder.length() > 0) {
                    bodyBuilder.append("\n");
                }
                bodyBuilder.append("- ");
                bodyBuilder.append(notificationSpec.subject);
            }

            if(notificationSpec.body != null) {
                if(bodyBuilder.length() > 0) {
                    bodyBuilder.append("\n");
                }
                bodyBuilder.append(notificationSpec.body);
            }

            String actionLabel = notificationSpec.attachedActions.isEmpty() ? "" :
                    notificationSpec.attachedActions.get(0).title;

            boolean hasAction = !notificationSpec.attachedActions.isEmpty();

            NotificationFlags flags = NotificationFlags.NONE;
            // TODO: Figure out how actions work

            LedColor led = LedColor.valueOf(prefs.getString(SonyWena3SettingKeys.DEFAULT_LED_COLOR, LedColor.BLUE.name()).toUpperCase());
            VibrationKind vibra = VibrationKind.valueOf(prefs.getString(SonyWena3SettingKeys.DEFAULT_VIBRATION_PATTERN, VibrationKind.BASIC.name()).toUpperCase());
            boolean vibraContinuous = false;
            int vibraRepeats = prefs.getInt(SonyWena3SettingKeys.DEFAULT_VIBRATION_REPETITION, 1);

            if(notificationSpec.sourceAppId != null) {
                AppSpecificNotificationSetting appSpecificSetting = perAppNotificationSettingsRepository.getSettingsForAppId(notificationSpec.sourceAppId);
                if(appSpecificSetting != null) {
                    if(appSpecificSetting.getLedPattern() != null) {
                        led = LedColor.valueOf(appSpecificSetting.getLedPattern().toUpperCase());
                    }

                    if(appSpecificSetting.getVibrationPattern() != null) {
                        vibra = VibrationKind.valueOf(appSpecificSetting.getVibrationPattern().toUpperCase());
                    }

                    if(appSpecificSetting.getVibrationRepetition() != null) {
                        vibraRepeats = Integer.valueOf(appSpecificSetting.getVibrationRepetition());
                    }
                }
            }

            if(vibraRepeats == 0) {
                vibraContinuous = true;
            }

            builder.write(
                    getCharacteristic(SonyWena3Constants.NOTIFICATION_SERVICE_CHARACTERISTIC_UUID),
                    new NotificationArrival(
                            NotificationKind.APP,
                            notificationSpec.getId(),
                            notificationSpec.sourceName,
                            bodyBuilder.toString(),
                            actionLabel,
                            new Date(notificationSpec.when),
                            new VibrationOptions(vibra, vibraRepeats, vibraContinuous),
                            led,
                            flags
                    ).toByteArray()
            );

            performImmediately(builder);
        } catch (IOException e) {
            LOG.warn("Unable to send notification", e);
        }
    }

    @Override
    public void onDeleteNotification(int id) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        boolean enableNotifications = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE, false);
        if(!enableNotifications) return;

        try {
            TransactionBuilder builder = performInitialized("delNotify");

            builder.write(
                    getCharacteristic(SonyWena3Constants.NOTIFICATION_SERVICE_CHARACTERISTIC_UUID),
                    new NotificationRemoval(NotificationKind.APP, id).toByteArray()
            );

            performImmediately(builder);
        } catch (IOException e) {
            LOG.warn("Unable to send notification", e);
        }
    }

    @Override
    public void onSendWeather(ArrayList<WeatherSpec> weatherSpecs) {
        WeatherSpec weatherSpec = weatherSpecs.get(0);
        if(weatherSpec.forecasts.size() < 4) return;

        ArrayList<WeatherDay> days = new ArrayList<>();
        // Add today
        days.add(
                new WeatherDay(
                        Weather.fromOpenWeatherMap(weatherSpec.currentConditionCode),
                        Weather.fromOpenWeatherMap(weatherSpec.currentConditionCode),
                        weatherSpec.todayMaxTemp,
                        weatherSpec.todayMinTemp
                )
        );

        // Add other days
        for(int i = 0; i < 4; i++) {
            days.add(WeatherDay.fromSpec(weatherSpec.forecasts.get(i)));
        }

        WeatherReport report = new WeatherReport(days);
        sendWeatherInfo(report, null);
    }

    @Override
    public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
        try {
            Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
            TransactionBuilder builder = performInitialized("alarmSetting");

            assert alarms.size() <= SonyWena3Constants.ALARM_SLOTS;

            int wakeupMargin = prefs.getInt(SonyWena3SettingKeys.SMART_WAKEUP_MARGIN_MINUTES,
                    SonyWena3Constants.ALARM_DEFAULT_SMART_WAKEUP_MARGIN_MINUTES);

            for(
                int i = 0;
                i < SonyWena3Constants.ALARM_SLOTS;
                i += AlarmListSettings.MAX_ALARMS_IN_PACKET
            ) {
                AlarmListSettings pkt = new AlarmListSettings(new ArrayList<>(), i);

                for(int j = 0; j < AlarmListSettings.MAX_ALARMS_IN_PACKET; j++) {
                    if(i + j < alarms.size()) {
                        Alarm alarm = alarms.get(i + j);
                        SingleAlarmSetting sas = new SingleAlarmSetting(
                                alarm.getEnabled(),
                                (byte) alarm.getRepetition(),
                                alarm.getSmartWakeup() ? wakeupMargin : 0,
                                alarm.getHour(),
                                alarm.getMinute()
                        );
                        pkt.alarms.add(sas);
                    }
                }

                builder.write(
                        getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                        pkt.toByteArray()
                );
            }

            performImmediately(builder);
        } catch (IOException e) {
            LOG.warn("Unable to send alarms", e);
            GB.toast("Failed to save alarms", Toast.LENGTH_SHORT, GB.ERROR);
        }
    }

    private void sendDisplaySettings(TransactionBuilder b) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));

        String localeString = prefs.getString(DeviceSettingsPreferenceConst.PREF_LANGUAGE, DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO);
        if (localeString == null || localeString.equals(DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO)) {
            String language = Locale.getDefault().getLanguage();
            String country = Locale.getDefault().getCountry();

            if (country == null) {
                country = language;
            }
            localeString = language + "_" + country.toUpperCase();
        }
        LOG.info("Setting device to locale: " + localeString);

    Language languageCode = Language.ENGLISH;

        switch (localeString.substring(0, 2)) {
            case "en":
                languageCode = Language.ENGLISH;
                break;
            case "ja":
                languageCode = Language.JAPANESE;
                break;
        }
        LOG.info("Resolved locale: " + languageCode.name());

        DisplaySetting pkt = new DisplaySetting(
                prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SCREEN_LIFT_WRIST, false),
                languageCode,
                prefs.getInt(DeviceSettingsPreferenceConst.PREF_SCREEN_TIMEOUT, 5),
                (prefs.getString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, "left")
                        .equals("left") ? DisplayOrientation.LEFT_HAND : DisplayOrientation.RIGHT_HAND),
                (prefs.getBoolean(SonyWena3SettingKeys.RICH_DESIGN_MODE, false) ? DisplayDesign.RICH : DisplayDesign.NORMAL),
                (prefs.getBoolean(SonyWena3SettingKeys.LARGE_FONT_SIZE, false) ? FontSize.LARGE : FontSize.NORMAL),
                prefs.getBoolean(SonyWena3SettingKeys.WEATHER_IN_STATUSBAR, true)
        );

        b.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                pkt.toByteArray()
        );
    }

    private void sendDnDSettings(TransactionBuilder b) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        String dndMode = prefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO, DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_OFF);
        boolean isDndOn = (dndMode != null && dndMode.equals(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_SCHEDULED));
        String start = prefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_START, "22:00");
        String end = prefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_END, "06:00");

        Calendar startCalendar = GregorianCalendar.getInstance();
        Calendar endCalendar = GregorianCalendar.getInstance();
        DateFormat df = new SimpleDateFormat("HH:mm");

        try {
            startCalendar.setTime(df.parse(start));
            endCalendar.setTime(df.parse(end));
        } catch (ParseException e) {
            LOG.error("settings error: " + e);
        }

        byte startH = (byte)startCalendar.get(Calendar.HOUR_OF_DAY);
        byte startM = (byte)startCalendar.get(Calendar.MINUTE);
        byte endH = (byte)endCalendar.get(Calendar.HOUR_OF_DAY);
        byte endM = (byte)endCalendar.get(Calendar.MINUTE);

        DoNotDisturbSettings dndPkt = new DoNotDisturbSettings(isDndOn, startH, startM, endH, endM);
        b.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                dndPkt.toByteArray()
        );
    }

    private void sendAutoPowerSettings(TransactionBuilder b) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        String autoPowerMode = prefs.getString(SonyWena3SettingKeys.AUTO_POWER_SCHEDULE_KIND, DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_OFF);
        boolean isAutoPowerOffEnabled = (autoPowerMode != null && autoPowerMode.equals(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_SCHEDULED));
        String start = prefs.getString(SonyWena3SettingKeys.AUTO_POWER_SCHEDULE_START_HHMM, "22:00");
        String end = prefs.getString(SonyWena3SettingKeys.AUTO_POWER_SCHEDULE_END_HHMM, "06:00");

        Calendar startCalendar = GregorianCalendar.getInstance();
        Calendar endCalendar = GregorianCalendar.getInstance();
        DateFormat df = new SimpleDateFormat("HH:mm");

        try {
            startCalendar.setTime(df.parse(start));
            endCalendar.setTime(df.parse(end));
        } catch (ParseException e) {
            LOG.error("settings error: " + e);
        }

        byte startH = (byte)startCalendar.get(Calendar.HOUR_OF_DAY);
        byte startM = (byte)startCalendar.get(Calendar.MINUTE);
        byte endH = (byte)endCalendar.get(Calendar.HOUR_OF_DAY);
        byte endM = (byte)endCalendar.get(Calendar.MINUTE);

        AutoPowerOffSettings powerOffPkt = new AutoPowerOffSettings(isAutoPowerOffEnabled, startH, startM, endH, endM);
        b.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                powerOffPkt.toByteArray()
        );
    }

    private void sendVibrationSettings(TransactionBuilder b) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        boolean smartVibration = prefs.getBoolean(SonyWena3SettingKeys.SMART_VIBRATION, true);
        VibrationStrength strength = VibrationStrength.valueOf(prefs.getString(SonyWena3SettingKeys.VIBRATION_STRENGTH, VibrationStrength.NORMAL.name()).toUpperCase());
        VibrationSetting pkt = new VibrationSetting(smartVibration, strength);

        b.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                pkt.toByteArray()
        );
    }

    private void sendHomeScreenSettings(TransactionBuilder b) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        String leftIdName = prefs.getString(SonyWena3SettingKeys.LEFT_HOME_ICON, HomeIconId.MUSIC.name()).toUpperCase();
        String centerIdName = prefs.getString(SonyWena3SettingKeys.CENTER_HOME_ICON, HomeIconId.PEDOMETER.name()).toUpperCase();
        String rightIdName = prefs.getString(SonyWena3SettingKeys.RIGHT_HOME_ICON, HomeIconId.CALORIES.name()).toUpperCase();

        b.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                new HomeIconOrderSetting(
                        HomeIconId.valueOf(leftIdName),
                        HomeIconId.valueOf(centerIdName),
                        HomeIconId.valueOf(rightIdName)
                ).toByteArray()
        );
    }

    private void sendMenuSettings(TransactionBuilder b) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        String[] csv = prefs.getString(SonyWena3SettingKeys.MENU_ICON_CSV_KEY,
                        TextUtils.join(",", getContext().getResources().getStringArray(R.array.prefs_wena3_menu_icons_default_list)))
                .toUpperCase()
                .split(",");

        MenuIconSetting menu = new MenuIconSetting();

        for(String iconIdName: csv) {
            if(!iconIdName.equals(MenuIconId.NONE.name())) {
                menu.iconList.add(MenuIconId.valueOf(iconIdName));
            }
        }

        b.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                menu.toByteArray()
        );
    }

    private void sendStatusPageSettings(TransactionBuilder b) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        StatusPageOrderSetting pageOrderSetting = new StatusPageOrderSetting();
        String[] csv = prefs.getString(SonyWena3SettingKeys.STATUS_PAGE_CSV_KEY,
                        TextUtils.join(",", getContext().getResources().getStringArray(R.array.prefs_wena3_status_page_default_list)))
                .toUpperCase()
                .split(",");
        for(String idName: csv) {
            if(!idName.equals(StatusPageId.NONE.name())) {
                pageOrderSetting.pages.add(StatusPageId.valueOf(idName));
            }
        }

        b.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                pageOrderSetting.toByteArray()
        );
    }

    private void sendActivityGoalSettings(TransactionBuilder b) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        ActivityUser user = new ActivityUser();
        if(user.getYearOfBirth() < 1920) {
            LOG.error("Device does not support this year of birth");
            return;
        }
        boolean stepsNotification = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_USER_FITNESS_GOAL_NOTIFICATION, false);

        GenderSetting gender = user.getGender() == ActivityUser.GENDER_FEMALE ? GenderSetting.FEMALE : GenderSetting.MALE;

        // Maybe we need to set the full birth date?
        BodyPropertiesSetting bodyPropertiesSetting = new BodyPropertiesSetting(gender, (short)user.getYearOfBirth(), (short)0, (short)1, (short)user.getHeightCm(), (short)user.getWeightKg());
        GoalStepsSetting stepsSetting = new GoalStepsSetting(stepsNotification, user.getStepsGoal());

        b.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                bodyPropertiesSetting.toByteArray()
        );
        b.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                stepsSetting.toByteArray()
        );
    }

    private void sendDayStartHour(TransactionBuilder b) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        int hour = prefs.getInt(SonyWena3SettingKeys.DAY_START_HOUR, 6);
        DayStartHourSetting setting = new DayStartHourSetting(hour);

        b.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                setting.toByteArray()
        );
    }

    private void sendButtonActions(TransactionBuilder b) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        String doubleIdName = prefs.getString(SonyWena3SettingKeys.BUTTON_DOUBLE_PRESS_ACTION, DeviceButtonActionId.NONE.name()).toUpperCase();
        String longIdName = prefs.getString(SonyWena3SettingKeys.BUTTON_LONG_PRESS_ACTION, DeviceButtonActionId.NONE.name()).toUpperCase();
        DeviceButtonActionSetting setting = new DeviceButtonActionSetting(
                DeviceButtonActionId.valueOf(longIdName),
                DeviceButtonActionId.valueOf(doubleIdName)
        );

        b.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                setting.toByteArray()
        );
    }

    private void sendCalendarNotificationToggles(TransactionBuilder b) {
        Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
        boolean enableCalendar = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SYNC_CALENDAR, false);
        boolean enableNotifications = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE, false);
        CalendarNotificationEnableSetting setting = new CalendarNotificationEnableSetting(enableCalendar, enableNotifications);

        b.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                setting.toByteArray()
        );
    }

    private void sendAllSettings(TransactionBuilder builder) {
        sendCurrentTime(builder);
        builder.write(
                getCharacteristic(SonyWena3Constants.COMMON_SERVICE_CHARACTERISTIC_CONTROL_UUID),
                CameraAppTypeSetting.findOut(getContext().getPackageManager()).toByteArray()
        );
        sendMenuSettings(builder);
        sendStatusPageSettings(builder);
        sendDisplaySettings(builder);
        sendDnDSettings(builder);
        sendAutoPowerSettings(builder);
        sendVibrationSettings(builder);
        sendHomeScreenSettings(builder);
        sendActivityGoalSettings(builder);
        sendDayStartHour(builder);
        sendButtonActions(builder);
        sendCalendarNotificationToggles(builder);
    }

    private void sendAllCalendarEvents(TransactionBuilder b) {
        try {
            Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
            boolean enableCalendar = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SYNC_CALENDAR, false);

            TransactionBuilder builder = b == null ? performInitialized("updateCalendarEvents") : b;

            if(!enableCalendar || calendarEvents.isEmpty()) {
                builder.write(
                        getCharacteristic(SonyWena3Constants.NOTIFICATION_SERVICE_CHARACTERISTIC_UUID),
                        CalendarEntry.byteArrayForEmptyEvent((byte) 0, (byte) 0)
                );
            }
            else {
                int i = 1;
                int total = Math.min(calendarEvents.size(), 255);
                for(CalendarEventSpec evt: calendarEvents) {
                    builder.write(
                            getCharacteristic(SonyWena3Constants.NOTIFICATION_SERVICE_CHARACTERISTIC_UUID),
                            new CalendarEntry(
                                    new Date(evt.timestamp * 1000L),
                                    new Date((evt.timestamp * 1000L) + (evt.durationInSeconds * 1000L)),
                                    evt.allDay,
                                    (evt.title == null ? "" : evt.title),
                                    (evt.location == null ? "" : evt.location),
                                    (byte) i,
                                    (byte) total
                            ).toByteArray()
                    );
                    if(i == 255) break;
                    else i++;
                }
            }

            if(b == null) performImmediately(builder);
        } catch (IOException e) {
            LOG.warn("Unable to send calendar events", e);
        }
    }

    @Override
    public void onSetTime() {
        sendCurrentTime(null);
    }

    @Override
    public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
        calendarEvents.add(calendarEventSpec);
        sendAllCalendarEvents(null);
    }

    @Override
    public void onDeleteCalendarEvent(byte type, long id) {
        for(CalendarEventSpec evt : calendarEvents) {
            if(evt.type == type && evt.id == id) {
                calendarEvents.remove(evt);
            }
        }
        sendAllCalendarEvents(null);
    }

    @Override
    public void onSendConfiguration(String config) {
        try {
            TransactionBuilder builder = performInitialized("sendConfig");
            switch (config) {
                case SonyWena3SettingKeys.STATUS_PAGE_CSV_KEY:
                    sendStatusPageSettings(builder);
                    break;

                case SonyWena3SettingKeys.MENU_ICON_CSV_KEY:
                    sendMenuSettings(builder);
                    break;

                case DeviceSettingsPreferenceConst.PREF_SCREEN_LIFT_WRIST:
                case DeviceSettingsPreferenceConst.PREF_LANGUAGE:
                case DeviceSettingsPreferenceConst.PREF_SCREEN_TIMEOUT:
                case DeviceSettingsPreferenceConst.PREF_WEARLOCATION:
                case SonyWena3SettingKeys.RICH_DESIGN_MODE:
                case SonyWena3SettingKeys.LARGE_FONT_SIZE:
                case SonyWena3SettingKeys.WEATHER_IN_STATUSBAR:
                    sendDisplaySettings(builder);
                    break;

                case SonyWena3SettingKeys.SMART_WAKEUP_MARGIN_MINUTES:
                    // Resend alarms
                    onSetAlarms(new ArrayList<>(DBHelper.getAlarms(gbDevice)));
                    break;

                case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO:
                case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_END:
                case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_START:
                    sendDnDSettings(builder);
                    break;

                case SonyWena3SettingKeys.AUTO_POWER_SCHEDULE_KIND:
                case SonyWena3SettingKeys.AUTO_POWER_SCHEDULE_START_HHMM:
                case SonyWena3SettingKeys.AUTO_POWER_SCHEDULE_END_HHMM:
                    sendAutoPowerSettings(builder);
                    break;

                case SonyWena3SettingKeys.VIBRATION_STRENGTH:
                case SonyWena3SettingKeys.SMART_VIBRATION:
                    sendVibrationSettings(builder);
                    break;

                case SonyWena3SettingKeys.LEFT_HOME_ICON:
                case SonyWena3SettingKeys.CENTER_HOME_ICON:
                case SonyWena3SettingKeys.RIGHT_HOME_ICON:
                    sendHomeScreenSettings(builder);
                    break;

                case ActivityUser.PREF_USER_YEAR_OF_BIRTH:
                case ActivityUser.PREF_USER_GENDER:
                case ActivityUser.PREF_USER_HEIGHT_CM:
                case ActivityUser.PREF_USER_WEIGHT_KG:
                case ActivityUser.PREF_USER_STEPS_GOAL:
                case DeviceSettingsPreferenceConst.PREF_USER_FITNESS_GOAL_NOTIFICATION:
                    sendActivityGoalSettings(builder);
                    break;

                case SonyWena3SettingKeys.DAY_START_HOUR:
                    sendDayStartHour(builder);
                    break;

                case SonyWena3SettingKeys.BUTTON_DOUBLE_PRESS_ACTION:
                case SonyWena3SettingKeys.BUTTON_LONG_PRESS_ACTION:
                    sendButtonActions(builder);
                    break;

                case DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE:
                case DeviceSettingsPreferenceConst.PREF_SYNC_CALENDAR:
                    sendCalendarNotificationToggles(builder);
                    sendAllCalendarEvents(builder);
                    break;

                default:
                    LOG.warn("Unsupported setting %s", config);
                    return;
            }

            performImmediately(builder);
        } catch(Exception e) {
            GB.toast("Failed to send settings update", Toast.LENGTH_SHORT, GB.ERROR);
        }
    }
}
