/*
 * 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.
 */

#pragma once

#include "activity.h"
#include "hr_util.h"

#include "applib/event_service_client.h"
#include "kernel/events.h"
#include "os/mutex.h"
#include "services/normal/data_logging/data_logging_service.h"
#include "services/normal/settings/settings_file.h"
#include "system/hexdump.h"
#include "system/logging.h"
#include "util/attributes.h"

#include <stdbool.h>
#include <stdint.h>

#define ACTIVITY_LOG_DEBUG(fmt, args...) \
        PBL_LOG_D(LOG_DOMAIN_ACTIVITY, LOG_LEVEL_DEBUG, fmt, ## args)

#define ACTIVITY_HEXDUMP(data, length) \
        PBL_HEXDUMP_D(LOG_DOMAIN_DATA_ACTIVITY, LOG_LEVEL_DEBUG, data, length)

// How often we update settings with the current step/sleep stats for today.
#define ACTIVITY_SETTINGS_UPDATE_MIN              15

// How often we recompute the activity sessions (like sleep, walks, runs). This has significant
// enough CPU requirements to warrant only recomputing occasionally
#define ACTIVITY_SESSION_UPDATE_MIN               15

// Every scalar metric and setting is stored in globals and in the settings file using this
// typedef
typedef uint16_t ActivityScalarStore;
#define ACTIVITY_SCALAR_MAX                       UINT16_MAX

// Each step average interval covers this many minutes
#define ACTIVITY_STEP_AVERAGES_MINUTES             (MINUTES_PER_DAY / ACTIVITY_NUM_METRIC_AVERAGES)

// flash vs. the most amount of data we could lose if we reset.
#define ACTIVITY_STEP_AVERAGES_PER_KEY 4
#define ACTIVITY_STEP_AVERAGES_KEYS_PER_DAY   \
                      (ACTIVITY_NUM_METRIC_AVERAGES / ACTIVITY_STEP_AVERAGES_PER_KEY)

// If we see at least this many steps in a minute, it was an "active minute"
#define ACTIVITY_ACTIVE_MINUTE_MIN_STEPS 40

// We consider any sleep session that ends after this minute of the day (representing 9pm) as
// part of the next day's sleep
#define ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY  (21 * MINUTES_PER_HOUR)

// Default HeartRate sampling period (Must take a sample every X seconds by default)
#define ACTIVITY_DEFAULT_HR_PERIOD_SEC (10 * SECONDS_PER_MINUTE)

// Default HeartRate sampling ON time (Stays on for X seconds every
// ACTIVITY_DEFAULT_HR_PERIOD_SEC seconds)
#define ACTIVITY_DEFAULT_HR_ON_TIME_SEC (SECONDS_PER_MINUTE)

// Turn off the HR device after we've received X number of thresholded samples
#define ACTIVITY_MIN_NUM_SAMPLES_SHORT_CIRCUIT (15)

// The minimum number of samples needed before we can approximate the user's HR zone
#define ACTIVITY_MIN_NUM_SAMPLES_FOR_HR_ZONE (10)

#define ACTIVITY_MIN_HR_QUALITY_THRESH (HRMQuality_Good)

// HRM Subscription values during ON and OFF periods
#define ACTIVITY_HRM_SUBSCRIPTION_ON_PERIOD_SEC  (1)
#define ACTIVITY_HRM_SUBSCRIPTION_OFF_PERIOD_SEC (SECONDS_PER_DAY)

// Max number of stored HR samples to compute the median
#define ACTIVITY_MAX_HR_SAMPLES (3 * SECONDS_PER_MINUTE)

// Conversion factors
#define ACTIVITY_DAG_PER_KG  100


// -----------------------------------------------------------------------------------------
// Settings file info and keys
#define ACTIVITY_SETTINGS_FILE_NAME           "activity"
#define ACTIVITY_SETTINGS_FILE_LEN            0x4000

// The version of our settings file
// Version 1 - ActivitySettingsKeyVersion didn't exist
// Version 2 - Changed file size from 2k to 16k
#define ACTIVITY_SETTINGS_CURRENT_VERSION     2

typedef struct {
  uint32_t utc_sec;                     // timestamp of first entry in list
  // One entry per day. The most recent day (today) is stored at index 0
  ActivityScalarStore values[ACTIVITY_HISTORY_DAYS];
} ActivitySettingsValueHistory;


// Keys of the settings we save in our settings file.
typedef enum {
  ActivitySettingsKeyInvalid = 0,                 // Used for error discovery
  ActivitySettingsKeyVersion,                     // uint16_t: ACTIVITY_SETTINGS_CURRENT_VERSION
  ActivitySettingsKeyUnused0,                     // Unused
  ActivitySettingsKeyUnused1,                     // Unused
  ActivitySettingsKeyUnused2,                     // Unused
  ActivitySettingsKeyUnused3,                     // Unused

  ActivitySettingsKeyStepCountHistory,            // ActivitySettingsValueHistory
  ActivitySettingsKeyStepMinutesHistory,          // ActivitySettingsValueHistory
  ActivitySettingsKeyUnused4,                     // Unused
  ActivitySettingsKeyDistanceMetersHistory,       // ActivitySettingsValueHistory
  ActivitySettingsKeySleepTotalMinutesHistory,    // ActivitySettingsValueHistory
  ActivitySettingsKeySleepDeepMinutesHistory,     // ActivitySettingsValueHistory
  ActivitySettingsKeySleepEntryMinutesHistory,    // ActivitySettingsValueHistory
                                                  // How long it took to fall asleep
  ActivitySettingsKeySleepEnterAtHistory,         // ActivitySettingsValueHistory
                                                  // What time the user fell asleep. Measured in
                                                  // minutes after midnight.
  ActivitySettingsKeySleepExitAtHistory,          // ActivitySettingsValueHistory
                                                  // What time the user woke up. Measured in
                                                  // minutes after midnight
  ActivitySettingsKeySleepState,                  // uint16_t
  ActivitySettingsKeySleepStateMinutes,           // uint16_t
  ActivitySettingsKeyStepAveragesWeekdayFirst,    // ACTIVITY_STEP_AVERAGES_PER_CHUNK * uint16_t
  ActivitySettingsKeyStepAveragesWeekdayLast =
        ActivitySettingsKeyStepAveragesWeekdayFirst + ACTIVITY_STEP_AVERAGES_KEYS_PER_DAY - 1,

  ActivitySettingsKeyStepAveragesWeekendFirst,    // ACTIVITY_STEP_AVERAGES_PER_CHUNK * uint16_t
  ActivitySettingsKeyStepAveragesWeekendLast =
        ActivitySettingsKeyStepAveragesWeekendFirst + ACTIVITY_STEP_AVERAGES_KEYS_PER_DAY - 1,
  ActivitySettingsKeyAgeYears,                    // uint16_t: age in years

  ActivitySettingsKeyUnused5,                     // Unused

  ActivitySettingsKeyInsightSleepRewardTime,      // time_t: time we last showed the sleep reward
                                                  // This will be 0 if we haven't triggered one yet
  ActivitySettingsKeyInsightActivityRewardTime,   // time_t: time we last showed the activity reward
                                                  // This will be 0 if we haven't triggered one yet
  ActivitySettingsKeyInsightActivitySummaryState, // SummaryPinLastState: the UUID and last time the
                                                  // pin was added
  ActivitySettingsKeyInsightSleepSummaryState,    // SummaryPinLastState: the UUID and last time the
                                                  // pin was added
  ActivitySettingsKeyRestingKCaloriesHistory,     // ActivitySettingsValueHistory
  ActivitySettingsKeyActiveKCaloriesHistory,      // ActivitySettingsValueHistory
  ActivitySettingsKeyLastSleepActivityUTC,        // time_t: UTC timestamp of the last sleep related
                                                  //  activity we logged to analytics
  ActivitySettingsKeyLastRestfulSleepActivityUTC, // time_t: UTC timestamp of the last restful sleep
                                                  // related activity we logged to analytics
  ActivitySettingsKeyLastStepActivityUTC,         // time_t: UTC timestamp of the last step related
                                                  //  activity we logged to analytics
  ActivitySettingsKeyStoredActivities,            // ActivitySession[
                                                  //         ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT]
  ActivitySettingsKeyInsightNapSessionTime,       // time_t: time we last showed the nap pin
  ActivitySettingsKeyInsightActivitySessionTime,  // time_t: time we last showed the activity pin
  ActivitySettingsKeyLastVMC,                     // uint16_t: the VMC at the last processed minute
  ActivitySettingsKeyRestingHeartRate,            // ActivitySettingsValueHistory
  ActivitySettingsKeyHeartRateZone1Minutes,
  ActivitySettingsKeyHeartRateZone2Minutes,
  ActivitySettingsKeyHeartRateZone3Minutes,
} ActivitySettingsKey;


// -----------------------------------------------------------------------------------------
// Internal structs
// IMPORTANT: activity_metrics_prv_get_metric_info() assumes that every element of ActivityStepData
// is an ActivityScalarStore
typedef struct {
  ActivityScalarStore steps;
  ActivityScalarStore step_minutes;
  ActivityScalarStore distance_meters;
  ActivityScalarStore resting_kcalories;
  ActivityScalarStore active_kcalories;
} ActivityStepData;

// IMPORTANT: activity_metrics_prv_get_metric_info() assumes that every element of ActivitySleepData
// is an ActivityScalarStore
typedef struct {
  ActivityScalarStore total_minutes;
  ActivityScalarStore restful_minutes;
  ActivityScalarStore enter_at_minute;            // minutes after midnight
  ActivityScalarStore exit_at_minute;             // minutes after midnight
  ActivityScalarStore cur_state;                  // HealthActivity
  ActivityScalarStore cur_state_elapsed_minutes;
} ActivitySleepData;

// IMPORTANT: activity_metrics_prv_get_metric_info() assumes that elements of
// ActivityHeartRateData are ActivityScalarStore by default. The update_time_utc is
// specially coded as a 32-bit metric and is allowed to be because we don't persist it in
// the settings file and it has no history
typedef struct {
  ActivityScalarStore current_bpm;           // Most current reading
  uint32_t current_update_time_utc;          // Timestamp of the current HR reading
  ActivityScalarStore current_hr_zone;
  ActivityScalarStore resting_bpm;
  ActivityScalarStore current_quality;       // HRMQuality
  ActivityScalarStore last_stable_bpm;
  uint32_t last_stable_bpm_update_time_utc;  // Timestamp of the last stable BPM
  ActivityScalarStore previous_median_bpm;   // Most recently calculated median HR in a minute
  int32_t previous_median_total_weight_x100;
  ActivityScalarStore minutes_in_zone[HRZoneCount];
  bool is_hr_elevated;
} ActivityHeartRateData;


// This callback used to convert a metric from the storage format (as a ActivityScalarStore) into
// the return format (uint32_t) returned by activity_get_metric. It might convert minutes to
// seconds, etc.
typedef uint32_t (*ActivityMetricConverter)(ActivityScalarStore storage_value);

// Filled in by activity_metrics_prv_get_metric_info()
typedef struct {
  ActivityScalarStore *value_p;       // pointer to storage in globals
  uint32_t *value_u32p;               // alternate value pointer for 32-bit metrics. These
                                      // can NOT have history and settings_key MUST be
                                      // ActivitySettingsKeyInvalid.
  bool has_history;                   // True if this metric has history. This determines the
                                      // size of the value as stored in settings
  ActivitySettingsKey settings_key;   // Settings key for this value
  ActivityMetricConverter converter;  // convert from storage value to return value.
} ActivityMetricInfo;

// Used by activity_feed_samples
typedef struct {
  uint16_t num_samples;
  AccelRawData data[];
} ActivityFeedSamples;

// Version of our legacy sleep session logging records (prior to FW 3.11). NOTE: The version
// field is treated as a bitfield. For version 1, only bit 0 is set. As long as we keep bit 0 set,
// we are free to add more fields to the end of ActivityLegacySleepSessionDataLoggingRecord and the
// mobile app will continue to assume it can parse the blob. If bit 0 is cleared, the mobile app
// will know that it has no chance of parsing the blob (until the mobile app is updated of course).
#define ACTIVITY_SLEEP_SESSION_LOGGING_VERSION 1

// Data logging record used to send sleep sessions to the phone
typedef struct PACKED {
  uint16_t version;              // set to ACTIVITY_SLEEP_SESSION_LOGGING_VERSION
  int32_t utc_to_local;          // Add this to UTC to get local time
  uint32_t start_utc;            // The start time in UTC
  uint32_t end_utc;              // The end time in UTC
  uint32_t restful_secs;
} ActivityLegacySleepSessionDataLoggingRecord;

// Version of our activity session logging records. NOTE: The version field is treated as a
// bitfield. For version 1, only bit 0 is set. As long as we keep bit 0 set, we are free to
// add more fields to the end of ActivitySessionDataLoggingRecord and the mobile app
// will continue to assume it can parse the blob. If bit 0 is cleared, the mobile app will know that
// it has no chance of parsing the blob (until the mobile app is updated of course).
#define ACTIVITY_SESSION_LOGGING_VERSION 3

// Data logging record used to send activity sessions to the phone
// NOTE: modifying this struct requires a bump to the ACTIVITY_SESSION_LOGGING_VERSION and
// an update to documentation on this wiki page:
//   https://pebbletechnology.atlassian.net/wiki/pages/viewpage.action?pageId=46301269
typedef struct PACKED {
  uint16_t version;                 // set to ACTIVITY_SESSION_LOGGING_VERSION
  uint16_t size;                    // size of this structure
  uint16_t activity;                // ActivitySessionType: the type of activity
  int32_t utc_to_local;             // Add this to UTC to get local time
  uint32_t start_utc;               // The start time in UTC
  uint32_t elapsed_sec;             // Elapsed time in seconds

  // New fields add in version 3
  union {
    ActivitySessionDataStepping step_data;
    ActivitySessionDataSleeping sleep_data;
  };
} ActivitySessionDataLoggingRecord;

// -----------------------------------------------------------------------------------------
// Globals

// Support for raw accel sample collection
typedef struct {
  // The data logging session for the current sample collection session
  DataLoggingSession *dls_session;

  // Most recently encoded accel sample value. Used for detecting and encoding runs of the same
  // value
  uint32_t prev_sample;           // See comments in ActivityRawSamplesRecord for encoding
  uint8_t run_size;               // run size of prev_sample

  // The currently forming record
  ActivityRawSamplesRecord record;

  // large enough to base64 encode half of the record at once.
  char base64_buf[sizeof(ActivityRawSamplesRecord)];

  // True if we are forming the first record
  bool first_record;
} ActivitySampleCollectionData;

// This type is defined in measurements_log.h but we can't include measurements_log.h in this header
// because of build issues with the auto-generated SDK files.
typedef void *ProtobufLogRef;

// Support for heart rate
typedef struct {
  ActivityHeartRateData metrics;      // ActivityMetrics for heart rate

  HRMSessionRef hrm_session;          // The HRM session we use
  ProtobufLogRef log_session;     // The measurements log we send data to

  bool currently_sampling;            // Are we activity sampling the HR
  uint32_t toggled_sampling_at_ts;    // When we last toggled our sampling rate
                                      // (from time_get_uptime_seconds)

  uint32_t last_sample_ts;            // When we last received a HR sample
                                      // (from time_get_uptime_seconds)

  uint16_t num_samples;               // number of samples in the past minute
  uint16_t num_quality_samples;       // number of samples in the past minute that have met our
                                      // quality threshold ACTIVITY_MIN_HR_QUALITY_THRESH
                                      // NOTE: Used to short circuit
                                      //   our HR polling when enough samples have been taken
  uint8_t  samples[ACTIVITY_MAX_HR_SAMPLES]; // HR Samples stored
  uint8_t  weights[ACTIVITY_MAX_HR_SAMPLES]; // HR Sample Weights
} ActivityHRSupport;

typedef struct {
  // Mutex for serializing access to these globals
  PebbleRecursiveMutex *mutex;

  // Semaphore used for waiting for KernelBG to finish a callback
  SemaphoreHandle_t bg_wait_semaphore;

  // Accel session ref
  AccelServiceState *accel_session;

  // Event Service to keep track of whether the charger is connected
  EventServiceInfo charger_subscription;

  // Cumulative stats for today
  ActivityStepData step_data;
  ActivitySleepData sleep_data;

  // We accumulate distance in mm to and active/resting calories in calories (not kcalories) to
  // minimize rounding errors since we increment them every time we get a new rate reading from the
  // algorithm (every 5 seconds).
  uint32_t distance_mm;
  uint32_t active_calories;
  uint32_t resting_calories;
  ActivityScalarStore last_vmc;
  uint8_t last_orientation;
  time_t rate_last_update_time;

  // Most recently calculated minute average walking rate
  ActivityScalarStore steps_per_minute;
  ActivityScalarStore steps_per_minute_last_steps;

  // The most recent minute that had any significant step activity. Used for computing
  // amount of time it takes to fall asleep
  uint16_t last_active_minute;

  // Heart rate support
  ActivityHRSupport hr;

  // Most recent values from prv_get_day()
  uint16_t cur_day_index;

  // Modulo counter used to periodically update settings file
  int8_t update_settings_counter;

  // Captured activity sessions
  uint16_t activity_sessions_count;        // how many sessions we have captured
  ActivitySession activity_sessions[ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT];
  bool need_activities_saved;              // true if activities need to be persisted

  // Set to true when a new sleep session is registered
  bool sleep_sessions_modified;

  // Exit time for the last sleep/step activities we logged. Used to prevent logging the same event
  // more than once.
  time_t logged_sleep_activity_exit_at_utc;
  time_t logged_restful_sleep_activity_exit_at_utc;
  time_t logged_step_activity_exit_at_utc;

  // Data logging session used for sending activity sessions (introduced in v3.11)
  DataLoggingSession *activity_dls_session;

  // Variables used for detecting "significant activity" events
  time_t activity_event_start_utc;            // UTC of first active minute, 0 if none detected

  // True if service has been enabled via services_set_runlevel.
  bool enabled_run_level;
  // True if the current state of charging allows the service to run.
  bool enabled_charging_state;

  // True if activity tracking should be started. If enabled is false, this can still be true
  // and will tell us that we should re-start tracking once enabled gets set again.
  bool should_be_started;

  // True if tracking has actually been started. This will only ever be set if enabled is also
  // true.
  bool started;

  // Support for raw accel sample collection
  bool sample_collection_enabled;
  uint16_t sample_collection_session_id;         // raw sample collection session id
  time_t sample_collection_seconds;              // if enabled is true, the UTC when sample
  // collection started, else the # of seconds of
  // of data in recently ended session
  uint16_t sample_collection_num_samples;        // number of samples collected so far
  ActivitySampleCollectionData *sample_collection_data;

  // True if activity_start_tracking was called with test_mode = true
  bool test_mode;
  bool pending_test_cb;
} ActivityState;

//! Get pointer to the activity state
ActivityState *activity_private_state(void);

//! Get whether HRM is present
bool activity_is_hrm_present(void);

//! Shared with activity_insights.c - opens the activity settings file
//! IMPORTANT: This function must only be called during activity init routines or while holding
//! the activity mutex
SettingsFile *activity_private_settings_open(void);

//! Shared with activity_insights.c - closes the activity settings file
//! IMPORTANT: This function must only be called during activity init routines or while holding
//! the activity mutex
void activity_private_settings_close(SettingsFile *file);

//! Used by test apps (running on firmware): Re-initialize activity service. If reset_settings is
//! true, all persistent data is cleared
//! @param[in] reset_settings if true, reset all stored settings
//! @param[in] tracking_on if true, turn on tracking if not already on. Otherwise, preserve
//!                         the current tracking status
//! @param[in] sleep_history if not NULL, rewrite sleep history to these values
//! @param[in] step_history if not NULL, rewrite step history to these values
bool activity_test_reset(bool reset_settings, bool tracking_on,
                         const ActivitySettingsValueHistory *sleep_history,
                         const ActivitySettingsValueHistory *step_history);

// --------------------------------------------------------------------------------
// Activity Sessions
// Load in the stored activities from our settings file
void activity_sessions_prv_init(SettingsFile *file, time_t utc_now);

// Get the UTC time bounds for the current day
void activity_sessions_prv_get_sleep_bounds_utc(time_t now_utc, time_t *enter_utc,
                                                time_t *exit_utc);

// Remove all activity sessions that are older than "today", those that are invalid because they
// are in the future, and optionally those that are still ongoing.
void activity_sessions_prv_remove_out_of_range_activity_sessions(time_t utc_sec,
                                                                 bool remove_ongoing);

//! Return true if the given activity type is sleep related
bool activity_sessions_prv_is_sleep_activity(ActivitySessionType activity_type);

//! Return true if the given activity type has session that is currently ongoing.
bool activity_sessions_is_session_type_ongoing(ActivitySessionType activity_type);

//! Register a new activity session. This is called by the algorithm logic when it detects a new
//! activity.
void activity_sessions_prv_add_activity_session(ActivitySession *session);

//! Delete an activity session. This is called by the algorithm logic when it decides to not
//! register a sleep session after all. Only sessions that are still 'ongoing' are allowed to be
//! deleted.
void activity_sessions_prv_delete_activity_session(ActivitySession *session);

//! Perform our once a minute activity session maintenance logic
void activity_sessions_prv_minute_handler(time_t utc_sec);

//! Send an activity session to data logging
void activity_sessions_prv_send_activity_session_to_data_logging(ActivitySession *session);


// ---------------------------------------------------------------------------
// Activity Metrics

//! Init all metrics
void activity_metrics_prv_init(SettingsFile *file, time_t utc_now);

//! Returns info about each metric we capture
void activity_metrics_prv_get_metric_info(ActivityMetric metric, ActivityMetricInfo *info);

//! Perform our once a minute metrics maintenance logic
void activity_metrics_prv_minute_handler(time_t utc_sec);

//! Returns the number of millimeters the user has walked so far today (since midnight)
uint32_t activity_metrics_prv_get_distance_mm(void);

//! Returns the number of resting calories the user has consumed so far today (since midnight)
uint32_t activity_metrics_prv_get_resting_calories(void);

//! Returns the number of active calories the user has consumed so far today (since midnight)
uint32_t activity_metrics_prv_get_active_calories(void);

//! Retrieve the median heart rate and the total weight x100 since it was last reset.
//! If no readings were recorded since it was reset, it will return 0.
//! This median can be reset using activity_metrics_prv_reset_hr_stats().
//! It is by default reset once a minute.
void activity_metrics_prv_get_median_hr_bpm(int32_t *median_out,
                                            int32_t *heart_rate_total_weight_x100_out);

//! Retrieve the current HR zone since it was last reset.
//! If no readings were recorded since it was reset, it will return 0.
//! This HR zone can be reset using activity_metrics_prv_reset_hr_stats().
//! It is by default reset once a minute.
HRZone activity_metrics_prv_get_hr_zone(void);

//! Reset the average / median heart rate and hr zone
void activity_metrics_prv_reset_hr_stats(void);

//! Feed in a new heart rate sample that will be used to update the median. This updates
//! the value returned by activity_metrics_prv_get_median_hr_bpm().
void activity_metrics_prv_add_median_hr_sample(PebbleHRMEvent *hrm_event, time_t now_utc,
                                               time_t now_uptime);

//! Returns the number of steps the user has taken so far today (since midnight)
uint32_t activity_metrics_prv_get_steps(void);

//! Returns the number of steps the user has walked in the past minute
ActivityScalarStore activity_metrics_prv_steps_per_minute(void);

//! Set a metric's value. Used from BlobDB to honor requests from the phone
void activity_metrics_prv_set_metric(ActivityMetric metric, DayInWeek day, int32_t value);
