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

#include "services/common/analytics/analytics_heartbeat.h"
#include "services/common/analytics/analytics_metric.h"
#include "services/common/analytics/analytics_logging.h"

#include "system/logging.h"
#include "system/passert.h"
#include "kernel/pbl_malloc.h"
#include "util/math.h"

#include <inttypes.h>
#include <stdio.h>

uint32_t analytics_heartbeat_kind_data_size(AnalyticsHeartbeatKind kind) {
  AnalyticsMetric last = ANALYTICS_METRIC_INVALID;
  switch (kind) {
  case ANALYTICS_HEARTBEAT_KIND_DEVICE:
    last = ANALYTICS_DEVICE_METRIC_END - 1;
    break;
  case ANALYTICS_HEARTBEAT_KIND_APP:
    last = ANALYTICS_APP_METRIC_END - 1;
    break;
  }
  PBL_ASSERTN(last != ANALYTICS_METRIC_INVALID);
  return analytics_metric_offset(last) + analytics_metric_size(last);
}

/////////////////////
// Private
static bool prv_verify_kinds_match(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric) {
  AnalyticsMetricKind metric_kind = analytics_metric_kind(metric);
  if ((metric_kind == ANALYTICS_METRIC_KIND_DEVICE) &&
      (heartbeat->kind == ANALYTICS_HEARTBEAT_KIND_DEVICE)) {
    return true;
  } else if ((metric_kind == ANALYTICS_METRIC_KIND_APP) &&
      (heartbeat->kind == ANALYTICS_HEARTBEAT_KIND_APP)) {
    return true;
  } else {
    PBL_CROAK("Metric kind does not match heartbeat kind! %d %d", metric_kind, heartbeat->kind);
  }
}
static uint8_t *prv_heartbeat_get_location(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric) {
  prv_verify_kinds_match(heartbeat, metric);
  if (analytics_metric_is_array(metric)) {
    PBL_CROAK("Attempt to use integer value for array metric.");
  }
  return heartbeat->data + analytics_metric_offset(metric);
}
static uint8_t *prv_heartbeat_get_array_location(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric,
          uint32_t index) {
  prv_verify_kinds_match(heartbeat, metric);
  if (!analytics_metric_is_array(metric)) {
    PBL_CROAK("Attempt to use array value for integer metric.");
  }
  uint32_t len = analytics_metric_num_elements(metric);
  uint32_t element_size = analytics_metric_element_size(metric);
  if (index > len) {
    PBL_CROAK("Attempt to use array value at invalid index %" PRId32 " (len %" PRId32 ")",
              index, len);
  }
  return heartbeat->data + analytics_metric_offset(metric) + index*element_size;
}

static void prv_location_set_value(uint8_t *location, int64_t val, AnalyticsMetricElementType type) {
  switch (type) {
  case ANALYTICS_METRIC_ELEMENT_TYPE_NIL:
    WTF;
 case ANALYTICS_METRIC_ELEMENT_TYPE_UINT8:
   {
     *((uint8_t*)location) = (uint8_t)CLIP(val, 0, UINT8_MAX);
    return;
   }
  case ANALYTICS_METRIC_ELEMENT_TYPE_UINT16:
    {
      *((uint16_t*)location) = (uint16_t)CLIP(val, 0, UINT16_MAX);
    return;
    }
  case ANALYTICS_METRIC_ELEMENT_TYPE_UINT32:
    {
      *((uint32_t*)location) = (uint32_t)CLIP(val, 0, UINT32_MAX);
    return;
    }
  case ANALYTICS_METRIC_ELEMENT_TYPE_INT8:
    {
      *((int8_t*)location) = (int8_t)CLIP(val, INT8_MIN, INT8_MAX);
    return;
    }
  case ANALYTICS_METRIC_ELEMENT_TYPE_INT16:
    {
      *((int16_t*)location) = (int16_t)CLIP(val, INT16_MIN, INT16_MAX);
    return;
    }
  case ANALYTICS_METRIC_ELEMENT_TYPE_INT32:
    {
      *((int32_t*)location) = (int32_t)CLIP(val, INT32_MIN, INT32_MAX);
    return;
    }
  }
  WTF; // Should not get here!
}
static int64_t prv_location_get_value(uint8_t *location, AnalyticsMetricElementType type) {
  switch (type) {
  case ANALYTICS_METRIC_ELEMENT_TYPE_NIL:
    WTF;
  case ANALYTICS_METRIC_ELEMENT_TYPE_UINT8:
    return *(uint8_t*)location;
  case ANALYTICS_METRIC_ELEMENT_TYPE_UINT16:
    return *(uint16_t*)location;
  case ANALYTICS_METRIC_ELEMENT_TYPE_UINT32:
    return *(uint32_t*)location;
  case ANALYTICS_METRIC_ELEMENT_TYPE_INT8:
    return *(int8_t*)location;
  case ANALYTICS_METRIC_ELEMENT_TYPE_INT16:
    return *(int16_t*)location;
  case ANALYTICS_METRIC_ELEMENT_TYPE_INT32:
    return *(int32_t*)location;
  }
  WTF; // Should not get here!
}

//////////
// Set

void memfault_metric_set_device_from_pebble_analytics(AnalyticsMetric metric, int64_t val);

void analytics_heartbeat_set(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric, int64_t val) {
  uint8_t *location = prv_heartbeat_get_location(heartbeat, metric);
  prv_location_set_value(location, val, analytics_metric_element_type(metric));

#if MEMFAULT
  if (heartbeat->kind == ANALYTICS_HEARTBEAT_KIND_DEVICE) {
    memfault_metric_set_device_from_pebble_analytics(metric, val);
  }
#endif
}

void analytics_heartbeat_set_array(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric, uint32_t index, int64_t val) {
  uint8_t *location = prv_heartbeat_get_array_location(heartbeat, metric, index);
  prv_location_set_value(location, val, analytics_metric_element_type(metric));
}

void analytics_heartbeat_set_entire_array(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric, const void* data) {
  uint8_t *location = prv_heartbeat_get_array_location(heartbeat, metric, 0);
  uint32_t size = analytics_metric_size(metric);
  memcpy(location, data, size);
}

/////////
// Get
int64_t analytics_heartbeat_get(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric) {
  uint8_t *location = prv_heartbeat_get_location(heartbeat, metric);
  return prv_location_get_value(location, analytics_metric_element_type(metric));
}

int64_t analytics_heartbeat_get_array(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric, uint32_t index) {
  uint8_t *location = prv_heartbeat_get_array_location(heartbeat, metric, index);
  return prv_location_get_value(location, analytics_metric_element_type(metric));
}

const Uuid *analytics_heartbeat_get_uuid(AnalyticsHeartbeat *heartbeat) {
  return (const Uuid*)prv_heartbeat_get_array_location(heartbeat, ANALYTICS_APP_METRIC_UUID, 0);
}

///////////////////
// Create / Clear
AnalyticsHeartbeat *analytics_heartbeat_create(AnalyticsHeartbeatKind kind) {
  uint32_t size = sizeof(AnalyticsHeartbeat) + analytics_heartbeat_kind_data_size(kind);
  AnalyticsHeartbeat *heartbeat = kernel_malloc_check(size);
  heartbeat->kind = kind;
  analytics_heartbeat_clear(heartbeat);
  return heartbeat;
}

AnalyticsHeartbeat *analytics_heartbeat_device_create() {
  AnalyticsHeartbeat *hb = analytics_heartbeat_create(ANALYTICS_HEARTBEAT_KIND_DEVICE);
  analytics_heartbeat_set(hb, ANALYTICS_DEVICE_METRIC_BLOB_KIND,
                          ANALYTICS_BLOB_KIND_DEVICE_HEARTBEAT);
  analytics_heartbeat_set(hb, ANALYTICS_DEVICE_METRIC_BLOB_VERSION,
                          ANALYTICS_DEVICE_HEARTBEAT_BLOB_VERSION);
  return hb;
}

AnalyticsHeartbeat *analytics_heartbeat_app_create(const Uuid *uuid) {
  AnalyticsHeartbeat *hb = analytics_heartbeat_create(ANALYTICS_HEARTBEAT_KIND_APP);
  analytics_heartbeat_set_entire_array(hb, ANALYTICS_APP_METRIC_UUID, uuid);
  analytics_heartbeat_set(hb, ANALYTICS_APP_METRIC_BLOB_KIND,
                          ANALYTICS_BLOB_KIND_APP_HEARTBEAT);
  analytics_heartbeat_set(hb, ANALYTICS_APP_METRIC_BLOB_VERSION,
                          ANALYTICS_APP_HEARTBEAT_BLOB_VERSION);
  return hb;
}

void analytics_heartbeat_clear(AnalyticsHeartbeat *heartbeat) {
  AnalyticsHeartbeatKind kind = heartbeat->kind;
  uint32_t size = sizeof(AnalyticsHeartbeat) + analytics_heartbeat_kind_data_size(kind);
  memset(heartbeat, 0, size);
  heartbeat->kind = kind;
}

//////////////////
// Debug
#ifdef ANALYTICS_DEBUG
// Function to get the name of a macro given it's runtime value. (i.e. mapping
//   (1: "ANALYTICS_DEVICE_METRIC_MSG_ID"),
//   (2: "ANALYTICS_DEVICE_METRIC_VERSION"),
//   ...
// )
#define CASE(name, ...) case name: return #name;
static const char *prv_get_metric_name(AnalyticsMetric metric) {
  switch (metric) {
    ANALYTICS_METRIC_TABLE(CASE,CASE,CASE,,,,,,)
    default: return "";
  }
}
#undef CASE

static void prv_print_heartbeat(AnalyticsHeartbeat *heartbeat, AnalyticsMetric start, AnalyticsMetric end) {
  for (AnalyticsMetric metric = start + 1; metric < end; metric++) {
    const char *name = prv_get_metric_name(metric);
    if (!analytics_metric_is_array(metric)) {
      int64_t val = analytics_heartbeat_get(heartbeat, metric);
      if (val >= 0) {
        PBL_LOG(LOG_LEVEL_DEBUG, "%3" PRIu32 ": %s: %" PRIu32 " (0x%" PRIx32")",
            analytics_metric_offset(metric), name, (uint32_t)val, (uint32_t)val);
      } else {
        PBL_LOG(LOG_LEVEL_DEBUG, "%3" PRIu32 ": %s: %" PRId32 " (0x%" PRIx32")",
            analytics_metric_offset(metric), name, (int32_t)val, (int32_t)val);
      }
      continue;
    }
    const size_t BUF_LENGTH = 256;
    char buf[BUF_LENGTH];
    uint32_t written = 0;
    for (uint32_t i = 0; i < analytics_metric_num_elements(metric); i++) {
      if (written > BUF_LENGTH) {
        PBL_LOG(LOG_LEVEL_DEBUG, "print buffer overflow by %lu bytes",
            BUF_LENGTH - written);
        continue;
      }
      int64_t val = analytics_heartbeat_get_array(heartbeat, metric, i);
      const char *sep = (i == 0 ? "" : ", ");
      if (val >= 0) {
        written += snprintf(buf + written, BUF_LENGTH - written,
            "%s%" PRIu32 " (0x%" PRIx32 ")", sep, (uint32_t)val, (uint32_t)val);
      } else {
        written += snprintf(buf + written, BUF_LENGTH - written,
            "%s%" PRId32 " (0x%" PRIx32 ")", sep, (int32_t)val, (int32_t)val);
      }
    }
    PBL_LOG(LOG_LEVEL_DEBUG, "%3" PRIu32 ": %s: %s", analytics_metric_offset(metric), name, buf);
  }
}

void analytics_heartbeat_print(AnalyticsHeartbeat *heartbeat) {
  switch (heartbeat->kind) {
  case ANALYTICS_HEARTBEAT_KIND_DEVICE:
    PBL_LOG(LOG_LEVEL_DEBUG, "Device heartbeat:");
    prv_print_heartbeat(heartbeat, ANALYTICS_DEVICE_METRIC_START, ANALYTICS_DEVICE_METRIC_END);
    break;
  case ANALYTICS_HEARTBEAT_KIND_APP: {
    const Uuid *uuid = analytics_heartbeat_get_uuid(heartbeat);
    char uuid_buf[UUID_STRING_BUFFER_LENGTH];
    uuid_to_string(uuid, uuid_buf);
    PBL_LOG(LOG_LEVEL_DEBUG, "App heartbeat for %s:", uuid_buf);
    prv_print_heartbeat(heartbeat, ANALYTICS_APP_METRIC_START, ANALYTICS_APP_METRIC_END);
    break;
  }
  default:
    PBL_LOG(LOG_LEVEL_DEBUG, "Unable to print heartbeat: Unrecognized kind %d", heartbeat->kind);
  }
}
#else
void analytics_heartbeat_print(AnalyticsHeartbeat *heartbeat) {
  PBL_LOG(LOG_LEVEL_DEBUG, "Turn on ANALYTICS_DEBUG to get heartbeat printing support.");
}
#endif
