/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */

#include "gap_le_slave_reconnect.h"

#include "applib/bluetooth/ble_ad_parse.h"

#include "gap_le.h"
#include "gap_le_advert.h"
#include "gap_le_connect.h"

#include "comm/ble/ble_log.h"
#include "comm/bt_lock.h"

#include "kernel/event_loop.h"
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
#include "services/common/regular_timer.h"
#include "util/size.h"

#include <btutil/bt_uuid.h>

//! Reference to the reconnection advertising job.
//! bt_lock() needs to be taken before accessing this variable.
static GAPLEAdvertisingJobRef s_reconnect_advert_job;
static bool s_is_basic_reconnection_enabled;
static bool s_is_hrm_reconnection_enabled;

typedef enum {
  ReconnectType_None,  // Not advertising for reconnection
  ReconnectType_Plain, // Advertising for reconnection with empty payload
  ReconnectType_BleHrm // Advertising for reconnection with HRM payload
} ReconnectType;

// -----------------------------------------------------------------------------
//! Static, internal helper functions
static void prv_advert_job_unscheduled_callback(GAPLEAdvertisingJobRef job,
                                                bool completed,
                                                void *data) {
  // bt_lock() is still held for us by gap_le_advert
  s_reconnect_advert_job = NULL;
}

static bool prv_is_advertising_for_reconnection(void) {
  return (s_reconnect_advert_job != NULL);
}

static ReconnectType prv_current_reconnect_type(void) {
  if (s_is_hrm_reconnection_enabled) {
    return ReconnectType_BleHrm;
  }
  if (s_is_basic_reconnection_enabled) {
    return ReconnectType_Plain;
  }
  return ReconnectType_None;
}

static void prv_unschedule_adv_if_needed(void) {
  if (prv_is_advertising_for_reconnection()) {
    gap_le_advert_unschedule(s_reconnect_advert_job);
  }
}

static void prv_evaluate(ReconnectType prev_type) {
  ReconnectType cur_type = prv_current_reconnect_type();
  if (cur_type == prev_type) {
    PBL_LOG(LOG_LEVEL_DEBUG, "Reconnect type unchanged: %d", cur_type);
    return;
  }

  PBL_LOG(LOG_LEVEL_DEBUG, "Reconnect type changed: %d -> %d", prev_type, cur_type);

  if (cur_type != ReconnectType_None) {
    prv_unschedule_adv_if_needed();

#if CAPABILITY_HAS_BUILTIN_HRM
    const bool use_hrm_payload = (cur_type == ReconnectType_BleHrm);
#else
    const bool use_hrm_payload = false;
#endif

    BLEAdData *ad;
    if (use_hrm_payload) {
      // Create adv payload with only flags + HR service UUID. This is enough for various mobile
      // fitness apps to be able to reconnect to Pebble as BLE HRM.
      ad = ble_ad_create();
      ble_ad_set_flags(ad, GAP_LE_AD_FLAGS_GEN_DISCOVERABLE_MASK);
      Uuid service_uuid = bt_uuid_expand_16bit(0x180D);
      ble_ad_set_service_uuids(ad, &service_uuid, 1);
    } else {
      // [MT] Advertise with an empty payload to save battery life with these
      // reconnection ad packets. This should be enough for the other
      // device to be able to reconnect. With iOS it works, need to test Android.

      // [MT] Note we leave out the Flags AD. According to the spec you have to
      // include flags if any are non-zero. To abide, Pebble ought to always
      // include the SIMULTANEOUS_LE_BR_EDR_TO_SAME_DEVICE_CONTROLLER and
      // SIMULTANEOUS_LE_BR_EDR_TO_SAME_DEVICE_HOST flags. However, we have never
      // done this (ignorance) and gotten by, by using a "random" address (the
      // public address, but then inverted) as a work-around for the problems
      // leaving out these flags caused with Android.
      // I intend to use use the "Peripheral privacy feature" some time in the
      // near future. With this, these flags and the issues on Android become
      // a non-issue (because addresses will be private). Therefore I decided to
      // still leave out the flags.

      static BLEAdData payload = {
        .ad_data_length = 0,
        .scan_resp_data_length = 0,
      };
      ad = &payload;
    }

    // Values chosen according to:
    // "Accessory Design Guidelies for Apple Devices" 55.4 Advertising Data
    const GAPLEAdvertisingJobTerm advert_terms[] = {
        {
            .duration_secs = 30,
            .min_interval_slots = 32, // 20ms
            .max_interval_slots = 32, // 20ms
        },
        {
            .duration_secs = GAPLE_ADVERTISING_DURATION_INFINITE,
            .min_interval_slots = 1636, // 1022.5ms
            .max_interval_slots = 1636, // 1022.5ms
        },
    };

    s_reconnect_advert_job = gap_le_advert_schedule(
        ad, advert_terms, sizeof(advert_terms) / sizeof(GAPLEAdvertisingJobTerm),
        prv_advert_job_unscheduled_callback, NULL, GAPLEAdvertisingJobTagReconnection);

    if (use_hrm_payload) {
      ble_ad_destroy(ad);
    }
  } else {
    prv_unschedule_adv_if_needed();
  }
}

static void prv_set_and_evaluate(bool *val, bool new_value) {
  const ReconnectType prev_type = prv_current_reconnect_type();
  *val = new_value;
  prv_evaluate(prev_type);
}

// -----------------------------------------------------------------------------
void gap_le_slave_reconnect_stop(void) {
  bt_lock();
  {
    prv_set_and_evaluate(&s_is_basic_reconnection_enabled, false);
  }
  bt_unlock();
}

// -----------------------------------------------------------------------------
void gap_le_slave_reconnect_start(void) {
#if RECOVERY_FW
  return; // Only use discoverable packet for PRF
#endif
  bt_lock();
  {
    if (prv_is_advertising_for_reconnection()) {
      PBL_LOG(LOG_LEVEL_DEBUG, "Already advertising for reconnection");
      goto unlock;
    }

    if (gap_le_connect_is_connected_as_slave()) {
      PBL_LOG(LOG_LEVEL_DEBUG, "Already connected as slave");
      goto unlock;
    }

    if (!bt_persistent_storage_has_active_ble_gateway_bonding() &&
        !bt_persistent_storage_has_ble_ancs_bonding()) {
      PBL_LOG(LOG_LEVEL_DEBUG, "No bonded master device");
      goto unlock;
    }

    prv_set_and_evaluate(&s_is_basic_reconnection_enabled, true);
  }
unlock:
  bt_unlock();
}

#if CAPABILITY_HAS_BUILTIN_HRM

#define RECONNECT_HRM_TIMEOUT_SECS (60)

static RegularTimerInfo s_hrm_reconnect_timer;

static void prv_hrm_reconnect_timeout_kernel_main_callback(void *data) {
  gap_le_slave_reconnect_hrm_stop();
}

static void prv_hrm_reconnect_timeout_timer_callback(void *data) {
  launcher_task_add_callback(prv_hrm_reconnect_timeout_kernel_main_callback, NULL);
}

// -----------------------------------------------------------------------------
void gap_le_slave_reconnect_hrm_restart(void) {
  bt_lock();
  {
    prv_set_and_evaluate(&s_is_hrm_reconnection_enabled, true);

    // Always restart the timer:
    if (!regular_timer_is_scheduled(&s_hrm_reconnect_timer)) {
      s_hrm_reconnect_timer = (RegularTimerInfo) {
        .cb = prv_hrm_reconnect_timeout_timer_callback,
      };
      regular_timer_add_multisecond_callback(&s_hrm_reconnect_timer, RECONNECT_HRM_TIMEOUT_SECS);
    }
  }
  bt_unlock();
}

// -----------------------------------------------------------------------------
void gap_le_slave_reconnect_hrm_stop(void) {
  bt_lock();
  {
    prv_set_and_evaluate(&s_is_hrm_reconnection_enabled, false);

    if (regular_timer_is_scheduled(&s_hrm_reconnect_timer)) {
      regular_timer_remove_callback(&s_hrm_reconnect_timer);
    }
  }
  bt_unlock();
}

#endif
