/*
 * Copyright (C) 2016 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 <stdlib.h>
#include <string.h>
#include <timer.h>
#include <heap.h>
#include <plat/rtc.h>
#include <plat/syscfg.h>
#include <hostIntf.h>
#include <nanohubPacket.h>
#include <floatRt.h>

#include <seos.h>

#include <nanohub_math.h>
#include <algos/fusion.h>
#include <sensors.h>
#include <variant/sensType.h>
#include <limits.h>
#include <slab.h>

#define ORIENTATION_APP_VERSION 1

#define MAX_NUM_COMMS_EVENT_SAMPLES 15 // at most 15 samples can fit in one comms_event
#define NUM_COMMS_EVENTS_IN_FIFO    2  // This controls how often the hub needs to wake up
                                       // in batching

// needs to be greater than max raw sensor rate ratio
#define FIFO_DEPTH                  (NUM_COMMS_EVENTS_IN_FIFO * MAX_NUM_COMMS_EVENT_SAMPLES)

/*
 * FIFO_MARGIN: max raw sensor rate ratio is 8:1.
 * If 2 batchs of high rate data comes before 1 low rate data, there can be at max 15 samples left
 * in the FIFO
 */
#define FIFO_MARGIN                 15
#define MAX_NUM_SAMPLES             (FIFO_MARGIN + FIFO_DEPTH) // actual input sample fifo depth
#define EVT_SENSOR_ACC_DATA_RDY     sensorGetMyEventType(SENS_TYPE_ACCEL)
#define EVT_SENSOR_GYR_DATA_RDY     sensorGetMyEventType(SENS_TYPE_GYRO)
#define EVT_SENSOR_MAG_DATA_RDY     sensorGetMyEventType(SENS_TYPE_MAG)
#define EVT_SENSOR_MAG_BIAS         sensorGetMyEventType(SENS_TYPE_MAG_BIAS)

#define kGravityEarth               9.80665f
#define kRad2deg                    (180.0f / M_PI)
#define MIN_GYRO_RATE_HZ            SENSOR_HZ(100.0f)
#define MAX_MAG_RATE_HZ             SENSOR_HZ(50.0f)

enum
{
    FUSION_FLAG_ENABLED             = 0x01,
    FUSION_FLAG_INITIALIZED         = 0x08,
    FUSION_FLAG_GAME_ENABLED        = 0x10,
    FUSION_FLAG_GAME_INITIALIZED    = 0x20
};

enum RawSensorType
{
    ACC,
    GYR,
    MAG,
    NUM_OF_RAW_SENSOR
};

enum FusionSensorType
{
    ORIENT,
    GRAVITY,
    GEOMAG,
    LINEAR,
    GAME,
    ROTAT,
    NUM_OF_FUSION_SENSOR
};


struct FusionSensorSample {
    uint64_t time;
    float x, y, z;
};

struct FusionSensor {
    uint32_t handle;
    struct TripleAxisDataEvent *ev;
    uint64_t prev_time;
    uint64_t latency;
    uint32_t rate;
    bool active;
    bool use_gyro_data;
    bool use_mag_data;
    uint8_t idx;
};

struct FusionTask {
    uint32_t tid;
    uint32_t accelHandle;
    uint32_t gyroHandle;
    uint32_t magHandle;

    struct Fusion fusion;
    struct Fusion game;

    struct FusionSensor sensors[NUM_OF_FUSION_SENSOR];
    struct FusionSensorSample samples[NUM_OF_RAW_SENSOR][MAX_NUM_SAMPLES];
    size_t sample_indices[NUM_OF_RAW_SENSOR];
    size_t sample_counts[NUM_OF_RAW_SENSOR];
    uint32_t counters[NUM_OF_RAW_SENSOR];
    uint64_t ResamplePeriodNs[NUM_OF_RAW_SENSOR];
    uint64_t last_time[NUM_OF_RAW_SENSOR];
    struct TripleAxisDataPoint last_sample[NUM_OF_RAW_SENSOR];

    uint32_t flags;

    uint32_t raw_sensor_rate[NUM_OF_RAW_SENSOR];
    uint64_t raw_sensor_latency;

    uint8_t accel_client_cnt;
    uint8_t gyro_client_cnt;
    uint8_t mag_client_cnt;
};

static uint32_t FusionRates[] = {
    SENSOR_HZ(12.5f),
    SENSOR_HZ(25.0f),
    SENSOR_HZ(50.0f),
    SENSOR_HZ(100.0f),
    SENSOR_HZ(200.0f),
    0,
};

//should match "supported rates in length" and be the timer length for that rate in nanosecs
static const uint64_t rateTimerVals[] = {
    1000000000ULL / 12.5f,
    1000000000ULL / 25,
    1000000000ULL / 50,
    1000000000ULL / 100,
    1000000000ULL / 200,
};

static struct FusionTask mTask;

#define DEC_INFO_RATE(name, rates, type, axis, inter, samples) \
    .sensorName = name, \
    .supportedRates = rates, \
    .sensorType = type, \
    .numAxis = axis, \
    .interrupt = inter, \
    .minSamples = samples

static const struct SensorInfo mSi[NUM_OF_FUSION_SENSOR] =
{
    { DEC_INFO_RATE("Orientation", FusionRates, SENS_TYPE_ORIENTATION, NUM_AXIS_THREE,
            NANOHUB_INT_NONWAKEUP, 20) },
    { DEC_INFO_RATE("Gravity", FusionRates, SENS_TYPE_GRAVITY, NUM_AXIS_THREE,
            NANOHUB_INT_NONWAKEUP, 20) },
    { DEC_INFO_RATE("Geomagnetic Rotation Vector", FusionRates, SENS_TYPE_GEO_MAG_ROT_VEC,
            NUM_AXIS_THREE, NANOHUB_INT_NONWAKEUP, 20) },
    { DEC_INFO_RATE("Linear Acceleration", FusionRates, SENS_TYPE_LINEAR_ACCEL, NUM_AXIS_THREE,
            NANOHUB_INT_NONWAKEUP, 20) },
    { DEC_INFO_RATE("Game Rotation Vector", FusionRates, SENS_TYPE_GAME_ROT_VECTOR, NUM_AXIS_THREE,
            NANOHUB_INT_NONWAKEUP, 300) },
    { DEC_INFO_RATE("Rotation Vector", FusionRates, SENS_TYPE_ROTATION_VECTOR, NUM_AXIS_THREE,
            NANOHUB_INT_NONWAKEUP, 20) },
};

static struct SlabAllocator *mDataSlab;

static void dataEvtFree(void *ptr)
{
    slabAllocatorFree(mDataSlab, ptr);
}

static void fillSamples(struct TripleAxisDataEvent *ev, enum RawSensorType index)
{
    bool bad_timestamp;
    size_t i, w, n, num_samples;
    struct TripleAxisDataPoint *curr_sample, *next_sample;
    uint32_t counter;
    uint64_t ResamplePeriodNs, curr_time, next_time;
    uint64_t sample_spacing_ns;
    float weight_next;

    if (index == GYR && mTask.gyro_client_cnt == 0) {
        return;
    }
    if (index == MAG && mTask.mag_client_cnt == 0) {
        return;
    }

    n = mTask.sample_counts[index];
    i = mTask.sample_indices[index];
    counter = mTask.counters[index];
    ResamplePeriodNs = mTask.ResamplePeriodNs[index];
    w = (mTask.sample_indices[index] + n) % MAX_NUM_SAMPLES;

    // check if this sensor was used before
    if (mTask.last_time[index] == ULONG_LONG_MAX) {
        curr_sample = ev->samples;
        next_sample = curr_sample + 1;
        num_samples = ev->samples[0].firstSample.numSamples;
        curr_time = ev->referenceTime;
    } else {
        curr_sample = &mTask.last_sample[index];
        next_sample = ev->samples;
        num_samples = ev->samples[0].firstSample.numSamples + 1;
        curr_time = mTask.last_time[index];
    }

    while (num_samples > 1) {

        if (next_sample == ev->samples)
            next_time = ev->referenceTime;
        else
            next_time = curr_time + next_sample->deltaTime;

        // error handling for non-chronological accel timestamps
        sample_spacing_ns = (next_time > curr_time) ?  (next_time - curr_time) : 0;

        // This can happen during sensor config changes
        bad_timestamp = (sample_spacing_ns > 10 * ResamplePeriodNs);

        // Check to see if we need to move the interpolation window or
        // interpolate
        if ((counter >= sample_spacing_ns) || bad_timestamp) {
            num_samples--;
            counter -= (bad_timestamp ? counter : sample_spacing_ns);
            curr_sample = next_sample;
            next_sample++;

            curr_time = next_time;
        } else {
            weight_next = (float)counter / floatFromUint64(sample_spacing_ns);

            mTask.samples[index][w].x = curr_sample->x + weight_next *
                (next_sample->x - curr_sample->x);
            mTask.samples[index][w].y = curr_sample->y + weight_next *
                (next_sample->y - curr_sample->y);
            mTask.samples[index][w].z = curr_sample->z + weight_next *
                (next_sample->z - curr_sample->z);
            mTask.samples[index][w].time = curr_time + counter;

            // Move the read index when buffer is full
            if (++n > MAX_NUM_SAMPLES) {
                n = MAX_NUM_SAMPLES;

                if (++i == MAX_NUM_SAMPLES) {
                    i = 0;
                }
            }

            // Reset the write index
            if (++w == MAX_NUM_SAMPLES) {
                w = 0;
            }

            // Move to the next resample
            counter += ResamplePeriodNs;
        }
    }

    mTask.sample_counts[index] = n;
    mTask.sample_indices[index] = i;
    mTask.counters[index] = counter;
    mTask.last_sample[index] = *curr_sample;
    mTask.last_time[index] = curr_time;
}

static bool allocateDataEvt(struct FusionSensor *mSensor, uint64_t time)
{
    mSensor->ev = slabAllocatorAlloc(mDataSlab);
    if (mSensor->ev == NULL) {
        // slab allocation failed, need to stop draining raw samples for now.
        osLog(LOG_INFO, "ORIENTATION: slabAllocatorAlloc() Failed\n");
        return false;
    }

    // delta time for the first sample is sample count
    memset(&mSensor->ev->samples[0].firstSample, 0x00, sizeof(struct SensorFirstSample));
    mSensor->ev->referenceTime = time;
    mSensor->prev_time = time;

    return true;
}

// returns false if addSample() fails
static bool addSample(struct FusionSensor *mSensor, uint64_t time, float x, float y, float z)
{
    struct TripleAxisDataPoint *sample;

    // Bypass processing this accel sample.
    // This is needed after recovering from a slab shortage.
    if (mSensor->prev_time == time) {
        osLog(LOG_INFO, "Accel sample has been processed by fusion sensor %d\n",
              mSensor->idx);
        return true;
    }

    if (mSensor->ev == NULL) {
        if (!allocateDataEvt(mSensor, time))
            return false;
    }

    if (mSensor->ev->samples[0].firstSample.numSamples >= MAX_NUM_COMMS_EVENT_SAMPLES) {
        osLog(LOG_ERROR, "ORIENTATION: BAD_INDEX\n");
        return false;
    }

    sample = &mSensor->ev->samples[mSensor->ev->samples[0].firstSample.numSamples++];

    if (mSensor->ev->samples[0].firstSample.numSamples > 1) {
        sample->deltaTime = time > mSensor->prev_time ? (time - mSensor->prev_time) : 0;
        mSensor->prev_time = time;
    }

    sample->x = x;
    sample->y = y;
    sample->z = z;

    if (mSensor->ev->samples[0].firstSample.numSamples == MAX_NUM_COMMS_EVENT_SAMPLES) {
        osEnqueueEvtOrFree(
                EVENT_TYPE_BIT_DISCARDABLE | sensorGetMyEventType(mSi[mSensor->idx].sensorType),
                mSensor->ev, dataEvtFree);
        mSensor->ev = NULL;
    }
    return true;
}

// returns false if addSample fails for any fusion sensor
// (most likely due to slab allocation failure)
static bool updateOutput(ssize_t last_accel_sample_index, uint64_t last_sensor_time)
{
    struct Vec4 attitude;
    struct Vec3 g, a;
    struct Mat33 R;  // direction-cosine/rotation matrix, inertial -> device
    bool   rInited;  // indicates if matrix R has been initialzed. for avoiding repeated computation
    bool ret = true;

    if (fusionHasEstimate(&mTask.game)) {
        rInited = false;
        if (mTask.sensors[GAME].active) {
            fusionGetAttitude(&mTask.game, &attitude);
            if (!addSample(&mTask.sensors[GAME],
                    last_sensor_time,
                    attitude.x,
                    attitude.y,
                    attitude.z)) {
                ret = false;
            }
        }

        if (mTask.sensors[GRAVITY].active) {
            fusionGetRotationMatrix(&mTask.game, &R);
            rInited = true;
            initVec3(&g, R.elem[0][2], R.elem[1][2], R.elem[2][2]);
            vec3ScalarMul(&g, kGravityEarth);
            if (!addSample(&mTask.sensors[GRAVITY],
                    last_sensor_time,
                    g.x,
                    g.y,
                    g.z)) {
                ret = false;
            }
        }

        if (last_accel_sample_index >= 0
                && mTask.sensors[LINEAR].active) {
            if (!rInited) {
                fusionGetRotationMatrix(&mTask.game, &R);
            }
            initVec3(&g, R.elem[0][2], R.elem[1][2], R.elem[2][2]);
            vec3ScalarMul(&g, kGravityEarth);
            initVec3(&a,
                    mTask.samples[0][last_accel_sample_index].x,
                    mTask.samples[0][last_accel_sample_index].y,
                    mTask.samples[0][last_accel_sample_index].z);

            if (!addSample(&mTask.sensors[LINEAR],
                    mTask.samples[0][last_accel_sample_index].time,
                    a.x - g.x,
                    a.y - g.y,
                    a.z - g.z)) {
                ret = false;
            }
        }
    }

    if (fusionHasEstimate(&mTask.fusion)) {
        fusionGetAttitude(&mTask.fusion, &attitude);

        if (mTask.sensors[ORIENT].active) {
            fusionGetRotationMatrix(&mTask.fusion, &R);
            // x, y, z = yaw, pitch, roll
            float x = atan2f(-R.elem[0][1], R.elem[0][0]) * kRad2deg;
            float y = atan2f(-R.elem[1][2], R.elem[2][2]) * kRad2deg;
            float z = asinf(R.elem[0][2]) * kRad2deg;

            if (x < 0.0f) {
                x += 360.0f;
            }

            if (!addSample(&mTask.sensors[ORIENT],
                    last_sensor_time,
                    x,
                    y,
                    z)) {
                ret = false;
            }
        }

        if (mTask.sensors[GEOMAG].active) {
            if (!addSample(&mTask.sensors[GEOMAG],
                    last_sensor_time,
                    attitude.x,
                    attitude.y,
                    attitude.z)) {
                ret = false;
            }
        }

        if (mTask.sensors[ROTAT].active) {
            if (!addSample(&mTask.sensors[ROTAT],
                    last_sensor_time,
                    attitude.x,
                    attitude.y,
                    attitude.z)) {
                ret = false;
            }
        }

    }
    return ret;
}

static void drainSamples()
{
    struct Vec3 a, w, m;
    uint64_t a_time, g_time, m_time;
    size_t i = mTask.sample_indices[ACC];
    size_t j = 0;
    size_t k = 0;
    size_t which;
    float dT;
    bool success = true;

    if (mTask.gyro_client_cnt > 0)
        j = mTask.sample_indices[GYR];

    if (mTask.mag_client_cnt > 0)
        k = mTask.sample_indices[MAG];

    // Keep draining raw samples and producing fusion samples only if
    // 1) all raw sensors needed are present (to compare timestamp) and
    // 2) updateOutput() succeeded (no slab shortage)
    // Otherwise, wait till next raw sample event.
    while (mTask.sample_counts[ACC] > 0
            && (!(mTask.gyro_client_cnt > 0) || mTask.sample_counts[GYR] > 0)
            && (!(mTask.mag_client_cnt > 0) || mTask.sample_counts[MAG] > 0)
            && success) {
        a_time = mTask.samples[ACC][i].time;
        g_time = mTask.gyro_client_cnt > 0 ? mTask.samples[GYR][j].time
                            : ULONG_LONG_MAX;
        m_time = mTask.mag_client_cnt > 0 ? mTask.samples[MAG][k].time
                            : ULONG_LONG_MAX;

        // priority with same timestamp: gyro > acc > mag
        if (g_time <= a_time && g_time <= m_time) {
            which = GYR;
        } else if (a_time <= m_time) {
            which = ACC;
        } else {
            which = MAG;
        }

        dT = floatFromUint64(mTask.ResamplePeriodNs[which]) * 1e-9f;
        switch (which) {
        case ACC:
            initVec3(&a, mTask.samples[ACC][i].x, mTask.samples[ACC][i].y, mTask.samples[ACC][i].z);

            if (mTask.flags & FUSION_FLAG_ENABLED)
                fusionHandleAcc(&mTask.fusion, &a, dT);

            if (mTask.flags & FUSION_FLAG_GAME_ENABLED)
                fusionHandleAcc(&mTask.game, &a, dT);

            success = updateOutput(i, mTask.samples[ACC][i].time);

            // Do not remove the accel sample until all active fusion sesnsors
            // successfully updated the output.
            // Fusion sensors that have processed this accel sample will bypass
            // it in addSample().
            if (success) {
                --mTask.sample_counts[ACC];
                if (++i == MAX_NUM_SAMPLES) {
                    i = 0;
                }
            }
            break;
        case GYR:
            initVec3(&w, mTask.samples[GYR][j].x, mTask.samples[GYR][j].y, mTask.samples[GYR][j].z);

            if (mTask.flags & FUSION_FLAG_ENABLED)
                fusionHandleGyro(&mTask.fusion, &w, dT);

            if (mTask.flags & FUSION_FLAG_GAME_ENABLED)
                fusionHandleGyro(&mTask.game, &w, dT);

            --mTask.sample_counts[GYR];
            if (++j == MAX_NUM_SAMPLES)
                j = 0;
            break;
        case MAG:
            initVec3(&m, mTask.samples[MAG][k].x, mTask.samples[MAG][k].y, mTask.samples[MAG][k].z);

            fusionHandleMag(&mTask.fusion, &m, dT);

            --mTask.sample_counts[MAG];
            if (++k == MAX_NUM_SAMPLES)
                k = 0;
            break;
        }
    }

    mTask.sample_indices[ACC] = i;

    if (mTask.gyro_client_cnt > 0)
        mTask.sample_indices[GYR] = j;

    if (mTask.mag_client_cnt > 0)
        mTask.sample_indices[MAG] = k;

    for (i = ORIENT; i < NUM_OF_FUSION_SENSOR; i++) {
        if (mTask.sensors[i].ev != NULL) {
            osEnqueueEvtOrFree(EVENT_TYPE_BIT_DISCARDABLE | sensorGetMyEventType(mSi[i].sensorType),
                               mTask.sensors[i].ev, dataEvtFree);
            mTask.sensors[i].ev = NULL;
        }
    }
}

static void configureFusion()
{
    if (mTask.sensors[ORIENT].active
            || mTask.sensors[ROTAT].active
            || mTask.sensors[GEOMAG].active) {
        mTask.flags |= FUSION_FLAG_ENABLED;
        initFusion(&mTask.fusion,
                (mTask.mag_client_cnt > 0 ? FUSION_USE_MAG : 0) |
                (mTask.gyro_client_cnt > 0 ? FUSION_USE_GYRO : 0) |
                ((mTask.flags & FUSION_FLAG_INITIALIZED) ? 0 : FUSION_REINITIALIZE));
        mTask.flags |= FUSION_FLAG_INITIALIZED;
    } else {
        mTask.flags &= ~FUSION_FLAG_ENABLED;
        mTask.flags &= ~FUSION_FLAG_INITIALIZED;
    }
}

static void configureGame()
{
    if (mTask.sensors[GAME].active || mTask.sensors[GRAVITY].active ||
            mTask.sensors[LINEAR].active) {
        mTask.flags |= FUSION_FLAG_GAME_ENABLED;
        initFusion(&mTask.game, FUSION_USE_GYRO |
                ((mTask.flags & FUSION_FLAG_INITIALIZED) ? 0 : FUSION_REINITIALIZE));
        mTask.flags |= FUSION_FLAG_GAME_INITIALIZED;
    } else {
        mTask.flags &= ~FUSION_FLAG_GAME_ENABLED;
        mTask.flags &= ~FUSION_FLAG_GAME_INITIALIZED;
    }
}

static void fusionSetRateAcc(void)
{
    int i;
    if  (mTask.accelHandle == 0) {
        mTask.sample_counts[ACC] = 0;
        mTask.sample_indices[ACC] = 0;
        mTask.counters[ACC] = 0;
        mTask.last_time[ACC] = ULONG_LONG_MAX;
        for (i = 0; sensorFind(SENS_TYPE_ACCEL, i, &mTask.accelHandle) != NULL; i++) {
            if (sensorRequest(mTask.tid, mTask.accelHandle, mTask.raw_sensor_rate[ACC],
                        mTask.raw_sensor_latency)) {
                osEventSubscribe(mTask.tid, EVT_SENSOR_ACC_DATA_RDY);
                break;
            }
        }
    } else {
        sensorRequestRateChange(mTask.tid, mTask.accelHandle, mTask.raw_sensor_rate[ACC],
                mTask.raw_sensor_latency);
    }
}

static void fusionSetRateGyr(void)
{
    int i;
    if (mTask.gyroHandle == 0) {
        mTask.sample_counts[GYR] = 0;
        mTask.sample_indices[GYR] = 0;
        mTask.counters[GYR] = 0;
        mTask.last_time[GYR] = ULONG_LONG_MAX;
        for (i = 0; sensorFind(SENS_TYPE_GYRO, i, &mTask.gyroHandle) != NULL; i++) {
            if (sensorRequest(mTask.tid, mTask.gyroHandle, mTask.raw_sensor_rate[GYR],
                        mTask.raw_sensor_latency)) {
                osEventSubscribe(mTask.tid, EVT_SENSOR_GYR_DATA_RDY);
                break;
            }
        }
    } else {
        sensorRequestRateChange(mTask.tid, mTask.gyroHandle, mTask.raw_sensor_rate[GYR],
                mTask.raw_sensor_latency);
    }
}

static void fusionSetRateMag(void)
{
    int i;
    if (mTask.magHandle == 0) {
        mTask.sample_counts[MAG] = 0;
        mTask.sample_indices[MAG] = 0;
        mTask.counters[MAG] = 0;
        mTask.last_time[MAG] = ULONG_LONG_MAX;
        for (i = 0; sensorFind(SENS_TYPE_MAG, i, &mTask.magHandle) != NULL; i++) {
            if (sensorRequest(mTask.tid, mTask.magHandle, mTask.raw_sensor_rate[MAG],
                        mTask.raw_sensor_latency)) {
                osEventSubscribe(mTask.tid, EVT_SENSOR_MAG_DATA_RDY);
                osEventSubscribe(mTask.tid, EVT_SENSOR_MAG_BIAS);
                break;
            }
        }
    } else {
        sensorRequestRateChange(mTask.tid, mTask.magHandle, mTask.raw_sensor_rate[MAG],
                mTask.raw_sensor_latency);
    }
}

static bool fusionSetRate(uint32_t rate, uint64_t latency, void *cookie)
{
    struct FusionSensor *mSensor = &mTask.sensors[(int)cookie];
    int i;
    uint32_t max_rate = 0;
    uint32_t gyr_rate, mag_rate;
    uint64_t min_resample_period = ULONG_LONG_MAX;

    mSensor->rate = rate;
    mSensor->latency = latency;

    for (i = ORIENT; i < NUM_OF_FUSION_SENSOR; i++) {
        if (mTask.sensors[i].active) {
            max_rate = max_rate > mTask.sensors[i].rate ? max_rate : mTask.sensors[i].rate;
        }
    }

    if (mTask.accel_client_cnt > 0) {
        mTask.raw_sensor_rate[ACC] = max_rate;
        mTask.ResamplePeriodNs[ACC] = sensorTimerLookupCommon(FusionRates, rateTimerVals, max_rate);
        min_resample_period = mTask.ResamplePeriodNs[ACC] < min_resample_period ?
            mTask.ResamplePeriodNs[ACC] : min_resample_period;
    }

    if (mTask.gyro_client_cnt > 0) {
        gyr_rate = max_rate > MIN_GYRO_RATE_HZ ? max_rate : MIN_GYRO_RATE_HZ;
        mTask.raw_sensor_rate[GYR] = gyr_rate;
        mTask.ResamplePeriodNs[GYR] = sensorTimerLookupCommon(FusionRates, rateTimerVals, gyr_rate);
        min_resample_period = mTask.ResamplePeriodNs[GYR] < min_resample_period ?
            mTask.ResamplePeriodNs[GYR] : min_resample_period;
    }

    if (mTask.mag_client_cnt > 0) {
        mag_rate = max_rate < MAX_MAG_RATE_HZ ? max_rate : MAX_MAG_RATE_HZ;
        mTask.raw_sensor_rate[MAG] = mag_rate;
        mTask.ResamplePeriodNs[MAG] = sensorTimerLookupCommon(FusionRates, rateTimerVals, mag_rate);
        min_resample_period = mTask.ResamplePeriodNs[MAG] < min_resample_period ?
            mTask.ResamplePeriodNs[MAG] : min_resample_period;
    }

    // This guarantees that local raw sensor FIFOs won't overflow.
    mTask.raw_sensor_latency = min_resample_period * (FIFO_DEPTH - 1);

    for (i = ORIENT; i < NUM_OF_FUSION_SENSOR; i++) {
        if (mTask.sensors[i].active) {
            mTask.raw_sensor_latency = mTask.sensors[i].latency < mTask.raw_sensor_latency ?
                mTask.sensors[i].latency : mTask.raw_sensor_latency;
        }
    }

    if (mTask.accel_client_cnt > 0)
        fusionSetRateAcc();
    if (mTask.gyro_client_cnt > 0)
        fusionSetRateGyr();
    if (mTask.mag_client_cnt > 0)
        fusionSetRateMag();
    if (mSensor->rate > 0)
        sensorSignalInternalEvt(mSensor->handle, SENSOR_INTERNAL_EVT_RATE_CHG, rate, latency);

    return true;
}

static bool fusionPower(bool on, void *cookie)
{
    struct FusionSensor *mSensor = &mTask.sensors[(int)cookie];
    int idx;

    mSensor->active = on;
    if (on == false) {
        mTask.accel_client_cnt--;
        if (mSensor->use_gyro_data)
            mTask.gyro_client_cnt--;
        if (mSensor->use_mag_data)
            mTask.mag_client_cnt--;

        // if client_cnt == 0 and Handle == 0, nothing need to be done.
        // if client_cnt > 0 and Handle == 0, something else is turning it on, all will be done.
        if (mTask.accel_client_cnt == 0 && mTask.accelHandle != 0) {
            sensorRelease(mTask.tid, mTask.accelHandle);
            mTask.accelHandle = 0;
            osEventUnsubscribe(mTask.tid, EVT_SENSOR_ACC_DATA_RDY);
        }

        if (mTask.gyro_client_cnt == 0 && mTask.gyroHandle != 0) {
            sensorRelease(mTask.tid, mTask.gyroHandle);
            mTask.gyroHandle = 0;
            osEventUnsubscribe(mTask.tid, EVT_SENSOR_GYR_DATA_RDY);
        }

        if (mTask.mag_client_cnt == 0 && mTask.magHandle != 0) {
            sensorRelease(mTask.tid, mTask.magHandle);
            mTask.magHandle = 0;
            osEventUnsubscribe(mTask.tid, EVT_SENSOR_MAG_DATA_RDY);
        }

        idx = mSensor->idx;
        (void) fusionSetRate(0, ULONG_LONG_MAX, (void *)idx);
    } else {
        mTask.accel_client_cnt++;
        if (mSensor->use_gyro_data)
            mTask.gyro_client_cnt++;
        if (mSensor->use_mag_data)
            mTask.mag_client_cnt++;
    }

    configureFusion();
    configureGame();
    sensorSignalInternalEvt(mSensor->handle, SENSOR_INTERNAL_EVT_POWER_STATE_CHG, on, 0);

    return true;
}

static bool fusionFirmwareUpload(void *cookie)
{
    struct FusionSensor *mSensor = &mTask.sensors[(int)cookie];

    sensorSignalInternalEvt(mSensor->handle, SENSOR_INTERNAL_EVT_FW_STATE_CHG, 1, 0);
    return true;
}

static bool fusionFlush(void *cookie)
{
    struct FusionSensor *mSensor = &mTask.sensors[(int)cookie];
    uint32_t evtType = sensorGetMyEventType(mSi[mSensor->idx].sensorType);

    osEnqueueEvt(evtType, SENSOR_DATA_EVENT_FLUSH, NULL);
    return true;
}

static void fusionHandleEvent(uint32_t evtType, const void* evtData)
{
    struct TripleAxisDataEvent *ev;
    int i;

    if (evtData == SENSOR_DATA_EVENT_FLUSH)
        return;

    switch (evtType) {
    case EVT_APP_START:
        // check for gyro and mag
        osEventUnsubscribe(mTask.tid, EVT_APP_START);
        if (!sensorFind(SENS_TYPE_GYRO, 0, &mTask.gyroHandle)) {
            for (i = ORIENT; i < NUM_OF_FUSION_SENSOR; i++)
                mTask.sensors[i].use_gyro_data = false;
        }
        mTask.gyroHandle = 0;
        if (!sensorFind(SENS_TYPE_MAG, 0, &mTask.magHandle)) {
            for (i = ORIENT; i < NUM_OF_FUSION_SENSOR; i++)
                mTask.sensors[i].use_mag_data = false;
        }
        mTask.magHandle = 0;
        break;
    case EVT_SENSOR_ACC_DATA_RDY:
        ev = (struct TripleAxisDataEvent *)evtData;
        fillSamples(ev, ACC);
        drainSamples();
        break;
    case EVT_SENSOR_GYR_DATA_RDY:
        ev = (struct TripleAxisDataEvent *)evtData;
        fillSamples(ev, GYR);
        drainSamples();
        break;
    case EVT_SENSOR_MAG_BIAS:
        ev = (struct TripleAxisDataEvent *)evtData;
        if (ev->samples[0].firstSample.biasPresent && mTask.flags & FUSION_FLAG_ENABLED) {
            //it is a user initiated mag cal event
            fusionSetMagTrust(&mTask.fusion, MANUAL_MAG_CAL);
        }
        break;
    case EVT_SENSOR_MAG_DATA_RDY:
        ev = (struct TripleAxisDataEvent *)evtData;
        fillSamples(ev, MAG);
        drainSamples();
        break;
    }
}

static const struct SensorOps mSops =
{
    .sensorPower = fusionPower,
    .sensorFirmwareUpload = fusionFirmwareUpload,
    .sensorSetRate = fusionSetRate,
    .sensorFlush = fusionFlush,
};

static bool fusionStart(uint32_t tid)
{
    size_t i, slabSize;

    mTask.tid = tid;
    mTask.flags = 0;

    for (i = 0; i < NUM_OF_RAW_SENSOR; i++) {
         mTask.sample_counts[i] = 0;
         mTask.sample_indices[i] = 0;
    }

    for (i = ORIENT; i < NUM_OF_FUSION_SENSOR; i++) {
        mTask.sensors[i].handle = sensorRegister(&mSi[i], &mSops, (void *)i, true);
        mTask.sensors[i].idx = i;
        mTask.sensors[i].use_gyro_data = true;
        mTask.sensors[i].use_mag_data = true;
    }

    mTask.sensors[GEOMAG].use_gyro_data = false;
    mTask.sensors[GAME].use_mag_data = false;
    mTask.sensors[GRAVITY].use_mag_data = false;
    mTask.sensors[LINEAR].use_mag_data = false;

    mTask.accel_client_cnt = 0;
    mTask.gyro_client_cnt = 0;
    mTask.mag_client_cnt = 0;

    slabSize = sizeof(struct TripleAxisDataEvent)
        + MAX_NUM_COMMS_EVENT_SAMPLES * sizeof(struct TripleAxisDataPoint);

    // worst case 6 output sensors * (N + 1) comms_events
    mDataSlab = slabAllocatorNew(slabSize, 4, 6 * (NUM_COMMS_EVENTS_IN_FIFO + 1));
    if (!mDataSlab) {
        osLog(LOG_ERROR, "ORIENTATION: slabAllocatorNew() FAILED\n");
        return false;
    }

    osEventSubscribe(mTask.tid, EVT_APP_START);

    return true;
}

static void fusionEnd()
{
    mTask.flags &= ~FUSION_FLAG_INITIALIZED;
    mTask.flags &= ~FUSION_FLAG_GAME_INITIALIZED;
    slabAllocatorDestroy(mDataSlab);
}

INTERNAL_APP_INIT(
        APP_ID_MAKE(NANOHUB_VENDOR_GOOGLE, 4),
        ORIENTATION_APP_VERSION,
        fusionStart,
        fusionEnd,
        fusionHandleEvent);