/*
 *
 * Copyright (c) [2018] by InvenSense, Inc.
 * 
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

/** @defgroup Driver Driver
 *  @brief High-level functions to drive the device
 *  @{
 */

/** @file inv_imu_driver.h */

#ifndef _INV_IMU_DRIVER_H_
#define _INV_IMU_DRIVER_H_

#ifdef __cplusplus
extern "C" {
#endif

#include "imu/inv_imu_defs.h"
#include "imu/inv_imu_transport.h"

/** Max FSR values for accel */
#if INV_IMU_HFSR_SUPPORTED
#define ACCEL_CONFIG0_FS_SEL_MAX ACCEL_CONFIG0_FS_SEL_32g
#else
#define ACCEL_CONFIG0_FS_SEL_MAX ACCEL_CONFIG0_FS_SEL_16g
#endif
/** Max user offset value for accel (mg) */
#define ACCEL_OFFUSER_MAX_MG 1000
/** Accel start-up time */
#define ACC_STARTUP_TIME_US 10000

#if INV_IMU_IS_GYRO_SUPPORTED
/** Max FSR values for gyro */
#if INV_IMU_HFSR_SUPPORTED
#define GYRO_CONFIG0_FS_SEL_MAX GYRO_CONFIG0_FS_SEL_4000dps
#else
#define GYRO_CONFIG0_FS_SEL_MAX GYRO_CONFIG0_FS_SEL_2000dps
#endif
/** Max user offset value for gyro (dps) */
#define GYRO_OFFUSER_MAX_DPS 64
/** Gyro start-up time */
#define GYR_STARTUP_TIME_US 70000
#endif

/** Max buffer size mirrored from FIFO at polling time */
#define FIFO_MIRRORING_SIZE 16 * 258 // packet size * max_count = 4kB

/** Sensor identifier for UI control function */
enum inv_imu_sensor {
	INV_SENSOR_ACCEL, /**< Accelerometer */
#if INV_IMU_IS_GYRO_SUPPORTED
	INV_SENSOR_GYRO, /**< Gyroscope */
	INV_SENSOR_FSYNC_EVENT, /**< FSYNC */
#else
	INV_SENSOR_RESERVED1,
	INV_SENSOR_RESERVED2,
#endif
	INV_SENSOR_TEMPERATURE, /**< Chip temperature */
	INV_SENSOR_DMP_PEDOMETER_EVENT, /**< Pedometer: step detected */
	INV_SENSOR_DMP_PEDOMETER_COUNT, /**< Pedometer: step counter */
	INV_SENSOR_DMP_TILT, /**< Tilt */
	INV_SENSOR_DMP_FF, /**< FreeFall */
	INV_SENSOR_DMP_LOWG, /**< Low G */
	INV_SENSOR_DMP_SMD, /**< Significant Motion Detection */
	INV_SENSOR_MAX
};

/** Configure FIFO usage */
typedef enum {
	INV_IMU_FIFO_DISABLED = 0, /**< FIFO is disabled and data source is sensors registers */
	INV_IMU_FIFO_ENABLED  = 1, /**< FIFO is used as data source */
} INV_IMU_FIFO_CONFIG_t;

/** Sensor event structure definition */
typedef struct {
	int      sensor_mask;
	uint16_t timestamp_fsync;
	int16_t  accel[3];
#if INV_IMU_IS_GYRO_SUPPORTED
	int16_t gyro[3];
#endif
	int16_t temperature;
	int8_t  accel_high_res[3];
#if INV_IMU_IS_GYRO_SUPPORTED
	int8_t gyro_high_res[3];
#endif
} inv_imu_sensor_event_t;

/** IMU driver states definition */
typedef struct inv_imu_device {
	/** Transport layer. 
	 *  @warning Must be the first one of inv_imu_device_t 
	 */
	inv_imu_transport_t transport;

	/** callback executed by:
	 *  * inv_imu_get_data_from_fifo (if FIFO is used).
	 *  * inv_imu_get_data_from_registers (if FIFO isn't used).
	 *  May be NULL if above API are not used by application 
	 */
	void (*sensor_event_cb)(inv_imu_sensor_event_t *event);

	uint8_t               fifo_data[FIFO_MIRRORING_SIZE]; /**< FIFO mirroring memory area */
	uint8_t               dmp_is_on; /**< DMP started status */
	uint8_t               endianness_data; /**< Data endianness configuration */
	uint8_t               fifo_highres_enabled; /**< Highres mode configuration */
	INV_IMU_FIFO_CONFIG_t fifo_is_used; /**< FIFO configuration */
#if INV_IMU_IS_GYRO_SUPPORTED
	uint64_t gyro_start_time_us; /**< Gyro start time to discard first samples */
#endif
	uint64_t accel_start_time_us; /**< Accel start time to discard first samples */
} inv_imu_device_t;

/** Interrupt enum state for INT1, INT2, and IBI */
typedef enum { INV_IMU_DISABLE = 0, INV_IMU_ENABLE } inv_imu_interrupt_value;

/** Interrupt definition */
typedef struct {
	inv_imu_interrupt_value INV_UI_FSYNC;
	inv_imu_interrupt_value INV_UI_DRDY;
	inv_imu_interrupt_value INV_FIFO_THS;
	inv_imu_interrupt_value INV_FIFO_FULL;
	inv_imu_interrupt_value INV_SMD;
	inv_imu_interrupt_value INV_WOM_X;
	inv_imu_interrupt_value INV_WOM_Y;
	inv_imu_interrupt_value INV_WOM_Z;
	inv_imu_interrupt_value INV_FF;
	inv_imu_interrupt_value INV_LOWG;
	inv_imu_interrupt_value INV_STEP_DET;
	inv_imu_interrupt_value INV_STEP_CNT_OVFL;
	inv_imu_interrupt_value INV_TILT_DET;
} inv_imu_interrupt_parameter_t;

/** INT1 pin configuration */
typedef struct {
	INT_CONFIG_INT1_POLARITY_t      int_polarity;
	INT_CONFIG_INT1_MODE_t          int_mode;
	INT_CONFIG_INT1_DRIVE_CIRCUIT_t int_drive;
} inv_imu_int1_pin_config_t;

/** INT2 pin configuration */
typedef struct {
	INT_CONFIG_INT2_POLARITY_t      int_polarity;
	INT_CONFIG_INT2_MODE_t          int_mode;
	INT_CONFIG_INT2_DRIVE_CIRCUIT_t int_drive;
} inv_imu_int2_pin_config_t;

/** @brief Initializes device.
 *  @param[in] s                Pointer to device.
 *  @param[in] serif            Pointer on serial interface structure.
 *  @param[in] sensor_event_cb  Callback executed when a new sensor event is available. *
 *  @return                     0 on success, negative value on error.
 */
int inv_imu_init(inv_imu_device_t *s, const struct inv_imu_serif *serif,
                 void (*sensor_event_cb)(inv_imu_sensor_event_t *event));

/** @brief Reset device by reloading OTPs.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_device_reset(inv_imu_device_t *s);

/** @brief return WHOAMI value.
 *  @param[in] s          Pointer to device.
 *  @param[out] who_am_i  WHOAMI for device.
 *  @return 0 on success, negative value on error.
 */
int inv_imu_get_who_am_i(inv_imu_device_t *s, uint8_t *who_am_i);

/** @brief Enable/put accel in low power mode.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_enable_accel_low_power_mode(inv_imu_device_t *s);

/** @brief Enable/put accel in low noise mode.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_enable_accel_low_noise_mode(inv_imu_device_t *s);

/** @brief Disable all 3 axes of accel.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_disable_accel(inv_imu_device_t *s);

/** @brief Configure accel Output Data Rate.
 *  @param[in] s          Pointer to device.
 *  @param[in] frequency  The requested frequency.
 *  @return               0 on success, negative value on error.
 */
int inv_imu_set_accel_frequency(inv_imu_device_t *s, const ACCEL_CONFIG0_ODR_t frequency);

/** @brief Set accel Low-Power averaging value.
 *  @param[in] s        Pointer to device.
 *  @param[in] acc_avg  Requested averaging value.
 *  @return             0 on success, negative value on error.
 */
int inv_imu_set_accel_lp_avg(inv_imu_device_t *s, ACCEL_CONFIG1_ACCEL_FILT_AVG_t acc_avg);

/** @brief Set accel Low-Noise bandwidth value.
 *  @param[in] s       Pointer to device.
 *  @param[in] acc_bw  Requested averaging value.
 *  @return            0 on success, negative value on error.
 */
int inv_imu_set_accel_ln_bw(inv_imu_device_t *s, ACCEL_CONFIG1_ACCEL_FILT_BW_t acc_bw);

/** @brief Set accel full scale range.
 *  @param[in] s            Pointer to device.
 *  @param[in] accel_fsr_g  Requested full scale range.
 *  @return                 0 on success, negative value on error.
 */
int inv_imu_set_accel_fsr(inv_imu_device_t *s, ACCEL_CONFIG0_FS_SEL_t accel_fsr_g);

/** @brief Access accel full scale range.
 *  @param[in] s             Pointer to device.
 *  @param[out] accel_fsr_g  Current full scale range.
 *  @return 0 on success, negative value on error.
 */
int inv_imu_get_accel_fsr(inv_imu_device_t *s, ACCEL_CONFIG0_FS_SEL_t *accel_fsr_g);

#if INV_IMU_IS_GYRO_SUPPORTED
/** @brief Enable/put gyro in low noise mode.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_enable_gyro_low_noise_mode(inv_imu_device_t *s);

/** @brief Disable all 3 axes of gyro.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_disable_gyro(inv_imu_device_t *s);

/** @brief Configure gyro Output Data Rate.
 *  @param[in] s          Pointer to device.
 *  @param[in] frequency  The requested frequency.
 *  @return               0 on success, negative value on error.
 */
int inv_imu_set_gyro_frequency(inv_imu_device_t *s, const GYRO_CONFIG0_ODR_t frequency);

/** @brief Set gyro Low-Noise bandwidth value.
 *  @param[in] s       Pointer to device.
 *  @param[in] gyr_bw  Requested averaging value.
 *  @return            0 on success, negative value on error.
 */
int inv_imu_set_gyro_ln_bw(inv_imu_device_t *s, GYRO_CONFIG1_GYRO_FILT_BW_t gyr_bw);

/** @brief Set gyro full scale range.
 *  @param[in] s             Pointer to device.
 *  @param[in] gyro_fsr_dps  Requested full scale range.
*   @return                  0 on success, negative value on error.
 */
int inv_imu_set_gyro_fsr(inv_imu_device_t *s, GYRO_CONFIG0_FS_SEL_t gyro_fsr_dps);

/** @brief Access gyro full scale range.
 *  @param[in] s              Pointer to device.
 *  @param[out] gyro_fsr_dps  Current full scale range.
 *  @return                   0 on success, negative value on error.
 */
int inv_imu_get_gyro_fsr(inv_imu_device_t *s, GYRO_CONFIG0_FS_SEL_t *gyro_fsr_dps);

/** @brief Enable fsync tagging functionality.
 *         * Enables fsync.
 *         * Enables timestamp to registers. Once fsync is enabled fsync counter is pushed to 
 *           FIFO instead of timestamp. So timestamp is made available in registers. Note that 
 *           this increase power consumption.
 *         * Enables fsync related interrupt.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_enable_fsync(inv_imu_device_t *s);

/** @brief Disable fsync tagging functionality.
 *         * Disables fsync.
 *         * Disables timestamp to registers. Once fsync is disabled  timestamp is pushed to FIFO 
 *           instead of fsync counter. So in order to decrease power consumption, timestamp is no 
 *           more available in registers.
 *         * Disables fsync related interrupt.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_disable_fsync(inv_imu_device_t *s);
#endif

/** @brief Configure SPI slew-rate.
 *  @param[in] s          Pointer to device.
 *  @param[in] slew_rate  Requested slew-rate.
 *  @return               0 on success, negative value on error.
 */
int inv_imu_set_spi_slew_rate(inv_imu_device_t *s, const DRIVE_CONFIG3_SPI_SLEW_RATE_t slew_rate);

/** @brief Configure INT1 pin behavior.
 *  @param[in] s     Pointer to device.
 *  @param[in] conf  Structure with the requested configuration.
 *  @return          0 on success, negative value on error.
 */
int inv_imu_set_pin_config_int1(inv_imu_device_t *s, const inv_imu_int1_pin_config_t *conf);

/** @brief Configure INT2 pin behavior.
 *  @param[in] s     Pointer to device.
 *  @param[in] conf  Structure with the requested configuration.
 *  @return          0 on success, negative value on error.
 */
int inv_imu_set_pin_config_int2(inv_imu_device_t *s, const inv_imu_int2_pin_config_t *conf);

/** @brief Configure which interrupt source can trigger INT1.
 *  @param[in] s   Pointer to device.
 *  @param[in] it  Structure with the corresponding state to manage INT1.
 *  @return        0 on success, negative value on error.
 */
int inv_imu_set_config_int1(inv_imu_device_t *s, const inv_imu_interrupt_parameter_t *it);

/** @brief Retrieve interrupts configuration.
 *  @param[in] s    Pointer to device.
 *  @param[out] it  Structure with the corresponding state to manage INT1.
 *  @return         0 on success, negative value on error.
 */
int inv_imu_get_config_int1(inv_imu_device_t *s, inv_imu_interrupt_parameter_t *it);

/** @brief  Configure which interrupt source can trigger INT2.
 *  @param[in] s   Pointer to device.
 *  @param[in] it  Structure with the corresponding state to INT2.
 *  @return        0 on success, negative value on error.
 */
int inv_imu_set_config_int2(inv_imu_device_t *s, const inv_imu_interrupt_parameter_t *it);

/** @brief  Retrieve interrupts configuration.
 *  @param[in] s    Pointer to device.
 *  @param[out] it  Structure with the corresponding state to manage INT2.
 *  @return         0 on success, negative value on error.
 */
int inv_imu_get_config_int2(inv_imu_device_t *s, inv_imu_interrupt_parameter_t *it);

/** @brief Read all data registers. 
 *         Then it calls sensor_event_cb function passed at init for each packet.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_get_data_from_registers(inv_imu_device_t *s);

/** @brief Read FIFO frame count. 
 *  @param[in] s             Pointer to device.
 *  @param[out] frame_count  Number of frame available in the FIFO.
 *  @return                  0 on success, negative value on error.
 */
int inv_imu_get_frame_count(inv_imu_device_t *s, uint16_t *frame_count);

/** @brief Decode FIFO frame. 
 *  @param[in] s       Pointer to device.
 *  @param[in] frame   FIFO frame data.
 *  @param[out] event  Data content coded as an event.
 *  @return            0 on success, negative value on error.
 */
int inv_imu_decode_fifo_frame(inv_imu_device_t *s, const uint8_t *frame,
                              inv_imu_sensor_event_t *event);

/** @brief Read all available packets from the FIFO. 
 *         For each packet function builds a sensor event containing packet data 
 *         and validity information. Then it calls sensor_event_cb function passed 
 *         at init for each packet.
 *  @param[in] s  Pointer to device.
 *  @return       Number of valid packets read on success, negative value on error.
 */
int inv_imu_get_data_from_fifo(inv_imu_device_t *s);

/** @brief Converts ODR's enum to period in us.
 *  @param[in] odr_bitfield ACCEL_CONFIG0_ODR_t or GYRO_CONFIG0_ODR_t enum.
 *  @return    The corresponding period in us.
 */
uint32_t inv_imu_convert_odr_bitfield_to_us(uint32_t odr_bitfield);

/** @brief Set timestamp resolution.
 *  @param[in] s                Pointer to device.
 *  @param[in] timestamp_resol  Requested timestamp resolution.
 *  @return                     0 on success, negative value on error.
 */
int inv_imu_set_timestamp_resolution(inv_imu_device_t *         s,
                                     const TMST_CONFIG1_RESOL_t timestamp_resol);

/** @brief Reset IMU FIFO.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_reset_fifo(inv_imu_device_t *s);

/** @brief Enable 20 bits raw data in FIFO.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_enable_high_resolution_fifo(inv_imu_device_t *s);

/** @brief Disable 20 bits raw data in FIFO.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_disable_high_resolution_fifo(inv_imu_device_t *s);

/** @brief Configure FIFO.
  *  @param[in] s            Pointer to device.
  *  @param[in] fifo_config  FIFO configuration method.
  *                          Enabled: data are pushed to FIFO and FIFO THS interrupt is set.
  *                          Disabled: data are not pushed to FIFO and DRDY interrupt is set.
  *  @return                 0 on success, negative value on error.
  */
int inv_imu_configure_fifo(inv_imu_device_t *s, INV_IMU_FIFO_CONFIG_t fifo_config);

/** @brief Get timestamp resolution
 *  @param[in] s  Pointer to device.
 *  @return       The timestamp resolution in us or 0 in case of error
 */
uint32_t inv_imu_get_timestamp_resolution_us(inv_imu_device_t *s);

/** @brief  Enable Wake On Motion.
 *  @param[in] s         Pointer to device.
 *  @param[in] wom_x_th  Threshold value for the Wake on Motion Interrupt for X-axis accel.
 *  @param[in] wom_y_th  Threshold value for the Wake on Motion Interrupt for Y-axis accel.
 *  @param[in] wom_z_th  Threshold value for the Wake on Motion Interrupt for Z-axis accel.
 *  @param[in] wom_int   Select which mode between AND/OR is used to generate interrupt.
 *  @param[in] wom_dur   Select the number of over-threshold events to wait before generating
 *                       interrupt.
 *  @return              0 on success, negative value on error.
 */
int inv_imu_configure_wom(inv_imu_device_t *s, const uint8_t wom_x_th, const uint8_t wom_y_th,
                          const uint8_t wom_z_th, WOM_CONFIG_WOM_INT_MODE_t wom_int,
                          WOM_CONFIG_WOM_INT_DUR_t wom_dur);

/** @brief Enable Wake On Motion.
 *         WoM requests to have the accelerometer enabled to work. 
 *         As a consequence FIFO water-mark interrupt is disabled to only trigger WoM interrupts.
 *         To have good performance, it's recommended to set accel ODR to 20 ms and in Low Power
 *         Mode. 
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_enable_wom(inv_imu_device_t *s);

/** @brief Disable Wake On Motion.
 *         Fifo water-mark interrupt is re-enabled when WoM is disabled.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_disable_wom(inv_imu_device_t *s);

/** @brief Start DMP for APEX algorithms and self-test.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_start_dmp(inv_imu_device_t *s);

/** @brief Resume DMP operations.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_resume_dmp(struct inv_imu_device *s);

/** @brief Reset DMP for APEX algorithms and self-test.
 *  @param[in] s           Pointer to device.
 *  @param[in] sram_reset  Reset mode for the SRAM.
 *  @return                0 on success, negative value on error.
 */
int inv_imu_reset_dmp(inv_imu_device_t *s, const APEX_CONFIG0_DMP_MEM_RESET_t sram_reset);

/** @brief Set the UI endianness and set the inv_device endianness field.
 *  @param[in] s           Pointer to device.
 *  @param[in] endianness  Endianness to be set.
 *  @return                0 on success, negative value on error.
 */
int inv_imu_set_endianness(inv_imu_device_t *s, INTF_CONFIG0_DATA_ENDIAN_t endianness);

/** @brief Read the UI endianness and set the inv_device endianness field.
 *  @param[in] s  Pointer to device.
 *  @return       0 on success, negative value on error.
 */
int inv_imu_get_endianness(inv_imu_device_t *s);

/** @brief Configure FIFO decimation.
 *  @param[in] s           Pointer to device.
 *  @param[in] dec_factor  Requested decimation factor value from 2 to 256.
 *  @return                0 on success, negative value on error.
 */
int inv_imu_configure_fifo_data_rate(inv_imu_device_t *s, FDR_CONFIG_FDR_SEL_t dec_factor);

/** @brief Return driver version x.y.z-suffix as a char array
 *  @return driver version a char array "x.y.z-suffix"
 */
const char *inv_imu_get_version(void);

/** @brief Converts two bytes in one unsigned half-word depending on endianness 
 *  @param[in] endianness  IMU's endianness.
 *  @param[in]  in         Pointer to input.
 *  @param[out] out        Pointer to output.
 */
static inline void format_u16_data(uint8_t endianness, const uint8_t *in, uint16_t *out)
{
	*out = (uint16_t)(endianness == INTF_CONFIG0_DATA_BIG_ENDIAN ? (in[0] << 8) | in[1] :
                                                                   (in[1] << 8) | in[0]);
}

/** @brief Converts two bytes in one signed half-word depending on endianness 
 *  @param[in] endianness  IMU's endianness.
 *  @param[in]  in         Pointer to input.
 *  @param[out] out        Pointer to output.
 */
static inline void format_s16_data(uint8_t endianness, const uint8_t *in, int16_t *out)
{
	*out = (int16_t)(endianness == INTF_CONFIG0_DATA_BIG_ENDIAN ? (in[0] << 8) | in[1] :
                                                                  (in[1] << 8) | in[0]);
}

#if INV_IMU_HFSR_SUPPORTED
/** @brief  Write 'size' bytes pointed by 'data' in SRAM at offset given in parameters.
 *  @param[in] data    pointer to data to be written in SRAM
 *  @param[in] offset  offset in bytes from SRAM start address where data should be written
 *  @param[in] size    number of bytes to write
 *  @return            0 in case of success, negative value on error.
 */
int inv_imu_write_sram(struct inv_imu_device *s, const uint8_t *data, uint32_t offset,
                       uint32_t size);
#endif

#ifdef __cplusplus
}
#endif

#endif /* _INV_IMU_DRIVER_H_ */

/** @} */
