/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * 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 "calibration/nano_calibration/nano_calibration.h"

#include <cstdint>
#include <cstring>

#include "common/techeng_log_util.h"

namespace nano_calibration {
namespace {

// Common log message sensor-specific identifiers.
constexpr char kAccelTag[] = {"[NanoSensorCal:ACCEL_MPS2]"};
constexpr char kGyroTag[] = {"[NanoSensorCal:GYRO_RPS]"};
constexpr char kMagTag[] = {"[NanoSensorCal:MAG_UT]"};

// Defines a plan for limiting log messages so that upon initialization there
// begins a period set by 'duration_of_rapid_messages_min' where log messages
// appear at a rate set by 'rapid_message_interval_sec'. Afterwards, log
// messages will be produced at a rate determined by
// 'slow_message_interval_min'.
struct LogMessageRegimen {
  uint8_t rapid_message_interval_sec;  // Assists device verification.
  uint8_t slow_message_interval_min;   // Avoids long-term log spam.
  uint8_t duration_of_rapid_messages_min;
};

constexpr LogMessageRegimen kGyroscopeMessagePlan = {
    /*rapid_message_interval_sec*/ 20,
    /*slow_message_interval_min*/ 5,
    /*duration_of_rapid_messages_min*/ 3};

using ::online_calibration::CalibrationDataThreeAxis;
using ::online_calibration::CalibrationTypeFlags;
using ::online_calibration::SensorData;
using ::online_calibration::SensorIndex;
using ::online_calibration::SensorType;

// NanoSensorCal logging macros.
#ifndef LOG_TAG
#define LOG_TAG "[ImuCal]"
#endif

#ifdef NANO_SENSOR_CAL_DBG_ENABLED
#define NANO_CAL_LOGD(tag, format, ...) \
  TECHENG_LOGD("%s " format, tag, ##__VA_ARGS__)
#define NANO_CAL_LOGW(tag, format, ...) \
  TECHENG_LOGW("%s " format, tag, ##__VA_ARGS__)
#define NANO_CAL_LOGE(tag, format, ...) \
  TECHENG_LOGE("%s " format, tag, ##__VA_ARGS__)
#else
#define NANO_CAL_LOGD(tag, format, ...) techeng_log_null(format, ##__VA_ARGS__)
#define NANO_CAL_LOGW(tag, format, ...) techeng_log_null(format, ##__VA_ARGS__)
#define NANO_CAL_LOGE(tag, format, ...) techeng_log_null(format, ##__VA_ARGS__)
#endif  // NANO_SENSOR_CAL_DBG_ENABLED

// NOTE: LOGI is defined to ensure calibration updates are always logged for
// field diagnosis and verification.
#define NANO_CAL_LOGI(tag, format, ...) \
  TECHENG_LOGI("%s " format, tag, ##__VA_ARGS__)

}  // namespace

void NanoSensorCal::Initialize(OnlineCalibrationThreeAxis *accel_cal,
                               OnlineCalibrationThreeAxis *gyro_cal,
                               OnlineCalibrationThreeAxis *mag_cal) {
  // Loads stored calibration data and initializes the calibration algorithms.
  accel_cal_ = accel_cal;
  if (accel_cal_ != nullptr) {
    if (accel_cal_->get_sensor_type() == SensorType::kAccelerometerMps2) {
      LoadAshCalibration(CHRE_SENSOR_TYPE_ACCELEROMETER, accel_cal_,
                         &accel_cal_update_flags_, kAccelTag);
      NANO_CAL_LOGI(kAccelTag,
                    "Accelerometer runtime calibration initialized.");
    } else {
      accel_cal_ = nullptr;
      NANO_CAL_LOGE(kAccelTag, "Failed to initialize: wrong sensor type.");
    }
  }

  gyro_cal_ = gyro_cal;
  if (gyro_cal_ != nullptr) {
    if (gyro_cal_->get_sensor_type() == SensorType::kGyroscopeRps) {
      LoadAshCalibration(CHRE_SENSOR_TYPE_GYROSCOPE, gyro_cal_,
                         &gyro_cal_update_flags_, kGyroTag);
      NANO_CAL_LOGI(kGyroTag, "Gyroscope runtime calibration initialized.");
    } else {
      gyro_cal_ = nullptr;
      NANO_CAL_LOGE(kGyroTag, "Failed to initialize: wrong sensor type.");
    }
  }

  mag_cal_ = mag_cal;
  if (mag_cal != nullptr) {
    if (mag_cal->get_sensor_type() == SensorType::kMagnetometerUt) {
      LoadAshCalibration(CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD, mag_cal_,
                         &mag_cal_update_flags_, kMagTag);
      NANO_CAL_LOGI(kMagTag, "Magnetometer runtime calibration initialized.");
    } else {
      mag_cal_ = nullptr;
      NANO_CAL_LOGE(kMagTag, "Failed to initialize: wrong sensor type.");
    }
  }

  // Resets the initialization timestamp. Set below in HandleSensorSamples.
  initialization_start_time_nanos_ = 0;
}

void NanoSensorCal::HandleSensorSamples(
    uint16_t event_type, const chreSensorThreeAxisData *event_data) {
  // Converts CHRE Event -> SensorData::SensorType.
  SensorData sample;
  switch (event_type) {
    case CHRE_EVENT_SENSOR_UNCALIBRATED_ACCELEROMETER_DATA:
      sample.type = SensorType::kAccelerometerMps2;
      break;
    case CHRE_EVENT_SENSOR_UNCALIBRATED_GYROSCOPE_DATA:
      sample.type = SensorType::kGyroscopeRps;
      break;
    case CHRE_EVENT_SENSOR_UNCALIBRATED_GEOMAGNETIC_FIELD_DATA:
      sample.type = SensorType::kMagnetometerUt;
      break;
    default:
      // This sensor type is not used.
      NANO_CAL_LOGW("[NanoSensorCal]",
                    "Unexpected 3-axis sensor type received.");
      return;
  }

  // Sends the sensor payload to the calibration algorithms and checks for
  // calibration updates.
  const auto &header = event_data->header;
  const auto *data = event_data->readings;
  sample.timestamp_nanos = header.baseTimestamp;
  for (size_t i = 0; i < header.readingCount; i++) {
    sample.timestamp_nanos += data[i].timestampDelta;
    memcpy(sample.data, data[i].v, sizeof(sample.data));
    ProcessSample(sample);
  }

  // Starts tracking the time after initialization to help rate limit gyro log
  // messaging.
  if (initialization_start_time_nanos_ == 0) {
    initialization_start_time_nanos_ = header.baseTimestamp;
    gyro_notification_time_nanos_ = 0;
  }
}

void NanoSensorCal::HandleTemperatureSamples(
    uint16_t event_type, const chreSensorFloatData *event_data) {
  // Computes the mean of the batched temperature samples and delivers it to the
  // calibration algorithms. Note, the temperature sensor batch size determines
  // its minimum update interval.
  if (event_type == CHRE_EVENT_SENSOR_ACCELEROMETER_TEMPERATURE_DATA &&
      event_data->header.readingCount > 0) {
    const auto header = event_data->header;
    const auto *data = event_data->readings;

    SensorData sample;
    sample.type = SensorType::kTemperatureCelsius;
    sample.timestamp_nanos = header.baseTimestamp;

    float accum_temperature_celsius = 0.0f;
    for (size_t i = 0; i < header.readingCount; i++) {
      sample.timestamp_nanos += data[i].timestampDelta;
      accum_temperature_celsius += data[i].value;
    }
    sample.data[SensorIndex::kSingleAxis] =
        accum_temperature_celsius / header.readingCount;
    ProcessSample(sample);
  } else {
    NANO_CAL_LOGW("[NanoSensorCal]",
                  "Unexpected single-axis sensor type received.");
  }
}

void NanoSensorCal::ProcessSample(const SensorData &sample) {
  // Sends a new sensor sample to each active calibration algorithm and sends
  // out notifications for new calibration updates.
  if (accel_cal_ != nullptr) {
    const CalibrationTypeFlags new_cal_flags =
        accel_cal_->SetMeasurement(sample);
    if (new_cal_flags != CalibrationTypeFlags::NONE) {
      accel_cal_update_flags_ |= new_cal_flags;
      NotifyAshCalibration(CHRE_SENSOR_TYPE_ACCELEROMETER,
                           accel_cal_->GetSensorCalibration(),
                           accel_cal_update_flags_, kAccelTag);
      PrintCalibration(accel_cal_->GetSensorCalibration(),
                       accel_cal_update_flags_, kAccelTag);

      if (result_callback_ != nullptr) {
        result_callback_->SetCalibrationEvent(sample.timestamp_nanos,
                                              SensorType::kAccelerometerMps2,
                                              accel_cal_update_flags_);
      }
    }
  }

  if (gyro_cal_ != nullptr) {
    const CalibrationTypeFlags new_cal_flags =
        gyro_cal_->SetMeasurement(sample);
    if (new_cal_flags != CalibrationTypeFlags::NONE) {
      gyro_cal_update_flags_ |= new_cal_flags;
      if (NotifyAshCalibration(CHRE_SENSOR_TYPE_GYROSCOPE,
                               gyro_cal_->GetSensorCalibration(),
                               gyro_cal_update_flags_, kGyroTag)) {
        const bool print_gyro_log =
            HandleGyroLogMessage(sample.timestamp_nanos);

        if (result_callback_ != nullptr &&
            (print_gyro_log ||
             gyro_cal_update_flags_ != CalibrationTypeFlags::BIAS)) {
          // Rate-limits OTC gyro telemetry updates since they can happen
          // frequently with temperature change. However, all GyroCal stillness
          // and OTC model parameter updates will be recorded.
          result_callback_->SetCalibrationEvent(sample.timestamp_nanos,
                                                SensorType::kGyroscopeRps,
                                                gyro_cal_update_flags_);
        }
      }
    }
  }

  if (mag_cal_ != nullptr) {
    const CalibrationTypeFlags new_cal_flags = mag_cal_->SetMeasurement(sample);
    if (new_cal_flags != CalibrationTypeFlags::NONE) {
      mag_cal_update_flags_ |= new_cal_flags;
      NotifyAshCalibration(CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD,
                           mag_cal_->GetSensorCalibration(),
                           mag_cal_update_flags_, kMagTag);
      PrintCalibration(mag_cal_->GetSensorCalibration(), mag_cal_update_flags_,
                       kMagTag);

      if (result_callback_ != nullptr) {
        result_callback_->SetCalibrationEvent(sample.timestamp_nanos,
                                              SensorType::kMagnetometerUt,
                                              mag_cal_update_flags_);
      }
    }
  }
}

bool NanoSensorCal::NotifyAshCalibration(
    uint8_t chreSensorType, const CalibrationDataThreeAxis &cal_data,
    CalibrationTypeFlags flags, const char *sensor_tag) {
  // Updates the sensor offset calibration using the ASH API.
  ashCalInfo ash_cal_info;
  memset(&ash_cal_info, 0, sizeof(ashCalInfo));
  ash_cal_info.compMatrix[0] = 1.0f;  // Sets diagonal to unity (scale factor).
  ash_cal_info.compMatrix[4] = 1.0f;
  ash_cal_info.compMatrix[8] = 1.0f;
  memcpy(ash_cal_info.bias, cal_data.offset, sizeof(ash_cal_info.bias));

  // Maps CalibrationQualityLevel to ASH calibration accuracy.
  switch (cal_data.calibration_quality.level) {
    case online_calibration::CalibrationQualityLevel::HIGH_QUALITY:
      ash_cal_info.accuracy = ASH_CAL_ACCURACY_HIGH;
      break;

    case online_calibration::CalibrationQualityLevel::MEDIUM_QUALITY:
      ash_cal_info.accuracy = ASH_CAL_ACCURACY_MEDIUM;
      break;

    case online_calibration::CalibrationQualityLevel::LOW_QUALITY:
      ash_cal_info.accuracy = ASH_CAL_ACCURACY_LOW;
      break;

    default:
      ash_cal_info.accuracy = ASH_CAL_ACCURACY_UNRELIABLE;
      break;
  }

  if (!ashSetCalibration(chreSensorType, &ash_cal_info)) {
    NANO_CAL_LOGE(sensor_tag, "ASH failed to apply calibration update.");
    return false;
  }

  // Uses the ASH API to store all calibration parameters relevant to a given
  // algorithm as indicated by the input calibration type flags.
  ashCalParams ash_cal_parameters;
  memset(&ash_cal_parameters, 0, sizeof(ashCalParams));
  if (flags & CalibrationTypeFlags::BIAS) {
    ash_cal_parameters.offsetTempCelsius = cal_data.offset_temp_celsius;
    memcpy(ash_cal_parameters.offset, cal_data.offset,
           sizeof(ash_cal_parameters.offset));
    ash_cal_parameters.offsetSource = ASH_CAL_PARAMS_SOURCE_RUNTIME;
    ash_cal_parameters.offsetTempCelsiusSource = ASH_CAL_PARAMS_SOURCE_RUNTIME;
  }

  if (flags & CalibrationTypeFlags::OVER_TEMP) {
    memcpy(ash_cal_parameters.tempSensitivity, cal_data.temp_sensitivity,
           sizeof(ash_cal_parameters.tempSensitivity));
    memcpy(ash_cal_parameters.tempIntercept, cal_data.temp_intercept,
           sizeof(ash_cal_parameters.tempIntercept));
    ash_cal_parameters.tempSensitivitySource = ASH_CAL_PARAMS_SOURCE_RUNTIME;
    ash_cal_parameters.tempInterceptSource = ASH_CAL_PARAMS_SOURCE_RUNTIME;
  }

  if (!ashSaveCalibrationParams(chreSensorType, &ash_cal_parameters)) {
    NANO_CAL_LOGE(sensor_tag, "ASH failed to write calibration update.");
    return false;
  }

  return true;
}

bool NanoSensorCal::LoadAshCalibration(uint8_t chreSensorType,
                                       OnlineCalibrationThreeAxis *online_cal,
                                       CalibrationTypeFlags *flags,
                                       const char *sensor_tag) {
  ashCalParams recalled_ash_cal_parameters;
  if (ashLoadCalibrationParams(chreSensorType, ASH_CAL_STORAGE_ASH,
                               &recalled_ash_cal_parameters)) {
    // Checks whether a valid set of runtime calibration parameters was received
    // and can be used for initialization.
    if (DetectRuntimeCalibration(chreSensorType, sensor_tag, flags,
                                 &recalled_ash_cal_parameters)) {
      CalibrationDataThreeAxis cal_data;
      cal_data.type = online_cal->get_sensor_type();
      cal_data.cal_update_time_nanos = chreGetTime();

      // Analyzes the calibration flags and sets only the runtime calibration
      // values that were received.
      if (*flags & CalibrationTypeFlags::BIAS) {
        cal_data.offset_temp_celsius =
            recalled_ash_cal_parameters.offsetTempCelsius;
        memcpy(cal_data.offset, recalled_ash_cal_parameters.offset,
               sizeof(cal_data.offset));
      }

      if (*flags & CalibrationTypeFlags::OVER_TEMP) {
        memcpy(cal_data.temp_sensitivity,
               recalled_ash_cal_parameters.tempSensitivity,
               sizeof(cal_data.temp_sensitivity));
        memcpy(cal_data.temp_intercept,
               recalled_ash_cal_parameters.tempIntercept,
               sizeof(cal_data.temp_intercept));
      }

      // Sets the algorithm's initial calibration data and notifies ASH to apply
      // the recalled calibration data.
      if (online_cal->SetInitialCalibration(cal_data)) {
        return NotifyAshCalibration(chreSensorType,
                                    online_cal->GetSensorCalibration(), *flags,
                                    sensor_tag);
      } else {
        NANO_CAL_LOGE(sensor_tag,
                      "Calibration data failed to initialize algorithm.");
      }
    }
  } else {
    // This is not necessarily an error since there may not be any previously
    // stored runtime calibration data to load yet (e.g., first device boot).
    NANO_CAL_LOGW(sensor_tag, "ASH did not recall calibration data.");
  }

  return false;
}

bool NanoSensorCal::DetectRuntimeCalibration(uint8_t chreSensorType,
                                             const char *sensor_tag,
                                             CalibrationTypeFlags *flags,
                                             ashCalParams *ash_cal_parameters) {
  // Analyzes calibration source flags to determine whether runtime
  // calibration values have been loaded and may be used for initialization. A
  // valid runtime calibration source will include at least an offset.
  *flags = CalibrationTypeFlags::NONE;  // Resets the calibration flags.

  // Uses the ASH calibration source flags to set the appropriate
  // CalibrationTypeFlags. These will be used to determine which values to copy
  // from 'ash_cal_parameters' and provide to the calibration algorithms for
  // initialization.
  bool runtime_cal_detected = false;
  if (ash_cal_parameters->offsetSource == ASH_CAL_PARAMS_SOURCE_RUNTIME &&
      ash_cal_parameters->offsetTempCelsiusSource ==
          ASH_CAL_PARAMS_SOURCE_RUNTIME) {
    runtime_cal_detected = true;
    *flags = CalibrationTypeFlags::BIAS;
  }

  if (ash_cal_parameters->tempSensitivitySource ==
          ASH_CAL_PARAMS_SOURCE_RUNTIME &&
      ash_cal_parameters->tempInterceptSource ==
          ASH_CAL_PARAMS_SOURCE_RUNTIME) {
    *flags |= CalibrationTypeFlags::OVER_TEMP;
  }

  if (runtime_cal_detected) {
    // Prints the retrieved runtime calibration data.
    NANO_CAL_LOGI(sensor_tag, "Runtime calibration data detected.");
    PrintAshCalParams(*ash_cal_parameters, sensor_tag);
  } else {
    // This is a warning (not an error) since the runtime algorithms will
    // function correctly with no recalled calibration values. They will
    // eventually trigger and update the system with valid calibration data.
    NANO_CAL_LOGW(sensor_tag, "No runtime offset calibration data found.");
  }

  return runtime_cal_detected;
}

// Helper functions for logging calibration information.
void NanoSensorCal::PrintAshCalParams(const ashCalParams &cal_params,
                                      const char *sensor_tag) {
  if (cal_params.offsetSource == ASH_CAL_PARAMS_SOURCE_RUNTIME) {
    NANO_CAL_LOGI(sensor_tag,
                  "Offset | Temperature [C]: %.6f, %.6f, %.6f | %.2f",
                  cal_params.offset[0], cal_params.offset[1],
                  cal_params.offset[2], cal_params.offsetTempCelsius);
  }

  if (cal_params.tempSensitivitySource == ASH_CAL_PARAMS_SOURCE_RUNTIME) {
    NANO_CAL_LOGI(sensor_tag, "Temp Sensitivity [units/C]: %.6f, %.6f, %.6f",
                  cal_params.tempSensitivity[0], cal_params.tempSensitivity[1],
                  cal_params.tempSensitivity[2]);
  }

  if (cal_params.tempInterceptSource == ASH_CAL_PARAMS_SOURCE_RUNTIME) {
    NANO_CAL_LOGI(sensor_tag, "Temp Intercept [units]: %.6f, %.6f, %.6f",
                  cal_params.tempIntercept[0], cal_params.tempIntercept[1],
                  cal_params.tempIntercept[2]);
  }

  if (cal_params.scaleFactorSource == ASH_CAL_PARAMS_SOURCE_RUNTIME) {
    NANO_CAL_LOGI(sensor_tag, "Scale Factor: %.6f, %.6f, %.6f",
                  cal_params.scaleFactor[0], cal_params.scaleFactor[1],
                  cal_params.scaleFactor[2]);
  }

  if (cal_params.crossAxisSource == ASH_CAL_PARAMS_SOURCE_RUNTIME) {
    NANO_CAL_LOGI(sensor_tag,
                  "Cross-Axis in [yx, zx, zy] order: %.6f, %.6f, %.6f",
                  cal_params.crossAxis[0], cal_params.crossAxis[1],
                  cal_params.crossAxis[2]);
  }
}

void NanoSensorCal::PrintCalibration(const CalibrationDataThreeAxis &cal_data,
                                     CalibrationTypeFlags flags,
                                     const char *sensor_tag) {
  if (flags & CalibrationTypeFlags::BIAS) {
    NANO_CAL_LOGI(sensor_tag,
                  "Offset | Temperature [C]: %.6f, %.6f, %.6f | %.2f",
                  cal_data.offset[0], cal_data.offset[1], cal_data.offset[2],
                  cal_data.offset_temp_celsius);
  }

  if (flags & CalibrationTypeFlags::OVER_TEMP) {
    NANO_CAL_LOGI(sensor_tag, "Temp Sensitivity: %.6f, %.6f, %.6f",
                  cal_data.temp_sensitivity[0], cal_data.temp_sensitivity[1],
                  cal_data.temp_sensitivity[2]);
    NANO_CAL_LOGI(sensor_tag, "Temp Intercept: %.6f, %.6f, %.6f",
                  cal_data.temp_intercept[0], cal_data.temp_intercept[1],
                  cal_data.temp_intercept[2]);
  }
}

bool NanoSensorCal::HandleGyroLogMessage(uint64_t timestamp_nanos) {
  // Limits the log messaging update rate for the gyro calibrations since
  // these can occur frequently with rapid temperature changes.
  const int64_t next_log_interval_nanos =
      (NANO_TIMER_CHECK_T1_GEQUAL_T2_PLUS_DELTA(
          timestamp_nanos, initialization_start_time_nanos_,
          MIN_TO_NANOS(kGyroscopeMessagePlan.duration_of_rapid_messages_min)))
          ? MIN_TO_NANOS(kGyroscopeMessagePlan.slow_message_interval_min)
          : SEC_TO_NANOS(kGyroscopeMessagePlan.rapid_message_interval_sec);

  const bool print_gyro_log = NANO_TIMER_CHECK_T1_GEQUAL_T2_PLUS_DELTA(
      timestamp_nanos, gyro_notification_time_nanos_, next_log_interval_nanos);

  if (print_gyro_log) {
    gyro_notification_time_nanos_ = timestamp_nanos;
    PrintCalibration(gyro_cal_->GetSensorCalibration(), gyro_cal_update_flags_,
                     kGyroTag);
  }

  return print_gyro_log;
}

}  // namespace nano_calibration
