/*  Copyright (C) 2019-2024 Andreas Shimokawa, Arjan Schrijver, Carsten
    Pfeiffer, Daniel Dakhno, Dmitriy Bogdanov, Taavi Eomäe

    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.qhybrid;

import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;

import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.PackageConfigHelper;
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NavigationInfoSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapterFactory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.file.FileHandle;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.DownloadFileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.MoveHandsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;

public class QHybridSupport extends QHybridBaseSupport {
    public static final String QHYBRID_COMMAND_CONTROL = "qhybrid_command_control";
    public static final String QHYBRID_COMMAND_UNCONTROL = "qhybrid_command_uncontrol";
    public static final String QHYBRID_COMMAND_SET = "qhybrid_command_set";
    public static final String QHYBRID_COMMAND_MOVE = "qhybrid_command_move";
    public static final String QHYBRID_COMMAND_SAVE_CALIBRATION = "qhybrid_command_save_calibration";
    public static final String QHYBRID_COMMAND_VIBRATE = "qhybrid_command_vibrate";
    public static final String QHYBRID_COMMAND_UPDATE = "qhybrid_command_update";
    public static final String QHYBRID_COMMAND_UPDATE_TIMEZONE = "qhybrid_command_update_timezone";
    public static final String QHYBRID_COMMAND_NOTIFICATION = "qhybrid_command_notification";
    public static final String QHYBRID_COMMAND_UPDATE_SETTINGS = "nodomain.freeyourgadget.gadgetbridge.Q_UPDATE_SETTINGS";
    public static final String QHYBRID_COMMAND_OVERWRITE_BUTTONS = "nodomain.freeyourgadget.gadgetbridge.Q_OVERWRITE_BUTTONS";
    public static final String QHYBRID_COMMAND_UPDATE_WIDGETS = "nodomain.freeyourgadget.gadgetbridge.Q_UPDATE_WIDGETS";
    public static final String QHYBRID_COMMAND_SET_MENU_MESSAGE = "nodomain.freeyourgadget.gadgetbridge.Q_SET_MENU_MESSAGE";
    public static final String QHYBRID_COMMAND_SEND_MENU_ITEMS = "nodomain.freeyourgadget.gadgetbridge.Q_SEND_MENU_ITEMS";
    public static final String QHYBRID_COMMAND_SET_WIDGET_CONTENT = "nodomain.freeyourgadget.gadgetbridge.Q_SET_WIDGET_CONTENT";
    public static final String QHYBRID_COMMAND_SET_BACKGROUND_IMAGE = "nodomain.freeyourgadget.gadgetbridge.Q_SET_BACKGROUND_IMAGE";
    public static final String QHYBRID_COMMAND_UNINSTALL_APP = "nodomain.freeyourgadget.gadgetbridge.Q_UNINSTALL_APP";
    public static final String QHYBRID_COMMAND_PUSH_CONFIG = "nodomain.freeyourgadget.gadgetbridge.Q_PUSH_CONFIG";
    public static final String QHYBRID_COMMAND_SWITCH_WATCHFACE = "nodomain.freeyourgadget.gadgetbridge.Q_SWITCH_WATCHFACE";

    public static final String QHYBRID_COMMAND_DOWNLOAD_FILE = "nodomain.freeyourgadget.gadgetbridge.Q_DOWNLOAD_FILE";
    public static final String QHYBRID_COMMAND_UPLOAD_FILE = "nodomain.freeyourgadget.gadgetbridge.Q_UPLOAD_FILE";

    public static final String QHYBRID_COMMAND_SET_MENU_STRUCTURE = "nodomain.freeyourgadget.gadgetbridge.Q_SET_MENU_STRUCTURE";

    public static final String QHYBRID_ACTION_DOWNLOADED_FILE = "nodomain.freeyourgadget.gadgetbridge.Q_DOWNLOADED_FILE";
    public static final String QHYBRID_ACTION_UPLOADED_FILE = "nodomain.freeyourgadget.gadgetbridge.Q_UPLOADED_FILE";

    private static final String QHYBRID_ACTION_SET_ACTIVITY_HAND = "nodomain.freeyourgadget.gadgetbridge.Q_SET_ACTIVITY_HAND";

    public static final String QHYBRID_EVENT_SETTINGS_UPDATED = "nodomain.freeyourgadget.gadgetbridge.Q_SETTINGS_UPDATED";
    public static final String QHYBRID_EVENT_FILE_UPLOADED = "nodomain.freeyourgadget.gadgetbridge.Q_FILE_UPLOADED";
    public static final String QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED = "nodomain.freeyourgadget.gadgetbridge.Q_NOTIFICATION_CONFIG_CHANGED";

    public static final String QHYBRID_EVENT_BUTTON_PRESS = "nodomain.freeyourgadget.gadgetbridge.Q_BUTTON_PRESSED";
    public static final String QHYBRID_EVENT_MULTI_BUTTON_PRESS = "nodomain.freeyourgadget.gadgetbridge.Q_MULTI_BUTTON_PRESSED";
    public static final String QHYBRID_EVENT_COMMUTE_MENU = "nodomain.freeyourgadget.gadgetbridge.Q_COMMUTE_MENU";

    public static final String ITEM_STEP_GOAL = "STEP_GOAL";
    public static final String ITEM_STEP_COUNT = "STEP_COUNT";
    public static final String ITEM_VIBRATION_STRENGTH = "VIBRATION_STRENGTH";
    public static final String ITEM_ACTIVITY_POINT = "ACTIVITY_POINT";
    public static final String ITEM_EXTENDED_VIBRATION_SUPPORT = "EXTENDED_VIBRATION";
    public static final String ITEM_HAS_ACTIVITY_HAND = "HAS_ACTIVITY_HAND";
    public static final String ITEM_USE_ACTIVITY_HAND = "USE_ACTIVITY_HAND";
    public static final String ITEM_LAST_HEARTBEAT = "LAST_HEARTBEAT";
    public static final String ITEM_TIMEZONE_OFFSET = "TIMEZONE_OFFSET_COUNT";
    public static final String ITEM_HEART_RATE_MEASUREMENT_MODE = "HEART_RATE_MEASUREMENT_MODE";

    private static final Logger logger = LoggerFactory.getLogger(QHybridSupport.class);
    private final BroadcastReceiver commandReceiver;
    private final BroadcastReceiver globalCommandReceiver;

    private final PackageConfigHelper helper;

    public volatile boolean searchDevice = false;

    private long timeOffset;

    private boolean useActivityHand;

    private WatchAdapter watchAdapter;

    public QHybridSupport() {
        super(logger);
        addSupportedService(UUID.fromString("108b5094-4c03-e51c-555e-105d1a1155f0"));
        addSupportedService(UUID.fromString("3dda0001-957f-7d4a-34a6-74696673696d"));
        addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
        addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
        addSupportedService(GattService.UUID_SERVICE_HEART_RATE);
        addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE);
        IntentFilter commandFilter = new IntentFilter(QHYBRID_COMMAND_CONTROL);
        commandFilter.addAction(QHYBRID_COMMAND_UNCONTROL);
        commandFilter.addAction(QHYBRID_COMMAND_SET);
        commandFilter.addAction(QHYBRID_COMMAND_MOVE);
        commandFilter.addAction(QHYBRID_COMMAND_SAVE_CALIBRATION);
        commandFilter.addAction(QHYBRID_COMMAND_VIBRATE);
        commandFilter.addAction(QHYBRID_COMMAND_UPDATE);
        commandFilter.addAction(QHYBRID_COMMAND_UPDATE_TIMEZONE);
        commandFilter.addAction(QHYBRID_COMMAND_NOTIFICATION);
        commandFilter.addAction(QHYBRID_COMMAND_UPDATE_SETTINGS);
        commandFilter.addAction(QHYBRID_COMMAND_OVERWRITE_BUTTONS);
        commandFilter.addAction(QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED);
        commandFilter.addAction(QHYBRID_COMMAND_UPDATE_WIDGETS);
        commandFilter.addAction(QHYBRID_COMMAND_SEND_MENU_ITEMS);
        commandFilter.addAction(QHYBRID_COMMAND_SET_BACKGROUND_IMAGE);
        commandFilter.addAction(QHYBRID_COMMAND_UNINSTALL_APP);
        commandFilter.addAction(QHYBRID_COMMAND_UPLOAD_FILE);
        commandFilter.addAction(QHYBRID_COMMAND_DOWNLOAD_FILE);
        commandReceiver = new BroadcastReceiver() {

            @Override
            public void onReceive(Context context, Intent intent) {
                Bundle extras = intent.getExtras();
                NotificationConfiguration config = extras == null ? null : (NotificationConfiguration) intent.getExtras().get("CONFIG");
                if (intent.getAction() == null) {
                    return;
                }

                switch (intent.getAction()) {
                    case QHYBRID_COMMAND_CONTROL: {
                        log("sending control request");
                        watchAdapter.requestHandsControl();
                        MoveHandsRequest.MovementConfiguration movement = new MoveHandsRequest.MovementConfiguration(false);
                        if (config != null) {
                            if(config.getHour() != -1) movement.setHourDegrees(config.getHour());
                            if(config.getMin() != -1) movement.setHourDegrees(config.getMin());
                            if(config.getSubEye() != -1) movement.setHourDegrees(config.getSubEye());
                            watchAdapter.setHands(movement);
                        } else {
                            movement.setHourDegrees(0);
                            movement.setMinuteDegrees(0);
                            movement.setSubDegrees(0);
                            watchAdapter.setHands(movement);
                        }
                        break;
                    }
                    case QHYBRID_COMMAND_UNCONTROL: {
                        watchAdapter.releaseHandsControl();
                        break;
                    }
                    case QHYBRID_COMMAND_SET: {
                        if(config == null) break;
                        MoveHandsRequest.MovementConfiguration movement = new MoveHandsRequest.MovementConfiguration(false);
                        if(config.getHour() != -1) movement.setHourDegrees(config.getHour());
                        if(config.getMin() != -1) movement.setHourDegrees(config.getMin());
                        if(config.getSubEye() != -1) movement.setHourDegrees(config.getSubEye());
                        watchAdapter.setHands(movement);
                        break;
                    }
                    case QHYBRID_COMMAND_MOVE: {
                        MoveHandsRequest.MovementConfiguration movement = new MoveHandsRequest.MovementConfiguration(true);
                        if(extras.containsKey("EXTRA_DISTANCE_HOUR")) movement.setHourDegrees(extras.getShort("EXTRA_DISTANCE_HOUR"));
                        if(extras.containsKey("EXTRA_DISTANCE_MINUTE")) movement.setMinuteDegrees(extras.getShort("EXTRA_DISTANCE_MINUTE"));
                        if(extras.containsKey("EXTRA_DISTANCE_SUB")) movement.setSubDegrees(extras.getShort("EXTRA_DISTANCE_SUB"));
                        watchAdapter.setHands(movement);
                        break;
                    }
                    case QHYBRID_COMMAND_SAVE_CALIBRATION: {
                        watchAdapter.saveCalibration();
                        break;
                    }
                    case QHYBRID_COMMAND_VIBRATE: {
                        if (config != null) {
                            watchAdapter.vibrate(config.getVibration());
                        }
                        break;
                    }
                    case QHYBRID_COMMAND_NOTIFICATION: {
                        if (config != null) {
                            watchAdapter.playNotification(config);
                        }
                        break;
                    }
                    case QHYBRID_COMMAND_UPDATE: {
                        loadTimeOffset();
                        onSetTime();
                        break;
                    }
                    case QHYBRID_COMMAND_UPDATE_TIMEZONE:{
                        loadTimezoneOffset();
                        break;
                    }
                    case QHYBRID_COMMAND_UPDATE_SETTINGS: {
                        String newSetting = intent.getStringExtra("EXTRA_SETTING");
                        switch (newSetting) {
                            case ITEM_VIBRATION_STRENGTH: {
                                watchAdapter.setVibrationStrength(Short.parseShort(gbDevice.getDeviceInfo(ITEM_VIBRATION_STRENGTH).getDetails()));
                                break;
                            }
                            case ITEM_STEP_GOAL: {
                                watchAdapter.setStepGoal(Integer.parseInt(gbDevice.getDeviceInfo(ITEM_STEP_GOAL).getDetails()));
                                break;
                            }
                            case ITEM_USE_ACTIVITY_HAND: {
                                QHybridSupport.this.useActivityHand = gbDevice.getDeviceInfo(ITEM_USE_ACTIVITY_HAND).getDetails().equals("true");
                                GBApplication.getPrefs().getPreferences().edit().putBoolean("QHYBRID_USE_ACTIVITY_HAND", useActivityHand).apply();
                                break;
                            }
                        }

                        LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(QHYBRID_EVENT_SETTINGS_UPDATED));
                        break;
                    }
                    case QHYBRID_COMMAND_OVERWRITE_BUTTONS: {
                        String buttonConfig = intent.getStringExtra(FossilWatchAdapter.ITEM_BUTTONS);
                        watchAdapter.overwriteButtons(buttonConfig);
                        break;
                    }
                    case QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED: {
                        watchAdapter.syncNotificationSettings();
                        break;
                    }
                    case QHYBRID_COMMAND_UPDATE_WIDGETS: {
                        watchAdapter.updateWidgets();
                        break;
                    }
                    case QHYBRID_COMMAND_SET_BACKGROUND_IMAGE:{
                        byte[] pixels = intent.getByteArrayExtra("EXTRA_PIXELS_ENCODED");
                        watchAdapter.setBackgroundImage(pixels);
                        break;
                    }
                    case QHYBRID_COMMAND_DOWNLOAD_FILE:{
                        byte majorHandle = intent.getByteExtra("EXTRA_MAJORHANDLE", (byte) 0x00);
                        byte minorHandle = intent.getByteExtra("EXTRA_MINORHANDLE", (byte) 0x00);
                        String filename = intent.getStringExtra("EXTRA_NAME");
                        watchAdapter.downloadFile(majorHandle, minorHandle, filename, intent.getBooleanExtra("EXTRA_ENCRYPTED", false), false);
                        break;
                    }
                    case QHYBRID_COMMAND_UPLOAD_FILE:{
                        handleFileUploadIntent(intent);
                        break;
                    }
                    case QHYBRID_COMMAND_UNINSTALL_APP:{
                        watchAdapter.uninstallApp(intent.getStringExtra("EXTRA_APP_NAME"));
                        break;
                    }
                }
            }
        };
        LocalBroadcastManager.getInstance(GBApplication.getContext()).registerReceiver(commandReceiver, commandFilter);

        helper = new PackageConfigHelper(GBApplication.getContext());

        IntentFilter globalFilter = new IntentFilter();
        globalFilter.addAction(QHYBRID_ACTION_SET_ACTIVITY_HAND);
        globalFilter.addAction(QHYBRID_COMMAND_SET_MENU_MESSAGE);
        globalFilter.addAction(QHYBRID_COMMAND_SET_WIDGET_CONTENT);
        globalFilter.addAction(QHYBRID_COMMAND_UPLOAD_FILE);
        globalFilter.addAction(QHYBRID_COMMAND_PUSH_CONFIG);
        globalFilter.addAction(QHYBRID_COMMAND_SWITCH_WATCHFACE);
        globalFilter.addAction(QHYBRID_COMMAND_SET_MENU_STRUCTURE);
        globalCommandReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (watchAdapter == null) {
                    return;
                }

                switch (intent.getAction()) {
                    case QHYBRID_ACTION_SET_ACTIVITY_HAND: {
                        try {
                            String extra = String.valueOf(intent.getExtras().get("EXTRA_PROGRESS"));
                            float progress = Float.parseFloat(extra);
                            watchAdapter.setActivityHand(progress);

                            watchAdapter.playNotification(new NotificationConfiguration(
                                    (short) -1,
                                    (short) -1,
                                    (short) (progress * 180),
                                    PlayNotificationRequest.VibrationType.NO_VIBE
                            ));
                        } catch (Exception e) {
                            GB.log("wrong number format", GB.ERROR, e);
                            logger.debug("trash extra should be number 0.0-1.0");
                        }
                        break;
                    }
                    case QHYBRID_COMMAND_SET_MENU_MESSAGE: {
                        String message = String.valueOf(intent.getExtras().get("EXTRA_MESSAGE"));
                        boolean finished = Boolean.valueOf(String.valueOf(intent.getExtras().get("EXTRA_FINISHED")));

                        watchAdapter.setCommuteMenuMessage(message, finished);

                        break;
                    }
                    case QHYBRID_COMMAND_SET_WIDGET_CONTENT: {
                        HashMap<String, String> widgetValues = new HashMap<>();

                        for(String key : intent.getExtras().keySet()){
                            if(key.matches("^EXTRA_WIDGET_ID_.*$")){
                                widgetValues.put(key.substring(16), String.valueOf(intent.getExtras().get(key)));
                            }
                        }
                        boolean render = intent.getBooleanExtra("EXTRA_RENDER", true);
                        if(widgetValues.size() > 0){
                            Iterator<String> valuesIterator = widgetValues.keySet().iterator();
                            valuesIterator.next();

                            while(valuesIterator.hasNext()){
                                String id = valuesIterator.next();
                                watchAdapter.setWidgetContent(id, widgetValues.get(id), false);
                            }

                            valuesIterator = widgetValues.keySet().iterator();
                            String id = valuesIterator.next();
                            watchAdapter.setWidgetContent(id, widgetValues.get(id), render);
                        }else {
                            String id = String.valueOf(intent.getExtras().get("EXTRA_WIDGET_ID"));
                            String content = String.valueOf(intent.getExtras().get("EXTRA_CONTENT"));
                            watchAdapter.setWidgetContent(id, content, render);
                        }
                        break;
                    }
                    case QHYBRID_COMMAND_UPLOAD_FILE:{
                        if(!dangerousIntentsAllowed()) break;
                        handleFileUploadIntent(intent);
                        break;
                    }
                    case QHYBRID_COMMAND_PUSH_CONFIG:{
                        handleConfigSetIntent(intent);
                        break;
                    }
                    case QHYBRID_COMMAND_SWITCH_WATCHFACE:{
                        handleSwitchWatchfaceIntent(intent);
                        break;
                    }
                    case QHYBRID_COMMAND_SET_MENU_STRUCTURE:{
                        handleSetMenuStructure(intent);
                        break;
                    }
                }
            }
        };
        ContextCompat.registerReceiver(GBApplication.getContext(), globalCommandReceiver, globalFilter, ContextCompat.RECEIVER_EXPORTED);
    }

    private void handleConfigSetIntent(Intent intent) {
        String configJson = intent.getExtras().getString("EXTRA_CONFIG_JSON", "{}");
        watchAdapter.pushConfigJson(configJson);
    }

    private void handleSwitchWatchfaceIntent(Intent intent) {
        String watchfaceName = intent.getExtras().getString("WATCHFACE_NAME", "");
        if (watchfaceName != "") {
            ((FossilHRWatchAdapter) watchAdapter).activateWatchface(watchfaceName);
        }
    }

    private void handleSetMenuStructure(Intent intent){
        if(intent == null){
            logger.error("intent null");
            return;
        }
        String menuStructureJson = intent.getStringExtra("EXTRA_MENU_STRUCTURE_JSON");
        if(menuStructureJson == null){
            logger.error("Menu structure json null");
            return;
        }
        if(menuStructureJson.isEmpty()){
            logger.error("Menu structure json empty");
            return;
        }
        try {
            JSONObject menuStructure = new JSONObject(menuStructureJson);
            watchAdapter.handleSetMenuStructure(menuStructure);
            GB.toast(getContext().getString(R.string.info_menu_structure_set), Toast.LENGTH_SHORT, GB.INFO);
        } catch (JSONException e) {
            logger.error("Menu structure json empty");
            GB.toast(getContext().getString(R.string.error_invalid_menu_structure), Toast.LENGTH_SHORT, GB.ERROR);
        }
    }

    private boolean dangerousIntentsAllowed(){
        return GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_DANGEROUS_EXTERNAL_INTENTS, true);
    }

    private void handleFileUploadIntent(Intent intent){
        boolean generateHeader = intent.getBooleanExtra("EXTRA_GENERATE_FILE_HEADER", false);
        String filePath = intent.getStringExtra("EXTRA_PATH");
        if(!generateHeader){
            watchAdapter.uploadFileIncludesHeader(filePath);
            return;
        }
        Object handleObject = intent.getSerializableExtra("EXTRA_HANDLE");
        if(handleObject == null)return;
        if(handleObject instanceof String){
            handleObject = FileHandle.fromName((String)handleObject);
        }
        if(!(handleObject instanceof FileHandle)) return;
        FileHandle handle = (FileHandle) handleObject;
        watchAdapter.uploadFileGenerateHeader(handle, filePath, intent.getBooleanExtra("EXTRA_ENCRYPTED", false));
    }

    @Override
    public void onSetCallState(CallSpec callSpec) {
        super.onSetCallState(callSpec);
        watchAdapter.onSetCallState(callSpec);
    }

    @Override
    public void dispose() {
        LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(commandReceiver);
        GBApplication.getContext().unregisterReceiver(globalCommandReceiver);
        if (watchAdapter != null) {
            watchAdapter.dispose();
        }
        super.dispose();
    }

    @Override
    public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
        super.onSetAlarms(alarms);
        if(this.watchAdapter == null){
            GB.toast("watch not connected", Toast.LENGTH_LONG, GB.ERROR);
            return;
        }
        this.watchAdapter.onSetAlarms(alarms);
    }

    @Override
    public void onSendConfiguration(String config) {
        if (watchAdapter != null) {
            watchAdapter.onSendConfiguration(config);
        }
    }
    private void loadTimeOffset() {
        timeOffset = getContext().getSharedPreferences(getContext().getPackageName(), Context.MODE_PRIVATE).getInt("QHYBRID_TIME_OFFSET", 0);
    }

    private void loadTimezoneOffset(){
        short offset = (short) getContext().getSharedPreferences(getContext().getPackageName(), Context.MODE_PRIVATE).getInt("QHYBRID_TIMEZONE_OFFSET", 0);

        this.watchAdapter.setTimezoneOffsetMinutes(offset);
    }

    public long getTimeOffset(){
        return this.timeOffset;
    }

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

        this.useActivityHand = GBApplication.getPrefs().getBoolean("QHYBRID_USE_ACTIVITY_HAND", false);
        getDevice().addDeviceInfo(new GenericItem(ITEM_USE_ACTIVITY_HAND, String.valueOf(this.useActivityHand)));

        if (GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_USE_CUSTOM_DEVICEICON, true)) {
            getDevice().setNotificationIconConnected(R.drawable.ic_notification_qhybrid);
            getDevice().setNotificationIconDisconnected(R.drawable.ic_notification_disconnected_qhybrid);
        }

        for (int i = 2; i <= 7; i++)
            builder.notify(getCharacteristic(UUID.fromString("3dda000" + i + "-957f-7d4a-34a6-74696673696d")), true);

        builder.notify(getCharacteristic(UUID.fromString("010541ae-efe8-11c0-91c0-105d1a1155f0")), true);
        builder.notify(getCharacteristic(UUID.fromString("fef9589f-9c21-4d19-9fc0-105d1a1155f0")), true);
        builder.notify(getCharacteristic(UUID.fromString("842d2791-0d20-4ce4-1ada-105d1a1155f0")), true);

        builder
                .read(getCharacteristic(UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb")))
                .read(getCharacteristic(UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb")))
                .read(getCharacteristic(UUID.fromString("00002a24-0000-1000-8000-00805f9b34fb")))
                // .notify(getCharacteristic(UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb")), true)
        ;


        loadTimeOffset();

        return builder;
    }

    @Override
    public void onSetMusicInfo(MusicSpec musicSpec) {
        super.onSetMusicInfo(musicSpec);

        try {
            watchAdapter.setMusicInfo(musicSpec);
        }catch (Exception e){
            GB.log("setMusicInfo error", GB.ERROR, e);
        }
    }

    @Override
    public void onSetMusicState(MusicStateSpec stateSpec) {
        super.onSetMusicState(stateSpec);

        watchAdapter.setMusicState(stateSpec);
    }

    @Override
    public void onFetchRecordedData(int dataTypes) {
        if ((dataTypes & RecordedDataTypes.TYPE_ACTIVITY) != 0) {
            GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data), "", true, 0, getContext());
            getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_activity_data));
            getDevice().sendDeviceUpdateIntent(getContext());
            this.watchAdapter.onFetchActivityData();
        }
    }

    @Override
    public void onNotification(NotificationSpec notificationSpec) {
        log("notif from " + notificationSpec.sourceAppId + "  " + notificationSpec.sender + "   " + notificationSpec.phoneNumber);
        //new Exception().printStackTrace();

        if(this.watchAdapter instanceof FossilHRWatchAdapter){
            if(((FossilHRWatchAdapter) watchAdapter).playRawNotification(notificationSpec)) return;
        }

        String packageName = notificationSpec.sourceName;

        NotificationConfiguration config = null;
        try {
            config = helper.getNotificationConfiguration(packageName);
        } catch (Exception e) {
            GB.toast("error getting notification configuration", Toast.LENGTH_SHORT, GB.ERROR, e);
        }
        if (config == null) return;

        log("handling notification");

        if (config.getRespectSilentMode()) {
            int mode = ((AudioManager) getContext().getApplicationContext().getSystemService(Context.AUDIO_SERVICE)).getRingerMode();
            if (mode == AudioManager.RINGER_MODE_SILENT) return;
        }

        boolean enforceActivityHandNotification = config.getHour() == -1 && config.getMin() == -1;

        playNotification(config);

        showNotificationsByAllActive(enforceActivityHandNotification);
    }

    private void log(String message){
        logger.debug(message);
    }

    @Override
    public String customStringFilter(String inputString) {
        // Remove Unicode code points that can't be displayed by the watch like emoji skin tones and variation selectors
        // \x{1f3fb}-\x{1f3ff} = Emoji skin tones: https://en.wikipedia.org/wiki/Emoticons_(Unicode_block)
        // \ufe00-\ufe0f = Variation selectors: https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)
        return inputString.replaceAll("[\\x{1f3fb}-\\x{1f3ff}|\\ufe00-\\ufe0f]", "");
    }

    @Override
    public void onDeleteNotification(int id) {
        super.onDeleteNotification(id);

        this.watchAdapter.onDeleteNotification(id);

        showNotificationsByAllActive(true);
    }

    private void showNotificationsByAllActive(boolean enforceByNotification) {
        if (!this.useActivityHand) return;
        double progress = calculateNotificationProgress();
        showNotificationCountOnActivityHand(progress);

        if (enforceByNotification) {
            watchAdapter.playNotification(new NotificationConfiguration(
                    (short) -1,
                    (short) -1,
                    (short) (progress * 180),
                    PlayNotificationRequest.VibrationType.NO_VIBE
            ));
        }
    }

    @Override
    public void onReset(int flags) {
        super.onReset(flags);
        if ((flags & GBDeviceProtocol.RESET_FLAGS_FACTORY_RESET) != 0) {
            this.watchAdapter.factoryReset();
        }
    }

    public double calculateNotificationProgress() {
        HashMap<NotificationConfiguration, Boolean> configs = new HashMap<>(0);
        try {
            for (NotificationConfiguration config : helper.getNotificationConfigurations()) {
                configs.put(config, false);
            }
        } catch (Exception e) {
            GB.toast("error getting notification configs", Toast.LENGTH_SHORT, GB.ERROR, e);
        }

        double notificationProgress = 0;

        for (String notificationPackage : NotificationListener.notificationStack) {
            for (NotificationConfiguration notificationConfiguration : configs.keySet()) {
                if (configs.get(notificationConfiguration)) continue;
                if (notificationConfiguration.getPackageName().equals(notificationPackage)) {
                    notificationProgress += 0.25;
                    configs.put(notificationConfiguration, true);
                }
            }
        }

        return notificationProgress;
    }

    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
        watchAdapter.onConnectionStateChange(gatt, status, newState);
    }

    //TODO toggle "Notifications when screen on" options on this check
    private void showNotificationCountOnActivityHand(double progress) {
        if (useActivityHand) {
            watchAdapter.setActivityHand(progress);
        }
    }

    @Override
    public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
        super.onMtuChanged(gatt, mtu, status);
        if(watchAdapter == null) return;
        watchAdapter.onMtuChanged(gatt, mtu, status);
    }

    private void playNotification(NotificationConfiguration config) {
        if (config.getMin() == -1 && config.getHour() == -1 && config.getVibration() == PlayNotificationRequest.VibrationType.NO_VIBE)
            return;
        watchAdapter.playNotification(config);
    }

    @Override
    public void onSetTime() {
        watchAdapter.setTime();
    }

    @Override
    public void onFindDevice(boolean start) {
        watchAdapter.onFindDevice(start);
    }

    @Override
    public void onSendWeather(ArrayList<WeatherSpec> weatherSpecs) {
        watchAdapter.onSendWeather(weatherSpecs.get(0));
    }

    @Override
    public void onTestNewFunction() {
        watchAdapter.onTestNewFunction();
    }

    @Override
    public void onInstallApp(Uri uri) {
        watchAdapter.onInstallApp(uri);
    }

    private void backupFile(DownloadFileRequest request) {
        try {
            File file = FileUtils.getExternalFile("qFiles/" + request.timeStamp);
            if (file.exists()) {
                throw new Exception("file " + file.getPath() + " exists");
            }
            logger.debug("Writing file " + file.getPath());
            try (FileOutputStream fos = new FileOutputStream(file)) {
                fos.write(request.file);
            }
            logger.debug("file written.");

            file = FileUtils.getExternalFile("qFiles/steps");
            try (FileOutputStream fos = new FileOutputStream(file, true)) {
                fos.write(("file " + request.timeStamp + " cut\n\n").getBytes());
            }

            //TODO file stuff
            // queueWrite(new EraseFileRequest((short) request.fileHandle));
        } catch (Exception e) {
            GB.log("error", GB.ERROR, e);
            if (request.fileHandle > 257) {
                // queueWrite(new DownloadFileRequest((short) (request.fileHandle - 1)));
            }
        }
    }

    @Override
    public void handleGBDeviceEvent(GBDeviceEventBatteryInfo deviceEvent){
        super.handleGBDeviceEvent(deviceEvent);
    }

    @Override
    public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        watchAdapter.onCharacteristicWrite(gatt, characteristic, status);
        return super.onCharacteristicWrite(gatt, characteristic, status);
    }

    @Override
    public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        switch (characteristic.getUuid().toString()) {
            case "00002a26-0000-1000-8000-00805f9b34fb": {
                String firmwareVersion = characteristic.getStringValue(0);
                gbDevice.setFirmwareVersion(firmwareVersion);

                Matcher matcher = Pattern
                        .compile("(?<=[A-Z]{2}[0-9]\\.[0-9]\\.)[0-9]+\\.[0-9]+")
                        .matcher(firmwareVersion);
                if(matcher.find()){
                    gbDevice.setFirmwareVersion2(matcher.group());
                }

                this.watchAdapter = new WatchAdapterFactory().createWatchAdapter(firmwareVersion, this);
                this.watchAdapter.initialize();
                showNotificationsByAllActive(false);
                break;
            }
            case "00002a24-0000-1000-8000-00805f9b34fb": {
                String modelNumber = characteristic.getStringValue(0);
                gbDevice.setModel(modelNumber);
                gbDevice.setName(watchAdapter.getModelName());
                try {
                    gbDevice.addDeviceInfo(new GenericItem(ITEM_EXTENDED_VIBRATION_SUPPORT, String.valueOf(watchAdapter.supportsExtendedVibration())));
                    gbDevice.addDeviceInfo(new GenericItem(ITEM_HAS_ACTIVITY_HAND, String.valueOf(watchAdapter.supportsActivityHand())));
                } catch (UnsupportedOperationException e) {
                    GB.log("error", GB.ERROR, e);
                    gbDevice.addDeviceInfo(new GenericItem(ITEM_EXTENDED_VIBRATION_SUPPORT, "false"));
                }
                break;
            }
            case "00002a19-0000-1000-8000-00805f9b34fb": {
                short level = characteristic.getValue()[0];
                gbDevice.setBatteryLevel(level);

                GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
                batteryInfo.level = gbDevice.getBatteryLevel();
                batteryInfo.state = BatteryState.BATTERY_NORMAL;
                handleGBDeviceEvent(batteryInfo);
                break;

            }
        }

        return true;
    }

    @Override
    public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic
            characteristic) {
        if(watchAdapter == null) return super.onCharacteristicChanged(gatt, characteristic);
        return watchAdapter.onCharacteristicChanged(gatt, characteristic);
    }

    @Override
    public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
        if(this.watchAdapter instanceof FossilHRWatchAdapter){
            ((FossilHRWatchAdapter) watchAdapter).setQuickRepliesConfiguration();
        }
    }

    @Override
    public void onAppInfoReq() {
        if(this.watchAdapter instanceof FossilHRWatchAdapter){
            ((FossilHRWatchAdapter) watchAdapter).listApplications();
        }
    }

    @Override
    public void onAppStart(UUID uuid, boolean start) {
        if(this.watchAdapter instanceof FossilHRWatchAdapter) {
            String appName = ((FossilHRWatchAdapter) watchAdapter).getInstalledAppNameFromUUID(uuid);
            if ((appName != null) && (appName.endsWith("App"))) {
                ((FossilHRWatchAdapter) watchAdapter).startAppOnWatch(appName);
            } else {
                ((FossilHRWatchAdapter) watchAdapter).activateWatchface(appName);
            }
        }
    }

    @Override
    public void onAppDownload(UUID uuid) {
        ((FossilHRWatchAdapter) watchAdapter).downloadAppToCache(uuid);
    }

    @Override
    public void onAppDelete(UUID uuid) {
        if(this.watchAdapter instanceof FossilHRWatchAdapter) {
            String appName = ((FossilHRWatchAdapter) watchAdapter).getInstalledAppNameFromUUID(uuid);
            if (appName != null) {
                watchAdapter.uninstallApp(appName);
            }
        }
    }

    @Override
    public void onSetNavigationInfo(NavigationInfoSpec navigationInfoSpec) {
        ((FossilHRWatchAdapter) watchAdapter).onSetNavigationInfo(navigationInfoSpec);
    }
}
