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

#include "wakeup.h"

#include "popups/wakeup_ui.h"

#include "os/mutex.h"
#include "process_management/app_install_manager.h"
#include "process_management/app_manager.h"
#include "process_management/app_storage.h"
#include "services/common/clock.h"
#include "services/common/event_service.h"
#include "services/common/new_timer/new_timer.h"
#include "services/common/system_task.h"
#include "services/normal/settings/settings_file.h"
#include "syscall/syscall.h"
#include "syscall/syscall_internal.h"
#include "system/logging.h"
#include "util/attributes.h"
#include "util/math.h"
#include "util/units.h"

#include "kernel/pbl_malloc.h"

#define SETTINGS_FILE_NAME "wakeup"
// settings file => 29 bytes * 30 apps * 8 wakeup events = ~7000 bytes
// This should be more than enough space to store all the wakeup events we will ever want.
#define SETTINGS_FILE_SIZE KiBYTES(8)
// This represents the size of the buffer that is allocated to pass into the wakeup_ui
// to show that an app's wakeup event had triggered while the watch was off. To reduce
// complexity, I have hard coded this buffer to a max size instead of going the linked_list
// route. 16 apps should be more than enough.
#define NUM_APPS_ALERT_ON_BOOT 16

static PebbleMutex *s_mutex;

//! Settings entries == WakeupId are stored by timestamp,
//! duplicate timestamps not allowed (can't have 2 wakeup events at same time)
//! repeating and repeat_hours_offset were included for future use
//! and use in repeat support for alarms
typedef struct PACKED {
  Uuid uuid; //!< UUID of app that scheduled the wakeup event
  int32_t reason; //!< App provided value to differentiate wakeup event
  bool repeating; //!< Enable event repetition
  uint16_t repeat_hours_offset; //!< repeat hour interval
  bool notify_if_missed; //!< Notify user if wakeup event has been missed
  time_t timestamp; //!< The time at which this entry will wake up at
  bool utc; //!< If timezone has been set, the this is UTC time
} WakeupEntry;

typedef struct PACKED {
  WakeupId current_wakeup_id;
  WakeupId next_wakeup_id;
  time_t timestamp;
} WakeupState;

struct prv_missed_events_s {
  uint8_t missed_apps_count;
  AppInstallId *missed_app_ids;
};

struct prv_check_app_and_wakeup_event_s {
  time_t wakeup_timestamp; //!< Timestamp of the WakupEntry
  int wakeup_count; //!< wakeup event count for app, negative for error (StatusCode)
};

// Local prototypes
static WakeupEntry prv_wakeup_settings_get_entry(WakeupId wakeup_id);
static void prv_wakeup_settings_delete_entry(WakeupId wakeup_id);
static StatusCode prv_wakeup_settings_add_entry(WakeupId wakeup_id, WakeupEntry entry);
static void prv_wakeup_timer_next_pending(void);

static bool s_wakeup_enabled = false;
static TimerID s_current_timer_id = TIMER_INVALID_ID; // single timer reused for each event
// single structure containing the global wakeup state
static WakeupState s_wakeup_state = { -1, -1, 0 };
static bool s_catchup_enabled = false; // enables catching up with missed events

void wakeup_dispatcher_system_task(void *data){
  WakeupId wakeup_id = (WakeupId)data;
  WakeupEntry entry = prv_wakeup_settings_get_entry(wakeup_id);

  // Delete event from settings
  prv_wakeup_settings_delete_entry(wakeup_id);

  AppInstallId app_id = app_install_get_id_for_uuid(&entry.uuid);

  // If specified app isn't currently running, launch
  if (!(app_manager_get_current_app_id() == app_id)) {
    // Lookup app, and if installed, launch
    if (app_id != INSTALL_ID_INVALID) {
      PebbleLaunchAppEventExtended* data =
          kernel_malloc_check(sizeof(PebbleLaunchAppEventExtended));
      *data = (PebbleLaunchAppEventExtended) {
        .common.reason = APP_LAUNCH_WAKEUP,
        .wakeup.wakeup_id = wakeup_id,
        .wakeup.wakeup_reason = entry.reason,
      };
      data->common.args = &data->wakeup;

      PebbleEvent event = {
        .type = PEBBLE_APP_LAUNCH_EVENT,
        .launch_app = {
          .id = app_id,
          .data = data
        }
      };

      event_put(&event);
    }
  } else {
    // If app running, send event
    PebbleEvent event = {
      .type = PEBBLE_WAKEUP_EVENT,
      .wakeup = {
        .wakeup_info = {
          .wakeup_id = wakeup_id,
          .wakeup_reason = entry.reason
        }
      }
    };
    event_put(&event);
  }

  prv_wakeup_timer_next_pending();
}

// This callback is the system dispatcher that wakes up the application by AppInstallId
// OR queues a wakeup callback for that application
// and is triggered by NewTimer
static void prv_wakeup_dispatcher(void *data) {
  // Place actual work on system_task to unload it from new_timer task
  system_task_add_callback(wakeup_dispatcher_system_task, data);
}

static bool prv_find_next_wakeup_id_callback(SettingsFile *file,
    SettingsRecordInfo *info, void *context) {
  // Check if valid entry
  if (info->key_len != sizeof(WakeupId) || info->val_len != sizeof(WakeupEntry)) {
    return true; // continue iterating
  }

  WakeupId wakeup_id;
  info->get_key(file, (uint8_t*)&wakeup_id, sizeof(WakeupId));

  WakeupEntry entry;
  info->get_val(file, (uint8_t*)&entry, sizeof(WakeupEntry));

  // If the wakeup_id is valid, and the timestamp of the entry is closer than
  // the timestamp of our global wakeup state, then set the next close wakeup event
  if (wakeup_id > 0 && (s_wakeup_state.current_wakeup_id == -1 ||
        entry.timestamp < s_wakeup_state.timestamp)) {
    s_wakeup_state.timestamp = entry.timestamp;
    s_wakeup_state.current_wakeup_id = wakeup_id;
  }

  return true; // continue iterating
}


// Checks for the next pending wakeup event and sets up a timer for the event
static void prv_wakeup_timer_next_pending(void) {
  if (!s_wakeup_enabled) {
    return;
  }

  // If there is already a wakeup timer scheduled, cancel it. There will be a
  // new timer scheduled for the soonest wakeup that is registered.
  if (new_timer_scheduled(s_current_timer_id, NULL)) {
    new_timer_stop(s_current_timer_id);
  }

  mutex_lock(s_mutex);
  {
    // Find the next event to occur
    SettingsFile wakeup_settings;
    if (settings_file_open(&wakeup_settings, SETTINGS_FILE_NAME, SETTINGS_FILE_SIZE) == S_SUCCESS) {
      // Reset wakeup state to use for the search
      s_wakeup_state.current_wakeup_id = -1;
      s_wakeup_state.timestamp = 0;
      settings_file_each(&wakeup_settings, prv_find_next_wakeup_id_callback, NULL);
      settings_file_close(&wakeup_settings);
    } else {
      PBL_LOG(LOG_LEVEL_ERROR, "Error: could not open APP_WAKEUP settings");
    }
  }
  mutex_unlock(s_mutex);

  // Create a timer for the found wakeup id, given it's valid
  if (s_wakeup_state.current_wakeup_id >= 0) {
    int32_t timestamp = s_wakeup_state.timestamp;
    time_t current_time = rtc_get_time();
    int32_t time_difference = timestamp - current_time;

    // If time_difference is negative, this was a missed past event due to set_time
    // changing current time beyond the event
    // Note: this catches the edge case that there are several wakeup events skipped
    // and avoids clobbering these events with a WAKEUP_CATCHUP_WINDOW gap,
    // including an event occurring after missed events
    if (time_difference < 0 || s_catchup_enabled) {
      // next catchup_enabled state before modifying time_difference
      s_catchup_enabled = (time_difference < 0) ? true : false;
      // Enforces the catchup gap to be at least WAKEUP_CATCHUP_WINDOW gap
      time_difference = MAX(time_difference, WAKEUP_CATCHUP_WINDOW);
    }

    // timers are in milliseconds, set main callback dispatch for wakeup
    // WakeupId is used to save/restore/lookup wakeup events
    new_timer_start(s_current_timer_id, (time_difference * 1000), prv_wakeup_dispatcher,
        (void*)((intptr_t)s_wakeup_state.current_wakeup_id), 0);
  }
}


static void prv_migrate_events_callback(SettingsFile *old_file, SettingsFile *new_file,
    SettingsRecordInfo *info, void *utc_diff) {
  if (!utc_diff || info->key_len != sizeof(WakeupId) || info->val_len != sizeof(WakeupEntry)) {
    return;
  }

  WakeupId wakeup_id;
  info->get_val(old_file, (uint8_t*)&wakeup_id, sizeof(WakeupId));

  WakeupEntry entry;
  info->get_val(old_file, (uint8_t*)&entry, sizeof(WakeupEntry));

  // Migrate the entries to the new timezone
  if (entry.utc == false) {
    entry.timestamp -= *((int*)utc_diff);
    entry.utc = true;
    if (wakeup_id == s_wakeup_state.current_wakeup_id) {
      s_wakeup_state.timestamp = entry.timestamp;
    }
  }

  // Write the new entry to the settings file.  We always write as there's no
  // chance of it being invalid.
  settings_file_set(new_file, (uint8_t*)&wakeup_id, sizeof(WakeupId),
      (uint8_t*)&entry, sizeof(WakeupEntry));
}

static bool prv_check_for_events(SettingsFile *file, SettingsRecordInfo *info, void *context) {
  bool *event_found = (bool *)context;
  *event_found = true;
  return false; // stop iterating
}

static void prv_update_events_callback(SettingsFile *old_file, SettingsFile *new_file,
    SettingsRecordInfo *info, void *context) {
  // Check if valid entry
  if (!context || info->key_len != sizeof(WakeupId)) {
    return;
  }

  bool struct_size_mismatch = info->val_len != sizeof(WakeupEntry);
  bool struct_migration = !clock_is_timezone_set() && struct_size_mismatch;
  if (struct_size_mismatch && !struct_migration) {
    return;
  }

  struct prv_missed_events_s *missed_events = (struct prv_missed_events_s*)context;

  WakeupId wakeup_id;
  info->get_key(old_file, (uint8_t*)&wakeup_id, sizeof(WakeupId));

  WakeupEntry entry;
  info->get_val(old_file, (uint8_t*)&entry, info->val_len);
  if (struct_migration) {
    entry.timestamp = wakeup_id; // WakeupId (key) is a timestamp
    entry.utc = false; // If we're migrating, this has not been utc
  }

  int32_t timestamp = entry.timestamp;
  time_t current_time = rtc_get_time();
  int32_t time_difference = timestamp - current_time;

  if (timestamp >= s_wakeup_state.next_wakeup_id) {
    s_wakeup_state.next_wakeup_id = timestamp + 1;
  } else if (wakeup_id >= s_wakeup_state.next_wakeup_id) {
    s_wakeup_state.next_wakeup_id = wakeup_id + 1;
  }

  // schedule non-expired events
  if (time_difference > 0) {
    // Using settings_file_rewrite, need to write to keep key/value
    settings_file_set(new_file, (uint8_t*)&wakeup_id, sizeof(WakeupId),
        (uint8_t*)&entry, sizeof(WakeupEntry));
  } else {
    if (entry.notify_if_missed) {
      if (missed_events->missed_app_ids == NULL) {
        // This is allocated here, but free'd in the wakup_ui.h module
        missed_events->missed_app_ids =
            kernel_malloc(NUM_APPS_ALERT_ON_BOOT * sizeof(AppInstallId));
      }
      if (missed_events->missed_apps_count > NUM_APPS_ALERT_ON_BOOT) {
        // We have more than NUM_APPS_ALERT_ON_BOOT apps that had events fire while the watch
        // was shut off. Very rare this will happen, but just down show that the apps > 16 missed
        // an event.
        return;
      }
      // Set the AppInstallId of the app that had an alert fired
      missed_events->missed_app_ids[missed_events->missed_apps_count++] =
          app_install_get_id_for_uuid(&entry.uuid);
    }
    // Deletes the entry automatically if not written
  }
}

void wakeup_init(void) {
  struct prv_missed_events_s missed_events = { 0, NULL };

  s_mutex = mutex_create();

  event_service_init(PEBBLE_WAKEUP_EVENT, NULL, NULL);

  // Create single reusable timer for wakeup events
  s_current_timer_id = new_timer_create();
  s_wakeup_state.next_wakeup_id = rtc_get_time();
  s_wakeup_state.timestamp = -1;

  SettingsFile wakeup_settings;
  if (settings_file_open(&wakeup_settings, SETTINGS_FILE_NAME, SETTINGS_FILE_SIZE) != S_SUCCESS) {
    PBL_LOG(LOG_LEVEL_ERROR, "Error: could not open wakeup settings");
    return;
  }

  // Want to check if there are any events first to prevent us from rewriting the file on boot if
  // we don't need to.
  bool event_found = false;
  settings_file_each(&wakeup_settings, prv_check_for_events, &event_found);
  if (event_found) {
    PBL_LOG(LOG_LEVEL_DEBUG, "Rewriting wakeup file");
    // Update settings file removing expired events
    settings_file_rewrite(&wakeup_settings, prv_update_events_callback, &missed_events);
  } else {
    PBL_LOG(LOG_LEVEL_DEBUG, "Not rewriting wakeup file because no entries were found");
  }
  settings_file_close(&wakeup_settings);

  // If wakeup events were missed by apps requesting notify_if_missed
  // popup a notification window displaying these apps
  if (missed_events.missed_apps_count) {
    wakeup_popup_window(missed_events.missed_apps_count, missed_events.missed_app_ids);
  }
}


static bool prv_compiled_without_utc_support(void) {
  static const Version first_utc_version = {
    // See list of changes in pebble_process_info.h. Apps compiled prior to this version will
    // get local time returned from the time() call.
    0x5,
    0x2f
  };
  Version app_sdk_version = process_metadata_get_sdk_version(
                                            sys_process_manager_get_current_process_md());

  if (version_compare(app_sdk_version, first_utc_version) < 0) {
    return true;
  }
  return false;
}


DEFINE_SYSCALL(WakeupId, sys_wakeup_schedule, time_t timestamp, int32_t reason,
                                              bool notify_if_missed) {

  if (prv_compiled_without_utc_support()) {
    // Legacy apps get local time returned from the time() call.
    timestamp = time_local_to_utc(timestamp);
  }

  time_t current_time = rtc_get_time();
  int32_t time_difference = timestamp - current_time;
  WakeupId wakeup_id = s_wakeup_state.next_wakeup_id++;

  // Disallow scheduling past events
  if (time_difference <= 0) {
    return E_INVALID_ARGUMENT;
  }

  Uuid uuid = app_manager_get_current_app_md()->uuid;

  WakeupEntry entry = {
    .uuid = uuid,
    .reason = reason,
    .notify_if_missed = notify_if_missed,
    .timestamp = timestamp,
    .utc = clock_is_timezone_set()
  };

  // Add to settings file
  StatusCode retval = prv_wakeup_settings_add_entry(wakeup_id, entry);

  // If there was an error adding the wakeup event, return the error
  if (retval < S_SUCCESS) {
    return retval;
  }

  // If this new event is sooner than the currently scheduled timer, make this
  // the current one
  prv_wakeup_timer_next_pending();
  return wakeup_id;
}


static void prv_wakeup_settings_delete_entry(WakeupId wakeup_id) {
  mutex_lock(s_mutex);
  {
    SettingsFile wakeup_settings;
    if (settings_file_open(&wakeup_settings, SETTINGS_FILE_NAME, SETTINGS_FILE_SIZE) == S_SUCCESS) {
      settings_file_delete(&wakeup_settings, (uint8_t*)&wakeup_id, sizeof(WakeupId));
      settings_file_close(&wakeup_settings);
    }
  }
  mutex_unlock(s_mutex);
}

static WakeupEntry prv_wakeup_settings_get_entry(WakeupId wakeup_id) {
  WakeupEntry entry = {{0}};

  mutex_lock(s_mutex);
  {
    SettingsFile wakeup_settings;
    if (settings_file_open(&wakeup_settings, SETTINGS_FILE_NAME, SETTINGS_FILE_SIZE) == S_SUCCESS) {
      settings_file_get(&wakeup_settings, (uint8_t*)&wakeup_id, sizeof(WakeupId),
                        (uint8_t*)&entry, sizeof(WakeupEntry));
      settings_file_close(&wakeup_settings);
    }
  }
  mutex_unlock(s_mutex);
  return entry;
}

DEFINE_SYSCALL(void, sys_wakeup_delete, WakeupId wakeup_id) {

  WakeupEntry entry = prv_wakeup_settings_get_entry(wakeup_id);

  // Only allow owner to delete its own wakeup events
  if (uuid_equal(&app_manager_get_current_app_md()->uuid, &entry.uuid)) {
    if (wakeup_id == s_wakeup_state.current_wakeup_id &&
        new_timer_scheduled(s_current_timer_id, NULL)) {
      new_timer_stop(s_current_timer_id);
    }
    prv_wakeup_settings_delete_entry(wakeup_id);
    prv_wakeup_timer_next_pending();
  }
}

static bool prv_check_count_and_availability_callback(SettingsFile *file,
    SettingsRecordInfo *info, void *context) {
  // Check if valid entry
  if (!context || info->key_len != sizeof(WakeupId) || info->val_len != sizeof(WakeupEntry)) {
    return true; // continue iterating
  }

  struct prv_check_app_and_wakeup_event_s *check = (struct prv_check_app_and_wakeup_event_s*)context;

  WakeupId wakeup_id;
  info->get_key(file, (uint8_t*)&wakeup_id, sizeof(WakeupId));

  WakeupEntry entry;
  info->get_val(file, (uint8_t*)&entry, sizeof(WakeupEntry));

  //If we have already flagged an error, just skip the rest
  if (check->wakeup_count < S_SUCCESS) {
    return true; // continue iterating
  }

  if (uuid_equal(&app_manager_get_current_app_md()->uuid, &entry.uuid)) {
    check->wakeup_count++;
  }
  // If the wakeup_id is with the same minute as another wakeup event
  if ((entry.timestamp - WAKEUP_EVENT_WINDOW < check->wakeup_timestamp) &&
      (check->wakeup_timestamp < (entry.timestamp + WAKEUP_EVENT_WINDOW))) {
    check->wakeup_count = E_RANGE;
  }

  return true; // continue iterating
}


static StatusCode prv_wakeup_settings_add_entry(WakeupId wakeup_id, WakeupEntry entry) {
  status_t status = S_SUCCESS;

  mutex_lock(s_mutex);
  {
    SettingsFile wakeup_settings;
    if (settings_file_open(&wakeup_settings, SETTINGS_FILE_NAME, SETTINGS_FILE_SIZE) == S_SUCCESS) {
      // Check if current app already has MAX_WAKEUP_EVENTS_PER_APP scheduled
      // or if the minute event window is already occupied
      struct prv_check_app_and_wakeup_event_s check = {
        .wakeup_count = 0,
        .wakeup_timestamp = entry.timestamp
      };
      settings_file_each(&wakeup_settings, prv_check_count_and_availability_callback, &check);

      if (check.wakeup_count < S_SUCCESS) {
        status = check.wakeup_count;
      } else if (check.wakeup_count >= MAX_WAKEUP_EVENTS_PER_APP) {
        status = E_OUT_OF_RESOURCES;
      } else {
        settings_file_set(&wakeup_settings, (uint8_t*)&wakeup_id, sizeof(WakeupId),
                          (uint8_t*)&entry, sizeof(WakeupEntry));
      }
      settings_file_close(&wakeup_settings);
    } else {
      status = E_INTERNAL;
    }
  }
  mutex_unlock(s_mutex);

  return status;
}

static void prv_delete_events_by_uuid_callback(SettingsFile *old_file, SettingsFile *new_file,
    SettingsRecordInfo *info, void *context) {
  // Check if valid entry
  if (info->key_len != sizeof(WakeupId) || info->val_len != sizeof(WakeupEntry)) {
    return;
  }

  WakeupId wakeup_id;
  info->get_key(old_file, (uint8_t*)&wakeup_id, sizeof(WakeupId));

  WakeupEntry entry;
  info->get_val(old_file, (uint8_t*)&entry, sizeof(WakeupEntry));

  // If the UUID is equal, delete the entry
  if (uuid_equal(&app_manager_get_current_app_md()->uuid, &entry.uuid)) {
    // if this is the current timer event, cancel it
    if (wakeup_id == s_wakeup_state.current_wakeup_id &&
        new_timer_scheduled(s_current_timer_id, NULL)) {
      new_timer_stop(s_current_timer_id);
    }
    // Deletes the entry automatically if not written
  } else { // Keep the entry
    // Using settings_file_rewrite, need to write to keep key/value
    settings_file_set(new_file, (uint8_t*)&wakeup_id, sizeof(WakeupId),
        (uint8_t*)&entry, sizeof(WakeupEntry));
  }
}


DEFINE_SYSCALL(void, sys_wakeup_cancel_all_for_app, void) {
  mutex_lock(s_mutex);
  {
    SettingsFile wakeup_settings;
    if (settings_file_open(&wakeup_settings, SETTINGS_FILE_NAME, SETTINGS_FILE_SIZE) == S_SUCCESS) {
      // Update settings file removing all events with UUID = uuid
      settings_file_rewrite(&wakeup_settings, prv_delete_events_by_uuid_callback, NULL);
      settings_file_close(&wakeup_settings);
    }
  }
  mutex_unlock(s_mutex);

  // We may have cancelled the timer, next_pending will check
  prv_wakeup_timer_next_pending();
}

DEFINE_SYSCALL(time_t, sys_wakeup_query, WakeupId wakeup_id) {
  status_t status = E_DOES_NOT_EXIST;
  WakeupEntry entry = {};

  if (wakeup_id < 0) {
    return status;
  }

  mutex_lock(s_mutex);
  {
    SettingsFile wakeup_settings;
    if (settings_file_open(&wakeup_settings, SETTINGS_FILE_NAME, SETTINGS_FILE_SIZE) == S_SUCCESS) {
      // Check if the wakeup id is valid by seeing if it is in the wakeup settings_file
      status = settings_file_get(&wakeup_settings, (uint8_t*)&wakeup_id, sizeof(WakeupId),
                                 (uint8_t*)&entry, sizeof(WakeupEntry));
      settings_file_close(&wakeup_settings);
    } else {
      status = E_INTERNAL;
    }
  }
  mutex_unlock(s_mutex);

  if (status != S_SUCCESS) {
    return status;
  }

  // timer doesn't belong to this app
  if (!uuid_equal(&app_manager_get_current_app_md()->uuid, &entry.uuid)) {
    return E_DOES_NOT_EXIST;
  }

  time_t return_time = entry.timestamp;
  if (prv_compiled_without_utc_support()) {
    // Legacy apps expect everything in local time.
    return_time = time_utc_to_local(return_time);
  }
  return return_time;
}

void wakeup_enable(bool enable) {
  bool was_enabled = s_wakeup_enabled;
  s_wakeup_enabled = enable;
  if (enable && !was_enabled) {
    prv_wakeup_timer_next_pending();
  } else if (!enable && s_current_timer_id &&
             new_timer_scheduled(s_current_timer_id, NULL)) {
    new_timer_stop(s_current_timer_id);
  }
}

TimerID wakeup_get_current(void) {
  return s_current_timer_id;
}

WakeupId wakeup_get_next_scheduled(void) {
  return s_wakeup_state.current_wakeup_id;
}

void wakeup_migrate_timezone(int utc_diff) {
  mutex_lock(s_mutex);
  {
    SettingsFile wakeup_settings;
    if (settings_file_open(&wakeup_settings, SETTINGS_FILE_NAME, SETTINGS_FILE_SIZE) == S_SUCCESS) {
      settings_file_rewrite(&wakeup_settings, prv_migrate_events_callback, (void*)&utc_diff);
      settings_file_close(&wakeup_settings);
    } else {
      PBL_LOG(LOG_LEVEL_ERROR, "Error: could not open wakeup settings");
    }
  }
  mutex_unlock(s_mutex);
}

static void prv_wakeup_rewrite_kernel_bg_cb(void *data) {
  // Update each wakeup entry via prv_update_events_callback and record any missed events
  struct prv_missed_events_s missed_events = { 0, NULL };

  mutex_lock(s_mutex);
  {
    SettingsFile wakeup_settings;
    if (settings_file_open(&wakeup_settings, SETTINGS_FILE_NAME, SETTINGS_FILE_SIZE) == S_SUCCESS) {
      // Update each wakeup entry via prv_update_events_callback and record any missed events
      settings_file_rewrite(&wakeup_settings, prv_update_events_callback, &missed_events);
      settings_file_close(&wakeup_settings);
    } else {
      PBL_LOG(LOG_LEVEL_ERROR, "Error: could not open wakeup settings");
    }
  }
  mutex_unlock(s_mutex);

  // If any events were missed due to time change display the wakeup popup
  if (missed_events.missed_apps_count) {
    wakeup_popup_window(missed_events.missed_apps_count, missed_events.missed_app_ids);
  }

  // Setup a timer for the next wakeup event; will return if wakeup is not enabled
  prv_wakeup_timer_next_pending();
}

void wakeup_handle_clock_change(void) {
  // Offload the rewrite of the wakeup file to KernelBG as it may take a while
  //
  // TODO: The flash burden of this routine could also be reduced by not doing
  // rewrites and instead updating records in place
  if (pebble_task_get_current() == PebbleTask_KernelBackground) {
    prv_wakeup_rewrite_kernel_bg_cb(NULL);
  } else {
    system_task_add_callback(prv_wakeup_rewrite_kernel_bg_cb, NULL);
  }
}
