/*
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "applib/data_logging.h"
#include "applib/health_service.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "os/mutex.h"
#include "os/tick.h"
#include "popups/health_tracking_ui.h"
#include "services/common/analytics/analytics_event.h"
#include "services/normal/protobuf_log/protobuf_log.h"
#include "syscall/syscall.h"
#include "syscall/syscall_internal.h"
#include "system/hexdump.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/base64.h"
#include "util/math.h"
#include "util/size.h"
#include "util/stats.h"
#include "util/units.h"

#include "activity.h"
#include "activity_algorithm.h"
#include "activity_calculators.h"
#include "activity_insights.h"
#include "activity_private.h"


// ---------------------------------------------------------------------------------------
// Storage converters. These convert metrics from their storage type (ActivityScalarStore,
// which is only 16-bits) into the uint32_t value returned by activity_get_metric. For example,
// we might convert minutes to seconds.
static uint32_t prv_convert_none(ActivityScalarStore in) {
  return in;
}

static uint32_t prv_convert_minutes_to_seconds(ActivityScalarStore in) {
  return (uint32_t)in * SECONDS_PER_MINUTE;
}


// ------------------------------------------------------------------------------------------------
// Returns info about each metric we capture
void activity_metrics_prv_get_metric_info(ActivityMetric metric, ActivityMetricInfo *info) {
  ActivityState *state = activity_private_state();
  *info = (ActivityMetricInfo) {
    .converter = prv_convert_none,
  };
  switch (metric) {
    case ActivityMetricStepCount:
      info->value_p = &state->step_data.steps;
      info->settings_key = ActivitySettingsKeyStepCountHistory;
      info->has_history = true;
      break;
    case ActivityMetricActiveSeconds:
      info->value_p = &state->step_data.step_minutes;
      info->settings_key = ActivitySettingsKeyStepMinutesHistory;
      info->has_history = true;
      info->converter = prv_convert_minutes_to_seconds;
      break;
    case ActivityMetricDistanceMeters:
      info->value_p = &state->step_data.distance_meters;
      info->settings_key = ActivitySettingsKeyDistanceMetersHistory;
      info->has_history = true;
      break;
    case ActivityMetricRestingKCalories:
      info->value_p = &state->step_data.resting_kcalories;
      info->settings_key = ActivitySettingsKeyRestingKCaloriesHistory;
      info->has_history = true;
      break;
    case ActivityMetricActiveKCalories:
      info->value_p = &state->step_data.active_kcalories;
      info->settings_key = ActivitySettingsKeyActiveKCaloriesHistory;
      info->has_history = true;
      break;
    case ActivityMetricSleepTotalSeconds:
      info->value_p = &state->sleep_data.total_minutes;
      info->settings_key = ActivitySettingsKeySleepTotalMinutesHistory;
      info->has_history = true;
      info->converter = prv_convert_minutes_to_seconds;
      break;
    case ActivityMetricSleepRestfulSeconds:
      info->value_p = &state->sleep_data.restful_minutes;
      info->settings_key = ActivitySettingsKeySleepDeepMinutesHistory;
      info->has_history = true;
      info->converter = prv_convert_minutes_to_seconds;
      break;
    case ActivityMetricSleepEnterAtSeconds:
      info->value_p = &state->sleep_data.enter_at_minute;
      info->settings_key = ActivitySettingsKeySleepEnterAtHistory;
      info->has_history = true;
      info->converter = prv_convert_minutes_to_seconds;
      break;
    case ActivityMetricSleepExitAtSeconds:
      info->value_p = &state->sleep_data.exit_at_minute;
      info->settings_key = ActivitySettingsKeySleepExitAtHistory;
      info->has_history = true;
      info->converter = prv_convert_minutes_to_seconds;
      break;
    case ActivityMetricSleepState:
      info->value_p = &state->sleep_data.cur_state;
      info->settings_key = ActivitySettingsKeySleepState;
      break;
    case ActivityMetricSleepStateSeconds:
      info->value_p = &state->sleep_data.cur_state_elapsed_minutes;
      info->settings_key = ActivitySettingsKeySleepStateMinutes;
      info->converter = prv_convert_minutes_to_seconds;
      break;
    case ActivityMetricLastVMC:
      info->value_p = &state->last_vmc;
      info->settings_key = ActivitySettingsKeyLastVMC;
      break;
    case ActivityMetricHeartRateRawBPM:
      info->value_p = &state->hr.metrics.current_bpm;
      break;
    case ActivityMetricHeartRateRawQuality:
      info->value_p = &state->hr.metrics.current_quality;
      break;
    case ActivityMetricHeartRateRawUpdatedTimeUTC:
      info->value_u32p = &state->hr.metrics.current_update_time_utc;
      break;
    case ActivityMetricHeartRateFilteredBPM:
      info->value_p = &state->hr.metrics.last_stable_bpm;
      break;
    case ActivityMetricHeartRateFilteredUpdatedTimeUTC:
      info->value_u32p = &state->hr.metrics.last_stable_bpm_update_time_utc;
      break;
    case ActivityMetricHeartRateZone1Minutes:
      info->value_p = &state->hr.metrics.minutes_in_zone[HRZone_Zone1];
      info->settings_key = ActivitySettingsKeyHeartRateZone1Minutes;
      info->has_history = false;
      break;
    case ActivityMetricHeartRateZone2Minutes:
      info->value_p = &state->hr.metrics.minutes_in_zone[HRZone_Zone2];
      info->settings_key = ActivitySettingsKeyHeartRateZone2Minutes;
      info->has_history = false;
      break;
    case ActivityMetricHeartRateZone3Minutes:
      info->value_p = &state->hr.metrics.minutes_in_zone[HRZone_Zone3];
      info->settings_key = ActivitySettingsKeyHeartRateZone3Minutes;
      info->has_history = false;
      break;
    case ActivityMetricNumMetrics:
      WTF;
      break;
  }
}


// ----------------------------------------------------------------------------------------------
// Set the value of a given metric
// The current value will only be overridden if the new value is higher
// Historical values can be overridden with any value
void activity_metrics_prv_set_metric(ActivityMetric metric, DayInWeek wday, int32_t value) {
  if (!activity_tracking_on()) {
    return;
  }

  ActivityState *state = activity_private_state();
  mutex_lock_recursive(state->mutex);

  switch (metric) {
    case ActivityMetricActiveSeconds:
    case ActivityMetricSleepTotalSeconds:
    case ActivityMetricSleepRestfulSeconds:
    case ActivityMetricSleepEnterAtSeconds:
    case ActivityMetricSleepExitAtSeconds:
      // We only store minutes for these metrics. Convert before saving
      value /= SECONDS_PER_MINUTE;
      break;
    default:
      break;
  }

  ActivityMetricInfo m_info = {};
  activity_metrics_prv_get_metric_info(metric, &m_info);
  const DayInWeek cur_wday = time_util_get_day_in_week(rtc_get_time());

  bool current_value_updated = false;

  if (cur_wday == wday) {
    // Update our cached copy of the value if it is larger than what we currently have
    if (m_info.value_p && value > *m_info.value_p) {
      *m_info.value_p = value;
      current_value_updated = true;
    } else if (m_info.value_u32p && (uint32_t)value > *m_info.value_u32p) {
      *m_info.value_u32p = value;
      current_value_updated = true;
    }
  } else if (m_info.has_history) {
    // This update is for a day in the past. Modify the copy stored in the settings file
    SettingsFile *file = activity_private_settings_open();
    if (!file) {
      goto unlock;
    }
    ActivitySettingsValueHistory history;
    settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key),
                      &history, sizeof(history));

    int day = positive_modulo(cur_wday - wday, DAYS_PER_WEEK);
    if (history.values[day] != value) {
      history.values[day] = value;

      settings_file_set(file, &m_info.settings_key, sizeof(m_info.settings_key),
                        &history, sizeof(history));
    }
    activity_private_settings_close(file);
  }

  if (current_value_updated) {
    if (metric == ActivityMetricStepCount) {
      PebbleEvent e = {
        .type = PEBBLE_HEALTH_SERVICE_EVENT,
        .health_event = {
          .type = HealthEventMovementUpdate,
          .data.movement_update = {
            .steps = value,
          },
        },
      };
      event_put(&e);
    } else if (metric == ActivityMetricDistanceMeters) {
      state->distance_mm = state->step_data.distance_meters * MM_PER_METER;
    } else if (metric == ActivityMetricActiveKCalories) {
      state->active_calories = state->step_data.active_kcalories * ACTIVITY_CALORIES_PER_KCAL;
    } else if (metric == ActivityMetricRestingKCalories) {
      state->resting_calories = state->step_data.resting_kcalories * ACTIVITY_CALORIES_PER_KCAL;
    }
    activity_algorithm_metrics_changed_notification();
  }

unlock:
  mutex_unlock_recursive(state->mutex);
}


// ----------------------------------------------------------------------------------------------
// Shift the history back one day and reset the current day's stats.
// We use NOINLINE to reduce the stack requirements during the minute handler (see PBL-38130)
static void NOINLINE prv_shift_history(time_t utc_now) {
  ActivityState *state = activity_private_state();
  PBL_LOG(LOG_LEVEL_INFO, "resetting metrics for new day");
  mutex_lock_recursive(state->mutex);
  {
    SettingsFile *file = activity_private_settings_open();
    if (!file) {
      goto unlock;
    }
    ActivitySettingsValueHistory history;
    ActivityMetricInfo m_info;

    for (ActivityMetric metric = ActivityMetricFirst; metric < ActivityMetricNumMetrics;
         metric++) {
      activity_metrics_prv_get_metric_info(metric, &m_info);

      // Shift the history
      if (m_info.has_history) {
        PBL_ASSERTN(m_info.value_p);
        settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key), &history,
                          sizeof(history));

        for (int i = ACTIVITY_HISTORY_DAYS - 1; i >= 1; i--) {
          history.values[i] = history.values[i - 1];
        }
        // We just wrapped up yesterday
        history.values[1] = *m_info.value_p;

        // Reset stats for today
        history.values[0] = 0;
        history.utc_sec = utc_now;

        settings_file_set(file, &m_info.settings_key, sizeof(m_info.settings_key), &history,
                          sizeof(history));
      }
    }
    activity_private_settings_close(file);
  }
unlock:
  mutex_unlock_recursive(state->mutex);
}


// --------------------------------------------------------------------------------------------
// Called from activity_get_metric() every time a client asks for a metric. Also called
// periodically from the minute handler before we save current metrics to setting.
static void prv_update_real_time_derived_metrics(void) {
  ActivityState *state = activity_private_state();
  mutex_lock_recursive(state->mutex);
  {
    state->step_data.distance_meters = ROUND(state->distance_mm,
                                                       MM_PER_METER);
    ACTIVITY_LOG_DEBUG("new distance: %"PRIu16"", state->step_data.distance_meters);

    state->step_data.active_kcalories = ROUND(state->active_calories,
                                                        ACTIVITY_CALORIES_PER_KCAL);
    ACTIVITY_LOG_DEBUG("new active kcal: %"PRIu16"", state->step_data.active_kcalories);
  }
  mutex_unlock_recursive(state->mutex);
}


// --------------------------------------------------------------------------------------------
// Called periodically from the minute handler to update step derived metrics that do not have to
// be updated in real time.
// We use NOINLINE to reduce the stack requirements during the minute handler (see PBL-38130)
static void NOINLINE prv_update_step_derived_metrics(time_t utc_sec) {
  ActivityState *state = activity_private_state();
  mutex_lock_recursive(state->mutex);
  {
    int minute_of_day = time_util_get_minute_of_day(utc_sec);
    // The "no-steps-during-sleep" logic can introduce negative steps, so make sure we clip
    // negative steps to 0 when computing the metrics below
    uint16_t steps_in_minute = 0;
    if (state->step_data.steps >= state->steps_per_minute_last_steps) {
      steps_in_minute = state->step_data.steps
                        - state->steps_per_minute_last_steps;
    }

    // Update the walking rate
    state->steps_per_minute = steps_in_minute;
    state->steps_per_minute_last_steps = state->step_data.steps;
    ACTIVITY_LOG_DEBUG("new steps/minute: %"PRIu16"", state->steps_per_minute);

    // Update the number of stepping minutes and the last active minute
    if (state->steps_per_minute >= ACTIVITY_ACTIVE_MINUTE_MIN_STEPS) {
      state->step_data.step_minutes++;
      ACTIVITY_LOG_DEBUG("new step minutes: %"PRIu16"", state->step_data.step_minutes);

      // The prior minute was the most recent active one
      state->last_active_minute = time_util_minute_of_day_adjust(minute_of_day, -1);
      ACTIVITY_LOG_DEBUG("last active minute: %"PRIu16"", state->last_active_minute);
    }

    // Update the resting calories
    state->resting_calories = activity_private_compute_resting_calories(minute_of_day);
    state->step_data.resting_kcalories = ROUND(state->resting_calories,
                                                         ACTIVITY_CALORIES_PER_KCAL);
    ACTIVITY_LOG_DEBUG("resting kcalories: %"PRIu16"",
                       state->step_data.resting_kcalories);
  }
  mutex_unlock_recursive(state->mutex);
}


// ------------------------------------------------------------------------------------------
// Pushes an HR Median/Filtered/LastStable event.
static void prv_push_median_hr_event(uint8_t median_hr) {
  if (median_hr > 0) {
    PebbleEvent event = {
      .type = PEBBLE_HEALTH_SERVICE_EVENT,
      .health_event = {
        .type = HealthEventHeartRateUpdate,
        .data.heart_rate_update = {
          .current_bpm = median_hr,
          .is_filtered = true,
        }
      }
    };
    event_put(&event);
  }
}


// ------------------------------------------------------------------------------------------
// Calculates and stores the most recent minutes median heart rate value.
// Used for the health_service and the minute level data.
static void prv_update_median_hr_bpm(ActivityState *state) {
  const ActivityHRSupport *hr = &state->hr;

  const uint16_t num_hr_samples = hr->num_samples;
  if (num_hr_samples > 0) {
    int32_t median, total_weight;

    // Stats requires an int32_t array and we need one for both the samples and the weights
    int32_t *sample_buf = task_zalloc_check(num_hr_samples * sizeof(int32_t));
    int32_t *weight_buf = task_zalloc_check(num_hr_samples * sizeof(int32_t));
    for (size_t i = 0; i < num_hr_samples; i++) {
      sample_buf[i] = hr->samples[i];
      weight_buf[i] = hr->weights[i];
    }

    // Calculate the total weight
    stats_calculate_basic(StatsBasicOp_Sum, weight_buf, hr->num_samples, NULL, NULL,
                          &total_weight);

    // Calculate the weighted median
    median = stats_calculate_weighted_median(sample_buf, weight_buf, num_hr_samples);
    task_free(sample_buf);
    task_free(weight_buf);

    state->hr.metrics.last_stable_bpm = (uint8_t)median;
    state->hr.metrics.last_stable_bpm_update_time_utc = rtc_get_time();
    state->hr.metrics.previous_median_bpm = (uint8_t)median;
    state->hr.metrics.previous_median_total_weight_x100 = total_weight;

    prv_push_median_hr_event(state->hr.metrics.previous_median_bpm);
  }
}

// ------------------------------------------------------------------------------------------
static void prv_write_hr_zone_info_to_flash(HRZone zone) {
  ActivityMetric metric;
  if (zone == HRZone_Zone1) {
    metric = ActivityMetricHeartRateZone1Minutes;
  } else if (zone == HRZone_Zone2) {
    metric = ActivityMetricHeartRateZone2Minutes;
  } else if (zone == HRZone_Zone3) {
    metric = ActivityMetricHeartRateZone3Minutes;
  } else {
    // Don't store data for Zone 0
    return;
  }

  SettingsFile *file = activity_private_settings_open();
  if (!file) {
    return;
  }

  ActivityMetricInfo m_info;
  activity_metrics_prv_get_metric_info(metric, &m_info);
  settings_file_set(file, &m_info.settings_key, sizeof(m_info.settings_key),
                    m_info.value_p, sizeof(*m_info.value_p));
  activity_private_settings_close(file);
}

// ------------------------------------------------------------------------------------------
// The median HR should get updated before calling this
static void prv_update_current_hr_zone(ActivityState *state) {
  int32_t hr_median;
  activity_metrics_prv_get_median_hr_bpm(&hr_median, NULL);
  HRZone new_hr_zone = hr_util_get_hr_zone(hr_median);

  if (new_hr_zone != HRZone_Zone0 && state->hr.num_samples < ACTIVITY_MIN_NUM_SAMPLES_FOR_HR_ZONE) {
    // There wasn't enough data in the past minute to give us confidence that
    // the new HR zone will represents that minute, default to Zone0
    new_hr_zone = HRZone_Zone0;
  }

  bool new_hr_elevated = hr_util_is_elevated(hr_median);
  // Before changing the zone make sure the user has an elevated heart rate.
  // This prevents erroneous HRM readings accumulating minutes in zone 1.
  // Then only go up/down 1 zone per minute.
  // This prevents erroneous HRM readings accumulating minutes in higher zones.
  if (!state->hr.metrics.is_hr_elevated && new_hr_elevated) {
    state->hr.metrics.is_hr_elevated = new_hr_elevated;
  } else if (new_hr_zone > state->hr.metrics.current_hr_zone) {
    state->hr.metrics.current_hr_zone++;
  } else if (new_hr_zone < state->hr.metrics.current_hr_zone) {
    state->hr.metrics.current_hr_zone--;
  } else if (!new_hr_elevated) {
    state->hr.metrics.is_hr_elevated = new_hr_elevated;
  }

  state->hr.metrics.minutes_in_zone[state->hr.metrics.current_hr_zone]++;

  prv_write_hr_zone_info_to_flash(state->hr.metrics.current_hr_zone);
}

// ------------------------------------------------------------------------------------------
// Called periodically from the minute handler to update the median HR and time spent in HR zones
static void prv_update_hr_derived_metrics(void) {
  ActivityState *state = activity_private_state();
  mutex_lock_recursive(state->mutex);
  {
    // Update the median HR / HR weight for the minute
    prv_update_median_hr_bpm(state);

    // Update our current HR zone (based on the median which is calculated above)
    prv_update_current_hr_zone(state);
  }
  mutex_unlock_recursive(state->mutex);
}

// ------------------------------------------------------------------------------------------
// The metrics minute handler
void activity_metrics_prv_minute_handler(time_t utc_sec) {
  ActivityState *state = activity_private_state();

  uint16_t cur_day_index = time_util_get_day(utc_sec);
  if (cur_day_index != state->cur_day_index) {
    // If we've just encountered a midnight rollover, shift history to the new day
    // before we compute metrics for the new day
    prv_shift_history(utc_sec);
  }

  // Update the derived metrics
  prv_update_real_time_derived_metrics();
  prv_update_step_derived_metrics(utc_sec);
  prv_update_hr_derived_metrics();
}


// --------------------------------------------------------------------------------------------
ActivityScalarStore activity_metrics_prv_steps_per_minute(void) {
  ActivityState *state = activity_private_state();
  return state->steps_per_minute;
}


// --------------------------------------------------------------------------------------------
uint32_t activity_metrics_prv_get_distance_mm(void) {
  ActivityState *state = activity_private_state();
  return state->distance_mm;
}


// --------------------------------------------------------------------------------------------
uint32_t activity_metrics_prv_get_resting_calories(void) {
  ActivityState *state = activity_private_state();
  return state->resting_calories;
}


// --------------------------------------------------------------------------------------------
uint32_t activity_metrics_prv_get_active_calories(void) {
  ActivityState *state = activity_private_state();
  return state->active_calories;
}


// --------------------------------------------------------------------------------------------
uint32_t activity_metrics_prv_get_steps(void) {
  ActivityState *state = activity_private_state();
  return state->step_data.steps;
}

static uint8_t prv_get_hr_quality_weight(HRMQuality quality) {
  static const struct {
    HRMQuality quality;
    uint8_t weight_x100;
  } s_hr_quality_weights_x100[] = {
    {HRMQuality_NoAccel, 0 },
    {HRMQuality_OffWrist, 0 },
    {HRMQuality_NoSignal, 0 },
    {HRMQuality_Worst, 1 },
    {HRMQuality_Poor, 1 },
    {HRMQuality_Acceptable, 60 },
    {HRMQuality_Good, 65 },
    {HRMQuality_Excellent, 85 },
  };

  for (size_t i = 0; i < ARRAY_LENGTH(s_hr_quality_weights_x100); i++) {
    if (quality == s_hr_quality_weights_x100[i].quality) {
      return s_hr_quality_weights_x100[i].weight_x100;
    }
  }
  return 0;
}

// --------------------------------------------------------------------------------------------
HRZone activity_metrics_prv_get_hr_zone(void) {
  ActivityState *state = activity_private_state();

  return state->hr.metrics.current_hr_zone;
}

// --------------------------------------------------------------------------------------------
void activity_metrics_prv_get_median_hr_bpm(int32_t *median_out,
                                            int32_t *heart_rate_total_weight_x100_out) {
  ActivityState *state = activity_private_state();

  if (median_out) {
    *median_out = state->hr.metrics.previous_median_bpm;
  }
  if (heart_rate_total_weight_x100_out) {
    *heart_rate_total_weight_x100_out = state->hr.metrics.previous_median_total_weight_x100;
  }
}

// --------------------------------------------------------------------------------------------
void activity_metrics_prv_reset_hr_stats(void) {
  ActivityState *state = activity_private_state();
  mutex_lock_recursive(state->mutex);
  {
    state->hr.num_samples = 0;
    state->hr.num_quality_samples = 0;
    memset(state->hr.samples, 0, sizeof(state->hr.samples));
    memset(state->hr.weights, 0, sizeof(state->hr.weights));

    state->hr.metrics.previous_median_bpm = 0;
    state->hr.metrics.previous_median_total_weight_x100 = 0;
  }
  mutex_unlock_recursive(state->mutex);
}

// --------------------------------------------------------------------------------------------
void activity_metrics_prv_add_median_hr_sample(PebbleHRMEvent *hrm_event, time_t now_utc,
                                               time_t now_uptime) {
  ActivityState *state = activity_private_state();
  mutex_lock_recursive(state->mutex);
  {
    // Update stats used for computing the average
    if (hrm_event->bpm.bpm > 0) {
      // This should get reset about once a minute, so X minutes worth of samples means something
      // is terribly wrong.
      PBL_ASSERT(state->hr.num_samples <= ACTIVITY_MAX_HR_SAMPLES, "Too many samples");
      state->hr.samples[state->hr.num_samples] = hrm_event->bpm.bpm;
      state->hr.weights[state->hr.num_samples] =
          prv_get_hr_quality_weight(hrm_event->bpm.quality);
      if (hrm_event->bpm.quality >= ACTIVITY_MIN_HR_QUALITY_THRESH) {
        state->hr.num_quality_samples++;
      }

      state->hr.num_samples++;
    }
    // Update the timestamp used for figuring out when we should change the sampling period.
    // This is based on uptime so that it doesn't get messed up if the mobile changes the
    // UTC time on us.
    state->hr.last_sample_ts = now_uptime;

    // Save the BPM, quality, and update time (UTC) of the last reading for activity_get_metric()
    state->hr.metrics.current_bpm = hrm_event->bpm.bpm;
    state->hr.metrics.current_quality = hrm_event->bpm.quality;
    state->hr.metrics.current_update_time_utc = now_utc;
  }
  mutex_unlock_recursive(state->mutex);
}

// ------------------------------------------------------------------------------------------------
void activity_metrics_prv_init(SettingsFile *file, time_t utc_now) {
  ActivityState *state = activity_private_state();
  // Roll back the history if needed and init each of the metrics for today
  for (ActivityMetric metric = ActivityMetricFirst; metric < ActivityMetricNumMetrics;
       metric++) {
    ActivityMetricInfo m_info;
    activity_metrics_prv_get_metric_info(metric, &m_info);
    if (m_info.has_history) {
      PBL_ASSERTN(m_info.value_p);
      ActivitySettingsValueHistory old_history = { 0 };
      ActivitySettingsValueHistory new_history = { 0 };

      // In case we change the length of the history, fetch the old size
      int fetch_size = sizeof(old_history);
      fetch_size = MIN(fetch_size, settings_file_get_len(file, &m_info.settings_key,
                       sizeof(m_info.settings_key)));
      settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key), &old_history,
                        fetch_size);

      uint16_t day = time_util_get_day(old_history.utc_sec);
      int old_age = state->cur_day_index - day;

      // If this is resting kcalories, the default for each day is not 0
      if (metric == ActivityMetricRestingKCalories) {
        uint32_t full_day_resting_calories =
            activity_private_compute_resting_calories(MINUTES_PER_DAY);
        for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++) {
          if (i == 0) {
            uint32_t elapsed_minutes = time_util_get_minute_of_day(utc_now);
            uint32_t cur_day_resting_calories =
                activity_private_compute_resting_calories(elapsed_minutes);
            new_history.values[i] = ROUND(cur_day_resting_calories, ACTIVITY_CALORIES_PER_KCAL);
          } else {
            new_history.values[i] = ROUND(full_day_resting_calories, ACTIVITY_CALORIES_PER_KCAL);
          }
        }
      }

      // Copy values from old history into correct slot in new history
      for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++) {
        int new_index = i + old_age;
        if (new_index >= 0 && new_index < ACTIVITY_HISTORY_DAYS) {
          new_history.values[new_index] = old_history.values[i];
        }
      }
      // init the time stamp if not initialized yet
      if (new_history.utc_sec == 0) {
        new_history.utc_sec = utc_now;
      }

      // Init current value
      *m_info.value_p = new_history.values[0];

      // Only write to flash if the values change or this is a new day (to update the timestamp)
      if (memcmp(old_history.values, new_history.values, sizeof(old_history.values)) != 0
          || old_age != 0) {
        // Write out the updated history
        settings_file_set(file, &m_info.settings_key, sizeof(m_info.settings_key), &new_history,
                          sizeof(new_history));
      }

    } else if (m_info.settings_key != ActivitySettingsKeyInvalid) {
      // Metric with no history, just init current value
      PBL_ASSERTN(m_info.value_p);
      settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key), m_info.value_p,
                        sizeof(*m_info.value_p));
    }
  }
}


// ------------------------------------------------------------------------------------------------
bool activity_get_metric(ActivityMetric metric, uint32_t history_len, int32_t *history) {
  ActivityState *state = activity_private_state();
  bool success = true;

  // Default results
  for (uint32_t i = 0; i < history_len; i++) {
    history[i] = -1;
  }

  mutex_lock_recursive(state->mutex);
  {
    if (!activity_prefs_tracking_is_enabled() && pebble_task_get_current() == PebbleTask_App) {
      health_tracking_ui_app_show_disabled();
    }

    // Update derived metrics
    prv_update_real_time_derived_metrics();

    ActivityMetricInfo m_info;
    activity_metrics_prv_get_metric_info(metric, &m_info);

    if (history_len == 0) {
      goto unlock;
    }

    // Clip history length
    history_len = MIN(history_len, ACTIVITY_HISTORY_DAYS);
    if (!m_info.has_history) {
      history_len = 1;
    }

    // Fill in current value
    if (m_info.value_p) {
      history[0] = m_info.converter(*m_info.value_p);
    } else {
      PBL_ASSERTN(m_info.value_u32p && (m_info.converter == prv_convert_none));
      history[0] = *m_info.value_u32p;
    }
    ACTIVITY_LOG_DEBUG("get current metric %"PRIi32" : %"PRIi32"", (int32_t)metric, history[0]);

    // Look up historical values
    if (history_len > 1) {
      // Read from the history stored in settings
      ActivitySettingsValueHistory setting_history = {};
      SettingsFile *file = activity_private_settings_open();
      if (!file) {
        PBL_LOG(LOG_LEVEL_ERROR, "Settings file DNE. No need to continue getting metric");
        success = false;
        goto unlock;
      }
      settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key), &setting_history,
                        sizeof(setting_history));
      for (uint32_t i = 1; i < history_len; i++) {
        history[i] = m_info.converter(setting_history.values[i]);
        ACTIVITY_LOG_DEBUG("get metric %"PRIi32" %"PRIu32" days ago: %"PRIi32"", (int32_t)metric,
                           i, history[i]);
      }
      activity_private_settings_close(file);
    }
  }
unlock:
  mutex_unlock_recursive(state->mutex);
  return success;
}


// ------------------------------------------------------------------------------------------------
DEFINE_SYSCALL(bool, sys_activity_get_metric, ActivityMetric metric,
               uint32_t history_len, int32_t *history) {
  if (PRIVILEGE_WAS_ELEVATED) {
    if (history) {
      syscall_assert_userspace_buffer(history, history_len * sizeof(*history));
    }
  }

  return activity_get_metric(metric, history_len, history);
}
